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 = `
`;
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);
+ });
});