1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | 1x 1x 1x 1x 606x 606x 77020x 570x 2345x 77020x 228550x 77020x 1235x 1213x 1213x 8491x 8491x 84x 84x 84x 84x 84x 84x 84x 84x 4x 4x 4x 3x 1x 2x 3x 50x 50x 30x 90x 30x 30x 27x 3x | import { DOMNode } from '../dom'; import { MetaElement, ElementTable, PropertyExpression } from './element'; const allowedKeys = [ 'tagName', 'metadata', 'flow', 'sectioning', 'heading', 'phrasing', 'embedded', 'interactive', 'deprecated', 'void', 'transparent', 'implicitClosed', 'deprecatedAttributes', 'permittedContent', 'permittedDescendants', 'permittedOrder', ]; const dynamicKeys = [ 'metadata', 'flow', 'sectioning', 'heading', 'phrasing', 'embedded', 'interactive', ]; // eslint-disable-next-line no-unused-vars type PropertyEvaluator = (node: DOMNode, options: any) => boolean; const functionTable: { [key: string]: PropertyEvaluator } = { isDescendant, hasAttribute, matchAttribute, }; export class MetaTable { readonly elements: ElementTable; constructor(){ this.elements = {}; } loadFromObject(obj: ElementTable){ for (const key of Object.keys(obj)) { this.addEntry(key, obj[key]); } } loadFromFile(filename: string){ this.loadFromObject(require(filename)); } getMetaFor(tagName: string): MetaElement { /* @TODO Only entries with dynamic properties has to be copied, static * entries could be shared */ return this.elements[tagName] ? Object.assign({}, this.elements[tagName]) : null; } private addEntry(tagName: string, entry: MetaElement): void { for (const key of Object.keys(entry)) { Iif (allowedKeys.indexOf(key) === -1){ throw new Error(`Metadata for <${tagName}> contains unknown property "${key}"`); } } this.elements[tagName] = Object.assign({ tagName, void: false, }, entry); } resolve(node: DOMNode){ if (node.meta){ expandProperties(node, node.meta); } } } function expandProperties(node: DOMNode, entry: MetaElement){ for (const key of dynamicKeys){ const property = entry[key]; if (property && typeof property !== 'boolean'){ entry[key] = evaluateProperty(node, property as PropertyExpression); } } } function evaluateProperty(node: DOMNode, expr: PropertyExpression): boolean { const [func, options] = parseExpression(expr); return func(node, options); } function parseExpression(expr: PropertyExpression): [PropertyEvaluator, any] { Iif (typeof expr === 'string'){ return parseExpression([expr, {}]); } else { const [funcName, options] = expr; const func = functionTable[funcName]; Iif (!func){ throw new Error(`Failed to find function when evaluation property expression "${expr}"`); } return [func, options]; } } function isDescendant(node: DOMNode, tagName: any): boolean { Iif (typeof tagName !== 'string'){ throw new Error(`Property expression "isDescendant" must take string argument`); } let cur: DOMNode = node.parent; while (!cur.isRootElement()){ if (cur.is(tagName)){ return true; } cur = cur.parent; } return false; } function hasAttribute(node: DOMNode, attr: any): boolean { Iif (typeof attr !== 'string'){ throw new Error(`Property expression "hasAttribute" must take string argument`); } return node.hasAttribute(attr); } function matchAttribute(node: DOMNode, match: any): boolean { Iif (!Array.isArray(match) || match.length !== 3){ throw new Error(`Property expression "matchAttribute" must take [key, op, value] array as argument`); } const [key, op, value] = match.map(x => x.toLowerCase()); const nodeValue = (node.getAttribute(key) || '').toLowerCase(); switch (op){ case '!=': return nodeValue !== value; case '=': return nodeValue === value; default: throw new Error(`Property expression "matchAttribute" has invalid operator "${op}"`); } } |