1
0
mirror of https://github.com/svg/svgo.git synced 2025-08-07 15:22:54 +03:00

Support matches and combinations

This commit is contained in:
Bogdan Chadkin
2021-08-23 13:24:48 +03:00
parent 2d0d3e2889
commit 4da3ede37b
4 changed files with 196 additions and 69 deletions

View File

@@ -3,6 +3,7 @@
/** /**
* @typedef {import('css-tree').Rule} CsstreeRule * @typedef {import('css-tree').Rule} CsstreeRule
* @typedef {import('./types').Specificity} Specificity * @typedef {import('./types').Specificity} Specificity
* @typedef {import('./types').Stylesheet} Stylesheet
* @typedef {import('./types').StylesheetRule} StylesheetRule * @typedef {import('./types').StylesheetRule} StylesheetRule
* @typedef {import('./types').StylesheetDeclaration} StylesheetDeclaration * @typedef {import('./types').StylesheetDeclaration} StylesheetDeclaration
* @typedef {import('./types').ComputedStyles} ComputedStyles * @typedef {import('./types').ComputedStyles} ComputedStyles
@@ -128,7 +129,7 @@ const parseStyleDeclarations = (css) => {
}; };
/** /**
* @type {(stylesheet: Array<StylesheetRule>, node: XastElement) => ComputedStyles} * @type {(stylesheet: Stylesheet, node: XastElement) => ComputedStyles}
*/ */
const computeOwnStyle = (stylesheet, node) => { const computeOwnStyle = (stylesheet, node) => {
/** /**
@@ -146,8 +147,8 @@ const computeOwnStyle = (stylesheet, node) => {
} }
// collect matching rules // collect matching rules
for (const { selectors, declarations, dynamic } of stylesheet) { for (const { selectors, declarations, dynamic } of stylesheet.rules) {
if (matches(node, selectors)) { if (matches(selectors, node, stylesheet.root)) {
for (const { name, value, important } of declarations) { for (const { name, value, important } of declarations) {
const computed = computedStyle[name]; const computed = computedStyle[name];
if (computed && computed.type === 'dynamic') { if (computed && computed.type === 'dynamic') {
@@ -211,13 +212,13 @@ const compareSpecificity = (a, b) => {
}; };
/** /**
* @type {(root: XastRoot) => Array<StylesheetRule>} * @type {(root: XastRoot) => Stylesheet}
*/ */
const collectStylesheet = (root) => { const collectStylesheet = (root) => {
/** /**
* @type {Array<StylesheetRule>} * @type {Array<StylesheetRule>}
*/ */
const stylesheet = []; const rules = [];
// find and parse all styles // find and parse all styles
visit(root, { visit(root, {
element: { element: {
@@ -233,7 +234,7 @@ const collectStylesheet = (root) => {
const children = node.children; const children = node.children;
for (const child of children) { for (const child of children) {
if (child.type === 'text' || child.type === 'cdata') { if (child.type === 'text' || child.type === 'cdata') {
stylesheet.push(...parseStylesheet(child.value, dynamic)); rules.push(...parseStylesheet(child.value, dynamic));
} }
} }
} }
@@ -242,15 +243,15 @@ const collectStylesheet = (root) => {
}, },
}); });
// sort by selectors specificity // sort by selectors specificity
stable.inplace(stylesheet, (a, b) => stable.inplace(rules, (a, b) =>
compareSpecificity(a.specificity, b.specificity) compareSpecificity(a.specificity, b.specificity)
); );
return stylesheet; return { root, rules };
}; };
exports.collectStylesheet = collectStylesheet; exports.collectStylesheet = collectStylesheet;
/** /**
* @type {(stylesheet: Array<StylesheetRule>, node: XastElement) => ComputedStyles} * @type {(stylesheet: Stylesheet, node: XastElement) => ComputedStyles}
*/ */
const computeStyle = (stylesheet, node) => { const computeStyle = (stylesheet, node) => {
// collect inherited styles // collect inherited styles

View File

@@ -88,6 +88,11 @@ export type StylesheetRule = {
declarations: Array<StylesheetDeclaration>; declarations: Array<StylesheetDeclaration>;
}; };
export type Stylesheet = {
root: XastRoot;
rules: Array<StylesheetRule>;
};
type StaticStyle = { type StaticStyle = {
type: 'static'; type: 'static';
inherited: boolean; inherited: boolean;

View File

@@ -5,13 +5,12 @@
* @typedef {import('./types').XastNode} XastNode * @typedef {import('./types').XastNode} XastNode
* @typedef {import('./types').XastChild} XastChild * @typedef {import('./types').XastChild} XastChild
* @typedef {import('./types').XastParent} XastParent * @typedef {import('./types').XastParent} XastParent
* @typedef {import('./types').XastRoot} XastRoot
* @typedef {import('./types').XastElement} XastElement * @typedef {import('./types').XastElement} XastElement
* @typedef {import('./types').Visitor} Visitor * @typedef {import('./types').Visitor} Visitor
*/ */
const csstree = require('css-tree'); const csstree = require('css-tree');
const { is } = require('css-select');
const xastAdaptor = require('./svgo/css-select-adapter.js');
/** /**
* @type {(string: string) => string} * @type {(string: string) => string}
@@ -65,24 +64,145 @@ const elementMatches = (csstreeNode, xastElement) => {
}; };
/** /**
* @type {(csstreeNode: CsstreeNode, xastNode: XastNode) => Array<XastElement>} * @type {(
* startNode: XastParent,
* descendants: Array<XastElement>,
* parents: WeakMap<XastElement, XastParent>
* ) => void}
*/ */
const descendantElement = (csstreeNode, xastNode) => { const collectDescendantElements = (startNode, descendants, parents) => {
const result = []; for (const childNode of startNode.children) {
if (xastNode.type === 'root') { if (childNode.type === 'element') {
for (const xastChild of xastNode.children) { parents.set(childNode, startNode);
result.push(...descendantElement(csstreeNode, xastChild)); descendants.push(childNode);
collectDescendantElements(childNode, descendants, parents);
} }
} }
if (xastNode.type === 'element') { };
if (elementMatches(csstreeNode, xastNode)) {
result.push(xastNode); /**
} * @type {(
for (const xastChild of xastNode.children) { * startNodes: Array<XastElement>,
result.push(...descendantElement(csstreeNode, xastChild)); * children: Array<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.push(child);
} }
} }
return result; }
};
/**
* @type {(
* startNodes: Array<XastElement>,
* siblings: Array<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.push(adjacentSibling);
}
}
}
}
};
/**
* @type {(
* startNodes: Array<XastElement>,
* siblings: Array<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.push(generalSibling);
}
}
}
}
};
/**
* @type {(csstreeNodes: Array<CsstreeNode>, xastNode:XastParent) => Array<XastElement>}
*/
const combination = (csstreeNodes, xastNode) => {
/**
* @type {Array<XastElement>}
*/
let candidateNodes = [];
/**
* @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') {
for (const node of lastMatchedNodes) {
candidateNodes = [];
collectDescendantElements(node, candidateNodes, candidateParents);
}
} else if (csstreeChild.type === 'Combinator') {
candidateNodes = [];
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;
}; };
/** /**
@@ -97,7 +217,7 @@ const any = (csstreeNode, xastNode) => {
return result; return result;
} }
if (csstreeNode.type === 'Selector') { if (csstreeNode.type === 'Selector') {
return descendantElement(csstreeNode.children[0], xastNode); return combination(csstreeNode.children, xastNode);
} }
throw Error(`Unknown type ${csstreeNode.type}`); throw Error(`Unknown type ${csstreeNode.type}`);
}; };
@@ -111,8 +231,6 @@ const select = (selector, node) => {
); );
const match = any(parsedSelector, node); const match = any(parsedSelector, node);
return match.length === 0 ? null : match[0]; return match.length === 0 ? null : match[0];
// const match = any(parse(selector), node, { one: true, any });
// return match.length === 0 ? null : match[0];
}; };
/** /**
@@ -124,12 +242,8 @@ const selectAll = (selector, node) => {
); );
const match = any(parsedSelector, node); const match = any(parsedSelector, node);
return match; return match;
// const match = any(parse(selector), node, { any });
// return match;
}; };
///
/** /**
* @type {(node: XastParent, selector: string) => Array<XastElement>} * @type {(node: XastParent, selector: string) => Array<XastElement>}
*/ */
@@ -146,16 +260,12 @@ const querySelector = (node, selector) => {
}; };
exports.querySelector = querySelector; exports.querySelector = querySelector;
const cssSelectOptions = {
xmlMode: true,
adapter: xastAdaptor,
};
/** /**
* @type {(node: XastElement, selector: string) => boolean} * @type {(selector: string, node: XastElement, root: XastRoot) => boolean}
*/ */
const matches = (node, selector) => { const matches = (selector, node, root) => {
return is(node, selector, cssSelectOptions); const match = selectAll(selector, root);
return match.includes(node);
}; };
exports.matches = matches; exports.matches = matches;

View File

@@ -11,36 +11,7 @@ const x = (name, attributes, children = []) => {
}; };
const getAst = () => { const getAst = () => {
const ast = { const ast = root([x('g', null, [x('rect'), x('circle')]), x('ellipse')]);
type: 'root',
children: [
{
type: 'element',
name: 'g',
attributes: {},
children: [
{
type: 'element',
name: 'rect',
attributes: {},
children: [],
},
{
type: 'element',
name: 'circle',
attributes: {},
children: [],
},
],
},
{
type: 'element',
name: 'ellipse',
attributes: {},
children: [],
},
],
};
ast.children[0].parentNode = ast; ast.children[0].parentNode = ast;
ast.children[0].children[0].parentNode = ast.children[0]; ast.children[0].children[0].parentNode = ast.children[0];
ast.children[0].children[1].parentNode = ast.children[0]; ast.children[0].children[1].parentNode = ast.children[0];
@@ -152,3 +123,43 @@ test('select elements by matching attribute', () => {
x('rect', { attr: 'value' }), x('rect', { attr: 'value' }),
]); ]);
}); });
test('select sibling and child elements', () => {
const ast = root([
x('g', null, [
x('rect', { class: 'inside-g' }),
x('another-group', null, [x('rect', { class: 'deep-inside-g' })]),
]),
x('rect', { class: 'inside-root' }),
]);
expect(querySelectorAll(ast, 'g rect')).toEqual([
x('rect', { class: 'inside-g' }),
x('rect', { class: 'deep-inside-g' }),
]);
});
test('select sibling and child elements', () => {
const ast = root([
x('g', null, [
x('rect', { class: 'inside-g' }),
x('circle', { class: 'inside-g' }),
x('ellipse', { class: 'inside-g' }),
x('another-group', null, [x('rect', { class: 'deep-inside-g' })]),
]),
x('rect', { class: 'inside-root' }),
x('circle', { class: 'inside-root' }),
x('ellipse', { class: 'inside-root' }),
]);
expect(querySelectorAll(ast, 'rect + circle')).toEqual([
x('circle', { class: 'inside-g' }),
x('circle', { class: 'inside-root' }),
]);
expect(querySelectorAll(ast, 'rect + ellipse')).toEqual([]);
expect(querySelectorAll(ast, 'rect ~ ellipse')).toEqual([
x('ellipse', { class: 'inside-g' }),
x('ellipse', { class: 'inside-root' }),
]);
expect(querySelectorAll(ast, 'g > rect')).toEqual([
x('rect', { class: 'inside-g' }),
]);
});