Skip to content
Merged
12,558 changes: 5,797 additions & 6,761 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@
"igniteui-dockmanager": "^1.17.0",
"igniteui-i18n-resources": "^1.0.2",
"igniteui-sassdoc-theme": "^2.1.0",
"igniteui-webcomponents": "^6.4.0",
"igniteui-webcomponents": "^6.5.0",
"jasmine": "^5.6.0",
"jasmine-core": "^5.6.0",
"karma": "^6.4.4",
Expand Down
24 changes: 15 additions & 9 deletions projects/igniteui-angular/chat-extras/src/markdown-pipe.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ 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 = `
// Mock the Service: We trust the service to provide safe HTML from Shiki.
const mockSafeHtml = `
<pre class="shiki" style="color: var(--shiki-fg);"><code><span style="color: #FF0000;">unsafe</span></code></pre>
<img src="x" onerror="alert(1)">
<img src="x">
`;

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

Expand All @@ -39,15 +38,22 @@ describe('MarkdownPipe', () => {
expect(pipe).toBeTruthy();
});

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

expect(bypassSpy).toHaveBeenCalledTimes(1);

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

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

it('should trust the service to provide safe HTML', async () => {
const result = await pipe.transform('# Test');

expect(bypassSpy).toHaveBeenCalledWith(mockSafeHtml);
expect(result).toBeTruthy();
});

it('should handle undefined input text', async () => {
Expand Down
5 changes: 2 additions & 3 deletions projects/igniteui-angular/chat-extras/src/markdown-pipe.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
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';
Expand All @@ -11,8 +10,8 @@ export class MarkdownPipe implements PipeTransform {


public async transform(text?: string): Promise<SafeHtml> {
return this._sanitizer.bypassSecurityTrustHtml(DOMPurify.sanitize(
return this._sanitizer.bypassSecurityTrustHtml(
await this._service.parse(text ?? '')
));
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ describe('IgxChatMarkdownService', () => {
expect(result).toContain('code');
});

it('should apply custom link extension with target="_blank"', async () => {
it('should apply custom link extension', 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 expectedLink = '<p><a href="https://www.infragistics.com" rel="noopener noreferrer">Infragistics</a></p>';

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


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>`;
}
}
})
);
}

private _renderer: MarkdownRenderer | null = null;

constructor() {
this._initializeMarked();
this._isInitialized = this._initializeShiki();
private async _getRenderer(): Promise<MarkdownRenderer> {
if (!this._renderer) {
this._renderer = await setupMarkdownRenderer();
}
return this._renderer;
}

public async parse(text: string): Promise<string> {
await this._isInitialized;
return await this._instance.parse(text);
const renderer = await this._getRenderer();
return await renderer.parse(text);
}
}
6 changes: 1 addition & 5 deletions projects/igniteui-angular/ng-package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@
"@igniteui/material-icons-extended",
"igniteui-theming",
"igniteui-i18n-core",
"igniteui-webcomponents",
"dompurify",
"marked",
"marked-shiki",
"shiki"
"igniteui-webcomponents"
]
}
6 changes: 1 addition & 5 deletions projects/igniteui-angular/ng-package.prod.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@
"@igniteui/material-icons-extended",
"igniteui-theming",
"igniteui-i18n-core",
"igniteui-webcomponents",
"dompurify",
"marked",
"marked-shiki",
"shiki"
"igniteui-webcomponents"
]
}
18 changes: 1 addition & 17 deletions projects/igniteui-angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,7 @@
"@angular/forms": "21",
"hammerjs": "^2.0.8",
"@types/hammerjs": "^2.0.46",
"igniteui-webcomponents": "^6.3.0",
"dompurify": "^3.2.0",
"marked": ">=16.3.0",
"marked-shiki": "^1.2.0",
"shiki": "^3.12.0"
"igniteui-webcomponents": "^6.5.0"
},
"peerDependenciesMeta": {
"hammerjs": {
Expand All @@ -101,18 +97,6 @@
},
"igniteui-webcomponents": {
"optional": true
},
"dompurify": {
"optional": true
},
"marked": {
"optional": true
},
"marked-shiki": {
"optional": true
},
"shiki": {
"optional": true
}
},
"igxDevDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@ export const DEPENDENCIES_MAP: PackageEntry[] = [
{ name: 'igniteui-i18n-core', target: PackageTarget.REGULAR },
{ name: 'igniteui-theming', target: PackageTarget.NONE },
{ name: 'igniteui-webcomponents', target: PackageTarget.NONE },
{ name: 'dompurify', target: PackageTarget.NONE },
{ name: 'marked', target: PackageTarget.NONE },
{ name: 'marked-shiki', target: PackageTarget.NONE },
{ name: 'shiki', target: PackageTarget.NONE },
// peerDependencies
{ name: '@angular/forms', target: PackageTarget.NONE },
{ name: '@angular/common', target: PackageTarget.NONE },
Expand Down
Loading