fix: HTML-escape seed + NPC fields in <npc-tray> to prevent markup breakage
This commit is contained in:
@@ -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, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
export class NpcTray extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
@@ -21,18 +30,18 @@ export class NpcTray extends HTMLElement {
|
||||
|
||||
render() {
|
||||
const pills = listGenrePacks().map((p) =>
|
||||
`<button class="pill ${p.id === this.genreId ? 'on' : ''}" data-genre="${p.id}">${p.label}</button>`).join('');
|
||||
`<button class="pill ${p.id === this.genreId ? 'on' : ''}" data-genre="${p.id}">${escapeHtml(p.label)}</button>`).join('');
|
||||
const n = this.last;
|
||||
const card = n ? `
|
||||
<div class="npc-card" data-role="card">
|
||||
<div class="npc-name" data-role="npc-name">${n.name}</div>
|
||||
<div class="npc-seed">${n.seed}</div>
|
||||
<div class="npc-name" data-role="npc-name">${escapeHtml(n.name)}</div>
|
||||
<div class="npc-seed">${escapeHtml(n.seed)}</div>
|
||||
<dl>
|
||||
<dt>Look</dt><dd>${n.look}</dd>
|
||||
<dt>Manner</dt><dd>${n.manner}</dd>
|
||||
<dt>Wants</dt><dd>${n.wants}</dd>
|
||||
<dt>Secret</dt><dd class="secret">${n.secret}</dd>
|
||||
<dt>Voice</dt><dd>${n.voice}</dd>
|
||||
<dt>Look</dt><dd>${escapeHtml(n.look)}</dd>
|
||||
<dt>Manner</dt><dd>${escapeHtml(n.manner)}</dd>
|
||||
<dt>Wants</dt><dd>${escapeHtml(n.wants)}</dd>
|
||||
<dt>Secret</dt><dd class="secret">${escapeHtml(n.secret)}</dd>
|
||||
<dt>Voice</dt><dd>${escapeHtml(n.voice)}</dd>
|
||||
</dl>
|
||||
<div class="npc-actions">
|
||||
<button class="npc-btn" data-role="reroll">↻ Reroll</button>
|
||||
@@ -43,7 +52,7 @@ export class NpcTray extends HTMLElement {
|
||||
this.innerHTML = `
|
||||
<div class="npc-tray">
|
||||
<div class="pills">${pills}</div>
|
||||
<input class="seed" data-role="seed" placeholder="a little info — e.g. nervous dock foreman" value="${this._seedValue}" />
|
||||
<input class="seed" data-role="seed" placeholder="a little info — e.g. nervous dock foreman" value="${escapeHtml(this._seedValue)}" />
|
||||
<button class="generate" data-role="generate">✦ Generate NPC</button>
|
||||
${card}
|
||||
</div>`;
|
||||
|
||||
@@ -37,4 +37,14 @@ describe('<npc-tray>', () => {
|
||||
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" <guy>';
|
||||
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" <guy>');
|
||||
// card still renders a name
|
||||
expect(el.querySelector('[data-role=npc-name]').textContent.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user