├── .prettierignore ├── .npmrc ├── .gitignore ├── test ├── index.js ├── all.js ├── select.js ├── select-all.js └── matches.js ├── .editorconfig ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── tsconfig.json ├── lib ├── parse.js ├── util.js ├── types.js ├── test.js ├── attribute.js ├── walk.js └── pseudo.js ├── license ├── package.json ├── index.js └── readme.md /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.md 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.d.ts 3 | *.log 4 | coverage/ 5 | node_modules/ 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import './all.js' 2 | import './matches.js' 3 | import './select.js' 4 | import './select-all.js' 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/workflows/bb.yml: -------------------------------------------------------------------------------- 1 | name: bb 2 | on: 3 | issues: 4 | types: [opened, reopened, edited, closed, labeled, unlabeled] 5 | pull_request_target: 6 | types: [opened, reopened, edited, closed, labeled, unlabeled] 7 | jobs: 8 | main: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: unifiedjs/beep-boop-beta@main 12 | with: 13 | repo-token: ${{secrets.GITHUB_TOKEN}} 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "exactOptionalPropertyTypes": true, 8 | "lib": ["es2022"], 9 | "module": "node16", 10 | "strict": true, 11 | "target": "es2022" 12 | }, 13 | "exclude": ["coverage/", "node_modules/"], 14 | "include": ["**/*.js"] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | - pull_request 4 | - push 5 | jobs: 6 | main: 7 | name: ${{matrix.node}} 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: ${{matrix.node}} 14 | - run: npm install 15 | - run: npm test 16 | - uses: codecov/codecov-action@v3 17 | strategy: 18 | matrix: 19 | node: 20 | - lts/gallium 21 | - node 22 | -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('css-selector-parser').AstSelector} AstSelector 3 | */ 4 | 5 | import {createParser} from 'css-selector-parser' 6 | 7 | const cssSelectorParse = createParser({syntax: 'selectors-4'}) 8 | 9 | /** 10 | * @param {string} selector 11 | * @returns {AstSelector} 12 | */ 13 | export function parse(selector) { 14 | if (typeof selector !== 'string') { 15 | throw new TypeError('Expected `string` as selector, not `' + selector + '`') 16 | } 17 | 18 | return cssSelectorParse(selector) 19 | } 20 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('unist').Node} Node 3 | * @typedef {import('unist').Parent} Parent 4 | */ 5 | 6 | import {unreachable} from 'devlop' 7 | 8 | /** 9 | * TypeScript helper to check if something is indexable (any object is 10 | * indexable in JavaScript). 11 | * 12 | * @param {unknown} value 13 | * Thing to check. 14 | * @returns {asserts value is Record} 15 | * Nothing. 16 | * @throws {Error} 17 | * When `value` is not an object. 18 | */ 19 | export function indexable(value) { 20 | // Always called when something is an object, this is just for TS. 21 | /* c8 ignore next 3 */ 22 | if (!value || typeof value !== 'object') { 23 | unreachable('Expected object') 24 | } 25 | } 26 | 27 | /** 28 | * @param {Node} node 29 | * @returns {node is Parent} 30 | */ 31 | export function parent(node) { 32 | indexable(node) 33 | return Array.isArray(node.children) 34 | } 35 | -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('css-selector-parser').AstSelector} AstSelector 3 | * @typedef {import('unist').Node} Node 4 | */ 5 | 6 | /** 7 | * @typedef SelectState 8 | * Current state. 9 | * @property {AstSelector} rootQuery 10 | * Original root selectors. 11 | * @property {Array} results 12 | * Matches. 13 | * @property {Array} scopeNodes 14 | * Nodes in scope. 15 | * @property {boolean} one 16 | * Whether we can stop looking after we found one node. 17 | * @property {boolean} shallow 18 | * Whether we only allow selectors without nesting. 19 | * @property {boolean} found 20 | * Whether we found at least one match. 21 | * @property {number | undefined} typeIndex 22 | * Track siblings: this current node has `n` nodes with its type before it. 23 | * @property {number | undefined} nodeIndex 24 | * Track siblings: this current node has `n` nodes before it. 25 | * @property {number | undefined} typeCount 26 | * Track siblings: there are `n` siblings with this node’s type. 27 | * @property {number | undefined} nodeCount 28 | * Track siblings: there are `n` siblings. 29 | */ 30 | 31 | export {} 32 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Eugene Sharygin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('css-selector-parser').AstRule} AstRule 3 | * @typedef {import('unist').Node} Node 4 | * @typedef {import('unist').Parent} Parent 5 | * @typedef {import('./types.js').SelectState} SelectState 6 | */ 7 | 8 | import {attribute} from './attribute.js' 9 | import {pseudo} from './pseudo.js' 10 | 11 | /** 12 | * @param {AstRule} query 13 | * @param {Node} node 14 | * @param {number | undefined} index 15 | * @param {Parent | undefined} parent 16 | * @param {SelectState} state 17 | * @returns {boolean} 18 | */ 19 | export function test(query, node, index, parent, state) { 20 | for (const item of query.items) { 21 | // eslint-disable-next-line unicorn/prefer-switch 22 | if (item.type === 'Attribute') { 23 | if (!attribute(item, node)) return false 24 | } else if (item.type === 'Id') { 25 | throw new Error('Invalid selector: id') 26 | } else if (item.type === 'ClassName') { 27 | throw new Error('Invalid selector: class') 28 | } else if (item.type === 'PseudoClass') { 29 | if (!pseudo(item, node, index, parent, state)) return false 30 | } else if (item.type === 'PseudoElement') { 31 | throw new Error('Invalid selector: `::' + item.name + '`') 32 | } else if (item.type === 'TagName') { 33 | if (item.name !== node.type) return false 34 | } else { 35 | // Otherwise `item.type` is `WildcardTag`, which matches. 36 | } 37 | } 38 | 39 | return true 40 | } 41 | -------------------------------------------------------------------------------- /lib/attribute.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('css-selector-parser').AstAttribute} AstAttribute 3 | * @typedef {import('css-selector-parser').AstRule} AstRule 4 | * @typedef {import('./types.js').Node} Node 5 | */ 6 | 7 | import {ok as assert} from 'devlop' 8 | import {indexable} from './util.js' 9 | 10 | /** 11 | * @param {AstAttribute} query 12 | * Query. 13 | * @param {Node} node 14 | * Node. 15 | * @returns {boolean} 16 | * Whether `node` matches `query`. 17 | */ 18 | 19 | export function attribute(query, node) { 20 | indexable(node) 21 | const value = node[query.name] 22 | 23 | // Exists. 24 | if (!query.value) { 25 | return value !== null && value !== undefined 26 | } 27 | 28 | assert(query.value.type === 'String', 'expected plain string') 29 | let key = query.value.value 30 | let normal = value === null || value === undefined ? undefined : String(value) 31 | 32 | // Case-sensitivity. 33 | if (query.caseSensitivityModifier === 'i') { 34 | key = key.toLowerCase() 35 | 36 | if (normal) { 37 | normal = normal.toLowerCase() 38 | } 39 | } 40 | 41 | if (value !== undefined) { 42 | switch (query.operator) { 43 | // Exact. 44 | case '=': { 45 | return typeof normal === 'string' && key === normal 46 | } 47 | 48 | // Ends. 49 | case '$=': { 50 | return typeof value === 'string' && value.slice(-key.length) === key 51 | } 52 | 53 | // Contains. 54 | case '*=': { 55 | return typeof value === 'string' && value.includes(key) 56 | } 57 | 58 | // Begins. 59 | case '^=': { 60 | return typeof value === 'string' && key === value.slice(0, key.length) 61 | } 62 | 63 | // Space-separated list. 64 | case '~=': { 65 | // type-coverage:ignore-next-line -- some bug with TS. 66 | return (Array.isArray(value) && value.includes(key)) || normal === key 67 | } 68 | // Other values are not yet supported by CSS. 69 | // No default 70 | } 71 | } 72 | 73 | return false 74 | } 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unist-util-select", 3 | "version": "5.1.0", 4 | "description": "unist utility to select nodes with CSS-like selectors", 5 | "license": "MIT", 6 | "keywords": [ 7 | "unist", 8 | "unist-util", 9 | "util", 10 | "utility", 11 | "visit", 12 | "tree", 13 | "ast", 14 | "node", 15 | "visit", 16 | "walk", 17 | "select", 18 | "selector", 19 | "child", 20 | "descendant", 21 | "sibling", 22 | "type", 23 | "attribute", 24 | "expression", 25 | "filter", 26 | "find", 27 | "match" 28 | ], 29 | "repository": "syntax-tree/unist-util-select", 30 | "bugs": "https://github.com/syntax-tree/unist-util-select/issues", 31 | "funding": { 32 | "type": "opencollective", 33 | "url": "https://opencollective.com/unified" 34 | }, 35 | "author": "Eugene Sharygin ", 36 | "contributors": [ 37 | "Eugene Sharygin ", 38 | "Titus Wormer (https://wooorm.com)", 39 | "Christian Murphy " 40 | ], 41 | "sideEffects": false, 42 | "type": "module", 43 | "exports": "./index.js", 44 | "files": [ 45 | "lib/", 46 | "index.d.ts", 47 | "index.js" 48 | ], 49 | "dependencies": { 50 | "@types/unist": "^3.0.0", 51 | "css-selector-parser": "^3.0.0", 52 | "devlop": "^1.1.0", 53 | "nth-check": "^2.0.0", 54 | "zwitch": "^2.0.0" 55 | }, 56 | "devDependencies": { 57 | "@types/node": "^20.0.0", 58 | "c8": "^8.0.0", 59 | "prettier": "^3.0.0", 60 | "remark-cli": "^11.0.0", 61 | "remark-preset-wooorm": "^9.0.0", 62 | "type-coverage": "^2.0.0", 63 | "typescript": "^5.0.0", 64 | "unist-builder": "^4.0.0", 65 | "xo": "^0.56.0" 66 | }, 67 | "scripts": { 68 | "prepack": "npm run build && npm run format", 69 | "build": "tsc --build --clean && tsc --build && type-coverage", 70 | "format": "remark . -qfo && prettier . -w --log-level warn && xo --fix", 71 | "test-api": "node --conditions development test/index.js", 72 | "test-coverage": "c8 --100 --reporter lcov npm run test-api", 73 | "test": "npm run build && npm run format && npm run test-coverage" 74 | }, 75 | "prettier": { 76 | "bracketSpacing": false, 77 | "semi": false, 78 | "singleQuote": true, 79 | "tabWidth": 2, 80 | "trailingComma": "none", 81 | "useTabs": false 82 | }, 83 | "remarkConfig": { 84 | "plugins": [ 85 | "remark-preset-wooorm" 86 | ] 87 | }, 88 | "typeCoverage": { 89 | "atLeast": 100, 90 | "detail": true, 91 | "ignoreCatch": true, 92 | "strict": true 93 | }, 94 | "xo": { 95 | "overrides": [ 96 | { 97 | "files": [ 98 | "test/**/*.js" 99 | ], 100 | "rules": { 101 | "import/no-unassigned-import": "off", 102 | "max-nested-callbacks": "off", 103 | "no-await-in-loop": "off" 104 | } 105 | } 106 | ], 107 | "rules": { 108 | "max-params": "off" 109 | }, 110 | "prettier": true 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /test/all.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict' 2 | import test from 'node:test' 3 | import {u} from 'unist-builder' 4 | import {selectAll} from 'unist-util-select' 5 | 6 | test('all together now', async function (t) { 7 | await t.test('should expose the public api', async function () { 8 | assert.deepEqual(Object.keys(await import('unist-util-select')).sort(), [ 9 | 'matches', 10 | 'select', 11 | 'selectAll' 12 | ]) 13 | }) 14 | 15 | await t.test('#1', async function () { 16 | assert.deepEqual( 17 | selectAll( 18 | 'a > b[d]:nth-of-type(odd)', 19 | u('root', [ 20 | u('a', [ 21 | u('b', {d: 1}, 'Alpha'), 22 | u('c', 'Bravo'), 23 | u('b', 'Charlie'), 24 | u('c', 'Delta'), 25 | u('b', 'Echo'), 26 | u('c', 'Foxtrot') 27 | ]) 28 | ]) 29 | ), 30 | [u('b', {d: 1}, 'Alpha')] 31 | ) 32 | }) 33 | 34 | await t.test('#2', async function () { 35 | assert.deepEqual( 36 | selectAll( 37 | '[d] ~ c:nth-of-type(even)', 38 | u('root', [ 39 | u('a', [ 40 | u('b', 'Alpha'), 41 | u('c', 'Bravo'), 42 | u('b', {d: 1}, 'Charlie'), 43 | u('c', 'Delta'), 44 | u('b', 'Echo'), 45 | u('c', 'Foxtrot'), 46 | u('b', 'Golf'), 47 | u('c', 'Hotel') 48 | ]) 49 | ]) 50 | ), 51 | [u('c', 'Delta'), u('c', 'Hotel')] 52 | ) 53 | }) 54 | 55 | await t.test('#3', async function () { 56 | assert.deepEqual( 57 | selectAll( 58 | '[d] + c:nth-of-type(even)', 59 | u('root', [ 60 | u('a', [ 61 | u('b', 'Alpha'), 62 | u('c', 'Bravo'), 63 | u('b', {d: 1}, 'Charlie'), 64 | u('c', 'Delta'), 65 | u('b', 'Echo'), 66 | u('c', 'Foxtrot'), 67 | u('b', 'Golf'), 68 | u('c', 'Hotel') 69 | ]) 70 | ]) 71 | ), 72 | [u('c', 'Delta')] 73 | ) 74 | }) 75 | 76 | await t.test('#4', async function () { 77 | assert.deepEqual( 78 | selectAll( 79 | '[d], :nth-of-type(even), [e]', 80 | u('root', [ 81 | u('a', [ 82 | u('b', {e: 3}, 'Alpha'), 83 | u('c', 'Bravo'), 84 | u('b', {d: 1}, 'Charlie'), 85 | u('c', 'Delta'), 86 | u('b', 'Echo'), 87 | u('c', {d: 2, e: 4}, 'Foxtrot'), 88 | u('b', 'Golf'), 89 | u('c', 'Hotel') 90 | ]) 91 | ]) 92 | ), 93 | [ 94 | u('b', {e: 3}, 'Alpha'), 95 | u('b', {d: 1}, 'Charlie'), 96 | u('c', 'Delta'), 97 | u('c', {d: 2, e: 4}, 'Foxtrot'), 98 | u('b', 'Golf'), 99 | u('c', 'Hotel') 100 | ] 101 | ) 102 | }) 103 | 104 | await t.test('#5', async function () { 105 | assert.deepEqual( 106 | selectAll( 107 | 'a:not([b])', 108 | u('root', [ 109 | u('a', {id: 'w', b: 'a'}), 110 | u('a', {id: 'x'}), 111 | u('a', {id: 'y', b: 'a'}), 112 | u('a', {id: 'z'}) 113 | ]) 114 | ), 115 | [u('a', {id: 'x'}), u('a', {id: 'z'})] 116 | ) 117 | }) 118 | }) 119 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('unist').Position} Position 3 | * @typedef {import('unist').Node} Node 4 | * @typedef {import('./lib/types.js').SelectState} SelectState 5 | */ 6 | 7 | /** 8 | * @typedef {Record & {type: string, position?: Position | undefined}} NodeLike 9 | */ 10 | 11 | import {parse} from './lib/parse.js' 12 | import {parent} from './lib/util.js' 13 | import {walk} from './lib/walk.js' 14 | 15 | /** 16 | * Check that the given `node` matches `selector`. 17 | * 18 | * This only checks the node itself, not the surrounding tree. 19 | * Thus, nesting in selectors is not supported (`paragraph strong`, 20 | * `paragraph > strong`), neither are selectors like `:first-child`, etc. 21 | * This only checks that the given node matches the selector. 22 | * 23 | * @param {string} selector 24 | * CSS selector, such as (`heading`, `link, linkReference`). 25 | * @param {Node | NodeLike | null | undefined} [node] 26 | * Node that might match `selector`. 27 | * @returns {boolean} 28 | * Whether `node` matches `selector`. 29 | */ 30 | export function matches(selector, node) { 31 | const state = createState(selector, node) 32 | state.one = true 33 | state.shallow = true 34 | walk(state, node || undefined) 35 | return state.results.length > 0 36 | } 37 | 38 | /** 39 | * Select the first node that matches `selector` in the given `tree`. 40 | * 41 | * Searches the tree in *preorder*. 42 | * 43 | * @param {string} selector 44 | * CSS selector, such as (`heading`, `link, linkReference`). 45 | * @param {Node | NodeLike | null | undefined} [tree] 46 | * Tree to search. 47 | * @returns {Node | undefined} 48 | * First node in `tree` that matches `selector` or `null` if nothing is 49 | * found. 50 | * 51 | * This could be `tree` itself. 52 | */ 53 | export function select(selector, tree) { 54 | const state = createState(selector, tree) 55 | state.one = true 56 | walk(state, tree || undefined) 57 | return state.results[0] 58 | } 59 | 60 | /** 61 | * Select all nodes that match `selector` in the given `tree`. 62 | * 63 | * Searches the tree in *preorder*. 64 | * 65 | * @param {string} selector 66 | * CSS selector, such as (`heading`, `link, linkReference`). 67 | * @param {Node | NodeLike | null | undefined} [tree] 68 | * Tree to search. 69 | * @returns {Array} 70 | * Nodes in `tree` that match `selector`. 71 | * 72 | * This could include `tree` itself. 73 | */ 74 | export function selectAll(selector, tree) { 75 | const state = createState(selector, tree) 76 | walk(state, tree || undefined) 77 | return state.results 78 | } 79 | 80 | /** 81 | * @param {string} selector 82 | * Selector to parse. 83 | * @param {Node | null | undefined} tree 84 | * Tree to search. 85 | * @returns {SelectState} 86 | * State. 87 | */ 88 | function createState(selector, tree) { 89 | return { 90 | // State of the query. 91 | rootQuery: parse(selector), 92 | results: [], 93 | scopeNodes: tree 94 | ? parent(tree) && 95 | // Root in nlcst. 96 | (tree.type === 'RootNode' || tree.type === 'root') 97 | ? tree.children 98 | : [tree] 99 | : [], 100 | one: false, 101 | shallow: false, 102 | found: false, 103 | // State in the tree. 104 | typeIndex: undefined, 105 | nodeIndex: undefined, 106 | typeCount: undefined, 107 | nodeCount: undefined 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/walk.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('css-selector-parser').AstRule} AstRule 3 | * @typedef {import('unist').Node} Node 4 | * @typedef {import('unist').Parent} Parent 5 | * @typedef {import('./types.js').SelectState} SelectState 6 | * 7 | * @typedef Nest 8 | * Rule sets by nesting. 9 | * @property {Array | undefined} descendant 10 | * `a b` 11 | * @property {Array | undefined} directChild 12 | * `a > b` 13 | * @property {Array | undefined} adjacentSibling 14 | * `a + b` 15 | * @property {Array | undefined} generalSibling 16 | * `a ~ b` 17 | * 18 | * @typedef Counts 19 | * Info on nodes in a parent. 20 | * @property {number} count 21 | * Number of nodes. 22 | * @property {Map} types 23 | * Number of nodes by type. 24 | */ 25 | 26 | import {test} from './test.js' 27 | import {parent} from './util.js' 28 | 29 | /** @type {Array} */ 30 | const empty = [] 31 | 32 | /** 33 | * Walk a tree. 34 | * 35 | * @param {SelectState} state 36 | * @param {Node | undefined} tree 37 | */ 38 | export function walk(state, tree) { 39 | if (tree) { 40 | one(state, [], tree, undefined, undefined, tree) 41 | } 42 | } 43 | 44 | /** 45 | * Check a node. 46 | * 47 | * @param {SelectState} state 48 | * @param {Array} currentRules 49 | * @param {Node} node 50 | * @param {number | undefined} index 51 | * @param {Parent | undefined} parentNode 52 | * @param {Node} tree 53 | * @returns {Nest} 54 | */ 55 | function one(state, currentRules, node, index, parentNode, tree) { 56 | /** @type {Nest} */ 57 | let nestResult = { 58 | directChild: undefined, 59 | descendant: undefined, 60 | adjacentSibling: undefined, 61 | generalSibling: undefined 62 | } 63 | 64 | let rootRules = state.rootQuery.rules 65 | 66 | // Remove direct child rules if this is the root. 67 | // This only happens for a `:has()` rule, which can be like 68 | // `a:has(> b)`. 69 | if (parentNode && parentNode !== tree) { 70 | rootRules = state.rootQuery.rules.filter( 71 | (d) => 72 | d.combinator === undefined || 73 | (d.combinator === '>' && parentNode === tree) 74 | ) 75 | } 76 | 77 | nestResult = applySelectors( 78 | state, 79 | // Try the root rules for this node too. 80 | combine(currentRules, rootRules), 81 | node, 82 | index, 83 | parentNode 84 | ) 85 | 86 | // If this is a parent, and we want to delve into them, and we haven’t found 87 | // our single result yet. 88 | if (parent(node) && !state.shallow && !(state.one && state.found)) { 89 | all(state, nestResult, node, tree) 90 | } 91 | 92 | return nestResult 93 | } 94 | 95 | /** 96 | * Check a node. 97 | * 98 | * @param {SelectState} state 99 | * @param {Nest} nest 100 | * @param {Parent} node 101 | * @param {Node} tree 102 | * @returns {undefined} 103 | */ 104 | function all(state, nest, node, tree) { 105 | const fromParent = combine(nest.descendant, nest.directChild) 106 | /** @type {Array | undefined} */ 107 | let fromSibling 108 | let index = -1 109 | /** 110 | * Total counts. 111 | * @type {Counts} 112 | */ 113 | const total = {count: 0, types: new Map()} 114 | /** 115 | * Counts of previous siblings. 116 | * @type {Counts} 117 | */ 118 | const before = {count: 0, types: new Map()} 119 | 120 | while (++index < node.children.length) { 121 | count(total, node.children[index]) 122 | } 123 | 124 | index = -1 125 | 126 | while (++index < node.children.length) { 127 | const child = node.children[index] 128 | // Uppercase to prevent prototype polution, injecting `constructor` or so. 129 | const name = child.type.toUpperCase() 130 | // Before counting further nodes: 131 | state.nodeIndex = before.count 132 | state.typeIndex = before.types.get(name) || 0 133 | // After counting all nodes. 134 | state.nodeCount = total.count 135 | state.typeCount = total.types.get(name) 136 | 137 | // Only apply if this is a parent. 138 | const forSibling = combine(fromParent, fromSibling) 139 | const nest = one(state, forSibling, node.children[index], index, node, tree) 140 | fromSibling = combine(nest.generalSibling, nest.adjacentSibling) 141 | 142 | // We found one thing, and one is enough. 143 | if (state.one && state.found) { 144 | break 145 | } 146 | 147 | count(before, node.children[index]) 148 | } 149 | } 150 | 151 | /** 152 | * Apply selectors to a node. 153 | * 154 | * @param {SelectState} state 155 | * Current state. 156 | * @param {Array} rules 157 | * Rules to apply. 158 | * @param {Node} node 159 | * Node to apply rules to. 160 | * @param {number | undefined} index 161 | * Index of node in parent. 162 | * @param {Parent | undefined} parent 163 | * Parent of node. 164 | * @returns {Nest} 165 | * Further rules. 166 | */ 167 | function applySelectors(state, rules, node, index, parent) { 168 | /** @type {Nest} */ 169 | const nestResult = { 170 | directChild: undefined, 171 | descendant: undefined, 172 | adjacentSibling: undefined, 173 | generalSibling: undefined 174 | } 175 | let selectorIndex = -1 176 | 177 | while (++selectorIndex < rules.length) { 178 | const rule = rules[selectorIndex] 179 | 180 | // We found one thing, and one is enough. 181 | if (state.one && state.found) { 182 | break 183 | } 184 | 185 | // When shallow, we don’t allow nested rules. 186 | // Idea: we could allow a stack of parents? 187 | // Might get quite complex though. 188 | if (state.shallow && rule.nestedRule) { 189 | throw new Error('Expected selector without nesting') 190 | } 191 | 192 | // If this rule matches: 193 | if (test(rule, node, index, parent, state)) { 194 | const nest = rule.nestedRule 195 | 196 | // Are there more? 197 | if (nest) { 198 | /** @type {keyof Nest} */ 199 | const label = 200 | nest.combinator === '+' 201 | ? 'adjacentSibling' 202 | : nest.combinator === '~' 203 | ? 'generalSibling' 204 | : nest.combinator === '>' 205 | ? 'directChild' 206 | : 'descendant' 207 | add(nestResult, label, nest) 208 | } else { 209 | // We have a match! 210 | state.found = true 211 | 212 | if (!state.results.includes(node)) { 213 | state.results.push(node) 214 | } 215 | } 216 | } 217 | 218 | // Descendant. 219 | if (rule.combinator === undefined) { 220 | add(nestResult, 'descendant', rule) 221 | } 222 | // Adjacent. 223 | else if (rule.combinator === '~') { 224 | add(nestResult, 'generalSibling', rule) 225 | } 226 | // Drop direct child (`>`), adjacent sibling (`+`). 227 | } 228 | 229 | return nestResult 230 | } 231 | 232 | /** 233 | * Combine two lists, if needed. 234 | * 235 | * This is optimized to create as few lists as possible. 236 | * 237 | * @param {Array | undefined} left 238 | * @param {Array | undefined} right 239 | * @returns {Array} 240 | */ 241 | function combine(left, right) { 242 | return left && right && left.length > 0 && right.length > 0 243 | ? [...left, ...right] 244 | : left && left.length > 0 245 | ? left 246 | : right && right.length > 0 247 | ? right 248 | : empty 249 | } 250 | 251 | /** 252 | * Add a rule to a nesting map. 253 | * 254 | * @param {Nest} nest 255 | * @param {keyof Nest} field 256 | * @param {AstRule} rule 257 | */ 258 | function add(nest, field, rule) { 259 | const list = nest[field] 260 | if (list) { 261 | list.push(rule) 262 | } else { 263 | nest[field] = [rule] 264 | } 265 | } 266 | 267 | /** 268 | * Count a node. 269 | * 270 | * @param {Counts} counts 271 | * Counts. 272 | * @param {Node} node 273 | * Node. 274 | * @returns {undefined} 275 | * Nothing. 276 | */ 277 | function count(counts, node) { 278 | // Uppercase to prevent prototype polution, injecting `constructor` or so. 279 | // Normalize because HTML is insensitive. 280 | const name = node.type.toUpperCase() 281 | const count = (counts.types.get(name) || 0) + 1 282 | counts.count++ 283 | counts.types.set(name, count) 284 | } 285 | -------------------------------------------------------------------------------- /lib/pseudo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('css-selector-parser').AstPseudoClass} AstPseudoClass 3 | * @typedef {import('unist').Node} Node 4 | * @typedef {import('unist').Parent} Parent 5 | * @typedef {import('./types.js').SelectState} SelectState 6 | */ 7 | 8 | import {ok as assert, unreachable} from 'devlop' 9 | import fauxEsmNthCheck from 'nth-check' 10 | import {zwitch} from 'zwitch' 11 | import {parent} from './util.js' 12 | import {walk} from './walk.js' 13 | 14 | /** @type {import('nth-check').default} */ 15 | // @ts-expect-error: `nth-check` types are wrong. 16 | const nthCheck = fauxEsmNthCheck.default || fauxEsmNthCheck 17 | 18 | /** @type {(rule: AstPseudoClass, node: Node, index: number | undefined, parent: Parent | undefined, state: SelectState) => boolean} */ 19 | export const pseudo = zwitch('name', { 20 | // @ts-expect-error: always known. 21 | unknown: unknownPseudo, 22 | invalid: invalidPseudo, 23 | handlers: { 24 | is, 25 | blank: empty, 26 | empty, 27 | 'first-child': firstChild, 28 | 'first-of-type': firstOfType, 29 | has, 30 | 'last-child': lastChild, 31 | 'last-of-type': lastOfType, 32 | not, 33 | 'nth-child': nthChild, 34 | 'nth-last-child': nthLastChild, 35 | 'nth-of-type': nthOfType, 36 | 'nth-last-of-type': nthLastOfType, 37 | 'only-child': onlyChild, 38 | 'only-of-type': onlyOfType, 39 | root, 40 | scope 41 | } 42 | }) 43 | 44 | /** 45 | * Check whether a node matches an `:empty` pseudo. 46 | * 47 | * @param {AstPseudoClass} _1 48 | * @param {Node} node 49 | * @returns {boolean} 50 | */ 51 | function empty(_1, node) { 52 | return parent(node) ? node.children.length === 0 : !('value' in node) 53 | } 54 | 55 | /** 56 | * Check whether a node matches a `:first-child` pseudo. 57 | * 58 | * @param {AstPseudoClass} query 59 | * @param {Node} _1 60 | * @param {number | undefined} _2 61 | * @param {Parent | undefined} _3 62 | * @param {SelectState} state 63 | * @returns {boolean} 64 | */ 65 | function firstChild(query, _1, _2, _3, state) { 66 | assertDeep(state, query) 67 | return state.nodeIndex === 0 // Specifically `0`, not falsey. 68 | } 69 | 70 | /** 71 | * Check whether a node matches a `:first-of-type` pseudo. 72 | * 73 | * @param {AstPseudoClass} query 74 | * @param {Node} _1 75 | * @param {number | undefined} _2 76 | * @param {Parent | undefined} _3 77 | * @param {SelectState} state 78 | * @returns {boolean} 79 | */ 80 | function firstOfType(query, _1, _2, _3, state) { 81 | assertDeep(state, query) 82 | return state.typeIndex === 0 83 | } 84 | 85 | /** 86 | * @param {AstPseudoClass} query 87 | * @param {Node} node 88 | * @param {number | undefined} _1 89 | * @param {Parent | undefined} _2 90 | * @param {SelectState} state 91 | * @returns {boolean} 92 | */ 93 | function has(query, node, _1, _2, state) { 94 | const argument = query.argument 95 | 96 | /* c8 ignore next 3 -- never happens with our config */ 97 | if (!argument || argument.type !== 'Selector') { 98 | unreachable('`:has` has selectors') 99 | } 100 | 101 | const fragment = {type: 'root', children: parent(node) ? node.children : []} 102 | /** @type {SelectState} */ 103 | const childState = { 104 | ...state, 105 | // Not found yet. 106 | found: false, 107 | // Do walk deep. 108 | shallow: false, 109 | // One result is enough. 110 | one: true, 111 | scopeNodes: [node], 112 | results: [], 113 | rootQuery: argument 114 | } 115 | 116 | walk(childState, fragment) 117 | 118 | return childState.results.length > 0 119 | } 120 | 121 | /** 122 | * Check whether a node matches a `:last-child` pseudo. 123 | * 124 | * @param {AstPseudoClass} query 125 | * @param {Node} _1 126 | * @param {number | undefined} _2 127 | * @param {Parent | undefined} _3 128 | * @param {SelectState} state 129 | * @returns {boolean} 130 | */ 131 | function lastChild(query, _1, _2, _3, state) { 132 | assertDeep(state, query) 133 | return ( 134 | typeof state.nodeCount === 'number' && 135 | state.nodeIndex === state.nodeCount - 1 136 | ) 137 | } 138 | 139 | /** 140 | * Check whether a node matches a `:last-of-type` pseudo. 141 | * 142 | * @param {AstPseudoClass} query 143 | * @param {Node} _1 144 | * @param {number | undefined} _2 145 | * @param {Parent | undefined} _3 146 | * @param {SelectState} state 147 | * @returns {boolean} 148 | */ 149 | function lastOfType(query, _1, _2, _3, state) { 150 | assertDeep(state, query) 151 | return ( 152 | typeof state.typeCount === 'number' && 153 | state.typeIndex === state.typeCount - 1 154 | ) 155 | } 156 | 157 | /** 158 | * Check whether a node `:is` further selectors. 159 | * 160 | * @param {AstPseudoClass} query 161 | * @param {Node} node 162 | * @param {number | undefined} _1 163 | * @param {Parent | undefined} _2 164 | * @param {SelectState} state 165 | * @returns {boolean} 166 | */ 167 | function is(query, node, _1, _2, state) { 168 | const argument = query.argument 169 | 170 | /* c8 ignore next 3 -- never happens with our config */ 171 | if (!argument || argument.type !== 'Selector') { 172 | unreachable('`:is` has selectors') 173 | } 174 | 175 | /** @type {SelectState} */ 176 | const childState = { 177 | ...state, 178 | // Not found yet. 179 | found: false, 180 | // Do walk deep. 181 | shallow: false, 182 | // One result is enough. 183 | one: true, 184 | scopeNodes: [node], 185 | results: [], 186 | rootQuery: argument 187 | } 188 | 189 | walk(childState, node) 190 | 191 | return childState.results[0] === node 192 | } 193 | 194 | /** 195 | * Check whether a node does `:not` match further selectors. 196 | * 197 | * @param {AstPseudoClass} query 198 | * @param {Node} node 199 | * @param {number | undefined} index 200 | * @param {Parent | undefined} parent 201 | * @param {SelectState} state 202 | * @returns {boolean} 203 | */ 204 | function not(query, node, index, parent, state) { 205 | return !is(query, node, index, parent, state) 206 | } 207 | 208 | /** 209 | * Check whether a node matches an `:nth-child` pseudo. 210 | * 211 | * @param {AstPseudoClass} query 212 | * @param {Node} _1 213 | * @param {number | undefined} _2 214 | * @param {Parent | undefined} _3 215 | * @param {SelectState} state 216 | * @returns {boolean} 217 | */ 218 | function nthChild(query, _1, _2, _3, state) { 219 | const fn = getCachedNthCheck(query) 220 | assertDeep(state, query) 221 | return typeof state.nodeIndex === 'number' && fn(state.nodeIndex) 222 | } 223 | 224 | /** 225 | * Check whether a node matches an `:nth-last-child` pseudo. 226 | * 227 | * @param {AstPseudoClass} query 228 | * @param {Node} _1 229 | * @param {number | undefined} _2 230 | * @param {Parent | undefined} _3 231 | * @param {SelectState} state 232 | * @returns {boolean} 233 | */ 234 | function nthLastChild(query, _1, _2, _3, state) { 235 | const fn = getCachedNthCheck(query) 236 | assertDeep(state, query) 237 | return ( 238 | typeof state.nodeCount === 'number' && 239 | typeof state.nodeIndex === 'number' && 240 | fn(state.nodeCount - state.nodeIndex - 1) 241 | ) 242 | } 243 | 244 | /** 245 | * Check whether a node matches a `:nth-last-of-type` pseudo. 246 | * 247 | * @param {AstPseudoClass} query 248 | * @param {Node} _1 249 | * @param {number | undefined} _2 250 | * @param {Parent | undefined} _3 251 | * @param {SelectState} state 252 | * @returns {boolean} 253 | */ 254 | function nthLastOfType(query, _1, _2, _3, state) { 255 | const fn = getCachedNthCheck(query) 256 | assertDeep(state, query) 257 | return ( 258 | typeof state.typeIndex === 'number' && 259 | typeof state.typeCount === 'number' && 260 | fn(state.typeCount - 1 - state.typeIndex) 261 | ) 262 | } 263 | 264 | /** 265 | * Check whether a node matches an `:nth-of-type` pseudo. 266 | * 267 | * @param {AstPseudoClass} query 268 | * @param {Node} _1 269 | * @param {number | undefined} _2 270 | * @param {Parent | undefined} _3 271 | * @param {SelectState} state 272 | * @returns {boolean} 273 | */ 274 | function nthOfType(query, _1, _2, _3, state) { 275 | const fn = getCachedNthCheck(query) 276 | assertDeep(state, query) 277 | return typeof state.typeIndex === 'number' && fn(state.typeIndex) 278 | } 279 | 280 | /** 281 | * Check whether a node matches an `:only-child` pseudo. 282 | * 283 | * @param {AstPseudoClass} query 284 | * @param {Node} _1 285 | * @param {number | undefined} _2 286 | * @param {Parent | undefined} _3 287 | * @param {SelectState} state 288 | * @returns {boolean} 289 | */ 290 | function onlyChild(query, _1, _2, _3, state) { 291 | assertDeep(state, query) 292 | return state.nodeCount === 1 293 | } 294 | 295 | /** 296 | * Check whether a node matches an `:only-of-type` pseudo. 297 | * 298 | * @param {AstPseudoClass} query 299 | * @param {Node} _1 300 | * @param {number | undefined} _2 301 | * @param {Parent | undefined} _3 302 | * @param {SelectState} state 303 | * @returns {boolean} 304 | */ 305 | function onlyOfType(query, _1, _2, _3, state) { 306 | assertDeep(state, query) 307 | return state.typeCount === 1 308 | } 309 | 310 | /** 311 | * Check whether a node matches a `:root` pseudo. 312 | * 313 | * @param {AstPseudoClass} _1 314 | * @param {Node} node 315 | * @param {number | undefined} _2 316 | * @param {Parent | undefined} parent 317 | * @returns {boolean} 318 | */ 319 | function root(_1, node, _2, parent) { 320 | return node && !parent 321 | } 322 | 323 | /** 324 | * Check whether a node matches a `:scope` pseudo. 325 | * 326 | * @param {AstPseudoClass} _1 327 | * @param {Node} node 328 | * @param {number | undefined} _2 329 | * @param {Parent | undefined} _3 330 | * @param {SelectState} state 331 | * @returns {boolean} 332 | */ 333 | function scope(_1, node, _2, _3, state) { 334 | return node && state.scopeNodes.includes(node) 335 | } 336 | 337 | // Shouldn’t be called, parser gives correct data. 338 | /* c8 ignore next 3 */ 339 | function invalidPseudo() { 340 | throw new Error('Invalid pseudo-selector') 341 | } 342 | 343 | /** 344 | * @param {AstPseudoClass} query 345 | * @returns {never} 346 | */ 347 | function unknownPseudo(query) { 348 | throw new Error('Unknown pseudo-selector `' + query.name + '`') 349 | } 350 | 351 | /** 352 | * @param {SelectState} state 353 | * @param {AstPseudoClass} query 354 | */ 355 | function assertDeep(state, query) { 356 | if (state.shallow) { 357 | throw new Error('Cannot use `:' + query.name + '` without parent') 358 | } 359 | } 360 | 361 | /** 362 | * @param {AstPseudoClass} query 363 | * @returns {(value: number) => boolean} 364 | */ 365 | function getCachedNthCheck(query) { 366 | /** @type {(value: number) => boolean} */ 367 | // @ts-expect-error: cache. 368 | let fn = query._cachedFn 369 | 370 | if (!fn) { 371 | const value = query.argument 372 | assert(value, 'expected `argument`') 373 | 374 | if (value.type !== 'Formula') { 375 | throw new Error( 376 | 'Expected `nth` formula, such as `even` or `2n+1` (`of` is not yet supported)' 377 | ) 378 | } 379 | 380 | fn = nthCheck(value.a + 'n+' + value.b) 381 | // @ts-expect-error: cache. 382 | query._cachedFn = fn 383 | } 384 | 385 | return fn 386 | } 387 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # unist-util-select 2 | 3 | [![Build][build-badge]][build] 4 | [![Coverage][coverage-badge]][coverage] 5 | [![Downloads][downloads-badge]][downloads] 6 | [![Size][size-badge]][size] 7 | [![Sponsors][sponsors-badge]][collective] 8 | [![Backers][backers-badge]][collective] 9 | [![Chat][chat-badge]][chat] 10 | 11 | [unist][] utility with equivalents for `querySelector`, `querySelectorAll`, 12 | and `matches`. 13 | 14 | ## Contents 15 | 16 | * [What is this?](#what-is-this) 17 | * [When should I use this?](#when-should-i-use-this) 18 | * [Install](#install) 19 | * [Use](#use) 20 | * [API](#api) 21 | * [`matches(selector, node)`](#matchesselector-node) 22 | * [`select(selector, tree)`](#selectselector-tree) 23 | * [`selectAll(selector, tree)`](#selectallselector-tree) 24 | * [Support](#support) 25 | * [Types](#types) 26 | * [Compatibility](#compatibility) 27 | * [Related](#related) 28 | * [Contribute](#contribute) 29 | * [License](#license) 30 | 31 | ## What is this? 32 | 33 | This package lets you find nodes in a tree, similar to how `querySelector`, 34 | `querySelectorAll`, and `matches` work with the DOM. 35 | 36 | One notable difference between DOM and hast is that DOM nodes have references 37 | to their parents, meaning that `document.body.matches(':last-child')` can 38 | be evaluated to check whether the body is the last child of its parent. 39 | This information is not stored in hast, so selectors like that don’t work. 40 | 41 | ## When should I use this? 42 | 43 | This utility works on any unist syntax tree and you can select all node types. 44 | If you are working with [hast][], and only want to select elements, use 45 | [`hast-util-select`][hast-util-select] instead. 46 | 47 | This is a small utility that is quite useful, but is rather slow if you use it a 48 | lot. 49 | For each call, it has to walk the entire tree. 50 | In some cases, walking the tree once with [`unist-util-visit`][unist-util-visit] 51 | is smarter, such as when you want to change certain nodes. 52 | On the other hand, this is quite powerful and fast enough for many other cases. 53 | 54 | ## Install 55 | 56 | This package is [ESM only][esm]. 57 | In Node.js (version 16+), install with [npm][]: 58 | 59 | ```sh 60 | npm install unist-util-select 61 | ``` 62 | 63 | In Deno with [`esm.sh`][esmsh]: 64 | 65 | ```js 66 | import {matches, select, selectAll} from "https://esm.sh/unist-util-select@5" 67 | ``` 68 | 69 | In browsers with [`esm.sh`][esmsh]: 70 | 71 | ```html 72 | 75 | ``` 76 | 77 | ## Use 78 | 79 | ```js 80 | import {u} from 'unist-builder' 81 | import {matches, select, selectAll} from 'unist-util-select' 82 | 83 | const tree = u('blockquote', [ 84 | u('paragraph', [u('text', 'Alpha')]), 85 | u('paragraph', [u('text', 'Bravo')]), 86 | u('code', 'Charlie'), 87 | u('paragraph', [u('text', 'Delta')]), 88 | u('paragraph', [u('text', 'Echo')]), 89 | u('paragraph', [u('text', 'Foxtrot')]), 90 | u('paragraph', [u('text', 'Golf')]) 91 | ]) 92 | 93 | console.log(matches('blockquote, list', tree)) // => true 94 | 95 | console.log(select('code ~ :nth-child(even)', tree)) 96 | // The paragraph with `Delta` 97 | 98 | console.log(selectAll('code ~ :nth-child(even)', tree)) 99 | // The paragraphs with `Delta` and `Foxtrot` 100 | ``` 101 | 102 | ## API 103 | 104 | This package exports the identifiers [`matches`][api-matches], 105 | [`select`][api-select], and [`selectAll`][api-select-all]. 106 | There is no default export. 107 | 108 | ### `matches(selector, node)` 109 | 110 | Check that the given `node` matches `selector`. 111 | 112 | This only checks the node itself, not the surrounding tree. 113 | Thus, nesting in selectors is not supported (`paragraph strong`, 114 | `paragraph > strong`), neither are selectors like `:first-child`, etc. 115 | This only checks that the given node matches the selector. 116 | 117 | ###### Parameters 118 | 119 | * `selector` (`string`) 120 | — CSS selector, such as (`heading`, `link, linkReference`). 121 | * `node` ([`Node`][node], optional) 122 | — node that might match `selector` 123 | 124 | ###### Returns 125 | 126 | Whether `node` matches `selector` (`boolean`). 127 | 128 | ###### Example 129 | 130 | ```js 131 | import {u} from 'unist-builder' 132 | import {matches} from 'unist-util-select' 133 | 134 | matches('strong, em', u('strong', [u('text', 'important')])) // => true 135 | matches('[lang]', u('code', {lang: 'js'}, 'console.log(1)')) // => true 136 | ``` 137 | 138 | ### `select(selector, tree)` 139 | 140 | Select the first node that matches `selector` in the given `tree`. 141 | 142 | Searches the tree in *[preorder][]*. 143 | 144 | ###### Parameters 145 | 146 | * `selector` (`string`) 147 | — CSS selector, such as (`heading`, `link, linkReference`). 148 | * `tree` ([`Node`][node], optional) 149 | — tree to search 150 | 151 | ###### Returns 152 | 153 | First node in `tree` that matches `selector` or `undefined` if nothing is found. 154 | 155 | This could be `tree` itself. 156 | 157 | ###### Example 158 | 159 | ```js 160 | import {u} from 'unist-builder' 161 | import {select} from 'unist-util-select' 162 | 163 | console.log( 164 | select( 165 | 'code ~ :nth-child(even)', 166 | u('blockquote', [ 167 | u('paragraph', [u('text', 'Alpha')]), 168 | u('paragraph', [u('text', 'Bravo')]), 169 | u('code', 'Charlie'), 170 | u('paragraph', [u('text', 'Delta')]), 171 | u('paragraph', [u('text', 'Echo')]) 172 | ]) 173 | ) 174 | ) 175 | ``` 176 | 177 | Yields: 178 | 179 | ```js 180 | {type: 'paragraph', children: [{type: 'text', value: 'Delta'}]} 181 | ``` 182 | 183 | ### `selectAll(selector, tree)` 184 | 185 | Select all nodes that match `selector` in the given `tree`. 186 | 187 | Searches the tree in *[preorder][]*. 188 | 189 | ###### Parameters 190 | 191 | * `selector` (`string`) 192 | — CSS selector, such as (`heading`, `link, linkReference`). 193 | * `tree` ([`Node`][node], optional) 194 | — tree to search 195 | 196 | ###### Returns 197 | 198 | Nodes in `tree` that match `selector`. 199 | 200 | This could include `tree` itself. 201 | 202 | ###### Example 203 | 204 | ```js 205 | import {u} from 'unist-builder' 206 | import {selectAll} from 'unist-util-select' 207 | 208 | console.log( 209 | selectAll( 210 | 'code ~ :nth-child(even)', 211 | u('blockquote', [ 212 | u('paragraph', [u('text', 'Alpha')]), 213 | u('paragraph', [u('text', 'Bravo')]), 214 | u('code', 'Charlie'), 215 | u('paragraph', [u('text', 'Delta')]), 216 | u('paragraph', [u('text', 'Echo')]), 217 | u('paragraph', [u('text', 'Foxtrot')]), 218 | u('paragraph', [u('text', 'Golf')]) 219 | ]) 220 | ) 221 | ) 222 | ``` 223 | 224 | Yields: 225 | 226 | ```js 227 | [ 228 | {type: 'paragraph', children: [{type: 'text', value: 'Delta'}]}, 229 | {type: 'paragraph', children: [{type: 'text', value: 'Foxtrot'}]} 230 | ] 231 | ``` 232 | 233 | ## Support 234 | 235 | * [x] `*` (universal selector) 236 | * [x] `,` (multiple selector) 237 | * [x] `paragraph` (type selector) 238 | * [x] `blockquote paragraph` (combinator: descendant selector) 239 | * [x] `blockquote > paragraph` (combinator: child selector) 240 | * [x] `code + paragraph` (combinator: adjacent sibling selector) 241 | * [x] `code ~ paragraph` (combinator: general sibling selector) 242 | * [x] `[attr]` (attribute existence, checks that the value on the tree is not 243 | nullish) 244 | * [x] `[attr=value]` (attribute equality, this stringifies values on the tree) 245 | * [x] `[attr^=value]` (attribute begins with, only works on strings) 246 | * [x] `[attr$=value]` (attribute ends with, only works on strings) 247 | * [x] `[attr*=value]` (attribute contains, only works on strings) 248 | * [x] `[attr~=value]` (attribute contains, checks if `value` is in the array, 249 | if there’s an array on the tree, otherwise same as attribute equality) 250 | * [x] `:is()` (functional pseudo-class) 251 | * [x] `:has()` (functional pseudo-class; also supports `a:has(> b)`) 252 | * [x] `:not()` (functional pseudo-class) 253 | * [x] `:blank` (pseudo-class, blank and empty are the same: a parent without 254 | children, or a node without value) 255 | * [x] `:empty` (pseudo-class, blank and empty are the same: a parent without 256 | children, or a node without value) 257 | * [x] `:root` (pseudo-class, matches the given node) 258 | * [x] `:scope` (pseudo-class, matches the given node) 259 | * [x] \* `:first-child` (pseudo-class) 260 | * [x] \* `:first-of-type` (pseudo-class) 261 | * [x] \* `:last-child` (pseudo-class) 262 | * [x] \* `:last-of-type` (pseudo-class) 263 | * [x] \* `:only-child` (pseudo-class) 264 | * [x] \* `:only-of-type` (pseudo-class) 265 | * [x] \* `:nth-child()` (functional pseudo-class) 266 | * [x] \* `:nth-last-child()` (functional pseudo-class) 267 | * [x] \* `:nth-last-of-type()` (functional pseudo-class) 268 | * [x] \* `:nth-of-type()` (functional pseudo-class) 269 | 270 | ###### Notes 271 | 272 | * \* — not supported in `matches` 273 | * `:any()` and `:matches()` are renamed to `:is()` in CSS 274 | 275 | ## Types 276 | 277 | This package is fully typed with [TypeScript][]. 278 | It exports no additional types. 279 | 280 | ## Compatibility 281 | 282 | Projects maintained by the unified collective are compatible with maintained 283 | versions of Node.js. 284 | 285 | When we cut a new major release, we drop support for unmaintained versions of 286 | Node. 287 | This means we try to keep the current release line, `unist-util-select@^5`, 288 | compatible with Node.js 16. 289 | 290 | ## Related 291 | 292 | * [`unist-util-is`](https://github.com/syntax-tree/unist-util-is) 293 | — check if a node passes a test 294 | * [`unist-util-visit`](https://github.com/syntax-tree/unist-util-visit) 295 | — recursively walk over nodes 296 | * [`unist-util-visit-parents`](https://github.com/syntax-tree/unist-util-visit-parents) 297 | — like `visit`, but with a stack of parents 298 | * [`unist-builder`](https://github.com/syntax-tree/unist-builder) 299 | — create unist trees 300 | 301 | ## Contribute 302 | 303 | See [`contributing.md`][contributing] in [`syntax-tree/.github`][health] for 304 | ways to get started. 305 | See [`support.md`][help] for ways to get help. 306 | 307 | This project has a [code of conduct][coc]. 308 | By interacting with this repository, organization, or community you agree to 309 | abide by its terms. 310 | 311 | ## License 312 | 313 | [MIT][license] © Eugene Sharygin 314 | 315 | 316 | 317 | [build-badge]: https://github.com/syntax-tree/unist-util-select/workflows/main/badge.svg 318 | 319 | [build]: https://github.com/syntax-tree/unist-util-select/actions 320 | 321 | [coverage-badge]: https://img.shields.io/codecov/c/github/syntax-tree/unist-util-select.svg 322 | 323 | [coverage]: https://codecov.io/github/syntax-tree/unist-util-select 324 | 325 | [downloads-badge]: https://img.shields.io/npm/dm/unist-util-select.svg 326 | 327 | [downloads]: https://www.npmjs.com/package/unist-util-select 328 | 329 | [size-badge]: https://img.shields.io/badge/dynamic/json?label=minzipped%20size&query=$.size.compressedSize&url=https://deno.bundlejs.com/?q=unist-util-select 330 | 331 | [size]: https://bundlejs.com/?q=unist-util-select 332 | 333 | [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg 334 | 335 | [backers-badge]: https://opencollective.com/unified/backers/badge.svg 336 | 337 | [collective]: https://opencollective.com/unified 338 | 339 | [chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg 340 | 341 | [chat]: https://github.com/syntax-tree/unist/discussions 342 | 343 | [npm]: https://docs.npmjs.com/cli/install 344 | 345 | [esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 346 | 347 | [esmsh]: https://esm.sh 348 | 349 | [typescript]: https://www.typescriptlang.org 350 | 351 | [license]: license 352 | 353 | [health]: https://github.com/syntax-tree/.github 354 | 355 | [contributing]: https://github.com/syntax-tree/.github/blob/main/contributing.md 356 | 357 | [help]: https://github.com/syntax-tree/.github/blob/main/support.md 358 | 359 | [coc]: https://github.com/syntax-tree/.github/blob/main/code-of-conduct.md 360 | 361 | [unist]: https://github.com/syntax-tree/unist 362 | 363 | [node]: https://github.com/syntax-tree/unist#node 364 | 365 | [preorder]: https://github.com/syntax-tree/unist#preorder 366 | 367 | [unist-util-visit]: https://github.com/syntax-tree/unist-util-visit 368 | 369 | [hast]: https://github.com/syntax-tree/hast 370 | 371 | [hast-util-select]: https://github.com/syntax-tree/hast-util-select 372 | 373 | [api-matches]: #matchesselector-node 374 | 375 | [api-select]: #selectselector-tree 376 | 377 | [api-select-all]: #selectallselector-tree 378 | -------------------------------------------------------------------------------- /test/select.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict' 2 | import test from 'node:test' 3 | import {u} from 'unist-builder' 4 | import {select} from 'unist-util-select' 5 | 6 | test('select.select()', async function (t) { 7 | await t.test('invalid selectors', async function (t) { 8 | await t.test('should throw without selector', async function () { 9 | assert.throws(function () { 10 | // @ts-expect-error check that a runtime error is thrown. 11 | select() 12 | }, /Error: Expected `string` as selector, not `undefined`/) 13 | }) 14 | 15 | await t.test('should throw w/ invalid selector (1)', async function () { 16 | assert.throws(function () { 17 | // @ts-expect-error check that a runtime error is thrown. 18 | select([], u('a')) 19 | }, /Error: Expected `string` as selector, not ``/) 20 | }) 21 | 22 | await t.test('should throw w/ invalid selector (2)', async function () { 23 | assert.throws(function () { 24 | select('@supports (transform-origin: 5% 5%) {}', u('a')) 25 | }, /Expected rule but "@" found/) 26 | }) 27 | 28 | await t.test( 29 | 'should throw on invalid attribute operators', 30 | async function () { 31 | assert.throws(function () { 32 | select('[foo%=bar]', u('a')) 33 | }, /Expected a valid attribute selector operator/) 34 | } 35 | ) 36 | 37 | await t.test('should throw on invalid pseudo classes', async function () { 38 | assert.throws(function () { 39 | select(':active', u('a')) 40 | }, /Error: Unknown pseudo-selector `active`/) 41 | }) 42 | 43 | await t.test( 44 | 'should throw on invalid pseudo class “functions”', 45 | async function () { 46 | assert.throws(function () { 47 | select(':nth-foo(2n+1)', u('a')) 48 | }, /Unknown pseudo-class/) 49 | } 50 | ) 51 | 52 | await t.test('should throw on invalid pseudo elements', async function () { 53 | assert.throws(function () { 54 | select('::before', u('a')) 55 | }, /Invalid selector: `::before`/) 56 | }) 57 | }) 58 | 59 | await t.test('general', async function (t) { 60 | await t.test( 61 | 'should throw for the empty string as selector', 62 | async function () { 63 | assert.throws(function () { 64 | select('', u('a')) 65 | }, /Expected rule but end of input reached/) 66 | } 67 | ) 68 | 69 | await t.test( 70 | 'should throw for a white-space only selector', 71 | async function () { 72 | assert.throws(function () { 73 | select(' ', u('a')) 74 | }, /Expected rule but end of input reached/) 75 | } 76 | ) 77 | 78 | await t.test('should yield nothing if not given a node', async function () { 79 | assert.equal(select('*'), undefined) 80 | }) 81 | 82 | await t.test('should yield the node if given a node', async function () { 83 | assert.deepEqual(select('*', u('a')), u('a')) 84 | }) 85 | }) 86 | 87 | await t.test('descendant selector', async function (t) { 88 | await t.test('should return the first descendant node', async function () { 89 | assert.deepEqual( 90 | select( 91 | 'b', 92 | u('a', [ 93 | u('b', {x: 1}), 94 | u('c', [u('b', {x: 2}), u('d', u('b', {x: 3}))]) 95 | ]) 96 | ), 97 | u('b', {x: 1}) 98 | ) 99 | }) 100 | 101 | await t.test( 102 | 'should return the given node if it matches', 103 | async function () { 104 | assert.deepEqual(select('a', u('a', {c: 1})), u('a', {c: 1})) 105 | } 106 | ) 107 | 108 | await t.test('should return the first match', async function () { 109 | assert.deepEqual( 110 | select( 111 | 'b', 112 | u('a', [ 113 | u('b', {x: 1}, [u('b', {x: 2}), u('b', {x: 3}, [u('b', {x: 4})])]) 114 | ]) 115 | ), 116 | u('b', {x: 1}, [u('b', {x: 2}), u('b', {x: 3}, [u('b', {x: 4})])]) 117 | ) 118 | }) 119 | 120 | await t.test('should return deep matches', async function () { 121 | assert.deepEqual( 122 | select('a c d', u('a', [u('b', [u('c', [u('d', [u('d')])])])])), 123 | u('d', [u('d')]) 124 | ) 125 | }) 126 | }) 127 | 128 | await t.test('child selector', async function (t) { 129 | await t.test('should return child nodes', async function () { 130 | assert.deepEqual( 131 | select('c > e', u('a', [u('b'), u('c', [u('d'), u('e', [u('f')])])])), 132 | u('e', [u('f')]) 133 | ) 134 | }) 135 | 136 | await t.test( 137 | 'should return matches with nested matches', 138 | async function () { 139 | assert.deepEqual( 140 | select( 141 | 'b > b', 142 | u('a', [ 143 | u('b', {x: 1}, [u('b', {x: 2}), u('b', {x: 3}, [u('b', {x: 4})])]) 144 | ]) 145 | ), 146 | u('b', {x: 2}) 147 | ) 148 | } 149 | ) 150 | 151 | await t.test('should return deep matches', async function () { 152 | assert.deepEqual( 153 | select('b > c > d', u('a', [u('b', [u('c', [u('d', [u('d')])])])])), 154 | u('d', [u('d')]) 155 | ) 156 | }) 157 | }) 158 | 159 | await t.test('adjacent sibling selector', async function (t) { 160 | await t.test('should return adjacent sibling', async function () { 161 | assert.deepEqual( 162 | select( 163 | 'c + b', 164 | u('a', [ 165 | u('b', 'Alpha'), 166 | u('c', 'Bravo'), 167 | u('b', 'Charlie'), 168 | u('b', 'Delta'), 169 | u('d', [u('e', 'Echo')]) 170 | ]) 171 | ), 172 | u('b', 'Charlie') 173 | ) 174 | }) 175 | 176 | await t.test('should return nothing without matches', async function () { 177 | assert.equal( 178 | select( 179 | 'c + b', 180 | u('a', [ 181 | u('b', 'Alpha'), 182 | u('c', 'Bravo'), 183 | u('d', 'Charlie'), 184 | u('b', 'Delta') 185 | ]) 186 | ), 187 | undefined 188 | ) 189 | }) 190 | }) 191 | 192 | await t.test('general sibling selector', async function (t) { 193 | await t.test('should return the first adjacent sibling', async function () { 194 | assert.deepEqual( 195 | select( 196 | 'c ~ b', 197 | u('a', [ 198 | u('b', 'Alpha'), 199 | u('c', 'Bravo'), 200 | u('b', 'Charlie'), 201 | u('b', 'Delta'), 202 | u('d', [u('e', 'Echo')]) 203 | ]) 204 | ), 205 | u('b', 'Charlie') 206 | ) 207 | }) 208 | 209 | await t.test('should return future siblings', async function () { 210 | assert.deepEqual( 211 | select( 212 | 'c ~ b', 213 | u('a', [ 214 | u('b', 'Alpha'), 215 | u('c', 'Bravo'), 216 | u('d', 'Charlie'), 217 | u('b', 'Delta') 218 | ]) 219 | ), 220 | u('b', 'Delta') 221 | ) 222 | }) 223 | 224 | await t.test('should return nothing without matches', async function () { 225 | assert.equal( 226 | select( 227 | 'c ~ b', 228 | u('a', [u('b', 'Alpha'), u('c', 'Bravo'), u('d', 'Charlie')]) 229 | ), 230 | undefined 231 | ) 232 | }) 233 | }) 234 | 235 | await t.test('parent-sensitive pseudo-selectors', async function (t) { 236 | await t.test(':first-child', async function (t) { 237 | await t.test('should return the first child', async function () { 238 | assert.deepEqual( 239 | select( 240 | ':first-child', 241 | u('a', [ 242 | u('b', 'Alpha'), 243 | u('c', 'Bravo'), 244 | u('b', 'Charlie'), 245 | u('b', 'Delta'), 246 | u('d', [u('e', 'Echo')]) 247 | ]) 248 | ), 249 | u('b', 'Alpha') 250 | ) 251 | }) 252 | 253 | await t.test( 254 | 'should return nothing if nothing matches', 255 | async function () { 256 | assert.equal( 257 | select( 258 | 'c:first-child', 259 | u('a', [u('b', 'Alpha'), u('c', 'Bravo'), u('d', 'Charlie')]) 260 | ), 261 | undefined 262 | ) 263 | } 264 | ) 265 | }) 266 | 267 | await t.test(':last-child', async function (t) { 268 | await t.test('should return the last child', async function () { 269 | assert.deepEqual( 270 | select( 271 | ':last-child', 272 | u('a', [ 273 | u('b', 'Alpha'), 274 | u('c', 'Bravo'), 275 | u('b', 'Charlie'), 276 | u('b', 'Delta'), 277 | u('d', [u('e', 'Echo')]) 278 | ]) 279 | ), 280 | u('d', [u('e', 'Echo')]) 281 | ) 282 | }) 283 | 284 | await t.test( 285 | 'should return nothing if nothing matches', 286 | async function () { 287 | assert.equal( 288 | select( 289 | 'c:last-child', 290 | u('a', [u('b', 'Alpha'), u('c', 'Bravo'), u('d', 'Charlie')]) 291 | ), 292 | undefined 293 | ) 294 | } 295 | ) 296 | }) 297 | 298 | await t.test(':only-child', async function (t) { 299 | await t.test('should return an only child', async function () { 300 | assert.deepEqual( 301 | select( 302 | ':only-child', 303 | u('a', [ 304 | u('b', 'Alpha'), 305 | u('c', 'Bravo'), 306 | u('b', 'Charlie'), 307 | u('b', 'Delta'), 308 | u('d', [u('b', 'Echo')]) 309 | ]) 310 | ), 311 | u('b', 'Echo') 312 | ) 313 | }) 314 | 315 | await t.test( 316 | 'should return nothing if nothing matches', 317 | async function () { 318 | assert.equal( 319 | select( 320 | 'c:only-child', 321 | u('a', [u('b', 'Alpha'), u('c', 'Bravo'), u('d', 'Charlie')]) 322 | ), 323 | undefined 324 | ) 325 | } 326 | ) 327 | }) 328 | 329 | await t.test(':nth-child', async function (t) { 330 | await t.test( 331 | 'should return the match for `:nth-child(odd)`', 332 | async function () { 333 | assert.deepEqual( 334 | select( 335 | 'b:nth-child(odd)', 336 | u('a', [ 337 | u('b', 'Alpha'), 338 | u('b', 'Bravo'), 339 | u('b', 'Charlie'), 340 | u('b', 'Delta'), 341 | u('b', 'Echo'), 342 | u('b', 'Foxtrot') 343 | ]) 344 | ), 345 | u('b', 'Alpha') 346 | ) 347 | } 348 | ) 349 | 350 | await t.test( 351 | 'should return the match for `:nth-child(2n+1)`', 352 | async function () { 353 | assert.deepEqual( 354 | select( 355 | 'b:nth-child(2n+1)', 356 | u('a', [ 357 | u('b', 'Alpha'), 358 | u('b', 'Bravo'), 359 | u('b', 'Charlie'), 360 | u('b', 'Delta'), 361 | u('b', 'Echo'), 362 | u('b', 'Foxtrot') 363 | ]) 364 | ), 365 | u('b', 'Alpha') 366 | ) 367 | } 368 | ) 369 | 370 | await t.test( 371 | 'should return the match for `:nth-child(even)`', 372 | async function () { 373 | assert.deepEqual( 374 | select( 375 | 'b:nth-child(even)', 376 | u('a', [ 377 | u('b', 'Alpha'), 378 | u('b', 'Bravo'), 379 | u('b', 'Charlie'), 380 | u('b', 'Delta'), 381 | u('b', 'Echo'), 382 | u('b', 'Foxtrot') 383 | ]) 384 | ), 385 | u('b', 'Bravo') 386 | ) 387 | } 388 | ) 389 | 390 | await t.test( 391 | 'should return the match for `:nth-child(2n+0)`', 392 | async function () { 393 | assert.deepEqual( 394 | select( 395 | 'b:nth-child(2n+0)', 396 | u('a', [ 397 | u('b', 'Alpha'), 398 | u('b', 'Bravo'), 399 | u('b', 'Charlie'), 400 | u('b', 'Delta'), 401 | u('b', 'Echo'), 402 | u('b', 'Foxtrot') 403 | ]) 404 | ), 405 | u('b', 'Bravo') 406 | ) 407 | } 408 | ) 409 | }) 410 | 411 | await t.test(':nth-last-child', async function (t) { 412 | await t.test( 413 | 'should return the last match for `:nth-last-child(odd)`', 414 | async function () { 415 | assert.deepEqual( 416 | select( 417 | 'b:nth-last-child(odd)', 418 | u('a', [ 419 | u('b', 'Alpha'), 420 | u('b', 'Bravo'), 421 | u('b', 'Charlie'), 422 | u('b', 'Delta'), 423 | u('b', 'Echo'), 424 | u('b', 'Foxtrot') 425 | ]) 426 | ), 427 | u('b', 'Bravo') 428 | ) 429 | } 430 | ) 431 | 432 | await t.test( 433 | 'should return the last match for `:nth-last-child(2n+1)`', 434 | async function () { 435 | assert.deepEqual( 436 | select( 437 | 'b:nth-last-child(2n+1)', 438 | u('a', [ 439 | u('b', 'Alpha'), 440 | u('b', 'Bravo'), 441 | u('b', 'Charlie'), 442 | u('b', 'Delta'), 443 | u('b', 'Echo'), 444 | u('b', 'Foxtrot') 445 | ]) 446 | ), 447 | u('b', 'Bravo') 448 | ) 449 | } 450 | ) 451 | 452 | await t.test( 453 | 'should return the last match for `:nth-last-child(even)`', 454 | async function () { 455 | assert.deepEqual( 456 | select( 457 | 'b:nth-last-child(even)', 458 | u('a', [ 459 | u('b', 'Alpha'), 460 | u('b', 'Bravo'), 461 | u('b', 'Charlie'), 462 | u('b', 'Delta'), 463 | u('b', 'Echo'), 464 | u('b', 'Foxtrot') 465 | ]) 466 | ), 467 | u('b', 'Alpha') 468 | ) 469 | } 470 | ) 471 | 472 | await t.test( 473 | 'should return the last match for `:nth-last-child(2n+0)`', 474 | async function () { 475 | assert.deepEqual( 476 | select( 477 | 'b:nth-last-child(2n+0)', 478 | u('a', [ 479 | u('b', 'Alpha'), 480 | u('b', 'Bravo'), 481 | u('b', 'Charlie'), 482 | u('b', 'Delta'), 483 | u('b', 'Echo'), 484 | u('b', 'Foxtrot') 485 | ]) 486 | ), 487 | u('b', 'Alpha') 488 | ) 489 | } 490 | ) 491 | }) 492 | 493 | await t.test(':nth-of-type', async function (t) { 494 | await t.test( 495 | 'should return the first match for `:nth-of-type(odd)`', 496 | async function () { 497 | assert.deepEqual( 498 | select( 499 | 'b:nth-of-type(odd)', 500 | u('a', [ 501 | u('b', 'Alpha'), 502 | u('c', 'Bravo'), 503 | u('b', 'Charlie'), 504 | u('c', 'Delta'), 505 | u('b', 'Echo'), 506 | u('c', 'Foxtrot') 507 | ]) 508 | ), 509 | u('b', 'Alpha') 510 | ) 511 | } 512 | ) 513 | 514 | await t.test( 515 | 'should return the first match for `:nth-of-type(2n+1)`', 516 | async function () { 517 | assert.deepEqual( 518 | select( 519 | 'b:nth-of-type(2n+1)', 520 | u('a', [ 521 | u('b', 'Alpha'), 522 | u('c', 'Bravo'), 523 | u('b', 'Charlie'), 524 | u('c', 'Delta'), 525 | u('b', 'Echo'), 526 | u('c', 'Foxtrot') 527 | ]) 528 | ), 529 | u('b', 'Alpha') 530 | ) 531 | } 532 | ) 533 | 534 | await t.test( 535 | 'should return the first match for `:nth-of-type(even)`', 536 | async function () { 537 | assert.deepEqual( 538 | select( 539 | 'b:nth-of-type(even)', 540 | u('a', [ 541 | u('b', 'Alpha'), 542 | u('c', 'Bravo'), 543 | u('b', 'Charlie'), 544 | u('c', 'Delta'), 545 | u('b', 'Echo'), 546 | u('c', 'Foxtrot') 547 | ]) 548 | ), 549 | u('b', 'Charlie') 550 | ) 551 | } 552 | ) 553 | 554 | await t.test( 555 | 'should return the first match for `:nth-of-type(2n+0)`', 556 | async function () { 557 | assert.deepEqual( 558 | select( 559 | 'b:nth-of-type(2n+0)', 560 | u('a', [ 561 | u('b', 'Alpha'), 562 | u('c', 'Bravo'), 563 | u('b', 'Charlie'), 564 | u('c', 'Delta'), 565 | u('b', 'Echo'), 566 | u('c', 'Foxtrot') 567 | ]) 568 | ), 569 | u('b', 'Charlie') 570 | ) 571 | } 572 | ) 573 | }) 574 | 575 | await t.test(':nth-last-of-type', async function (t) { 576 | await t.test( 577 | 'should return the last match for `:nth-last-of-type(odd)`s', 578 | async function () { 579 | assert.deepEqual( 580 | select( 581 | 'b:nth-last-of-type(odd)', 582 | u('a', [ 583 | u('b', 'Alpha'), 584 | u('c', 'Bravo'), 585 | u('b', 'Charlie'), 586 | u('c', 'Delta'), 587 | u('b', 'Echo'), 588 | u('c', 'Foxtrot') 589 | ]) 590 | ), 591 | u('b', 'Alpha') 592 | ) 593 | } 594 | ) 595 | 596 | await t.test( 597 | 'should return the last match for `:nth-last-of-type(2n+1)`', 598 | async function () { 599 | assert.deepEqual( 600 | select( 601 | 'b:nth-last-of-type(2n+1)', 602 | u('a', [ 603 | u('b', 'Alpha'), 604 | u('c', 'Bravo'), 605 | u('b', 'Charlie'), 606 | u('c', 'Delta'), 607 | u('b', 'Echo'), 608 | u('c', 'Foxtrot') 609 | ]) 610 | ), 611 | u('b', 'Alpha') 612 | ) 613 | } 614 | ) 615 | 616 | await t.test( 617 | 'should return the last match for `:nth-last-of-type(even)`', 618 | async function () { 619 | assert.deepEqual( 620 | select( 621 | 'b:nth-last-of-type(even)', 622 | u('a', [ 623 | u('b', 'Alpha'), 624 | u('c', 'Bravo'), 625 | u('b', 'Charlie'), 626 | u('c', 'Delta'), 627 | u('b', 'Echo'), 628 | u('c', 'Foxtrot') 629 | ]) 630 | ), 631 | u('b', 'Charlie') 632 | ) 633 | } 634 | ) 635 | 636 | await t.test( 637 | 'should return the last match for `:nth-last-of-type(2n+0)`', 638 | async function () { 639 | assert.deepEqual( 640 | select( 641 | 'b:nth-last-of-type(2n+0)', 642 | u('a', [ 643 | u('b', 'Alpha'), 644 | u('c', 'Bravo'), 645 | u('b', 'Charlie'), 646 | u('c', 'Delta'), 647 | u('b', 'Echo'), 648 | u('c', 'Foxtrot') 649 | ]) 650 | ), 651 | u('b', 'Charlie') 652 | ) 653 | } 654 | ) 655 | }) 656 | 657 | await t.test(':first-of-type', async function (t) { 658 | await t.test( 659 | 'should return the first match for `:first-of-type`', 660 | async function () { 661 | assert.deepEqual( 662 | select( 663 | 'b:first-of-type', 664 | u('a', [ 665 | u('b', 'Alpha'), 666 | u('c', 'Bravo'), 667 | u('b', 'Charlie'), 668 | u('c', 'Delta'), 669 | u('b', 'Echo'), 670 | u('c', 'Foxtrot') 671 | ]) 672 | ), 673 | u('b', 'Alpha') 674 | ) 675 | } 676 | ) 677 | 678 | await t.test('should return nothing without matches', async function () { 679 | assert.equal(select('b:first-of-type', u('a', [])), undefined) 680 | }) 681 | }) 682 | 683 | await t.test(':last-of-type', async function (t) { 684 | await t.test( 685 | 'should return the last match for `:last-of-type`s', 686 | async function () { 687 | assert.deepEqual( 688 | select( 689 | 'b:last-of-type', 690 | u('a', [ 691 | u('b', 'Alpha'), 692 | u('c', 'Bravo'), 693 | u('b', 'Charlie'), 694 | u('c', 'Delta'), 695 | u('b', 'Echo'), 696 | u('c', 'Foxtrot') 697 | ]) 698 | ), 699 | u('b', 'Echo') 700 | ) 701 | } 702 | ) 703 | 704 | await t.test('should return nothing without matches', async function () { 705 | assert.equal(select('b:last-of-type', u('a', [])), undefined) 706 | }) 707 | }) 708 | 709 | await t.test(':only-of-type', async function (t) { 710 | await t.test('should return the only match', async function () { 711 | assert.deepEqual( 712 | select( 713 | 'c:only-of-type', 714 | u('a', [ 715 | u('b', 'Alpha'), 716 | u('b', 'Bravo'), 717 | u('c', 'Charlie'), 718 | u('b', 'Delta') 719 | ]) 720 | ), 721 | u('c', 'Charlie') 722 | ) 723 | }) 724 | 725 | await t.test( 726 | 'should return nothing with too many matches', 727 | async function () { 728 | assert.equal( 729 | select( 730 | 'b:only-of-type', 731 | u('a', [ 732 | u('b', 'Alpha'), 733 | u('c', 'Bravo'), 734 | u('b', 'Charlie'), 735 | u('c', 'Delta'), 736 | u('b', 'Echo'), 737 | u('c', 'Foxtrot') 738 | ]) 739 | ), 740 | undefined 741 | ) 742 | } 743 | ) 744 | 745 | await t.test('should return nothing without matches', async function () { 746 | assert.equal(select('b:only-of-type', u('a', [])), undefined) 747 | }) 748 | }) 749 | 750 | await t.test(':root', async function (t) { 751 | await t.test('should return the given node', async function () { 752 | assert.deepEqual( 753 | select(':root', u('a', [u('b'), u('c', [u('d')])])), 754 | u('a', [u('b'), u('c', [u('d')])]) 755 | ) 756 | }) 757 | }) 758 | 759 | await t.test(':scope', async function (t) { 760 | await t.test('should return the given node', async function () { 761 | assert.deepEqual( 762 | select(':scope', u('a', [u('b'), u('c', [u('d')])])), 763 | u('a', [u('b'), u('c', [u('d')])]) 764 | ) 765 | }) 766 | }) 767 | 768 | await t.test(':has', async function (t) { 769 | await t.test('should select a node', async function () { 770 | assert.deepEqual( 771 | select('c:has(:first-child)', u('a', [u('b'), u('c', [u('d')])])), 772 | u('c', [u('d')]) 773 | ) 774 | }) 775 | }) 776 | }) 777 | 778 | await t.test(':is', async function (t) { 779 | await t.test('should support parent-sensitive `:is`', async function () { 780 | assert.deepEqual( 781 | select('y:is(:first-child)', u('x', [u('y', 'a'), u('y', 'b')])), 782 | u('y', 'a') 783 | ) 784 | }) 785 | }) 786 | 787 | await t.test(':not', async function (t) { 788 | await t.test('should support parent-sensitive `:not`', async function () { 789 | assert.deepEqual( 790 | select('y:not(:first-child)', u('x', [u('y', 'a'), u('y', 'b')])), 791 | u('y', 'b') 792 | ) 793 | }) 794 | }) 795 | }) 796 | -------------------------------------------------------------------------------- /test/select-all.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict' 2 | import test from 'node:test' 3 | import {u} from 'unist-builder' 4 | import {selectAll} from 'unist-util-select' 5 | 6 | test('select.selectAll()', async function (t) { 7 | await t.test('invalid selectors', async function (t) { 8 | await t.test('should throw without selector', async function () { 9 | assert.throws(function () { 10 | // @ts-expect-error check that a runtime error is thrown. 11 | selectAll() 12 | }, /Error: Expected `string` as selector, not `undefined`/) 13 | }) 14 | 15 | await t.test('should throw w/ invalid selector (1)', async function () { 16 | assert.throws(function () { 17 | // @ts-expect-error check that a runtime error is thrown. 18 | selectAll([], u('a')) 19 | }, /Error: Expected `string` as selector, not ``/) 20 | }) 21 | 22 | await t.test('should throw w/ invalid selector (2)', async function () { 23 | assert.throws(function () { 24 | selectAll('@supports (transform-origin: 5% 5%) {}', u('a')) 25 | }, /Expected rule but "@" found/) 26 | }) 27 | 28 | await t.test( 29 | 'should throw on invalid attribute operators', 30 | async function () { 31 | assert.throws(function () { 32 | selectAll('[foo%=bar]', u('a')) 33 | }, /Expected a valid attribute selector operator/) 34 | } 35 | ) 36 | 37 | await t.test('should throw on invalid pseudo classes', async function () { 38 | assert.throws(function () { 39 | selectAll(':active', u('a')) 40 | }, /Error: Unknown pseudo-selector `active`/) 41 | }) 42 | 43 | await t.test( 44 | 'should throw on invalid pseudo class “functions”', 45 | async function () { 46 | assert.throws(function () { 47 | selectAll(':nth-foo(2n+1)', u('a')) 48 | }, /Unknown pseudo-class/) 49 | } 50 | ) 51 | 52 | await t.test('should throw on invalid pseudo elements', async function () { 53 | assert.throws(function () { 54 | selectAll('::before', u('a')) 55 | }, /Invalid selector: `::before`/) 56 | }) 57 | }) 58 | 59 | await t.test('general', async function (t) { 60 | await t.test( 61 | 'should throw for the empty string as selector', 62 | async function () { 63 | assert.throws(function () { 64 | selectAll('', u('a')) 65 | }, /Expected rule but end of input reached/) 66 | } 67 | ) 68 | 69 | await t.test( 70 | 'should throw for a white-space only selector', 71 | async function () { 72 | assert.throws(function () { 73 | selectAll(' ', u('a')) 74 | }, /Expected rule but end of input reached/) 75 | } 76 | ) 77 | 78 | await t.test('should yield nothing if not given a node', async function () { 79 | assert.deepEqual(selectAll('*'), []) 80 | }) 81 | 82 | await t.test('should yield the node if given a node', async function () { 83 | assert.deepEqual(selectAll('*', u('a')), [u('a')]) 84 | }) 85 | }) 86 | 87 | await t.test('descendant selector', async function (t) { 88 | await t.test('should return descendant nodes', async function () { 89 | assert.deepEqual( 90 | selectAll( 91 | 'b', 92 | u('a', [ 93 | u('b', 'Alpha'), 94 | u('c', [u('b', 'Bravo'), u('d', u('b', 'Charlie'))]) 95 | ]) 96 | ), 97 | [u('b', 'Alpha'), u('b', 'Bravo'), u('b', 'Charlie')] 98 | ) 99 | }) 100 | 101 | await t.test( 102 | 'should return the given node if it matches', 103 | async function () { 104 | assert.deepEqual(selectAll('a', u('a', 'Alpha')), [u('a', 'Alpha')]) 105 | } 106 | ) 107 | 108 | await t.test( 109 | 'should return matches with nested matches', 110 | async function () { 111 | assert.deepEqual( 112 | selectAll( 113 | 'b', 114 | u('a', [ 115 | u('b', {x: 1}, [u('b', {x: 2}), u('b', {x: 3}, [u('b', {x: 4})])]) 116 | ]) 117 | ), 118 | [ 119 | u('b', {x: 1}, [u('b', {x: 2}), u('b', {x: 3}, [u('b', {x: 4})])]), 120 | u('b', {x: 2}), 121 | u('b', {x: 3}, [u('b', {x: 4})]), 122 | u('b', {x: 4}) 123 | ] 124 | ) 125 | } 126 | ) 127 | 128 | await t.test('should return deep matches', async function () { 129 | assert.deepEqual( 130 | selectAll('b c d', u('a', [u('b', [u('c', [u('d', [u('d')])])])])), 131 | [u('d', [u('d')]), u('d')] 132 | ) 133 | }) 134 | 135 | await t.test('should not match outside other matches', async function () { 136 | assert.deepEqual( 137 | selectAll( 138 | 'b c', 139 | u('a', [u('b', [u('c', '1')]), u('d', [u('c', '2')])]) 140 | ), 141 | [u('c', '1')] 142 | ) 143 | }) 144 | }) 145 | 146 | await t.test('child selector', async function (t) { 147 | await t.test('should return child nodes', async function () { 148 | assert.deepEqual( 149 | selectAll( 150 | 'c > d', 151 | u('a', [ 152 | u('b', {x: 1}), 153 | u('c', [u('b', {x: 2}), u('d', [u('b', {x: 3})])]) 154 | ]) 155 | ), 156 | [u('d', [u('b', {x: 3})])] 157 | ) 158 | }) 159 | 160 | await t.test( 161 | 'should return matches with nested matches', 162 | async function () { 163 | assert.deepEqual( 164 | selectAll( 165 | 'b > b', 166 | u('a', [ 167 | u('b', {x: 1}, [u('b', {x: 2}), u('b', {x: 3}, [u('b', {x: 4})])]) 168 | ]) 169 | ), 170 | [u('b', {x: 2}), u('b', {x: 3}, [u('b', {x: 4})]), u('b', {x: 4})] 171 | ) 172 | } 173 | ) 174 | 175 | await t.test('should return deep matches', async function () { 176 | assert.deepEqual( 177 | selectAll('b > c > d', u('a', [u('b', [u('c', [u('d', [u('d')])])])])), 178 | [u('d', [u('d')])] 179 | ) 180 | }) 181 | }) 182 | 183 | await t.test('adjacent sibling selector', async function (t) { 184 | await t.test('should return adjacent sibling', async function () { 185 | assert.deepEqual( 186 | selectAll( 187 | 'c + b', 188 | u('a', [ 189 | u('b', 'Alpha'), 190 | u('c', 'Bravo'), 191 | u('b', 'Charlie'), 192 | u('b', 'Delta'), 193 | u('d', [u('b', 'Echo')]) 194 | ]) 195 | ), 196 | [u('b', 'Charlie')] 197 | ) 198 | }) 199 | 200 | await t.test('should return nothing without matches', async function () { 201 | assert.deepEqual( 202 | selectAll( 203 | 'c + b', 204 | u('a', [ 205 | u('b', 'Alpha'), 206 | u('c', 'Bravo'), 207 | u('d', 'Charlie'), 208 | u('b', 'Delta') 209 | ]) 210 | ), 211 | [] 212 | ) 213 | }) 214 | }) 215 | 216 | await t.test('general sibling selector', async function (t) { 217 | await t.test('should return adjacent sibling', async function () { 218 | assert.deepEqual( 219 | selectAll( 220 | 'c ~ b', 221 | u('a', [ 222 | u('b', 'Alpha'), 223 | u('c', 'Bravo'), 224 | u('b', 'Charlie'), 225 | u('b', 'Delta'), 226 | u('d', [u('b', 'Echo')]) 227 | ]) 228 | ), 229 | [u('b', 'Charlie'), u('b', 'Delta')] 230 | ) 231 | }) 232 | 233 | await t.test('should return future siblings', async function () { 234 | assert.deepEqual( 235 | selectAll( 236 | 'c ~ b', 237 | u('a', [ 238 | u('b', 'Alpha'), 239 | u('c', 'Bravo'), 240 | u('d', 'Charlie'), 241 | u('b', 'Delta') 242 | ]) 243 | ), 244 | [u('b', 'Delta')] 245 | ) 246 | }) 247 | 248 | await t.test('should return nothing without matches', async function () { 249 | assert.deepEqual( 250 | selectAll( 251 | 'c ~ b', 252 | u('a', [u('b', 'Alpha'), u('c', 'Bravo'), u('d', 'Charlie')]) 253 | ), 254 | [] 255 | ) 256 | }) 257 | }) 258 | 259 | await t.test('parent-sensitive pseudo-selectors', async function (t) { 260 | await t.test(':first-child', async function (t) { 261 | await t.test('should return all `:first-child`s (1)', async function () { 262 | assert.deepEqual( 263 | selectAll( 264 | ':first-child', 265 | u('a', [ 266 | u('b', 'Alpha'), 267 | u('c', 'Bravo'), 268 | u('b', 'Charlie'), 269 | u('b', 'Delta'), 270 | u('d', [u('b', 'Echo')]) 271 | ]) 272 | ), 273 | [u('b', 'Alpha'), u('b', 'Echo')] 274 | ) 275 | }) 276 | 277 | await t.test('should return all `:first-child`s (2)', async function () { 278 | assert.deepEqual( 279 | selectAll( 280 | 'b:first-child', 281 | u('a', [u('b', 'Alpha'), u('c', 'Bravo'), u('b', 'Charlie')]) 282 | ), 283 | [u('b', 'Alpha')] 284 | ) 285 | }) 286 | 287 | await t.test( 288 | 'should return nothing if nothing matches', 289 | async function () { 290 | assert.deepEqual( 291 | selectAll( 292 | 'h1:first-child', 293 | u('a', [u('b', 'Alpha'), u('c', 'Bravo'), u('b', 'Charlie')]) 294 | ), 295 | [] 296 | ) 297 | } 298 | ) 299 | }) 300 | 301 | await t.test(':last-child', async function (t) { 302 | await t.test('should return all `:last-child`s (1)', async function () { 303 | assert.deepEqual( 304 | selectAll( 305 | ':last-child', 306 | u('a', [ 307 | u('b', 'Alpha'), 308 | u('c', 'Bravo'), 309 | u('b', 'Charlie'), 310 | u('b', 'Delta'), 311 | u('d', [u('b', 'Echo')]) 312 | ]) 313 | ), 314 | [u('d', [u('b', 'Echo')]), u('b', 'Echo')] 315 | ) 316 | }) 317 | 318 | await t.test('should return all `:last-child`s (2)', async function () { 319 | assert.deepEqual( 320 | selectAll( 321 | 'b:last-child', 322 | u('a', [u('b', 'Alpha'), u('c', 'Bravo'), u('b', 'Charlie')]) 323 | ), 324 | [u('b', 'Charlie')] 325 | ) 326 | }) 327 | 328 | await t.test( 329 | 'should return nothing if nothing matches', 330 | async function () { 331 | assert.deepEqual( 332 | selectAll( 333 | 'h1:last-child', 334 | u('a', [u('b', 'Alpha'), u('c', 'Bravo'), u('b', 'Charlie')]) 335 | ), 336 | [] 337 | ) 338 | } 339 | ) 340 | }) 341 | 342 | await t.test(':only-child', async function (t) { 343 | await t.test('should return all `:only-child`s', async function () { 344 | assert.deepEqual( 345 | selectAll( 346 | ':only-child', 347 | u('a', [ 348 | u('b', 'Alpha'), 349 | u('c', 'Bravo'), 350 | u('b', 'Charlie'), 351 | u('b', 'Delta'), 352 | u('d', [u('b', 'Echo')]) 353 | ]) 354 | ), 355 | [u('b', 'Echo')] 356 | ) 357 | }) 358 | 359 | await t.test( 360 | 'should return nothing if nothing matches', 361 | async function () { 362 | assert.deepEqual( 363 | selectAll( 364 | 'c:only-child', 365 | u('a', [u('b', 'Alpha'), u('c', 'Bravo'), u('b', 'Charlie')]) 366 | ), 367 | [] 368 | ) 369 | } 370 | ) 371 | }) 372 | 373 | await t.test(':nth-child', async function (t) { 374 | await t.test('should return all `:nth-child(odd)`s', async function () { 375 | assert.deepEqual( 376 | selectAll( 377 | 'b:nth-child(odd)', 378 | u('a', [ 379 | u('b', 'Alpha'), 380 | u('b', 'Bravo'), 381 | u('b', 'Charlie'), 382 | u('b', 'Delta'), 383 | u('b', 'Echo'), 384 | u('b', 'Foxtrot') 385 | ]) 386 | ), 387 | [u('b', 'Alpha'), u('b', 'Charlie'), u('b', 'Echo')] 388 | ) 389 | }) 390 | 391 | await t.test('should return all `:nth-child(2n+1)`s', async function () { 392 | assert.deepEqual( 393 | selectAll( 394 | 'b:nth-child(2n+1)', 395 | u('a', [ 396 | u('b', 'Alpha'), 397 | u('b', 'Bravo'), 398 | u('b', 'Charlie'), 399 | u('b', 'Delta'), 400 | u('b', 'Echo'), 401 | u('b', 'Foxtrot') 402 | ]) 403 | ), 404 | [u('b', 'Alpha'), u('b', 'Charlie'), u('b', 'Echo')] 405 | ) 406 | }) 407 | 408 | await t.test('should return all `:nth-child(even)`s', async function () { 409 | assert.deepEqual( 410 | selectAll( 411 | 'b:nth-child(even)', 412 | u('a', [ 413 | u('b', 'Alpha'), 414 | u('b', 'Bravo'), 415 | u('b', 'Charlie'), 416 | u('b', 'Delta'), 417 | u('b', 'Echo'), 418 | u('b', 'Foxtrot') 419 | ]) 420 | ), 421 | [u('b', 'Bravo'), u('b', 'Delta'), u('b', 'Foxtrot')] 422 | ) 423 | }) 424 | 425 | await t.test('should return all `:nth-child(2n+0)`s', async function () { 426 | assert.deepEqual( 427 | selectAll( 428 | 'b:nth-child(2n+0)', 429 | u('a', [ 430 | u('b', 'Alpha'), 431 | u('b', 'Bravo'), 432 | u('b', 'Charlie'), 433 | u('b', 'Delta'), 434 | u('b', 'Echo'), 435 | u('b', 'Foxtrot') 436 | ]) 437 | ), 438 | [u('b', 'Bravo'), u('b', 'Delta'), u('b', 'Foxtrot')] 439 | ) 440 | }) 441 | 442 | await t.test( 443 | 'should throw on unsupported `of` syntax', 444 | async function () { 445 | assert.throws(function () { 446 | selectAll(':nth-child(odd of a)', u('a')) 447 | }, /Expected `nth` formula, such as `even` or `2n\+1` \(`of` is not yet supported\)/) 448 | } 449 | ) 450 | }) 451 | 452 | await t.test(':nth-last-child', async function (t) { 453 | await t.test( 454 | 'should return all `:nth-last-child(odd)`s', 455 | async function () { 456 | assert.deepEqual( 457 | selectAll( 458 | 'b:nth-last-child(odd)', 459 | u('a', [ 460 | u('b', 'Alpha'), 461 | u('b', 'Bravo'), 462 | u('b', 'Charlie'), 463 | u('b', 'Delta'), 464 | u('b', 'Echo'), 465 | u('b', 'Foxtrot') 466 | ]) 467 | ), 468 | [u('b', 'Bravo'), u('b', 'Delta'), u('b', 'Foxtrot')] 469 | ) 470 | } 471 | ) 472 | 473 | await t.test( 474 | 'should return all `:nth-last-child(2n+1)`s', 475 | async function () { 476 | assert.deepEqual( 477 | selectAll( 478 | 'b:nth-last-child(2n+1)', 479 | u('a', [ 480 | u('b', 'Alpha'), 481 | u('b', 'Bravo'), 482 | u('b', 'Charlie'), 483 | u('b', 'Delta'), 484 | u('b', 'Echo'), 485 | u('b', 'Foxtrot') 486 | ]) 487 | ), 488 | [u('b', 'Bravo'), u('b', 'Delta'), u('b', 'Foxtrot')] 489 | ) 490 | } 491 | ) 492 | 493 | await t.test( 494 | 'should return all `:nth-last-child(even)`s', 495 | async function () { 496 | assert.deepEqual( 497 | selectAll( 498 | 'b:nth-last-child(even)', 499 | u('a', [ 500 | u('b', 'Alpha'), 501 | u('b', 'Bravo'), 502 | u('b', 'Charlie'), 503 | u('b', 'Delta'), 504 | u('b', 'Echo'), 505 | u('b', 'Foxtrot') 506 | ]) 507 | ), 508 | [u('b', 'Alpha'), u('b', 'Charlie'), u('b', 'Echo')] 509 | ) 510 | } 511 | ) 512 | 513 | await t.test( 514 | 'should return all `:nth-last-child(2n+0)`s', 515 | async function () { 516 | assert.deepEqual( 517 | selectAll( 518 | 'b:nth-last-child(2n+0)', 519 | u('a', [ 520 | u('b', 'Alpha'), 521 | u('b', 'Bravo'), 522 | u('b', 'Charlie'), 523 | u('b', 'Delta'), 524 | u('b', 'Echo'), 525 | u('b', 'Foxtrot') 526 | ]) 527 | ), 528 | [u('b', 'Alpha'), u('b', 'Charlie'), u('b', 'Echo')] 529 | ) 530 | } 531 | ) 532 | }) 533 | 534 | await t.test(':nth-of-type', async function (t) { 535 | await t.test('should return all `:nth-of-type(odd)`s', async function () { 536 | assert.deepEqual( 537 | selectAll( 538 | 'b:nth-of-type(odd)', 539 | u('a', [ 540 | u('b', 'Alpha'), 541 | u('c', 'Bravo'), 542 | u('b', 'Charlie'), 543 | u('c', 'Delta'), 544 | u('b', 'Echo'), 545 | u('c', 'Foxtrot') 546 | ]) 547 | ), 548 | [u('b', 'Alpha'), u('b', 'Echo')] 549 | ) 550 | }) 551 | 552 | await t.test( 553 | 'should return all `:nth-of-type(2n+1)`s', 554 | async function () { 555 | assert.deepEqual( 556 | selectAll( 557 | 'b:nth-of-type(2n+1)', 558 | u('a', [ 559 | u('b', 'Alpha'), 560 | u('c', 'Bravo'), 561 | u('b', 'Charlie'), 562 | u('c', 'Delta'), 563 | u('b', 'Echo'), 564 | u('c', 'Foxtrot') 565 | ]) 566 | ), 567 | [u('b', 'Alpha'), u('b', 'Echo')] 568 | ) 569 | } 570 | ) 571 | 572 | await t.test( 573 | 'should return all `:nth-of-type(even)`s', 574 | async function () { 575 | assert.deepEqual( 576 | selectAll( 577 | 'b:nth-of-type(even)', 578 | u('a', [ 579 | u('b', 'Alpha'), 580 | u('c', 'Bravo'), 581 | u('b', 'Charlie'), 582 | u('c', 'Delta'), 583 | u('b', 'Echo'), 584 | u('c', 'Foxtrot') 585 | ]) 586 | ), 587 | [u('b', 'Charlie')] 588 | ) 589 | } 590 | ) 591 | 592 | await t.test( 593 | 'should return all `:nth-of-type(2n+0)`s', 594 | async function () { 595 | assert.deepEqual( 596 | selectAll( 597 | 'b:nth-of-type(2n+0)', 598 | u('a', [ 599 | u('b', 'Alpha'), 600 | u('c', 'Bravo'), 601 | u('b', 'Charlie'), 602 | u('c', 'Delta'), 603 | u('b', 'Echo'), 604 | u('c', 'Foxtrot') 605 | ]) 606 | ), 607 | [u('b', 'Charlie')] 608 | ) 609 | } 610 | ) 611 | }) 612 | 613 | await t.test(':nth-last-of-type', async function (t) { 614 | await t.test( 615 | 'should return all `:nth-last-of-type(odd)`s', 616 | async function () { 617 | assert.deepEqual( 618 | selectAll( 619 | 'b:nth-last-of-type(odd)', 620 | u('a', [ 621 | u('b', 'Alpha'), 622 | u('c', 'Bravo'), 623 | u('b', 'Charlie'), 624 | u('c', 'Delta'), 625 | u('b', 'Echo'), 626 | u('c', 'Foxtrot') 627 | ]) 628 | ), 629 | [u('b', 'Alpha'), u('b', 'Echo')] 630 | ) 631 | } 632 | ) 633 | 634 | await t.test( 635 | 'should return all `:nth-last-of-type(2n+1)`s', 636 | async function () { 637 | assert.deepEqual( 638 | selectAll( 639 | 'b:nth-last-of-type(2n+1)', 640 | u('a', [ 641 | u('b', 'Alpha'), 642 | u('c', 'Bravo'), 643 | u('b', 'Charlie'), 644 | u('c', 'Delta'), 645 | u('b', 'Echo'), 646 | u('c', 'Foxtrot') 647 | ]) 648 | ), 649 | [u('b', 'Alpha'), u('b', 'Echo')] 650 | ) 651 | } 652 | ) 653 | 654 | await t.test( 655 | 'should return all `:nth-last-of-type(even)`s', 656 | async function () { 657 | assert.deepEqual( 658 | selectAll( 659 | 'b:nth-last-of-type(even)', 660 | u('a', [ 661 | u('b', 'Alpha'), 662 | u('c', 'Bravo'), 663 | u('b', 'Charlie'), 664 | u('c', 'Delta'), 665 | u('b', 'Echo'), 666 | u('c', 'Foxtrot') 667 | ]) 668 | ), 669 | [u('b', 'Charlie')] 670 | ) 671 | } 672 | ) 673 | 674 | await t.test( 675 | 'should return all `:nth-last-of-type(2n+0)`s', 676 | async function () { 677 | assert.deepEqual( 678 | selectAll( 679 | 'b:nth-last-of-type(2n+0)', 680 | u('a', [ 681 | u('b', 'Alpha'), 682 | u('c', 'Bravo'), 683 | u('b', 'Charlie'), 684 | u('c', 'Delta'), 685 | u('b', 'Echo'), 686 | u('c', 'Foxtrot') 687 | ]) 688 | ), 689 | [u('b', 'Charlie')] 690 | ) 691 | } 692 | ) 693 | }) 694 | 695 | await t.test(':first-of-type', async function (t) { 696 | await t.test('should return all `:first-of-type`s', async function () { 697 | assert.deepEqual( 698 | selectAll( 699 | 'b:first-of-type', 700 | u('a', [ 701 | u('b', 'Alpha'), 702 | u('c', 'Bravo'), 703 | u('b', 'Charlie'), 704 | u('c', 'Delta'), 705 | u('b', 'Echo'), 706 | u('c', 'Foxtrot') 707 | ]) 708 | ), 709 | [u('b', 'Alpha')] 710 | ) 711 | }) 712 | 713 | await t.test('should return nothing without matches', async function () { 714 | assert.deepEqual(selectAll('b:first-of-type', u('a', [])), []) 715 | }) 716 | }) 717 | 718 | await t.test(':last-of-type', async function (t) { 719 | await t.test('should return all `:last-of-type`s', async function () { 720 | assert.deepEqual( 721 | selectAll( 722 | 'b:last-of-type', 723 | u('a', [ 724 | u('b', 'Alpha'), 725 | u('c', 'Bravo'), 726 | u('b', 'Charlie'), 727 | u('c', 'Delta'), 728 | u('b', 'Echo'), 729 | u('c', 'Foxtrot') 730 | ]) 731 | ), 732 | [u('b', 'Echo')] 733 | ) 734 | }) 735 | 736 | await t.test('should return nothing without matches', async function () { 737 | assert.deepEqual(selectAll('b:last-of-type', u('a', [])), []) 738 | }) 739 | }) 740 | 741 | await t.test(':only-of-type', async function (t) { 742 | await t.test('should return the only type', async function () { 743 | assert.deepEqual( 744 | selectAll( 745 | 'c:only-of-type', 746 | u('a', [ 747 | u('b', 'Alpha'), 748 | u('b', 'Bravo'), 749 | u('c', 'Charlie'), 750 | u('b', 'Delta') 751 | ]) 752 | ), 753 | [u('c', 'Charlie')] 754 | ) 755 | }) 756 | 757 | await t.test( 758 | 'should return nothing with too many matches', 759 | async function () { 760 | assert.deepEqual( 761 | selectAll( 762 | 'b:only-of-type', 763 | u('a', [ 764 | u('b', 'Alpha'), 765 | u('c', 'Bravo'), 766 | u('b', 'Charlie'), 767 | u('c', 'Delta'), 768 | u('b', 'Echo'), 769 | u('c', 'Foxtrot') 770 | ]) 771 | ), 772 | [] 773 | ) 774 | } 775 | ) 776 | 777 | await t.test('should return nothing without matches', async function () { 778 | assert.deepEqual(selectAll('b:only-of-type', u('a', [])), []) 779 | }) 780 | }) 781 | 782 | await t.test(':root', async function (t) { 783 | await t.test('should return the given node', async function () { 784 | assert.deepEqual( 785 | selectAll(':root', u('a', [u('b'), u('c', [u('d')])])), 786 | [u('a', [u('b'), u('c', [u('d')])])] 787 | ) 788 | }) 789 | }) 790 | 791 | await t.test(':scope', async function (t) { 792 | await t.test('should return the given node', async function () { 793 | assert.deepEqual( 794 | selectAll(':scope', u('a', [u('b'), u('c', [u('d')])])), 795 | [u('a', [u('b'), u('c', [u('d')])])] 796 | ) 797 | }) 798 | }) 799 | 800 | await t.test(':has', async function (t) { 801 | await t.test('should select a node', async function () { 802 | assert.deepEqual( 803 | selectAll('c:has(:first-child)', u('a', [u('b'), u('c', [u('d')])])), 804 | [u('c', [u('d')])] 805 | ) 806 | }) 807 | }) 808 | }) 809 | 810 | await t.test(':is', async function (t) { 811 | await t.test('should support parent-sensitive `:is`', async function () { 812 | assert.deepEqual( 813 | selectAll('y:is(:first-child)', u('x', [u('y', 'a'), u('y', 'b')])), 814 | [u('y', 'a')] 815 | ) 816 | }) 817 | }) 818 | 819 | await t.test(':not', async function (t) { 820 | await t.test('should support parent-sensitive `:not`', async function () { 821 | assert.deepEqual( 822 | selectAll('y:not(:first-child)', u('x', [u('y', 'a'), u('y', 'b')])), 823 | [u('y', 'b')] 824 | ) 825 | }) 826 | }) 827 | }) 828 | -------------------------------------------------------------------------------- /test/matches.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('unist').Literal} Literal 3 | */ 4 | 5 | import assert from 'node:assert/strict' 6 | import test from 'node:test' 7 | import {u} from 'unist-builder' 8 | import {matches} from 'unist-util-select' 9 | 10 | test('select.matches()', async function (t) { 11 | await t.test('should work (1)', async function () { 12 | assert.equal(matches('*', u('root', [])), true) 13 | }) 14 | 15 | await t.test('should work (2)', async function () { 16 | assert.equal(matches('*', {type: 'a', children: []}), true) 17 | }) 18 | 19 | await t.test('invalid selector', async function (t) { 20 | await t.test('should throw without selector', async function () { 21 | assert.throws(function () { 22 | // @ts-expect-error check that a runtime error is thrown. 23 | matches() 24 | }, /Error: Expected `string` as selector, not `undefined`/) 25 | }) 26 | 27 | await t.test('should throw w/ invalid selector (1)', async function () { 28 | assert.throws(function () { 29 | // @ts-expect-error check that a runtime error is thrown. 30 | matches([], u('root', [])) 31 | }, /Error: Expected `string` as selector, not ``/) 32 | }) 33 | 34 | await t.test('should throw w/ invalid selector (2)', async function () { 35 | assert.throws(function () { 36 | matches('@supports (transform-origin: 5% 5%) {}', u('root', [])) 37 | }, /Expected rule but "@" found/) 38 | }) 39 | 40 | await t.test( 41 | 'should throw on invalid attribute operators', 42 | async function () { 43 | assert.throws(function () { 44 | matches('[foo%=bar]', u('root', [])) 45 | }, /Expected a valid attribute selector operator/) 46 | } 47 | ) 48 | 49 | await t.test('should throw on invalid pseudo classes', async function () { 50 | assert.throws(function () { 51 | matches(':active', u('root', [])) 52 | }, /Error: Unknown pseudo-selector `active`/) 53 | }) 54 | 55 | await t.test( 56 | 'should throw on invalid pseudo class “functions”', 57 | async function () { 58 | assert.throws(function () { 59 | matches(':nth-foo(2n+1)', u('root', [])) 60 | }, /Unknown pseudo-class: "nth-foo"/) 61 | } 62 | ) 63 | 64 | await t.test('should throw on invalid pseudo elements', async function () { 65 | assert.throws(function () { 66 | matches('::before', u('root', [])) 67 | }, /Invalid selector: `::before`/) 68 | }) 69 | 70 | await t.test( 71 | 'should throw on nested selectors (descendant)', 72 | async function () { 73 | assert.throws(function () { 74 | matches('foo bar', u('root', [])) 75 | }, /Error: Expected selector without nesting/) 76 | } 77 | ) 78 | 79 | await t.test( 80 | 'should throw on nested selectors (direct child)', 81 | async function () { 82 | assert.throws(function () { 83 | matches('foo > bar', u('root', [])) 84 | }, /Error: Expected selector without nesting/) 85 | } 86 | ) 87 | }) 88 | 89 | await t.test('parent-sensitive pseudo-selectors', async function (t) { 90 | const simplePseudos = [ 91 | 'first-child', 92 | 'first-of-type', 93 | 'last-child', 94 | 'last-of-type', 95 | 'only-child', 96 | 'only-of-type' 97 | ] 98 | 99 | for (const pseudo of simplePseudos) { 100 | await t.test('should throw on `' + pseudo + '`', async function () { 101 | assert.throws( 102 | function () { 103 | matches(':' + pseudo, u('root', [])) 104 | }, 105 | new RegExp('Error: Cannot use `:' + pseudo + '` without parent') 106 | ) 107 | }) 108 | } 109 | 110 | const functionalPseudos = [ 111 | 'nth-child', 112 | 'nth-last-child', 113 | 'nth-of-type', 114 | 'nth-last-of-type' 115 | ] 116 | 117 | for (const pseudo of functionalPseudos) { 118 | await t.test('should throw on `' + pseudo + '()`', async function () { 119 | assert.throws(function () { 120 | matches(':' + pseudo + '()', u('root', [])) 121 | }, /Formula parse error/) 122 | }) 123 | } 124 | }) 125 | 126 | await t.test('general', async function (t) { 127 | await t.test('should throw on empty selectors', async function () { 128 | assert.throws(function () { 129 | matches('', u('root', [])) 130 | }, /Expected rule but end of input reached/) 131 | }) 132 | 133 | await t.test( 134 | 'should throw for a white-space only selector', 135 | async function () { 136 | assert.throws(function () { 137 | matches(' ', u('root', [])) 138 | }, /Expected rule but end of input reached/) 139 | } 140 | ) 141 | 142 | await t.test( 143 | 'should return `false` if not given a node', 144 | async function () { 145 | assert.ok(!matches('*')) 146 | } 147 | ) 148 | 149 | await t.test('should return `true` if given an node', async function () { 150 | assert.ok(matches('*', {type: 'text', value: 'a'})) 151 | }) 152 | }) 153 | 154 | await t.test('multiple selectors', async function (t) { 155 | await t.test('should support a matching selector', async function () { 156 | assert.ok(matches('a, b', u('a'))) 157 | }) 158 | 159 | await t.test('should support a non-matching selector', async function () { 160 | assert.ok(!matches('b, c', u('a'))) 161 | }) 162 | }) 163 | 164 | await t.test('tag-names: `div`, `*`', async function (t) { 165 | await t.test('should yield `true` for `*`', async function () { 166 | assert.ok(matches('*', u('a'))) 167 | }) 168 | 169 | await t.test('should yield `true` if types matches', async function () { 170 | assert.ok(matches('b', u('b'))) 171 | }) 172 | 173 | await t.test( 174 | 'should yield `false` if types don’t matches', 175 | async function () { 176 | assert.ok(!matches('b', u('a'))) 177 | } 178 | ) 179 | }) 180 | 181 | await t.test('id: `#id`', async function (t) { 182 | await t.test('should throw with id selector', async function () { 183 | assert.throws(function () { 184 | matches('#one', u('a')) 185 | }, /Error: Invalid selector: id/) 186 | }) 187 | }) 188 | 189 | await t.test('class: `.class`', async function (t) { 190 | await t.test('should throw with class selector', async function () { 191 | assert.throws(function () { 192 | matches('.one', u('a')) 193 | }, /Error: Invalid selector: class/) 194 | }) 195 | }) 196 | 197 | await t.test('attributes, existence: `[attr]`', async function (t) { 198 | await t.test( 199 | 'should yield `true` if attribute exists (string)', 200 | async function () { 201 | assert.ok(matches('[foo]', u('a', {foo: 'alpha'}))) 202 | } 203 | ) 204 | 205 | await t.test( 206 | 'should yield `true` if attribute exists (number)', 207 | async function () { 208 | assert.ok(matches('[foo]', u('a', {foo: 0}))) 209 | } 210 | ) 211 | 212 | await t.test( 213 | 'should yield `true` if attribute exists (array)', 214 | async function () { 215 | assert.ok(matches('[foo]', u('a', {foo: []}))) 216 | } 217 | ) 218 | 219 | await t.test( 220 | 'should yield `true` if attribute exists (object)', 221 | async function () { 222 | assert.ok(matches('[foo]', u('a', {foo: {}}))) 223 | } 224 | ) 225 | 226 | await t.test( 227 | 'should yield `false` if attribute does not exists', 228 | async function () { 229 | assert.ok(!matches('[foo]', u('a', {bar: 'bravo'}))) 230 | } 231 | ) 232 | 233 | await t.test( 234 | 'should yield `false` if attribute does not exists (null)', 235 | async function () { 236 | assert.ok(!matches('[foo]', u('a', {foo: null}))) 237 | } 238 | ) 239 | 240 | await t.test( 241 | 'should yield `false` if attribute does not exists (undefined)', 242 | async function () { 243 | assert.ok(!matches('[foo]', u('a', {foo: undefined}))) 244 | } 245 | ) 246 | }) 247 | 248 | await t.test('attributes, equality: `[attr=value]`', async function (t) { 249 | await t.test( 250 | 'should yield `true` if attribute matches (string)', 251 | async function () { 252 | assert.ok(matches('[foo=alpha]', u('a', {foo: 'alpha'}))) 253 | } 254 | ) 255 | 256 | await t.test( 257 | 'should yield `true` if attribute matches (number)', 258 | async function () { 259 | assert.ok(matches('[foo=1]', u('a', {foo: 1}))) 260 | } 261 | ) 262 | 263 | await t.test( 264 | 'should yield `true` if attribute matches (array)', 265 | async function () { 266 | assert.ok(matches('[foo=alpha]', u('a', {foo: ['alpha']}))) 267 | } 268 | ) 269 | 270 | await t.test( 271 | 'should yield `true` if attribute matches (array, 2)', 272 | async function () { 273 | assert.ok( 274 | matches('[foo="alpha,bravo"]', u('a', {foo: ['alpha', 'bravo']})) 275 | ) 276 | } 277 | ) 278 | 279 | await t.test( 280 | 'should yield `true` if attribute matches (boolean, true)', 281 | async function () { 282 | assert.ok(matches('[foo=true]', u('a', {foo: true}))) 283 | } 284 | ) 285 | 286 | await t.test( 287 | 'should yield `true` if attribute matches (boolean, false)', 288 | async function () { 289 | assert.ok(matches('[foo=false]', u('a', {foo: false}))) 290 | } 291 | ) 292 | 293 | await t.test( 294 | 'should yield `false` if attribute is missing (null)', 295 | async function () { 296 | assert.ok(!matches('[foo=null]', u('a', {foo: null}))) 297 | } 298 | ) 299 | 300 | await t.test( 301 | 'should yield `false` if attribute is missing (undefined)', 302 | async function () { 303 | assert.ok(!matches('[foo=undefined]', u('a', {foo: undefined}))) 304 | } 305 | ) 306 | 307 | await t.test( 308 | 'should yield `false` if not matches (string)', 309 | async function () { 310 | assert.ok(!matches('[foo=alpha]', u('a', {foo: 'bravo'}))) 311 | } 312 | ) 313 | 314 | await t.test( 315 | 'should yield `false` if not matches (number)', 316 | async function () { 317 | assert.ok(!matches('[foo=1]', u('a', {foo: 2}))) 318 | } 319 | ) 320 | 321 | await t.test( 322 | 'should yield `false` if not matches (array)', 323 | async function () { 324 | assert.ok(!matches('[foo=alpha]', u('a', {foo: ['bravo']}))) 325 | } 326 | ) 327 | 328 | await t.test( 329 | 'should yield `false` if not matches (array, 2)', 330 | async function () { 331 | assert.ok( 332 | !matches('[foo="alpha,bravo"]', u('a', {foo: ['charlie', 'delta']})) 333 | ) 334 | } 335 | ) 336 | 337 | await t.test( 338 | 'should yield `false` if not matches (boolean, true)', 339 | async function () { 340 | assert.ok(!matches('[foo=true]', u('a', {foo: false}))) 341 | } 342 | ) 343 | 344 | await t.test( 345 | 'should yield `false` if not matches (boolean, false)', 346 | async function () { 347 | assert.ok(!matches('[foo=false]', u('a', {foo: true}))) 348 | } 349 | ) 350 | }) 351 | 352 | await t.test('attributes, begins: `[attr^=value]`', async function (t) { 353 | await t.test( 354 | 'should yield `true` if attribute matches (string)', 355 | async function () { 356 | assert.ok(matches('[foo^=al]', u('a', {foo: 'alpha'}))) 357 | } 358 | ) 359 | 360 | await t.test( 361 | 'should yield `false` if not matches (string)', 362 | async function () { 363 | assert.ok(!matches('[foo^=al]', u('a', {foo: 'bravo'}))) 364 | } 365 | ) 366 | 367 | await t.test( 368 | 'should yield `false` if not string (number)', 369 | async function () { 370 | assert.ok(!matches('[foo^=1]', u('a', {foo: 1}))) 371 | } 372 | ) 373 | 374 | await t.test( 375 | 'should yield `false` if not string (array)', 376 | async function () { 377 | assert.ok(!matches('[foo^=alpha]', u('a', {foo: ['alpha']}))) 378 | } 379 | ) 380 | 381 | await t.test( 382 | 'should yield `false` if not string (boolean, true)', 383 | async function () { 384 | assert.ok(!matches('[foo^=true]', u('a', {foo: true}))) 385 | } 386 | ) 387 | 388 | await t.test( 389 | 'should yield `false` if not string (boolean, false)', 390 | async function () { 391 | assert.ok(!matches('[foo^=false]', u('a', {foo: false}))) 392 | } 393 | ) 394 | }) 395 | 396 | await t.test('attributes, ends: `[attr$=value]`', async function (t) { 397 | await t.test( 398 | 'should yield `true` if attribute matches (string)', 399 | async function () { 400 | assert.ok(matches('[foo$=ha]', u('a', {foo: 'alpha'}))) 401 | } 402 | ) 403 | 404 | await t.test( 405 | 'should yield `false` if not matches (string)', 406 | async function () { 407 | assert.ok(!matches('[foo$=ha]', u('a', {foo: 'bravo'}))) 408 | } 409 | ) 410 | 411 | await t.test( 412 | 'should yield `false` if not string (number)', 413 | async function () { 414 | assert.ok(!matches('[foo$=1]', u('a', {foo: 1}))) 415 | } 416 | ) 417 | 418 | await t.test( 419 | 'should yield `false` if not string (array)', 420 | async function () { 421 | assert.ok(!matches('[foo$=alpha]', u('a', {foo: ['alpha']}))) 422 | } 423 | ) 424 | 425 | await t.test( 426 | 'should yield `false` if not string (boolean, true)', 427 | async function () { 428 | assert.ok(!matches('[foo$=true]', u('a', {foo: true}))) 429 | } 430 | ) 431 | 432 | await t.test( 433 | 'should yield `false` if not string (boolean, false)', 434 | async function () { 435 | assert.ok(!matches('[foo$=false]', u('a', {foo: false}))) 436 | } 437 | ) 438 | }) 439 | 440 | await t.test('attributes, contains: `[attr*=value]`', async function (t) { 441 | await t.test( 442 | 'should yield `true` if attribute matches (string)', 443 | async function () { 444 | assert.ok(matches('[foo*=ph]', u('a', {foo: 'alpha'}))) 445 | } 446 | ) 447 | 448 | await t.test( 449 | 'should yield `false` if not matches (string)', 450 | async function () { 451 | assert.ok(!matches('[foo*=ph]', u('a', {foo: 'bravo'}))) 452 | } 453 | ) 454 | 455 | await t.test( 456 | 'should yield `false` if not string (number)', 457 | async function () { 458 | assert.ok(!matches('[foo*=1]', u('a', {foo: 1}))) 459 | } 460 | ) 461 | 462 | await t.test( 463 | 'should yield `false` if not string (array)', 464 | async function () { 465 | assert.ok(!matches('[foo*=alpha]', u('a', {foo: ['alpha']}))) 466 | } 467 | ) 468 | 469 | await t.test( 470 | 'should yield `false` if not string (boolean, true)', 471 | async function () { 472 | assert.ok(!matches('[foo*=true]', u('a', {foo: true}))) 473 | } 474 | ) 475 | 476 | await t.test( 477 | 'should yield `false` if not string (boolean, false)', 478 | async function () { 479 | assert.ok(!matches('[foo*=false]', u('a', {foo: false}))) 480 | } 481 | ) 482 | }) 483 | 484 | await t.test( 485 | 'attributes, contains in a list: `[attr~=value]`', 486 | async function (t) { 487 | await t.test( 488 | 'should yield `true` if attribute matches (string)', 489 | async function () { 490 | assert.ok(matches('[foo~=alpha]', u('a', {foo: 'alpha'}))) 491 | } 492 | ) 493 | 494 | await t.test( 495 | 'should yield `true` if attribute matches (number)', 496 | async function () { 497 | assert.ok(matches('[foo~=1]', u('a', {foo: 1}))) 498 | } 499 | ) 500 | 501 | await t.test( 502 | 'should yield `true` if attribute matches (array)', 503 | async function () { 504 | assert.ok(matches('[foo~=alpha]', u('a', {foo: ['alpha']}))) 505 | } 506 | ) 507 | 508 | await t.test( 509 | 'should yield `true` if attribute matches (array, 2)', 510 | async function () { 511 | assert.ok( 512 | matches('[foo~="alpha,bravo"]', u('a', {foo: ['alpha', 'bravo']})) 513 | ) 514 | } 515 | ) 516 | 517 | await t.test( 518 | 'should yield `true` if attribute matches (boolean, true)', 519 | async function () { 520 | assert.ok(matches('[foo~=true]', u('a', {foo: true}))) 521 | } 522 | ) 523 | 524 | await t.test( 525 | 'should yield `true` if attribute matches (boolean, false)', 526 | async function () { 527 | assert.ok(matches('[foo~=false]', u('a', {foo: false}))) 528 | } 529 | ) 530 | 531 | await t.test( 532 | 'should yield `false` if attribute is missing (null)', 533 | async function () { 534 | assert.ok(!matches('[foo~=null]', u('a', {foo: null}))) 535 | } 536 | ) 537 | 538 | await t.test( 539 | 'should yield `false` if attribute is missing (undefined)', 540 | async function () { 541 | assert.ok(!matches('[foo~=undefined]', u('a', {foo: undefined}))) 542 | } 543 | ) 544 | 545 | await t.test( 546 | 'should yield `false` if not matches (string)', 547 | async function () { 548 | assert.ok(!matches('[foo~=alpha]', u('a', {foo: 'bravo'}))) 549 | } 550 | ) 551 | 552 | await t.test( 553 | 'should yield `false` if not matches (number)', 554 | async function () { 555 | assert.ok(!matches('[foo~=1]', u('a', {foo: 2}))) 556 | } 557 | ) 558 | 559 | await t.test( 560 | 'should yield `false` if not matches (array)', 561 | async function () { 562 | assert.ok(!matches('[foo~=alpha]', u('a', {foo: ['bravo']}))) 563 | } 564 | ) 565 | 566 | await t.test( 567 | 'should yield `false` if not matches (array, 2)', 568 | async function () { 569 | assert.ok( 570 | !matches( 571 | '[foo~="alpha,bravo"]', 572 | u('a', {foo: ['charlie', 'delta']}) 573 | ) 574 | ) 575 | } 576 | ) 577 | 578 | await t.test( 579 | 'should yield `false` if not matches (boolean, true)', 580 | async function () { 581 | assert.ok(!matches('[foo~=true]', u('a', {foo: false}))) 582 | } 583 | ) 584 | 585 | await t.test( 586 | 'should yield `false` if not matches (boolean, false)', 587 | async function () { 588 | assert.ok(!matches('[foo=false]', u('a', {foo: true}))) 589 | } 590 | ) 591 | 592 | await t.test( 593 | 'should yield `true` if attribute is contained (array of strings)', 594 | async function () { 595 | assert.ok( 596 | matches( 597 | '[foo~=bravo]', 598 | u('a', {foo: ['alpha', 'bravo', 'charlie']}) 599 | ) 600 | ) 601 | } 602 | ) 603 | 604 | await t.test( 605 | 'should yield `true` if attribute is contained (array of strings)', 606 | async function () { 607 | assert.ok( 608 | matches( 609 | '[foo~=bravo]', 610 | u('a', {foo: ['alpha', 'bravo', 'charlie']}) 611 | ) 612 | ) 613 | } 614 | ) 615 | 616 | await t.test( 617 | 'should yield `false` if attribute is not contained (array of strings)', 618 | async function () { 619 | assert.ok( 620 | !matches( 621 | '[foo~=delta]', 622 | u('a', {foo: ['alpha', 'bravo', 'charlie']}) 623 | ) 624 | ) 625 | } 626 | ) 627 | 628 | await t.test( 629 | 'should yield `false` if attribute is not contained (array of strings)', 630 | async function () { 631 | assert.ok( 632 | !matches( 633 | '[foo~=delta]', 634 | u('a', {foo: ['alpha', 'bravo', 'charlie']}) 635 | ) 636 | ) 637 | } 638 | ) 639 | } 640 | ) 641 | 642 | await t.test('attributes, case modifiers `[attr i]`', async function (t) { 643 | await t.test( 644 | 'should throw when using a modifier in a wrong place', 645 | async function () { 646 | assert.throws(function () { 647 | matches('[x y]', u('a')) 648 | }, /Expected a valid attribute selector operator/) 649 | } 650 | ) 651 | 652 | await t.test( 653 | 'should throw when using an unknown modifier', 654 | async function () { 655 | assert.throws(function () { 656 | matches('[x=y z]', u('a')) 657 | }, /Unknown attribute case sensitivity modifier/) 658 | } 659 | ) 660 | 661 | await t.test( 662 | 'should match sensitively (default) with `s` (#1)', 663 | async function () { 664 | assert.ok(matches('[x=y s]', u('a', {x: 'y'}))) 665 | } 666 | ) 667 | 668 | await t.test( 669 | 'should match sensitively (default) with `s` (#2)', 670 | async function () { 671 | assert.ok(!matches('[x=y s]', u('a', {x: 'Y'}))) 672 | } 673 | ) 674 | 675 | await t.test('should match insensitively with `i` (#1)', async function () { 676 | assert.ok(matches('[x=y i]', u('a', {x: 'y'}))) 677 | }) 678 | 679 | await t.test('should match insensitively with `i` (#2)', async function () { 680 | assert.ok(matches('[x=y i]', u('a', {x: 'Y'}))) 681 | }) 682 | 683 | await t.test('should match insensitively with `i` (#3)', async function () { 684 | assert.ok(matches('[x=Y i]', u('a', {x: 'y'}))) 685 | }) 686 | 687 | await t.test('should match insensitively with `i` (#4)', async function () { 688 | assert.ok(matches('[x=Y i]', u('a', {x: 'Y'}))) 689 | }) 690 | }) 691 | 692 | await t.test('pseudo-classes', async function (t) { 693 | await t.test(':is', async function (t) { 694 | await t.test( 695 | 'should yield `true` if any matches (type)', 696 | async function () { 697 | assert.ok(matches(':is(a, [b])', u('a'))) 698 | } 699 | ) 700 | 701 | await t.test( 702 | 'should yield `true` if any matches (attribute)', 703 | async function () { 704 | assert.ok(matches(':is(a, [b])', u('c', {b: 1}))) 705 | } 706 | ) 707 | 708 | await t.test( 709 | 'should yield `false` if nothing matches', 710 | async function () { 711 | assert.ok(!matches(':is(a, [b])', u('c'))) 712 | } 713 | ) 714 | 715 | await t.test('should yield `false` if children match', async function () { 716 | assert.ok(!matches(':is(a, [b])', u('c', [u('a')]))) 717 | }) 718 | }) 719 | 720 | await t.test(':not()', async function (t) { 721 | await t.test( 722 | 'should yield `false` if any matches (type)', 723 | async function () { 724 | assert.ok(!matches(':not(a, [b])', u('a'))) 725 | } 726 | ) 727 | 728 | await t.test( 729 | 'should yield `false` if any matches (attribute)', 730 | async function () { 731 | assert.ok(!matches(':not(a, [b])', u('c', {b: 1}))) 732 | } 733 | ) 734 | 735 | await t.test('should yield `true` if nothing matches', async function () { 736 | assert.ok(matches(':not(a, [b])', u('c'))) 737 | }) 738 | 739 | await t.test('should yield `true` if children match', async function () { 740 | assert.ok(matches(':not(a, [b])', u('c', [u('a')]))) 741 | }) 742 | }) 743 | 744 | await t.test(':has', async function (t) { 745 | await t.test('should throw on empty selectors', async function () { 746 | assert.throws(function () { 747 | matches('a:not(:has())', u('b')) 748 | }, /Expected rule but "\)" found/) 749 | }) 750 | 751 | await t.test('should throw on empty selectors', async function () { 752 | assert.throws(function () { 753 | matches('a:has()', u('b')) 754 | }, /Expected rule but "\)" found/) 755 | }) 756 | 757 | await t.test( 758 | 'should not match the scope element (#1)', 759 | async function () { 760 | assert.ok(!matches('a:has(b)', u('a', [u('c')]))) 761 | } 762 | ) 763 | 764 | await t.test( 765 | 'should not match the scope element (#2)', 766 | async function () { 767 | assert.ok(matches('a:has(b)', u('a', [u('b')]))) 768 | } 769 | ) 770 | 771 | await t.test( 772 | 'should yield `true` if children match the descendant selector', 773 | async function () { 774 | assert.ok(matches('a:has(b)', u('a', [u('b')]))) 775 | } 776 | ) 777 | 778 | await t.test( 779 | 'should yield `false` if no children match the descendant selector', 780 | async function () { 781 | assert.ok(!matches('a:has(b)', u('a', [u('c')]))) 782 | } 783 | ) 784 | 785 | await t.test( 786 | 'should yield `true` if descendants match the descendant selector', 787 | async function () { 788 | assert.ok(matches('a:has(c)', u('a', [u('b'), u('c')]))) 789 | } 790 | ) 791 | 792 | await t.test( 793 | 'should yield `false` if no descendants match the descendant selector', 794 | async function () { 795 | assert.ok(!matches('a:has(d)', u('a', [u('b', [u('c')])]))) 796 | } 797 | ) 798 | 799 | await t.test( 800 | 'should support a nested next-sibling selector (#1)', 801 | async function () { 802 | assert.ok(matches('a:has(b + c)', u('a', [u('b'), u('c')]))) 803 | } 804 | ) 805 | 806 | await t.test( 807 | 'should support a nested next-sibling selector (#2)', 808 | async function () { 809 | assert.ok(!matches('a:has(b + a)', u('a', [u('b'), u('b')]))) 810 | } 811 | ) 812 | 813 | await t.test( 814 | 'should add `:scope` to sub-selectors (#1)', 815 | async function () { 816 | assert.ok(matches('a:has([c])', u('a', [u('b', {c: 'd'})]))) 817 | } 818 | ) 819 | 820 | await t.test( 821 | 'should add `:scope` to sub-selectors (#2)', 822 | async function () { 823 | assert.ok(!matches('a:has([b])', u('a', {b: 'c'}, [u('d')]))) 824 | } 825 | ) 826 | 827 | await t.test( 828 | 'should add `:scope` to all sub-selectors (#2)', 829 | async function () { 830 | assert.ok(!matches('a:has(a, :scope c)', u('a', u('b')))) 831 | } 832 | ) 833 | 834 | await t.test( 835 | 'should add `:scope` to all sub-selectors (#3)', 836 | async function () { 837 | assert.ok(matches('a:not(:has(b, c, d))', u('a', []))) 838 | } 839 | ) 840 | 841 | await t.test( 842 | 'should add `:scope` to all sub-selectors (#4)', 843 | async function () { 844 | assert.ok(matches('a:not(:has(d, e, f))', u('a', [u('b', 'c')]))) 845 | } 846 | ) 847 | 848 | await t.test('should ignore commas in parens (#1)', async function () { 849 | assert.ok(!matches('a:has(:is(c, d))', u('a', [u('b')]))) 850 | }) 851 | 852 | await t.test('should ignore commas in parens (#2)', async function () { 853 | assert.ok(matches('a:has(:is(b, c))', u('a', [u('b')]))) 854 | }) 855 | 856 | await t.test( 857 | 'should support multiple relative selectors (#1)', 858 | async function () { 859 | assert.ok(!matches('a:has(:is(c), :is(d))', u('a', [u('b')]))) 860 | } 861 | ) 862 | 863 | await t.test( 864 | 'should support multiple relative selectors (#2)', 865 | async function () { 866 | assert.ok(matches('a:has(:is(c), :is(b))', u('a', [u('b')]))) 867 | } 868 | ) 869 | 870 | await t.test('assertion (#1)', async function () { 871 | // This checks white-space. 872 | assert.ok(matches('a:has( b)', u('a', [u('b')]))) 873 | }) 874 | 875 | await t.test('assertion (#2)', async function () { 876 | assert.ok(matches('a:has( b )', u('a', [u('b')]))) 877 | }) 878 | 879 | await t.test('assertion (#3)', async function () { 880 | assert.ok(matches('a:has(b )', u('a', [u('b')]))) 881 | }) 882 | 883 | await t.test('assertion (#4)', async function () { 884 | assert.ok(matches('a:has( b ,\t p )', u('a', [u('b')]))) 885 | }) 886 | 887 | assert.ok( 888 | matches('a:has(> b)', u('a', [u('b')])), 889 | 'should yield `true` for relative direct child selector' 890 | ) 891 | assert.ok( 892 | !matches('a:has(> c)', u('a', [u('b', [u('c')])])), 893 | 'should yield `false` for relative direct child selectors' 894 | ) 895 | assert.ok( 896 | matches('a:has(> c, > b)', u('a', [u('b', [u('b')])])), 897 | 'should support a list of relative selectors' 898 | ) 899 | }) 900 | 901 | const emptyBlankPseudos = [':empty', ':blank'] 902 | 903 | for (const pseudo of emptyBlankPseudos) { 904 | await t.test(pseudo, async function (t) { 905 | await t.test('should yield `true` for void node', async function () { 906 | assert.ok(matches(pseudo, u('a'))) 907 | }) 908 | 909 | await t.test( 910 | 'should yield `true` for parent without children', 911 | async function () { 912 | assert.ok(matches(pseudo, u('a', []))) 913 | } 914 | ) 915 | 916 | await t.test( 917 | 'should yield `false` for falsey literal', 918 | async function () { 919 | assert.ok(!matches(pseudo, u('a', ''))) 920 | } 921 | ) 922 | 923 | await t.test('should yield `false` if w/ nodes', async function () { 924 | assert.ok(!matches(pseudo, u('a', [u('b')]))) 925 | }) 926 | 927 | await t.test('should yield `false` if w/ literal', async function () { 928 | assert.ok(!matches(pseudo, u('a', 'b'))) 929 | }) 930 | }) 931 | } 932 | 933 | await t.test(':root', function () { 934 | assert.ok(matches(':root', u('a'))) 935 | }) 936 | 937 | await t.test(':scope', function () { 938 | assert.ok(matches(':scope', u('a'))) 939 | }) 940 | }) 941 | }) 942 | --------------------------------------------------------------------------------