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', 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'))