All files / src/meta table.ts

85.42% Statements 41/48
75.86% Branches 22/29
100% Functions 13/13
85.11% Lines 40/47
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}"`);
	}
}