UNPKG

clarity-pattern-parser

Version:

Parsing Library for Typescript and Javascript.

280 lines (226 loc) 8.3 kB
import { generateErrorMessage } from "../patterns/generate_error_message"; import { selectorParser } from "./selector_parser"; import { Node } from "../ast/Node"; const combinatorMap: Record<string, boolean> = { "adjacent": true, "after": true, "descendant": true, "direct-child": true }; const operatorMap: Record<string, boolean> = { "equal": true, "not-equal": true, "starts-with": true, "ends-with": true, "contains": true, "greater-than-or-equal": true, "less-than-or-equal": true, "greater-than": true, "less-than": true }; export class Selector { private _selectedNodes: Node[]; private _selectorAst: Node; private _combinator: string | null; constructor(selector: string) { this._selectedNodes = []; this._combinator = null; const result = selectorParser.exec(selector); if (result.ast == null) { const message = generateErrorMessage(selectorParser, result.cursor); throw new Error(`[Invalid Selector] ${message}`); } this._selectorAst = result.ast; } find(nodes: Node[]) { this._selectedNodes = nodes; const ast = this._selectorAst; ast.walkUp((node) => { this._process(node); }); return this._selectedNodes; } filter(nodes: Node[]) { if (nodes.length < 1) { return []; } const nodeMap = new Map(); nodes.forEach(n => nodeMap.set(n, n)); this._selectedNodes = [nodes[0].findRoot()]; const ast = this._selectorAst; ast.walkUp((node) => { this._process(node); }); return this._selectedNodes.filter(n => nodeMap.has(n)); } not(nodes: Node[]) { if (nodes.length < 1) { return []; } this._selectedNodes = [nodes[0].findRoot()]; const ast = this._selectorAst; ast.walkUp((node) => { this._process(node); }); const selectedNodeMap = new Map(); this._selectedNodes.forEach(n => selectedNodeMap.set(n, n)); return nodes.filter(n => !selectedNodeMap.has(n)); } parents(nodes: Node[]) { if (nodes.length < 1) { return []; } this._selectedNodes = [nodes[0].findRoot()]; const ast = this._selectorAst; ast.walkUp((node) => { this._process(node); }); const result = new Set<Node>(); const ancestorMap = new Map<Node, boolean>(); this._selectedNodes.forEach(n => ancestorMap.set(n, true)); nodes.forEach(n => { const ancestor = n.findAncestor(a => ancestorMap.has(a)); if (ancestor != null) { result.add(ancestor); } }); return Array.from(result); } private _process(ast: Node) { const nodeName = ast.name; if (nodeName === "wild-card") { this._selectedNodes = this._processWildCard(); } else if (nodeName === "or-selector") { this._selectedNodes = this._processOrSelector(ast); } else if (nodeName === "name-selector" || (nodeName === "name" && (ast.parent == null || ast.parent.name === "selector-expression"))) { this._selectedNodes = this._processNameSelector(ast); } else if (nodeName === "attribute-selector" && (ast.parent == null || ast.parent.name === "selector-expression")) { this._selectedNodes = this._processAttributeSelector(ast); } else if (combinatorMap[nodeName]) { this._combinator = nodeName; } else if (nodeName === "selector-expression") { this._combinator = null; } } private _processWildCard() { return this._selectedNodes.map(n => { return this._selectWithCombinator(n, () => true); }).flat(); } private _processOrSelector(ast: Node) { const selectorNodes = ast.children.filter(n => n.name !== "comma"); const set: Set<Node> = new Set(); const selectors = selectorNodes.map(n => new Selector(n.toString())); selectors.map(s => { return s.find(this._selectedNodes.slice()); }).flat().forEach((node) => { set.add(node); }); return Array.from(set); } private _processNameSelector(ast: Node) { if (ast.children.length > 1) { return this._selectedNodes.map(n => { const name = ast.children[0].value; return this._selectWithCombinator(n, (node: Node) => { return node.name === name && this._isAttributeMatch(node, ast); }); }).flat(); } else { return this._selectedNodes.map(n => { return this._selectWithCombinator(n, (node: Node) => node.name === ast.value); }).flat(); } } private _processAttributeSelector(ast: Node) { return this._selectedNodes.map(n => { return this._selectWithCombinator(n, (node: Node) => { return this._isAttributeMatch(node, ast); }); }).flat(); } private _selectWithCombinator(node: Node, predicate: (node: Node) => boolean) { if (this._combinator === "adjacent") { const sibling = node.nextSibling(); if (sibling == null) { return []; } if (predicate(sibling)) { return [sibling]; } else { return []; } } else if (this._combinator === "after") { const parent = node.parent; if (parent == null) { return []; } const index = parent.findChildIndex(node); const after = parent.children.slice(index + 1); return after.filter(predicate); } else if (this._combinator === "direct-child") { return node.children.filter(predicate); } else if (this._combinator === "descendant" || this._combinator == null) { return node.findAll(predicate); } else { return []; } } private _isAttributeMatch(node: Node, ast: Node) { const name = this._getAttributeName(ast); const operator = this._getAttributeOperator(ast); const value = this._getAttributeValue(ast); const anyNode = node as any; if (anyNode[name] == null) { return false; } if (operator === "equal") { return anyNode[name] === value; } else if (operator === "not-equal") { return anyNode[name] !== value; } else if (operator === "starts-with") { return anyNode[name].toString().startsWith(value); } else if (operator === "ends-with") { return anyNode[name].toString().endsWith(value); } else if (operator === "contains") { return anyNode[name].toString().includes(value); } else if (operator === "greater-than-or-equal") { return anyNode[name] >= value; } else if (operator === "less-than-or-equal") { return anyNode[name] <= value; } else if (operator === "greater-than") { return anyNode[name] > value; } else if (operator === "less-than") { return anyNode[name] < value; } return false; } private _getAttributeName(ast: Node) { return (ast.find(n => n.name === "attribute-name") as Node).value; } private _getAttributeValue(ast: Node) { let valueNode: Node | null = ast.find(n => n.name === "single-quote-string-literal"); if (valueNode != null) { return valueNode.value.slice(1, -1); } else { valueNode = ast.find(n => n.name === "value"); } if (valueNode != null) { return valueNode.value; } else { valueNode = ast.find(n => n.name === "number"); } return (valueNode as Node).value; } private _getAttributeOperator(ast: Node) { return (ast.find(n => operatorMap[n.name]) as Node).name; } }