refactor: shared escapeHtml util; escape prop-viewer src/alt

This commit is contained in:
2026-07-20 15:35:57 +01:00
parent 9a5e4c4ce4
commit 5bdeef654e
5 changed files with 35 additions and 10 deletions
+1 -9
View File
@@ -1,14 +1,6 @@
import { generateNpc } from '../npc/generator.js';
import { getGenrePack, listGenrePacks } from '../npc/packs/index.js';
function escapeHtml(s) {
return String(s ?? '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
import { escapeHtml } from '../core/escape-html.js';
export class NpcTray extends HTMLElement {
constructor() {
+3 -1
View File
@@ -1,3 +1,5 @@
import { escapeHtml } from '../core/escape-html.js';
export class PropViewer extends HTMLElement {
constructor() { super(); this._src = ''; this._label = ''; }
connectedCallback() { if (!this.hasAttribute('hidden')) this.setAttribute('hidden', ''); this.render(); }
@@ -14,7 +16,7 @@ export class PropViewer extends HTMLElement {
render() {
this.innerHTML = `
<div class="prop-overlay" data-role="overlay">
<img class="prop-img" data-role="prop-img" src="${this._src}" alt="${this._label}" />
<img class="prop-img" data-role="prop-img" src="${escapeHtml(this._src)}" alt="${escapeHtml(this._label)}" />
</div>`;
this.querySelector('[data-role=overlay]').addEventListener('click', () => this.dismiss());
}
+8
View File
@@ -0,0 +1,8 @@
export function escapeHtml(s) {
return String(s ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
+7
View File
@@ -26,4 +26,11 @@ describe('<prop-viewer>', () => {
el.querySelector('[data-role=overlay]').click();
expect(el.isOpen()).toBe(false);
});
it('escapes a label containing a quote (no attribute break)', () => {
el.show('/art/x.png', 'x" onerror="boom');
const img = el.querySelector('[data-role=prop-img]');
expect(img.getAttribute('alt')).toBe('x" onerror="boom');
expect(img.hasAttribute('onerror')).toBe(false);
});
});
+16
View File
@@ -0,0 +1,16 @@
import { describe, it, expect } from 'vitest';
import { escapeHtml } from '../../src/core/escape-html.js';
describe('escapeHtml', () => {
it('escapes the five HTML-significant characters', () => {
expect(escapeHtml(`a & b < c > d " e ' f`)).toBe('a &amp; b &lt; c &gt; d &quot; e &#39; f');
});
it('replaces & first so entities are not double-escaped', () => {
expect(escapeHtml('<')).toBe('&lt;');
expect(escapeHtml('&lt;')).toBe('&amp;lt;');
});
it('coerces null/undefined to empty string', () => {
expect(escapeHtml(null)).toBe('');
expect(escapeHtml(undefined)).toBe('');
});
});