@netflix/x-element
Advanced tools
Comparing version 1.0.0-rc.55 to 1.0.0-rc.56
@@ -35,64 +35,89 @@ import XElement from '../../x-element.js'; | ||
}; | ||
const tick = async () => { | ||
await new Promise(resolve => requestAnimationFrame(resolve)); | ||
}; | ||
const run = async (output, constructor) => { | ||
const run = async (output, constructor, ...tests) => { | ||
output.textContent = ''; | ||
const { render, html } = constructor.templateEngine; | ||
const injectCount = 100000; | ||
const initialCount = 1000000; | ||
const updateCount = 1000000; | ||
const slop = 1000; | ||
const injectCount = 10000; | ||
const initialCount = 100000; | ||
const updateCount = 100000; | ||
// Test inject performance. | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
let injectSum = 0; | ||
const injectProperties = { attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' }; | ||
const injectContainer = document.createElement('div'); | ||
for (let iii = 0; iii < injectCount; iii++) { | ||
const strings = getStrings(); | ||
const values = getValues(injectProperties); | ||
const t0 = performance.now(); | ||
render(injectContainer, html(strings, ...values)); | ||
const t1 = performance.now(); | ||
injectSum += t1 - t0; | ||
if (tests.includes('inject')) { | ||
// Test inject performance. | ||
await tick(); | ||
let injectSum = 0; | ||
const injectProperties = { attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' }; | ||
const injectContainer = document.createElement('div'); | ||
for (let iii = 0; iii < injectCount + slop; iii++) { | ||
const strings = getStrings(); | ||
const values = getValues(injectProperties); | ||
const t0 = performance.now(); | ||
render(injectContainer, html(strings, ...values)); | ||
const t1 = performance.now(); | ||
if (iii >= slop) { | ||
injectSum += t1 - t0; | ||
} | ||
if (iii % 100 === 0) { | ||
await tick(); | ||
} | ||
} | ||
const injectAverage = `${(injectSum / injectCount * 1000).toFixed(1).padStart(6)} µs`; | ||
output.textContent += `${output.textContent ? '\n' : ''}inject: ${injectAverage} (tested ${injectCount.toLocaleString()} times)`; | ||
} | ||
const injectAverage = `${(injectSum / injectCount * 1000).toFixed(1).padStart(6)} µs`; | ||
output.textContent += `${output.textContent ? '\n' : ''}inject: ${injectAverage} (tested ${injectCount.toLocaleString()} times)`; | ||
// Test initial performance. | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
let initialSum = 0; | ||
const initialStrings = getStrings(); | ||
const initialProperties = { attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' }; | ||
const initialValues = getValues(initialProperties); | ||
const initialContainer = document.createElement('div'); | ||
for (let iii = 0; iii < initialCount; iii++) { | ||
const t0 = performance.now(); | ||
render(initialContainer, html(initialStrings, ...initialValues)); | ||
const t1 = performance.now(); | ||
initialSum += t1 - t0; | ||
if (tests.includes('initial')) { | ||
// Test initial performance. | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
let initialSum = 0; | ||
const initialStrings = getStrings(); | ||
const initialProperties = { attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' }; | ||
const initialValues = getValues(initialProperties); | ||
const initialContainer = document.createElement('div'); | ||
for (let iii = 0; iii < initialCount + slop; iii++) { | ||
const t0 = performance.now(); | ||
render(initialContainer, html(initialStrings, ...initialValues)); | ||
const t1 = performance.now(); | ||
if (iii >= slop) { | ||
initialSum += t1 - t0; | ||
} | ||
if (iii % 1000 === 0) { | ||
await tick(); | ||
} | ||
} | ||
const initialAverage = `${(initialSum / initialCount * 1000).toFixed(1).padStart(4)} µs`; | ||
output.textContent += `${output.textContent ? '\n' : ''}initial: ${initialAverage} (tested ${initialCount.toLocaleString()} times)`; | ||
} | ||
const initialAverage = `${(initialSum / initialCount * 1000).toFixed(1).padStart(4)} µs`; | ||
output.textContent += `${output.textContent ? '\n' : ''}initial: ${initialAverage} (tested ${initialCount.toLocaleString()} times)`; | ||
// Test update performance. | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
let updateSum = 0; | ||
const updateStrings = getStrings(); | ||
const updateProperties = [ | ||
{ attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' }, | ||
{ attr: '456', id: 'bar', hidden: false, title: 'test', content1: 'ZZZ', content2: 'BBB' }, | ||
]; | ||
const updateContainer = document.createElement('div'); | ||
for (let iii = 0; iii < updateCount; iii++) { | ||
const values = getValues(updateProperties[iii % 2]); | ||
const t0 = performance.now(); | ||
render(updateContainer, html(updateStrings, ...values)); | ||
const t1 = performance.now(); | ||
updateSum += t1 - t0; | ||
if (tests.includes('update')) { | ||
// Test update performance. | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
let updateSum = 0; | ||
const updateStrings = getStrings(); | ||
const updateProperties = [ | ||
{ attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' }, | ||
{ attr: '456', id: 'bar', hidden: false, title: 'test', content1: 'ZZZ', content2: 'BBB' }, | ||
]; | ||
const updateContainer = document.createElement('div'); | ||
for (let iii = 0; iii < updateCount + slop; iii++) { | ||
const values = getValues(updateProperties[iii % 2]); | ||
const t0 = performance.now(); | ||
render(updateContainer, html(updateStrings, ...values)); | ||
const t1 = performance.now(); | ||
if (iii >= slop) { | ||
updateSum += t1 - t0; | ||
} | ||
if (iii % 1000 === 0) { | ||
await tick(); | ||
} | ||
} | ||
const updateAverage = `${(updateSum / updateCount * 1000).toFixed(1).padStart(5)} µs`; | ||
output.textContent += `${output.textContent ? '\n' : ''}update: ${updateAverage} (tested ${updateCount.toLocaleString()} times)`; | ||
} | ||
const updateAverage = `${(updateSum / updateCount * 1000).toFixed(1).padStart(5)} µs`; | ||
output.textContent += `${output.textContent ? '\n' : ''}update: ${updateAverage} (tested ${updateCount.toLocaleString()} times)`; | ||
}; | ||
await run(document.getElementById('default'), XElement); | ||
await run(document.getElementById('lit-html'), LitHtmlElement); | ||
await run(document.getElementById('uhtml'), UhtmlElement); | ||
await run(document.getElementById('default'), XElement, 'inject', 'initial', 'update'); | ||
await run(document.getElementById('lit-html'), LitHtmlElement, 'inject', 'initial', 'update'); | ||
await run(document.getElementById('uhtml'), UhtmlElement, 'inject', 'initial', 'update'); |
@@ -5,3 +5,3 @@ { | ||
"description": "Custom Element base class.", | ||
"version": "1.0.0-rc.55", | ||
"version": "1.0.0-rc.56", | ||
"license": "SEE LICENSE IN LICENSE", | ||
@@ -8,0 +8,0 @@ "repository": "https://github.com/Netflix/x-element", |
@@ -216,2 +216,15 @@ import XElement from '../x-element.js'; | ||
it('resets compute validity on initialization to catch upgrade edge cases with internal, computed properties', () => { | ||
const el = document.createElement('test-element'); | ||
el.setAttribute('a', '1'); | ||
el.setAttribute('b', '2'); | ||
assert(el.a === undefined); | ||
assert(el.b === undefined); | ||
assert(Number.isNaN(el.internal.c)); | ||
document.body.append(el); | ||
assert(el.a === 1); | ||
assert(el.b === 2); | ||
assert(el.internal.c === 3); | ||
}); | ||
it('cannot be written to from host', () => { | ||
@@ -218,0 +231,0 @@ const el = document.createElement('test-element'); |
@@ -22,36 +22,2 @@ import XElement from '../x-element.js'; | ||
it('refuses to interpolate within a style tag', () => { | ||
const getTemplate = ({ color }) => { | ||
return html` | ||
<style id="target"> | ||
#target { | ||
background-color: ${color}; | ||
} | ||
</style> | ||
`; | ||
}; | ||
const container = document.createElement('div'); | ||
document.body.append(container); | ||
render(container, getTemplate({ color: 'url(evil-url)' })); | ||
const textContent = container.querySelector('#target').textContent; | ||
assert(textContent === '/* x-element: Interpolation is not allowed here. */', textContent); | ||
container.remove(); | ||
}); | ||
it('refuses to interpolate within a script tag', () => { | ||
const getTemplate = ({ message }) => { | ||
return html` | ||
<script id="target"> | ||
console.log('${message}'); | ||
</script> | ||
`; | ||
}; | ||
const container = document.createElement('div'); | ||
document.body.append(container); | ||
render(container, getTemplate({ message: '\' + prompt(\'evil\') + \'' })); | ||
const textContent = container.querySelector('#target').textContent; | ||
assert(textContent === '/* x-element: Interpolation is not allowed here. */', textContent); | ||
container.remove(); | ||
}); | ||
it('renders interpolated content without parsing', () => { | ||
@@ -728,5 +694,43 @@ const userContent = '<a href="https://evil.com">Click Me!</a>'; | ||
describe('templating', () => { | ||
it.skip('throws when attempting to interpolate within a style tag', () => {}); | ||
it('throws when attempting to interpolate within a style tag', () => { | ||
const getTemplate = ({ color }) => { | ||
return html` | ||
<style id="target"> | ||
#target { | ||
background-color: ${color}; | ||
} | ||
</style> | ||
`; | ||
}; | ||
const container = document.createElement('div'); | ||
document.body.append(container); | ||
let error; | ||
try { | ||
render(container, getTemplate({ color: 'url(evil-url)' })); | ||
} catch (e) { | ||
error = e; | ||
} | ||
assert(error?.message === `Interpolation of "style" tags is not allowed.`, error.message); | ||
container.remove(); | ||
}); | ||
it.skip('throws when attempting to interpolate within a script tag', () => {}); | ||
it('throws when attempting to interpolate within a script tag', () => { | ||
const getTemplate = ({ message }) => { | ||
return html` | ||
<script id="target"> | ||
console.log('${message}'); | ||
</script> | ||
`; | ||
}; | ||
const container = document.createElement('div'); | ||
document.body.append(container); | ||
let error; | ||
try { | ||
render(container, getTemplate({ message: '\' + prompt(\'evil\') + \'' })); | ||
} catch (e) { | ||
error = e; | ||
} | ||
assert(error?.message === `Interpolation of "script" tags is not allowed.`, error.message); | ||
container.remove(); | ||
}); | ||
@@ -743,3 +747,3 @@ it('throws for unquoted attributes', () => { | ||
} | ||
assert(error?.message === `Found invalid template near " not-ok=".`, error.message); | ||
assert(error?.message === `Found invalid template string "<div id="target" not-ok=" at " not-ok=".`, error.message); | ||
container.remove(); | ||
@@ -758,3 +762,3 @@ }); | ||
} | ||
assert(error?.message === `Found invalid template near " not-ok='".`, error.message); | ||
assert(error?.message === `Found invalid template string "<div id="target" not-ok='" at " not-ok='".`, error.message); | ||
container.remove(); | ||
@@ -773,3 +777,3 @@ }); | ||
} | ||
assert(error?.message === `Found invalid template near " .notOk=".`, error.message); | ||
assert(error?.message === `Found invalid template string "<div id="target" .notOk=" at " .notOk=".`, error.message); | ||
container.remove(); | ||
@@ -788,3 +792,3 @@ }); | ||
} | ||
assert(error?.message === `Found invalid template near " .notOk='".`, error.message); | ||
assert(error?.message === `Found invalid template string "<div id="target" .notOk='" at " .notOk='".`, error.message); | ||
container.remove(); | ||
@@ -813,4 +817,4 @@ }); | ||
describe('ifDefined', () => { | ||
it('throws if used on a "boolean-attribute"', () => { | ||
const expected = 'The ifDefined update must be used on an attribute, not on a boolean-attribute.'; | ||
it('throws if used on a "boolean"', () => { | ||
const expected = 'The ifDefined update must be used on an attribute, not on a boolean attribute.'; | ||
const getTemplate = ({ maybe }) => { | ||
@@ -868,4 +872,4 @@ return html`<div id="target" ?maybe="${ifDefined(maybe)}"></div>`; | ||
it('throws if used with "text-content"', () => { | ||
const expected = 'The ifDefined update must be used on an attribute, not on text-content.'; | ||
it('throws if used with "text"', () => { | ||
const expected = 'The ifDefined update must be used on an attribute, not on text content.'; | ||
const getTemplate = ({ maybe }) => { | ||
@@ -889,4 +893,4 @@ return html`<textarea id="target">${ifDefined(maybe)}</textarea>`; | ||
describe('nullish', () => { | ||
it('throws if used on a "boolean-attribute"', () => { | ||
const expected = 'The nullish update must be used on an attribute, not on a boolean-attribute.'; | ||
it('throws if used on a "boolean"', () => { | ||
const expected = 'The nullish update must be used on an attribute, not on a boolean attribute.'; | ||
const getTemplate = ({ maybe }) => { | ||
@@ -944,4 +948,4 @@ return html`<div id="target" ?maybe="${nullish(maybe)}"></div>`; | ||
it('throws if used with "text-content"', () => { | ||
const expected = 'The nullish update must be used on an attribute, not on text-content.'; | ||
it('throws if used with "text"', () => { | ||
const expected = 'The nullish update must be used on an attribute, not on text content.'; | ||
const getTemplate = ({ maybe }) => { | ||
@@ -983,4 +987,4 @@ return html`<textarea id="target">${nullish(maybe)}</textarea>`; | ||
it('throws if used on a "boolean-attribute"', () => { | ||
const expected = 'The live update must be used on a property, not on a boolean-attribute.'; | ||
it('throws if used on a "boolean"', () => { | ||
const expected = 'The live update must be used on a property, not on a boolean attribute.'; | ||
const getTemplate = ({ maybe }) => { | ||
@@ -1020,4 +1024,4 @@ return html`<div id="target" ?maybe="${live(maybe)}"></div>`; | ||
it('throws if used with "text-content"', () => { | ||
const expected = 'The live update must be used on a property, not on text-content.'; | ||
it('throws if used with "text"', () => { | ||
const expected = 'The live update must be used on a property, not on text content.'; | ||
const getTemplate = ({ maybe }) => { | ||
@@ -1059,4 +1063,4 @@ return html`<textarea id="target">${live(maybe)}</textarea>`; | ||
it('throws if used on a "boolean-attribute"', () => { | ||
const expected = 'The unsafeHTML update must be used on content, not on a boolean-attribute.'; | ||
it('throws if used on a "boolean"', () => { | ||
const expected = 'The unsafeHTML update must be used on content, not on a boolean attribute.'; | ||
const getTemplate = ({ maybe }) => { | ||
@@ -1096,4 +1100,4 @@ return html`<div id="target" ?maybe="${unsafeHTML(maybe)}"></div>`; | ||
it('throws if used with "text-content"', () => { | ||
const expected = 'The unsafeHTML update must be used on content, not on text-content.'; | ||
it('throws if used with "text"', () => { | ||
const expected = 'The unsafeHTML update must be used on content, not on text content.'; | ||
const getTemplate = ({ maybe }) => { | ||
@@ -1155,4 +1159,4 @@ return html`<textarea id="target">${unsafeHTML(maybe)}</textarea>`; | ||
it('throws if used on a "boolean-attribute"', () => { | ||
const expected = 'The unsafeSVG update must be used on content, not on a boolean-attribute.'; | ||
it('throws if used on a "boolean"', () => { | ||
const expected = 'The unsafeSVG update must be used on content, not on a boolean attribute.'; | ||
const getTemplate = ({ maybe }) => { | ||
@@ -1192,4 +1196,4 @@ return html`<div id="target" ?maybe="${unsafeSVG(maybe)}"></div>`; | ||
it('throws if used with "text-content"', () => { | ||
const expected = 'The unsafeSVG update must be used on content, not on text-content.'; | ||
it('throws if used with "text"', () => { | ||
const expected = 'The unsafeSVG update must be used on content, not on text content.'; | ||
const getTemplate = ({ maybe }) => { | ||
@@ -1287,4 +1291,4 @@ return html`<textarea id="target">${unsafeSVG(maybe)}</textarea>`; | ||
it('throws if used on a "boolean-attribute"', () => { | ||
const expected = 'The map update must be used on content, not on a boolean-attribute.'; | ||
it('throws if used on a "boolean"', () => { | ||
const expected = 'The map update must be used on content, not on a boolean attribute.'; | ||
const getTemplate = ({ maybe }) => { | ||
@@ -1324,4 +1328,4 @@ return html`<div id="target" ?maybe="${map(maybe, () => {}, () => {})}"></div>`; | ||
it('throws if used with "text-content"', () => { | ||
const expected = 'The map update must be used on content, not on text-content.'; | ||
it('throws if used with "text"', () => { | ||
const expected = 'The map update must be used on content, not on text content.'; | ||
const getTemplate = ({ maybe }) => { | ||
@@ -1460,4 +1464,4 @@ return html`<textarea id="target">${map(maybe, () => {}, () => {})}</textarea>`; | ||
it('throws if used on a "boolean-attribute"', () => { | ||
const expected = 'The repeat update must be used on content, not on a boolean-attribute.'; | ||
it('throws if used on a "boolean"', () => { | ||
const expected = 'The repeat update must be used on content, not on a boolean attribute.'; | ||
const getTemplate = ({ maybe }) => { | ||
@@ -1497,4 +1501,4 @@ return html`<div id="target" ?maybe="${repeat(maybe, () => {})}"></div>`; | ||
it('throws if used with "text-content"', () => { | ||
const expected = 'The repeat update must be used on content, not on text-content.'; | ||
it('throws if used with "text"', () => { | ||
const expected = 'The repeat update must be used on content, not on text content.'; | ||
const getTemplate = ({ maybe }) => { | ||
@@ -1501,0 +1505,0 @@ return html`<textarea id="target">${repeat(maybe, () => {})}</textarea>`; |
117
x-element.js
@@ -649,3 +649,3 @@ /** Base element class for creating custom elements. */ | ||
const hostInfo = XElement.#hosts.get(host); | ||
const { initialized, invalidProperties } = hostInfo; | ||
const { computeMap, initialized, invalidProperties } = hostInfo; | ||
if (initialized === false) { | ||
@@ -665,2 +665,5 @@ XElement.#upgradeOwnProperties(host); | ||
invalidProperties.add(property); | ||
if (property.compute) { | ||
computeMap.get(property).valid = false; | ||
} | ||
} | ||
@@ -1067,7 +1070,14 @@ hostInfo.initialized = true; | ||
static #updaters = new WeakMap(); | ||
static #ATTRIBUTE_PREFIXES = { | ||
attribute: 'x-element-attribute-', | ||
boolean: 'x-element-boolean-', | ||
property: 'x-element-property-', | ||
}; | ||
static #CONTENT_PREFIX = 'x-element-content-'; | ||
static #CONTENT_REGEX = new RegExp(`${Template.#CONTENT_PREFIX}(\\d+)`); | ||
static #OPEN = /<[a-z][a-z0-9-]*(?=\s)/g; | ||
static #STEP = /\s+[a-z][a-z0-9-]*(?=[\s>])|\s+[a-z][a-zA-Z0-9-]*="[^"]*"/y; | ||
static #CLOSE = />/g; | ||
static #ATTRIBUTE = /\s+(\??([a-z][a-zA-Z0-9-]*))="$/y; | ||
static #PROPERTY = /\s+\.([a-z][a-zA-Z0-9_]*)="$/y; | ||
static #CLOSE = />/g; | ||
@@ -1085,7 +1095,6 @@ #type = null; | ||
if (!this.#analysis) { | ||
let html = ''; | ||
const htmlStrings = []; | ||
const state = { inside: false, index: 0 }; | ||
for (let iii = 0; iii < this.#strings.length; iii++) { | ||
const string = this.#strings[iii]; | ||
html += string; | ||
let string = this.#strings[iii]; | ||
Template.#exhaustString(string, state); | ||
@@ -1099,6 +1108,6 @@ if (state.inside) { | ||
// We found a match like this: html`<div ?hidden="${!!value}"></div>`. | ||
html = html.slice(0, -3 - name.length) + `x-element-boolean-attribute-$${iii}="${name}`; | ||
string = string.slice(0, -3 - name.length) + `${Template.#ATTRIBUTE_PREFIXES.boolean}${iii}="${name}`; | ||
} else { | ||
// We found a match like this: html`<div title="${value}"></div>`. | ||
html = html.slice(0, -2 - name.length) + `x-element-attribute-$${iii}="${name}`; | ||
string = string.slice(0, -2 - name.length) + `${Template.#ATTRIBUTE_PREFIXES.attribute}${iii}="${name}`; | ||
} | ||
@@ -1112,6 +1121,6 @@ state.index = 1; // Accounts for an expected quote character next. | ||
const name = propertyMatch[1]; | ||
html = html.slice(0, -3 - name.length) + `x-element-property-$${iii}="${name}`; | ||
string = string.slice(0, -3 - name.length) + `${Template.#ATTRIBUTE_PREFIXES.property}${iii}="${name}`; | ||
state.index = 1; // Accounts for an expected quote character next. | ||
} else { | ||
throw new Error(`Found invalid template near "${string.slice(state.index)}".`); | ||
throw new Error(`Found invalid template string "${string}" at "${string.slice(state.index)}".`); | ||
} | ||
@@ -1121,9 +1130,10 @@ } | ||
// Assume it's a match like this: html`<div>${value}</div>`. | ||
html += `<!--x-element-content-$${iii}-->`; | ||
string += `<!--${Template.#CONTENT_PREFIX}${iii}-->`; | ||
state.index = 0; // No characters to account for. Reset to zero. | ||
} | ||
htmlStrings[iii] = string; | ||
} | ||
if (this.#type === 'svg') { | ||
html = `<svg xmlns="http://www.w3.org/2000/svg">${html}</svg>`; | ||
} | ||
const html = this.#type === 'svg' | ||
? `<svg xmlns="http://www.w3.org/2000/svg">${htmlStrings.join('')}</svg>` | ||
: htmlStrings.join(''); | ||
const element = document.createElement('template'); | ||
@@ -1265,31 +1275,35 @@ element.innerHTML = html; | ||
if (node.nodeType === Node.ELEMENT_NODE) { | ||
for (const attribute of [...node.attributes]) { | ||
const attributeMatch = attribute.name.match(/^x-element-attribute-\$(\d+)$/); | ||
const booleanAttributeMatch = !attributeMatch ? attribute.name.match(/^x-element-boolean-attribute-\$(\d+)$/) : null; | ||
const propertyMatch = !attributeMatch && !booleanAttributeMatch ? attribute.name.match(/^x-element-property-\$(\d+)$/) : null; | ||
if (attributeMatch) { | ||
node.removeAttribute(attributeMatch[0]); | ||
items.push({ path, key: attributeMatch[1], type: 'attribute', name: attribute.value }); | ||
} else if (booleanAttributeMatch) { | ||
node.removeAttribute(booleanAttributeMatch[0]); | ||
items.push({ path, key: booleanAttributeMatch[1], type: 'boolean-attribute', name: attribute.value }); | ||
} else if (propertyMatch) { | ||
node.removeAttribute(propertyMatch[0]); | ||
items.push({ path, key: propertyMatch[1], type: 'property', name: attribute.value }); | ||
const attributesToRemove = new Set(); | ||
for (const attribute of node.attributes) { | ||
const name = attribute.name; | ||
const type = name.startsWith(Template.#ATTRIBUTE_PREFIXES.attribute) | ||
? 'attribute' | ||
: name.startsWith(Template.#ATTRIBUTE_PREFIXES.boolean) | ||
? 'boolean' | ||
: name.startsWith(Template.#ATTRIBUTE_PREFIXES.property) | ||
? 'property' | ||
: null; | ||
if (type) { | ||
const prefix = Template.#ATTRIBUTE_PREFIXES[type]; | ||
const key = name.slice(prefix.length); | ||
const value = attribute.value; | ||
items.push({ path, key, type, name: value }); | ||
attributesToRemove.add(name); | ||
} | ||
} | ||
for (const attribute of attributesToRemove) { | ||
node.removeAttribute(attribute); | ||
} | ||
// Special case to handle elements which only allow text content (no comments). | ||
if (node.localName.match(/^plaintext|script|style|textarea|title$/)) { | ||
const contentMatch = node.textContent.match(/x-element-content-\$(\d+)/); | ||
const localName = node.localName; | ||
if ((localName === 'style' || localName === 'script') && Template.#CONTENT_REGEX.exec(node.textContent)) { | ||
throw new Error(`Interpolation of "${localName}" tags is not allowed.`); | ||
} else if (localName === 'plaintext' || localName === 'textarea' || localName === 'title') { | ||
const contentMatch = Template.#CONTENT_REGEX.exec(node.textContent); | ||
if (contentMatch) { | ||
if (node.localName.match(/^plaintext|textarea|title$/)) { | ||
node.textContent = ''; | ||
items.push({ path, key: contentMatch[1], type: 'text-content' }); | ||
} else { | ||
node.textContent = '/* x-element: Interpolation is not allowed here. */'; | ||
} | ||
items.push({ path, key: contentMatch[1], type: 'text' }); | ||
} | ||
} | ||
} else if (node.nodeType === Node.COMMENT_NODE) { | ||
const contentMatch = node.textContent.match(/x-element-content-\$(\d+)/); | ||
const contentMatch = Template.#CONTENT_REGEX.exec(node.textContent); | ||
if (contentMatch) { | ||
@@ -1324,3 +1338,3 @@ node.textContent = ''; | ||
case 'attribute': | ||
case 'boolean-attribute': | ||
case 'boolean': | ||
case 'property': { | ||
@@ -1336,3 +1350,3 @@ nextItems.push({ key: item.key, type: item.type, name: item.name, node }); | ||
} | ||
case 'text-content': { | ||
case 'text': { | ||
const nextItem = { key: item.key, type: item.type, node }; | ||
@@ -1358,6 +1372,6 @@ nextItems.push(nextItem); | ||
break; | ||
case 'boolean-attribute': | ||
case 'boolean': | ||
updater | ||
? updater(type, lastValue, { node, name }) | ||
: Template.#booleanAttribute(type, values[key], lastValue, { node, name }); | ||
: Template.#boolean(type, values[key], lastValue, { node, name }); | ||
break; | ||
@@ -1374,6 +1388,6 @@ case 'property': | ||
break; | ||
case 'text-content': | ||
case 'text': | ||
updater | ||
? updater(type, lastValue, { node }) | ||
: Template.#textContent(type, values[key], lastValue, { node }); | ||
: Template.#text(type, values[key], lastValue, { node }); | ||
break; | ||
@@ -1390,3 +1404,3 @@ } | ||
static #booleanAttribute(type, value, lastValue, { node, name }) { | ||
static #boolean(type, value, lastValue, { node, name }) { | ||
if (value !== lastValue) { | ||
@@ -1405,3 +1419,3 @@ value | ||
static #textContent(type, value, lastValue, { node }) { | ||
static #text(type, value, lastValue, { node }) { | ||
if (value !== lastValue) { | ||
@@ -1646,7 +1660,14 @@ node.textContent = value; | ||
static #getTypeText(type) { | ||
return type === 'attribute' | ||
? `an ${type}` | ||
: type === 'boolean-attribute' || type === 'property' | ||
? `a ${type}` | ||
: `${type}`; | ||
switch (type) { | ||
case 'attribute': | ||
return 'an attribute'; | ||
case 'boolean': | ||
return 'a boolean attribute'; | ||
case 'property': | ||
return 'a property'; | ||
case 'content': | ||
return 'content'; | ||
case 'text': | ||
return 'text content'; | ||
} | ||
} | ||
@@ -1653,0 +1674,0 @@ } |
Sorry, the diff of this file is not supported yet
269143
6933