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.10

2

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

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

@@ -50,11 +50,17 @@ ```

and...
Then import it using a bare module specifier...
```js
import XElement from '@netflix/x-element';
```
...or use a package manager:
```
```bash
npm install @netflix/x-element
```
Then import it using a bare module specifier...
```js
import XElement from '@netflix/x-element';
```
## Project Philosophy:

@@ -61,0 +67,0 @@

@@ -357,12 +357,25 @@ import { assert, describe, it } from '@netflix/x-test/x-test.js';

it('renders “on*” attributes as event handlers', () => {
const container = document.createElement('div');
document.body.append(container);
render(container, html`<div onclick="this.textContent = '&hellip;hi&hellip;';"></div>`);
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. The default template engine doesn’t allow
// any character escapes, so there is no way to pass “\u2026” as we can
// below in the interpolated property test.
assert(container.firstElementChild.textContent = '…hi…');
container.remove();
});
it('renders “on*” properties as event handlers', () => {
const container = document.createElement('div');
document.body.append(container);
render(container, html`<div .onclick="${function() { this.textContent = '…hi\u2026'; }}"></div>`);
render(container, html`<div .onclick="${'function() { this.textContent = \'…hi\u2026\'; }'}"></div>`);
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).
// to-be-evaluated JS script. Because the “\u2026” is interpolated, it’s
// not validated at all (it’s not even seen by the parser).
assert(container.firstElementChild.textContent = '…hi…');

@@ -775,7 +788,10 @@ container.remove();

// TODO: #254: Uncomment “moves” lines when we leverage “moveBefore”.
it('native map does not cause disconnectedCallback on prefix removal', () => {
let connects = 0;
// let moves = 0;
let disconnects = 0;
class TestPrefixRemoval extends HTMLElement {
connectedCallback() { connects++; }
// connectedMoveCallback() { moves++; }
disconnectedCallback() { disconnects++; }

@@ -799,2 +815,3 @@ }

assert(connects === 0);
// assert(moves === 0);
assert(disconnects === 0);

@@ -804,2 +821,3 @@

assert(connects === 2);
// assert(moves === 0);
assert(disconnects === 0);

@@ -809,2 +827,3 @@

assert(connects === 2);
// assert(moves === 1);
assert(disconnects === 1);

@@ -814,2 +833,3 @@

assert(connects === 2);
// assert(moves === 1);
assert(disconnects === 2);

@@ -820,7 +840,10 @@

// TODO: #254: Uncomment “moves” lines when we leverage “moveBefore”.
it('native map does not cause disconnectedCallback on suffix removal', () => {
let connects = 0;
// let moves = 0;
let disconnects = 0;
class TestSuffixRemoval extends HTMLElement {
connectedCallback() { connects++; }
// connectedMoveCallback() { moves++; }
disconnectedCallback() { disconnects++; }

@@ -844,2 +867,3 @@ }

assert(connects === 0);
// assert(moves === 0);
assert(disconnects === 0);

@@ -849,2 +873,3 @@

assert(connects === 2);
// assert(moves === 0);
assert(disconnects === 0);

@@ -854,2 +879,3 @@

assert(connects === 2);
// assert(moves === 0);
assert(disconnects === 1);

@@ -859,2 +885,3 @@

assert(connects === 2);
// assert(moves === 0);
assert(disconnects === 2);

@@ -868,5 +895,7 @@

let connects = 0;
let moves = 0;
let disconnects = 0;
class TestListShuffle extends HTMLElement {
connectedCallback() { connects++; }
connectedMoveCallback() { moves++; }
disconnectedCallback() { disconnects++; }

@@ -890,2 +919,3 @@ }

assert(connects === 0);
assert(moves === 0);
assert(disconnects === 0);

@@ -895,2 +925,3 @@

assert(connects === 2);
assert(moves === 0);
assert(disconnects === 0);

@@ -900,2 +931,3 @@

assert(connects === 2);
assert(moves === 1);
assert(disconnects === 0);

@@ -905,2 +937,3 @@

assert(connects === 2);
assert(moves === 1);
assert(disconnects === 2);

@@ -907,0 +940,0 @@

@@ -25,3 +25,2 @@ /** Strict HTML parser meant to handle interpolated HTML. */

static "__#1@#throughTextarea": RegExp;
static "__#1@#rawJsEscape": RegExp;
static "__#1@#entity": RegExp;

@@ -41,7 +40,7 @@ static "__#1@#htmlEntityStart": 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@#getErrorMessage"(key: any): "Could not parse template markup (at template start)." | "Could not parse template markup (after text content)." | "Could not parse template markup (after a comment)." | "Could not parse template markup (after interpolated content)." | "Could not parse template markup (after a spacing within start tag)." | "Could not parse template markup (after a start tag)." | "Could not parse template markup (after a boolean attribute interpolation in a start tag)." | "Could not parse template markup (after a defined attribute interpolation in a start tag)." | "Could not parse template markup (after an attribute interpolation in a start tag)." | "Could not parse template markup (after a property interpolation in a start tag)." | "Could not parse template markup (after an end tag)." | "Invalid tag name - refer to https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define#valid_custom_element_names)." | "Invalid tag whitespace (extraneous whitespace in start tag)." | "Invalid start tag (extraneous whitespace at close of start tag)." | "Invalid end tag." | "Invalid tag attribute (must use kebab-case names and double-quoted values)." | "Invalid tag attribute interpolation (must use kebab-case names and double-quoted values)." | "Invalid tag property interpolation (must use kebab-case names and double-quoted values)." | "Invalid closing quote on tag attribute or property." | "CDATA sections are not supported. Use character references instead: https://developer.mozilla.org/en-US/docs/Glossary/Character_reference." | "Bad escape in tagged template string. Use “&bsol;” for “\\”, “&dollar;” for “\\$”, and “&grave;” for “\\`”. All character references are supported: https://developer.mozilla.org/en-US/docs/Glossary/Character_reference." | "Ambiguous ampersand character or invalid hexadecimal character reference." | "Invalid comment. Comments cannot start with “>” or “->” characters, they cannot include a set of “--” characters, and they cannot end with a “-” character." | "Unsupported native tag - supported native tags are listed here: https://github.com/Netflix/x-element/blob/main/doc/TEMPLATES.md#supported-native-tags." | "Invalid end tag (all non-void start tags much have matching end tags)." | "Unsupported <textarea> interpolation. Interpolation must be exact (<textarea>${…}</textarea>)." | "Unsupported declarative shadow root on <template>. The “shadowrootmode” attribute is not supported." | "Missing closing quote on bound attribute or property.";
static "__#1@#getErrorMessageKeyFromValue"(value: any): "#100" | "#101" | "#102" | "#103" | "#104" | "#105" | "#106" | "#107" | "#108" | "#109" | "#110";
static "__#1@#getErrorMessageKeyFromValueMalformed"(valueMalformed: any): "#120" | "#121" | "#122" | "#123" | "#124" | "#125" | "#126" | "#127";
static "__#1@#getErrorMessageKeyFromValueForbidden"(valueForbidden: any): string;
static "__#1@#getErrorMessageKeyFromErrorName"(errorName: any): "#150" | "#151" | "#152" | "#153" | "#154" | "#155" | "#156" | "#157";
static "__#1@#try"(string: any, stringIndex: any, ...values: any[]): any;

@@ -57,3 +56,3 @@ static "__#1@#forbiddenTransition"(string: any, stringIndex: any, value: any): any;

static "__#1@#validateRawString"(rawString: any): void;
static "__#1@#validateExit"(tagName: any): void;
static "__#1@#validateExit"(value: any, tagName: any): void;
static "__#1@#sendInnerTextTokens"(onToken: any, string: any, index: any, start: any, end: any, plaintextType: any, referenceType: any): void;

@@ -60,0 +59,0 @@ static "__#1@#validateTagName"(tagName: any): void;

@@ -175,2 +175,8 @@ import * as defaultTemplateEngine from './x-template.js';

// TODO: #254: Uncomment once we leverage “moveBefore”.
// /**
// * Extends HTMLElement.prototype.connectedMoveCallback.
// */
// connectedMoveCallback() {}
/**

@@ -177,0 +183,0 @@ * Extends HTMLElement.prototype.attributeChangedCallback.

@@ -81,12 +81,12 @@ /** Strict HTML parser meant to handle interpolated HTML. */

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

@@ -103,8 +103,6 @@ // Our tag name rules are more restrictive than the “tag name” spec.

// - not ok: <-div>, <1-my-element>
static #startTagOpen = /<(?![0-9-])[a-z0-9-]+(?<!-)(?=[\s\n>])/y;
static #endTag = /<\/(?![0-9-])[a-z0-9-]+(?<!-)>/y;
static #startTagClose = /(?<![\s\n])>/y;
static #startTagOpen = /<(?![0-9-])[a-z0-9-]+(?<!-)(?=[\s>])/y;
static #endTag = /<\/(?![0-9-])[a-z0-9-]+(?<!-)>/y;
static #startTagClose = /(?<!\s)>/y;
// TODO: Check on performance for this pattern. We want to do a positive
// lookahead so that we report the correct failure on fail.
// Our space-delimiter rules more restrictive than the “space” spec.

@@ -118,3 +116,3 @@ // https://w3c.github.io/html-reference/terminology.html#space

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

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

// - not ok: foo='bar', ?foo, foo=${'bar'}
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;
static #boolean = /(?![0-9-])[a-z0-9-]+(?<!-)(?=[\s>])/y;
static #attribute = /(?![0-9-])[a-z0-9-]+(?<!-)="[^"]*"(?=[\s>])/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;

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

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

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

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

@@ -175,25 +173,5 @@ //////////////////////////////////////////////////////////////////////////////

// https://w3c.github.io/html-reference/syntax.html#replaceable-character-data
static #throughTextarea = /.*?<\/textarea>/ys;
static #throughTextarea = /.*?<\/textarea>/ys;
//////////////////////////////////////////////////////////////////////////////
// JS-y Escapes //////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
// Character escapes like “\n”, “\u” or ”\x” are a JS-ism. We want developers
// to use HTML here, not JS. You can of course interpolate whatever you want.
// https://w3c.github.io/html-reference/syntax.html#character-encoding
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Character_escape
// Note that syntax highlighters expect the text after the “html” tag to be
// real HTML. Another reason to reject JS-y unicode is that it won’t be
// interpreted correctly by tooling that expects _html_.
// The only escapes we expect to see are for the “$”, “\”, and “`” characters,
// which you _must_ use if you need those literal characters.
// The simplest way to check this is to ensure that back slashes always come
// in pairs of two or a single back slash preceding a back tick.
// Examples:
// - ok: html`&#8230;`, html`&#x2026;`, html`&mldr;`, html`&hellip;`, html`\\n`
// - not ok: html`\nhi\nthere`, html`\x8230`, html`\u2026`, html`\s\t\o\p\ \i\t\.`
static #rawJsEscape = /.*(?<!\\)(?:\\{2})*\\(?![$\\`])/ys;
//////////////////////////////////////////////////////////////////////////////
// Character References //////////////////////////////////////////////////////

@@ -212,4 +190,4 @@ //////////////////////////////////////////////////////////////////////////////

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

@@ -227,3 +205,3 @@ //////////////////////////////////////////////////////////////////////////////

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

@@ -236,21 +214,21 @@ //////////////////////////////////////////////////////////////////////////////

// open or close tags.
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;
static #startTagOpenMalformed = /<\s*[a-zA-Z0-9_-]+/y;
static #startTagSpaceMalformed = /\s+/y;
static #startTagCloseMalformed = /\s*\/?>/y;
static #endTagMalformed = /<\s*\/\s*[a-zA-Z0-9_-]+[^>]*>/y;
// See if incorrect characters, wrong quotes, or no quotes were used with
// 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;
static #booleanMalformed = /[a-zA-Z0-9-_]+(?=[\s>])/y;
static #attributeMalformed = /[a-zA-Z0-9-_]+=(?:"[^"]*"|'[^']*')?(?=[\s>])/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.
static #boundPropertyMalformed = /\.[a-zA-Z0-9-_]+=(?:"|')?$/y;
static #boundPropertyMalformed = /\.[a-zA-Z0-9-_]+=(?:"|')?$/y;
// See if the quote pair was malformed or missing.
static #danglingQuoteMalformed = /'?(?=[\s\n>])/y;
static #danglingQuoteMalformed = /'?(?=[\s>])/y;

@@ -261,95 +239,105 @@ //////////////////////////////////////////////////////////////////////////////

// The following “mappings” (switch statements) are written this way so code
// coverage tooling will enforce that all these errors are reachable.
// Simple mapping of all the errors which can be thrown by the parser. The
// parsing errors are allotted numbers #100-#199.
static #errorMessages = new Map([
['#100', 'Markup at the start of your template could not be parsed.'],
['#101', 'Markup after content text found in your template could not be parsed.'],
['#102', 'Markup after a comment found in your template could not be parsed.'],
['#103', 'Markup after a content interpolation in your template could not be parsed.'],
['#104', 'Markup after the tag name in an opening tag in your template could not be parsed.'],
['#105', 'Markup after a spacing in an opening tag in your template could not be parsed.'],
['#106', 'Markup after an opening tag in your template could not be parsed.'],
['#107', 'Markup after a boolean attribute in an opening tag in your template could not be parsed.'],
['#108', 'Markup after an attribute in an opening tag in your template could not be parsed.'],
['#109', 'Markup after a boolean attribute interpolation in an opening tag in your template could not be parsed.'],
['#110', 'Markup after a defined attribute interpolation in an opening tag in your template could not be parsed.'],
['#111', 'Markup after an attribute interpolation in an opening tag in your template could not be parsed.'],
['#112', 'Markup after a property interpolation in an opening tag in your template could not be parsed.'],
['#113', 'Markup after the closing quote of an interpolated attribute or property in an opening tag in your template could not be parsed.'],
['#114', 'Markup after a closing tag in your template could not be parsed.'],
static #getErrorMessage(key) {
switch (key) {
case '#100': return 'Could not parse template markup (at template start).';
case '#101': return 'Could not parse template markup (after text content).';
case '#102': return 'Could not parse template markup (after a comment).';
case '#103': return 'Could not parse template markup (after interpolated content).';
case '#104': return 'Could not parse template markup (after a spacing within start tag).';
case '#105': return 'Could not parse template markup (after a start tag).';
case '#106': return 'Could not parse template markup (after a boolean attribute interpolation in a start tag).';
case '#107': return 'Could not parse template markup (after a defined attribute interpolation in a start tag).';
case '#108': return 'Could not parse template markup (after an attribute interpolation in a start tag).';
case '#109': return 'Could not parse template markup (after a property interpolation in a start tag).';
case '#110': return 'Could not parse template markup (after an end tag).';
['#120', 'Malformed open start tag — tag names must be alphanumeric, lowercase, cannot start or end with hyphens, and cannot start with a number.'],
['#121', 'Malformed open tag space — spaces in open tags must be literal whitespace characters or newlines. Inter-declaration spaces must be singular. Spaces after newlines may be used for indentation. Only one newline is allowed.'],
['#122', 'Malformed end to an opening tag — opening tags must close without any extraneous spaces or newlines.'],
['#123', 'Malformed close tag — close tags must not contain any extraneous spaces or newlines and tag names must be alphanumeric, lowercase, cannot start or end with hyphens, and cannot start with a number.'],
['#124', 'Malformed boolean attribute text — attribute names must be alphanumeric, must be lowercase, must not start or end with hyphens, and cannot start with a number — and, attribute values must be enclosed in double-quotes.'],
['#125', 'Malformed attribute text — attribute names must be alphanumeric, must be lowercase, must not start or end with hyphens, and cannot start with a number — and, attribute values must be enclosed in double-quotes.'],
['#126', 'Malformed boolean attribute interpolation — attribute names must be alphanumeric, must be lowercase, must not start or end with hyphens, and cannot start with a number — and, attribute values must be enclosed in double-quotes.'],
['#127', 'Malformed defined attribute interpolation — attribute names must be alphanumeric, must be lowercase, must not start or end with hyphens, and cannot start with a number — and, attribute values must be enclosed in double-quotes.'],
['#128', 'Malformed attribute interpolation — attribute names must be alphanumeric, must be lowercase, must not start or end with hyphens, and cannot start with a number — and, attribute values must be enclosed in double-quotes.'],
['#129', 'Malformed property interpolation — property names must be alphanumeric, must be lowercase, must not start or end with underscores, and cannot start with a number — and, property values must be enclosed in double-quotes.'],
['#130', 'Malformed closing quote to a bound attribute or property. Enclosing quotes must be simple, double-quotes.'],
case '#120': return 'Invalid tag name - refer to https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define#valid_custom_element_names).';
case '#121': return 'Invalid tag whitespace (extraneous whitespace in start tag).';
case '#122': return 'Invalid start tag (extraneous whitespace at close of start tag).';
case '#123': return 'Invalid end tag.';
case '#124': return 'Invalid tag attribute (must use kebab-case names and double-quoted values).';
case '#125': return 'Invalid tag attribute interpolation (must use kebab-case names and double-quoted values).';
case '#126': return 'Invalid tag property interpolation (must use kebab-case names and double-quoted values).';
case '#127': return 'Invalid closing quote on tag attribute or property.';
['#140', 'CDATA sections are forbidden. Use html entities (character encodings) instead.'],
case '#140': return 'CDATA sections are not supported. Use character references instead: https://developer.mozilla.org/en-US/docs/Glossary/Character_reference.';
['#150', 'Improper javascript escape (\\x, \\u, \\t, \\n, etc.) in raw template string. Only escapes to create a literal dollar (“$”), slash (“\\”), or back tick (“`”) is allowed. Only valid HTML entities (character references) are supported in html as code points. Use literal characters (e.g., newlines) to enter newlines in your templates.'],
['#151', 'Malformed hexadecimal character reference (html entity) or ambiguous ampersand.'],
['#152', 'Malformed html comment. Comments cannot start with “>” or “->” characters, they cannot include a set of “--” characters, and they cannot end with a “-” character.'],
['#153', 'Forbidden html element used — this parser is opinionated about which elements are allowed in order to reduce complexity and improve performance.'],
['#154', 'Unmatched closing tag at the end of your template. To avoid unintended markup, non-void tags must explicitly be closed.'],
['#155', 'Mismatched closing tag used. To avoid unintended markup, non-void tags must explicitly be closed and all closing tag names must be a case-sensitive match.'],
['#156', 'Forbidden, nontrivial interpolation of <textarea> tag used. Only basic interpolation is allowed — e.g., <textarea>${…}</textarea>.'],
['#157', 'Forbidden declarative shadow root used (e.g., `<template shadowrootmode="open">`).'],
]);
case '#150': return 'Bad escape in tagged template string. Use “&bsol;” for “\\”, “&dollar;” for “\\$”, and “&grave;” for “\\`”. All character references are supported: https://developer.mozilla.org/en-US/docs/Glossary/Character_reference.';
case '#151': return 'Ambiguous ampersand character or invalid hexadecimal character reference.';
case '#152': return 'Invalid comment. Comments cannot start with “>” or “->” characters, they cannot include a set of “--” characters, and they cannot end with a “-” character.';
case '#153': return 'Unsupported native tag - supported native tags are listed here: https://github.com/Netflix/x-element/blob/main/doc/TEMPLATES.md#supported-native-tags.';
case '#154': return 'Invalid end tag (all non-void start tags much have matching end tags).';
case '#155': return 'Unsupported <textarea> interpolation. Interpolation must be exact (<textarea>${…}</textarea>).';
case '#156': return 'Unsupported declarative shadow root on <template>. The “shadowrootmode” attribute is not supported.';
case '#157': return 'Missing closing quote on bound attribute or property.';
}
}
// Block #100-#119 — Invalid transition errors.
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'],
]);
static #getErrorMessageKeyFromValue(value) {
// Note that the following states _never_ have their own generic errors. The
// main reason is because they all encode a lookahead which is a subset of
// a related “*Malformed” pattern.
// - XParser.#startTagOpen (caught by XParser.#startTagSpaceMalformed)
// - XParser.#boolean (caught by XParser.#startTagSpaceMalformed)
// - XParser.#attribute (caught by XParser.#startTagSpaceMalformed)
// - XParser.#danglingQuote (caught by XParser.#startTagSpaceMalformed)
switch (value) {
case XParser.#initial: return '#100';
case XParser.#text: return '#101';
case XParser.#comment: return '#102';
case XParser.#boundContent: return '#103';
case XParser.#startTagSpace: return '#104';
case XParser.#startTagClose: return '#105';
case XParser.#boundBoolean: return '#106';
case XParser.#boundDefined: return '#107';
case XParser.#boundAttribute: return '#108';
case XParser.#boundProperty: return '#109';
case XParser.#endTag: return '#110';
}
}
// Block #120-#139 — Common mistakes.
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'],
]);
static #getErrorMessageKeyFromValueMalformed(valueMalformed) {
switch (valueMalformed) {
case XParser.#startTagOpenMalformed: return '#120';
case XParser.#startTagSpaceMalformed: return '#121';
case XParser.#startTagCloseMalformed: return '#122';
case XParser.#endTagMalformed: return '#123';
case XParser.#booleanMalformed: return '#124';
case XParser.#attributeMalformed: return '#124';
case XParser.#boundBooleanMalformed: return '#125';
case XParser.#boundDefinedMalformed: return '#125';
case XParser.#boundAttributeMalformed: return '#125';
case XParser.#boundPropertyMalformed: return '#126';
case XParser.#danglingQuoteMalformed: return '#127';
}
}
// Block #140-#149 — Forbidden transitions.
static #valueForbiddenToErrorMessagesKey = new Map([
[XParser.#cdataStart, '#140'],
]);
static #getErrorMessageKeyFromValueForbidden(valueForbidden) {
switch (valueForbidden) {
case XParser.#cdataStart: return '#140';
}
}
// Block #150+ — Special, named issues.
static #namedErrorsToErrorMessagesKey = new Map([
['javascript-escape', '#150'],
['malformed-html-entity', '#151'],
['malformed-comment', '#152'],
['forbidden-html-element', '#153'],
['missing-closing-tag', '#154'],
['mismatched-closing-tag', '#155'],
['complex-textarea-interpolation', '#156'],
['declarative-shadow-root', '#157'],
]);
static #getErrorMessageKeyFromErrorName(errorName) {
switch (errorName) {
case 'javascript-escape': return '#150';
case 'malformed-html-entity': return '#151';
case 'malformed-comment': return '#152';
case 'forbidden-html-element': return '#153';
case 'missing-end-tag': return '#154';
case 'mismatched-end-tag': return '#154';
case 'complex-textarea-interpolation': return '#155';
case 'declarative-shadow-root': return '#156';
case 'missing-closing-quote': return '#157';
}
}

@@ -403,2 +391,3 @@ //////////////////////////////////////////////////////////////////////////////

case XParser.#startTagSpace: return XParser.#try(string, stringIndex,
XParser.#startTagSpaceMalformed,
XParser.#booleanMalformed,

@@ -554,24 +543,33 @@ XParser.#attributeMalformed,

const valueMalformed = XParser.#invalidTransition(string, stringIndex, value);
const errorMessagesKey = valueForbidden
? XParser.#valueForbiddenToErrorMessagesKey.get(valueForbidden)
const errorMessageKey = valueForbidden
? XParser.#getErrorMessageKeyFromValueForbidden(valueForbidden)
: valueMalformed
? XParser.#valueMalformedToErrorMessagesKey.get(valueMalformed)
: XParser.#valueToErrorMessagesKey.get(value);
const errorMessage = XParser.#errorMessages.get(errorMessagesKey);
? XParser.#getErrorMessageKeyFromValueMalformed(valueMalformed)
: XParser.#getErrorMessageKeyFromValue(value);
const errorMessage = XParser.#getErrorMessage(errorMessageKey);
const substringMessage = `See substring \`${notParsed}\`.`;
const parsedThroughMessage = `Your HTML was parsed through: \`${parsed}\`.`;
const message = `[${errorMessagesKey}] ${errorMessage}\n${substringMessage}\n${parsedThroughMessage}`;
const message = `[${errorMessageKey}] ${errorMessage}\n${substringMessage}\n${parsedThroughMessage}`;
throw new Error(message);
}
// This validates a value from our “strings.raw” array passed into our tagged
// template function. It checks to make sure superfluous, JS-y escapes are
// not being used as html (since there are perfectly-valid alternatives).
// Character escapes like “\n”, “\u” or ”\x” are a JS-ism. We want developers
// to use HTML here, not JS. You can of course interpolate whatever you want.
// https://w3c.github.io/html-reference/syntax.html#character-encoding
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Character_escape
// Note that syntax highlighters expect the text after the “html” tag to be
// real HTML. Another reason to reject JS-y unicode is that it won’t be
// interpreted correctly by tooling that expects _html_.
// Escapes for the “$”, “\”, and “`” characters are required within a template
// literal — users will need to use “&dollar;”, “&bsol;”, and “&grave;”.
// Examples:
// - ok: html`&#8230;`, html`&#x2026;`, html`&mldr;`, html`&hellip;`
// - not ok: html`\nhi\nthere`, html`\x8230`, html`\u2026`, html`\s\t\o\p\ \i\t\.`
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}`;
const backslashIndex = rawString.indexOf('\\');
if (backslashIndex !== -1) {
const errorMessageKey = XParser.#getErrorMessageKeyFromErrorName('javascript-escape');
const errorMessage = XParser.#getErrorMessage(errorMessageKey);
const substringMessage = `See (raw) substring \`${rawString.slice(0, backslashIndex + 1)}\`.`;
const message = `[${errorMessageKey}] ${errorMessage}\n${substringMessage}`;
throw new Error(message);

@@ -583,8 +581,19 @@ }

// have been matched successfully to prevent any unexpected behavior.
static #validateExit(tagName) {
static #validateExit(value, tagName) {
switch (value) {
case XParser.#boundBoolean:
case XParser.#boundDefined:
case XParser.#boundAttribute:
case XParser.#boundProperty: {
const errorMessageKey = XParser.#getErrorMessageKeyFromErrorName('missing-closing-quote');
const errorMessage = XParser.#getErrorMessage(errorMessageKey);
const substringMessage = `Missing closing double-quote.`;
throw new Error(`[${errorMessageKey}] ${errorMessage}\n${substringMessage}`);
}
}
if (tagName) {
const errorMessagesKey = XParser.#namedErrorsToErrorMessagesKey.get('missing-closing-tag');
const errorMessage = XParser.#errorMessages.get(errorMessagesKey);
const errorMessageKey = XParser.#getErrorMessageKeyFromErrorName('missing-end-tag');
const errorMessage = XParser.#getErrorMessage(errorMessageKey);
const substringMessage = `Missing a closing </${tagName}>.`;
throw new Error(`[${errorMessagesKey}] ${errorMessage}\n${substringMessage}`);
throw new Error(`[${errorMessageKey}] ${errorMessage}\n${substringMessage}`);
}

@@ -618,6 +627,6 @@ }

if (!XParser.#entity.test(content)) {
const errorMessagesKey = XParser.#namedErrorsToErrorMessagesKey.get('malformed-html-entity');
const errorMessage = XParser.#errorMessages.get(errorMessagesKey);
const errorMessageKey = XParser.#getErrorMessageKeyFromErrorName('malformed-html-entity');
const errorMessage = XParser.#getErrorMessage(errorMessageKey);
const substringMessage = `See substring \`${content}\`.`;
throw new Error(`[${errorMessagesKey}] ${errorMessage}\n${substringMessage}`);
throw new Error(`[${errorMessageKey}] ${errorMessage}\n${substringMessage}`);
}

@@ -652,6 +661,6 @@ referenceEnd = XParser.#entity.lastIndex;

) {
const errorMessagesKey = XParser.#namedErrorsToErrorMessagesKey.get('forbidden-html-element');
const errorMessage = XParser.#errorMessages.get(errorMessagesKey);
const errorMessageKey = XParser.#getErrorMessageKeyFromErrorName('forbidden-html-element');
const errorMessage = XParser.#getErrorMessage(errorMessageKey);
const substringMessage = `The <${tagName}> html element is forbidden.`;
throw new Error(`[${errorMessagesKey}] ${errorMessage}\n${substringMessage}`);
throw new Error(`[${errorMessageKey}] ${errorMessage}\n${substringMessage}`);
}

@@ -664,5 +673,5 @@ }

if (tagName === 'template' && attributeName === 'shadowrootmode') {
const errorMessagesKey = XParser.#namedErrorsToErrorMessagesKey.get('declarative-shadow-root');
const errorMessage = XParser.#errorMessages.get(errorMessagesKey);
throw new Error(`[${errorMessagesKey}] ${errorMessage}`);
const errorMessageKey = XParser.#getErrorMessageKeyFromErrorName('declarative-shadow-root');
const errorMessage = XParser.#getErrorMessage(errorMessageKey);
throw new Error(`[${errorMessageKey}] ${errorMessage}`);
}

@@ -681,5 +690,5 @@ }

if (sloppyStartInterpolation || !string.startsWith(`</textarea>`)) {
const errorMessagesKey = XParser.#namedErrorsToErrorMessagesKey.get('complex-textarea-interpolation');
const errorMessage = XParser.#errorMessages.get(errorMessagesKey);
throw new Error(`[${errorMessagesKey}] ${errorMessage}`);
const errorMessageKey = XParser.#getErrorMessageKeyFromErrorName('complex-textarea-interpolation');
const errorMessage = XParser.#getErrorMessage(errorMessageKey);
throw new Error(`[${errorMessageKey}] ${errorMessage}`);
}

@@ -712,6 +721,6 @@ onToken(XParser.tokenTypes.boundTextValue, stringsIndex, stringIndex, stringIndex, '');

if (data.startsWith('>') || data.startsWith('->') || data.includes('--') || data.endsWith('-')) {
const errorMessagesKey = XParser.#namedErrorsToErrorMessagesKey.get('malformed-comment');
const errorMessage = XParser.#errorMessages.get(errorMessagesKey);
const errorMessageKey = XParser.#getErrorMessageKeyFromErrorName('malformed-comment');
const errorMessage = XParser.#getErrorMessage(errorMessageKey);
const substringMessage = `See substring \`${string.slice(stringIndex, nextStringIndex)}\`.`;
throw new Error(`[${errorMessagesKey}] ${errorMessage}\n${substringMessage}`);
throw new Error(`[${errorMessageKey}] ${errorMessage}\n${substringMessage}`);
}

@@ -855,5 +864,5 @@ onToken(XParser.tokenTypes.comment, stringsIndex, commentStart, commentEnd, data);

} else {
const errorMessagesKey = XParser.#namedErrorsToErrorMessagesKey.get('complex-textarea-interpolation');
const errorMessage = XParser.#errorMessages.get(errorMessagesKey);
throw new Error(`[${errorMessagesKey}] ${errorMessage}`);
const errorMessageKey = XParser.#getErrorMessageKeyFromErrorName('complex-textarea-interpolation');
const errorMessage = XParser.#getErrorMessage(errorMessageKey);
throw new Error(`[${errorMessageKey}] ${errorMessage}`);
}

@@ -874,7 +883,7 @@ }

const { parsed } = XParser.#getErrorInfo(strings, stringsIndex, string, stringIndex);
const errorMessagesKey = XParser.#namedErrorsToErrorMessagesKey.get('mismatched-closing-tag');
const errorMessage = XParser.#errorMessages.get(errorMessagesKey);
const errorMessageKey = XParser.#getErrorMessageKeyFromErrorName('mismatched-end-tag');
const errorMessage = XParser.#getErrorMessage(errorMessageKey);
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}`);
throw new Error(`[${errorMessageKey}] ${errorMessage}\n${substringMessage}\n${parsedThroughMessage}`);
}

@@ -1090,3 +1099,3 @@ onToken(XParser.tokenTypes.endTagOpen, stringsIndex, stringIndex, endTagNameStart, '</');

XParser.#validateExit(tagName);
XParser.#validateExit(value, tagName);
} catch (error) {

@@ -1093,0 +1102,0 @@ // Roughly match the conventions for “onToken”.

@@ -317,109 +317,2 @@ import { XParser } from './x-parser.js';

// Validates array item or map entry and returns an “id” and a “rawResult”.
static #parseListValue(value, index, category, ids) {
if (category === 'array') {
// Values should look like "<raw result>".
const id = String(index);
const rawResult = value;
ids.add(id);
if (!TemplateEngine.#isRawResult(rawResult)) {
throw new Error(`Unexpected non-template value found in array item at ${index} "${rawResult}".`);
}
return [id, rawResult];
} else {
// Values should look like "[<key>, <raw result>]".
if (value.length !== 2) {
throw new Error(`Unexpected entry length found in map entry at ${index} with length "${value.length}".`);
}
const [id, rawResult] = value;
if (typeof id !== 'string') {
throw new Error(`Unexpected non-string key found in map entry at ${index} "${id}".`);
}
if (ids.has(id)) {
throw new Error(`Unexpected duplicate key found in map entry at ${index} "${id}".`);
}
ids.add(id);
if (!TemplateEngine.#isRawResult(rawResult)) {
throw new Error(`Unexpected non-template value found in map entry at ${index} "${rawResult}".`);
}
return [id, rawResult];
}
}
// TODO: #254: Use new “moveBefore” when available with cross-browser support.
// This enables us to preserve things like animations and prevent node
// disconnects. See https://chromestatus.com/feature/5135990159835136
// Loops over given value array to either create-or-update a list of nodes.
static #list(node, startNode, values, category) {
const arrayState = TemplateEngine.#getState(startNode, TemplateEngine.#ARRAY_STATE);
if (!arrayState.map) {
// There is no mapping in our state — we have a clean slate to work with.
TemplateEngine.#clearObject(arrayState);
arrayState.map = new Map();
const ids = new Set(); // Populated in “parseListValue”.
let index = 0;
for (const value of values) {
const [id, rawResult] = TemplateEngine.#parseListValue(value, index, category, ids);
const cursors = TemplateEngine.#createCursors(node);
const preparedResult = TemplateEngine.#inject(rawResult, cursors.node, true);
arrayState.map.set(id, { id, preparedResult, ...cursors });
index++;
}
} else {
// TODO: Can we refactor this all into a _single_ loop? Right now, we do
// the following:
// 1. Loop once to add new things.
// 2. Loop a second time to remove old things.
// 3. Loop a third time to reorder (if we have a mapping).
// A mapping has already been created — we need to update the items.
const ids = new Set(); // Populated in “parseListValue”.
let index = 0;
for (const value of values) {
const [id, rawResult] = TemplateEngine.#parseListValue(value, index, category, ids);
let item = arrayState.map.get(id);
if (item) {
if (!TemplateEngine.#canReuseDom(item.preparedResult, rawResult)) {
// Add new comment cursors before removing old comment cursors.
const cursors = TemplateEngine.#createCursors(item.startNode);
TemplateEngine.#removeThrough(item.startNode, item.node);
item.preparedResult = TemplateEngine.#inject(rawResult, cursors.node, true);
item.startNode = cursors.startNode;
item.node = cursors.node;
} else {
TemplateEngine.#update(item.preparedResult, rawResult);
}
} else {
const cursors = TemplateEngine.#createCursors(node);
const preparedResult = TemplateEngine.#inject(rawResult, cursors.node, true);
item = { id, preparedResult, ...cursors };
arrayState.map.set(id, item);
}
index++;
}
for (const [id, item] of arrayState.map.entries()) {
if (!ids.has(id)) {
TemplateEngine.#removeThrough(item.startNode, item.node);
arrayState.map.delete(id);
}
}
let lastItem;
for (const id of ids) {
const item = arrayState.map.get(id);
// TODO: We should be able to make the following code more performant.
if (category === 'map') {
const referenceNode = lastItem ? lastItem.node.nextSibling : startNode.nextSibling;
if (referenceNode !== item.startNode) {
const nodesToMove = [item.startNode];
while (nodesToMove[nodesToMove.length - 1] !== item.node) {
nodesToMove.push(nodesToMove[nodesToMove.length - 1].nextSibling);
}
TemplateEngine.#insertAllBefore(referenceNode.parentNode, referenceNode, nodesToMove);
}
}
lastItem = item;
}
}
}
static #commitAttribute(node, name, value, lastValue) {

@@ -481,36 +374,221 @@ const update = TemplateEngine.#symbolToUpdate.get(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);
static #commitContentResultValue(node, startNode, value) {
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);
}
}
// Validates array value and returns a “rawResult”.
static #parseArrayValue(value, index) {
// Values should look like "<raw result>".
const rawResult = value;
if (!TemplateEngine.#isRawResult(rawResult)) {
throw new Error(`Unexpected non-template value found in array item at ${index} "${rawResult}".`);
}
return rawResult;
}
// Validates array entry and returns an “id” and a “rawResult”.
static #parseArrayEntry(entry, index, ids) {
// Entries should look like "[<key>, <raw result>]".
if (entry.length !== 2) {
throw new Error(`Unexpected entry length found in map entry at ${index} with length "${entry.length}".`);
}
const [id, rawResult] = entry;
if (typeof id !== 'string') {
throw new Error(`Unexpected non-string key found in map entry at ${index} "${id}".`);
}
if (ids.has(id)) {
throw new Error(`Unexpected duplicate key found in map entry at ${index} "${id}".`);
}
ids.add(id);
if (!TemplateEngine.#isRawResult(rawResult)) {
throw new Error(`Unexpected non-template value found in map entry at ${index} "${rawResult}".`);
}
return [id, rawResult];
}
// Helper to create / insert “cursors” in managed array of nodes.
static #createArrayItem(node, id, rawResult) {
const cursors = TemplateEngine.#createCursors(node);
const preparedResult = TemplateEngine.#inject(rawResult, cursors.node, true);
return { id, preparedResult, ...cursors };
}
// Helper to destroy, create, and replace “cursors” in managed array of nodes.
static #recreateArrayItem(item, rawResult) {
// Add new comment cursors before removing old comment cursors.
const cursors = TemplateEngine.#createCursors(item.startNode);
TemplateEngine.#removeThrough(item.startNode, item.node);
item.preparedResult = TemplateEngine.#inject(rawResult, cursors.node, true);
item.startNode = cursors.startNode;
item.node = cursors.node;
}
// Loops over given array of “values” to manage an array of nodes.
static #commitContentArrayValues(node, startNode, values) {
const arrayState = TemplateEngine.#getState(startNode, TemplateEngine.#ARRAY_STATE);
if (!arrayState.map) {
// There is no mapping in our state — create an empty one as our base.
TemplateEngine.#clearObject(arrayState);
arrayState.map = new Map();
}
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;
if (values.length > 0 && arrayState.map.size > 0) {
// Update existing values.
for (let index = 0; index < Math.min(arrayState.map.size, values.length); index++) {
const id = String(index);
const value = values[index];
const rawResult = TemplateEngine.#parseArrayValue(value, index);
const item = arrayState.map.get(id);
if (!TemplateEngine.#canReuseDom(item.preparedResult, rawResult)) {
TemplateEngine.#recreateArrayItem(item, rawResult);
} else {
TemplateEngine.#update(item.preparedResult, rawResult);
}
}
}
if (values.length > arrayState.map.size) {
// Add new values.
for (let index = arrayState.map.size; index < values.length; index++) {
const id = String(index);
const value = values[index];
const rawResult = TemplateEngine.#parseArrayValue(value, index);
const item = TemplateEngine.#createArrayItem(node, id, rawResult);
arrayState.map.set(id, item);
}
}
if (arrayState.map.size > values.length) {
// Delete removed values.
const index = values.length;
const id = String(index);
const item = arrayState.map.get(id);
TemplateEngine.#removeThrough(item.startNode, node);
arrayState.map.delete(id);
}
}
// Loops over given array of “entries” to manage an array of nodes.
static #commitContentArrayEntries(node, startNode, entries) {
const arrayState = TemplateEngine.#getState(startNode, TemplateEngine.#ARRAY_STATE);
if (!arrayState.map) {
// There is no mapping in our state — create an empty one as our base.
TemplateEngine.#clearObject(arrayState);
arrayState.map = new Map();
}
// A mapping has already been created — we need to update the items.
const ids = new Set(); // Populated in “parseListValue”.
let index = 0;
for (const entry of entries) {
const [id, rawResult] = TemplateEngine.#parseArrayEntry(entry, index, ids);
let item = arrayState.map.get(id);
if (item) {
if (!TemplateEngine.#canReuseDom(item.preparedResult, rawResult)) {
TemplateEngine.#recreateArrayItem(item, rawResult);
} else {
TemplateEngine.#update(item.preparedResult, rawResult);
}
} else {
TemplateEngine.#update(state.preparedResult, rawResult);
item = TemplateEngine.#createArrayItem(node, id, rawResult);
arrayState.map.set(id, item);
}
} 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.`);
index++;
}
for (const [id, item] of arrayState.map.entries()) {
if (!ids.has(id)) {
TemplateEngine.#removeThrough(item.startNode, item.node);
arrayState.map.delete(id);
}
const previousSibling = node.previousSibling;
if (previousSibling !== startNode) {
TemplateEngine.#removeBetween(startNode, node);
}
let lastItem;
for (const id of ids) {
const item = arrayState.map.get(id);
// TODO: We should be able to make the following code more performant.
const referenceNode = lastItem ? lastItem.node.nextSibling : startNode.nextSibling;
if (referenceNode !== item.startNode) {
const nodesToMove = [item.startNode];
while (nodesToMove[nodesToMove.length - 1] !== item.node) {
nodesToMove.push(nodesToMove[nodesToMove.length - 1].nextSibling);
}
TemplateEngine.#insertAllBefore(referenceNode.parentNode, referenceNode, nodesToMove);
}
node.parentNode.insertBefore(value, node);
} else {
lastItem = item;
}
}
// TODO: #254: Future state where the “moveBefore” API is better-supported.
// // Loops over given array of “entries” to manage an array of nodes.
// static #commitContentArrayEntries(node, startNode, entries) {
// const arrayState = TemplateEngine.#getState(startNode, TemplateEngine.#ARRAY_STATE);
// if (!arrayState.map) {
// // There is no mapping in our state — create an empty one as our base.
// TemplateEngine.#clearObject(arrayState);
// arrayState.map = new Map();
// }
//
// const idsToRemove = new Set(arrayState.map.keys());
// const ids = new Set(); // Populated in “parseArrayEntry”.
// let reference = startNode.nextSibling;
// for (let index = 0; index < entries.length; index++) {
// const entry = entries[index];
// const [id, rawResult] = TemplateEngine.#parseArrayEntry(entry, index, ids);
// let item = arrayState.map.get(id);
// if (item) {
// // Update existing item.
// idsToRemove.delete(id);
// if (!TemplateEngine.#canReuseDom(item.preparedResult, rawResult)) {
// const referenceWasStartNode = reference === item.startNode;
// TemplateEngine.#recreateArrayItem(item, rawResult);
// reference = referenceWasStartNode ? item.startNode : reference;
// } else {
// TemplateEngine.#update(item.preparedResult, rawResult);
// }
// } else {
// // Create new item.
// item = TemplateEngine.#createArrayItem(node, id, rawResult);
// arrayState.map.set(id, item);
// }
// // Move to the correct location
// if (item.startNode !== reference) {
// const nodesToMove = [item.startNode];
// while (nodesToMove[nodesToMove.length - 1] !== item.node) {
// nodesToMove.push(nodesToMove[nodesToMove.length - 1].nextSibling);
// }
// TemplateEngine.#moveAllBefore(reference.parentNode, reference, nodesToMove);
// }
//
// // Move our position forward.
// reference = item.node.nextSibling;
// }
//
// // Remove any ids which are not longer in the entries.
// for (const id of idsToRemove) {
// const item = arrayState.map.get(id);
// TemplateEngine.#removeThrough(item.startNode, item.node);
// arrayState.map.delete(id);
// }
// }
static #commitContentFragmentValue(node, startNode, value) {
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);
}
static #commitContentTextValue(node, startNode, value) {
// TODO: Is there a way to more-performantly skip this init step? E.g., if

@@ -531,3 +609,22 @@ // the prior value here was not “unset” and we didn’t just reset? We

}
}
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);
}
switch (category) {
case 'result': TemplateEngine.#commitContentResultValue(node, startNode, value); break;
case 'array': TemplateEngine.#commitContentArrayValues(node, startNode, value); break;
case 'map': TemplateEngine.#commitContentArrayEntries(node, startNode, value); break;
case 'fragment': TemplateEngine.#commitContentFragmentValue(node, startNode, value); break;
default: TemplateEngine.#commitContentTextValue(node, startNode, value); break;
}
}

@@ -684,2 +781,13 @@

// TODO: #254: Future state when we leverage “moveBefore”.
// static #moveAllBefore(parentNode, referenceNode, nodes) {
// // Iterate backwards over the live node collection since we’re mutating it.
// // Note that passing “null” as the reference node moves nodes to the end.
// for (let iii = nodes.length - 1; iii >= 0; iii--) {
// const node = nodes[iii];
// parentNode.moveBefore(node, referenceNode);
// referenceNode = node;
// }
// }
static #insertAllBefore(parentNode, referenceNode, nodes) {

@@ -686,0 +794,0 @@ // Iterate backwards over the live node collection since we’re mutating it.

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