a post-component library for building user-interfaces on the web.
at main 129 lines 4.3 kB view raw
1import { assert, lexer } from '../shared.ts' 2import { is_comment, is_document_fragment, is_element } from './util.ts' 3 4export const PART_CHILD = 0 5export const PART_DIRECTIVE = 1 6export const PART_ATTRIBUTE = 2 7export const PART_PROPERTY = 3 8 9export type PartData = 10 | [type: typeof PART_CHILD, index: number] 11 | [type: typeof PART_DIRECTIVE] 12 | [type: typeof PART_ATTRIBUTE, name: string] 13 | [type: typeof PART_PROPERTY, name: string] 14 15export interface CompiledTemplate { 16 _content: DocumentFragment 17 _parts: [idx: number, PartData][] 18 _root_parts: number[] 19} 20 21export const DYNAMIC_WHOLE: RegExp = /^dyn-\$(\d+)\$$/ 22const DYNAMIC_GLOBAL = /dyn-\$(\d+)\$/g 23const FORCE_ATTRIBUTES = /-|^class$|^for$/i 24const NEEDS_UPPERCASING = /\$./g 25 26const templates: WeakMap<TemplateStringsArray, CompiledTemplate> = new WeakMap() 27export function compile_template(statics: TemplateStringsArray): CompiledTemplate { 28 const cached = templates.get(statics) 29 if (cached) return cached 30 31 const template_element = document.createElement('template') 32 let next_part = 0 33 template_element.innerHTML = [...lexer.lex(statics)] 34 .map(([char, state]) => { 35 if (char === '\0') { 36 if (state === lexer.DATA) return `<!--dyn-$${next_part++}$-->` 37 else return `dyn-$${next_part++}$` 38 } 39 if (state === lexer.ATTR_NAME && char.toLowerCase() !== char) { 40 return `$${char}` 41 } 42 return char 43 }) 44 .join('') 45 46 next_part = 0 47 48 const compiled: CompiledTemplate = { 49 _content: template_element.content, 50 _parts: Array(statics.length - 1), 51 _root_parts: [], 52 } 53 54 function patch(node: DocumentFragment | HTMLElement | SVGElement, idx: number, data: PartData) { 55 assert(next_part < compiled._parts.length, 'got more parts than expected') 56 if (is_document_fragment(node)) compiled._root_parts.push(next_part) 57 else if ('dynparts' in node.dataset) node.dataset.dynparts += ' ' + next_part 58 // @ts-expect-error -- this assigment will cast nextPart to a string 59 else node.dataset.dynparts = next_part 60 compiled._parts[next_part++] = [idx, data] 61 } 62 63 const walker = document.createTreeWalker(template_element.content, 129) 64 assert((NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT) === 129) 65 66 while (walker.nextNode()) { 67 const node = walker.currentNode 68 if (is_comment(node)) { 69 const match = DYNAMIC_WHOLE.exec(node.data) 70 if (match !== null) { 71 const parent_node = node.parentNode 72 assert(parent_node !== null, 'all text nodes should have a parent node') 73 assert( 74 parent_node instanceof DocumentFragment || 75 parent_node instanceof HTMLElement || 76 parent_node instanceof SVGElement, 77 ) 78 79 // these will become the start and end of the span: 80 parent_node.insertBefore(new Text(), node) 81 parent_node.insertBefore(new Text(), node.nextSibling) 82 83 patch(parent_node, parseInt(match[1]), [PART_CHILD, [...parent_node.childNodes].indexOf(node)]) 84 } 85 } else { 86 assert(is_element(node)) 87 assert(node instanceof HTMLElement || node instanceof SVGElement) 88 89 for (const name of node.getAttributeNames()) { 90 const value = node.getAttribute(name) 91 assert(value !== null) 92 93 let match = DYNAMIC_WHOLE.exec(name) 94 if (match !== null) { 95 // directive: 96 node.removeAttribute(name) 97 assert(value === '', `directives must not have values`) 98 patch(node, parseInt(match[1]), [PART_DIRECTIVE]) 99 } else { 100 // properties: 101 match = DYNAMIC_WHOLE.exec(value) 102 const remapped_name = name.replace(NEEDS_UPPERCASING, match => match[1].toUpperCase()) 103 if (match !== null) { 104 node.removeAttribute(name) 105 if (FORCE_ATTRIBUTES.test(remapped_name)) { 106 patch(node, parseInt(match[1]), [PART_ATTRIBUTE, remapped_name]) 107 } else { 108 patch(node, parseInt(match[1]), [PART_PROPERTY, remapped_name]) 109 } 110 } else if (remapped_name !== name) { 111 assert(!node.hasAttribute(remapped_name), `duplicate attribute ${remapped_name}`) 112 node.setAttribute(remapped_name, value) 113 node.removeAttribute(name) 114 } else { 115 assert( 116 !DYNAMIC_GLOBAL.test(value), 117 `expected a whole dynamic value for ${remapped_name}, got a partial one`, 118 ) 119 } 120 } 121 } 122 } 123 } 124 125 compiled._parts.length = next_part 126 127 templates.set(statics, compiled) 128 return compiled 129}