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