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:
19
lib/style.js
19
lib/style.js
@@ -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
|
||||||
|
@@ -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;
|
||||||
|
170
lib/xast.js
170
lib/xast.js
@@ -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;
|
||||||
|
|
||||||
|
@@ -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' }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
Reference in New Issue
Block a user