Skip to content

Commit

Permalink
Match selectors without using css-select where possible. (#19)
Browse files Browse the repository at this point in the history
* Do simple matches without external module.
* Updated test case.
  • Loading branch information
johnkenny54 authored Sep 30, 2024
1 parent 0fe9ae3 commit e141521
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 23 deletions.
45 changes: 45 additions & 0 deletions lib/css.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ export class CSSRule {
#specificity;
#declarations;
#isInMediaQuery;
/** @type {(function(XastElement):boolean)|undefined} */
#matcher;

/**
* @param {CSSSelector} selector
Expand All @@ -111,6 +113,7 @@ export class CSSRule {
this.#specificity = specificity;
this.#declarations = declarations;
this.#isInMediaQuery = isInMediaQuery;
this.#matcher = this.#initMatcher();
}

/**
Expand Down Expand Up @@ -197,10 +200,44 @@ export class CSSRule {
return this.#selector.hasPseudos();
}

/**
* @returns {(function(XastElement):boolean)|undefined}
*/
#initMatcher() {
const sequences = this.#selector.getSequences();
if (sequences.length > 1) {
return;
}
const simpleSelectors = sequences[0].getSelectors();
if (simpleSelectors.length > 1) {
return;
}
switch (simpleSelectors[0].type) {
case 'ClassSelector':
return (element) =>
element.attributes.class === simpleSelectors[0].name;
case 'IdSelector':
return (element) => element.attributes.id === simpleSelectors[0].name;
case 'TypeSelector':
return (element) => element.name === simpleSelectors[0].name;
}
}

isInMediaQuery() {
return this.#isInMediaQuery;
}

/**
* @param {XastElement} element
* @return {boolean|null}
*/
_matches(element) {
if (this.#matcher !== undefined) {
return this.#matcher(element);
}
return null;
}

/**
* @param {XastElement} element
* @return {boolean}
Expand Down Expand Up @@ -281,6 +318,10 @@ export class CSSSelector {
return features;
}

getSequences() {
return this.#selectorSequences;
}

getString() {
return this.#str;
}
Expand Down Expand Up @@ -374,6 +415,10 @@ export class CSSSelectorSequence {
return ids;
}

getSelectors() {
return this.#simpleSelectors;
}

/**
* @param {boolean} includePseudos
*/
Expand Down
5 changes: 5 additions & 0 deletions lib/style-css-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ class CSSRuleConcrete extends CSSRule {
*/
matches(element) {
if (!this.#compiledSelector) {
// Try to do a simple match with the rule.
const result = super._matches(element);
if (result !== null) {
return result;
}
this.#compiledSelector = CSSselect.compile(
this.getSelectorStringWithoutPseudos(),
SELECT_OPTIONS,
Expand Down
75 changes: 75 additions & 0 deletions test/lib/css.rule.match.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { CSSRule, CSSSelector, CSSSelectorSequence } from '../../lib/css.js';
import { createElement } from './testutils.js';

/**
* @typedef {import('../../lib/css.js').SimpleSelector} SimpleSelector
*/

describe('test parsing and stringifying of selectors', function () {
/** @type {{
* selectorInfo:SimpleSelector[],
* elData:{elName:string,atts:[string,string][]},
* expected:boolean|null
* }[]} */
const tests = [
{
selectorInfo: [{ type: 'ClassSelector', name: 'class1' }],
elData: { elName: 'path', atts: [] },
expected: false,
},
{
selectorInfo: [{ type: 'ClassSelector', name: 'class1' }],
elData: { elName: 'path', atts: [['class', 'class2']] },
expected: false,
},
{
selectorInfo: [{ type: 'ClassSelector', name: 'class1' }],
elData: { elName: 'path', atts: [['class', 'class1']] },
expected: true,
},
{
selectorInfo: [
{ type: 'ClassSelector', name: 'class1' },
{ type: 'IdSelector', name: 'id1' },
],
elData: { elName: 'path', atts: [['class', 'class1']] },
expected: null,
},
{
selectorInfo: [{ type: 'IdSelector', name: 'id1' }],
elData: { elName: 'path', atts: [] },
expected: false,
},
{
selectorInfo: [{ type: 'IdSelector', name: 'id1' }],
elData: { elName: 'path', atts: [['id', 'id1']] },
expected: true,
},
{
selectorInfo: [{ type: 'IdSelector', name: 'id1' }],
elData: { elName: 'path', atts: [['id', 'class1']] },
expected: false,
},
{
selectorInfo: [{ type: 'TypeSelector', name: 'path' }],
elData: { elName: 'path', atts: [] },
expected: true,
},
{
selectorInfo: [{ type: 'TypeSelector', name: 'path' }],
elData: { elName: 'circle', atts: [] },
expected: false,
},
];
const declarations = new Map();

for (const test of tests) {
const sequences = [new CSSSelectorSequence(undefined, test.selectorInfo)];
const selector = new CSSSelector(sequences);
it(`test ${selector.getString()}`, function () {
const rule = new CSSRule(selector, [0, 0, 0], declarations, false);
const element = createElement(test.elData);
expect(rule._matches(element)).toBe(test.expected);
});
}
});
4 changes: 4 additions & 0 deletions test/lib/styledata.updatereferencedids.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,19 @@ test('make sure compiled query cache is cleared correctly when style ids are cha

// Should match to begin with.
const rules = styleData.getMatchingRules(element);
const rule = rules[0];
expect(rule._matches(element)).toBe(true);
expect(rules.length).toBe(1);

// Replace the id in the rules; should no longer match.
const m = new Map();
m.set('id1', 'a');
styleData.updateReferencedIds(styleData.getReferencedIds(), m);
expect(rule._matches(element)).toBe(false);
expect(styleData.getMatchingRules(element).length).toBe(0);

// Change the id of the element; it should match again.
element.attributes.id = 'a';
expect(rule._matches(element)).toBe(true);
expect(styleData.getMatchingRules(element).length).toBe(1);
});
22 changes: 22 additions & 0 deletions test/lib/testutils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,28 @@ import { parseSvg } from '../../lib/parser.js';
import { getDocData } from '../../lib/docdata.js';
import { visit } from '../../lib/xast.js';

/**
* @param {{elName:string,atts:[string,string][]}} elData
* @returns {import('../../lib/types.js').XastElement}
*/
export function createElement(elData) {
/** @type {import('../../lib/types.js').XastRoot} */
const root = { type: 'root', children: [] };
/** @type {import('../../lib/types.js').XastElement} */
const element = {
type: 'element',
name: elData.elName,
parentNode: root,
attributes: {},
children: [],
};
for (const att of elData.atts) {
element.attributes[att[0]] = att[1];
}
root.children.push(element);
return element;
}

/**
* @param {string} fileName
*/
Expand Down
24 changes: 1 addition & 23 deletions test/lib/tools.getReferencedIds.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getReferencedIds } from '../../lib/svgo/tools.js';
import { createElement } from './testutils.js';

/**
* @typedef {{
Expand All @@ -8,29 +9,6 @@ import { getReferencedIds } from '../../lib/svgo/tools.js';
* }} TestInfo
*/

/**
*
* @param {TestInfo} test
* @returns {import('../../lib/types.js').XastElement}
*/
function createElement(test) {
/** @type {import('../../lib/types.js').XastRoot} */
const root = { type: 'root', children: [] };
/** @type {import('../../lib/types.js').XastElement} */
const element = {
type: 'element',
name: test.elName,
parentNode: root,
attributes: {},
children: [],
};
for (const att of test.atts) {
element.attributes[att[0]] = att[1];
}
root.children.push(element);
return element;
}

describe('getReferencedIds()', () => {
/**
* @type {TestInfo[]}
Expand Down

0 comments on commit e141521

Please sign in to comment.