New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@netflix/x-element

Package Overview
Dependencies
Maintainers
0
Versions
31
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@netflix/x-element - npm Package Compare versions

Comparing version

to
2.0.0-rc.8

test/test-styles.css

2

package.json
{
"name": "@netflix/x-element",
"description": "A dead simple starting point for custom elements.",
"version": "2.0.0-rc.7",
"version": "2.0.0-rc.8",
"license": "Apache-2.0",

@@ -6,0 +6,0 @@ "repository": "github:Netflix/x-element",

@@ -1,3 +0,1 @@

// TODO: The `XParser` interface now requires instantiation.
// This is just kept here as an example alternative to our more “unforgiving”

@@ -7,3 +5,3 @@ // parsing solution. In particular, it could be interesting to try and keep the

// enable us to show performance-testing deltas in the future.
/** Forgiving HTML parser which leverages innerHTML. */
/** Forgiving HTML parser which leverages setHTMLUnsafe. */
export default class Forgiving {

@@ -154,3 +152,3 @@ // Special markers added to markup enabling discovery post-instantiation.

const html = Forgiving.#createHtml(language, strings);
template.innerHTML = html;
template.setHTMLUnsafe(html);
return template.content;

@@ -157,0 +155,0 @@ }

@@ -1,2 +0,2 @@

import { test, coverage } from './x-test.js';
import { test, coverage } from '@netflix/x-test/x-test.js';

@@ -3,0 +3,0 @@ // We import these here so we can see code coverage.

@@ -0,3 +1,3 @@

import { assert, it } from '@netflix/x-test/x-test.js';
import XElement from '../x-element.js';
import { assert, it } from './x-test.js';

@@ -4,0 +4,0 @@ it('properties should not have hyphens (conflicts with attribute names)', () => {

@@ -0,3 +1,3 @@

import { assert, it } from '@netflix/x-test/x-test.js';
import XElement from '../x-element.js';
import { assert, it } from './x-test.js';

@@ -4,0 +4,0 @@ it('errors are thrown in attributeChangedCallback for read-only properties', () => {

@@ -0,3 +1,3 @@

import { assert, it } from '@netflix/x-test/x-test.js';
import XElement from '../x-element.js';
import { assert, it } from './x-test.js';

@@ -4,0 +4,0 @@ class TestElement extends XElement {

@@ -0,3 +1,3 @@

import { it, assert } from '@netflix/x-test/x-test.js';
import XElement from '../x-element.js';
import { it, assert } from './x-test.js';

@@ -4,0 +4,0 @@ let _count = 0;

@@ -0,3 +1,3 @@

import { assert, it } from '@netflix/x-test/x-test.js';
import XElement from '../x-element.js';
import { assert, it } from './x-test.js';

@@ -4,0 +4,0 @@ class TestElementBasic extends XElement {

@@ -1,2 +0,2 @@

import { assert, it } from './x-test.js';
import { assert, it } from '@netflix/x-test/x-test.js';
import XElement from '../x-element.js';

@@ -3,0 +3,0 @@

@@ -0,3 +1,3 @@

import { assert, it } from '@netflix/x-test/x-test.js';
import XElement from '../x-element.js';
import { assert, it } from './x-test.js';

@@ -4,0 +4,0 @@ class TestElementBasic extends XElement {

@@ -0,3 +1,3 @@

import { assert, it } from '@netflix/x-test/x-test.js';
import XElement from '../x-element.js';
import { assert, it } from './x-test.js';

@@ -4,0 +4,0 @@ it('errors are thrown in connectedCallback for initializing values with bad types', () => {

@@ -0,3 +1,3 @@

import { assert, it } from '@netflix/x-test/x-test.js';
import XElement from '../x-element.js';
import { assert, it } from './x-test.js';

@@ -4,0 +4,0 @@ class TestElement extends XElement {

@@ -0,3 +1,3 @@

import { assert, it } from '@netflix/x-test/x-test.js';
import XElement from '../x-element.js';
import { assert, it } from './x-test.js';

@@ -4,0 +4,0 @@ class TestElementChild extends HTMLElement {

@@ -0,3 +1,3 @@

import { assert, it } from '@netflix/x-test/x-test.js';
import XElement from '../x-element.js';
import { assert, it } from './x-test.js';

@@ -4,0 +4,0 @@ class TestElement extends XElement {

@@ -0,3 +1,3 @@

import { assert, it } from '@netflix/x-test/x-test.js';
import XElement from '../x-element.js';
import { assert, it } from './x-test.js';

@@ -4,0 +4,0 @@ class TestElement extends XElement {

@@ -0,3 +1,3 @@

import { assert, it } from '@netflix/x-test/x-test.js';
import XElement from '../x-element.js';
import { it, assert } from './x-test.js';

@@ -4,0 +4,0 @@ class TestElement extends XElement {

@@ -0,3 +1,3 @@

import { assert, it } from '@netflix/x-test/x-test.js';
import XElement from '../x-element.js';
import { assert, it } from './x-test.js';

@@ -4,0 +4,0 @@ class TestElement1 extends XElement {

@@ -0,3 +1,3 @@

import { assert, it } from '@netflix/x-test/x-test.js';
import XElement from '../x-element.js';
import { assert, it } from './x-test.js';

@@ -4,0 +4,0 @@ class TestElement extends XElement {

@@ -0,3 +1,3 @@

import { assert, it } from '@netflix/x-test/x-test.js';
import XElement from '../x-element.js';
import { assert, it } from './x-test.js';

@@ -4,0 +4,0 @@ class TestElement extends XElement {

@@ -1,3 +0,3 @@

import { assert, it } from './x-test.js';
import styleSheet from './test-styles.css.js';
import { assert, it } from '@netflix/x-test/x-test.js';
import styleSheet from './test-styles.css' with { type: 'css' };
import XElement from '../x-element.js';

@@ -4,0 +4,0 @@

@@ -0,3 +1,3 @@

import { assert, describe, it } from '@netflix/x-test/x-test.js';
import XElement from '../x-element.js';
import { assert, describe, it } from './x-test.js';

@@ -56,2 +56,40 @@ // Long-term interface.

it('renders comments', () => {
const container = document.createElement('div');
render(container, html`<!--This is HTML: "&ldquo;<div></div>&rdquo;"-->`);
assert(container.childNodes.length === 1);
// Note that character references are _not_ replaced — this is _just text_.
assert(container.childNodes[0].textContent === 'This is HTML: "&ldquo;<div></div>&rdquo;"');
});
it('renders void tags', () => {
const container = document.createElement('div');
render(container, html`<input><br><input>`);
assert(container.childNodes.length === 3);
assert(container.childNodes[0].localName === 'input');
assert(container.childNodes[1].localName === 'br');
assert(container.childNodes[2].localName === 'input');
});
it('renders template elements', () => {
// It’s important that the _content_ is populated here. Not the template.
const container = document.createElement('div');
render(container, html`<template><div><p></p></div></template>`);
assert(!!container.querySelector('template'));
assert(container.querySelector('template').content.childElementCount === 1);
assert(container.querySelector('template').content.children[0].childElementCount === 1);
assert(container.querySelector('template').content.children[0].children[0].localName === 'p');
});
it('renders pre elements with optional, initial newline', () => {
const expected = ' hi\n ';
const container = document.createElement('div');
render(container, html`
<pre>
<span>hi</span>
</pre>
`);
assert(container.querySelector('pre').textContent === expected); // first newline is removed
});
it('renders named html entities in text content', () => {

@@ -64,2 +102,26 @@ const container = document.createElement('div');

it('renders direct references in replaceable character data', () => {
const container = document.createElement('div');
render(container, html`&amp;&lt;&gt;&quot;&apos;`);
assert(container.textContent === `&<>"'`);
});
it('renders references for commonly-used characters', () => {
const container = document.createElement('div');
render(container, html`&nbsp;&lsquo;&rsquo;&ldquo;&rdquo;&ndash;&mdash;&hellip;&bull;&middot;&dagger;`);
assert(container.textContent === '\u00A0\u2018\u2019\u201C\u201D\u2013\u2014\u2026\u2022\u00B7\u2020');
});
it('renders named references which require surrogate pairs', () => {
const container = document.createElement('div');
render(container, html`<div>--&bopf;&bopf;--&bopf;--</div>`);
assert(container.children[0].textContent === `--\uD835\uDD53\uD835\uDD53--\uD835\uDD53--`);
});
it('leaves malformed references as-is', () => {
const container = document.createElement('div');
render(container, html`<div>--&:^);--</div>`);
assert(container.children[0].textContent === `--&:^);--`);
});
it('renders interpolated content without parsing', () => {

@@ -230,2 +292,9 @@ const userContent = '<a href="https://evil.com">Click Me!</a>';

it('renders references in attribute values', () => {
const container = document.createElement('div');
render(container, html`<div foo="--&#123;&lt;&amp;&gt;&apos;&quot;&#x007D;--"></div>`);
assert(container.childElementCount === 1);
assert(container.children[0].getAttribute('foo') === `--{<&>'"}--`);
});
it('renders boolean attributes', () => {

@@ -296,2 +365,7 @@ const getTemplate = ({ attr }) => {

container.firstElementChild.click();
// Because attributes have _replaceable_ content, the “&hellip;” should be
// immediately replaced and injected as the actual character “…” within the
// to-be-evaluated JS script. Because the “\\u2026” is escaped, it passes
// validation. Finally, because this is valid HTML text, it ought to
// highlight correctly in an IDE (you have to just confirm that visually).
assert(container.firstElementChild.textContent = '…hi…');

@@ -480,7 +554,7 @@ container.remove();

const template = document.createElement('template');
template.innerHTML = '<input>';
template.setHTMLUnsafe('<input>');
render(container, getTemplate({ fragment: template.content.cloneNode(true) }));
assert(container.childElementCount === 1);
assert(container.children[0].localName === 'input');
template.innerHTML = '<textarea></textarea>';
template.setHTMLUnsafe('<textarea></textarea>');
render(container, getTemplate({ fragment: template.content.cloneNode(true) }));

@@ -1199,38 +1273,2 @@ assert(container.childElementCount === 1);

});
it('throws if used with "content"', () => {
const expected = 'The ifDefined update must be used on an attribute, not on content.';
const getTemplate = ({ maybe }) => {
return html`<div id="target">${ifDefined(maybe)}</div>`;
};
const container = document.createElement('div');
document.body.append(container);
let actual;
try {
render(container, getTemplate({ maybe: 'yes' }));
} catch (error) {
actual = error.message;
}
assert(!!actual, 'No error was thrown.');
assert(actual === expected, actual);
container.remove();
});
it('throws if used with "text"', () => {
const expected = 'The ifDefined update must be used on an attribute, not on text content.';
const getTemplate = ({ maybe }) => {
return html`<textarea id="target">${ifDefined(maybe)}</textarea>`;
};
const container = document.createElement('div');
document.body.append(container);
let actual;
try {
render(container, getTemplate({ maybe: 'yes' }));
} catch (error) {
actual = error.message;
}
assert(!!actual, 'No error was thrown.');
assert(actual === expected, actual);
container.remove();
});
});

@@ -1237,0 +1275,0 @@

/** Strict HTML parser meant to handle interpolated HTML. */
export class XParser {
static "__#1@#errorContextKey": symbol;
static "__#1@#delimiter": string;
static "__#1@#voidHtmlElements": Set<string>;
static "__#1@#htmlElements": Set<string>;
static "__#1@#deniedHtmlElements": Set<string>;
static "__#1@#allowedHtmlElements": Set<string>;
static "__#1@#initial": RegExp;
static "__#1@#boundContent": RegExp;
static "__#1@#text": RegExp;
static "__#1@#comment": RegExp;
static "__#1@#startTagOpen": RegExp;
static "__#1@#endTag": RegExp;
static "__#1@#startTagClose": RegExp;
static "__#1@#startTagSpace": RegExp;
static "__#1@#boolean": RegExp;
static "__#1@#attribute": RegExp;
static "__#1@#boundBoolean": RegExp;
static "__#1@#boundDefined": RegExp;
static "__#1@#boundAttribute": RegExp;
static "__#1@#boundProperty": RegExp;
static "__#1@#danglingQuote": RegExp;
static "__#1@#throughTextarea": RegExp;
static "__#1@#rawJsEscape": RegExp;
static "__#1@#entity": RegExp;
static "__#1@#htmlEntityStart": RegExp;
static "__#1@#cdataStart": RegExp;
static "__#1@#startTagOpenMalformed": RegExp;
static "__#1@#startTagSpaceMalformed": RegExp;
static "__#1@#startTagCloseMalformed": RegExp;
static "__#1@#endTagMalformed": RegExp;
static "__#1@#booleanMalformed": RegExp;
static "__#1@#attributeMalformed": RegExp;
static "__#1@#boundBooleanMalformed": RegExp;
static "__#1@#boundDefinedMalformed": RegExp;
static "__#1@#boundAttributeMalformed": RegExp;
static "__#1@#boundPropertyMalformed": RegExp;
static "__#1@#danglingQuoteMalformed": RegExp;
static "__#1@#errorMessages": Map<string, string>;
static "__#1@#valueToErrorMessagesKey": Map<RegExp, string>;
static "__#1@#valueMalformedToErrorMessagesKey": Map<RegExp, string>;
static "__#1@#valueForbiddenToErrorMessagesKey": Map<RegExp, string>;
static "__#1@#namedErrorsToErrorMessagesKey": Map<string, string>;
static "__#1@#try"(string: any, stringIndex: any, ...values: any[]): any;
static "__#1@#forbiddenTransition"(string: any, stringIndex: any, value: any): any;
static "__#1@#invalidTransition"(string: any, stringIndex: any, value: any): any;
static "__#1@#validTransition"(string: any, stringIndex: any, value: any): any;
static "__#1@#getErrorInfo"(strings: any, stringsIndex: any, string: any, stringIndex: any): {
parsed: any;
notParsed: string;
};
static "__#1@#throwTransitionError"(strings: any, stringsIndex: any, string: any, stringIndex: any, value: any): void;
static "__#1@#validateRawString"(rawString: any): void;
static "__#1@#validateExit"(tagName: any): void;
static "__#1@#sendInnerTextTokens"(onToken: any, string: any, index: any, start: any, end: any, plaintextType: any, referenceType: any): void;
static "__#1@#validateTagName"(tagName: any): void;
static "__#1@#validateNoDeclarativeShadowRoots"(tagName: any, attributeName: any): void;
static "__#1@#sendBoundTextTokens"(onToken: any, stringsIndex: any, string: any, stringIndex: any, sloppyStartInterpolation: any): void;
static "__#1@#sendBoundContentTokens"(onToken: any, stringsIndex: any, string: any, stringIndex: any): void;
static "__#1@#sendTextTokens"(onToken: any, stringsIndex: any, string: any, stringIndex: any, nextStringIndex: any): void;
static "__#1@#sendCommentTokens"(onToken: any, stringsIndex: any, string: any, stringIndex: any, nextStringIndex: any): void;
static "__#1@#sendStartTagOpenTokens"(onToken: any, stringsIndex: any, string: any, stringIndex: any, nextStringIndex: any): any;
static "__#1@#sendStartTagSpaceTokens"(onToken: any, stringsIndex: any, string: any, stringIndex: any, nextStringIndex: any): void;
static "__#1@#sendDanglingQuoteTokens"(onToken: any, stringsIndex: any, string: any, stringIndex: any, nextStringIndex: any): void;
static "__#1@#sendBooleanTokens"(onToken: any, tagName: any, stringsIndex: any, string: any, stringIndex: any, nextStringIndex: any): void;
static "__#1@#sendAttributeTokens"(onToken: any, tagName: any, stringsIndex: any, string: any, stringIndex: any, nextStringIndex: any): void;
static "__#1@#sendBoundBooleanTokens"(onToken: any, stringsIndex: any, string: any, stringIndex: any, nextStringIndex: any): void;
static "__#1@#sendBoundDefinedTokens"(onToken: any, stringsIndex: any, string: any, stringIndex: any, nextStringIndex: any): void;
static "__#1@#sendBoundAttributeTokens"(onToken: any, stringsIndex: any, string: any, stringIndex: any, nextStringIndex: any): void;
static "__#1@#sendBoundPropertyTokens"(onToken: any, stringsIndex: any, string: any, stringIndex: any, nextStringIndex: any): void;
static "__#1@#sendVoidElementTokens"(onToken: any, stringsIndex: any, stringIndex: any, nextStringIndex: any): void;
static "__#1@#sendTextareaTokens"(onToken: any, stringsIndex: any, string: any, stringIndex: any, nextStringIndex: any): number;
static "__#1@#sendStartTagCloseTokens"(onToken: any, stringsIndex: any, stringIndex: any, nextStringIndex: any): void;
static "__#1@#sendEndTagTokens"(onToken: any, tagName: any, strings: any, stringsIndex: any, string: any, stringIndex: any, nextStringIndex: any): void;
static tokenTypes: {
startTagOpen: string;
startTagSpace: string;
startTagEquals: string;
startTagQuote: string;
startTagClose: string;
voidTagClose: string;
boundBooleanPrefix: string;
boundDefinedPrefix: string;
boundPropertyPrefix: string;
endTagOpen: string;
endTagClose: string;
commentOpen: string;
commentClose: string;
startTagName: string;
endTagName: string;
comment: string;
attributeName: string;
booleanName: string;
textStart: string;
textReference: string;
textPlaintext: string;
textEnd: string;
attributeValueStart: string;
attributeValueReference: string;
attributeValuePlaintext: string;
attributeValueEnd: string;
boundAttributeName: string;
boundBooleanName: string;
boundDefinedName: string;
boundPropertyName: string;
boundTextValue: string;
boundContentValue: string;
boundAttributeValue: string;
boundBooleanValue: string;
boundDefinedValue: string;
boundPropertyValue: string;
};
/**

@@ -22,64 +132,18 @@ * Additional error context.

/**
* Instantiation options.
* @typedef {object} XParserOptions
* @property {window} [window]
* Main parsing callback.
* @callback onToken
* @param {string} type
* @param {number} index
* @param {number} start
* @param {number} end
* @param {string} substring
*/
/**
* Creates an XParser instance. Mock the “window” for validation-only usage.
* @param {XParserOptions} [options]
*/
constructor(options?: {
window?: Window & typeof globalThis;
});
/**
* The onBoolean callback.
* @callback onBoolean
* @param {string} attributeName
* @param {number[]} path
*/
/**
* The onDefined callback.
* @callback onDefined
* @param {string} attributeName
* @param {number[]} path
*/
/**
* The onAttribute callback.
* @callback onAttribute
* @param {string} attributeName
* @param {number[]} path
*/
/**
* The onProperty callback.
* @callback onProperty
* @param {string} propertyName
* @param {number[]} path
*/
/**
* The onContent callback.
* @callback onContent
* @param {number[]} path
*/
/**
* The onText callback.
* @callback onText
* @param {number[]} path
*/
/**
* The core parse function takes in the “strings” from a tagged template
* function and returns a document fragment. The “on*” callbacks are an
* optimization to allow a subscriber to store future lookups without
* needing to re-walk the resulting document fragment.
* @param {TemplateStringsArray} strings
* @param {onBoolean} onBoolean
* @param {onDefined} onDefined
* @param {onAttribute} onAttribute
* @param {onProperty} onProperty
* @param {onContent} onContent
* @param {onText} onText
* @returns {DocumentFragment}
* function and returns an array of tokens representing the parsed result.
* @param {*} strings
* @param {onToken} onToken
*/
parse(strings: TemplateStringsArray, onBoolean: (attributeName: string, path: number[]) => any, onDefined: (attributeName: string, path: number[]) => any, onAttribute: (attributeName: string, path: number[]) => any, onProperty: (propertyName: string, path: number[]) => any, onContent: (path: number[]) => any, onText: (path: number[]) => any): DocumentFragment;
#private;
static parse(strings: any, onToken: (type: string, index: number, start: number, end: number, substring: string) => any): void;
}
//# sourceMappingURL=x-parser.d.ts.map

@@ -6,18 +6,4 @@ /** Strict HTML parser meant to handle interpolated HTML. */

// Integrators may mock global window object (e.g., for eslint validation).
#window = null;
// It’s more performant to clone a single fragment, so we keep a reference.
#fragment = null;
// We decode character references via “setHTMLUnsafe” on this container.
#htmlEntityContainer = null;
// DOM introspection is expensive. Since we are creating all of the elements,
// we can cache the introspections we need behind performant lookups.
#localName = Symbol();
#parentNode = Symbol();
// Delimiter we add to improve debugging. E.g., `<div id="${…}"></div>`.
#delimiter = '${\u2026}';
static #delimiter = '${\u2026}';

@@ -29,3 +15,3 @@ //////////////////////////////////////////////////////////////////////////////

// Void tags - https://developer.mozilla.org/en-US/docs/Glossary/Void_element
#voidHtmlElements = new Set([
static #voidHtmlElements = new Set([
'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input',

@@ -35,3 +21,3 @@ 'keygen', 'link', 'meta', 'source', 'track', 'wbr',

#htmlElements = new Set([
static #htmlElements = new Set([
// Main Root

@@ -78,3 +64,3 @@ 'html',

]);
#deniedHtmlElements = new Set([
static #deniedHtmlElements = new Set([
'html', 'head', 'base', 'link', 'meta', 'title', 'style', 'body', 'script',

@@ -86,3 +72,3 @@ 'noscript', 'canvas', 'acronym', 'big', 'center', 'content', 'dir', 'font',

]);
#allowedHtmlElements = this.#htmlElements.difference(this.#deniedHtmlElements);
static #allowedHtmlElements = XParser.#htmlElements.difference(XParser.#deniedHtmlElements);

@@ -100,12 +86,12 @@ //////////////////////////////////////////////////////////////////////////////

// patterns below are intentionally unmatchable.
#initial = /\b\B/y;
#boundContent = /\b\B/y;
static #initial = /\b\B/y;
static #boundContent = /\b\B/y;
// Our unbound content rules follow the “normal character data” spec.
// Our text rules follow the “normal character data” spec.
// https://w3c.github.io/html-reference/syntax.html#normal-character-data
#unboundContent = /[^<]+/y;
static #text = /[^<]+/y;
// Our comment rules follow the “comments” spec.
// https://w3c.github.io/html-reference/syntax.html#comments
#unboundComment = /<!--.*?-->/ys;
static #comment = /<!--.*?-->/ys;

@@ -122,5 +108,5 @@ // Our tag name rules are more restrictive than the “tag name” spec.

// - not ok: <-div>, <1-my-element>
#openTagStart = /<(?![0-9-])[a-z0-9-]+(?<!-)(?=[\s\n>])/y;
#closeTag = /<\/(?![0-9-])[a-z0-9-]+(?<!-)>/y;
#openTagEnd = /(?<![\s\n])>/y;
static #startTagOpen = /<(?![0-9-])[a-z0-9-]+(?<!-)(?=[\s\n>])/y;
static #endTag = /<\/(?![0-9-])[a-z0-9-]+(?<!-)>/y;
static #startTagClose = /(?<![\s\n])>/y;

@@ -137,3 +123,3 @@ // TODO: Check on performance for this pattern. We want to do a positive

// - not ok: <div foo bar>, <div\n\n foo\n\n bar>, <div\tfoo\tbar>
#openTagSpace = / (?! )|\n *(?!\n)(?=[-_.?a-zA-Z0-9>])/y;
static #startTagSpace = / (?! )|\n *(?!\n)(?=[-_.?a-zA-Z0-9>])/y;

@@ -154,7 +140,7 @@ // Our attribute rules are more restrictive than the “attribute” spec.

// - not ok: foo='bar', ?foo, foo=${'bar'}
#unboundBoolean = /(?![0-9-])[a-z0-9-]+(?<!-)(?=[\s\n>])/y;
#unboundAttribute = /(?![0-9-])[a-z0-9-]+(?<!-)="[^"]*"(?=[\s\n>])/y;
#boundBoolean = /\?(?![0-9-])[a-z0-9-]+(?<!-)="$/y;
#boundDefined = /\?\?(?![0-9-])[a-z0-9-]+(?<!-)="$/y;
#boundAttribute = /(?![0-9-])[a-z0-9-]+(?<!-)="$/y;
static #boolean = /(?![0-9-])[a-z0-9-]+(?<!-)(?=[\s\n>])/y;
static #attribute = /(?![0-9-])[a-z0-9-]+(?<!-)="[^"]*"(?=[\s\n>])/y;
static #boundBoolean = /\?(?![0-9-])[a-z0-9-]+(?<!-)="$/y;
static #boundDefined = /\?\?(?![0-9-])[a-z0-9-]+(?<!-)="$/y;
static #boundAttribute = /(?![0-9-])[a-z0-9-]+(?<!-)="$/y;

@@ -174,3 +160,3 @@ // There is no concept of a property binding in the HTML specification, but

// - not ok: .foo='${'bar'}', .foo="bar"
#boundProperty = /\.(?![A-Z0-9_])[a-zA-Z0-9_]+(?<!_)="$/y;
static #boundProperty = /\.(?![A-Z0-9_])[a-zA-Z0-9_]+(?<!_)="$/y;

@@ -182,3 +168,3 @@ // We require that values bound to attributes and properties be enclosed

// angle bracket of the opening tag.
#danglingQuote = /"(?=[ \n>])/y;
static #danglingQuote = /"(?=[ \n>])/y;

@@ -194,3 +180,3 @@ //////////////////////////////////////////////////////////////////////////////

// https://w3c.github.io/html-reference/syntax.html#replaceable-character-data
#throughTextarea = /.*?<\/textarea>/ys;
static #throughTextarea = /.*?<\/textarea>/ys;

@@ -215,3 +201,3 @@ //////////////////////////////////////////////////////////////////////////////

// - not ok: html`\nhi\nthere`, html`\x8230`, html`\u2026`, html`\s\t\o\p\ \i\t\.`
#rawJsEscape = /.*(?<!\\)(?:\\{2})*\\(?![$\\`])/ys;
static #rawJsEscape = /.*(?<!\\)(?:\\{2})*\\(?![$\\`])/ys;

@@ -232,4 +218,4 @@ //////////////////////////////////////////////////////////////////////////////

// https://w3c.github.io/html-reference/syntax.html#character-encoding
#entity = /&.*?;/ys;
#htmlEntityStart = /[^&]*&[^&\s\n<]/y;
static #entity = /&.*?;/ys;
static #htmlEntityStart = /[^&]*&[^&\s\n<]/y;

@@ -247,3 +233,3 @@ //////////////////////////////////////////////////////////////////////////////

// … we make an opinion that authors should just use the latter.
#cdataStart = /<!\[CDATA\[/y;
static #cdataStart = /<!\[CDATA\[/y;

@@ -256,21 +242,21 @@ //////////////////////////////////////////////////////////////////////////////

// open or close tags.
#openTagStartMalformed = /<[\s\n]*[a-zA-Z0-9_-]+/y;
#openTagSpaceMalformed = /[\s\n]+/y;
#openTagEndMalformed = /[\s\n]*\/?>/y;
#closeTagMalformed = /<[\s\n]*\/[\s\n]*[a-zA-Z0-9_-]+[^>]*>/y;
static #startTagOpenMalformed = /<[\s\n]*[a-zA-Z0-9_-]+/y;
static #startTagSpaceMalformed = /[\s\n]+/y;
static #startTagCloseMalformed = /[\s\n]*\/?>/y;
static #endTagMalformed = /<[\s\n]*\/[\s\n]*[a-zA-Z0-9_-]+[^>]*>/y;
// See if incorrect characters, wrong quotes, or no quotes were used with
// either unbound or bound attributes.
#unboundBooleanMalformed = /[a-zA-Z0-9-_]+(?=[\s\n>])/y;
#unboundAttributeMalformed = /[a-zA-Z0-9-_]+=(?:"[^"]*"|'[^']*')?(?=[\s\n>])/y;
#boundBooleanMalformed = /\?[a-zA-Z0-9-_]+=(?:"|')?$/y;
#boundDefinedMalformed = /\?\?[a-zA-Z0-9-_]+=(?:"|')?$/y;
#boundAttributeMalformed = /[a-zA-Z0-9-_]+=(?:"|')?$/y;
// either normal or bound attributes.
static #booleanMalformed = /[a-zA-Z0-9-_]+(?=[\s\n>])/y;
static #attributeMalformed = /[a-zA-Z0-9-_]+=(?:"[^"]*"|'[^']*')?(?=[\s\n>])/y;
static #boundBooleanMalformed = /\?[a-zA-Z0-9-_]+=(?:"|')?$/y;
static #boundDefinedMalformed = /\?\?[a-zA-Z0-9-_]+=(?:"|')?$/y;
static #boundAttributeMalformed = /[a-zA-Z0-9-_]+=(?:"|')?$/y;
// See if incorrect characters, wrong quotes, or no quotes were used with
// a bound property.
#boundPropertyMalformed = /\.[a-zA-Z0-9-_]+=(?:"|')?$/y;
static #boundPropertyMalformed = /\.[a-zA-Z0-9-_]+=(?:"|')?$/y;
// See if the quote pair was malformed or missing.
#danglingQuoteMalformed = /'?(?=[\s\n>])/y;
static #danglingQuoteMalformed = /'?(?=[\s\n>])/y;

@@ -283,3 +269,3 @@ //////////////////////////////////////////////////////////////////////////////

// parsing errors are allotted numbers #100-#199.
#errorMessages = new Map([
static #errorMessages = new Map([
['#100', 'Markup at the start of your template could not be parsed.'],

@@ -326,42 +312,42 @@ ['#101', 'Markup after content text found in your template could not be parsed.'],

// Block #100-#119 — Invalid transition errors.
#valueToErrorMessagesKey = new Map([
[this.#initial, '#100'],
[this.#unboundContent, '#101'],
[this.#unboundComment, '#102'],
[this.#boundContent, '#103'],
[this.#openTagStart, '#104'],
[this.#openTagSpace, '#105'],
[this.#openTagEnd, '#106'],
[this.#unboundBoolean, '#107'],
[this.#unboundAttribute, '#108'],
[this.#boundBoolean, '#109'],
[this.#boundDefined, '#110'],
[this.#boundAttribute, '#111'],
[this.#boundProperty, '#112'],
[this.#danglingQuote, '#113'],
[this.#closeTag, '#114'],
static #valueToErrorMessagesKey = new Map([
[XParser.#initial, '#100'],
[XParser.#text, '#101'],
[XParser.#comment, '#102'],
[XParser.#boundContent, '#103'],
[XParser.#startTagOpen, '#104'],
[XParser.#startTagSpace, '#105'],
[XParser.#startTagClose, '#106'],
[XParser.#boolean, '#107'],
[XParser.#attribute, '#108'],
[XParser.#boundBoolean, '#109'],
[XParser.#boundDefined, '#110'],
[XParser.#boundAttribute, '#111'],
[XParser.#boundProperty, '#112'],
[XParser.#danglingQuote, '#113'],
[XParser.#endTag, '#114'],
]);
// Block #120-#139 — Common mistakes.
#valueMalformedToErrorMessagesKey = new Map([
[this.#openTagStartMalformed, '#120'],
[this.#openTagSpaceMalformed, '#121'],
[this.#openTagEndMalformed, '#122'],
[this.#closeTagMalformed, '#123'],
[this.#unboundBooleanMalformed, '#124'],
[this.#unboundAttributeMalformed, '#125'],
[this.#boundBooleanMalformed, '#126'],
[this.#boundDefinedMalformed, '#127'],
[this.#boundAttributeMalformed, '#128'],
[this.#boundPropertyMalformed, '#129'],
[this.#danglingQuoteMalformed, '#130'],
static #valueMalformedToErrorMessagesKey = new Map([
[XParser.#startTagOpenMalformed, '#120'],
[XParser.#startTagSpaceMalformed, '#121'],
[XParser.#startTagCloseMalformed, '#122'],
[XParser.#endTagMalformed, '#123'],
[XParser.#booleanMalformed, '#124'],
[XParser.#attributeMalformed, '#125'],
[XParser.#boundBooleanMalformed, '#126'],
[XParser.#boundDefinedMalformed, '#127'],
[XParser.#boundAttributeMalformed, '#128'],
[XParser.#boundPropertyMalformed, '#129'],
[XParser.#danglingQuoteMalformed, '#130'],
]);
// Block #140-#149 — Forbidden transitions.
#valueForbiddenToErrorMessagesKey = new Map([
[this.#cdataStart, '#140'],
static #valueForbiddenToErrorMessagesKey = new Map([
[XParser.#cdataStart, '#140'],
]);
// Block #150+ — Special, named issues.
#namedErrorsToErrorMessagesKey = new Map([
static #namedErrorsToErrorMessagesKey = new Map([
['javascript-escape', '#150'],

@@ -382,3 +368,3 @@ ['malformed-html-entity', '#151'],

// Returns the first valid state-machine transition (if one exists).
#try(string, stringIndex, ...values) {
static #try(string, stringIndex, ...values) {
for (const value of values) {

@@ -394,10 +380,10 @@ value.lastIndex = stringIndex;

// versions of valid transitions.
#forbiddenTransition(string, stringIndex, value) {
static #forbiddenTransition(string, stringIndex, value) {
switch (value) {
case this.#initial:
case this.#boundContent:
case this.#unboundContent:
case this.#openTagEnd:
case this.#closeTag: return this.#try(string, stringIndex,
this.#cdataStart);
case XParser.#initial:
case XParser.#boundContent:
case XParser.#text:
case XParser.#startTagClose:
case XParser.#endTag: return XParser.#try(string, stringIndex,
XParser.#cdataStart);
}

@@ -407,39 +393,39 @@ }

// This should roughly match our “valid” transition mapping, but for errors.
#invalidTransition(string, stringIndex, value) {
static #invalidTransition(string, stringIndex, value) {
switch (value) {
case this.#initial: return this.#try(string, stringIndex,
this.#openTagStartMalformed);
case this.#unboundContent: return this.#try(string, stringIndex,
this.#closeTagMalformed,
this.#openTagStartMalformed);
case this.#boundContent: return this.#try(string, stringIndex,
this.#closeTagMalformed,
this.#openTagStartMalformed);
case this.#unboundComment: return this.#try(string, stringIndex,
this.#closeTagMalformed,
this.#openTagStartMalformed);
case this.#openTagStart:
case this.#unboundBoolean:
case this.#unboundAttribute:
case this.#danglingQuote: return this.#try(string, stringIndex,
this.#openTagSpaceMalformed);
case this.#openTagSpace: return this.#try(string, stringIndex,
this.#unboundBooleanMalformed,
this.#unboundAttributeMalformed,
this.#boundBooleanMalformed,
this.#boundDefinedMalformed,
this.#boundAttributeMalformed,
this.#boundPropertyMalformed,
this.#openTagEndMalformed);
case this.#openTagEnd: return this.#try(string, stringIndex,
this.#openTagStartMalformed,
this.#closeTagMalformed);
case this.#boundBoolean:
case this.#boundDefined:
case this.#boundAttribute:
case this.#boundProperty: return this.#try(string, stringIndex,
this.#danglingQuoteMalformed);
case this.#closeTag: return this.#try(string, stringIndex,
this.#openTagStartMalformed,
this.#closeTagMalformed);
case XParser.#initial: return XParser.#try(string, stringIndex,
XParser.#startTagOpenMalformed);
case XParser.#text: return XParser.#try(string, stringIndex,
XParser.#endTagMalformed,
XParser.#startTagOpenMalformed);
case XParser.#boundContent: return XParser.#try(string, stringIndex,
XParser.#endTagMalformed,
XParser.#startTagOpenMalformed);
case XParser.#comment: return XParser.#try(string, stringIndex,
XParser.#endTagMalformed,
XParser.#startTagOpenMalformed);
case XParser.#startTagOpen:
case XParser.#boolean:
case XParser.#attribute:
case XParser.#danglingQuote: return XParser.#try(string, stringIndex,
XParser.#startTagSpaceMalformed);
case XParser.#startTagSpace: return XParser.#try(string, stringIndex,
XParser.#booleanMalformed,
XParser.#attributeMalformed,
XParser.#boundBooleanMalformed,
XParser.#boundDefinedMalformed,
XParser.#boundAttributeMalformed,
XParser.#boundPropertyMalformed,
XParser.#startTagCloseMalformed);
case XParser.#startTagClose: return XParser.#try(string, stringIndex,
XParser.#startTagOpenMalformed,
XParser.#endTagMalformed);
case XParser.#boundBoolean:
case XParser.#boundDefined:
case XParser.#boundAttribute:
case XParser.#boundProperty: return XParser.#try(string, stringIndex,
XParser.#danglingQuoteMalformed);
case XParser.#endTag: return XParser.#try(string, stringIndex,
XParser.#startTagOpenMalformed,
XParser.#endTagMalformed);
}

@@ -450,19 +436,18 @@ }

// through a set of html template “strings” array.
#validTransition(string, stringIndex, value) {
static #validTransition(string, stringIndex, value) {
switch (value) {
// The “initial” state is where we start when we begin parsing.
// E.g., html`‸hello world!`
case this.#initial: return this.#try(string, stringIndex,
this.#unboundContent,
this.#openTagStart,
this.#unboundComment);
case XParser.#initial: return XParser.#try(string, stringIndex,
XParser.#text,
XParser.#startTagOpen,
XParser.#comment);
// The “unboundContent” state means that we’ve just parsed through some
// literal html text either in the root of the template or between an
// open / close tag pair.
// The “text” state means that we’ve just parsed through some literal html
// text either in the root of the template or between an start / end tag.
// E.g., html`hello ‸${world}!`
case this.#unboundContent: return this.#try(string, stringIndex,
this.#closeTag,
this.#openTagStart,
this.#unboundComment);
case XParser.#text: return XParser.#try(string, stringIndex,
XParser.#endTag,
XParser.#startTagOpen,
XParser.#comment);

@@ -472,20 +457,20 @@ // The “boundContent” state means that we just hit an interpolation (i.e.,

// E.g., html`hello ${world}‸!`
// The “unboundComment” state means that we just completed a comment. We
// don’t allow comment interpolations.
// The “comment” state means that we just completed a comment. We don’t
// allow comment interpolations.
// E.g., html`hello <!-- todo -->‸ ${world}!`
case this.#boundContent:
case this.#unboundComment: return this.#try(string, stringIndex,
this.#unboundContent,
this.#closeTag,
this.#openTagStart,
this.#unboundComment);
case XParser.#boundContent:
case XParser.#comment: return XParser.#try(string, stringIndex,
XParser.#text,
XParser.#endTag,
XParser.#startTagOpen,
XParser.#comment);
// The “openTagStart” means that we’ve successfully parsed through the
// open angle bracket (“<”) and the tag name.
// The “startTagOpen” means that we’ve successfully parsed through the
// open angle bracket (“<”) and the tag name in a start tag.
// E.g., html`<div‸></div>`
// The “unboundBoolean” means we parsed through a literal boolean
// attribute which doesn’t have an interpolated binding.
// The “boolean” means we parsed through a literal boolean attribute which
// doesn’t have an interpolated binding.
// E.g., html`<div foo‸></div>`
// The “unboundAttribute” means we parsed through a literal key-value
// attribute pair which doesn’t have an interpolated binding.
// The “attribute” means we parsed through a literal key-value attribute
// pair which doesn’t have an interpolated binding.
// E.g., html`<div foo="bar"‸></div>`

@@ -497,28 +482,28 @@ // The “danglingQuote” means we parsed through a prefixing, closing quote

// E.g., html`<div foo="${bar}"‸></div>`
case this.#openTagStart:
case this.#unboundBoolean:
case this.#unboundAttribute:
case this.#danglingQuote: return this.#try(string, stringIndex,
this.#openTagSpace,
this.#openTagEnd);
case XParser.#startTagOpen:
case XParser.#boolean:
case XParser.#attribute:
case XParser.#danglingQuote: return XParser.#try(string, stringIndex,
XParser.#startTagSpace,
XParser.#startTagClose);
// The “openTagSpace” is either one space or a single newline and some
// indentation space after the open tag name, an attribute, or property.
// The “startTagSpace” is either one space or a single newline and some
// indentation space after the start tag name, an attribute, or property.
// E.g., html`<div ‸foo></div>`
case this.#openTagSpace: return this.#try(string, stringIndex,
this.#unboundBoolean,
this.#unboundAttribute,
this.#boundBoolean,
this.#boundDefined,
this.#boundAttribute,
this.#boundProperty,
this.#openTagEnd);
case XParser.#startTagSpace: return XParser.#try(string, stringIndex,
XParser.#boolean,
XParser.#attribute,
XParser.#boundBoolean,
XParser.#boundDefined,
XParser.#boundAttribute,
XParser.#boundProperty,
XParser.#startTagClose);
// The “openTagEnd” is just the “>” character.
// The “startTagClose” is just the “>” character.
// E.g., html`<div>‸</div>`
case this.#openTagEnd: return this.#try(string, stringIndex,
this.#openTagStart,
this.#unboundContent,
this.#closeTag,
this.#unboundComment);
case XParser.#startTagClose: return XParser.#try(string, stringIndex,
XParser.#startTagOpen,
XParser.#text,
XParser.#endTag,
XParser.#comment);

@@ -537,15 +522,15 @@ // The “boundBoolean” state means we just ended our prior string with an

// E.g., html`<div .foo="${bar}‸"></div>`
case this.#boundBoolean:
case this.#boundDefined:
case this.#boundAttribute:
case this.#boundProperty: return this.#try(string, stringIndex,
this.#danglingQuote);
case XParser.#boundBoolean:
case XParser.#boundDefined:
case XParser.#boundAttribute:
case XParser.#boundProperty: return XParser.#try(string, stringIndex,
XParser.#danglingQuote);
// The “closeTag” state means we just closed some tag successfully,
// The “endTag” state means we just found an some end tag successfully,
// E.g., html`<div><span></span>‸</div>`
case this.#closeTag: return this.#try(string, stringIndex,
this.#unboundContent,
this.#openTagStart,
this.#closeTag,
this.#unboundComment);
case XParser.#endTag: return XParser.#try(string, stringIndex,
XParser.#text,
XParser.#startTagOpen,
XParser.#endTag,
XParser.#comment);
}

@@ -556,9 +541,9 @@ }

// helpful error messages to developers.
#getErrorInfo(strings, stringsIndex, string, stringIndex) {
static #getErrorInfo(strings, stringsIndex, string, stringIndex) {
let prefix;
let prefixIndex;
if (stringsIndex > 0) {
const validPrefix = strings.slice(0, stringsIndex).join(this.#delimiter);
prefix = [validPrefix, string].join(this.#delimiter);
prefixIndex = validPrefix.length + this.#delimiter.length + stringIndex;
const validPrefix = strings.slice(0, stringsIndex).join(XParser.#delimiter);
prefix = [validPrefix, string].join(XParser.#delimiter);
prefixIndex = validPrefix.length + XParser.#delimiter.length + stringIndex;
} else {

@@ -579,12 +564,12 @@ prefix = string;

// This would otherwise be non-performant — but we are about to error anyhow.
#throwTransitionError(strings, stringsIndex, string, stringIndex, value) {
const { parsed, notParsed } = this.#getErrorInfo(strings, stringsIndex, string, stringIndex);
const valueForbidden = this.#forbiddenTransition(string, stringIndex, value);
const valueMalformed = this.#invalidTransition(string, stringIndex, value);
static #throwTransitionError(strings, stringsIndex, string, stringIndex, value) {
const { parsed, notParsed } = XParser.#getErrorInfo(strings, stringsIndex, string, stringIndex);
const valueForbidden = XParser.#forbiddenTransition(string, stringIndex, value);
const valueMalformed = XParser.#invalidTransition(string, stringIndex, value);
const errorMessagesKey = valueForbidden
? this.#valueForbiddenToErrorMessagesKey.get(valueForbidden)
? XParser.#valueForbiddenToErrorMessagesKey.get(valueForbidden)
: valueMalformed
? this.#valueMalformedToErrorMessagesKey.get(valueMalformed)
: this.#valueToErrorMessagesKey.get(value);
const errorMessage = this.#errorMessages.get(errorMessagesKey);
? XParser.#valueMalformedToErrorMessagesKey.get(valueMalformed)
: XParser.#valueToErrorMessagesKey.get(value);
const errorMessage = XParser.#errorMessages.get(errorMessagesKey);
const substringMessage = `See substring \`${notParsed}\`.`;

@@ -599,8 +584,8 @@ const parsedThroughMessage = `Your HTML was parsed through: \`${parsed}\`.`;

// not being used as html (since there are perfectly-valid alternatives).
#validateRawString(rawString) {
this.#rawJsEscape.lastIndex = 0;
if (this.#rawJsEscape.test(rawString)) {
const errorMessagesKey = this.#namedErrorsToErrorMessagesKey.get('javascript-escape');
const errorMessage = this.#errorMessages.get(errorMessagesKey);
const substringMessage = `See (raw) substring \`${rawString.slice(0, this.#rawJsEscape.lastIndex)}\`.`;
static #validateRawString(rawString) {
XParser.#rawJsEscape.lastIndex = 0;
if (XParser.#rawJsEscape.test(rawString)) {
const errorMessagesKey = XParser.#namedErrorsToErrorMessagesKey.get('javascript-escape');
const errorMessage = XParser.#errorMessages.get(errorMessagesKey);
const substringMessage = `See (raw) substring \`${rawString.slice(0, XParser.#rawJsEscape.lastIndex)}\`.`;
const message = `[${errorMessagesKey}] ${errorMessage}\n${substringMessage}`;

@@ -613,7 +598,6 @@ throw new Error(message);

// have been matched successfully to prevent any unexpected behavior.
#validateExit(fragment, element) {
if (element.value !== fragment) {
const tagName = element.value[this.#localName];
const errorMessagesKey = this.#namedErrorsToErrorMessagesKey.get('missing-closing-tag');
const errorMessage = this.#errorMessages.get(errorMessagesKey);
static #validateExit(tagName) {
if (tagName) {
const errorMessagesKey = XParser.#namedErrorsToErrorMessagesKey.get('missing-closing-tag');
const errorMessage = XParser.#errorMessages.get(errorMessagesKey);
const substringMessage = `Missing a closing </${tagName}>.`;

@@ -625,96 +609,74 @@ throw new Error(`[${errorMessagesKey}] ${errorMessage}\n${substringMessage}`);

// Certain parts of an html document may contain character references (html
// entities). We find them via a performant pattern, and then replace them
// via a non-performant usage of “setHTMLUnsafe”. This way, the cost is only
// as high as the number of character references used (which is often low).
// entities). We find them via a performant pattern, and then parse them out
// via a non-performant pattern. This way, the cost is only as high as the
// number of character references used (which is often low).
// Note that malformed references or ambiguous ampersands will cause errors.
// https://html.spec.whatwg.org/multipage/named-characters.html
#replaceHtmlEntities(originalContent) {
let content = originalContent;
this.#htmlEntityStart.lastIndex = 0;
while (this.#htmlEntityStart.test(content)) {
const contentIndex = this.#htmlEntityStart.lastIndex - 2;
this.#entity.lastIndex = contentIndex;
if (!this.#entity.test(content)) {
const errorMessagesKey = this.#namedErrorsToErrorMessagesKey.get('malformed-html-entity');
const errorMessage = this.#errorMessages.get(errorMessagesKey);
const substringMessage = `See substring \`${originalContent}\`.`;
// Note that we test against the “content”, but ensure to report tokens as
// compared to the original “string”. This is significantly more performant.
static #sendInnerTextTokens(onToken, string, index, start, end, plaintextType, referenceType) {
const content = string.slice(start, end);
const contentStart = 0;
const contentEnd = content.length;
let plaintextStart = contentStart;
XParser.#htmlEntityStart.lastIndex = plaintextStart;
let referenceEnd = contentEnd;
while (XParser.#htmlEntityStart.test(content)) {
const referenceStart = XParser.#htmlEntityStart.lastIndex - 2;
if (plaintextStart < referenceStart) {
const substringStart = start + plaintextStart;
const substringEnd = start + referenceStart;
const substring = string.slice(substringStart, substringEnd);
onToken(plaintextType, index, substringStart, substringEnd, substring);
}
XParser.#entity.lastIndex = referenceStart;
if (!XParser.#entity.test(content)) {
const errorMessagesKey = XParser.#namedErrorsToErrorMessagesKey.get('malformed-html-entity');
const errorMessage = XParser.#errorMessages.get(errorMessagesKey);
const substringMessage = `See substring \`${content}\`.`;
throw new Error(`[${errorMessagesKey}] ${errorMessage}\n${substringMessage}`);
}
const encoded = content.slice(contentIndex, this.#entity.lastIndex);
this.#htmlEntityContainer.innerHTML = encoded;
const decoded = this.#htmlEntityContainer.content.textContent;
content = content.replace(encoded, decoded);
this.#htmlEntityStart.lastIndex = contentIndex + decoded.length;
referenceEnd = XParser.#entity.lastIndex;
plaintextStart = referenceEnd;
XParser.#htmlEntityStart.lastIndex = plaintextStart;
const substringStart = start + referenceStart;
const substringEnd = start + referenceEnd;
const substring = string.slice(substringStart, substringEnd);
onToken(referenceType, index, substringStart, substringEnd, substring);
}
return content;
}
// Void elements are treated with special consideration as they will never
// contain child nodes.
#finalizeVoidElement(path, element, childNodesIndex, nextStringIndex) {
childNodesIndex.value = path.pop();
element.value = element.value[this.#parentNode];
this.#closeTag.lastIndex = nextStringIndex;
return this.#closeTag;
}
// Textarea contains so-called “replaceable” character data. We throw an error
// if a “complex” interpolation exists — anything other than a perfectly-fit
// content interpolation between the opening and closing tags.
#finalizeTextarea(string, path, element, childNodesIndex, nextStringIndex) {
const closeTagLength = 11; // </textarea>
this.#throughTextarea.lastIndex = nextStringIndex;
if (this.#throughTextarea.test(string)) {
const encoded = string.slice(nextStringIndex, this.#throughTextarea.lastIndex - closeTagLength);
const decoded = this.#replaceHtmlEntities(encoded);
element.value.textContent = decoded;
} else {
const errorMessagesKey = this.#namedErrorsToErrorMessagesKey.get('complex-textarea-interpolation');
const errorMessage = this.#errorMessages.get(errorMessagesKey);
throw new Error(`[${errorMessagesKey}] ${errorMessage}`);
if (plaintextStart === contentStart) {
// There were no references.
onToken(plaintextType, index, start, end, content);
} else if (referenceEnd !== contentEnd) {
// We had references and there was some leftover plaintext.
const substringStart = start + referenceEnd;
const substringEnd = start + contentEnd;
const substring = string.slice(substringStart, substringEnd);
onToken(plaintextType, index, substringStart, substringEnd, substring);
}
childNodesIndex.value = path.pop();
element.value = element.value[this.#parentNode];
this.#closeTag.lastIndex = this.#throughTextarea.lastIndex;
return this.#closeTag;
}
// Unbound content is just literal text in a template string that needs to
// land as text content. We replace any character references (html entities)
// found in the content.
#addUnboundContent(string, stringIndex, element, childNodesIndex, nextStringIndex) {
const encoded = string.slice(stringIndex, nextStringIndex);
const decoded = this.#replaceHtmlEntities(encoded);
element.value.appendChild(this.#window.document.createTextNode(decoded));
childNodesIndex.value += 1;
}
// An unbound comment is just a basic html comment. Comments may not be
// interpolated and follow some specific rules from the html specification.
// https://w3c.github.io/html-reference/syntax.html#comments
#addUnboundComment(string, stringIndex, element, childNodesIndex, nextStringIndex) {
const content = string.slice(stringIndex, nextStringIndex);
const data = content.slice(4, -3);
if (data.startsWith('>') || data.startsWith('->') || data.includes('--') || data.endsWith('-')) {
const errorMessagesKey = this.#namedErrorsToErrorMessagesKey.get('malformed-comment');
const errorMessage = this.#errorMessages.get(errorMessagesKey);
const substringMessage = `See substring \`${content}\`.`;
// In addition to the allow-list of html tag names, any tag with a hyphen in
// the middle is considered a valid custom element. Therefore, we must allow
// for such declarations.
static #validateTagName(tagName) {
if (
tagName.indexOf('-') === -1 &&
!XParser.#allowedHtmlElements.has(tagName)
) {
const errorMessagesKey = XParser.#namedErrorsToErrorMessagesKey.get('forbidden-html-element');
const errorMessage = XParser.#errorMessages.get(errorMessagesKey);
const substringMessage = `The <${tagName}> html element is forbidden.`;
throw new Error(`[${errorMessagesKey}] ${errorMessage}\n${substringMessage}`);
}
element.value.appendChild(this.#window.document.createComment(data));
childNodesIndex.value += 1;
}
// Bound content is simply an interpolation in the template which exists in a
// location destined to be bound as “textContent” on some node. We notify our
// listener about the content binding’s path.
#addBoundContent(onContent, path, element, childNodesIndex) {
element.value.append(
this.#window.document.createComment(''),
this.#window.document.createComment('')
);
childNodesIndex.value += 2;
path.push(childNodesIndex.value);
onContent(path);
path.pop();
// This validates a specific case where we need to reject “template” elements
// which have “declarative shadow roots” via a “shadowrootmode” attribute.
static #validateNoDeclarativeShadowRoots(tagName, attributeName) {
if (tagName === 'template' && attributeName === 'shadowrootmode') {
const errorMessagesKey = XParser.#namedErrorsToErrorMessagesKey.get('declarative-shadow-root');
const errorMessage = XParser.#errorMessages.get(errorMessagesKey);
throw new Error(`[${errorMessagesKey}] ${errorMessage}`);
}
}

@@ -726,3 +688,3 @@

// “textContent” property as a string — no matter the type.
#addBoundText(onText, string, path, sloppyStartInterpolation) {
static #sendBoundTextTokens(onToken, stringsIndex, string, stringIndex, sloppyStartInterpolation) {
// If the prior match isn’t our opening tag… that’s a problem. If the next

@@ -733,35 +695,102 @@ // match isn’t our closing tag… that’s also a problem.

if (sloppyStartInterpolation || !string.startsWith(`</textarea>`)) {
const errorMessagesKey = this.#namedErrorsToErrorMessagesKey.get('complex-textarea-interpolation');
const errorMessage = this.#errorMessages.get(errorMessagesKey);
const errorMessagesKey = XParser.#namedErrorsToErrorMessagesKey.get('complex-textarea-interpolation');
const errorMessage = XParser.#errorMessages.get(errorMessagesKey);
throw new Error(`[${errorMessagesKey}] ${errorMessage}`);
}
onText(path);
onToken(XParser.tokenTypes.boundTextValue, stringsIndex, stringIndex, stringIndex, '');
}
// An unbound boolean is a literal boolean attribute declaration with no
// associated value at all.
#addUnboundBoolean(string, stringIndex, element, nextStringIndex) {
// Bound content is simply an interpolation in the template which exists in a
// location destined to be bound as “textContent” on some node.
static #sendBoundContentTokens(onToken, stringsIndex, string, stringIndex) {
onToken(XParser.tokenTypes.boundContentValue, stringsIndex, stringIndex, stringIndex, '');
}
// This handles literal text in a template that needs to become text content.
static #sendTextTokens(onToken, stringsIndex, string, stringIndex, nextStringIndex) {
onToken(XParser.tokenTypes.textStart, stringsIndex, stringIndex, stringIndex, '');
XParser.#sendInnerTextTokens(onToken, string, stringsIndex, stringIndex, nextStringIndex, XParser.tokenTypes.textPlaintext, XParser.tokenTypes.textReference);
onToken(XParser.tokenTypes.textEnd, stringsIndex, nextStringIndex, nextStringIndex, '');
}
// A comment is just a basic html comment. Comments may not be interpolated
// and follow some specific rules from the html specification. Note that
// character references are not replaced in comments.
// https://w3c.github.io/html-reference/syntax.html#comments
static #sendCommentTokens(onToken, stringsIndex, string, stringIndex, nextStringIndex) {
const commentStart = stringIndex + 4;
const commentEnd = nextStringIndex - 3;
onToken(XParser.tokenTypes.commentOpen, stringsIndex, stringIndex, commentStart, '<!--');
const data = string.slice(commentStart, commentEnd);
if (data.startsWith('>') || data.startsWith('->') || data.includes('--') || data.endsWith('-')) {
const errorMessagesKey = XParser.#namedErrorsToErrorMessagesKey.get('malformed-comment');
const errorMessage = XParser.#errorMessages.get(errorMessagesKey);
const substringMessage = `See substring \`${string.slice(stringIndex, nextStringIndex)}\`.`;
throw new Error(`[${errorMessagesKey}] ${errorMessage}\n${substringMessage}`);
}
onToken(XParser.tokenTypes.comment, stringsIndex, commentStart, commentEnd, data);
onToken(XParser.tokenTypes.commentClose, stringsIndex, commentEnd, nextStringIndex, '-->');
}
// The beginning of a start tag — e.g., “<div”.
static #sendStartTagOpenTokens(onToken, stringsIndex, string, stringIndex, nextStringIndex) {
const tagNameStart = stringIndex + 1;
const tagName = string.slice(tagNameStart, nextStringIndex);
XParser.#validateTagName(tagName);
onToken(XParser.tokenTypes.startTagOpen, stringsIndex, stringIndex, tagNameStart, '<');
onToken(XParser.tokenTypes.startTagName, stringsIndex, tagNameStart, nextStringIndex, tagName);
return tagName;
}
// Simple spaces and newlines withing a start tag.
static #sendStartTagSpaceTokens(onToken, stringsIndex, string, stringIndex, nextStringIndex) {
const substring = string.slice(stringIndex, nextStringIndex);
onToken(XParser.tokenTypes.startTagSpace, stringsIndex, stringIndex, nextStringIndex, substring);
}
// A single double-quote after a binding in a start tag.
static #sendDanglingQuoteTokens(onToken, stringsIndex, string, stringIndex, nextStringIndex) {
onToken(XParser.tokenTypes.startTagQuote, stringsIndex, stringIndex, nextStringIndex, '"');
}
// A boolean is a literal boolean attribute declaration with no value.
static #sendBooleanTokens(onToken, tagName, stringsIndex, string, stringIndex, nextStringIndex) {
// A boolean attribute in a start tag — “data-has-flag”
const attributeName = string.slice(stringIndex, nextStringIndex);
element.value.setAttribute(attributeName, '');
XParser.#validateNoDeclarativeShadowRoots(tagName, attributeName);
onToken(XParser.tokenTypes.booleanName, stringsIndex, stringIndex, nextStringIndex, attributeName);
}
// An unbound attribute is a literal attribute declaration, but this time, it
// does have an associated value — forming a key-value pair.
#addUnboundAttribute(string, stringIndex, element, nextStringIndex) {
const unboundAttribute = string.slice(stringIndex, nextStringIndex);
const equalsIndex = unboundAttribute.indexOf('=');
const attributeName = unboundAttribute.slice(0, equalsIndex);
const encoded = unboundAttribute.slice(equalsIndex + 2, -1);
const decoded = this.#replaceHtmlEntities(encoded);
element.value.setAttribute(attributeName, decoded);
// An attribute is a literal attribute declaration. It has an associated value
// which forms a key-value pair.
static #sendAttributeTokens(onToken, tagName, stringsIndex, string, stringIndex, nextStringIndex) {
// An attribute in a start tag — “data-foo="bar"”
const equalsStart = string.indexOf('=', stringIndex);
const attributeName = string.slice(stringIndex, equalsStart);
XParser.#validateNoDeclarativeShadowRoots(tagName, attributeName);
const equalsEnd = equalsStart + 1;
const valueStart = equalsEnd + 1;
const valueEnd = nextStringIndex - 1;
onToken(XParser.tokenTypes.attributeName, stringsIndex, stringIndex, equalsStart, attributeName);
onToken(XParser.tokenTypes.startTagEquals, stringsIndex, equalsStart, equalsEnd, '=');
onToken(XParser.tokenTypes.startTagQuote, stringsIndex, equalsEnd, valueStart, '"');
onToken(XParser.tokenTypes.attributeValueStart, stringsIndex, valueStart, valueStart, '');
XParser.#sendInnerTextTokens(onToken, string, stringsIndex, valueStart, valueEnd, XParser.tokenTypes.attributeValuePlaintext, XParser.tokenTypes.attributeValueReference);
onToken(XParser.tokenTypes.attributeValueEnd, stringsIndex, valueEnd, valueEnd, '');
onToken(XParser.tokenTypes.startTagQuote, stringsIndex, valueEnd, nextStringIndex, '"');
}
// A bound boolean is a boolean attribute flag with an associated value
// binding. It has a single, preceding “?” character. We notify subscribers
// about this flag.
#addBoundBoolean(onBoolean, string, stringIndex, path, element, nextStringIndex) {
const boundBoolean = string.slice(stringIndex, nextStringIndex);
const equalsIndex = boundBoolean.indexOf('=');
const attributeName = boundBoolean.slice(1, equalsIndex);
onBoolean(attributeName, path);
// binding. It has a single, preceding “?” character.
static #sendBoundBooleanTokens(onToken, stringsIndex, string, stringIndex, nextStringIndex) {
// A bound boolean in a start tag — “?data-foo="”
const nameStart = stringIndex + 1;
const equalsStart = string.indexOf('=', stringIndex);
const equalsEnd = equalsStart + 1;
const attributeName = string.slice(nameStart, equalsStart);
onToken(XParser.tokenTypes.boundBooleanPrefix, stringsIndex, stringIndex, nameStart, '?');
onToken(XParser.tokenTypes.boundBooleanName, stringsIndex, nameStart, equalsStart, attributeName);
onToken(XParser.tokenTypes.startTagEquals, stringsIndex, equalsStart, equalsEnd, '=');
onToken(XParser.tokenTypes.startTagQuote, stringsIndex, equalsEnd, nextStringIndex, '"');
onToken(XParser.tokenTypes.boundBooleanValue, stringsIndex, nextStringIndex, nextStringIndex, '');
}

@@ -771,76 +800,97 @@

// notify subscribers about this attribute which exists only when defined.
#addBoundDefined(onDefined, string, stringIndex, path, element, nextStringIndex) {
const boundDefined = string.slice(stringIndex, nextStringIndex);
const equalsIndex = boundDefined.indexOf('=');
const attributeName = boundDefined.slice(2, equalsIndex);
onDefined(attributeName, path);
static #sendBoundDefinedTokens(onToken, stringsIndex, string, stringIndex, nextStringIndex) {
// A bound defined in a start tag — “??data-foo="”
const nameStart = stringIndex + 2;
const equalsStart = string.indexOf('=', stringIndex);
const equalsEnd = equalsStart + 1;
const attributeName = string.slice(nameStart, equalsStart);
onToken(XParser.tokenTypes.boundDefinedPrefix, stringsIndex, stringIndex, nameStart, '??');
onToken(XParser.tokenTypes.boundDefinedName, stringsIndex, nameStart, equalsStart, attributeName);
onToken(XParser.tokenTypes.startTagEquals, stringsIndex, equalsStart, equalsEnd, '=');
onToken(XParser.tokenTypes.startTagQuote, stringsIndex, equalsEnd, nextStringIndex, '"');
onToken(XParser.tokenTypes.boundDefinedValue, stringsIndex, nextStringIndex, nextStringIndex, '');
}
// This is an attribute with a name / value pair where the “value” is bound
// as an interpolation. We notify subscribers about this attribute binding.
#addBoundAttribute(onAttribute, string, stringIndex, path, element, nextStringIndex) {
const boundAttribute = string.slice(stringIndex, nextStringIndex);
const equalsIndex = boundAttribute.indexOf('=');
const attributeName = boundAttribute.slice(0, equalsIndex);
onAttribute(attributeName, path);
// as an interpolation.
static #sendBoundAttributeTokens(onToken, stringsIndex, string, stringIndex, nextStringIndex) {
// A bound attribute in a start tag — “data-foo="”
const equalsStart = string.indexOf('=', stringIndex);
const equalsEnd = equalsStart + 1;
const attributeName = string.slice(stringIndex, equalsStart);
onToken(XParser.tokenTypes.boundAttributeName, stringsIndex, stringIndex, equalsStart, attributeName);
onToken(XParser.tokenTypes.startTagEquals, stringsIndex, equalsStart, equalsEnd, '=');
onToken(XParser.tokenTypes.startTagQuote, stringsIndex, equalsEnd, nextStringIndex, '"');
onToken(XParser.tokenTypes.boundAttributeValue, stringsIndex, nextStringIndex, nextStringIndex, '');
}
// This is an property with a name / value pair where the “value” is bound
// as an interpolation. We notify subscribers about this property binding.
#addBoundProperty(onProperty, string, stringIndex, path, nextStringIndex) {
const boundProperty = string.slice(stringIndex, nextStringIndex);
const equalsIndex = boundProperty.indexOf('=');
const propertyName = boundProperty.slice(1, equalsIndex);
onProperty(propertyName, path);
// as an interpolation.
static #sendBoundPropertyTokens(onToken, stringsIndex, string, stringIndex, nextStringIndex) {
// A bound boolean in a start tag — “.dataFoo="”
const nameStart = stringIndex + 1;
const equalsStart = string.indexOf('=', stringIndex);
const equalsEnd = equalsStart + 1;
const attributeName = string.slice(nameStart, equalsStart);
onToken(XParser.tokenTypes.boundPropertyPrefix, stringsIndex, stringIndex, nameStart, '.');
onToken(XParser.tokenTypes.boundPropertyName, stringsIndex, nameStart, equalsStart, attributeName);
onToken(XParser.tokenTypes.startTagEquals, stringsIndex, equalsStart, equalsEnd, '=');
onToken(XParser.tokenTypes.startTagQuote, stringsIndex, equalsEnd, nextStringIndex, '"');
onToken(XParser.tokenTypes.boundPropertyValue, stringsIndex, nextStringIndex, nextStringIndex, '');
}
// In addition to the allow-list of html tag names, any tag with a hyphen in
// the middle is considered a valid custom element. Therefore, we must allow
// for such declarations.
#validateTagName(tagName) {
if (
tagName.indexOf('-') === -1 &&
!this.#allowedHtmlElements.has(tagName)
) {
const errorMessagesKey = this.#namedErrorsToErrorMessagesKey.get('forbidden-html-element');
const errorMessage = this.#errorMessages.get(errorMessagesKey);
const substringMessage = `The <${tagName}> html element is forbidden.`;
throw new Error(`[${errorMessagesKey}] ${errorMessage}\n${substringMessage}`);
// Because void elements to not have an end tag, we close them slightly
// differently so downstream consumers can track DOM paths easily.
static #sendVoidElementTokens(onToken, stringsIndex, stringIndex, nextStringIndex) {
// Void elements are treated with special consideration as they
// will never contain child nodes.
onToken(XParser.tokenTypes.voidTagClose, stringsIndex, stringIndex, nextStringIndex, '>');
}
// Textarea contains so-called “replaceable” character data. We throw an error
// if a “complex” interpolation exists — anything other than a perfectly-fit
// content interpolation between the opening and closing tags.
static #sendTextareaTokens(onToken, stringsIndex, string, stringIndex, nextStringIndex) {
onToken(XParser.tokenTypes.startTagClose, stringsIndex, stringIndex, nextStringIndex, '>');
XParser.#throughTextarea.lastIndex = nextStringIndex;
if (XParser.#throughTextarea.test(string)) {
const nextNextStringIndex = XParser.#throughTextarea.lastIndex;
const textContentEnd = nextNextStringIndex - 11; // “</textarea>” has 11 characters.
const tagNameStart = textContentEnd + 2;
const tagNameEnd = nextNextStringIndex - 1;
onToken(XParser.tokenTypes.textStart, stringsIndex, nextStringIndex, nextStringIndex, '');
XParser.#sendInnerTextTokens(onToken, string, stringsIndex, nextStringIndex, textContentEnd, XParser.tokenTypes.textPlaintext, XParser.tokenTypes.textReference);
onToken(XParser.tokenTypes.textEnd, stringsIndex, textContentEnd, textContentEnd, '');
onToken(XParser.tokenTypes.endTagOpen, stringsIndex, textContentEnd, tagNameStart, '</');
onToken(XParser.tokenTypes.endTagName, stringsIndex, tagNameStart, tagNameEnd, 'textarea');
onToken(XParser.tokenTypes.endTagClose, stringsIndex, tagNameEnd, nextNextStringIndex, '>');
return nextNextStringIndex;
} else {
const errorMessagesKey = XParser.#namedErrorsToErrorMessagesKey.get('complex-textarea-interpolation');
const errorMessage = XParser.#errorMessages.get(errorMessagesKey);
throw new Error(`[${errorMessagesKey}] ${errorMessage}`);
}
}
// We’ve parsed through and open tag start and are ready to instantiate a new
// dom node and potentially add attributes, properties, and children.
#addElement(string, stringIndex, path, element, childNodesIndex, nextStringIndex) {
const prefixedTagName = string.slice(stringIndex, nextStringIndex);
const tagName = prefixedTagName.slice(1);
this.#validateTagName(tagName);
const childNode = this.#window.document.createElement(tagName);
element.value[this.#localName] === 'template'
? element.value.content.appendChild(childNode)
: element.value.appendChild(childNode);
childNode[this.#localName] = tagName;
childNode[this.#parentNode] = element.value;
element.value = childNode;
childNodesIndex.value += 1;
path.push(childNodesIndex.value);
// Literally just indicating the “>” to close a start tag.
static #sendStartTagCloseTokens(onToken, stringsIndex, stringIndex, nextStringIndex) {
onToken(XParser.tokenTypes.startTagClose, stringsIndex, stringIndex, nextStringIndex, '>');
}
// We’ve parsed through a close tag and can validate it, update our state to
// point back to our parent node, and continue parsing.
#finalizeElement(strings, stringsIndex, string, stringIndex, path, element, childNodesIndex, nextStringIndex) {
const closeTag = string.slice(stringIndex, nextStringIndex);
const tagName = closeTag.slice(2, -1);
const expectedTagName = element.value[this.#localName];
if (tagName !== expectedTagName) {
const { parsed } = this.#getErrorInfo(strings, stringsIndex, string, stringIndex);
const errorMessagesKey = this.#namedErrorsToErrorMessagesKey.get('mismatched-closing-tag');
const errorMessage = this.#errorMessages.get(errorMessagesKey);
const substringMessage = `The closing tag </${tagName}> does not match <${expectedTagName}>.`;
// An end tag — e.g., “</div>”.
static #sendEndTagTokens(onToken, tagName, strings, stringsIndex, string, stringIndex, nextStringIndex) {
const endTagNameStart = stringIndex + 2;
const endTagNameEnd = nextStringIndex - 1;
const endTagName = string.slice(endTagNameStart, endTagNameEnd);
if (endTagName !== tagName) {
const { parsed } = XParser.#getErrorInfo(strings, stringsIndex, string, stringIndex);
const errorMessagesKey = XParser.#namedErrorsToErrorMessagesKey.get('mismatched-closing-tag');
const errorMessage = XParser.#errorMessages.get(errorMessagesKey);
const substringMessage = `The closing tag </${endTagName}> does not match <${tagName}>.`;
const parsedThroughMessage = `Your HTML was parsed through: \`${parsed}\`.`;
throw new Error(`[${errorMessagesKey}] ${errorMessage}\n${substringMessage}\n${parsedThroughMessage}`);
}
childNodesIndex.value = path.pop();
element.value = element.value[this.#parentNode];
onToken(XParser.tokenTypes.endTagOpen, stringsIndex, stringIndex, endTagNameStart, '</');
onToken(XParser.tokenTypes.endTagName, stringsIndex, endTagNameStart, endTagNameEnd, endTagName);
onToken(XParser.tokenTypes.endTagClose, stringsIndex, endTagNameEnd, nextStringIndex, '>');
}

@@ -852,2 +902,52 @@

// This object enumerates all the different classifications we have for
// substring interpretations and bindings for interpolated html markup.
static tokenTypes = {
// Syntax
startTagOpen: 'start-tag-open', // “<”
startTagSpace: 'start-tag-space', // “ ”, “\n”, etc.
startTagEquals: 'start-tag-equals', // “=”
startTagQuote: 'start-tag-quote', // “"”
startTagClose: 'start-tag-close', // “>”
voidTagClose: 'void-tag-close', // “>” (special case)
boundBooleanPrefix: 'bound-boolean-prefix', // “?”
boundDefinedPrefix: 'bound-defined-prefix', // “??”
boundPropertyPrefix: 'bound-property-prefix', // “.”
endTagOpen: 'end-tag-open', // “</”
endTagClose: 'end-tag-close', // “>”
commentOpen: 'comment-open', // “<!--”
commentClose: 'comment-close', // “-->”
// Literals
startTagName: 'start-tag-name', // e.g., “div”
endTagName: 'end-tag-name', // e.g., “span”
comment: 'comment', // text in comment
attributeName: 'attribute-name', // e.g., “id”
booleanName: 'boolean-name', // e.g., “disabled”
// Text
textStart: 'text-start', // begin text
textReference: 'text-reference', // html entity
textPlaintext: 'text-plaintext', // normal text
textEnd: 'text-end', // end text
// Attribute Values
attributeValueStart: 'attribute-value-start', // begin value
attributeValueReference: 'attribute-value-reference', // html entity
attributeValuePlaintext: 'attribute-value-plaintext', // normal text
attributeValueEnd: 'attribute-value-end', // end value
// Bindings
boundAttributeName: 'bound-attribute-name', // binding name
boundBooleanName: 'bound-boolean-name', // binding name
boundDefinedName: 'bound-defined-name', // binding name
boundPropertyName: 'bound-property-name', // binding name
boundTextValue: 'bound-text-value', // binding location
boundContentValue: 'bound-content-value', // binding location
boundAttributeValue: 'bound-attribute-value', // binding location
boundBooleanValue: 'bound-boolean-value', // binding location
boundDefinedValue: 'bound-defined-value', // binding location
boundPropertyValue: 'bound-property-value', // binding location
};
/**

@@ -871,81 +971,21 @@ * Additional error context.

/**
* Instantiation options.
* @typedef {object} XParserOptions
* @property {window} [window]
* Main parsing callback.
* @callback onToken
* @param {string} type
* @param {number} index
* @param {number} start
* @param {number} end
* @param {string} substring
*/
/**
* Creates an XParser instance. Mock the “window” for validation-only usage.
* @param {XParserOptions} [options]
*/
constructor(options) {
if (this.constructor !== XParser) {
throw new Error('XParser class extension is not supported.');
}
this.#window = options?.window ?? globalThis;
this.#fragment = this.#window.document.createDocumentFragment();
this.#htmlEntityContainer = this.#window.document.createElement('template');
}
/**
* The onBoolean callback.
* @callback onBoolean
* @param {string} attributeName
* @param {number[]} path
*/
/**
* The onDefined callback.
* @callback onDefined
* @param {string} attributeName
* @param {number[]} path
*/
/**
* The onAttribute callback.
* @callback onAttribute
* @param {string} attributeName
* @param {number[]} path
*/
/**
* The onProperty callback.
* @callback onProperty
* @param {string} propertyName
* @param {number[]} path
*/
/**
* The onContent callback.
* @callback onContent
* @param {number[]} path
*/
/**
* The onText callback.
* @callback onText
* @param {number[]} path
*/
/**
* The core parse function takes in the “strings” from a tagged template
* function and returns a document fragment. The “on*” callbacks are an
* optimization to allow a subscriber to store future lookups without
* needing to re-walk the resulting document fragment.
* @param {TemplateStringsArray} strings
* @param {onBoolean} onBoolean
* @param {onDefined} onDefined
* @param {onAttribute} onAttribute
* @param {onProperty} onProperty
* @param {onContent} onContent
* @param {onText} onText
* @returns {DocumentFragment}
* function and returns an array of tokens representing the parsed result.
* @param {*} strings
* @param {onToken} onToken
*/
parse(strings, onBoolean, onDefined, onAttribute, onProperty, onContent, onText) {
const fragment = this.#fragment.cloneNode(false);
const path = [];
const childNodesIndex = { value: -1 }; // Wrapper to allow better factoring.
const element = { value: fragment }; // Wrapper to allow better factoring.
static parse(strings, onToken) {
const stringsLength = strings.length;
const tagNames = [null];
let tagName = null;
let stringsIndex = 0;

@@ -956,17 +996,17 @@ let string = null;

let nextStringIndex = null;
let value = this.#initial;
let value = XParser.#initial; // Values are stateful regular expressions.
try {
while (stringsIndex < stringsLength) {
XParser.#validateRawString(strings.raw[stringsIndex]);
string = strings[stringsIndex];
this.#validateRawString(strings.raw[stringsIndex]);
if (stringsIndex > 0) {
switch (value) {
case this.#initial:
case this.#boundContent:
case this.#unboundContent:
case this.#openTagEnd:
case this.#closeTag:
if (element.value[this.#localName] === 'textarea') {
case XParser.#initial:
case XParser.#boundContent:
case XParser.#text:
case XParser.#startTagClose:
case XParser.#endTag:
if (tagName === 'textarea') {
// The textarea tag only accepts text, we restrict interpolation

@@ -976,8 +1016,8 @@ // there. See note on “replaceable character data” in the

// https://w3c.github.io/html-reference/syntax.html#text-syntax
const sloppyStartInterpolation = value !== this.#openTagEnd;
this.#addBoundText(onText, string, path, sloppyStartInterpolation);
const sloppyStartInterpolation = value !== XParser.#startTagClose;
XParser.#sendBoundTextTokens(onToken, stringsIndex - 1, string, stringIndex, sloppyStartInterpolation);
} else {
this.#addBoundContent(onContent, path, element, childNodesIndex);
XParser.#sendBoundContentTokens(onToken, stringsIndex - 1, string, stringIndex);
}
value = this.#boundContent;
value = XParser.#boundContent;
nextStringIndex = value.lastIndex;

@@ -994,5 +1034,5 @@ break;

if (string.length > 0) {
const nextValue = this.#validTransition(string, stringIndex, value);
const nextValue = XParser.#validTransition(string, stringIndex, value);
if (!nextValue) {
this.#throwTransitionError(strings, stringsIndex, string, stringIndex, value);
XParser.#throwTransitionError(strings, stringsIndex, string, stringIndex, value);
}

@@ -1005,64 +1045,58 @@ value = nextValue;

switch (value) {
case this.#unboundContent:
this.#addUnboundContent(string, stringIndex, element, childNodesIndex, nextStringIndex);
case XParser.#text:
XParser.#sendTextTokens(onToken, stringsIndex, string, stringIndex, nextStringIndex);
break;
case this.#unboundComment:
this.#addUnboundComment(string, stringIndex, element, childNodesIndex, nextStringIndex);
case XParser.#comment:
XParser.#sendCommentTokens(onToken, stringsIndex, string, stringIndex, nextStringIndex);
break;
case this.#openTagStart:
this.#addElement(string, stringIndex, path, element, childNodesIndex, nextStringIndex);
case XParser.#startTagOpen:
tagName = XParser.#sendStartTagOpenTokens(onToken, stringsIndex, string, stringIndex, nextStringIndex);
tagNames.push(tagName);
break;
case this.#unboundBoolean:
this.#addUnboundBoolean(string, stringIndex, element, nextStringIndex);
case XParser.#startTagSpace:
XParser.#sendStartTagSpaceTokens(onToken, stringsIndex, string, stringIndex, nextStringIndex);
break;
case this.#unboundAttribute:
this.#addUnboundAttribute(string, stringIndex, element, nextStringIndex);
case XParser.#danglingQuote:
XParser.#sendDanglingQuoteTokens(onToken, stringsIndex, string, stringIndex, nextStringIndex);
break;
case this.#boundBoolean:
this.#addBoundBoolean(onBoolean, string, stringIndex, path, element, nextStringIndex);
case XParser.#boolean:
XParser.#sendBooleanTokens(onToken, tagName, stringsIndex, string, stringIndex, nextStringIndex);
break;
case this.#boundDefined:
this.#addBoundDefined(onDefined, string, stringIndex, path, element, nextStringIndex);
case XParser.#attribute:
XParser.#sendAttributeTokens(onToken, tagName, stringsIndex, string, stringIndex, nextStringIndex);
break;
case this.#boundAttribute:
this.#addBoundAttribute(onAttribute, string, stringIndex, path, element, nextStringIndex);
case XParser.#boundBoolean:
XParser.#sendBoundBooleanTokens(onToken, stringsIndex, string, stringIndex, nextStringIndex);
break;
case this.#boundProperty:
this.#addBoundProperty(onProperty, string, stringIndex, path, nextStringIndex);
case XParser.#boundDefined:
XParser.#sendBoundDefinedTokens(onToken, stringsIndex, string, stringIndex, nextStringIndex);
break;
case this.#openTagEnd: {
const tagName = element.value[this.#localName];
if (this.#voidHtmlElements.has(tagName)) {
value = this.#finalizeVoidElement(path, element, childNodesIndex, nextStringIndex);
nextStringIndex = value.lastIndex;
} else if (
tagName === 'textarea' &&
this.#openTagEnd.lastIndex !== string.length
) {
value = this.#finalizeTextarea(string, path, element, childNodesIndex, nextStringIndex);
nextStringIndex = value.lastIndex;
} else if (tagName === 'pre' && string[value.lastIndex] === '\n') {
// An initial newline character is optional for <pre> tags.
// https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
value.lastIndex++;
nextStringIndex = value.lastIndex;
// Assume we’re traversing into the new element and reset index.
childNodesIndex.value = -1;
} else if (
tagName === 'template' &&
// @ts-ignore — TypeScript doesn’t get that this is a “template”.
element.value.hasAttribute('shadowrootmode')
) {
const errorMessagesKey = this.#namedErrorsToErrorMessagesKey.get('declarative-shadow-root');
const errorMessage = this.#errorMessages.get(errorMessagesKey);
throw new Error(`[${errorMessagesKey}] ${errorMessage}`);
case XParser.#boundAttribute:
XParser.#sendBoundAttributeTokens(onToken, stringsIndex, string, stringIndex, nextStringIndex);
break;
case XParser.#boundProperty:
XParser.#sendBoundPropertyTokens(onToken, stringsIndex, string, stringIndex, nextStringIndex);
break;
case XParser.#startTagClose:
if (XParser.#voidHtmlElements.has(tagName)) {
XParser.#sendVoidElementTokens(onToken, stringsIndex, stringIndex, nextStringIndex);
tagNames.pop();
tagName = tagNames[tagNames.length - 1];
} else if (tagName === 'textarea' && XParser.#startTagClose.lastIndex !== string.length) {
// If successful, move cursor through textarea element end tag.
nextStringIndex = XParser.#sendTextareaTokens(onToken, stringsIndex, string, stringIndex, nextStringIndex);
value = XParser.#endTag;
value.lastIndex = nextStringIndex;
tagNames.pop();
tagName = tagNames[tagNames.length - 1];
} else {
// Assume we’re traversing into the new element and reset index.
childNodesIndex.value = -1;
XParser.#sendStartTagCloseTokens(onToken, stringsIndex, stringIndex, nextStringIndex);
}
break;
case XParser.#endTag: {
XParser.#sendEndTagTokens(onToken, tagName, strings, stringsIndex, string, stringIndex, nextStringIndex);
tagNames.pop();
tagName = tagNames[tagNames.length - 1];
break;
}
case this.#closeTag:
this.#finalizeElement(strings, stringsIndex, string, stringIndex, path, element, childNodesIndex, nextStringIndex);
break;
}

@@ -1073,4 +1107,4 @@ stringIndex = nextStringIndex; // Update out pointer from our pattern match.

}
this.#validateExit(fragment, element);
return fragment;
XParser.#validateExit(tagName);
} catch (error) {

@@ -1077,0 +1111,0 @@ error[XParser.#errorContextKey] = { stringsIndex, string, stringIndex };

import { XParser } from './x-parser.js';
const parser = new XParser();
/** Internal implementation details for template engine. */

@@ -26,2 +24,8 @@ class TemplateEngine {

// It’s more performant to clone a single fragment, so we keep a reference.
static #fragment = new DocumentFragment();
// We decode character references via “setHTMLUnsafe” on this container.
static #htmlEntityContainer = document.createElement('template');
// Mapping of tagged template function “strings” to caches computations.

@@ -139,2 +143,130 @@ static #stringsToAnalysis = new WeakMap();

// We only decode things we know to be encoded since it’s non-performant.
static #decode(encoded) {
this.#htmlEntityContainer.setHTMLUnsafe(encoded);
const decoded = this.#htmlEntityContainer.content.textContent;
return decoded;
}
// Walk over a pre-validated set of tokens from our parser. Note that because
// the parser is _very_ strict, we can make a lot of simplifying assumptions.
static #onToken(
// These areguments are passed in through a “bind”.
state, onBoolean, onDefined, onAttribute, onProperty, onContent, onText,
// These arguments are passed in through the “onToken” callback.
type, index, start, end, substring
) {
switch (type) {
case XParser.tokenTypes.startTagName: {
const tagName = substring;
const childNode = globalThis.document.createElement(tagName);
state.tagName === 'template'
// @ts-ignore — TypeScript doesn’t get that this is a template.
? state.element.content.appendChild(childNode)
: state.element.appendChild(childNode);
state.parentElements.push(state.element);
state.parentTagNames.push(state.tagName);
state.element = childNode;
state.tagName = tagName;
state.childNodesIndex += 1;
state.path.push(state.childNodesIndex);
break;
}
case XParser.tokenTypes.voidTagClose:
state.element = state.parentElements.pop();
state.tagName = state.parentTagNames.pop();
state.childNodesIndex = state.path.pop();
break;
case XParser.tokenTypes.startTagClose:
// Assume we’re traversing into the new element and reset index.
state.childNodesIndex = -1;
break;
case XParser.tokenTypes.endTagName:
state.childNodesIndex = state.path.pop();
state.element = state.parentElements.pop();
state.tagName = state.parentTagNames.pop();
break;
case XParser.tokenTypes.attributeName:
case XParser.tokenTypes.boundAttributeName:
case XParser.tokenTypes.boundBooleanName:
case XParser.tokenTypes.boundDefinedName:
case XParser.tokenTypes.boundPropertyName:
state.name = substring;
break;
case XParser.tokenTypes.booleanName: {
// @ts-ignore — TypeScript doesn’t get that this is an element.
state.element.setAttribute(substring, '');
break;
}
case XParser.tokenTypes.comment:
state.element.appendChild(document.createComment(substring));
state.childNodesIndex += 1;
break;
case XParser.tokenTypes.textPlaintext:
case XParser.tokenTypes.attributeValuePlaintext:
state.text += substring;
break;
case XParser.tokenTypes.textReference:
case XParser.tokenTypes.attributeValueReference:
state.text += substring;
state.encoded = true;
break;
case XParser.tokenTypes.textEnd: {
const decoded = state.encoded ? TemplateEngine.#decode(state.text) : state.text;
if (
state.tagName === 'pre' &&
state.childNodesIndex === -1 &&
decoded.startsWith('\n')
) {
// First newline is stripped according to the <pre> tag specification.
// https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
state.element.appendChild(document.createTextNode(decoded.slice(1)));
} else {
state.element.appendChild(document.createTextNode(decoded));
}
state.childNodesIndex += 1;
state.encoded = false;
state.text = '';
break;
}
case XParser.tokenTypes.attributeValueEnd: {
const decoded = state.encoded ? TemplateEngine.#decode(state.text) : state.text;
// @ts-ignore — TypeScript doesn’t get that this is an element.
state.element.setAttribute(state.name, decoded);
state.name = null;
state.encoded = false;
state.text = '';
break;
}
case XParser.tokenTypes.boundTextValue:
onText(state.path);
break;
case XParser.tokenTypes.boundContentValue:
// @ts-ignore — TypeScript doesn’t get that this is an element.
state.element.append(document.createComment(''), document.createComment(''));
state.childNodesIndex += 2;
state.path.push(state.childNodesIndex);
onContent(state.path);
state.path.pop();
break;
case XParser.tokenTypes.boundAttributeValue:
onAttribute(state.name, state.path);
state.name = null;
break;
case XParser.tokenTypes.boundBooleanValue:
onBoolean(state.name, state.path);
state.name = null;
break;
case XParser.tokenTypes.boundDefinedValue:
onDefined(state.name, state.path);
state.name = null;
break;
case XParser.tokenTypes.boundPropertyValue:
onProperty(state.name, state.path);
state.name = null;
break;
}
}
// After cloning our common fragment, we use the “lookups” to cache live

@@ -301,9 +433,7 @@ // references to DOM nodes so that we can surgically perform updates later in

} else {
if (value !== lastValue) {
node.setAttribute(name, value);
}
node.setAttribute(name, value);
}
}
static #commitBoolean(node, name, value, lastValue) {
static #commitBoolean(node, name, value) {
const update = TemplateEngine.#symbolToUpdate.get(value);

@@ -313,9 +443,7 @@ if (update) {

} else {
if (value !== lastValue) {
value ? node.setAttribute(name, '') : node.removeAttribute(name);
}
value ? node.setAttribute(name, '') : node.removeAttribute(name);
}
}
static #commitDefined(node, name, value, lastValue) {
static #commitDefined(node, name, value) {
const update = TemplateEngine.#symbolToUpdate.get(value);

@@ -325,11 +453,9 @@ if (update) {

} else {
if (value !== lastValue) {
value === undefined || value === null
? node.removeAttribute(name)
: node.setAttribute(name, value);
}
value === undefined || value === null
? node.removeAttribute(name)
: node.setAttribute(name, value);
}
}
static #commitProperty(node, name, value, lastValue) {
static #commitProperty(node, name, value) {
const update = TemplateEngine.#symbolToUpdate.get(value);

@@ -339,10 +465,7 @@ if (update) {

} else {
if (value !== lastValue) {
node[name] = value;
}
node[name] = value;
}
}
// TODO: Future state here — we’ll eventually just guard against value changes
// at a higher level and will remove all updater logic.
// TODO: Future state here once “ifDefined” is gone.
// static #commitAttribute(node, name, value) {

@@ -362,64 +485,7 @@ // node.setAttribute(name, value);

// }
// static #commitContent(node, startNode, value, lastValue) {
// const category = TemplateEngine.#getCategory(value);
// const lastCategory = TemplateEngine.#getCategory(lastValue);
// if (category !== lastCategory && lastValue !== TemplateEngine.#UNSET) {
// // Reset content under certain conditions. E.g., `map` >> `null`.
// const state = TemplateEngine.#getState(node, TemplateEngine.#STATE);
// const arrayState = TemplateEngine.#getState(startNode, TemplateEngine.#ARRAY_STATE);
// TemplateEngine.#removeBetween(startNode, node);
// TemplateEngine.#clearObject(state);
// TemplateEngine.#clearObject(arrayState);
// }
// if (category === 'result') {
// const state = TemplateEngine.#getState(node, TemplateEngine.#STATE);
// const rawResult = value;
// if (!TemplateEngine.#canReuseDom(state.preparedResult, rawResult)) {
// TemplateEngine.#removeBetween(startNode, node);
// TemplateEngine.#clearObject(state);
// const preparedResult = TemplateEngine.#inject(rawResult, node, true);
// state.preparedResult = preparedResult;
// } else {
// TemplateEngine.#update(state.preparedResult, rawResult);
// }
// } else if (category === 'array' || category === 'map') {
// TemplateEngine.#list(node, startNode, value, category);
// } else if (category === 'fragment') {
// if (value.childElementCount === 0) {
// throw new Error(`Unexpected child element count of zero for given DocumentFragment.`);
// }
// const previousSibling = node.previousSibling;
// if (previousSibling !== startNode) {
// TemplateEngine.#removeBetween(startNode, node);
// }
// node.parentNode.insertBefore(value, node);
// } else {
// // TODO: Is there a way to more-performantly skip this init step? E.g., if
// // the prior value here was not “unset” and we didn’t just reset? We
// // could cache the target node in these cases or something?
// const previousSibling = node.previousSibling;
// if (previousSibling === startNode) {
// // The `?? ''` is a shortcut for creating a text node and then
// // setting its textContent. It’s exactly equivalent to the
// // following code, but faster.
// // const textNode = document.createTextNode('');
// // textNode.textContent = value;
// const textNode = document.createTextNode(value ?? '');
// node.parentNode.insertBefore(textNode, node);
// } else {
// previousSibling.textContent = value;
// }
// }
// }
// static #commitText(node, value) {
// node.textContent = value;
// }
static #commitContent(node, startNode, value, lastValue) {
const introspection = TemplateEngine.#getValueIntrospection(value);
const lastIntrospection = TemplateEngine.#getValueIntrospection(lastValue);
if (
lastValue !== TemplateEngine.#UNSET &&
introspection?.category !== lastIntrospection?.category
) {
const category = TemplateEngine.#getCategory(value);
const lastCategory = TemplateEngine.#getCategory(lastValue);
if (category !== lastCategory && lastValue !== TemplateEngine.#UNSET) {
// Reset content under certain conditions. E.g., `map` >> `null`.

@@ -432,45 +498,39 @@ const state = TemplateEngine.#getState(node, TemplateEngine.#STATE);

}
if (introspection?.category === 'update') {
TemplateEngine.#throwIfDefinedError(TemplateEngine.#CONTENT);
if (category === 'result') {
const state = TemplateEngine.#getState(node, TemplateEngine.#STATE);
const rawResult = value;
if (!TemplateEngine.#canReuseDom(state.preparedResult, rawResult)) {
TemplateEngine.#removeBetween(startNode, node);
TemplateEngine.#clearObject(state);
const preparedResult = TemplateEngine.#inject(rawResult, node, true);
state.preparedResult = preparedResult;
} else {
TemplateEngine.#update(state.preparedResult, rawResult);
}
} else if (category === 'array' || category === 'map') {
TemplateEngine.#list(node, startNode, value, category);
} else if (category === 'fragment') {
if (value.childElementCount === 0) {
throw new Error(`Unexpected child element count of zero for given DocumentFragment.`);
}
const previousSibling = node.previousSibling;
if (previousSibling !== startNode) {
TemplateEngine.#removeBetween(startNode, node);
}
node.parentNode.insertBefore(value, node);
} else {
// Note that we always want to re-render results / lists, but because the
// way they are created, a new outer reference should always have been
// generated, so it’s ok to leave inside this value check.
if (value !== lastValue) {
if (introspection?.category === 'result') {
const state = TemplateEngine.#getState(node, TemplateEngine.#STATE);
const rawResult = value;
if (!TemplateEngine.#canReuseDom(state.preparedResult, rawResult)) {
TemplateEngine.#removeBetween(startNode, node);
TemplateEngine.#clearObject(state);
const preparedResult = TemplateEngine.#inject(rawResult, node, true);
state.preparedResult = preparedResult;
} else {
TemplateEngine.#update(state.preparedResult, rawResult);
}
} else if (introspection?.category === 'array' || introspection?.category === 'map') {
TemplateEngine.#list(node, startNode, value, introspection.category);
} else if (introspection?.category === 'fragment') {
if (value.childElementCount === 0) {
throw new Error(`Unexpected child element count of zero for given DocumentFragment.`);
}
const previousSibling = node.previousSibling;
if (previousSibling !== startNode) {
TemplateEngine.#removeBetween(startNode, node);
}
node.parentNode.insertBefore(value, node);
} else {
const previousSibling = node.previousSibling;
if (previousSibling === startNode) {
// The `?? ''` is a shortcut for creating a text node and then
// setting its textContent. It’s exactly equivalent to the
// following code, but faster.
// const textNode = document.createTextNode('');
// textNode.textContent = value;
const textNode = document.createTextNode(value ?? '');
node.parentNode.insertBefore(textNode, node);
} else {
previousSibling.textContent = value;
}
}
// TODO: Is there a way to more-performantly skip this init step? E.g., if
// the prior value here was not “unset” and we didn’t just reset? We
// could cache the target node in these cases or something?
const previousSibling = node.previousSibling;
if (previousSibling === startNode) {
// The `?? ''` is a shortcut for creating a text node and then
// setting its textContent. It’s exactly equivalent to the
// following code, but faster.
// const textNode = document.createTextNode('');
// textNode.textContent = value;
const textNode = document.createTextNode(value ?? '');
node.parentNode.insertBefore(textNode, node);
} else {
previousSibling.textContent = value;
}

@@ -480,30 +540,6 @@ }

static #commitText(node, value, lastValue) {
const update = TemplateEngine.#symbolToUpdate.get(value);
if (update) {
TemplateEngine.#throwIfDefinedError(TemplateEngine.#TEXT);
} else {
if (value !== lastValue) {
node.textContent = value;
}
}
static #commitText(node, value) {
node.textContent = value;
}
// TODO: Future state — we’ll later do change-by-reference detection here.
// // Bind the current values from a result by walking through each target and
// // updating the DOM if things have changed.
// static #commit(preparedResult) {
// preparedResult.values ??= preparedResult.rawResult.values;
// preparedResult.lastValues ??= preparedResult.values.map(() => TemplateEngine.#UNSET);
// const { targets, values, lastValues } = preparedResult;
// for (let iii = 0; iii < targets.length; iii++) {
// const value = values[iii];
// const lastValue = lastValues[iii];
// if (value !== lastValue) {
// const target = targets[iii];
// target(value, lastValue);
// }
// }
// }
// Bind the current values from a result by walking through each target and

@@ -516,6 +552,8 @@ // updating the DOM if things have changed.

for (let iii = 0; iii < targets.length; iii++) {
const target = targets[iii];
const value = values[iii];
const lastValue = lastValues[iii];
target(value, lastValue);
if (value !== lastValue) {
const target = targets[iii];
target(value, lastValue);
}
}

@@ -591,2 +629,14 @@ }

if (!analysis.done) {
const fragment = TemplateEngine.#fragment.cloneNode(false);
const state = {
path: [],
parentElements: [],
parentTagNames: [],
element: fragment,
tagName: null,
childNodesIndex: -1,
encoded: false,
text: '',
name: null,
};
const lookups = {};

@@ -599,3 +649,4 @@ const onBoolean = TemplateEngine.#storeKeyLookup.bind(null, lookups, TemplateEngine.#BOOLEAN);

const onText = TemplateEngine.#storeTextLookup.bind(null, lookups);
const fragment = parser.parse(strings, onBoolean, onDefined, onAttribute, onProperty, onContent, onText);
const onToken = TemplateEngine.#onToken.bind(null, state, onBoolean, onDefined, onAttribute, onProperty, onContent, onText);
XParser.parse(strings, onToken);
analysis.fragment = fragment;

@@ -614,32 +665,10 @@ analysis.lookups = lookups;

// TODO: Revisit this concept when we delete deprecated interfaces. Once that
// happens, the _only_ updater available for content is `map`, and we may be
// able to make this more performant.
// We can probably change this to something like the following eventually:
//
// static #getCategory(value) {
// if (typeof value === 'object') {
// if (TemplateEngine.#isRawResult(value)) {
// return 'result';
// } else if (Array.isArray(value)) {
// return Array.isArray(value[0]) ? 'map' : 'array';
// } else if (value instanceof DocumentFragment) {
// return 'fragment';
// }
// }
// }
//
static #getValueIntrospection(value) {
if (Array.isArray(value)) {
return Array.isArray(value[0]) ? { category: 'map' } : { category: 'array' };
} else if (value instanceof DocumentFragment) {
return { category: 'fragment' };
} else if (value !== null && typeof value === 'object') {
static #getCategory(value) {
if (typeof value === 'object') {
if (TemplateEngine.#isRawResult(value)) {
return { category: 'result' };
} else {
const update = TemplateEngine.#symbolToUpdate.get(value);
if (update) {
return { category: 'update', update };
}
return 'result';
} else if (Array.isArray(value)) {
return Array.isArray(value[0]) ? 'map' : 'array';
} else if (value instanceof DocumentFragment) {
return 'fragment';
}

@@ -654,4 +683,2 @@ }

static #canReuseDom(preparedResult, rawResult) {
// TODO: Is it possible that we might have the same strings from a different
// template language? Probably not. The following check should suffice.
return preparedResult?.rawResult.strings === rawResult?.strings;

@@ -761,4 +788,2 @@ }

case TemplateEngine.#PROPERTY: return 'a property';
case TemplateEngine.#CONTENT: return 'content';
case TemplateEngine.#TEXT: return 'text content';
}

@@ -765,0 +790,0 @@ }

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet