1 | /**
|
2 | * @typedef {import('./types.js').Rule} Rule
|
3 | * @typedef {import('./types.js').RuleAttr} RuleAttr
|
4 | * @typedef {import('./types.js').Node} Node
|
5 | */
|
6 |
|
7 | import {zwitch} from 'zwitch'
|
8 |
|
9 | const handle = zwitch('operator', {
|
10 | // @ts-expect-error: hush.
|
11 | unknown: unknownOperator,
|
12 | // @ts-expect-error: hush.
|
13 | invalid: exists,
|
14 | handlers: {
|
15 | // @ts-expect-error: hush.
|
16 | '=': exact,
|
17 | // @ts-expect-error: hush.
|
18 | '^=': begins,
|
19 | // @ts-expect-error: hush.
|
20 | '$=': ends,
|
21 | // @ts-expect-error: hush.
|
22 | '*=': containsString,
|
23 | // @ts-expect-error: hush.
|
24 | '~=': containsArray
|
25 | }
|
26 | })
|
27 |
|
28 | /**
|
29 | * @param {Rule} query
|
30 | * @param {Node} node
|
31 | */
|
32 | export function attribute(query, node) {
|
33 | let index = -1
|
34 |
|
35 | while (++index < query.attrs.length) {
|
36 | if (!handle(query.attrs[index], node)) return false
|
37 | }
|
38 |
|
39 | return true
|
40 | }
|
41 |
|
42 | /**
|
43 | * `[attr]`
|
44 | *
|
45 | * @param {RuleAttr} query
|
46 | * @param {Node} node
|
47 | */
|
48 | function exists(query, node) {
|
49 | // @ts-expect-error: Looks like a record.
|
50 | return node[query.name] !== null && node[query.name] !== undefined
|
51 | }
|
52 |
|
53 | /**
|
54 | * `[attr=value]`
|
55 | *
|
56 | * @param {RuleAttr} query
|
57 | * @param {Node} node
|
58 | */
|
59 | function exact(query, node) {
|
60 | // @ts-expect-error: Looks like a record.
|
61 | return exists(query, node) && String(node[query.name]) === query.value
|
62 | }
|
63 |
|
64 | /**
|
65 | * `[attr~=value]`
|
66 | *
|
67 | * @param {RuleAttr} query
|
68 | * @param {Node} node
|
69 | */
|
70 | function containsArray(query, node) {
|
71 | /** @type {unknown} */
|
72 | // @ts-expect-error: Looks like a record.
|
73 | const value = node[query.name]
|
74 |
|
75 | if (value === null || value === undefined) return false
|
76 |
|
77 | // If this is an array, and the query is contained in it, return true.
|
78 | // Coverage comment in place because TS turns `Array.isArray(unknown)`
|
79 | // into `Array.<any>` instead of `Array.<unknown>`.
|
80 | // type-coverage:ignore-next-line
|
81 | if (Array.isArray(value) && value.includes(query.value)) {
|
82 | return true
|
83 | }
|
84 |
|
85 | // For all other values, return whether this is an exact match.
|
86 | return String(value) === query.value
|
87 | }
|
88 |
|
89 | /**
|
90 | * `[attr^=value]`
|
91 | *
|
92 | * @param {RuleAttr} query
|
93 | * @param {Node} node
|
94 | */
|
95 | function begins(query, node) {
|
96 | /** @type {unknown} */
|
97 | // @ts-expect-error: Looks like a record.
|
98 | const value = node[query.name]
|
99 |
|
100 | return (
|
101 | query.value &&
|
102 | typeof value === 'string' &&
|
103 | value.slice(0, query.value.length) === query.value
|
104 | )
|
105 | }
|
106 |
|
107 | /**
|
108 | * `[attr$=value]`
|
109 | *
|
110 | * @param {RuleAttr} query
|
111 | * @param {Node} node
|
112 | */
|
113 | function ends(query, node) {
|
114 | /** @type {unknown} */
|
115 | // @ts-expect-error: Looks like a record.
|
116 | const value = node[query.name]
|
117 |
|
118 | return (
|
119 | query.value &&
|
120 | typeof value === 'string' &&
|
121 | value.slice(-query.value.length) === query.value
|
122 | )
|
123 | }
|
124 |
|
125 | /**
|
126 | * `[attr*=value]`
|
127 | *
|
128 | * @param {RuleAttr} query
|
129 | * @param {Node} node
|
130 | */
|
131 | function containsString(query, node) {
|
132 | /** @type {unknown} */
|
133 | // @ts-expect-error: Looks like a record.
|
134 | const value = node[query.name]
|
135 | return query.value && typeof value === 'string' && value.includes(query.value)
|
136 | }
|
137 |
|
138 | // Shouldn’t be invoked, Parser throws an error instead.
|
139 | /* c8 ignore next 6 */
|
140 | /**
|
141 | * @param {{[x: string]: unknown, type: string}} query
|
142 | */
|
143 | function unknownOperator(query) {
|
144 | throw new Error('Unknown operator `' + query.operator + '`')
|
145 | }
|