| /** |
| * @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, multi-binding attributes, and |
| * attributes with markup-like text values. |
| */ |
| export const nodeMarker = `<!--${marker}-->`; |
| export const markerRegex = new RegExp(`${marker}|${nodeMarker}`); |
| /** |
| * Suffix appended to all bound attribute names. |
| */ |
| export const boundAttributeSuffix = '$lit$'; |
| /** |
| * An updatable Template that tracks the location of dynamic parts. |
| */ |
| export class Template { |
| constructor(result, element) { |
| this.parts = []; |
| this.element = element; |
| const nodesToRemove = []; |
| const stack = []; |
| // Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null |
| const walker = document.createTreeWalker(element.content, 133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */, null, false); |
| // Keeps track of the last index associated with a part. We try to delete |
| // unnecessary nodes, but we never want to associate two different parts |
| // to the same index. They must have a constant node between. |
| let lastPartIndex = 0; |
| let index = -1; |
| let partIndex = 0; |
| const { strings, values: { length } } = result; |
| while (partIndex < length) { |
| const node = walker.nextNode(); |
| if (node === null) { |
| // We've exhausted the content inside a nested template element. |
| // Because we still have parts (the outer for-loop), we know: |
| // - There is a template in the stack |
| // - The walker will find a nextNode outside the template |
| walker.currentNode = stack.pop(); |
| continue; |
| } |
| index++; |
| if (node.nodeType === 1 /* Node.ELEMENT_NODE */) { |
| if (node.hasAttributes()) { |
| const attributes = node.attributes; |
| const { length } = 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 correspondence between part index and attribute index. |
| let count = 0; |
| for (let i = 0; i < length; i++) { |
| if (endsWith(attributes[i].name, boundAttributeSuffix)) { |
| count++; |
| } |
| } |
| while (count-- > 0) { |
| // Get the template literal section leading up to the first |
| // expression in this attribute |
| const stringForPart = strings[partIndex]; |
| // Find the attribute name |
| const name = lastAttributeNameRegex.exec(stringForPart)[2]; |
| // Find the corresponding attribute |
| // All bound attributes have had a suffix added in |
| // TemplateResult#getHTML to opt out of special attribute |
| // handling. To look up the attribute value we also need to add |
| // the suffix. |
| const attributeLookupName = name.toLowerCase() + boundAttributeSuffix; |
| const attributeValue = node.getAttribute(attributeLookupName); |
| node.removeAttribute(attributeLookupName); |
| const statics = attributeValue.split(markerRegex); |
| this.parts.push({ type: 'attribute', index, name, strings: statics }); |
| partIndex += statics.length - 1; |
| } |
| } |
| if (node.tagName === 'TEMPLATE') { |
| stack.push(node); |
| walker.currentNode = node.content; |
| } |
| } |
| else if (node.nodeType === 3 /* Node.TEXT_NODE */) { |
| const data = node.data; |
| if (data.indexOf(marker) >= 0) { |
| const parent = node.parentNode; |
| const strings = data.split(markerRegex); |
| const lastIndex = strings.length - 1; |
| // 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++) { |
| let insert; |
| let s = strings[i]; |
| if (s === '') { |
| insert = createMarker(); |
| } |
| else { |
| const match = lastAttributeNameRegex.exec(s); |
| if (match !== null && endsWith(match[2], boundAttributeSuffix)) { |
| s = s.slice(0, match.index) + match[1] + |
| match[2].slice(0, -boundAttributeSuffix.length) + match[3]; |
| } |
| insert = document.createTextNode(s); |
| } |
| parent.insertBefore(insert, node); |
| this.parts.push({ type: 'node', index: ++index }); |
| } |
| // If there's no text, we must insert a comment to mark our place. |
| // Else, we can trust it will stick around after cloning. |
| if (strings[lastIndex] === '') { |
| parent.insertBefore(createMarker(), node); |
| nodesToRemove.push(node); |
| } |
| else { |
| node.data = strings[lastIndex]; |
| } |
| // We have a part for each match found |
| partIndex += lastIndex; |
| } |
| } |
| else if (node.nodeType === 8 /* Node.COMMENT_NODE */) { |
| if (node.data === 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 |
| // * The previousSibling is already the start of a previous part |
| if (node.previousSibling === null || index === lastPartIndex) { |
| index++; |
| parent.insertBefore(createMarker(), node); |
| } |
| lastPartIndex = index; |
| this.parts.push({ type: 'node', index }); |
| // If we don't have a nextSibling, keep this node so we have an end. |
| // Else, we can remove it to save future costs. |
| if (node.nextSibling === null) { |
| node.data = ''; |
| } |
| else { |
| nodesToRemove.push(node); |
| index--; |
| } |
| partIndex++; |
| } |
| else { |
| let i = -1; |
| while ((i = node.data.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 }); |
| partIndex++; |
| } |
| } |
| } |
| } |
| // Remove text binding nodes after the walk to not disturb the TreeWalker |
| for (const n of nodesToRemove) { |
| n.parentNode.removeChild(n); |
| } |
| } |
| } |
| const endsWith = (str, suffix) => { |
| const index = str.length - suffix.length; |
| return index >= 0 && str.slice(index) === suffix; |
| }; |
| 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#elements-attributes |
| * |
| * " \x09\x0a\x0c\x0d" are HTML space characters: |
| * https://www.w3.org/TR/html5/infrastructure.html#space-characters |
| * |
| * "\0-\x1F\x7F-\x9F" are Unicode control characters, which includes every |
| * space character except " ". |
| * |
| * 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 = |
| // eslint-disable-next-line no-control-regex |
| /([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/; |
| //# sourceMappingURL=template.js.map |