Skip to content

Commit b600536

Browse files
committed
Refactor caching of nth checks
1 parent 191aa36 commit b600536

File tree

3 files changed

+45
-81
lines changed

3 files changed

+45
-81
lines changed

lib/parse.js

Lines changed: 8 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,34 @@
11
/**
2-
* @typedef {import('./types.js').Selector} Selector
32
* @typedef {import('./types.js').Selectors} Selectors
4-
* @typedef {import('./types.js').RuleSet} RuleSet
5-
* @typedef {import('./types.js').Rule} Rule
63
*/
74

85
import {CssSelectorParser} from 'css-selector-parser'
9-
import fauxEsmNthCheck from 'nth-check'
10-
import {zwitch} from 'zwitch'
11-
12-
/** @type {import('nth-check').default} */
13-
// @ts-expect-error
14-
const nthCheck = fauxEsmNthCheck.default || fauxEsmNthCheck
15-
16-
const nth = new Set([
17-
'nth-child',
18-
'nth-last-child',
19-
'nth-of-type',
20-
'nth-last-of-type'
21-
])
226

237
const parser = new CssSelectorParser()
248

25-
/** @type {(query: Selectors | RuleSet | Rule | undefined) => void} */
26-
const compile = zwitch('type', {handlers: {selectors, ruleSet, rule}})
27-
289
parser.registerAttrEqualityMods('~', '|', '^', '$', '*')
2910
parser.registerSelectorPseudos('any', 'matches', 'not', 'has')
3011
parser.registerNestingOperators('>', '+', '~')
3112

3213
/**
3314
* @param {string} selector
34-
* @returns {Selector}
15+
* @returns {Selectors}
3516
*/
3617
export function parse(selector) {
3718
if (typeof selector !== 'string') {
3819
throw new TypeError('Expected `string` as selector, not `' + selector + '`')
3920
}
4021

41-
const parsed = parser.parse(selector)
42-
compile(parsed)
43-
return parsed
44-
}
45-
46-
/**
47-
* @param {Selectors} query
48-
* @returns {void}
49-
*/
50-
function selectors(query) {
51-
let index = -1
22+
const query = parser.parse(selector)
5223

53-
while (++index < query.selectors.length) {
54-
compile(query.selectors[index])
24+
// Empty selectors object doesn’t match anything.
25+
if (!query) {
26+
return {type: 'selectors', selectors: []}
5527
}
56-
}
57-
58-
/**
59-
* @param {RuleSet} query
60-
* @returns {void}
61-
*/
62-
function ruleSet(query) {
63-
rule(query.rule)
64-
}
65-
66-
/**
67-
* @param {Rule} query
68-
* @returns {void}
69-
*/
70-
function rule(query) {
71-
const pseudos = query.pseudos || []
72-
let index = -1
73-
74-
while (++index < pseudos.length) {
75-
const pseudo = pseudos[index]
7628

77-
if (nth.has(pseudo.name)) {
78-
// @ts-expect-error Patch a non-primitive type.
79-
pseudo.value = nthCheck(pseudo.value)
80-
// @ts-expect-error Patch a non-primitive type.
81-
pseudo.valueType = 'function'
82-
}
29+
if (query.type === 'selectors') {
30+
return query
8331
}
8432

85-
compile(query.rule)
33+
return {type: 'selectors', selectors: [query]}
8634
}

lib/pseudo.js

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/**
22
* @typedef {import('./types.js').Rule} Rule
33
* @typedef {import('./types.js').RulePseudo} RulePseudo
4-
* @typedef {import('./types.js').RulePseudoNth} RulePseudoNth
54
* @typedef {import('./types.js').RulePseudoSelector} RulePseudoSelector
65
* @typedef {import('./types.js').Parent} Parent
76
* @typedef {import('./types.js').Selector} Selector
@@ -16,9 +15,14 @@ import {parse as commas} from 'comma-separated-tokens'
1615
import {hasProperty} from 'hast-util-has-property'
1716
import {isElement} from 'hast-util-is-element'
1817
import {whitespace} from 'hast-util-whitespace'
18+
import fauxEsmNthCheck from 'nth-check'
1919
import {zwitch} from 'zwitch'
2020
import {any} from './any.js'
2121

22+
/** @type {import('nth-check').default} */
23+
// @ts-expect-error
24+
const nthCheck = fauxEsmNthCheck.default || fauxEsmNthCheck
25+
2226
/** @type {(rule: Rule | RulePseudo, element: Element, index: number | undefined, parent: Parent | undefined, state: SelectState) => boolean} */
2327
const handle = zwitch('name', {
2428
unknown: unknownPseudo,
@@ -375,64 +379,66 @@ function onlyChild(query, _1, _2, _3, state) {
375379
}
376380

377381
/**
378-
* @param {RulePseudoNth} query
382+
* @param {RulePseudo} query
379383
* @param {Element} _1
380384
* @param {number | undefined} _2
381385
* @param {Parent | undefined} _3
382386
* @param {SelectState} state
383387
* @returns {boolean}
384388
*/
385389
function nthChild(query, _1, _2, _3, state) {
390+
const fn = getCachedNthCheck(query)
386391
assertDeep(state, query)
387-
return (
388-
typeof state.elementIndex === 'number' && query.value(state.elementIndex)
389-
)
392+
return typeof state.elementIndex === 'number' && fn(state.elementIndex)
390393
}
391394

392395
/**
393-
* @param {RulePseudoNth} query
396+
* @param {RulePseudo} query
394397
* @param {Element} _1
395398
* @param {number | undefined} _2
396399
* @param {Parent | undefined} _3
397400
* @param {SelectState} state
398401
* @returns {boolean}
399402
*/
400403
function nthLastChild(query, _1, _2, _3, state) {
404+
const fn = getCachedNthCheck(query)
401405
assertDeep(state, query)
402406
return Boolean(
403407
typeof state.elementCount === 'number' &&
404408
typeof state.elementIndex === 'number' &&
405-
query.value(state.elementCount - state.elementIndex - 1)
409+
fn(state.elementCount - state.elementIndex - 1)
406410
)
407411
}
408412

409413
/**
410-
* @param {RulePseudoNth} query
414+
* @param {RulePseudo} query
411415
* @param {Element} _1
412416
* @param {number | undefined} _2
413417
* @param {Parent | undefined} _3
414418
* @param {SelectState} state
415419
* @returns {boolean}
416420
*/
417421
function nthOfType(query, _1, _2, _3, state) {
422+
const fn = getCachedNthCheck(query)
418423
assertDeep(state, query)
419-
return typeof state.typeIndex === 'number' && query.value(state.typeIndex)
424+
return typeof state.typeIndex === 'number' && fn(state.typeIndex)
420425
}
421426

422427
/**
423-
* @param {RulePseudoNth} query
428+
* @param {RulePseudo} query
424429
* @param {Element} _1
425430
* @param {number | undefined} _2
426431
* @param {Parent | undefined} _3
427432
* @param {SelectState} state
428433
* @returns {boolean}
429434
*/
430435
function nthLastOfType(query, _1, _2, _3, state) {
436+
const fn = getCachedNthCheck(query)
431437
assertDeep(state, query)
432438
return (
433439
typeof state.typeCount === 'number' &&
434440
typeof state.typeIndex === 'number' &&
435-
query.value(state.typeCount - 1 - state.typeIndex)
441+
fn(state.typeCount - 1 - state.typeIndex)
436442
)
437443
}
438444

@@ -517,7 +523,7 @@ function unknownPseudo(query) {
517523

518524
/**
519525
* @param {SelectState} state
520-
* @param {RulePseudo | RulePseudoNth} query
526+
* @param {RulePseudo} query
521527
*/
522528
function assertDeep(state, query) {
523529
if (state.shallow) {
@@ -581,3 +587,22 @@ function appendScope(value) {
581587

582588
return selector
583589
}
590+
591+
/**
592+
* @param {RulePseudo} query
593+
* @returns {(value: number) => boolean}
594+
*/
595+
function getCachedNthCheck(query) {
596+
/** @type {(value: number) => boolean} */
597+
// @ts-expect-error: cache.
598+
let fn = query._cachedFn
599+
600+
if (!fn) {
601+
// @ts-expect-error: always string.
602+
fn = nthCheck(query.value)
603+
// @ts-expect-error: cache.
604+
query._cachedFn = fn
605+
}
606+
607+
return fn
608+
}

lib/types.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,6 @@
5050
* @property {Selector} value
5151
* Selector.
5252
*
53-
* @typedef RulePseudoNth
54-
* Overwrite to compile nth-checks once.
55-
* @property {string} name
56-
* Name of pseudo, such as `'nth-child'`.
57-
* @property {'function'} valueType
58-
* Set to `'function'`, because `value` is a compiled check.
59-
* @property {(index: number) => boolean} value
60-
* Compiled function from `nth-check`.
61-
*
6253
* @typedef {'html' | 'svg'} Space
6354
* Name of namespace.
6455
* @typedef {'auto' | 'ltr' | 'rtl'} Direction

0 commit comments

Comments
 (0)