From 4da3ede37b3fe3111cb1b240822a0fbdf8c255e6 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Mon, 23 Aug 2021 13:24:48 +0300 Subject: [PATCH] Support matches and combinations --- lib/style.js | 19 +++--- lib/types.ts | 5 ++ lib/xast.js | 170 ++++++++++++++++++++++++++++++++++++++--------- lib/xast.test.js | 71 +++++++++++--------- 4 files changed, 196 insertions(+), 69 deletions(-) diff --git a/lib/style.js b/lib/style.js index 02a86839..60db2aa4 100644 --- a/lib/style.js +++ b/lib/style.js @@ -3,6 +3,7 @@ /** * @typedef {import('css-tree').Rule} CsstreeRule * @typedef {import('./types').Specificity} Specificity + * @typedef {import('./types').Stylesheet} Stylesheet * @typedef {import('./types').StylesheetRule} StylesheetRule * @typedef {import('./types').StylesheetDeclaration} StylesheetDeclaration * @typedef {import('./types').ComputedStyles} ComputedStyles @@ -128,7 +129,7 @@ const parseStyleDeclarations = (css) => { }; /** - * @type {(stylesheet: Array, node: XastElement) => ComputedStyles} + * @type {(stylesheet: Stylesheet, node: XastElement) => ComputedStyles} */ const computeOwnStyle = (stylesheet, node) => { /** @@ -146,8 +147,8 @@ const computeOwnStyle = (stylesheet, node) => { } // collect matching rules - for (const { selectors, declarations, dynamic } of stylesheet) { - if (matches(node, selectors)) { + for (const { selectors, declarations, dynamic } of stylesheet.rules) { + if (matches(selectors, node, stylesheet.root)) { for (const { name, value, important } of declarations) { const computed = computedStyle[name]; if (computed && computed.type === 'dynamic') { @@ -211,13 +212,13 @@ const compareSpecificity = (a, b) => { }; /** - * @type {(root: XastRoot) => Array} + * @type {(root: XastRoot) => Stylesheet} */ const collectStylesheet = (root) => { /** * @type {Array} */ - const stylesheet = []; + const rules = []; // find and parse all styles visit(root, { element: { @@ -233,7 +234,7 @@ const collectStylesheet = (root) => { const children = node.children; for (const child of children) { 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 - stable.inplace(stylesheet, (a, b) => + stable.inplace(rules, (a, b) => compareSpecificity(a.specificity, b.specificity) ); - return stylesheet; + return { root, rules }; }; exports.collectStylesheet = collectStylesheet; /** - * @type {(stylesheet: Array, node: XastElement) => ComputedStyles} + * @type {(stylesheet: Stylesheet, node: XastElement) => ComputedStyles} */ const computeStyle = (stylesheet, node) => { // collect inherited styles diff --git a/lib/types.ts b/lib/types.ts index 65116fa7..78589a36 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -88,6 +88,11 @@ export type StylesheetRule = { declarations: Array; }; +export type Stylesheet = { + root: XastRoot; + rules: Array; +}; + type StaticStyle = { type: 'static'; inherited: boolean; diff --git a/lib/xast.js b/lib/xast.js index f4f5c0eb..d8397040 100644 --- a/lib/xast.js +++ b/lib/xast.js @@ -5,13 +5,12 @@ * @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'); -const { is } = require('css-select'); -const xastAdaptor = require('./svgo/css-select-adapter.js'); /** * @type {(string: string) => string} @@ -65,24 +64,145 @@ const elementMatches = (csstreeNode, xastElement) => { }; /** - * @type {(csstreeNode: CsstreeNode, xastNode: XastNode) => Array} + * @type {( + * startNode: XastParent, + * descendants: Array, + * parents: WeakMap + * ) => void} */ -const descendantElement = (csstreeNode, xastNode) => { - const result = []; - if (xastNode.type === 'root') { - for (const xastChild of xastNode.children) { - result.push(...descendantElement(csstreeNode, xastChild)); +const collectDescendantElements = (startNode, descendants, parents) => { + for (const childNode of startNode.children) { + if (childNode.type === 'element') { + parents.set(childNode, startNode); + descendants.push(childNode); + collectDescendantElements(childNode, descendants, parents); } } - if (xastNode.type === 'element') { - if (elementMatches(csstreeNode, xastNode)) { - result.push(xastNode); - } - for (const xastChild of xastNode.children) { - result.push(...descendantElement(csstreeNode, xastChild)); +}; + +/** + * @type {( + * startNodes: Array, + * children: Array, + * parents: WeakMap + * ) => 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, + * siblings: Array, + * parents: WeakMap + * ) => 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, + * siblings: Array, + * parents: WeakMap + * ) => 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, xastNode:XastParent) => Array} + */ +const combination = (csstreeNodes, xastNode) => { + /** + * @type {Array} + */ + let candidateNodes = []; + /** + * @type {WeakMap} + */ + let candidateParents = new WeakMap(); + collectDescendantElements(xastNode, candidateNodes, candidateParents); + /** + * @type {Array} + */ + 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; } if (csstreeNode.type === 'Selector') { - return descendantElement(csstreeNode.children[0], xastNode); + return combination(csstreeNode.children, xastNode); } throw Error(`Unknown type ${csstreeNode.type}`); }; @@ -111,8 +231,6 @@ const select = (selector, node) => { ); const match = any(parsedSelector, node); 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); return match; - // const match = any(parse(selector), node, { any }); - // return match; }; -/// - /** * @type {(node: XastParent, selector: string) => Array} */ @@ -146,16 +260,12 @@ const querySelector = (node, selector) => { }; 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) => { - return is(node, selector, cssSelectOptions); +const matches = (selector, node, root) => { + const match = selectAll(selector, root); + return match.includes(node); }; exports.matches = matches; diff --git a/lib/xast.test.js b/lib/xast.test.js index 42a7ba8e..ce16ee68 100644 --- a/lib/xast.test.js +++ b/lib/xast.test.js @@ -11,36 +11,7 @@ const x = (name, attributes, children = []) => { }; const getAst = () => { - const ast = { - 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: [], - }, - ], - }; + const ast = root([x('g', null, [x('rect'), x('circle')]), x('ellipse')]); ast.children[0].parentNode = ast; ast.children[0].children[0].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' }), ]); }); + +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' }), + ]); +});