From f61eb48038859f0270cabe8c68132595e417869d Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Mon, 20 Jul 2026 15:21:59 +0100 Subject: [PATCH] fix: HTML-escape seed + NPC fields in to prevent markup breakage --- src/components/npc-tray.js | 27 ++++++++++++++++++--------- tests/components/npc-tray.test.js | 10 ++++++++++ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/components/npc-tray.js b/src/components/npc-tray.js index 403716c..19a3022 100644 --- a/src/components/npc-tray.js +++ b/src/components/npc-tray.js @@ -1,6 +1,15 @@ import { generateNpc } from '../npc/generator.js'; import { getGenrePack, listGenrePacks } from '../npc/packs/index.js'; +function escapeHtml(s) { + return String(s ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + export class NpcTray extends HTMLElement { constructor() { super(); @@ -21,18 +30,18 @@ export class NpcTray extends HTMLElement { render() { const pills = listGenrePacks().map((p) => - ``).join(''); + ``).join(''); const n = this.last; const card = n ? `
-
${n.name}
-
${n.seed}
+
${escapeHtml(n.name)}
+
${escapeHtml(n.seed)}
-
Look
${n.look}
-
Manner
${n.manner}
-
Wants
${n.wants}
-
Secret
${n.secret}
-
Voice
${n.voice}
+
Look
${escapeHtml(n.look)}
+
Manner
${escapeHtml(n.manner)}
+
Wants
${escapeHtml(n.wants)}
+
Secret
${escapeHtml(n.secret)}
+
Voice
${escapeHtml(n.voice)}
@@ -43,7 +52,7 @@ export class NpcTray extends HTMLElement { this.innerHTML = `
${pills}
- + ${card}
`; diff --git a/tests/components/npc-tray.test.js b/tests/components/npc-tray.test.js index bcd8701..702323c 100644 --- a/tests/components/npc-tray.test.js +++ b/tests/components/npc-tray.test.js @@ -37,4 +37,14 @@ describe('', () => { expect(detail).not.toBe(null); expect(detail.name).toBe(el.last.name); }); + + it('does not break when the seed contains quotes or angle brackets', () => { + el.rng = seq([0.0]); + el.querySelector('[data-role=seed]').value = 'the "doc" '; + el.querySelector('[data-role=generate]').click(); + // seed round-trips intact through the re-rendered input (escaping worked) + expect(el.querySelector('[data-role=seed]').value).toBe('the "doc" '); + // card still renders a name + expect(el.querySelector('[data-role=npc-name]').textContent.length).toBeGreaterThan(0); + }); });