1/// BareSpecifier=lit-html/lib/template 2 3/** 4 * @license 5 * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 6 * This code may only be used under the BSD style license found at 7 * http://polymer.github.io/LICENSE.txt 8 * The complete set of authors may be found at 9 * http://polymer.github.io/AUTHORS.txt 10 * The complete set of contributors may be found at 11 * http://polymer.github.io/CONTRIBUTORS.txt 12 * Code distributed by Google as part of the polymer project is also 13 * subject to an additional IP rights grant found at 14 * http://polymer.github.io/PATENTS.txt 15 */ 16 17/** 18 * An expression marker with embedded unique key to avoid collision with 19 * possible text in templates. 20 */ 21export const marker = `{{lit-${String(Math.random()).slice(2)}}}`; 22/** 23 * An expression marker used text-positions, multi-binding attributes, and 24 * attributes with markup-like text values. 25 */ 26 27export const nodeMarker = `<!--${marker}-->`; 28export const markerRegex = new RegExp(`${marker}|${nodeMarker}`); 29/** 30 * Suffix appended to all bound attribute names. 31 */ 32 33export const boundAttributeSuffix = '$lit$'; 34/** 35 * An updatable Template that tracks the location of dynamic parts. 36 */ 37 38export class Template { 39 constructor(result, element) { 40 this.parts = []; 41 this.element = element; 42 const nodesToRemove = []; 43 const stack = []; // Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null 44 45 const walker = document.createTreeWalker(element.content, 133 46 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */ 47 , null, false); // Keeps track of the last index associated with a part. We try to delete
48 // unnecessary nodes, but we never want to associate two different parts 49 // to the same index. They must have a constant node between. 50 51 let lastPartIndex = 0; 52 let index = -1; 53 let partIndex = 0; 54 const { 55 strings, 56 values: { 57 length 58 } 59 } = result; 60 61 while (partIndex < length) { 62 const node = walker.nextNode(); 63 64 if (node === null) { 65 // We've exhausted the content inside a nested template element. 66 // Because we still have parts (the outer for-loop), we know: 67 // - There is a template in the stack 68 // - The walker will find a nextNode outside the template 69 walker.currentNode = stack.pop(); 70 continue; 71 } 72 73 index++; 74 75 if (node.nodeType === 1 76 /* Node.ELEMENT_NODE */ 77 ) { 78 if (node.hasAttributes()) { 79 const attributes = node.attributes; 80 const { 81 length 82 } = attributes; // Per 83 // https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap, 84 // attributes are not guaranteed to be returned in document order. 85 // In particular, Edge/IE can return them out of order, so we cannot 86 // assume a correspondence between part index and attribute index. 87 88 let count = 0; 89 90 for (let i = 0; i < length; i++) { 91 if (endsWith(attributes[i].name, boundAttributeSuffix)) { 92 count++; 93 } 94 } 95 96 while (count-- > 0) { 97 // Get the template literal section leading up to the first 98 // expression in this attribute 99 const stringForPart = strings[partIndex]; // Find the attribute name 100 101 const name = lastAttributeNameRegex.exec(stringForPart)[2]; // Find the corresponding attribute 102 // All bound attributes have had a suffix added in 103 // TemplateResult#getHTML to opt out of special attribute 104 // handling. To look up the attribute value we also need to add 105 // the suffix. 106 107 const attributeLookupName = name.toLowerCase() + boundAttributeSuffix; 108 const attributeValue = node.getAttribute(attributeLookupName); 109 node.removeAttribute(attributeLookupName); 110 const statics = attributeValue.split(markerRegex); 111 this.parts.push({ 112 type: 'attribute', 113 index, 114 name, 115 strings: statics 116 }); 117 partIndex += statics.length - 1; 118 } 119 } 120 121 if (node.tagName === 'TEMPLATE') { 122 stack.push(node); 123 walker.currentNode = node.content; 124 } 125 } else if (node.nodeType === 3 126 /* Node.TEXT_NODE */ 127 ) { 128 const data = node.data; 129 130 if (data.indexOf(marker) >= 0) { 131 const parent = node.parentNode; 132 const strings = data.split(markerRegex); 133 const lastIndex = strings.length - 1; // Generate a new text node for each literal section 134 // These nodes are also used as the markers for node parts 135 136 for (let i = 0; i < lastIndex; i++) { 137 let insert; 138 let s = strings[i]; 139 140 if (s === '') { 141 insert = createMarker(); 142 } else { 143 const match = lastAttributeNameRegex.exec(s); 144 145 if (match !== null && endsWith(match[2], boundAttributeSuffix)) { 146 s = s.slice(0, match.index) + match[1] + match[2].slice(0, -boundAttributeSuffix.length) + match[3]; 147 } 148 149 insert = document.createTextNode(s); 150 } 151 152 parent.insertBefore(insert, node); 153 this.parts.push({ 154 type: 'node', 155 index: ++index 156 }); 157 } // If there's no text, we must insert a comment to mark our place. 158 // Else, we can trust it will stick around after cloning. 159 160 161 if (strings[lastIndex] === '') { 162 parent.insertBefore(createMarker(), node); 163 nodesToRemove.push(node); 164 } else { 165 node.data = strings[lastIndex]; 166 } // We have a part for each match found 167 168 169 partIndex += lastIndex; 170 } 171 } else if (node.nodeType === 8 172 /* Node.COMMENT_NODE */ 173 ) { 174 if (node.data === marker) { 175 const parent = node.parentNode; // Add a new marker node to be the startNode of the Part if any of 176 // the following are true: 177 // * We don't have a previousSibling 178 // * The previousSibling is already the start of a previous part 179 180 if (node.previousSibling === null || index === lastPartIndex) { 181 index++; 182 parent.insertBefore(createMarker(), node); 183 } 184 185 lastPartIndex = index; 186 this.parts.push({ 187 type: 'node', 188 index 189 }); // If we don't have a nextSibling, keep this node so we have an end. 190 // Else, we can remove it to save future costs. 191 192 if (node.nextSibling === null) { 193 node.data = ''; 194 } else { 195 nodesToRemove.push(node); 196 index--; 197 } 198 199 partIndex++; 200 } else { 201 let i = -1; 202 203 while ((i = node.data.indexOf(marker, i + 1)) !== -1) { 204 // Comment node has a binding marker inside, make an inactive part 205 // The binding won't work, but subsequent bindings will 206 // TODO (justinfagnani): consider whether it's even worth it to 207 // make bindings in comments work 208 this.parts.push({ 209 type: 'node', 210 index: -1 211 }); 212 partIndex++; 213 } 214 } 215 } 216 } // Remove text binding nodes after the walk to not disturb the TreeWalker 217 218 219 for (const n of nodesToRemove) { 220 n.parentNode.removeChild(n); 221 } 222 } 223 224} 225
226const endsWith = (str, suffix) => { 227 const index = str.length - suffix.length; 228 return index >= 0 && str.slice(index) === suffix; 229}; 230 231export const isTemplatePartActive = part => part.index !== -1; // Allows `document.createComment('')` to be renamed for a 232// small manual size-savings. 233 234export const createMarker = () => document.createComment(''); 235/** 236 * This regex extracts the attribute name preceding an attribute-position 237 * expression. It does this by matching the syntax allowed for attributes 238 * against the string literal directly preceding the expression, assuming that 239 * the expression is in an attribute-value position. 240 * 241 * See attributes in the HTML spec: 242 * https://www.w3.org/TR/html5/syntax.html#elements-attributes 243 * 244 * " \x09\x0a\x0c\x0d" are HTML space characters: 245 * https://www.w3.org/TR/html5/infrastructure.html#space-characters 246 * 247 * "\0-\x1F\x7F-\x9F" are Unicode control characters, which includes every 248 * space character except " ". 249 * 250 * So an attribute is: 251 * * The name: any character except a control character, space character, ('), 252 * ("), ">", "=", or "/" 253 * * Followed by zero or more space characters 254 * * Followed by "=" 255 * * Followed by zero or more space characters 256 * * Followed by: 257 * * Any character except space, ('), ("), "<", ">", "=", (`), or 258 * * (") then any non-("), or 259 * * (') then any non-(') 260 */ 261 262export const lastAttributeNameRegex = // eslint-disable-next-line no-control-regex 263/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.