Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c96c716
feat: Added chat component Angular wrapper
rkaraivanov Sep 29, 2025
feb599b
Merge branch 'master' into rkaraivanov/wc-chat-wrapper
ChronosSF Sep 30, 2025
b1a45c0
chore(*): pushing updated package.lock
ChronosSF Sep 30, 2025
e0939f4
fix: Old templates refs handling when there is a dynamic change
rkaraivanov Sep 30, 2025
23490fb
Merge branch 'rkaraivanov/wc-chat-wrapper' of https://github.com/Igni…
rkaraivanov Sep 30, 2025
91e241c
feat: Added chat template directives
rkaraivanov Oct 3, 2025
fe80cc1
chore(*): Added a dev sample for the Chat component
gedinakova Oct 13, 2025
94ee98a
fix: Markdown render and shiki initialization
rkaraivanov Oct 20, 2025
9d0675e
Merge remote-tracking branch 'origin/master' into rkaraivanov/wc-chat…
rkaraivanov Oct 20, 2025
8fcb12c
chore: Dedupe and regenerate package-lock
rkaraivanov Oct 20, 2025
2ca4602
Merge branch 'master' into rkaraivanov/wc-chat-wrapper
gedinakova Oct 21, 2025
67389ae
fix(CHat): Adjusted suggestions to dev context.
gedinakova Oct 21, 2025
2d6086f
chore(*): Updated package-lock.
gedinakova Oct 21, 2025
80fe475
Merge branch 'master' into rkaraivanov/wc-chat-wrapper
rkaraivanov Oct 22, 2025
f0da68c
Merge remote-tracking branch 'origin/master' into rkaraivanov/wc-chat…
rkaraivanov Oct 23, 2025
a0c1e68
fix(chat): Added ng-content to support underlying slots
rkaraivanov Oct 23, 2025
83a600a
Merge branch 'rkaraivanov/wc-chat-wrapper' of https://github.com/Igni…
rkaraivanov Oct 23, 2025
5bdbb25
Merge remote-tracking branch 'origin/master' into rkaraivanov/wc-chat…
rkaraivanov Oct 30, 2025
945c9a7
fix(*): Added messageReact event
gedinakova Oct 30, 2025
bba26ce
Merge branch 'rkaraivanov/wc-chat-wrapper' of https://github.com/Igni…
gedinakova Oct 30, 2025
99be9cb
Merge branch 'master' into rkaraivanov/wc-chat-wrapper
gedinakova Oct 30, 2025
6e6215b
refactor: Moved markdown pipe into a separate entry point
rkaraivanov Oct 31, 2025
f7465a2
Merge branch 'master' into rkaraivanov/wc-chat-wrapper
rkaraivanov Oct 31, 2025
7c66dbf
Merge branch 'master' into rkaraivanov/wc-chat-wrapper
rkaraivanov Nov 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
990 changes: 458 additions & 532 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,16 @@
"@igniteui/material-icons-extended": "^3.1.0",
"@lit-labs/ssr-dom-shim": "^1.3.0",
"@types/source-map": "0.5.2",
"dompurify": "^3.3.0",
"express": "^5.1.0",
"fflate": "^0.8.1",
"igniteui-theming": "^22.0.0",
"igniteui-trial-watermark": "^3.1.0",
"lodash-es": "^4.17.21",
"marked": "^16.4.0",
"marked-shiki": "^1.2.1",
"rxjs": "^7.8.2",
"shiki": "^3.13.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
Expand Down Expand Up @@ -121,7 +125,7 @@
"ig-typedoc-theme": "^6.0.0",
"igniteui-dockmanager": "^1.17.0",
"igniteui-sassdoc-theme": "^2.1.0",
"igniteui-webcomponents": "6.2.1",
"igniteui-webcomponents": "^6.3.1",
"jasmine": "^5.6.0",
"jasmine-core": "^5.6.0",
"karma": "^6.4.4",
Expand Down
1 change: 1 addition & 0 deletions projects/igniteui-angular/extras/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './src/public_api';
1 change: 1 addition & 0 deletions projects/igniteui-angular/extras/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
57 changes: 57 additions & 0 deletions projects/igniteui-angular/extras/src/markdown-pipe.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { DomSanitizer } from '@angular/platform-browser';
import { TestBed } from '@angular/core/testing';
import { IgxChatMarkdownService } from './markdown-service';
import { MarkdownPipe } from './markdown-pipe';
import Spy = jasmine.Spy;

// Mock the Service: We only care that the pipe calls the service and gets an HTML string.
// We provide a *known* unsafe HTML string to ensure sanitization is working.
const mockUnsafeHtml = `
<pre class="shiki" style="color: var(--shiki-fg);"><code><span style="color: #FF0000;">unsafe</span></code></pre>
<img src="x" onerror="alert(1)">
`;

class MockChatMarkdownService {
public async parse(_: string): Promise<string> {
return mockUnsafeHtml;
}
}

describe('MarkdownPipe', () => {
let pipe: MarkdownPipe;
let sanitizer: DomSanitizer;
let bypassSpy: Spy;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [
MarkdownPipe,
{ provide: IgxChatMarkdownService, useClass: MockChatMarkdownService },
],
});

pipe = TestBed.inject(MarkdownPipe);
sanitizer = TestBed.inject(DomSanitizer);
bypassSpy = spyOn(sanitizer, 'bypassSecurityTrustHtml').and.callThrough();
});

it('should be created', () => {
expect(pipe).toBeTruthy();
});

it('should call the service, sanitize content, and return SafeHtml', async () => {
await pipe.transform('some markdown');

expect(bypassSpy).toHaveBeenCalledTimes(1);

const sanitizedString = bypassSpy.calls.mostRecent().args[0];

expect(sanitizedString).not.toContain('onerror');
expect(sanitizedString).toContain('style="color: var(--shiki-fg);"');
});

it('should handle undefined input text', async () => {
await pipe.transform(undefined);
expect(sanitizer.bypassSecurityTrustHtml).toHaveBeenCalled();
});
});
18 changes: 18 additions & 0 deletions projects/igniteui-angular/extras/src/markdown-pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import DOMPurify from 'dompurify';
import { inject, Pipe, type PipeTransform } from '@angular/core';
import { IgxChatMarkdownService } from './markdown-service';
import { DomSanitizer, type SafeHtml } from '@angular/platform-browser';


@Pipe({ name: 'fromMarkdown' })
export class MarkdownPipe implements PipeTransform {
private _service = inject(IgxChatMarkdownService);
private _sanitizer = inject(DomSanitizer);


public async transform(text?: string): Promise<SafeHtml> {
return this._sanitizer.bypassSecurityTrustHtml(DOMPurify.sanitize(
await this._service.parse(text ?? '')
));
}
}
41 changes: 41 additions & 0 deletions projects/igniteui-angular/extras/src/markdown-service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { TestBed } from '@angular/core/testing';
import { IgxChatMarkdownService } from './markdown-service';

describe('IgxChatMarkdownService', () => {
let service: IgxChatMarkdownService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(IgxChatMarkdownService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});

it('should parse basic markdown to HTML', async () => {
const markdown = '**Hello** *World*';
const expectedHtml = '<p><strong>Hello</strong> <em>World</em></p>\n';

const result = await service.parse(markdown);
expect(result).toBe(expectedHtml);
});

it('should parse a code block with shiki highlighting', async () => {
const markdown = '```typescript\nconst x = 5;\n```';
const result = await service.parse(markdown);

expect(result).toContain('<pre class="shiki shiki-themes github-light github-dark"');
expect(result).toContain('const');
expect(result).toMatch(/--shiki-.*?/);
expect(result).toContain('code');
});

it('should apply custom link extension with target="_blank"', async () => {
const markdown = '[Infragistics](https://www.infragistics.com)';
const expectedLink = '<p><a href="https://www.infragistics.com" target="_blank" rel="noopener noreferrer" >Infragistics</a></p>';

const result = await service.parse(markdown);
expect(result).toContain(expectedLink);
});
});
67 changes: 67 additions & 0 deletions projects/igniteui-angular/extras/src/markdown-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Injectable } from '@angular/core';
import { Marked } from 'marked';
import markedShiki from 'marked-shiki';
import { bundledThemes, createHighlighter } from 'shiki/bundle/web';


const DEFAULT_LANGUAGES = ['javascript', 'typescript', 'html', 'css'];
const DEFAULT_THEMES = {
light: 'github-light',
dark: 'github-dark'
};

@Injectable({ providedIn: 'root' })
export class IgxChatMarkdownService {

private _instance: Marked;
private _isInitialized: Promise<void>;

private _initializeMarked(): void {
this._instance = new Marked({
breaks: true,
gfm: true,
extensions: [
{
name: 'link',
renderer({ href, title, text }) {
return `<a href="${href}" target="_blank" rel="noopener noreferrer" ${title ? `title="${title}"` : ''}>${text}</a>`;
}
}
]
});
}

private async _initializeShiki(): Promise<void> {
const highlighter = await createHighlighter({
langs: DEFAULT_LANGUAGES,
themes: Object.keys(bundledThemes)
});

this._instance.use(
markedShiki({
highlight(code, lang, _) {
try {
return highlighter.codeToHtml(code, {
lang,
themes: DEFAULT_THEMES,
});

} catch {
return `<pre><code>${code}</code></pre>`;
}
}
})
);
}


constructor() {
this._initializeMarked();
this._isInitialized = this._initializeShiki();
}

public async parse(text: string): Promise<string> {
await this._isInitialized;
return await this._instance.parse(text);
}
}
1 change: 1 addition & 0 deletions projects/igniteui-angular/extras/src/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { MarkdownPipe } from './markdown-pipe';
16 changes: 16 additions & 0 deletions projects/igniteui-angular/src/lib/chat/chat.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<igc-chat
[messages]="messages()"
[draftMessage]="draftMessage()"
[options]="_mergedOptions()"
(igcMessageCreated)="messageCreated.emit($event.detail)"
(igcMessageReact)="messageReact.emit($event.detail)"
(igcAttachmentClick)="attachmentClick.emit($event.detail)"
(igcAttachmentDrag)="attachmentDrag.emit()"
(igcAttachmentDrop)="attachmentDrop.emit()"
(igcTypingChange)="typingChange.emit($event.detail)"
(igcInputFocus)="inputFocus.emit()"
(igcInputBlur)="inputBlur.emit()"
(igcInputChange)="inputChange.emit($event.detail)"
>
<ng-content></ng-content>
</igc-chat>
Loading
Loading