@memberjunction/ng-markdown 6.1.0-edge.4 → 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":"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"}
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"]}