All files / src/meta validator.ts

91.43% Statements 64/70
89.13% Branches 41/46
100% Functions 12/12
91.3% Lines 63/69
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 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165      1x       1x   998x 398x   600x 841x         456x 14x   442x     717x     717x 717x   442x 442x                               865x 785x   80x 80x 80x   107x 107x 152x     107x           47x 18x 12x 12x         6x   95x   68x       1127x 979x 148x 22x 30x     126x 126x 126x 102x 232x     24x                                         1267x 801x 801x       466x 3x     463x 6x 193x 24x 23x 199x 2x 16x             126x 126x               442x     310x     132x 132x 49x 81x 2x          
import { Permitted, PermittedEntry, PermittedGroup, PermittedOrder } from './element';
import { DOMNode } from '../dom';
 
const allowedKeys = [
	'exclude',
];
 
export class Validator {
	public static validatePermitted(node: DOMNode, rules: Permitted): boolean {
		if (!rules){
			return true;
		}
		return rules.some(rule => {
			return Validator.validatePermittedRule(node, rule);
		});
	}
 
	public static validateOccurrences(node: DOMNode, rules: Permitted, numSiblings: number): boolean {
		if (!rules){
			return true;
		}
		const category = rules.find(cur => {
			/** @todo handle complex rules and not just plain arrays (but as of now
			 * there is no use-case for it) */
			Iif (typeof cur !== 'string'){
				return false;
			}
			const match = cur.match(/^(.*?)[?*]?$/);
			return match && match[1] === node.tagName;
		});
		const limit = parseAmountQualifier(category as string);
		return limit === null || numSiblings <= limit;
	}
 
	/**
	 * Validate elements order.
	 *
	 * Given a parent element with children and metadata containing permitted
	 * order it will validate each children and ensure each one exists in the
	 * specified order.
	 *
	 * For instance, for a <table> element the <caption> element must come before
	 * a <thead> which must come before <tbody>.
	 *
	 * @param {DOMNode[]} children - Array of children to validate.
	 */
	public static validateOrder(children: DOMNode[], rules: PermittedOrder, cb: (node: DOMNode, prev: DOMNode) => void): boolean {
		if (!rules) {
			return true;
		}
		let i = 0;
		let prev = null;
		for (const node of children){
 
			const old = i;
			while (rules[i] && !Validator.validatePermittedCategory(node, rules[i])){
				i++;
			}
 
			if (i >= rules.length){
				/* Second check is if the order is specified for this element at all. It
				 * will be unspecified in two cases:
				 * - disallowed elements
				 * - elements where the order doesn't matter
				 * In both of these cases no error should be reported. */
				const orderSpecified = rules.find((cur: string) => Validator.validatePermittedCategory(node, cur));
				if (orderSpecified){
					cb(node, prev);
					return false;
				}
 
				/* if this element has unspecified order the index is restored so new
				 * elements of the same type can be specified again */
				i = old;
			}
			prev = node;
		}
		return true;
	}
 
	private static validatePermittedRule(node: DOMNode, rule: PermittedEntry): boolean {
		if (typeof rule === 'string'){
			return Validator.validatePermittedCategory(node, rule);
		} else if (Array.isArray(rule)){
			return rule.every((inner: PermittedEntry) => {
				return Validator.validatePermittedRule(node, inner);
			});
		} else {
			validateKeys(rule);
			Eif (rule.exclude){
				if (Array.isArray(rule.exclude)){
					return !rule.exclude.some((inner: PermittedEntry) => {
						return Validator.validatePermittedRule(node, inner);
					});
				} else {
					return !Validator.validatePermittedRule(node, rule.exclude);
				}
			} else {
				return false;
			}
		}
	}
 
	/**
	 * Validate node against a content category.
	 *
	 * When matching parent nodes against permitted parents use the superset
	 * parameter to also match for @flow. E.g. if a node expects a @phrasing
	 * parent it should also allow @flow parent since @phrasing is a subset of
	 * @flow.
	 *
	 * @param {DOMNode} node - The node to test against
	 * @param {string} category - Name of category with '@' prefix or tag name.
	 */
	private static validatePermittedCategory(node: DOMNode, category: string): boolean {
		/* match tagName when an explicit name is given */
		if (category[0] !== '@'){
			const [, tagName] = category.match(/^(.*?)[?*]?$/);
			return node.tagName === tagName;
		}
 
		/* if the meta entry is missing assume any content model would match */
		if (!node.meta){
			return true;
		}
 
		switch (category){
		case '@meta': return node.meta.metadata as boolean;
		case '@flow': return node.meta.flow as boolean;
		case '@sectioning': return node.meta.sectioning as boolean;
		case '@heading': return node.meta.heading as boolean;
		case '@phrasing': return node.meta.phrasing as boolean;
		case '@embedded': return node.meta.embedded as boolean;
		case '@interactive': return node.meta.interactive as boolean;
		default: throw new Error(`Invalid content category "${category}"`);
		}
	}
}
 
function validateKeys(rule: PermittedGroup): void {
	for (const key of Object.keys(rule)) {
		Iif (allowedKeys.indexOf(key) === -1){
			const str = JSON.stringify(rule);
			throw new Error(`Permitted rule "${str}" contains unknown property "${key}"`);
		}
	}
}
 
function parseAmountQualifier(category: string): number {
	if (!category){
		/* content not allowed, catched by another rule so just assume unlimited
		 * usage for this purpose */
		return null;
	}
 
	const [, qualifier] = category.match(/^.*?([?*]?)$/);
	switch (qualifier){
	case '?': return 1;
	case '': return null;
	case '*': return null;
	default:
		throw new Error(`Invalid amount qualifier "${qualifier}" used`);
	}
}