|
| 1 | +import { GlobalRegistrator } from "@happy-dom/global-registrator" |
| 2 | +import { afterAll, beforeAll, expect, test } from "bun:test" |
| 3 | +import { markdownClipboardHtml } from "./markdown-clipboard" |
| 4 | + |
| 5 | +beforeAll(() => GlobalRegistrator.register()) |
| 6 | + |
| 7 | +afterAll(() => GlobalRegistrator.unregister()) |
| 8 | + |
| 9 | +function markdownRoot(html: string) { |
| 10 | + const root = document.createElement("div") |
| 11 | + root.setAttribute("data-component", "markdown") |
| 12 | + root.innerHTML = html |
| 13 | + document.body.replaceChildren(root) |
| 14 | + |
| 15 | + return root |
| 16 | +} |
| 17 | + |
| 18 | +function rangeOver(node: Node) { |
| 19 | + const range = document.createRange() |
| 20 | + range.selectNodeContents(node) |
| 21 | + |
| 22 | + return range |
| 23 | +} |
| 24 | + |
| 25 | +test("keeps semantic structure without colours, classes, or layout attributes", () => { |
| 26 | + const root = markdownRoot( |
| 27 | + [ |
| 28 | + '<div data-markdown-block="" style="display: contents">', |
| 29 | + '<h2 class="heading" style="color: rgb(237, 237, 237)">Title</h2>', |
| 30 | + '<p style="color: white">Some <strong>bold</strong>, <em>italic</em> and <code data-inline-code-kind="path">src/app.ts</code>.</p>', |
| 31 | + '<ol start="3"><li>three</li><li>four</li></ol>', |
| 32 | + '<table><thead><tr><th style="text-align: left">A</th></tr></thead><tbody><tr><td colspan="2">1</td></tr></tbody></table>', |
| 33 | + "<blockquote><p>quoted</p></blockquote>", |
| 34 | + "</div>", |
| 35 | + ].join(""), |
| 36 | + ) |
| 37 | + |
| 38 | + expect(markdownClipboardHtml(root, rangeOver(root))).toBe( |
| 39 | + [ |
| 40 | + "<div>", |
| 41 | + "<h2>Title</h2>", |
| 42 | + "<p>Some <strong>bold</strong>, <em>italic</em> and <code>src/app.ts</code>.</p>", |
| 43 | + '<ol start="3"><li>three</li><li>four</li></ol>', |
| 44 | + '<table><thead><tr><th>A</th></tr></thead><tbody><tr><td colspan="2">1</td></tr></tbody></table>', |
| 45 | + "<blockquote><p>quoted</p></blockquote>", |
| 46 | + "</div>", |
| 47 | + ].join(""), |
| 48 | + ) |
| 49 | +}) |
| 50 | + |
| 51 | +test("strips syntax highlight colours and the copy button from code blocks", () => { |
| 52 | + const root = markdownRoot( |
| 53 | + [ |
| 54 | + '<div data-component="markdown-code" data-language="ts">', |
| 55 | + '<pre class="highlights"><code class="language-ts">', |
| 56 | + '<span style="color: #c678dd">const</span><span style="color: #e5c07b"> a</span>', |
| 57 | + "</code></pre>", |
| 58 | + '<div data-slot="markdown-copy-button"><button aria-label="Copy"><svg></svg></button></div>', |
| 59 | + "</div>", |
| 60 | + ].join(""), |
| 61 | + ) |
| 62 | + |
| 63 | + expect(markdownClipboardHtml(root, rangeOver(root))).toBe( |
| 64 | + "<div><pre><code><span>const</span><span> a</span></code></pre></div>", |
| 65 | + ) |
| 66 | +}) |
| 67 | + |
| 68 | +test("wraps a partial selection in its semantic ancestors", () => { |
| 69 | + const root = markdownRoot( |
| 70 | + '<ul><li>item <strong class="x">very bold</strong></li></ul><pre><code><span style="color: red">let x</span></code></pre>', |
| 71 | + ) |
| 72 | + |
| 73 | + const bold = root.querySelector("strong")!.firstChild! |
| 74 | + const token = root.querySelector("pre span")!.firstChild! |
| 75 | + const boldRange = document.createRange() |
| 76 | + boldRange.setStart(bold, 5) |
| 77 | + boldRange.setEnd(bold, 9) |
| 78 | + const codeRange = document.createRange() |
| 79 | + codeRange.setStart(token, 0) |
| 80 | + codeRange.setEnd(token, 3) |
| 81 | + |
| 82 | + expect(markdownClipboardHtml(root, boldRange)).toBe("<ul><li><strong>bold</strong></li></ul>") |
| 83 | + expect(markdownClipboardHtml(root, codeRange)).toBe("<pre><code>let</code></pre>") |
| 84 | +}) |
| 85 | + |
| 86 | +test("keeps web links and drops local paths, scripts, and decorative icons", () => { |
| 87 | + const root = markdownRoot( |
| 88 | + [ |
| 89 | + '<p><a class="external-link" target="_blank" href="https://opencode.ai/docs">', |
| 90 | + '<span class="markdown-link-favicon" aria-hidden="true"><img src="https://example.com/icon.png"></span>docs</a> ', |
| 91 | + '<a href="javascript:alert(1)">bad</a> <a href="mailto:[email protected]">mail</a> ', |
| 92 | + '<a data-local-link="src/app.ts" href="src/app.ts">app</a> ', |
| 93 | + '<img src="https://example.com/chart.png" alt="chart" width="20"> <img src="blob:local" alt="local">', |
| 94 | + '<span class="sr-only">hidden</span></p>', |
| 95 | + ].join(""), |
| 96 | + ) |
| 97 | + |
| 98 | + expect(markdownClipboardHtml(root, rangeOver(root))).toBe( |
| 99 | + [ |
| 100 | + '<p><a href="https://opencode.ai/docs">docs</a> ', |
| 101 | + '<a>bad</a> <a href="mailto:[email protected]">mail</a> ', |
| 102 | + "<a>app</a> ", |
| 103 | + '<img src="https://example.com/chart.png" alt="chart"> <img alt="local">', |
| 104 | + "</p>", |
| 105 | + ].join(""), |
| 106 | + ) |
| 107 | +}) |
0 commit comments