| /** |
| * @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 |
| */ |
| |
| import {TemplateResult} from './template-result.js'; |
| |
| /** |
| * 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 { |
| readonly parts: TemplatePart[] = []; |
| readonly element: HTMLTemplateElement; |
| |
| constructor(result: TemplateResult, element: HTMLTemplateElement) { |
| this.element = element; |
| |
| const nodesToRemove: Node[] = []; |
| const stack: Node[] = []; |
| // 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() as Element | Comment | Text | null; |
| 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 as Element).hasAttributes()) { |
| const attributes = (node as Element).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 as Element).getAttribute(attributeLookupName)!; |
| (node as Element).removeAttribute(attributeLookupName); |
| const statics = attributeValue.split(markerRegex); |
| this.parts.push({type: 'attribute', index, name, strings: statics}); |
| partIndex += statics.length - 1; |
| } |
| } |
| if ((node as Element).tagName === 'TEMPLATE') { |
| stack.push(node); |
| walker.currentNode = (node as HTMLTemplateElement).content; |
| } |
| } else if (node.nodeType === 3 /* Node.TEXT_NODE */) { |
| const data = (node as Text).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: Node; |
| 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 as Text).data = strings[lastIndex]; |
| } |
| // We have a part for each match found |
| partIndex += lastIndex; |
| } |
| } else if (node.nodeType === 8 /* Node.COMMENT_NODE */) { |
| if ((node as Comment).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 as Comment).data = ''; |
| } else { |
| nodesToRemove.push(node); |
| index--; |
| } |
| partIndex++; |
| } else { |
| let i = -1; |
| while ((i = (node as Comment).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: string, suffix: string): boolean => { |
| const index = str.length - suffix.length; |
| return index >= 0 && str.slice(index) === suffix; |
| }; |
| |
| /** |
| * A placeholder for a dynamic expression in an HTML template. |
| * |
| * There are two built-in part types: AttributePart and NodePart. NodeParts |
| * always represent a single dynamic expression, while AttributeParts may |
| * represent as many expressions are contained in the attribute. |
| * |
| * A Template's parts are mutable, so parts can be replaced or modified |
| * (possibly to implement different template semantics). The contract is that |
| * parts can only be replaced, not removed, added or reordered, and parts must |
| * always consume the correct number of values in their `update()` method. |
| * |
| * TODO(justinfagnani): That requirement is a little fragile. A |
| * TemplateInstance could instead be more careful about which values it gives |
| * to Part.update(). |
| */ |
| export type TemplatePart = { |
| readonly type: 'node'; index: number; |
| }|{ |
| readonly type: 'attribute'; |
| index: number; |
| readonly name: string; |
| readonly strings: ReadonlyArray<string>; |
| }; |
| |
| export const isTemplatePartActive = (part: TemplatePart) => 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"'`<>=]*|"[^"]*|'[^']*))$/; |