Skip to content

Commit

Permalink
Update to match CSS selectors 4
Browse files Browse the repository at this point in the history
This commit updates `css-selector-parser`, which is completely different.
You’ll get some different errors if you do weird things.

Otherwise, this changes:

* change to throw on empty selector
* change to on invalid attribute selectors such as `[a=b,c]`, use `[a="b,c"]` instead
* remove `any`, `matches`: use `:is()` instead
  • Loading branch information
wooorm committed Aug 7, 2023
1 parent 11e66c0 commit daf8770
Show file tree
Hide file tree
Showing 16 changed files with 388 additions and 376 deletions.
90 changes: 54 additions & 36 deletions lib/attribute.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
/**
* @typedef {import('./types.js').Rule} Rule
* @typedef {import('./types.js').RuleAttr} RuleAttr
* @typedef {import('./types.js').Element} Element
* @typedef {import('./types.js').Schema} Schema
* @typedef {import('./types.js').Info} Info
* @typedef {import('./types.js').PropertyValue} PropertyValue
* @typedef {import('css-selector-parser').AstRule} AstRule
* @typedef {import('css-selector-parser').AstAttribute} AstAttribute
* @typedef {import('hast').Element} Element
* @typedef {import('hast').Properties} Properties
* @typedef {import('property-information').Schema} Schema
* @typedef {import('property-information').Info} Info
*/

import {stringify as commas} from 'comma-separated-tokens'
import {ok as assert} from 'devlop'
import {hasProperty} from 'hast-util-has-property'
import {find} from 'property-information'
import {stringify as spaces} from 'space-separated-tokens'
import {zwitch} from 'zwitch'

/** @type {(query: RuleAttr, element: Element, info: Info) => boolean} */
/** @type {(query: AstAttribute, element: Element, info: Info) => boolean} */
const handle = zwitch('operator', {
unknown: unknownOperator,
// @ts-expect-error: hush.
Expand All @@ -29,18 +30,21 @@ const handle = zwitch('operator', {
})

/**
* @param {Rule} query
* @param {AstRule} query
* @param {Element} element
* @param {Schema} schema
* @returns {boolean}
*/
export function attribute(query, element, schema) {
const attrs = query.attrs
let index = -1

while (++index < attrs.length) {
if (!handle(attrs[index], element, find(schema, attrs[index].name))) {
return false
if (query.attributes) {
while (++index < query.attributes.length) {
const attribute = query.attributes[index]

if (!handle(attribute, element, find(schema, attribute.name))) {
return false
}
}
}

Expand All @@ -52,7 +56,7 @@ export function attribute(query, element, schema) {
*
* `[attr]`
*
* @param {RuleAttr} _
* @param {AstAttribute} _
* @param {Element} element
* @param {Info} info
* @returns {boolean}
Expand All @@ -66,16 +70,20 @@ function exists(_, element, info) {
*
* `[attr=value]`
*
* @param {RuleAttr} query
* @param {AstAttribute} query
* @param {Element} element
* @param {Info} info
* @returns {boolean}
*/
function exact(query, element, info) {
assert(query.value, 'expected `value`')
assert(query.value.type === 'String', 'expected plain string')

return Boolean(
hasProperty(element, info.property) &&
element.properties &&
normalizeValue(element.properties[info.property], info) === query.value
normalizeValue(element.properties[info.property], info) ===
query.value.value
)
}

Expand All @@ -85,12 +93,15 @@ function exact(query, element, info) {
*
* `[attr~=value]`
*
* @param {RuleAttr} query
* @param {AstAttribute} query
* @param {Element} element
* @param {Info} info
* @returns {boolean}
*/
function spaceSeparatedList(query, element, info) {
assert(query.value, 'expected `value`')
assert(query.value.type === 'String', 'expected plain string')

const value = element.properties && element.properties[info.property]

return (
Expand All @@ -99,12 +110,11 @@ function spaceSeparatedList(query, element, info) {
(!info.commaSeparated &&
value &&
typeof value === 'object' &&
query.value &&
value.includes(query.value)) ||
value.includes(query.value.value)) ||
// For all other values (including comma-separated lists), return whether this
// is an exact match.
(hasProperty(element, info.property) &&
normalizeValue(value, info) === query.value)
normalizeValue(value, info) === query.value.value)
)
}

Expand All @@ -114,23 +124,25 @@ function spaceSeparatedList(query, element, info) {
*
* `[attr|=value]`
*
* @param {RuleAttr} query
* @param {AstAttribute} query
* @param {Element} element
* @param {Info} info
* @returns {boolean}
*/
function exactOrPrefix(query, element, info) {
assert(query.value, 'expected `value`')
assert(query.value.type === 'String', 'expected plain string')

const value = normalizeValue(
element.properties && element.properties[info.property],
info
)

return Boolean(
hasProperty(element, info.property) &&
query.value &&
(value === query.value ||
(value.slice(0, query.value.length) === query.value &&
value.charAt(query.value.length) === '-'))
(value === query.value.value ||
(value.slice(0, query.value.value.length) === query.value.value &&
value.charAt(query.value.value.length) === '-'))
)
}

Expand All @@ -139,20 +151,22 @@ function exactOrPrefix(query, element, info) {
*
* `[attr^=value]`
*
* @param {RuleAttr} query
* @param {AstAttribute} query
* @param {Element} element
* @param {Info} info
* @returns {boolean}
*/
function begins(query, element, info) {
assert(query.value, 'expected `value`')
assert(query.value.type === 'String', 'expected plain string')

return Boolean(
hasProperty(element, info.property) &&
element.properties &&
query.value &&
normalizeValue(element.properties[info.property], info).slice(
0,
query.value.length
) === query.value
query.value.value.length
) === query.value.value
)
}

Expand All @@ -161,19 +175,21 @@ function begins(query, element, info) {
*
* `[attr$=value]`
*
* @param {RuleAttr} query
* @param {AstAttribute} query
* @param {Element} element
* @param {Info} info
* @returns {boolean}
*/
function ends(query, element, info) {
assert(query.value, 'expected `value`')
assert(query.value.type === 'String', 'expected plain string')

return Boolean(
hasProperty(element, info.property) &&
element.properties &&
query.value &&
normalizeValue(element.properties[info.property], info).slice(
-query.value.length
) === query.value
-query.value.value.length
) === query.value.value
)
}

Expand All @@ -182,18 +198,20 @@ function ends(query, element, info) {
*
* `[attr*=value]`
*
* @param {RuleAttr} query
* @param {AstAttribute} query
* @param {Element} element
* @param {Info} info
* @returns {boolean}
*/
function contains(query, element, info) {
assert(query.value, 'expected `value`')
assert(query.value.type === 'String', 'expected plain string')

return Boolean(
hasProperty(element, info.property) &&
element.properties &&
query.value &&
normalizeValue(element.properties[info.property], info).includes(
query.value
query.value.value
)
)
}
Expand All @@ -212,7 +230,7 @@ function unknownOperator(query) {
/**
* Stringify a hast value back to its HTML form.
*
* @param {PropertyValue} value
* @param {Properties[keyof Properties]} value
* @param {Info} info
* @returns {string}
*/
Expand Down
11 changes: 7 additions & 4 deletions lib/class-name.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
/**
* @typedef {import('./types.js').Rule} Rule
* @typedef {import('./types.js').Element} Element
* @typedef {import('css-selector-parser').AstRule} AstRule
* @typedef {import('hast').Element} Element
*/

// Make VS Code see references to the above types.
''

/**
* Check whether an element has all class names.
*
* @param {Rule} query
* @param {AstRule} query
* @param {Element} element
* @returns {boolean}
*/
export function className(query, element) {
/** @type {readonly string[]} */
/** @type {Readonly<Array<string>>} */
// @ts-expect-error Assume array.
const value = element.properties.className || []
let index = -1
Expand Down
10 changes: 5 additions & 5 deletions lib/enter-state.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/**
* @typedef {import('./types.js').SelectState} SelectState
* @typedef {import('./types.js').Node} Node
* @typedef {import('./types.js').ElementChild} ElementChild
* @typedef {import('hast').Nodes} Nodes
* @typedef {import('hast').ElementContent} ElementContent
* @typedef {import('./types.js').Direction} Direction
* @typedef {import('unist-util-visit').Visitor<ElementChild>} Visitor
* @typedef {import('unist-util-visit').Visitor<ElementContent>} Visitor
*/

import {direction} from 'direction'
Expand All @@ -20,7 +20,7 @@ import {visit, EXIT, SKIP} from 'unist-util-visit'
* Current state.
*
* Will be mutated: `exit` undos the changes.
* @param {Node} node
* @param {Nodes} node
* Node to enter.
* @returns {() => void}
* Call to exit.
Expand Down Expand Up @@ -139,7 +139,7 @@ function dirBidi(value) {
}

/**
* @param {ElementChild} node
* @param {ElementContent} node
* @returns {Direction | undefined}
*/
function dirProperty(node) {
Expand Down
10 changes: 7 additions & 3 deletions lib/id.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
/**
* @typedef {import('./types.js').Rule} Rule
* @typedef {import('css-selector-parser').AstRule} AstRule
* @typedef {import('./types.js').Element} Element
*/

import {ok as assert} from 'devlop'

/**
* Check whether an element has an ID.
*
* @param {Rule} query
* @param {AstRule} query
* @param {Element} element
* @returns {boolean}
*/
export function id(query, element) {
return Boolean(element.properties && element.properties.id === query.id)
assert(query.ids, 'expected `ids`')
const id = query.ids[query.ids.length - 1]
return Boolean(element.properties && element.properties.id === id)
}
16 changes: 8 additions & 8 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/**
* @typedef {import('./types.js').Element} Element
* @typedef {import('./types.js').Node} Node
* @typedef {import('hast').Element} Element
* @typedef {import('hast').Nodes} Nodes
* @typedef {import('./types.js').Space} Space
* @typedef {import('./types.js').SelectState} SelectState
*/

import {html, svg} from 'property-information'
import {queryToSelectors, walk} from './walk.js'
import {parse} from './parse.js'
import {walk} from './walk.js'

/**
* Check that the given `node` matches `selector`.
Expand All @@ -19,7 +19,7 @@ import {parse} from './parse.js'
*
* @param {string} selector
* CSS selector, such as (`h1`, `a, b`).
* @param {Node | null | undefined} [node]
* @param {Nodes | null | undefined} [node]
* Node that might match `selector`, should be an element.
* @param {Space | null | undefined} [space='html']
* Name of namespace (`'svg'` or `'html'`).
Expand All @@ -40,7 +40,7 @@ export function matches(selector, node, space) {
*
* @param {string} selector
* CSS selector, such as (`h1`, `a, b`).
* @param {Node | null | undefined} [tree]
* @param {Nodes | null | undefined} [tree]
* Tree to search.
* @param {Space | null | undefined} [space='html']
* Name of namespace (`'svg'` or `'html'`).
Expand All @@ -63,7 +63,7 @@ export function select(selector, tree, space) {
*
* @param {string} selector
* CSS selector, such as (`h1`, `a, b`).
* @param {Node | null | undefined} [tree]
* @param {Nodes | null | undefined} [tree]
* Tree to search.
* @param {Space | null | undefined} [space='html']
* Name of namespace (`'svg'` or `'html'`).
Expand All @@ -80,7 +80,7 @@ export function selectAll(selector, tree, space) {
/**
* @param {string} selector
* Tree to search.
* @param {Node | null | undefined} [tree]
* @param {Nodes | null | undefined} [tree]
* Tree to search.
* @param {Space | null | undefined} [space='html']
* Name of namespace (`'svg'` or `'html'`).
Expand All @@ -89,7 +89,7 @@ export function selectAll(selector, tree, space) {
function createState(selector, tree, space) {
return {
// State of the query.
rootQuery: queryToSelectors(parse(selector)),
rootQuery: parse(selector),
results: [],
// @ts-expect-error assume elements.
scopeElements: tree ? (tree.type === 'root' ? tree.children : [tree]) : [],
Expand Down
9 changes: 6 additions & 3 deletions lib/name.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
/**
* @typedef {import('./types.js').Rule} Rule
* @typedef {import('css-selector-parser').AstRule} AstRule
* @typedef {import('./types.js').Element} Element
*/

import {ok as assert} from 'devlop'

/**
* Check whether an element has a tag name.
*
* @param {Rule} query
* @param {AstRule} query
* @param {Element} element
* @returns {boolean}
*/
export function name(query, element) {
return query.tagName === '*' || query.tagName === element.tagName
assert(query.tag, 'expected `tag`')
return query.tag.type === 'WildcardTag' || query.tag.name === element.tagName
}
Loading

0 comments on commit daf8770

Please sign in to comment.