@memberjunction/ng-markdown 6.1.0-edge.5 → 6.1.0-edge.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -56,6 +56,63 @@ describe('sanitizeSvgContent', () => {
56
56
  const el = container('');
57
57
  expect(() => sanitizeSvgContent(el)).not.toThrow();
58
58
  });
59
+ it('removes handler attributes that are not on a fixed list', () => {
60
+ const el = container('<svg><rect onbegin="a()" onpointerrawupdate="b()" ONANIMATIONEND="c()" OnMouseEnter="d()"/></svg>');
61
+ sanitizeSvgContent(el);
62
+ expect(el.querySelector('rect')?.attributes.length).toBe(0);
63
+ });
64
+ it.each([
65
+ ['vbscript:', 'vbscript:msgbox(1)'],
66
+ ['data:text/html', 'data:text/html,<script>alert(1)</script>'],
67
+ ['data:image/svg+xml', 'data:image/svg+xml,<svg onload=alert(1)>'],
68
+ ['tab inside scheme', 'java\tscript:alert(1)'],
69
+ ['newline inside scheme', 'java\nscript:alert(1)'],
70
+ ['leading control character', '\u0001javascript:alert(1)'],
71
+ ['mixed case', 'JaVaScRiPt:alert(1)'],
72
+ ['leading whitespace', ' javascript:alert(1)'],
73
+ ])('removes href using %s', (_label, href) => {
74
+ const el = container('<svg><a>x</a></svg>');
75
+ el.querySelector('a').setAttribute('href', href);
76
+ sanitizeSvgContent(el);
77
+ expect(el.querySelector('a')?.hasAttribute('href')).toBe(false);
78
+ });
79
+ it('removes script URLs from xlink:href, src, action and formaction', () => {
80
+ const el = container('<svg><a>x</a><image/><foo/></svg>');
81
+ el.querySelector('a').setAttribute('xlink:href', 'javascript:alert(1)');
82
+ el.querySelector('image').setAttribute('src', 'javascript:alert(1)');
83
+ el.querySelector('foo').setAttribute('action', 'javascript:alert(1)');
84
+ el.querySelector('foo').setAttribute('formaction', 'vbscript:alert(1)');
85
+ sanitizeSvgContent(el);
86
+ expect(el.querySelector('a')?.hasAttribute('xlink:href')).toBe(false);
87
+ expect(el.querySelector('image')?.hasAttribute('src')).toBe(false);
88
+ expect(el.querySelector('foo')?.attributes.length).toBe(0);
89
+ });
90
+ it('keeps a raster data: image on <image> but not an SVG one', () => {
91
+ const el = container('<svg><image id="png"/><image id="svg"/></svg>');
92
+ el.querySelector('#png').setAttribute('href', 'data:image/png;base64,iVBORw0KGgo=');
93
+ el.querySelector('#svg').setAttribute('href', 'data:image/svg+xml;base64,PHN2Zy8+');
94
+ sanitizeSvgContent(el);
95
+ expect(el.querySelector('#png')?.hasAttribute('href')).toBe(true);
96
+ expect(el.querySelector('#svg')?.hasAttribute('href')).toBe(false);
97
+ });
98
+ it('keeps safe hrefs and non-URL attributes untouched', () => {
99
+ const el = container('<svg><a href="https://example.test/" class="k" data-x="1">x</a></svg>');
100
+ sanitizeSvgContent(el);
101
+ const a = el.querySelector('a');
102
+ expect(a.getAttribute('href')).toBe('https://example.test/');
103
+ expect(a.getAttribute('class')).toBe('k');
104
+ expect(a.getAttribute('data-x')).toBe('1');
105
+ });
106
+ it.each([
107
+ ['protocol-relative', '//evil.test/sprite.svg#x'],
108
+ ['obfuscated scheme', 'ht\ttp://evil.test/sprite.svg#x'],
109
+ ['relative path', 'sprites.svg#x'],
110
+ ])('removes a <use> whose reference is not same-document (%s)', (_label, href) => {
111
+ const el = container('<svg><use/></svg>');
112
+ el.querySelector('use').setAttribute('href', href);
113
+ sanitizeSvgContent(el);
114
+ expect(el.querySelector('use')).toBeNull();
115
+ });
59
116
  });
60
117
  describe('addCopyButtonsToCodeBlocks / removeCopyButtonsFromCodeBlocks', () => {
61
118
  it('adds a copy button with a formatted language label', () => {
@@ -1 +1 @@
1
- {"version":3,"file":"web-dom-utils.test.js","sourceRoot":"","sources":["../../src/__tests__/web-dom-utils.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect, beforeEach, afterEach } from 'vitest';\nimport { sanitizeSvgContent } from '../lib/extensions/svg-renderer.extension';\nimport {\n addCopyButtonsToCodeBlocks,\n removeCopyButtonsFromCodeBlocks,\n} from '../lib/extensions/code-copy.extension';\nimport {\n toggleCollapsibleSection,\n expandAllSections,\n collapseAllSections,\n expandToHeading,\n} from '../lib/extensions/collapsible-headings.extension';\n\nfunction container(html: string): HTMLElement {\n const el = document.createElement('div');\n el.innerHTML = html;\n document.body.appendChild(el);\n return el;\n}\n\n// Prevent fixtures (and their duplicate ids) from leaking across tests.\nafterEach(() => {\n document.body.innerHTML = '';\n});\n\ndescribe('sanitizeSvgContent', () => {\n it('removes <script> elements', () => {\n const el = container('<svg><script>alert(1)</script><circle/></svg>');\n sanitizeSvgContent(el);\n expect(el.querySelector('script')).toBeNull();\n expect(el.querySelector('circle')).not.toBeNull();\n });\n\n it('strips inline event-handler attributes', () => {\n const el = container('<svg><rect onclick=\"hack()\" onload=\"x()\"/></svg>');\n sanitizeSvgContent(el);\n const rect = el.querySelector('rect');\n expect(rect?.hasAttribute('onclick')).toBe(false);\n expect(rect?.hasAttribute('onload')).toBe(false);\n });\n\n it('removes javascript: hrefs', () => {\n const el = container('<svg><a href=\"javascript:evil()\">x</a></svg>');\n sanitizeSvgContent(el);\n expect(el.querySelector('a')?.hasAttribute('href')).toBe(false);\n });\n\n it('removes foreignObject and external <use>', () => {\n const el = container('<svg><foreignObject></foreignObject><use href=\"https://evil.test/x\"/></svg>');\n sanitizeSvgContent(el);\n expect(el.querySelector('foreignObject')).toBeNull();\n expect(el.querySelector('use')).toBeNull();\n });\n\n it('strips javascript: from xlink:href', () => {\n const el = container('<svg><a xlink:href=\"javascript:evil()\">x</a></svg>');\n sanitizeSvgContent(el);\n expect(el.querySelector('a')?.hasAttribute('xlink:href')).toBe(false);\n });\n\n it('keeps a local <use> reference (only external ones are removed)', () => {\n const el = container('<svg><use href=\"#icon\"/></svg>');\n sanitizeSvgContent(el);\n expect(el.querySelector('use')).not.toBeNull();\n });\n\n it('is a no-op on a container with no dangerous content', () => {\n const el = container('<svg><circle cx=\"1\" cy=\"1\" r=\"1\"/></svg>');\n sanitizeSvgContent(el);\n expect(el.querySelector('circle')).not.toBeNull();\n });\n\n it('does not throw on an empty container', () => {\n const el = container('');\n expect(() => sanitizeSvgContent(el)).not.toThrow();\n });\n});\n\ndescribe('addCopyButtonsToCodeBlocks / removeCopyButtonsFromCodeBlocks', () => {\n it('adds a copy button with a formatted language label', () => {\n const el = container('<pre><code class=\"language-ts\">const x = 1;</code></pre>');\n addCopyButtonsToCodeBlocks(el);\n expect(el.querySelector('.code-copy-btn')).not.toBeNull();\n expect(el.querySelector('.code-language-label')?.textContent).toBe('TypeScript');\n });\n\n it('does not double-add a toolbar', () => {\n const el = container('<pre><code class=\"language-js\">a</code></pre>');\n addCopyButtonsToCodeBlocks(el);\n addCopyButtonsToCodeBlocks(el);\n expect(el.querySelectorAll('.code-toolbar')).toHaveLength(1);\n });\n\n it('removes toolbars on cleanup', () => {\n const el = container('<pre><code class=\"language-js\">a</code></pre>');\n addCopyButtonsToCodeBlocks(el);\n removeCopyButtonsFromCodeBlocks(el);\n expect(el.querySelector('.code-toolbar')).toBeNull();\n });\n\n it('adds a copy button but no language label for a code block without a language class', () => {\n const el = container('<pre><code>plain code</code></pre>');\n addCopyButtonsToCodeBlocks(el);\n expect(el.querySelector('.code-copy-btn')).not.toBeNull();\n expect(el.querySelector('.code-language-label')).toBeNull();\n });\n\n it('omits the language label when showLanguageLabel is false', () => {\n const el = container('<pre><code class=\"language-ts\">a</code></pre>');\n addCopyButtonsToCodeBlocks(el, { showLanguageLabel: false });\n expect(el.querySelector('.code-copy-btn')).not.toBeNull();\n expect(el.querySelector('.code-language-label')).toBeNull();\n });\n\n it('honors a custom button/toolbar class and can remove it again', () => {\n const el = container('<pre><code class=\"language-js\">a</code></pre>');\n addCopyButtonsToCodeBlocks(el, { toolbarClass: 'my-bar', buttonClass: 'my-btn' });\n expect(el.querySelector('.my-btn')).not.toBeNull();\n removeCopyButtonsFromCodeBlocks(el, 'my-bar');\n expect(el.querySelector('.my-bar')).toBeNull();\n });\n\n it('is a no-op on a container with no code blocks', () => {\n const el = container('<p>no code here</p>');\n expect(() => addCopyButtonsToCodeBlocks(el)).not.toThrow();\n expect(el.querySelector('.code-toolbar')).toBeNull();\n });\n\n it('removeCopyButtonsFromCodeBlocks is safe when there are no toolbars', () => {\n const el = container('<pre><code>a</code></pre>');\n expect(() => removeCopyButtonsFromCodeBlocks(el)).not.toThrow();\n });\n});\n\ndescribe('collapsible DOM helpers', () => {\n let el: HTMLElement;\n\n beforeEach(() => {\n el = container(\n '<div class=\"collapsible-section\" data-level=\"2\">' +\n '<div class=\"collapsible-heading-wrapper\"><button class=\"collapsible-toggle\" aria-expanded=\"true\"></button><h2 id=\"parent\">Parent</h2></div>' +\n '<div class=\"collapsible-content\">' +\n '<div class=\"collapsible-section collapsed\" data-level=\"3\">' +\n '<div class=\"collapsible-heading-wrapper\"><button class=\"collapsible-toggle\" aria-expanded=\"false\"></button><h3 id=\"child\">Child</h3></div>' +\n '<div class=\"collapsible-content\"><p>c</p></div>' +\n '</div>' +\n '</div>' +\n '</div>'\n );\n });\n\n it('toggleCollapsibleSection flips the collapsed class and aria-expanded', () => {\n const parent = el.querySelector('.collapsible-section') as HTMLElement;\n toggleCollapsibleSection(parent);\n expect(parent.classList.contains('collapsed')).toBe(true);\n expect(parent.querySelector('.collapsible-toggle')?.getAttribute('aria-expanded')).toBe('false');\n });\n\n it('expandAllSections clears every collapsed section', () => {\n expandAllSections(el);\n expect(el.querySelectorAll('.collapsible-section.collapsed')).toHaveLength(0);\n });\n\n it('collapseAllSections collapses every section', () => {\n collapseAllSections(el);\n expect(el.querySelectorAll('.collapsible-section:not(.collapsed)')).toHaveLength(0);\n });\n\n it('expandToHeading reveals ancestor sections of a heading', () => {\n // child section starts collapsed; expanding to #child should clear it\n expandToHeading(el, 'child');\n const child = el.querySelector('[data-level=\"3\"]') as HTMLElement;\n expect(child.classList.contains('collapsed')).toBe(false);\n });\n\n it('toggleCollapsibleSection expands a section that starts collapsed', () => {\n const child = el.querySelector('[data-level=\"3\"]') as HTMLElement;\n expect(child.classList.contains('collapsed')).toBe(true);\n toggleCollapsibleSection(child);\n expect(child.classList.contains('collapsed')).toBe(false);\n expect(child.querySelector('.collapsible-toggle')?.getAttribute('aria-expanded')).toBe('true');\n });\n\n it('expandToHeading is a no-op for an unknown heading id (no throw)', () => {\n const child = el.querySelector('[data-level=\"3\"]') as HTMLElement;\n expect(() => expandToHeading(el, 'does-not-exist')).not.toThrow();\n // The child that was collapsed stays collapsed — nothing was touched.\n expect(child.classList.contains('collapsed')).toBe(true);\n });\n\n it('expand/collapse helpers are safe on a container with no sections', () => {\n const empty = container('<p>nothing collapsible</p>');\n expect(() => expandAllSections(empty)).not.toThrow();\n expect(() => collapseAllSections(empty)).not.toThrow();\n });\n});\n"]}
1
+ {"version":3,"file":"web-dom-utils.test.js","sourceRoot":"","sources":["../../src/__tests__/web-dom-utils.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect, beforeEach, afterEach } from 'vitest';\nimport { sanitizeSvgContent } from '../lib/extensions/svg-renderer.extension';\nimport {\n addCopyButtonsToCodeBlocks,\n removeCopyButtonsFromCodeBlocks,\n} from '../lib/extensions/code-copy.extension';\nimport {\n toggleCollapsibleSection,\n expandAllSections,\n collapseAllSections,\n expandToHeading,\n} from '../lib/extensions/collapsible-headings.extension';\n\nfunction container(html: string): HTMLElement {\n const el = document.createElement('div');\n el.innerHTML = html;\n document.body.appendChild(el);\n return el;\n}\n\n// Prevent fixtures (and their duplicate ids) from leaking across tests.\nafterEach(() => {\n document.body.innerHTML = '';\n});\n\ndescribe('sanitizeSvgContent', () => {\n it('removes <script> elements', () => {\n const el = container('<svg><script>alert(1)</script><circle/></svg>');\n sanitizeSvgContent(el);\n expect(el.querySelector('script')).toBeNull();\n expect(el.querySelector('circle')).not.toBeNull();\n });\n\n it('strips inline event-handler attributes', () => {\n const el = container('<svg><rect onclick=\"hack()\" onload=\"x()\"/></svg>');\n sanitizeSvgContent(el);\n const rect = el.querySelector('rect');\n expect(rect?.hasAttribute('onclick')).toBe(false);\n expect(rect?.hasAttribute('onload')).toBe(false);\n });\n\n it('removes javascript: hrefs', () => {\n const el = container('<svg><a href=\"javascript:evil()\">x</a></svg>');\n sanitizeSvgContent(el);\n expect(el.querySelector('a')?.hasAttribute('href')).toBe(false);\n });\n\n it('removes foreignObject and external <use>', () => {\n const el = container('<svg><foreignObject></foreignObject><use href=\"https://evil.test/x\"/></svg>');\n sanitizeSvgContent(el);\n expect(el.querySelector('foreignObject')).toBeNull();\n expect(el.querySelector('use')).toBeNull();\n });\n\n it('strips javascript: from xlink:href', () => {\n const el = container('<svg><a xlink:href=\"javascript:evil()\">x</a></svg>');\n sanitizeSvgContent(el);\n expect(el.querySelector('a')?.hasAttribute('xlink:href')).toBe(false);\n });\n\n it('keeps a local <use> reference (only external ones are removed)', () => {\n const el = container('<svg><use href=\"#icon\"/></svg>');\n sanitizeSvgContent(el);\n expect(el.querySelector('use')).not.toBeNull();\n });\n\n it('is a no-op on a container with no dangerous content', () => {\n const el = container('<svg><circle cx=\"1\" cy=\"1\" r=\"1\"/></svg>');\n sanitizeSvgContent(el);\n expect(el.querySelector('circle')).not.toBeNull();\n });\n\n it('does not throw on an empty container', () => {\n const el = container('');\n expect(() => sanitizeSvgContent(el)).not.toThrow();\n });\n\n it('removes handler attributes that are not on a fixed list', () => {\n const el = container('<svg><rect onbegin=\"a()\" onpointerrawupdate=\"b()\" ONANIMATIONEND=\"c()\" OnMouseEnter=\"d()\"/></svg>');\n sanitizeSvgContent(el);\n expect(el.querySelector('rect')?.attributes.length).toBe(0);\n });\n\n it.each([\n ['vbscript:', 'vbscript:msgbox(1)'],\n ['data:text/html', 'data:text/html,<script>alert(1)</script>'],\n ['data:image/svg+xml', 'data:image/svg+xml,<svg onload=alert(1)>'],\n ['tab inside scheme', 'java\\tscript:alert(1)'],\n ['newline inside scheme', 'java\\nscript:alert(1)'],\n ['leading control character', '\\u0001javascript:alert(1)'],\n ['mixed case', 'JaVaScRiPt:alert(1)'],\n ['leading whitespace', ' javascript:alert(1)'],\n ])('removes href using %s', (_label, href) => {\n const el = container('<svg><a>x</a></svg>');\n el.querySelector('a')!.setAttribute('href', href);\n sanitizeSvgContent(el);\n expect(el.querySelector('a')?.hasAttribute('href')).toBe(false);\n });\n\n it('removes script URLs from xlink:href, src, action and formaction', () => {\n const el = container('<svg><a>x</a><image/><foo/></svg>');\n el.querySelector('a')!.setAttribute('xlink:href', 'javascript:alert(1)');\n el.querySelector('image')!.setAttribute('src', 'javascript:alert(1)');\n el.querySelector('foo')!.setAttribute('action', 'javascript:alert(1)');\n el.querySelector('foo')!.setAttribute('formaction', 'vbscript:alert(1)');\n sanitizeSvgContent(el);\n expect(el.querySelector('a')?.hasAttribute('xlink:href')).toBe(false);\n expect(el.querySelector('image')?.hasAttribute('src')).toBe(false);\n expect(el.querySelector('foo')?.attributes.length).toBe(0);\n });\n\n it('keeps a raster data: image on <image> but not an SVG one', () => {\n const el = container('<svg><image id=\"png\"/><image id=\"svg\"/></svg>');\n el.querySelector('#png')!.setAttribute('href', 'data:image/png;base64,iVBORw0KGgo=');\n el.querySelector('#svg')!.setAttribute('href', 'data:image/svg+xml;base64,PHN2Zy8+');\n sanitizeSvgContent(el);\n expect(el.querySelector('#png')?.hasAttribute('href')).toBe(true);\n expect(el.querySelector('#svg')?.hasAttribute('href')).toBe(false);\n });\n\n it('keeps safe hrefs and non-URL attributes untouched', () => {\n const el = container('<svg><a href=\"https://example.test/\" class=\"k\" data-x=\"1\">x</a></svg>');\n sanitizeSvgContent(el);\n const a = el.querySelector('a')!;\n expect(a.getAttribute('href')).toBe('https://example.test/');\n expect(a.getAttribute('class')).toBe('k');\n expect(a.getAttribute('data-x')).toBe('1');\n });\n\n it.each([\n ['protocol-relative', '//evil.test/sprite.svg#x'],\n ['obfuscated scheme', 'ht\\ttp://evil.test/sprite.svg#x'],\n ['relative path', 'sprites.svg#x'],\n ])('removes a <use> whose reference is not same-document (%s)', (_label, href) => {\n const el = container('<svg><use/></svg>');\n el.querySelector('use')!.setAttribute('href', href);\n sanitizeSvgContent(el);\n expect(el.querySelector('use')).toBeNull();\n });\n});\n\ndescribe('addCopyButtonsToCodeBlocks / removeCopyButtonsFromCodeBlocks', () => {\n it('adds a copy button with a formatted language label', () => {\n const el = container('<pre><code class=\"language-ts\">const x = 1;</code></pre>');\n addCopyButtonsToCodeBlocks(el);\n expect(el.querySelector('.code-copy-btn')).not.toBeNull();\n expect(el.querySelector('.code-language-label')?.textContent).toBe('TypeScript');\n });\n\n it('does not double-add a toolbar', () => {\n const el = container('<pre><code class=\"language-js\">a</code></pre>');\n addCopyButtonsToCodeBlocks(el);\n addCopyButtonsToCodeBlocks(el);\n expect(el.querySelectorAll('.code-toolbar')).toHaveLength(1);\n });\n\n it('removes toolbars on cleanup', () => {\n const el = container('<pre><code class=\"language-js\">a</code></pre>');\n addCopyButtonsToCodeBlocks(el);\n removeCopyButtonsFromCodeBlocks(el);\n expect(el.querySelector('.code-toolbar')).toBeNull();\n });\n\n it('adds a copy button but no language label for a code block without a language class', () => {\n const el = container('<pre><code>plain code</code></pre>');\n addCopyButtonsToCodeBlocks(el);\n expect(el.querySelector('.code-copy-btn')).not.toBeNull();\n expect(el.querySelector('.code-language-label')).toBeNull();\n });\n\n it('omits the language label when showLanguageLabel is false', () => {\n const el = container('<pre><code class=\"language-ts\">a</code></pre>');\n addCopyButtonsToCodeBlocks(el, { showLanguageLabel: false });\n expect(el.querySelector('.code-copy-btn')).not.toBeNull();\n expect(el.querySelector('.code-language-label')).toBeNull();\n });\n\n it('honors a custom button/toolbar class and can remove it again', () => {\n const el = container('<pre><code class=\"language-js\">a</code></pre>');\n addCopyButtonsToCodeBlocks(el, { toolbarClass: 'my-bar', buttonClass: 'my-btn' });\n expect(el.querySelector('.my-btn')).not.toBeNull();\n removeCopyButtonsFromCodeBlocks(el, 'my-bar');\n expect(el.querySelector('.my-bar')).toBeNull();\n });\n\n it('is a no-op on a container with no code blocks', () => {\n const el = container('<p>no code here</p>');\n expect(() => addCopyButtonsToCodeBlocks(el)).not.toThrow();\n expect(el.querySelector('.code-toolbar')).toBeNull();\n });\n\n it('removeCopyButtonsFromCodeBlocks is safe when there are no toolbars', () => {\n const el = container('<pre><code>a</code></pre>');\n expect(() => removeCopyButtonsFromCodeBlocks(el)).not.toThrow();\n });\n});\n\ndescribe('collapsible DOM helpers', () => {\n let el: HTMLElement;\n\n beforeEach(() => {\n el = container(\n '<div class=\"collapsible-section\" data-level=\"2\">' +\n '<div class=\"collapsible-heading-wrapper\"><button class=\"collapsible-toggle\" aria-expanded=\"true\"></button><h2 id=\"parent\">Parent</h2></div>' +\n '<div class=\"collapsible-content\">' +\n '<div class=\"collapsible-section collapsed\" data-level=\"3\">' +\n '<div class=\"collapsible-heading-wrapper\"><button class=\"collapsible-toggle\" aria-expanded=\"false\"></button><h3 id=\"child\">Child</h3></div>' +\n '<div class=\"collapsible-content\"><p>c</p></div>' +\n '</div>' +\n '</div>' +\n '</div>'\n );\n });\n\n it('toggleCollapsibleSection flips the collapsed class and aria-expanded', () => {\n const parent = el.querySelector('.collapsible-section') as HTMLElement;\n toggleCollapsibleSection(parent);\n expect(parent.classList.contains('collapsed')).toBe(true);\n expect(parent.querySelector('.collapsible-toggle')?.getAttribute('aria-expanded')).toBe('false');\n });\n\n it('expandAllSections clears every collapsed section', () => {\n expandAllSections(el);\n expect(el.querySelectorAll('.collapsible-section.collapsed')).toHaveLength(0);\n });\n\n it('collapseAllSections collapses every section', () => {\n collapseAllSections(el);\n expect(el.querySelectorAll('.collapsible-section:not(.collapsed)')).toHaveLength(0);\n });\n\n it('expandToHeading reveals ancestor sections of a heading', () => {\n // child section starts collapsed; expanding to #child should clear it\n expandToHeading(el, 'child');\n const child = el.querySelector('[data-level=\"3\"]') as HTMLElement;\n expect(child.classList.contains('collapsed')).toBe(false);\n });\n\n it('toggleCollapsibleSection expands a section that starts collapsed', () => {\n const child = el.querySelector('[data-level=\"3\"]') as HTMLElement;\n expect(child.classList.contains('collapsed')).toBe(true);\n toggleCollapsibleSection(child);\n expect(child.classList.contains('collapsed')).toBe(false);\n expect(child.querySelector('.collapsible-toggle')?.getAttribute('aria-expanded')).toBe('true');\n });\n\n it('expandToHeading is a no-op for an unknown heading id (no throw)', () => {\n const child = el.querySelector('[data-level=\"3\"]') as HTMLElement;\n expect(() => expandToHeading(el, 'does-not-exist')).not.toThrow();\n // The child that was collapsed stays collapsed — nothing was touched.\n expect(child.classList.contains('collapsed')).toBe(true);\n });\n\n it('expand/collapse helpers are safe on a container with no sections', () => {\n const empty = container('<p>nothing collapsible</p>');\n expect(() => expandAllSections(empty)).not.toThrow();\n expect(() => collapseAllSections(empty)).not.toThrow();\n });\n});\n"]}
@@ -213,16 +213,6 @@ export declare class MarkdownComponent implements OnChanges, AfterViewInit, OnDe
213
213
  * Scroll to a heading by ID
214
214
  */
215
215
  scrollToHeading(headingId: string): void;
216
- /**
217
- * Strip JavaScript from HTML content while preserving layout HTML.
218
- * Removes <script> tags, on* event handlers, and javascript: URLs.
219
- */
220
- private stripJavaScript;
221
- /**
222
- * Fallback regex-based sanitization when DOMParser is unavailable.
223
- * Note: This is less robust than DOM parsing and should only be a fallback.
224
- */
225
- private fallbackStripJavaScript;
226
216
  static ɵfac: i0.ɵɵFactoryDeclaration<MarkdownComponent, never>;
227
217
  static ɵcmp: i0.ɵɵComponentDeclaration<MarkdownComponent, "mj-markdown", never, { "data": { "alias": "data"; "required": false; }; "enableHighlight": { "alias": "enableHighlight"; "required": false; }; "enableMermaid": { "alias": "enableMermaid"; "required": false; }; "enableCodeCopy": { "alias": "enableCodeCopy"; "required": false; }; "enableCollapsibleHeadings": { "alias": "enableCollapsibleHeadings"; "required": false; }; "collapsibleHeadingLevel": { "alias": "collapsibleHeadingLevel"; "required": false; }; "collapsibleDefaultExpanded": { "alias": "collapsibleDefaultExpanded"; "required": false; }; "autoExpandLevels": { "alias": "autoExpandLevels"; "required": false; }; "enableAlerts": { "alias": "enableAlerts"; "required": false; }; "enableSmartypants": { "alias": "enableSmartypants"; "required": false; }; "enableSvgRenderer": { "alias": "enableSvgRenderer"; "required": false; }; "enableHtml": { "alias": "enableHtml"; "required": false; }; "enableJavaScript": { "alias": "enableJavaScript"; "required": false; }; "enableHeadingIds": { "alias": "enableHeadingIds"; "required": false; }; "headingIdPrefix": { "alias": "headingIdPrefix"; "required": false; }; "enableLineNumbers": { "alias": "enableLineNumbers"; "required": false; }; "containerClass": { "alias": "containerClass"; "required": false; }; "mermaidTheme": { "alias": "mermaidTheme"; "required": false; }; "sanitize": { "alias": "sanitize"; "required": false; }; }, { "rendered": "rendered"; "headingClick": "headingClick"; "codeCopied": "codeCopied"; }, never, never, false, never>;
228
218
  }
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.component.d.ts","sourceRoot":"","sources":["../../../src/lib/components/markdown.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,YAAY,EACZ,UAAU,EACV,SAAS,EACT,SAAS,EACT,aAAa,EAIb,iBAAiB,EACjB,aAAa,EACd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAA2C,mBAAmB,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;;AAG1H;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAQa,iBAAkB,YAAW,SAAS,EAAE,aAAa,EAAE,SAAS;IA8IzE,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,eAAe;IACvB,OAAO,CAAC,GAAG;IAhJb;;OAEG;IACM,IAAI,EAAE,MAAM,CAAM;IAE3B;;OAEG;IACM,eAAe,EAAE,OAAO,CAA2C;IAE5E;;OAEG;IACM,aAAa,EAAE,OAAO,CAAyC;IAExE;;OAEG;IACM,cAAc,EAAE,OAAO,CAA0C;IAE1E;;OAEG;IACM,yBAAyB,EAAE,OAAO,CAAqD;IAEhG;;OAEG;IACM,uBAAuB,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAmD;IAE1G;;OAEG;IACM,0BAA0B,EAAE,OAAO,CAAsD;IAElG;;;;;;;;;;OAUG;IACM,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAErC;;OAEG;IACM,YAAY,EAAE,OAAO,CAAwC;IAEtE;;OAEG;IACM,iBAAiB,EAAE,OAAO,CAA6C;IAEhF;;;OAGG;IACM,iBAAiB,EAAE,OAAO,CAA6C;IAEhF;;;OAGG;IACM,UAAU,EAAE,OAAO,CAAsC;IAElE;;;OAGG;IACM,gBAAgB,EAAE,OAAO,CAA4C;IAE9E;;OAEG;IACM,gBAAgB,EAAE,OAAO,CAA4C;IAE9E;;OAEG;IACM,eAAe,EAAE,MAAM,CAA2C;IAE3E;;OAEG;IACM,iBAAiB,EAAE,OAAO,CAA6C;IAEhF;;OAEG;IACM,cAAc,EAAE,MAAM,CAAM;IAErC;;;OAGG;IACM,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAwC;IAE1H;;OAEG;IACM,QAAQ,EAAE,OAAO,CAAoC;IAE9D;;OAEG;IACO,QAAQ,oCAA2C;IAE7D;;OAEG;IACO,YAAY,4BAAmC;IAEzD;;OAEG;IACO,UAAU,uBAA8B;IAElD;;OAEG;IACI,eAAe,EAAE,QAAQ,CAAM;IAEtC;;;OAGG;IACH,IAAW,OAAO,IAAI,UAAU,CAAC,WAAW,CAAC,CAE5C;IAED,OAAO,CAAC,eAAe,CAAa;IACpC,OAAO,CAAC,UAAU,CAAkB;IACpC,OAAO,CAAC,aAAa,CAAkB;IACvC,OAAO,CAAC,aAAa,CAAiC;gBAG5C,UAAU,EAAE,UAAU,CAAC,WAAW,CAAC,EACnC,SAAS,EAAE,YAAY,EACvB,eAAe,EAAE,eAAe,EAChC,GAAG,EAAE,iBAAiB;IAGhC,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IA0BzC,eAAe,IAAI,IAAI;IASvB,WAAW,IAAI,IAAI;IAMnB;;OAEG;IACH,OAAO,CAAC,MAAM;IA4Dd;;;OAGG;YACW,oBAAoB;IA2ClC;;OAEG;IACH,OAAO,CAAC,yBAAyB;IA4DjC;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAgD3B;;;;OAIG;IACH,OAAO,CAAC,aAAa;IASrB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAW3B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAWzB;;OAEG;IACH,OAAO,CAAC,0BAA0B;IAmBlC;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAc9B;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAkB1B;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAM/B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAS7B;;OAEG;IACI,OAAO,IAAI,IAAI;IAItB;;OAEG;IACI,WAAW,IAAI,WAAW,EAAE;IAInC;;OAEG;IACI,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAU/C;;;OAGG;IACH,OAAO,CAAC,eAAe;IAiDvB;;;OAGG;IACH,OAAO,CAAC,uBAAuB;yCA/mBpB,iBAAiB;2CAAjB,iBAAiB;CAgoB7B"}
1
+ {"version":3,"file":"markdown.component.d.ts","sourceRoot":"","sources":["../../../src/lib/components/markdown.component.ts"],"names":[],"mappings":"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"}
@@ -77,5 +77,124 @@ describe('MarkdownComponent (DOM)', () => {
77
77
  expect(events.length).toBeGreaterThanOrEqual(1);
78
78
  expect(events[events.length - 1].hasCodeBlocks).toBe(false);
79
79
  });
80
+ /**
81
+ * Sanitization, as seen through the component. With `enableHtml` or the default
82
+ * `enableSvgRenderer`, Angular's sanitizer is bypassed and the HTML the service returns
83
+ * (sanitized by DOMPurify in parse()) is trusted. jsdom does not execute handlers or
84
+ * load images, so assertions are structural: the vector is gone, the layout survives.
85
+ */
86
+ describe('sanitization', () => {
87
+ const attributeNamesIn = (root) => Array.from(root.querySelectorAll('*')).flatMap((el) => Array.from(el.attributes).map((a) => a.name.toLowerCase()));
88
+ const passthrough = (data) => container(render({ data, enableHtml: true }));
89
+ it('removes <script> elements, including ones with a spaced end tag', () => {
90
+ const c = passthrough('<div>before</div><script>alert(1)</script ><div>after</div>');
91
+ expect(c.querySelector('script')).toBeNull();
92
+ expect(c.textContent).toContain('before');
93
+ expect(c.textContent).toContain('after');
94
+ expect(c.textContent).not.toContain('alert(1)');
95
+ });
96
+ it('removes every on* handler, whatever its name or quoting', () => {
97
+ const c = passthrough('<img src="x" onerror="alert(1)"><div onpointerrawupdate=alert(1) ONMOUSEENTER="alert(1)">t</div><svg onload="alert(1)"><circle r="1"/></svg>');
98
+ expect(attributeNamesIn(c).some((n) => n.startsWith('on'))).toBe(false);
99
+ expect(c.querySelector('img')).not.toBeNull();
100
+ expect(c.querySelector('svg circle')).not.toBeNull();
101
+ });
102
+ it.each([
103
+ ['javascript:', '<a href="javascript:alert(1)">x</a>'],
104
+ ['vbscript:', '<a href="vbscript:msgbox(1)">x</a>'],
105
+ ['data:text/html', '<a href="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==">x</a>'],
106
+ ['data:image/svg+xml on a link', '<a href="data:image/svg+xml;base64,PHN2ZyBvbmxvYWQ9YWxlcnQoMSk+PC9zdmc+">x</a>'],
107
+ ['whitespace inside the scheme', '<a href="java\tscript:alert(1)">x</a>'],
108
+ ['entity inside the scheme', '<a href="jav&#x09;ascript:alert(1)">x</a>'],
109
+ ['newline inside the scheme', '<a href="java\nscript:alert(1)">x</a>'],
110
+ ['upper-case scheme', '<a href="JAVASCRIPT:alert(1)">x</a>'],
111
+ ])('drops the link target for %s', (_label, html) => {
112
+ const a = passthrough(html).querySelector('a');
113
+ expect(a).not.toBeNull();
114
+ expect(a?.hasAttribute('href')).toBe(false);
115
+ });
116
+ it('drops javascript: from a form action', () => {
117
+ const c = passthrough('<form action="javascript:alert(1)"><button>go</button></form>');
118
+ expect(c.querySelector('form')?.hasAttribute('action')).toBe(false);
119
+ });
120
+ it.each([
121
+ ['iframe', '<iframe src="https://evil.test/"></iframe>'],
122
+ ['iframe with srcdoc', '<iframe srcdoc="&lt;script&gt;alert(1)&lt;/script&gt;"></iframe>'],
123
+ ['object', '<object data="https://evil.test/x.swf"></object>'],
124
+ ['embed', '<embed src="https://evil.test/x.swf">'],
125
+ ['base', '<base href="https://evil.test/">'],
126
+ ])('removes %s while keeping the surrounding content', (_label, html) => {
127
+ const c = passthrough('<p>before</p>' + html + '<p>after</p>');
128
+ expect(c.querySelector('iframe, object, embed, base')).toBeNull();
129
+ expect(c.textContent).toContain('before');
130
+ expect(c.textContent).toContain('after');
131
+ });
132
+ it('removes <foreignObject> and SMIL animation from inline SVG, keeps shapes', () => {
133
+ const c = passthrough('<svg><foreignObject><body onload="alert(1)"></body></foreignObject><circle r="1"><animate attributeName="r" to="2"/></circle></svg>');
134
+ expect(c.querySelector('foreignObject, animate')).toBeNull();
135
+ expect(c.querySelector('svg circle')).not.toBeNull();
136
+ });
137
+ it('keeps a same-document <use> and removes an external one', () => {
138
+ const c = passthrough('<svg><defs><symbol id="ic"><circle r="1"/></symbol></defs><use href="#ic"/><use href="https://evil.test/s.svg#x"/></svg>');
139
+ const uses = Array.from(c.querySelectorAll('use')).map((u) => u.getAttribute('href'));
140
+ expect(uses).toEqual(['#ic']);
141
+ });
142
+ it('keeps layout HTML, inline styles, data attributes and data: images', () => {
143
+ const c = passthrough('<table class="grid" data-role="report"><tr><td style="color: red">cell</td></tr></table>' +
144
+ '<input type="text" value="v"><details><summary>s</summary>body</details>' +
145
+ '<img src="data:image/png;base64,iVBORw0KGgo=" alt="pixel">');
146
+ expect(c.querySelector('table.grid')?.getAttribute('data-role')).toBe('report');
147
+ expect(c.querySelector('td')?.getAttribute('style')).toContain('color');
148
+ expect(c.querySelector('input')?.getAttribute('value')).toBe('v');
149
+ expect(c.querySelector('details summary')?.textContent).toBe('s');
150
+ expect(c.querySelector('img')?.getAttribute('src')).toMatch(/^data:image\/png/);
151
+ });
152
+ it('keeps a leading <style> block (mockups start that way)', () => {
153
+ const c = passthrough('<style>.card{color:red}</style><div class="card">x</div>');
154
+ expect(c.querySelector('style')?.textContent).toContain('.card');
155
+ expect(c.querySelector('div.card')).not.toBeNull();
156
+ });
157
+ it('keeps target on links and enforces rel=noopener', () => {
158
+ const c = passthrough('<a href="https://example.test/page" target="_blank">site</a><a href="#section">local</a>');
159
+ const [ext, local] = Array.from(c.querySelectorAll('a'));
160
+ expect(ext.getAttribute('href')).toBe('https://example.test/page');
161
+ expect(ext.getAttribute('target')).toBe('_blank');
162
+ expect(ext.getAttribute('rel')).toContain('noopener');
163
+ expect(local.getAttribute('href')).toBe('#section');
164
+ expect(local.hasAttribute('rel')).toBe(false);
165
+ });
166
+ it('keeps heading ids that collide with document properties', () => {
167
+ // DOMPurify's default DOM-clobbering guard would strip these; heading anchors must work.
168
+ const c = container(render({ data: '# Title' + '\n\n' + '## Location' + '\n\n' + '## Overview' }));
169
+ expect(c.querySelector('h1')?.id).toBe('title');
170
+ expect(c.querySelector('h2')?.id).toBe('location');
171
+ expect(Array.from(c.querySelectorAll('h2')).map((h) => h.id)).toEqual(['location', 'overview']);
172
+ });
173
+ it('sanitizes HTML that marked miscoded into a code block and the service unwrapped', () => {
174
+ // A bare fenced block whose text is structural HTML is unwrapped by the service (no
175
+ // language class), then sanitized like any other passthrough markup.
176
+ const c = passthrough('```' + '\n' + '<div class="card"><img src="x" onerror="alert(1)"><p>inner</p></div>' + '\n' + '```');
177
+ expect(c.querySelector('pre')).toBeNull();
178
+ expect(c.querySelector('div.card p')?.textContent).toBe('inner');
179
+ expect(attributeNamesIn(c).some((n) => n.startsWith('on'))).toBe(false);
180
+ });
181
+ it('strips handlers and script from a rendered SVG code fence (enableSvgRenderer)', () => {
182
+ const data = '```svg' + '\n' + '<svg xmlns="http://www.w3.org/2000/svg" onload="alert(1)"><script>alert(2)</script><circle r="1"/></svg>' + '\n' + '```';
183
+ const c = container(render({ data, enableSvgRenderer: true }));
184
+ expect(c.querySelector('svg')).not.toBeNull();
185
+ expect(c.querySelector('script')).toBeNull();
186
+ expect(attributeNamesIn(c).some((n) => n.startsWith('on'))).toBe(false);
187
+ });
188
+ it('enableJavaScript=true is the documented opt-out and leaves script elements in place', () => {
189
+ // jsdom (as vitest configures it) executes this script and logs "Not implemented:
190
+ // window.alert" to stderr. Expected here and only here.
191
+ const c = container(render({ data: '<div>x</div><script>alert(1)</script>', enableHtml: true, enableJavaScript: true }));
192
+ expect(c.querySelector('script')).not.toBeNull();
193
+ });
194
+ it('Angular sanitizer path (no passthrough, no SVG renderer) still strips handlers', () => {
195
+ const c = container(render({ data: 'text <img src="x" onerror="alert(1)">', enableSvgRenderer: false }));
196
+ expect(attributeNamesIn(c).some((n) => n.startsWith('on'))).toBe(false);
197
+ });
198
+ });
80
199
  });
81
200
  //# sourceMappingURL=markdown.component.dom.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.component.dom.test.js","sourceRoot":"","sources":["../../../src/lib/components/markdown.component.dom.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect } from 'vitest';\nimport { ComponentFixture } from '@angular/core/testing';\nimport { CommonModule } from '@angular/common';\nimport { renderComponentFixture, query, capture } from '@memberjunction/ng-test-utils';\nimport { MarkdownComponent } from './markdown.component';\nimport { MarkdownRenderEvent } from '@memberjunction/markdown-core';\n\n/**\n * DOM-level spec for <mj-markdown>. The component is module-declared\n * (standalone:false), so we render it via `declarations` + `imports: [CommonModule]`\n * and configure it through `@Input`s.\n *\n * It uses the real (providedIn:'root') MarkdownService, which parses markdown\n * synchronously via marked.js. We assert on the rendered output that lands in the\n * `[innerHTML]`-bound container, plus the container-class binding and the `rendered`\n * @Output. Mermaid/Prism async post-processing is NOT asserted here (mermaid needs a\n * real browser; that's a live-test concern) — we only verify the deterministic,\n * media-free template surface.\n */\ndescribe('MarkdownComponent (DOM)', () => {\n function render(inputs: Record<string, unknown>): ComponentFixture<MarkdownComponent> {\n return renderComponentFixture(MarkdownComponent, {\n imports: [CommonModule],\n declarations: [MarkdownComponent],\n inputs,\n });\n }\n\n const container = (f: ComponentFixture<MarkdownComponent>): HTMLElement => f.nativeElement.querySelector('.mj-markdown-container') as HTMLElement;\n\n it('renders the static container element', () => {\n const f = render({ data: '' });\n expect(query(f, '.mj-markdown-container')).not.toBeNull();\n });\n\n it('renders markdown content into the innerHTML-bound container', () => {\n const f = render({ data: '# Hello World' });\n const c = container(f);\n const heading = c.querySelector('h1');\n expect(heading).not.toBeNull();\n expect(heading!.textContent).toContain('Hello World');\n });\n\n it('renders inline markdown formatting (bold) as the corresponding HTML element', () => {\n const f = render({ data: 'This is **strong** text' });\n expect(container(f).querySelector('strong')?.textContent).toBe('strong');\n });\n\n it('renders a fenced code block as a <pre><code> structure', () => {\n const f = render({ data: '```\\nconst x = 1;\\n```' });\n const c = container(f);\n expect(c.querySelector('pre')).not.toBeNull();\n expect(c.querySelector('pre code')).not.toBeNull();\n });\n\n it('renders an empty container when data is empty', () => {\n const f = render({ data: '' });\n // No data => renderedContent stays '' => no rendered child markup.\n expect(container(f).children.length).toBe(0);\n });\n\n it('applies the containerClass input to the container element', () => {\n const f = render({ data: '# X', containerClass: 'my-custom-class' });\n expect(container(f).classList.contains('my-custom-class')).toBe(true);\n });\n\n it('emits the rendered @Output with hasCodeBlocks=true for code content', async () => {\n const f = render({ data: '' });\n const events = capture<MarkdownRenderEvent>(f.componentInstance.rendered);\n\n // Setting data after first render drives ngOnChanges -> render() -> postRenderProcessing\n // is scheduled on a microtask. Use setInput (zoneless-correct), then flush the microtask.\n f.componentRef.setInput('data', '```\\nconst x = 1;\\n```');\n f.detectChanges();\n await Promise.resolve();\n await Promise.resolve();\n\n expect(events.length).toBeGreaterThanOrEqual(1);\n const last = events[events.length - 1];\n expect(last.hasCodeBlocks).toBe(true);\n expect(last.hasMermaid).toBe(false);\n });\n\n it('emits the rendered @Output with hasCodeBlocks=false for prose-only content', async () => {\n const f = render({ data: '' });\n const events = capture<MarkdownRenderEvent>(f.componentInstance.rendered);\n\n f.componentRef.setInput('data', 'Just some plain prose.');\n f.detectChanges();\n await Promise.resolve();\n await Promise.resolve();\n\n expect(events.length).toBeGreaterThanOrEqual(1);\n expect(events[events.length - 1].hasCodeBlocks).toBe(false);\n });\n});\n"]}
1
+ {"version":3,"file":"markdown.component.dom.test.js","sourceRoot":"","sources":["../../../src/lib/components/markdown.component.dom.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect } from 'vitest';\nimport { ComponentFixture } from '@angular/core/testing';\nimport { CommonModule } from '@angular/common';\nimport { renderComponentFixture, query, capture } from '@memberjunction/ng-test-utils';\nimport { MarkdownComponent } from './markdown.component';\nimport { MarkdownRenderEvent } from '@memberjunction/markdown-core';\n\n/**\n * DOM-level spec for <mj-markdown>. The component is module-declared\n * (standalone:false), so we render it via `declarations` + `imports: [CommonModule]`\n * and configure it through `@Input`s.\n *\n * It uses the real (providedIn:'root') MarkdownService, which parses markdown\n * synchronously via marked.js. We assert on the rendered output that lands in the\n * `[innerHTML]`-bound container, plus the container-class binding and the `rendered`\n * @Output. Mermaid/Prism async post-processing is NOT asserted here (mermaid needs a\n * real browser; that's a live-test concern) — we only verify the deterministic,\n * media-free template surface.\n */\ndescribe('MarkdownComponent (DOM)', () => {\n function render(inputs: Record<string, unknown>): ComponentFixture<MarkdownComponent> {\n return renderComponentFixture(MarkdownComponent, {\n imports: [CommonModule],\n declarations: [MarkdownComponent],\n inputs,\n });\n }\n\n const container = (f: ComponentFixture<MarkdownComponent>): HTMLElement => f.nativeElement.querySelector('.mj-markdown-container') as HTMLElement;\n\n it('renders the static container element', () => {\n const f = render({ data: '' });\n expect(query(f, '.mj-markdown-container')).not.toBeNull();\n });\n\n it('renders markdown content into the innerHTML-bound container', () => {\n const f = render({ data: '# Hello World' });\n const c = container(f);\n const heading = c.querySelector('h1');\n expect(heading).not.toBeNull();\n expect(heading!.textContent).toContain('Hello World');\n });\n\n it('renders inline markdown formatting (bold) as the corresponding HTML element', () => {\n const f = render({ data: 'This is **strong** text' });\n expect(container(f).querySelector('strong')?.textContent).toBe('strong');\n });\n\n it('renders a fenced code block as a <pre><code> structure', () => {\n const f = render({ data: '```\\nconst x = 1;\\n```' });\n const c = container(f);\n expect(c.querySelector('pre')).not.toBeNull();\n expect(c.querySelector('pre code')).not.toBeNull();\n });\n\n it('renders an empty container when data is empty', () => {\n const f = render({ data: '' });\n // No data => renderedContent stays '' => no rendered child markup.\n expect(container(f).children.length).toBe(0);\n });\n\n it('applies the containerClass input to the container element', () => {\n const f = render({ data: '# X', containerClass: 'my-custom-class' });\n expect(container(f).classList.contains('my-custom-class')).toBe(true);\n });\n\n it('emits the rendered @Output with hasCodeBlocks=true for code content', async () => {\n const f = render({ data: '' });\n const events = capture<MarkdownRenderEvent>(f.componentInstance.rendered);\n\n // Setting data after first render drives ngOnChanges -> render() -> postRenderProcessing\n // is scheduled on a microtask. Use setInput (zoneless-correct), then flush the microtask.\n f.componentRef.setInput('data', '```\\nconst x = 1;\\n```');\n f.detectChanges();\n await Promise.resolve();\n await Promise.resolve();\n\n expect(events.length).toBeGreaterThanOrEqual(1);\n const last = events[events.length - 1];\n expect(last.hasCodeBlocks).toBe(true);\n expect(last.hasMermaid).toBe(false);\n });\n\n it('emits the rendered @Output with hasCodeBlocks=false for prose-only content', async () => {\n const f = render({ data: '' });\n const events = capture<MarkdownRenderEvent>(f.componentInstance.rendered);\n\n f.componentRef.setInput('data', 'Just some plain prose.');\n f.detectChanges();\n await Promise.resolve();\n await Promise.resolve();\n\n expect(events.length).toBeGreaterThanOrEqual(1);\n expect(events[events.length - 1].hasCodeBlocks).toBe(false);\n });\n\n /**\n * Sanitization, as seen through the component. With `enableHtml` or the default\n * `enableSvgRenderer`, Angular's sanitizer is bypassed and the HTML the service returns\n * (sanitized by DOMPurify in parse()) is trusted. jsdom does not execute handlers or\n * load images, so assertions are structural: the vector is gone, the layout survives.\n */\n describe('sanitization', () => {\n const attributeNamesIn = (root: HTMLElement): string[] =>\n Array.from(root.querySelectorAll('*')).flatMap((el) => Array.from(el.attributes).map((a) => a.name.toLowerCase()));\n const passthrough = (data: string) => container(render({ data, enableHtml: true }));\n\n it('removes <script> elements, including ones with a spaced end tag', () => {\n const c = passthrough('<div>before</div><script>alert(1)</script ><div>after</div>');\n expect(c.querySelector('script')).toBeNull();\n expect(c.textContent).toContain('before');\n expect(c.textContent).toContain('after');\n expect(c.textContent).not.toContain('alert(1)');\n });\n\n it('removes every on* handler, whatever its name or quoting', () => {\n const c = passthrough('<img src=\"x\" onerror=\"alert(1)\"><div onpointerrawupdate=alert(1) ONMOUSEENTER=\"alert(1)\">t</div><svg onload=\"alert(1)\"><circle r=\"1\"/></svg>');\n expect(attributeNamesIn(c).some((n) => n.startsWith('on'))).toBe(false);\n expect(c.querySelector('img')).not.toBeNull();\n expect(c.querySelector('svg circle')).not.toBeNull();\n });\n\n it.each([\n ['javascript:', '<a href=\"javascript:alert(1)\">x</a>'],\n ['vbscript:', '<a href=\"vbscript:msgbox(1)\">x</a>'],\n ['data:text/html', '<a href=\"data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==\">x</a>'],\n ['data:image/svg+xml on a link', '<a href=\"data:image/svg+xml;base64,PHN2ZyBvbmxvYWQ9YWxlcnQoMSk+PC9zdmc+\">x</a>'],\n ['whitespace inside the scheme', '<a href=\"java\\tscript:alert(1)\">x</a>'],\n ['entity inside the scheme', '<a href=\"jav&#x09;ascript:alert(1)\">x</a>'],\n ['newline inside the scheme', '<a href=\"java\\nscript:alert(1)\">x</a>'],\n ['upper-case scheme', '<a href=\"JAVASCRIPT:alert(1)\">x</a>'],\n ])('drops the link target for %s', (_label, html) => {\n const a = passthrough(html).querySelector('a');\n expect(a).not.toBeNull();\n expect(a?.hasAttribute('href')).toBe(false);\n });\n\n it('drops javascript: from a form action', () => {\n const c = passthrough('<form action=\"javascript:alert(1)\"><button>go</button></form>');\n expect(c.querySelector('form')?.hasAttribute('action')).toBe(false);\n });\n\n it.each([\n ['iframe', '<iframe src=\"https://evil.test/\"></iframe>'],\n ['iframe with srcdoc', '<iframe srcdoc=\"&lt;script&gt;alert(1)&lt;/script&gt;\"></iframe>'],\n ['object', '<object data=\"https://evil.test/x.swf\"></object>'],\n ['embed', '<embed src=\"https://evil.test/x.swf\">'],\n ['base', '<base href=\"https://evil.test/\">'],\n ])('removes %s while keeping the surrounding content', (_label, html) => {\n const c = passthrough('<p>before</p>' + html + '<p>after</p>');\n expect(c.querySelector('iframe, object, embed, base')).toBeNull();\n expect(c.textContent).toContain('before');\n expect(c.textContent).toContain('after');\n });\n\n it('removes <foreignObject> and SMIL animation from inline SVG, keeps shapes', () => {\n const c = passthrough('<svg><foreignObject><body onload=\"alert(1)\"></body></foreignObject><circle r=\"1\"><animate attributeName=\"r\" to=\"2\"/></circle></svg>');\n expect(c.querySelector('foreignObject, animate')).toBeNull();\n expect(c.querySelector('svg circle')).not.toBeNull();\n });\n\n it('keeps a same-document <use> and removes an external one', () => {\n const c = passthrough('<svg><defs><symbol id=\"ic\"><circle r=\"1\"/></symbol></defs><use href=\"#ic\"/><use href=\"https://evil.test/s.svg#x\"/></svg>');\n const uses = Array.from(c.querySelectorAll('use')).map((u) => u.getAttribute('href'));\n expect(uses).toEqual(['#ic']);\n });\n\n it('keeps layout HTML, inline styles, data attributes and data: images', () => {\n const c = passthrough(\n '<table class=\"grid\" data-role=\"report\"><tr><td style=\"color: red\">cell</td></tr></table>' +\n '<input type=\"text\" value=\"v\"><details><summary>s</summary>body</details>' +\n '<img src=\"data:image/png;base64,iVBORw0KGgo=\" alt=\"pixel\">',\n );\n expect(c.querySelector('table.grid')?.getAttribute('data-role')).toBe('report');\n expect(c.querySelector('td')?.getAttribute('style')).toContain('color');\n expect(c.querySelector('input')?.getAttribute('value')).toBe('v');\n expect(c.querySelector('details summary')?.textContent).toBe('s');\n expect(c.querySelector('img')?.getAttribute('src')).toMatch(/^data:image\\/png/);\n });\n\n it('keeps a leading <style> block (mockups start that way)', () => {\n const c = passthrough('<style>.card{color:red}</style><div class=\"card\">x</div>');\n expect(c.querySelector('style')?.textContent).toContain('.card');\n expect(c.querySelector('div.card')).not.toBeNull();\n });\n\n it('keeps target on links and enforces rel=noopener', () => {\n const c = passthrough('<a href=\"https://example.test/page\" target=\"_blank\">site</a><a href=\"#section\">local</a>');\n const [ext, local] = Array.from(c.querySelectorAll('a'));\n expect(ext.getAttribute('href')).toBe('https://example.test/page');\n expect(ext.getAttribute('target')).toBe('_blank');\n expect(ext.getAttribute('rel')).toContain('noopener');\n expect(local.getAttribute('href')).toBe('#section');\n expect(local.hasAttribute('rel')).toBe(false);\n });\n\n it('keeps heading ids that collide with document properties', () => {\n // DOMPurify's default DOM-clobbering guard would strip these; heading anchors must work.\n const c = container(render({ data: '# Title' + '\\n\\n' + '## Location' + '\\n\\n' + '## Overview' }));\n expect(c.querySelector('h1')?.id).toBe('title');\n expect(c.querySelector('h2')?.id).toBe('location');\n expect(Array.from(c.querySelectorAll('h2')).map((h) => h.id)).toEqual(['location', 'overview']);\n });\n\n it('sanitizes HTML that marked miscoded into a code block and the service unwrapped', () => {\n // A bare fenced block whose text is structural HTML is unwrapped by the service (no\n // language class), then sanitized like any other passthrough markup.\n const c = passthrough('```' + '\\n' + '<div class=\"card\"><img src=\"x\" onerror=\"alert(1)\"><p>inner</p></div>' + '\\n' + '```');\n expect(c.querySelector('pre')).toBeNull();\n expect(c.querySelector('div.card p')?.textContent).toBe('inner');\n expect(attributeNamesIn(c).some((n) => n.startsWith('on'))).toBe(false);\n });\n\n it('strips handlers and script from a rendered SVG code fence (enableSvgRenderer)', () => {\n const data = '```svg' + '\\n' + '<svg xmlns=\"http://www.w3.org/2000/svg\" onload=\"alert(1)\"><script>alert(2)</script><circle r=\"1\"/></svg>' + '\\n' + '```';\n const c = container(render({ data, enableSvgRenderer: true }));\n expect(c.querySelector('svg')).not.toBeNull();\n expect(c.querySelector('script')).toBeNull();\n expect(attributeNamesIn(c).some((n) => n.startsWith('on'))).toBe(false);\n });\n\n it('enableJavaScript=true is the documented opt-out and leaves script elements in place', () => {\n // jsdom (as vitest configures it) executes this script and logs \"Not implemented:\n // window.alert\" to stderr. Expected here and only here.\n const c = container(render({ data: '<div>x</div><script>alert(1)</script>', enableHtml: true, enableJavaScript: true }));\n expect(c.querySelector('script')).not.toBeNull();\n });\n\n it('Angular sanitizer path (no passthrough, no SVG renderer) still strips handlers', () => {\n const c = container(render({ data: 'text <img src=\"x\" onerror=\"alert(1)\">', enableSvgRenderer: false }));\n expect(attributeNamesIn(c).some((n) => n.startsWith('on'))).toBe(false);\n });\n });\n});\n"]}
@@ -216,11 +216,9 @@ export class MarkdownComponent {
216
216
  const sanitized = this.sanitizer.sanitize(SecurityContext.HTML, html);
217
217
  html = sanitized || '';
218
218
  }
219
- // Strip JavaScript unless explicitly enabled
220
- // This removes <script> tags and on* event handlers while keeping layout HTML
221
- if (bypassAngularSanitizer && !this.enableJavaScript) {
222
- html = this.stripJavaScript(html);
223
- }
219
+ // When Angular's sanitizer is bypassed, the HTML has already been sanitized by
220
+ // MarkdownService.parse() (DOMPurify, HTML + SVG profiles) unless enableJavaScript
221
+ // opted out. Nothing further to do here.
224
222
  // Trust the HTML for display
225
223
  this.renderedContent = this.sanitizer.bypassSecurityTrustHtml(html);
226
224
  this.cdr.markForCheck();
@@ -504,67 +502,6 @@ export class MarkdownComponent {
504
502
  heading.scrollIntoView({ behavior: 'smooth', block: 'start' });
505
503
  }
506
504
  }
507
- /**
508
- * Strip JavaScript from HTML content while preserving layout HTML.
509
- * Removes <script> tags, on* event handlers, and javascript: URLs.
510
- */
511
- stripJavaScript(html) {
512
- if (typeof DOMParser === 'undefined') {
513
- // Fallback for environments without DOMParser
514
- return this.fallbackStripJavaScript(html);
515
- }
516
- try {
517
- const parser = new DOMParser();
518
- const doc = parser.parseFromString('<body>' + html + '</body>', 'text/html');
519
- const cleanNode = (node) => {
520
- const tagName = node.tagName.toLowerCase();
521
- // Remove unsafe elements completely
522
- if (tagName === 'script' || tagName === 'iframe' || tagName === 'object' || tagName === 'embed' || tagName === 'base') {
523
- node.parentNode?.removeChild(node);
524
- return;
525
- }
526
- if (node.attributes) {
527
- const attrs = Array.from(node.attributes);
528
- for (const attr of attrs) {
529
- const name = attr.name.toLowerCase();
530
- // Remove whitespace and control chars from value to prevent bypasses like "java\nscript:"
531
- // eslint-disable-next-line no-control-regex
532
- const valueStr = attr.value.toLowerCase().replace(/[\s\x00-\x20]/g, '');
533
- if (name.startsWith('on') ||
534
- ((name === 'href' || name === 'xlink:href' || name === 'src' || name === 'action' || name === 'formaction') &&
535
- (valueStr.startsWith('javascript:') || valueStr.startsWith('vbscript:') || valueStr.startsWith('data:text/html')))) {
536
- node.removeAttribute(attr.name);
537
- }
538
- }
539
- }
540
- // Recursively clean children (Array.from prevents issues with live collections when removing nodes)
541
- Array.from(node.children).forEach(cleanNode);
542
- };
543
- Array.from(doc.body.children).forEach(cleanNode);
544
- return doc.body.innerHTML;
545
- }
546
- catch (e) {
547
- // If parsing fails completely, fall back to aggressive regex
548
- return this.fallbackStripJavaScript(html);
549
- }
550
- }
551
- /**
552
- * Fallback regex-based sanitization when DOMParser is unavailable.
553
- * Note: This is less robust than DOM parsing and should only be a fallback.
554
- */
555
- fallbackStripJavaScript(html) {
556
- // Remove <script> tags and their content
557
- html = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
558
- // Remove on* event handlers (onclick, onload, onerror, etc.)
559
- html = html.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '');
560
- html = html.replace(/\s+on\w+\s*=\s*[^\s>]+/gi, '');
561
- // Remove javascript: URLs from href and src attributes
562
- html = html.replace(/\s+href\s*=\s*["']?javascript:[^"'>\s]*["']?/gi, '');
563
- html = html.replace(/\s+src\s*=\s*["']?javascript:[^"'>\s]*["']?/gi, '');
564
- // Remove data: URLs that could contain scripts (data:text/html, etc.)
565
- html = html.replace(/\s+src\s*=\s*["']?data:text\/html[^"'>\s]*["']?/gi, '');
566
- return html;
567
- }
568
505
  static { this.ɵfac = function MarkdownComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MarkdownComponent)(i0.ɵɵdirectiveInject(i0.ElementRef), i0.ɵɵdirectiveInject(i1.DomSanitizer), i0.ɵɵdirectiveInject(i2.MarkdownService), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); }; }
569
506
  static { this.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MarkdownComponent, selectors: [["mj-markdown"]], inputs: { data: "data", enableHighlight: "enableHighlight", enableMermaid: "enableMermaid", enableCodeCopy: "enableCodeCopy", enableCollapsibleHeadings: "enableCollapsibleHeadings", collapsibleHeadingLevel: "collapsibleHeadingLevel", collapsibleDefaultExpanded: "collapsibleDefaultExpanded", autoExpandLevels: "autoExpandLevels", enableAlerts: "enableAlerts", enableSmartypants: "enableSmartypants", enableSvgRenderer: "enableSvgRenderer", enableHtml: "enableHtml", enableJavaScript: "enableJavaScript", enableHeadingIds: "enableHeadingIds", headingIdPrefix: "headingIdPrefix", enableLineNumbers: "enableLineNumbers", containerClass: "containerClass", mermaidTheme: "mermaidTheme", sanitize: "sanitize" }, outputs: { rendered: "rendered", headingClick: "headingClick", codeCopied: "codeCopied" }, standalone: false, features: [i0.ɵɵNgOnChangesFeature], decls: 1, vars: 3, consts: [[1, "mj-markdown-container", 3, "innerHTML"]], template: function MarkdownComponent_Template(rf, ctx) { if (rf & 1) {
570
507
  i0.ɵɵelement(0, "div", 0);