1
0
mirror of https://github.com/svg/svgo.git synced 2025-04-19 10:22:15 +03:00
svgo/lib/xast.js
2021-08-23 19:24:04 +03:00

373 lines
10 KiB
JavaScript

'use strict';
/**
* @typedef {import('css-tree').CssNodePlain} CsstreeNode
* @typedef {import('./types').XastNode} XastNode
* @typedef {import('./types').XastChild} XastChild
* @typedef {import('./types').XastParent} XastParent
* @typedef {import('./types').XastRoot} XastRoot
* @typedef {import('./types').XastElement} XastElement
* @typedef {import('./types').Visitor} Visitor
*/
const csstree = require('css-tree');
/**
* @type {(string: string) => string}
*/
const trimQuotes = (string) => {
if (
(string.startsWith('"') && string.endsWith('"')) ||
(string.startsWith("'") && string.endsWith("'"))
) {
return string.slice(1, -1);
}
return string;
};
/**
* @type {(csstreeNode: CsstreeNode, xastElement: XastElement) => boolean}
*/
const elementMatches = (csstreeNode, xastElement) => {
if (csstreeNode.type === 'TypeSelector') {
return csstreeNode.name === '*' || csstreeNode.name === xastElement.name;
}
if (csstreeNode.type === 'IdSelector') {
return csstreeNode.name === xastElement.attributes.id;
}
if (csstreeNode.type === 'ClassSelector') {
return (
xastElement.attributes.class != null &&
xastElement.attributes.class.split(/\s+/).includes(csstreeNode.name)
);
}
if (csstreeNode.type === 'AttributeSelector') {
const name = csstreeNode.name.name;
let value;
if (csstreeNode.value != null && csstreeNode.value.type === 'Identifier') {
value = csstreeNode.value.name;
}
if (csstreeNode.value != null && csstreeNode.value.type === 'String') {
value = trimQuotes(csstreeNode.value.value);
}
if (xastElement.attributes[name] == null) {
return false;
}
if (csstreeNode.matcher == null) {
return true;
}
if (value == null) {
return false;
}
if (csstreeNode.matcher === '=') {
return xastElement.attributes[name] === value;
}
if (csstreeNode.matcher === '~=') {
return xastElement.attributes[name].split(/\s+/).includes(value);
}
if (csstreeNode.matcher === '|=') {
return (
xastElement.attributes[name] === value ||
xastElement.attributes[name].startsWith(`${value}-`)
);
}
if (csstreeNode.matcher === '^=') {
return xastElement.attributes[name].startsWith(value);
}
if (csstreeNode.matcher === '$=') {
return xastElement.attributes[name].endsWith(value);
}
if (csstreeNode.matcher === '*=') {
return xastElement.attributes[name].includes(value);
}
throw Error(`Unknown csstree attribute matcher "${csstreeNode.matcher}"`);
}
throw Error(
`Unknown csstree node type "${csstreeNode.type}" found when matching element`
);
};
/**
* @type {(
* startNode: XastParent,
* descendants: Set<XastElement>,
* parents: WeakMap<XastElement, XastParent>
* ) => void}
*/
const collectDescendantElements = (startNode, descendants, parents) => {
for (const childNode of startNode.children) {
if (childNode.type === 'element') {
parents.set(childNode, startNode);
descendants.add(childNode);
collectDescendantElements(childNode, descendants, parents);
}
}
};
/**
* @type {(
* startNodes: Array<XastElement>,
* children: Set<XastElement>,
* parents: WeakMap<XastElement, XastParent>
* ) => void}
*/
const collectChildrenElements = (startNodes, children, parents) => {
for (const startNode of startNodes) {
for (const child of startNode.children) {
if (child.type === 'element') {
parents.set(child, startNode);
children.add(child);
}
}
}
};
/**
* @type {(
* startNodes: Array<XastElement>,
* siblings: Set<XastElement>,
* parents: WeakMap<XastElement, XastParent>
* ) => void}
*/
const collectAdjacentSiblings = (startNodes, siblings, parents) => {
for (const startNode of startNodes) {
const parentNode = parents.get(startNode);
if (parentNode != null) {
const allSiblings = parentNode.children;
const index = allSiblings.indexOf(startNode);
const adjacentSiblings = allSiblings.slice(index + 1, index + 2);
for (const adjacentSibling of adjacentSiblings) {
if (adjacentSibling.type === 'element') {
parents.set(adjacentSibling, parentNode);
siblings.add(adjacentSibling);
}
}
}
}
};
/**
* @type {(
* startNodes: Array<XastElement>,
* siblings: Set<XastElement>,
* parents: WeakMap<XastElement, XastParent>
* ) => void}
*/
const collectGeneralSiblings = (startNodes, siblings, parents) => {
for (const startNode of startNodes) {
const parentNode = parents.get(startNode);
if (parentNode != null) {
const allSiblings = parentNode.children;
const index = allSiblings.indexOf(startNode);
const generalSiblings = allSiblings.slice(index + 1);
for (const generalSibling of generalSiblings) {
if (generalSibling.type === 'element') {
parents.set(generalSibling, parentNode);
siblings.add(generalSibling);
}
}
}
}
};
/**
* @type {(csstreeNodes: Array<CsstreeNode>, xastNode:XastParent) => Array<XastElement>}
*/
const combination = (csstreeNodes, xastNode) => {
/**
* @type {Set<XastElement>}
*/
let candidateNodes = new Set();
/**
* @type {WeakMap<XastElement, XastParent>}
*/
let candidateParents = new WeakMap();
collectDescendantElements(xastNode, candidateNodes, candidateParents);
/**
* @type {Array<XastElement>}
*/
let lastMatchedNodes = [];
for (const csstreeChild of csstreeNodes) {
if (csstreeChild.type === 'WhiteSpace') {
candidateNodes = new Set();
for (const node of lastMatchedNodes) {
collectDescendantElements(node, candidateNodes, candidateParents);
}
} else if (csstreeChild.type === 'Combinator') {
candidateNodes = new Set();
if (csstreeChild.name === '>') {
collectChildrenElements(
lastMatchedNodes,
candidateNodes,
candidateParents
);
} else if (csstreeChild.name === '+') {
collectAdjacentSiblings(
lastMatchedNodes,
candidateNodes,
candidateParents
);
} else if (csstreeChild.name === '~') {
collectGeneralSiblings(
lastMatchedNodes,
candidateNodes,
candidateParents
);
} else {
throw Error(`Unknown combinator ${csstreeChild.name}`);
}
} else {
// actionn
lastMatchedNodes = [];
for (const candidateNode of candidateNodes) {
if (elementMatches(csstreeChild, candidateNode)) {
lastMatchedNodes.push(candidateNode);
}
}
}
}
return lastMatchedNodes;
};
/**
* @type {(csstreeNode: CsstreeNode, xastNode:XastParent) => Array<XastElement>}
*/
const any = (csstreeNode, xastNode) => {
if (csstreeNode.type === 'SelectorList') {
const result = [];
for (const csstreeChild of csstreeNode.children) {
result.push(...any(csstreeChild, xastNode));
}
return result;
}
if (csstreeNode.type === 'Selector') {
return combination(csstreeNode.children, xastNode);
}
throw Error(`Unknown type ${csstreeNode.type}`);
};
/**
* @type {(selector: string, node: XastParent) => null | XastElement}
*/
const select = (selector, node) => {
const parsedSelector = csstree.toPlainObject(
csstree.parse(selector, { context: 'selectorList' })
);
const match = any(parsedSelector, node);
return match.length === 0 ? null : match[0];
};
exports.select = select;
/**
* @type {(selector: string, node: XastParent) => Array<XastElement>}
*/
const selectAll = (selector, node) => {
const parsedSelector = csstree.toPlainObject(
csstree.parse(selector, { context: 'selectorList' })
);
const match = any(parsedSelector, node);
return match;
};
exports.selectAll = selectAll;
/**
* @type {(node: XastParent, selector: string) => Array<XastElement>}
*/
const querySelectorAll = (node, selector) => {
return selectAll(selector, node);
};
exports.querySelectorAll = querySelectorAll;
/**
* @type {(node: XastParent, selector: string) => null | XastElement}
*/
const querySelector = (node, selector) => {
return select(selector, node);
};
exports.querySelector = querySelector;
/**
* @type {(selector: string, node: XastElement, root: XastRoot) => boolean}
*/
const matches = (selector, node, root) => {
const match = selectAll(selector, root);
return match.includes(node);
};
exports.matches = matches;
/**
* @type {(node: XastChild, name: string) => null | XastChild}
*/
const closestByName = (node, name) => {
let currentNode = node;
while (currentNode) {
if (currentNode.type === 'element' && currentNode.name === name) {
return currentNode;
}
// @ts-ignore parentNode is hidden from public usage
currentNode = currentNode.parentNode;
}
return null;
};
exports.closestByName = closestByName;
const traverseBreak = Symbol();
exports.traverseBreak = traverseBreak;
/**
* @type {(node: any, fn: any) => any}
*/
const traverse = (node, fn) => {
if (fn(node) === traverseBreak) {
return traverseBreak;
}
if (node.type === 'root' || node.type === 'element') {
for (const child of node.children) {
if (traverse(child, fn) === traverseBreak) {
return traverseBreak;
}
}
}
};
exports.traverse = traverse;
/**
* @type {(node: XastNode, visitor: Visitor, parentNode?: any) => void}
*/
const visit = (node, visitor, parentNode) => {
const callbacks = visitor[node.type];
if (callbacks && callbacks.enter) {
// @ts-ignore hard to infer
callbacks.enter(node, parentNode);
}
// visit root children
if (node.type === 'root') {
// copy children array to not loose cursor when children is spliced
for (const child of node.children) {
visit(child, visitor, node);
}
}
// visit element children if still attached to parent
if (node.type === 'element') {
if (parentNode.children.includes(node)) {
for (const child of node.children) {
visit(child, visitor, node);
}
}
}
if (callbacks && callbacks.exit) {
// @ts-ignore hard to infer
callbacks.exit(node, parentNode);
}
};
exports.visit = visit;
/**
* @type {(node: XastChild, parentNode: XastParent) => void}
*/
const detachNodeFromParent = (node, parentNode) => {
// avoid splice to not break for loops
parentNode.children = parentNode.children.filter((child) => child !== node);
};
exports.detachNodeFromParent = detachNodeFromParent;