From 3c825b70116681733e1e9905ad34feb4c310cc7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=7BAI=7Df=20D=2E=20M=C3=BCller?= Date: Fri, 29 May 2026 10:52:17 +0200 Subject: [PATCH 1/2] fix: decode HTML entities in anchor modal title The modal heading extracted the

text via regex from rendered HTML (which contains &) and set it as textContent, so anchors like "Plain English according to Strunk & White" showed the literal "&". Parse via DOMParser instead so the browser decodes entities; DOMParser is parse-don't-execute (no XSS risk). The share-link title, which reads from #modal-title, is fixed by the same change. Adds a regression test that fails on the old regex path. Co-Authored-By: Claude Opus 4.8 (1M context) --- website/src/components/anchor-modal.js | 9 ++++++--- website/src/components/anchor-modal.test.js | 13 +++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/website/src/components/anchor-modal.js b/website/src/components/anchor-modal.js index 15f58d4..fcba5e3 100644 --- a/website/src/components/anchor-modal.js +++ b/website/src/components/anchor-modal.js @@ -238,9 +238,12 @@ export async function loadAnchorContent(anchorId) { }, }) - // Extract title from HTML or use anchor ID - const titleMatch = htmlContent.match(/]*>([^<]+)<\/h1>/) - const title = titleMatch ? titleMatch[1] : anchorId + // Extract title from HTML or use anchor ID. + // Parse via DOMParser so entities like & decode to their characters — + // setting textContent on a regex-captured string would render + // "Strunk & White" literally (regressed twice; keep this DOM round-trip). + const h1El = new DOMParser().parseFromString(htmlContent, 'text/html').querySelector('h1') + const title = h1El ? h1El.textContent.trim() : anchorId titleEl.textContent = title // Safe: htmlContent is generated by asciidoctor from .adoc files served from our own origin diff --git a/website/src/components/anchor-modal.test.js b/website/src/components/anchor-modal.test.js index 165fc28..a09730d 100644 --- a/website/src/components/anchor-modal.test.js +++ b/website/src/components/anchor-modal.test.js @@ -129,6 +129,19 @@ describe('anchor-modal', () => { expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('test-anchor.adoc')) }) + it('should decode HTML entities in the modal title (regression)', async () => { + global.fetch.mockResolvedValue({ + ok: true, + text: async () => '= Plain English according to Strunk & White\n\nBody.', + }) + + await showAnchorDetails('plain-english-strunk-white') + + const title = document.getElementById('modal-title').textContent + expect(title).toBe('Plain English according to Strunk & White') + expect(title).not.toContain('&') + }) + it('should handle fetch errors gracefully', async () => { global.fetch.mockRejectedValue(new Error('Network error')) From 89d3d5249e1515599be03d5f722835fe1c04ab32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=7BAI=7Df=20D=2E=20M=C3=BCller?= Date: Fri, 29 May 2026 13:51:00 +0200 Subject: [PATCH 2/2] chore: allow DOMParser browser global in ESLint config The entity-decode fix uses DOMParser, which the explicit browser-globals allowlist did not include, causing a no-undef lint error. Co-Authored-By: Claude Opus 4.8 (1M context) --- website/eslint.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/website/eslint.config.js b/website/eslint.config.js index e6f5711..ddff543 100644 --- a/website/eslint.config.js +++ b/website/eslint.config.js @@ -18,6 +18,7 @@ const browserGlobals = { setInterval: 'readonly', clearInterval: 'readonly', HTMLElement: 'readonly', + DOMParser: 'readonly', Event: 'readonly', CustomEvent: 'readonly', KeyboardEvent: 'readonly',