lit-html
Version:
HTML template literals in JavaScript
206 lines • 10.6 kB
JavaScript
/**
* @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
/**
* An expression marker with embedded unique key to avoid collision with
* possible text in templates.
*/
export const marker = `{{lit-${String(Math.random()).slice(2)}}}`;
/**
* An expression marker used text-positions, not attribute positions,
* in template.
*/
export const nodeMarker = `<!--${marker}-->`;
export const markerRegex = new RegExp(`${marker}|${nodeMarker}`);
export const rewritesStyleAttribute = (() => {
const el = document.createElement('div');
el.setAttribute('style', '{{bad value}}');
return el.getAttribute('style') !== '{{bad value}}';
})();
/**
* An updateable Template that tracks the location of dynamic parts.
*/
export class Template {
constructor(result, element) {
this.parts = [];
this.element = element;
let index = -1;
let partIndex = 0;
const nodesToRemove = [];
const _prepareTemplate = (template) => {
const content = template.content;
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be
// null
const walker = document.createTreeWalker(content, 133 /* NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT |
NodeFilter.SHOW_TEXT */, null, false);
// The actual previous node, accounting for removals: if a node is removed
// it will never be the previousNode.
let previousNode;
// Used to set previousNode at the top of the loop.
let currentNode;
while (walker.nextNode()) {
index++;
previousNode = currentNode;
const node = currentNode = walker.currentNode;
if (node.nodeType === 1 /* Node.ELEMENT_NODE */) {
if (node.hasAttributes()) {
const attributes = node.attributes;
// Per
// https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap,
// attributes are not guaranteed to be returned in document order.
// In particular, Edge/IE can return them out of order, so we cannot
// assume a correspondance between part index and attribute index.
let count = 0;
for (let i = 0; i < attributes.length; i++) {
if (attributes[i].value.indexOf(marker) >= 0) {
count++;
}
}
while (count-- > 0) {
// Get the template literal section leading up to the first
// expression in this attribute
const stringForPart = result.strings[partIndex];
// Find the attribute name
const name = lastAttributeNameRegex.exec(stringForPart)[2];
// Find the corresponding attribute
// If the attribute name contains special characters, lower-case
// it so that on XML nodes with case-sensitive getAttribute() we
// can still find the attribute, which will have been lower-cased
// by the parser.
//
// If the attribute name doesn't contain special character, it's
// important to _not_ lower-case it, in case the name is
// case-sensitive, like with XML attributes like "viewBox".
const attributeLookupName = (rewritesStyleAttribute && name === 'style') ?
'style$' :
/^[a-zA-Z-]*$/.test(name) ? name : name.toLowerCase();
const attributeValue = node.getAttribute(attributeLookupName);
const strings = attributeValue.split(markerRegex);
this.parts.push({ type: 'attribute', index, name, strings });
node.removeAttribute(attributeLookupName);
partIndex += strings.length - 1;
}
}
if (node.tagName === 'TEMPLATE') {
_prepareTemplate(node);
}
}
else if (node.nodeType === 3 /* Node.TEXT_NODE */) {
const nodeValue = node.nodeValue;
if (nodeValue.indexOf(marker) < 0) {
continue;
}
const parent = node.parentNode;
const strings = nodeValue.split(markerRegex);
const lastIndex = strings.length - 1;
// We have a part for each match found
partIndex += lastIndex;
// Generate a new text node for each literal section
// These nodes are also used as the markers for node parts
for (let i = 0; i < lastIndex; i++) {
parent.insertBefore((strings[i] === '') ? createMarker() :
document.createTextNode(strings[i]), node);
this.parts.push({ type: 'node', index: index++ });
}
parent.insertBefore(strings[lastIndex] === '' ?
createMarker() :
document.createTextNode(strings[lastIndex]), node);
nodesToRemove.push(node);
}
else if (node.nodeType === 8 /* Node.COMMENT_NODE */) {
if (node.nodeValue === marker) {
const parent = node.parentNode;
// Add a new marker node to be the startNode of the Part if any of
// the following are true:
// * We don't have a previousSibling
// * previousSibling is being removed (thus it's not the
// `previousNode`)
// * previousSibling is not a Text node
//
// TODO(justinfagnani): We should be able to use the previousNode
// here as the marker node and reduce the number of extra nodes we
// add to a template. See
// https://github.com/PolymerLabs/lit-html/issues/147
const previousSibling = node.previousSibling;
if (previousSibling === null || previousSibling !== previousNode ||
previousSibling.nodeType !== Node.TEXT_NODE) {
parent.insertBefore(createMarker(), node);
}
else {
index--;
}
this.parts.push({ type: 'node', index: index++ });
nodesToRemove.push(node);
// If we don't have a nextSibling add a marker node.
// We don't have to check if the next node is going to be removed,
// because that node will induce a new marker if so.
if (node.nextSibling === null) {
parent.insertBefore(createMarker(), node);
}
else {
index--;
}
currentNode = previousNode;
partIndex++;
}
else {
let i = -1;
while ((i = node.nodeValue.indexOf(marker, i + 1)) !== -1) {
// Comment node has a binding marker inside, make an inactive part
// The binding won't work, but subsequent bindings will
// TODO (justinfagnani): consider whether it's even worth it to
// make bindings in comments work
this.parts.push({ type: 'node', index: -1 });
}
}
}
}
};
_prepareTemplate(element);
// Remove text binding nodes after the walk to not disturb the TreeWalker
for (const n of nodesToRemove) {
n.parentNode.removeChild(n);
}
}
}
export const isTemplatePartActive = (part) => part.index !== -1;
// Allows `document.createComment('')` to be renamed for a
// small manual size-savings.
export const createMarker = () => document.createComment('');
/**
* This regex extracts the attribute name preceding an attribute-position
* expression. It does this by matching the syntax allowed for attributes
* against the string literal directly preceding the expression, assuming that
* the expression is in an attribute-value position.
*
* See attributes in the HTML spec:
* https://www.w3.org/TR/html5/syntax.html#attributes-0
*
* "\0-\x1F\x7F-\x9F" are Unicode control characters
*
* " \x09\x0a\x0c\x0d" are HTML space characters:
* https://www.w3.org/TR/html5/infrastructure.html#space-character
*
* So an attribute is:
* * The name: any character except a control character, space character, ('),
* ("), ">", "=", or "/"
* * Followed by zero or more space characters
* * Followed by "="
* * Followed by zero or more space characters
* * Followed by:
* * Any character except space, ('), ("), "<", ">", "=", (`), or
* * (") then any non-("), or
* * (') then any non-(')
*/
export const lastAttributeNameRegex = /([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F \x09\x0a\x0c\x0d"'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;
//# sourceMappingURL=template.js.map