diff --git a/test/test-template-engine.js b/test/test-template-engine.js
index d257935..5cd2657 100644
--- a/test/test-template-engine.js
+++ b/test/test-template-engine.js
@@ -19,6 +19,7 @@ const localMessages = [
'Deprecated "unsafeSVG" from default templating engine interface.',
'Deprecated "repeat" from default templating engine interface.',
'Deprecated "map" from default templating engine interface.',
+ 'Support for the "style" tag is deprecated and will be removed in future versions.',
];
console.warn = (...args) => { // eslint-disable-line no-console
if (!localMessages.includes(args[0]?.message)) {
@@ -29,13 +30,17 @@ console.warn = (...args) => { // eslint-disable-line no-console
};
// Simple helper for asserting thrown messages.
-const assertThrows = (callback, expectedMessage) => {
+const assertThrows = (callback, expectedMessage, options) => {
let thrown = false;
try {
callback();
} catch (error) {
thrown = true;
- assert(error.message === expectedMessage, error.message);
+ if (options?.startsWith === true) {
+ assert(error.message.startsWith(expectedMessage), error.message);
+ } else {
+ assert(error.message === expectedMessage, error.message);
+ }
}
assert(thrown, 'no error was thrown');
};
@@ -80,6 +85,20 @@ describe('html rendering', () => {
assert(container.children[0].getAttribute('foo') === `--{<&>'"}--`);
});
+ it('renders named html entities which require surrogate pairs', () => {
+ const container = document.createElement('div');
+ render(container, html`
--𝕓𝕓--𝕓--
`);
+ assert(container.childElementCount === 1);
+ assert(container.children[0].textContent === `--\uD835\uDD53\uD835\uDD53--\uD835\uDD53--`);
+ });
+
+ it('renders malformed, named html entities', () => {
+ const container = document.createElement('div');
+ render(container, html`--&:^);--
`);
+ assert(container.childElementCount === 1);
+ assert(container.children[0].textContent === `--&:^);--`);
+ });
+
it('renders surprisingly-accepted characters in text', () => {
const container = document.createElement('div');
render(container, html`>'"&& & &&`);
@@ -654,18 +673,6 @@ describe('html rendering', () => {
assert(container.querySelector('textarea').value === 'foo');
});
- it('title elements with no interpolation work', () => {
- const container = document.createElement('div');
- render(container, html`this is the “default” value`);
- assert(container.querySelector('title').textContent === 'this is the “default” value');
- });
-
- it('title elements with strict interpolation work', () => {
- const container = document.createElement('div');
- render(container, html`${'foo'}`);
- assert(container.querySelector('title').textContent === 'foo');
- });
-
it('renders instantiated elements as dumb text', () => {
const getTemplate = ({ element }) => {
return html`${element}`;
@@ -775,24 +782,12 @@ describe('html rendering', () => {
#item = null;
set item(value) { updates.push(`outer-${value}`); this.#item = value; }
get item() { return this.#item; }
- connectedCallback() {
- // Prevent property shadowing by deleting before setting on connect.
- const item = this.item ?? '???';
- Reflect.deleteProperty(this, 'item');
- Reflect.set(this, 'item', item);
- }
}
customElements.define('test-depth-first-outer', TestDepthFirstOuter);
class TestDepthFirstInner extends HTMLElement {
#item = null;
set item(value) { updates.push(`inner-${value}`); this.#item = value; }
get item() { return this.#item; }
- connectedCallback() {
- // Prevent property shadowing by deleting before setting on connect.
- const item = this.item ?? '???';
- Reflect.deleteProperty(this, 'item');
- Reflect.set(this, 'item', item);
- }
}
customElements.define('test-depth-first-inner', TestDepthFirstInner);
@@ -1099,91 +1094,62 @@ describe('html errors', () => {
div { background-color: ${'red'}; }
`;
- const expectedMessage = 'Interpolation of
+ Unforgiving.#throughStyle.lastIndex = nextStringIndex;
+ if (Unforgiving.#throughStyle.test(string)) {
+ const content = string.slice(nextStringIndex, Unforgiving.#throughStyle.lastIndex - closeTagLength);
+ element.value.textContent = content;
+ } else {
+ const errorMessagesKey = Unforgiving.#namedErrorsToErrorMessagesKey.get('style-interpolation');
+ const errorMessage = Unforgiving.#errorMessages.get(errorMessagesKey);
+ throw new Error(`[${errorMessagesKey}] ${errorMessage}`);
+ }
+ childNodesIndex.value = path.pop();
+ element.value = element.value[Unforgiving.#parentNode];
+ Unforgiving.#closeTag.lastIndex = Unforgiving.#throughStyle.lastIndex;
+ return Unforgiving.#closeTag;
+ }
+
+ static #addUnboundContent(string, stringIndex, element, childNodesIndex, nextStringIndex) {
+ const encoded = string.slice(stringIndex, nextStringIndex);
+ const decoded = Unforgiving.#replaceHtmlEntities(encoded);
+ element.value.appendChild(document.createTextNode(decoded));
+ childNodesIndex.value += 1;
+ }
+
+ static #addUnboundComment(string, stringIndex, element, childNodesIndex, nextStringIndex) {
+ const content = string.slice(stringIndex, nextStringIndex);
+ const data = content.slice(4, -3);
+ // https://w3c.github.io/html-reference/syntax.html#comments
+ if (data.startsWith('>') || data.startsWith('->') || data.includes('--') || data.endsWith('-')) {
+ const errorMessagesKey = Unforgiving.#namedErrorsToErrorMessagesKey.get('malformed-comment');
+ const errorMessage = Unforgiving.#errorMessages.get(errorMessagesKey);
+ const substringMessage = `See substring \`${content}\`.`;
+ throw new Error(`[${errorMessagesKey}] ${errorMessage}\n${substringMessage}`);
+ }
+ element.value.appendChild(document.createComment(data));
+ childNodesIndex.value += 1;
+ }
+
+ static #addBoundContent(onContent, path, element, childNodesIndex) {
+ element.value.append(document.createComment(''), document.createComment(''));
+ childNodesIndex.value += 2;
+ path.push(childNodesIndex.value);
+ onContent(path);
+ path.pop();
+ }
+
+ // This can only happen with a “textarea” element, currently.
+ static #addBoundText(onText, string, path, element, sloppyStartInterpolation) {
+ // If the prior match isn’t our opening tag… that’s a problem. If the next
+ // match isn’t our closing tag… that’s also a problem.
+ // Because we tightly control the end-tag format, we can predict what the
+ // next string’s prefix should be.
+ if (sloppyStartInterpolation || !string.startsWith(``)) {
+ const errorMessagesKey = Unforgiving.#namedErrorsToErrorMessagesKey.get('complex-textarea-interpolation');
+ const errorMessage = Unforgiving.#errorMessages.get(errorMessagesKey);
+ throw new Error(`[${errorMessagesKey}] ${errorMessage}`);
+ }
+ onText(path);
+ }
+
+ static #addUnboundBoolean(string, stringIndex, element, nextStringIndex) {
+ const attributeName = string.slice(stringIndex, nextStringIndex);
+ element.value.setAttribute(attributeName, '');
+ }
+
+ static #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 = Unforgiving.#replaceHtmlEntities(encoded);
+ element.value.setAttribute(attributeName, decoded);
+ }
+
+ static #addBoundBoolean(onBoolean, string, stringIndex, path, nextStringIndex) {
+ const boundBoolean = string.slice(stringIndex, nextStringIndex);
+ const equalsIndex = boundBoolean.indexOf('=');
+ const attributeName = boundBoolean.slice(1, equalsIndex);
+ onBoolean(attributeName, path);
+ }
+
+ static #addBoundDefined(onDefined, string, stringIndex, path, nextStringIndex) {
+ const boundDefined = string.slice(stringIndex, nextStringIndex);
+ const equalsIndex = boundDefined.indexOf('=');
+ const attributeName = boundDefined.slice(2, equalsIndex);
+ onDefined(attributeName, path);
+ }
+
+ static #addBoundAttribute(onAttribute, string, stringIndex, path, nextStringIndex) {
+ const boundAttribute = string.slice(stringIndex, nextStringIndex);
+ const equalsIndex = boundAttribute.indexOf('=');
+ const attributeName = boundAttribute.slice(0, equalsIndex);
+ onAttribute(attributeName, path);
+ }
+
+ static #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);
+ }
+
+ static #validateTagName(namespace, tagName) {
+ switch (namespace) {
+ case Unforgiving.html:
+ if (
+ tagName.indexOf('-') === -1 &&
+ !Unforgiving.#allowedHtmlElements.has(tagName)
+ ) {
+ const errorMessagesKey = Unforgiving.#namedErrorsToErrorMessagesKey.get('forbidden-html-element');
+ const errorMessage = Unforgiving.#errorMessages.get(errorMessagesKey);
+ const substringMessage = `The <${tagName}> html element is forbidden.`;
+ throw new Error(`[${errorMessagesKey}] ${errorMessage}\n${substringMessage}`);
+ }
+ break;
+ case Unforgiving.svg:
+ if (!Unforgiving.#allowedSvgElements.has(tagName)) {
+ const errorMessagesKey = Unforgiving.#namedErrorsToErrorMessagesKey.get('forbidden-svg-element');
+ const errorMessage = Unforgiving.#errorMessages.get(errorMessagesKey);
+ const substringMessage = `The <${tagName}> svg element is forbidden.`;
+ throw new Error(`[${errorMessagesKey}] ${errorMessage}\n${substringMessage}`);
+ }
+ break;
+ case Unforgiving.math:
+ if (!Unforgiving.#allowedMathElements.has(tagName)) {
+ const errorMessagesKey = Unforgiving.#namedErrorsToErrorMessagesKey.get('forbidden-math-element');
+ const errorMessage = Unforgiving.#errorMessages.get(errorMessagesKey);
+ const substringMessage = `The <${tagName}> math element is forbidden.`;
+ throw new Error(`[${errorMessagesKey}] ${errorMessage}\n${substringMessage}`);
+ }
+ break;
+ }
+ }
+
+ static #addElement(string, stringIndex, path, element, childNodesIndex, nextStringIndex) {
+ const prefixedTagName = string.slice(stringIndex, nextStringIndex);
+ const tagName = prefixedTagName.slice(1);
+ const currentNamespace = element.value[Unforgiving.#namespace];
+ Unforgiving.#validateTagName(currentNamespace, tagName);
+ let namespace;
+ switch (tagName) {
+ case 'svg': namespace = Unforgiving.svg; break;
+ case 'math': namespace = Unforgiving.math; break;
+ default: namespace = currentNamespace; break;
+ }
+ const childNode = document.createElementNS(namespace, tagName);
+ element.value[Unforgiving.#localName] === 'template'
+ ? element.value.content.appendChild(childNode)
+ : element.value.appendChild(childNode);
+ childNode[Unforgiving.#localName] = tagName;
+ childNode[Unforgiving.#parentNode] = element.value;
+ childNode[Unforgiving.#namespace] = namespace;
+ element.value = childNode;
+ childNodesIndex.value += 1;
+ path.push(childNodesIndex.value);
+ }
+
+ static #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[Unforgiving.#localName];
+ if (tagName !== expectedTagName) {
+ const { parsed } = Unforgiving.#getErrorInfo(strings, stringsIndex, string, stringIndex);
+ const errorMessagesKey = Unforgiving.#namedErrorsToErrorMessagesKey.get('mismatched-closing-tag');
+ const errorMessage = Unforgiving.#errorMessages.get(errorMessagesKey);
+ const substringMessage = `The closing tag ${tagName}> does not match <${expectedTagName}>.`;
+ 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[Unforgiving.#parentNode];
+ }
+
+ static #styleDeprecationWarning() {
+ if (!Unforgiving.#hasWarnedAboutStyleDeprecation) {
+ Unforgiving.#hasWarnedAboutStyleDeprecation = true;
+ const error = new Error('Support for the "style" tag is deprecated and will be removed in future versions.');
+ console.warn(error); // eslint-disable-line no-console
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Public parsing interface //////////////////////////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////
+
+ static html = 'http://www.w3.org/1999/xhtml';
+ static svg = 'http://www.w3.org/2000/svg';
+ static math = 'http://www.w3.org/1998/Math/MathML';
+
+ static parse(strings, onBoolean, onDefined, onAttribute, onProperty, onContent, onText, namespace) {
+ const fragment = Unforgiving.#fragment.cloneNode(false);
+ fragment[Unforgiving.#namespace] = namespace ??= Unforgiving.html;
+
+ const path = [];
+ const childNodesIndex = { value: -1 }; // Wrapper to allow better factoring.
+ const element = { value: fragment }; // Wrapper to allow better factoring.
+
+ const stringsLength = strings.length;
+ let stringsIndex = 0;
+ let string = null;
+ let stringLength = null;
+ let stringIndex = null;
+ let nextStringIndex = null;
+ let value = Unforgiving.#initial;
+
+ while (stringsIndex < stringsLength) {
+ string = strings[stringsIndex];
+
+ Unforgiving.#validateRawString(strings.raw[stringsIndex]);
+ if (stringsIndex > 0) {
+ switch (value) {
+ case Unforgiving.#initial:
+ case Unforgiving.#boundContent:
+ case Unforgiving.#unboundContent:
+ case Unforgiving.#openTagEnd:
+ case Unforgiving.#closeTag:
+ if (element.value[Unforgiving.#localName] === 'textarea') {
+ // The textarea tag only accepts text, we restrict interpolation
+ // there. See note on “replaceable character data” in the
+ // following reference document:
+ // https://w3c.github.io/html-reference/syntax.html#text-syntax
+ const sloppyStartInterpolation = value !== Unforgiving.#openTagEnd;
+ Unforgiving.#addBoundText(onText, string, path, element, sloppyStartInterpolation);
+ } else {
+ Unforgiving.#addBoundContent(onContent, path, element, childNodesIndex);
+ }
+ value = Unforgiving.#boundContent;
+ nextStringIndex = value.lastIndex;
+ break;
+ }
+ }
+
+ stringLength = string.length;
+ stringIndex = 0;
+ while (stringIndex < stringLength) {
+ // The string will be empty if we have a template like this `${…}${…}`.
+ // See related logic at the end of the inner loop;
+ if (string.length > 0) {
+ const nextValue = Unforgiving.#validTransition(string, stringIndex, value);
+ if (!nextValue) {
+ Unforgiving.#throwTransitionError(strings, stringsIndex, string, stringIndex, value);
+ }
+ value = nextValue;
+ nextStringIndex = value.lastIndex;
+ }
+
+ // When we transition into certain values, we need to take action.
+ switch (value) {
+ case Unforgiving.#unboundContent:
+ Unforgiving.#addUnboundContent(string, stringIndex, element, childNodesIndex, nextStringIndex);
+ break;
+ case Unforgiving.#unboundComment:
+ Unforgiving.#addUnboundComment(string, stringIndex, element, childNodesIndex, nextStringIndex);
+ break;
+ case Unforgiving.#openTagStart:
+ Unforgiving.#addElement(string, stringIndex, path, element, childNodesIndex, nextStringIndex);
+ break;
+ case Unforgiving.#unboundBoolean:
+ Unforgiving.#addUnboundBoolean(string, stringIndex, element, nextStringIndex);
+ break;
+ case Unforgiving.#unboundAttribute:
+ Unforgiving.#addUnboundAttribute(string, stringIndex, element, nextStringIndex);
+ break;
+ case Unforgiving.#boundBoolean:
+ Unforgiving.#addBoundBoolean(onBoolean, string, stringIndex, path, nextStringIndex);
+ break;
+ case Unforgiving.#boundDefined:
+ Unforgiving.#addBoundDefined(onDefined, string, stringIndex, path, nextStringIndex);
+ break;
+ case Unforgiving.#boundAttribute:
+ Unforgiving.#addBoundAttribute(onAttribute, string, stringIndex, path, nextStringIndex);
+ break;
+ case Unforgiving.#boundProperty:
+ Unforgiving.#addBoundProperty(onProperty, string, stringIndex, path, nextStringIndex);
+ break;
+ case Unforgiving.#openTagEnd:
+ if (element.value[Unforgiving.#namespace] === Unforgiving.html) {
+ const tagName = element.value[Unforgiving.#localName];
+ if (Unforgiving.#voidHtmlElements.has(tagName)) {
+ value = Unforgiving.#finalizeVoidElement(path, element, childNodesIndex, nextStringIndex);
+ nextStringIndex = value.lastIndex;
+ } else if (tagName === 'style') {
+ Unforgiving.#styleDeprecationWarning();
+ value = Unforgiving.#finalizeStyle(string, path, element, childNodesIndex, nextStringIndex);
+ nextStringIndex = value.lastIndex;
+ } else if (
+ tagName === 'textarea' &&
+ Unforgiving.#openTagEnd.lastIndex !== string.length
+ ) {
+ value = Unforgiving.#finalizeTextarea(string, path, element, childNodesIndex, nextStringIndex);
+ nextStringIndex = value.lastIndex;
+ } else if (
+ tagName === 'template' &&
+ // @ts-ignore — TypeScript doesn’t get that this is a “template”.
+ element.value.hasAttribute('shadowrootmode')
+ ) {
+ const errorMessagesKey = Unforgiving.#namedErrorsToErrorMessagesKey.get('declarative-shadow-root');
+ const errorMessage = Unforgiving.#errorMessages.get(errorMessagesKey);
+ throw new Error(`[${errorMessagesKey}] ${errorMessage}`);
+ } else {
+ // Assume we’re traversing into the new element and reset index.
+ childNodesIndex.value = -1;
+ }
+ } else {
+ // Assume we’re traversing into the new element and reset index.
+ childNodesIndex.value = -1;
+ }
+ break;
+ case Unforgiving.#closeTag:
+ Unforgiving.#finalizeElement(strings, stringsIndex, string, stringIndex, path, element, childNodesIndex, nextStringIndex);
+ break;
+ }
+ stringIndex = nextStringIndex; // Update out pointer from our pattern match.
+ }
+ stringsIndex++;
+ }
+ Unforgiving.#validateExit(fragment, element);
+ return fragment;
+ }
+}
/** Internal implementation details for template engine. */
class TemplateEngine {
@@ -796,6 +1812,77 @@ class TemplateEngine {
}
}
+ // TODO: Future state here — we’ll eventually just guard against value changes
+ // at a higher level and will remove all updater logic.
+ // static #commitAttribute(node, name, value) {
+ // node.setAttribute(name, value);
+ // }
+ // static #commitBoolean(node, name, value) {
+ // value ? node.setAttribute(name, '') : node.removeAttribute(name);
+ // }
+ // static #commitDefined(node, name, value) {
+ // value === undefined || value === null
+ // ? node.removeAttribute(name)
+ // : node.setAttribute(name, value);
+ // }
+ // static #commitProperty(node, name, value) {
+ // node[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);
@@ -882,6 +1969,23 @@ class TemplateEngine {
}
}
+ // 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
// updating the DOM if things have changed.
static #commit(preparedResult) {
@@ -935,7 +2039,7 @@ class TemplateEngine {
// Inject a given result into a node for the first time.
static #inject(rawResult, node, before) {
- // Get fragment created from a tagged template function’s “strings”.
+ // Create and prepare a document fragment to be injected.
const { [TemplateEngine.#ANALYSIS]: analysis } = rawResult;
const fragment = analysis.fragment.cloneNode(true);
const targets = TemplateEngine.#findTargets(fragment, analysis.lookups);
@@ -970,8 +2074,8 @@ class TemplateEngine {
const onProperty = TemplateEngine.#storeKeyLookup.bind(null, lookups, TemplateEngine.#PROPERTY);
const onContent = TemplateEngine.#storeContentLookup.bind(null, lookups);
const onText = TemplateEngine.#storeTextLookup.bind(null, lookups);
- const forgivingLanguage = language === TemplateEngine.#SVG ? Forgiving.svg : Forgiving.html;
- const fragment = Forgiving.parse(strings, onBoolean, onDefined, onAttribute, onProperty, onContent, onText, forgivingLanguage);
+ const namespace = language === TemplateEngine.#SVG ? Unforgiving.svg : Unforgiving.html;
+ const fragment = Unforgiving.parse(strings, onBoolean, onDefined, onAttribute, onProperty, onContent, onText, namespace);
analysis.fragment = fragment;
analysis.lookups = lookups;
analysis.done = true;
@@ -1059,6 +2163,16 @@ class TemplateEngine {
}
}
+ // TODO: Future state — we may choose to iterate differently as an
+ // optimization in later versions.
+ // static #removeWithin(node) {
+ // let childNode = node.lastChild;
+ // while (childNode) {
+ // const nextChildNode = childNode.previousSibling;
+ // node.removeChild(childNode);
+ // childNode = nextChildNode;
+ // }
+ // }
static #removeWithin(node) {
// Iterate backwards over the live node collection since we’re mutating it.
const childNodes = node.childNodes;
@@ -1067,12 +2181,31 @@ class TemplateEngine {
}
}
+ // TODO: Future state — we may choose to iterate differently as an
+ // optimization in later versions.
+ // static #removeBetween(startNode, node, parentNode) {
+ // parentNode ??= node.parentNode;
+ // let childNode = node.previousSibling;
+ // while(childNode !== startNode) {
+ // const nextChildNode = childNode.previousSibling;
+ // parentNode.removeChild(childNode);
+ // childNode = nextChildNode;
+ // }
+ // }
static #removeBetween(startNode, node) {
while(node.previousSibling !== startNode) {
node.previousSibling.remove();
}
}
+ // TODO: Future state — we may choose to iterate differently as an
+ // optimization in later versions.
+ // static #removeThrough(startNode, node, parentNode) {
+ // parentNode ??= node.parentNode;
+ // TemplateEngine.#removeBetween(startNode, node, parentNode);
+ // parentNode.removeChild(startNode);
+ // parentNode.removeChild(node);
+ // }
static #removeThrough(startNode, node) {
TemplateEngine.#removeBetween(startNode, node);
startNode.remove();