Skip to content

Commit 3c825b7

Browse files
raifdmuellerclaude
andcommitted
fix: decode HTML entities in anchor modal title
The modal heading extracted the <h1> text via regex from rendered HTML (which contains &amp;) and set it as textContent, so anchors like "Plain English according to Strunk & White" showed the literal "&amp;". 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) <noreply@anthropic.com>
1 parent bd1a8a4 commit 3c825b7

2 files changed

Lines changed: 19 additions & 3 deletions

File tree

website/src/components/anchor-modal.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,12 @@ export async function loadAnchorContent(anchorId) {
238238
},
239239
})
240240

241-
// Extract title from HTML or use anchor ID
242-
const titleMatch = htmlContent.match(/<h1[^>]*>([^<]+)<\/h1>/)
243-
const title = titleMatch ? titleMatch[1] : anchorId
241+
// Extract title from HTML or use anchor ID.
242+
// Parse via DOMParser so entities like &amp; decode to their characters —
243+
// setting textContent on a regex-captured string would render
244+
// "Strunk &amp; White" literally (regressed twice; keep this DOM round-trip).
245+
const h1El = new DOMParser().parseFromString(htmlContent, 'text/html').querySelector('h1')
246+
const title = h1El ? h1El.textContent.trim() : anchorId
244247

245248
titleEl.textContent = title
246249
// Safe: htmlContent is generated by asciidoctor from .adoc files served from our own origin

website/src/components/anchor-modal.test.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,19 @@ describe('anchor-modal', () => {
129129
expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('test-anchor.adoc'))
130130
})
131131

132+
it('should decode HTML entities in the modal title (regression)', async () => {
133+
global.fetch.mockResolvedValue({
134+
ok: true,
135+
text: async () => '= Plain English according to Strunk & White\n\nBody.',
136+
})
137+
138+
await showAnchorDetails('plain-english-strunk-white')
139+
140+
const title = document.getElementById('modal-title').textContent
141+
expect(title).toBe('Plain English according to Strunk & White')
142+
expect(title).not.toContain('&amp;')
143+
})
144+
132145
it('should handle fetch errors gracefully', async () => {
133146
global.fetch.mockRejectedValue(new Error('Network error'))
134147

0 commit comments

Comments
 (0)