From 68543d30a49d9f20fc9e716b8da05c8840777960 Mon Sep 17 00:00:00 2001 From: Andrew Seier Date: Sat, 23 Nov 2024 18:35:28 -0800 Subject: [PATCH] =?UTF-8?q?Introduce=20=E2=80=9CUnforgivingHtml=E2=80=9D?= =?UTF-8?q?=20parser.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Goals of the parser: * Tighten control over things like double-quotes & closing tags. * Improve error messaging for malformed markup. * Improve performance. --- test/test-template-engine.js | 150 +++--- ts/x-template.d.ts.map | 2 +- x-template.js | 987 ++++++++++++++++++++++++++++++++++- 3 files changed, 1048 insertions(+), 91 deletions(-) diff --git a/test/test-template-engine.js b/test/test-template-engine.js index d257935..7d31c60 100644 --- a/test/test-template-engine.js +++ b/test/test-template-engine.js @@ -19,6 +19,7 @@ const localMessages = [ 'Deprecated "unsafeSVG" from default templating engine interface.', 'Deprecated "repeat" from default templating engine interface.', 'Deprecated "map" from default templating engine interface.', + 'Support for the "style" tag is deprecated and will be removed in future versions.', ]; console.warn = (...args) => { // eslint-disable-line no-console if (!localMessages.includes(args[0]?.message)) { @@ -80,6 +81,20 @@ describe('html rendering', () => { assert(container.children[0].getAttribute('foo') === `--{<&>'"}--`); }); + it('renders named html entities which require surrogate pairs', () => { + const container = document.createElement('div'); + render(container, html`
--𝕓𝕓--𝕓--
`); + assert(container.childElementCount === 1); + assert(container.children[0].textContent === `--\uD835\uDD53\uD835\uDD53--\uD835\uDD53--`); + }); + + it('renders malformed, named html entities', () => { + const container = document.createElement('div'); + render(container, html`
--&:^);--
`); + assert(container.childElementCount === 1); + assert(container.children[0].textContent === `--&:^);--`); + }); + it('renders surprisingly-accepted characters in text', () => { const container = document.createElement('div'); render(container, html`>'"&& & &
&`); @@ -654,18 +669,6 @@ describe('html rendering', () => { assert(container.querySelector('textarea').value === 'foo'); }); - it('title elements with no interpolation work', () => { - const container = document.createElement('div'); - render(container, html`<em>this</em> is the “default” value`); - assert(container.querySelector('title').textContent === 'this is the “default” value'); - }); - - it('title elements with strict interpolation work', () => { - const container = document.createElement('div'); - render(container, html`${'foo'}`); - assert(container.querySelector('title').textContent === 'foo'); - }); - it('renders instantiated elements as dumb text', () => { const getTemplate = ({ element }) => { return html`${element}`; @@ -775,24 +778,12 @@ describe('html rendering', () => { #item = null; set item(value) { updates.push(`outer-${value}`); this.#item = value; } get item() { return this.#item; } - connectedCallback() { - // Prevent property shadowing by deleting before setting on connect. - const item = this.item ?? '???'; - Reflect.deleteProperty(this, 'item'); - Reflect.set(this, 'item', item); - } } customElements.define('test-depth-first-outer', TestDepthFirstOuter); class TestDepthFirstInner extends HTMLElement { #item = null; set item(value) { updates.push(`inner-${value}`); this.#item = value; } get item() { return this.#item; } - connectedCallback() { - // Prevent property shadowing by deleting before setting on connect. - const item = this.item ?? '???'; - Reflect.deleteProperty(this, 'item'); - Reflect.set(this, 'item', item); - } } customElements.define('test-depth-first-inner', TestDepthFirstInner); @@ -1103,17 +1094,6 @@ describe('html errors', () => { assertThrows(callback, expectedMessage); }); - it('throws when attempting to interpolate within a script tag', () => { - const evil = '\' + prompt(\'evil\') + \''; - const callback = () => html` - - `; - const expectedMessage = 'Interpolation of