├── .prettierignore ├── .npmrc ├── .gitignore ├── .editorconfig ├── index.js ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── tsconfig.json ├── license ├── package.json ├── test └── index.js ├── index.test-d.ts ├── lib └── 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 | *.map 5 | *.tsbuildinfo 6 | coverage/ 7 | node_modules/ 8 | yarn.lock 9 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('./lib/index.js').Check} Check 3 | * @typedef {import('./lib/index.js').Test} Test 4 | * @typedef {import('./lib/index.js').TestFunction} TestFunction 5 | */ 6 | 7 | export {is, convert} from './lib/index.js' 8 | -------------------------------------------------------------------------------- /.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 | "declarationMap": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "exactOptionalPropertyTypes": true, 9 | "lib": ["es2022"], 10 | "module": "node16", 11 | "strict": true, 12 | "target": "es2022" 13 | }, 14 | "exclude": ["coverage/", "node_modules/"], 15 | "include": ["**/*.js"] 16 | } 17 | -------------------------------------------------------------------------------- /.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@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: ${{matrix.node}} 14 | - run: npm install 15 | - run: npm test 16 | - uses: codecov/codecov-action@v4 17 | strategy: 18 | matrix: 19 | node: 20 | - lts/gallium 21 | - node 22 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT license) 2 | 3 | Copyright (c) 2015 Titus Wormer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unist-util-is", 3 | "version": "6.0.1", 4 | "description": "unist utility to check if a node passes a test", 5 | "license": "MIT", 6 | "keywords": [ 7 | "unist", 8 | "unist-util", 9 | "util", 10 | "utility", 11 | "tree", 12 | "node", 13 | "is", 14 | "equal", 15 | "check", 16 | "test", 17 | "type" 18 | ], 19 | "repository": "syntax-tree/unist-util-is", 20 | "bugs": "https://github.com/syntax-tree/unist-util-is/issues", 21 | "funding": { 22 | "type": "opencollective", 23 | "url": "https://opencollective.com/unified" 24 | }, 25 | "author": "Titus Wormer (https://wooorm.com)", 26 | "contributors": [ 27 | "Titus Wormer (https://wooorm.com)", 28 | "Christian Murphy ", 29 | "Lucas Brandstaetter (https://github.com/Roang-zero1)" 30 | ], 31 | "sideEffects": false, 32 | "type": "module", 33 | "exports": "./index.js", 34 | "files": [ 35 | "lib/", 36 | "index.d.ts.map", 37 | "index.d.ts", 38 | "index.js" 39 | ], 40 | "dependencies": { 41 | "@types/unist": "^3.0.0" 42 | }, 43 | "devDependencies": { 44 | "@types/mdast": "^4.0.0", 45 | "@types/node": "^24.0.0", 46 | "c8": "^10.0.0", 47 | "prettier": "^3.0.0", 48 | "remark-cli": "^12.0.0", 49 | "remark-preset-wooorm": "^11.0.0", 50 | "tsd": "^0.33.0", 51 | "type-coverage": "^2.0.0", 52 | "typescript": "^5.0.0", 53 | "xo": "^0.58.0" 54 | }, 55 | "scripts": { 56 | "prepack": "npm run build && npm run format", 57 | "build": "tsc --build --clean && tsc --build && tsd && type-coverage", 58 | "format": "remark . -qfo && prettier . -w --log-level warn && xo --fix", 59 | "test-api": "node --conditions development test/index.js", 60 | "test-coverage": "c8 --100 --reporter lcov npm run test-api", 61 | "test": "npm run build && npm run format && npm run test-coverage" 62 | }, 63 | "prettier": { 64 | "bracketSpacing": false, 65 | "semi": false, 66 | "singleQuote": true, 67 | "tabWidth": 2, 68 | "trailingComma": "none", 69 | "useTabs": false 70 | }, 71 | "remarkConfig": { 72 | "plugins": [ 73 | "remark-preset-wooorm" 74 | ] 75 | }, 76 | "typeCoverage": { 77 | "atLeast": 100, 78 | "detail": true, 79 | "#": "needed `any`s", 80 | "ignoreFiles": [ 81 | "lib/index.d.ts" 82 | ], 83 | "ignoreCatch": true, 84 | "strict": true 85 | }, 86 | "xo": { 87 | "overrides": [ 88 | { 89 | "files": [ 90 | "**/*.ts" 91 | ], 92 | "rules": { 93 | "@typescript-eslint/consistent-type-definitions": "off", 94 | "@typescript-eslint/no-unnecessary-type-arguments": "off", 95 | "@typescript-eslint/no-unsafe-argument": "off", 96 | "@typescript-eslint/no-unsafe-assignment": "off", 97 | "import/no-extraneous-dependencies": "off" 98 | } 99 | } 100 | ], 101 | "prettier": true 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('unist').Node} Node 3 | * @typedef {import('unist').Parent} Parent 4 | */ 5 | 6 | import assert from 'node:assert/strict' 7 | import test from 'node:test' 8 | import {is} from 'unist-util-is' 9 | 10 | test('is', async function (t) { 11 | await t.test('should expose the public api', async function () { 12 | assert.deepEqual(Object.keys(await import('unist-util-is')).sort(), [ 13 | 'convert', 14 | 'is' 15 | ]) 16 | }) 17 | 18 | const node = {type: 'strong'} 19 | const parent = {type: 'paragraph', children: []} 20 | 21 | await t.test('should throw when `test` is invalid', async function () { 22 | assert.throws(function () { 23 | // @ts-expect-error: check that an error is thrown at runtime. 24 | is(null, false) 25 | }, /Expected function, string, or object as test/) 26 | }) 27 | 28 | await t.test('should throw when `index` is invalid (#1)', async function () { 29 | assert.throws(function () { 30 | is(node, null, -1, parent) 31 | }, /Expected positive finite index/) 32 | }) 33 | 34 | await t.test('should throw when `index` is invalid (#2)', async function () { 35 | assert.throws(function () { 36 | is(node, null, Number.POSITIVE_INFINITY, parent) 37 | }, /Expected positive finite index/) 38 | }) 39 | 40 | await t.test('should throw when `index` is invalid (#3)', async function () { 41 | assert.throws(function () { 42 | // @ts-expect-error: check that an error is thrown at runtime. 43 | is(node, null, false, parent) 44 | }, /Expected positive finite index/) 45 | }) 46 | 47 | await t.test('should throw when `parent` is invalid (#1)', async function () { 48 | assert.throws(function () { 49 | // @ts-expect-error: check that an error is thrown at runtime. 50 | is(node, null, 0, {}) 51 | }, /Expected parent node/) 52 | }) 53 | 54 | await t.test('should throw when `parent` is invalid (#2)', async function () { 55 | assert.throws(function () { 56 | // @ts-expect-error: check that an error is thrown at runtime. 57 | is(node, null, 0, {type: 'paragraph'}) 58 | }, /Expected parent node/) 59 | }) 60 | 61 | await t.test( 62 | 'should throw `parent` xor `index` are given (#1)', 63 | async function () { 64 | assert.throws(function () { 65 | is(node, null, 0) 66 | }, /Expected both parent and index/) 67 | } 68 | ) 69 | 70 | await t.test( 71 | 'should throw `parent` xor `index` are given (#2)', 72 | async function () { 73 | assert.throws(function () { 74 | is(node, null, null, parent) 75 | }, /Expected both parent and index/) 76 | } 77 | ) 78 | 79 | await t.test('should not fail without node', async function () { 80 | assert.ok(!is()) 81 | }) 82 | 83 | await t.test('should check if given a node (#1)', async function () { 84 | assert.ok(is(node)) 85 | }) 86 | 87 | await t.test('should check if given a node (#2)', async function () { 88 | assert.ok(!is({children: []}, null)) 89 | }) 90 | 91 | await t.test('should match types (#1)', async function () { 92 | assert.ok(is(node, 'strong')) 93 | }) 94 | 95 | await t.test('should match types (#2)', async function () { 96 | assert.ok(!is(node, 'emphasis')) 97 | }) 98 | 99 | await t.test('should match partially (#1)', async function () { 100 | assert.ok(is(node, node)) 101 | }) 102 | 103 | await t.test('should match partially (#2)', async function () { 104 | assert.ok(is(node, {type: 'strong'})) 105 | }) 106 | 107 | await t.test('should match partially (#3)', async function () { 108 | assert.ok(is(parent, {type: 'paragraph'})) 109 | }) 110 | 111 | await t.test('should match partially (#4)', async function () { 112 | assert.ok(!is(node, {type: 'paragraph'})) 113 | }) 114 | 115 | await t.test('should accept a test', function () { 116 | assert.ok(!is(node, test)) 117 | assert.ok(!is(node, test, 0, parent)) 118 | assert.ok(is(node, test, 5, parent)) 119 | 120 | /** 121 | * @param {unknown} _ 122 | * @param {number | undefined} n 123 | * @returns {boolean} 124 | */ 125 | function test(_, n) { 126 | return n === 5 127 | } 128 | }) 129 | 130 | await t.test('should call test', function () { 131 | const context = {foo: 'bar'} 132 | let calls = 0 133 | 134 | is(node, test, 5, parent, context) 135 | assert.equal(calls, 1) 136 | 137 | /** 138 | * @this {unknown} 139 | * @param {Node} a 140 | * @param {number | undefined} b 141 | * @param {Parent | undefined} c 142 | */ 143 | function test(a, b, c) { 144 | assert.equal(this, context) 145 | assert.equal(a, node) 146 | assert.equal(b, 5) 147 | assert.equal(c, parent) 148 | calls++ 149 | } 150 | }) 151 | 152 | await t.test('should match arrays (#1)', async function () { 153 | assert.ok(is(node, ['strong', 'emphasis'])) 154 | }) 155 | 156 | await t.test('should match arrays (#2)', async function () { 157 | assert.ok(!is(node, ['b', 'i'])) 158 | }) 159 | 160 | await t.test('should match arrays (#3)', function () { 161 | const context = {foo: 'bar'} 162 | let calls = 0 163 | 164 | assert.ok(is(node, [test, 'strong'], 5, parent, context)) 165 | assert.equal(calls, 1) 166 | 167 | /** 168 | * @this {unknown} 169 | * @param {Node} a 170 | * @param {number | undefined} b 171 | * @param {Parent | undefined} c 172 | * @returns {boolean} 173 | */ 174 | function test(a, b, c) { 175 | assert.equal(this, context) 176 | assert.equal(a, node) 177 | assert.equal(b, 5) 178 | assert.equal(c, parent) 179 | calls++ 180 | return false 181 | } 182 | }) 183 | }) 184 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import type {Heading, Paragraph, RootContent, Root} from 'mdast' 2 | import {expectAssignable, expectNotType, expectType} from 'tsd' 3 | import type {Node, Parent} from 'unist' 4 | import {convert, is} from 'unist-util-is' 5 | 6 | // # Setup 7 | 8 | const mdastNode = (function (): Root | RootContent { 9 | return {type: 'paragraph', children: []} 10 | })() 11 | 12 | const unknownValue = (function (): unknown { 13 | return {type: 'something'} 14 | })() 15 | 16 | // # `is` 17 | 18 | // No node. 19 | expectType(is()) 20 | 21 | // No test. 22 | expectType(is(mdastNode)) 23 | 24 | if (is(unknownValue)) { 25 | expectType(unknownValue) 26 | } 27 | 28 | /* Nullish test. */ 29 | expectType(is(mdastNode, null)) 30 | expectType(is(mdastNode, undefined)) 31 | 32 | // String test. 33 | if (is(mdastNode, 'heading')) { 34 | console.log('??', mdastNode) 35 | expectType(mdastNode) 36 | } 37 | 38 | if (is(mdastNode, 'paragraph')) { 39 | expectType(mdastNode) 40 | } 41 | 42 | // Object test. 43 | expectType(is(mdastNode, {type: 'heading', depth: 2})) 44 | 45 | if (is(mdastNode, {type: 'heading', depth: 2})) { 46 | expectType(mdastNode) 47 | } 48 | 49 | // TS makes this `type: string`. 50 | if (is(mdastNode, {type: 'paragraph'})) { 51 | expectNotType(mdastNode) 52 | expectNotType(mdastNode) 53 | } 54 | 55 | if (is(mdastNode, {type: 'paragraph'} as const)) { 56 | expectType(mdastNode) 57 | } 58 | 59 | if (is(mdastNode, {type: 'heading', depth: 2})) { 60 | expectType(mdastNode) 61 | expectNotType<2>(mdastNode.depth) // TS can’t narrow this normally. 62 | } 63 | 64 | if (is(mdastNode, {type: 'heading', depth: 2} as const)) { 65 | expectAssignable(mdastNode) 66 | expectType<2>(mdastNode.depth) 67 | } 68 | 69 | // Function test (with explicit assertion). 70 | expectType(is(mdastNode, isHeading)) 71 | 72 | if (is(mdastNode, isHeading)) { 73 | expectType(mdastNode) 74 | } 75 | 76 | if (is(mdastNode, isParagraph)) { 77 | expectType(mdastNode) 78 | } 79 | 80 | if (is(mdastNode, isParagraph)) { 81 | expectNotType(mdastNode) 82 | } 83 | 84 | if (is(mdastNode, isHeading2)) { 85 | expectAssignable(mdastNode) 86 | expectType<2>(mdastNode.depth) 87 | } 88 | 89 | // Function test (implicit assertion). 90 | expectType(is(mdastNode, isHeadingLoose)) 91 | 92 | if (is(mdastNode, isHeadingLoose)) { 93 | expectNotType(mdastNode) 94 | } 95 | 96 | if (is(mdastNode, isParagraphLoose)) { 97 | expectNotType(mdastNode) 98 | } 99 | 100 | if (is(mdastNode, isHead)) { 101 | expectNotType(mdastNode) 102 | } 103 | 104 | // Array tests. 105 | // Can’t narrow down. 106 | expectType( 107 | is(mdastNode, ['heading', isHeading, isHeadingLoose, {type: 'heading'}]) 108 | ) 109 | 110 | // Can’t narrow down. 111 | if (is(mdastNode, ['heading', isHeading, isHeadingLoose, {type: 'heading'}])) { 112 | expectNotType(mdastNode) 113 | } 114 | 115 | // Can’t narrow down. 116 | if (is(mdastNode, ['heading'])) { 117 | expectNotType(mdastNode) 118 | expectNotType(mdastNode) 119 | } 120 | 121 | // Can narrow down with `as const` on arrays of strings. 122 | if (is(mdastNode, ['heading'] as const)) { 123 | expectType(mdastNode) 124 | expectNotType(mdastNode) 125 | } 126 | 127 | // # `check` 128 | 129 | // No node. 130 | const checkNone = convert() 131 | expectType(checkNone()) 132 | 133 | // No test. 134 | expectType(checkNone(mdastNode)) 135 | 136 | if (checkNone(unknownValue)) { 137 | expectType(unknownValue) 138 | } 139 | 140 | /* Nullish test. */ 141 | const checkNull = convert(null) 142 | const checkUndefined = convert(null) 143 | expectType(checkNull(mdastNode)) 144 | expectType(checkUndefined(mdastNode)) 145 | 146 | // String test. 147 | const checkHeading = convert('heading') 148 | const checkParagraph = convert('paragraph') 149 | 150 | if (checkHeading(mdastNode)) { 151 | expectType(mdastNode) 152 | } 153 | 154 | if (checkParagraph(mdastNode)) { 155 | expectType(mdastNode) 156 | } 157 | 158 | // Object test. 159 | expectType(is(mdastNode, {type: 'heading', depth: 2})) 160 | 161 | if (is(mdastNode, {type: 'heading', depth: 2})) { 162 | expectType(mdastNode) 163 | } 164 | 165 | // TS makes this `type: string`. 166 | const checkParagraphProperties = convert({type: 'paragraph'}) 167 | const checkParagraphPropertiesConst = convert({type: 'paragraph'} as const) 168 | const checkHeading2Properties = convert({type: 'heading', depth: 2}) 169 | const checkHeading2PropertiesConst = convert({ 170 | type: 'heading', 171 | depth: 2 172 | } as const) 173 | 174 | if (checkParagraphProperties(mdastNode)) { 175 | expectNotType(mdastNode) 176 | } 177 | 178 | if (checkParagraphPropertiesConst(mdastNode)) { 179 | expectType(mdastNode) 180 | } 181 | 182 | if (checkHeading2Properties(mdastNode)) { 183 | expectType(mdastNode) 184 | expectNotType<2>(mdastNode.depth) // TS can’t narrow this normally. 185 | } 186 | 187 | if (checkHeading2PropertiesConst(mdastNode)) { 188 | expectAssignable(mdastNode) 189 | expectType<2>(mdastNode.depth) 190 | } 191 | 192 | // Function test (with explicit assertion). 193 | const checkHeadingFunction = convert(isHeading) 194 | const checkParagraphFunction = convert(isParagraph) 195 | const checkHeading2Function = convert(isHeading2) 196 | 197 | expectType(checkHeadingFunction(mdastNode)) 198 | 199 | if (checkHeadingFunction(mdastNode)) { 200 | expectType(mdastNode) 201 | } 202 | 203 | if (checkParagraphFunction(mdastNode)) { 204 | expectType(mdastNode) 205 | } 206 | 207 | if (checkParagraphFunction(mdastNode)) { 208 | expectNotType(mdastNode) 209 | } 210 | 211 | if (checkHeading2Function(mdastNode)) { 212 | expectAssignable(mdastNode) 213 | expectType<2>(mdastNode.depth) 214 | } 215 | 216 | // Function test (implicit assertion). 217 | const checkHeadingLooseFunction = convert(isHeadingLoose) 218 | const checkParagraphLooseFunction = convert(isParagraphLoose) 219 | const checkHeadFunction = convert(isHead) 220 | 221 | expectType(checkHeadingLooseFunction(mdastNode)) 222 | 223 | if (checkHeadingLooseFunction(mdastNode)) { 224 | expectNotType(mdastNode) 225 | } 226 | 227 | if (checkParagraphLooseFunction(mdastNode)) { 228 | expectNotType(mdastNode) 229 | } 230 | 231 | if (checkHeadFunction(mdastNode)) { 232 | expectNotType(mdastNode) 233 | } 234 | 235 | // Array tests. 236 | // Can’t narrow down. 237 | const isHeadingArray = convert([ 238 | 'heading', 239 | isHeading, 240 | isHeadingLoose, 241 | {type: 'heading'} 242 | ]) 243 | 244 | expectType(isHeadingArray(mdastNode)) 245 | 246 | // Can’t narrow down. 247 | if (isHeadingArray(mdastNode)) { 248 | expectNotType(mdastNode) 249 | } 250 | 251 | function isHeading(node: Node): node is Heading { 252 | return node ? node.type === 'heading' : false 253 | } 254 | 255 | function isHeading2(node: Node): node is Heading & {depth: 2} { 256 | return isHeading(node) && node.depth === 2 257 | } 258 | 259 | function isHeadingLoose(node: Node) { 260 | return node ? node.type === 'heading' : false 261 | } 262 | 263 | function isParagraph(node: Node): node is Paragraph { 264 | return node ? node.type === 'paragraph' : false 265 | } 266 | 267 | function isParagraphLoose(node: Node) { 268 | return node ? node.type === 'paragraph' : false 269 | } 270 | 271 | function isHead( 272 | node: Node, 273 | index: number | undefined, 274 | parent: Parent | undefined 275 | ) { 276 | return parent ? index === 0 : false 277 | } 278 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {Node, Parent} from 'unist' 3 | */ 4 | 5 | /** 6 | * @template Fn 7 | * @template Fallback 8 | * @typedef {Fn extends (value: any) => value is infer Thing ? Thing : Fallback} Predicate 9 | */ 10 | 11 | /** 12 | * @callback Check 13 | * Check that an arbitrary value is a node. 14 | * @param {unknown} this 15 | * The given context. 16 | * @param {unknown} [node] 17 | * Anything (typically a node). 18 | * @param {number | null | undefined} [index] 19 | * The node’s position in its parent. 20 | * @param {Parent | null | undefined} [parent] 21 | * The node’s parent. 22 | * @returns {boolean} 23 | * Whether this is a node and passes a test. 24 | * 25 | * @typedef {Record | Node} Props 26 | * Object to check for equivalence. 27 | * 28 | * Note: `Node` is included as it is common but is not indexable. 29 | * 30 | * @typedef {ReadonlyArray | Props | TestFunction | string | null | undefined} Test 31 | * Check for an arbitrary node. 32 | * 33 | * @callback TestFunction 34 | * Check if a node passes a test. 35 | * @param {unknown} this 36 | * The given context. 37 | * @param {Node} node 38 | * A node. 39 | * @param {number | undefined} [index] 40 | * The node’s position in its parent. 41 | * @param {Parent | undefined} [parent] 42 | * The node’s parent. 43 | * @returns {boolean | undefined | void} 44 | * Whether this node passes the test. 45 | * 46 | * Note: `void` is included until TS sees no return as `undefined`. 47 | */ 48 | 49 | /** 50 | * Check if `node` is a `Node` and whether it passes the given test. 51 | * 52 | * @param {unknown} node 53 | * Thing to check, typically `Node`. 54 | * @param {Test} test 55 | * A check for a specific node. 56 | * @param {number | null | undefined} index 57 | * The node’s position in its parent. 58 | * @param {Parent | null | undefined} parent 59 | * The node’s parent. 60 | * @param {unknown} context 61 | * Context object (`this`) to pass to `test` functions. 62 | * @returns {boolean} 63 | * Whether `node` is a node and passes a test. 64 | */ 65 | export const is = 66 | // Note: overloads in JSDoc can’t yet use different `@template`s. 67 | /** 68 | * @type {( 69 | * (>(node: unknown, test: Condition, index?: number | null | undefined, parent?: Parent | null | undefined, context?: unknown) => node is Node & {type: Condition[number]}) & 70 | * ((node: unknown, test: Condition, index?: number | null | undefined, parent?: Parent | null | undefined, context?: unknown) => node is Node & {type: Condition}) & 71 | * ((node: unknown, test: Condition, index?: number | null | undefined, parent?: Parent | null | undefined, context?: unknown) => node is Node & Condition) & 72 | * ((node: unknown, test: Condition, index?: number | null | undefined, parent?: Parent | null | undefined, context?: unknown) => node is Node & Predicate) & 73 | * ((node?: null | undefined) => false) & 74 | * ((node: unknown, test?: null | undefined, index?: number | null | undefined, parent?: Parent | null | undefined, context?: unknown) => node is Node) & 75 | * ((node: unknown, test?: Test, index?: number | null | undefined, parent?: Parent | null | undefined, context?: unknown) => boolean) 76 | * )} 77 | */ 78 | ( 79 | /** 80 | * @param {unknown} [node] 81 | * @param {Test} [test] 82 | * @param {number | null | undefined} [index] 83 | * @param {Parent | null | undefined} [parent] 84 | * @param {unknown} [context] 85 | * @returns {boolean} 86 | */ 87 | // eslint-disable-next-line max-params 88 | function (node, test, index, parent, context) { 89 | const check = convert(test) 90 | 91 | if ( 92 | index !== undefined && 93 | index !== null && 94 | (typeof index !== 'number' || 95 | index < 0 || 96 | index === Number.POSITIVE_INFINITY) 97 | ) { 98 | throw new Error('Expected positive finite index') 99 | } 100 | 101 | if ( 102 | parent !== undefined && 103 | parent !== null && 104 | (!is(parent) || !parent.children) 105 | ) { 106 | throw new Error('Expected parent node') 107 | } 108 | 109 | if ( 110 | (parent === undefined || parent === null) !== 111 | (index === undefined || index === null) 112 | ) { 113 | throw new Error('Expected both parent and index') 114 | } 115 | 116 | return looksLikeANode(node) 117 | ? check.call(context, node, index, parent) 118 | : false 119 | } 120 | ) 121 | 122 | /** 123 | * Generate an assertion from a test. 124 | * 125 | * Useful if you’re going to test many nodes, for example when creating a 126 | * utility where something else passes a compatible test. 127 | * 128 | * The created function is a bit faster because it expects valid input only: 129 | * a `node`, `index`, and `parent`. 130 | * 131 | * @param {Test} test 132 | * * when nullish, checks if `node` is a `Node`. 133 | * * when `string`, works like passing `(node) => node.type === test`. 134 | * * when `function` checks if function passed the node is true. 135 | * * when `object`, checks that all keys in test are in node, and that they have (strictly) equal values. 136 | * * when `array`, checks if any one of the subtests pass. 137 | * @returns {Check} 138 | * An assertion. 139 | */ 140 | export const convert = 141 | // Note: overloads in JSDoc can’t yet use different `@template`s. 142 | /** 143 | * @type {( 144 | * ((test: Condition) => (node: unknown, index?: number | null | undefined, parent?: Parent | null | undefined, context?: unknown) => node is Node & {type: Condition}) & 145 | * ((test: Condition) => (node: unknown, index?: number | null | undefined, parent?: Parent | null | undefined, context?: unknown) => node is Node & Condition) & 146 | * ((test: Condition) => (node: unknown, index?: number | null | undefined, parent?: Parent | null | undefined, context?: unknown) => node is Node & Predicate) & 147 | * ((test?: null | undefined) => (node?: unknown, index?: number | null | undefined, parent?: Parent | null | undefined, context?: unknown) => node is Node) & 148 | * ((test?: Test) => Check) 149 | * )} 150 | */ 151 | ( 152 | /** 153 | * @param {Test} [test] 154 | * @returns {Check} 155 | */ 156 | function (test) { 157 | if (test === null || test === undefined) { 158 | return ok 159 | } 160 | 161 | if (typeof test === 'function') { 162 | return castFactory(test) 163 | } 164 | 165 | if (typeof test === 'object') { 166 | return Array.isArray(test) 167 | ? anyFactory(test) 168 | : // Cast because `ReadonlyArray` goes into the above but `isArray` 169 | // narrows to `Array`. 170 | propertiesFactory(/** @type {Props} */ (test)) 171 | } 172 | 173 | if (typeof test === 'string') { 174 | return typeFactory(test) 175 | } 176 | 177 | throw new Error('Expected function, string, or object as test') 178 | } 179 | ) 180 | 181 | /** 182 | * @param {Array} tests 183 | * @returns {Check} 184 | */ 185 | function anyFactory(tests) { 186 | /** @type {Array} */ 187 | const checks = [] 188 | let index = -1 189 | 190 | while (++index < tests.length) { 191 | checks[index] = convert(tests[index]) 192 | } 193 | 194 | return castFactory(any) 195 | 196 | /** 197 | * @this {unknown} 198 | * @type {TestFunction} 199 | */ 200 | function any(...parameters) { 201 | let index = -1 202 | 203 | while (++index < checks.length) { 204 | if (checks[index].apply(this, parameters)) return true 205 | } 206 | 207 | return false 208 | } 209 | } 210 | 211 | /** 212 | * Turn an object into a test for a node with a certain fields. 213 | * 214 | * @param {Props} check 215 | * @returns {Check} 216 | */ 217 | function propertiesFactory(check) { 218 | const checkAsRecord = /** @type {Record} */ (check) 219 | 220 | return castFactory(all) 221 | 222 | /** 223 | * @param {Node} node 224 | * @returns {boolean} 225 | */ 226 | function all(node) { 227 | const nodeAsRecord = /** @type {Record} */ ( 228 | /** @type {unknown} */ (node) 229 | ) 230 | 231 | /** @type {string} */ 232 | let key 233 | 234 | for (key in check) { 235 | if (nodeAsRecord[key] !== checkAsRecord[key]) return false 236 | } 237 | 238 | return true 239 | } 240 | } 241 | 242 | /** 243 | * Turn a string into a test for a node with a certain type. 244 | * 245 | * @param {string} check 246 | * @returns {Check} 247 | */ 248 | function typeFactory(check) { 249 | return castFactory(type) 250 | 251 | /** 252 | * @param {Node} node 253 | */ 254 | function type(node) { 255 | return node && node.type === check 256 | } 257 | } 258 | 259 | /** 260 | * Turn a custom test into a test for a node that passes that test. 261 | * 262 | * @param {TestFunction} testFunction 263 | * @returns {Check} 264 | */ 265 | function castFactory(testFunction) { 266 | return check 267 | 268 | /** 269 | * @this {unknown} 270 | * @type {Check} 271 | */ 272 | function check(value, index, parent) { 273 | return Boolean( 274 | looksLikeANode(value) && 275 | testFunction.call( 276 | this, 277 | value, 278 | typeof index === 'number' ? index : undefined, 279 | parent || undefined 280 | ) 281 | ) 282 | } 283 | } 284 | 285 | function ok() { 286 | return true 287 | } 288 | 289 | /** 290 | * @param {unknown} value 291 | * @returns {value is Node} 292 | */ 293 | function looksLikeANode(value) { 294 | return value !== null && typeof value === 'object' && 'type' in value 295 | } 296 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # unist-util-is 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 to check if nodes pass a test. 12 | 13 | ## Contents 14 | 15 | * [What is this?](#what-is-this) 16 | * [When should I use this?](#when-should-i-use-this) 17 | * [Install](#install) 18 | * [Use](#use) 19 | * [API](#api) 20 | * [`is(node[, test[, index, parent[, context]]])`](#isnode-test-index-parent-context) 21 | * [`convert(test)`](#converttest) 22 | * [`Check`](#check) 23 | * [`Test`](#test) 24 | * [`TestFunction`](#testfunction) 25 | * [Examples](#examples) 26 | * [Example of `convert`](#example-of-convert) 27 | * [Types](#types) 28 | * [Compatibility](#compatibility) 29 | * [Related](#related) 30 | * [Contribute](#contribute) 31 | * [License](#license) 32 | 33 | ## What is this? 34 | 35 | This package is a small utility that checks that a node is a certain node. 36 | 37 | ## When should I use this? 38 | 39 | Use this small utility if you find yourself repeating code for checking what 40 | nodes are. 41 | 42 | A similar package, [`hast-util-is-element`][hast-util-is-element], works on hast 43 | elements. 44 | 45 | For more advanced tests, [`unist-util-select`][unist-util-select] can be used 46 | to match against CSS selectors. 47 | 48 | ## Install 49 | 50 | This package is [ESM only][esm]. 51 | In Node.js (version 16+), install with [npm][]: 52 | 53 | ```sh 54 | npm install unist-util-is 55 | ``` 56 | 57 | In Deno with [`esm.sh`][esmsh]: 58 | 59 | ```js 60 | import {is} from 'https://esm.sh/unist-util-is@6' 61 | ``` 62 | 63 | In browsers with [`esm.sh`][esmsh]: 64 | 65 | ```html 66 | 69 | ``` 70 | 71 | ## Use 72 | 73 | ```js 74 | import {is} from 'unist-util-is' 75 | 76 | const node = {type: 'strong'} 77 | const parent = {type: 'paragraph', children: [node]} 78 | 79 | is() // => false 80 | is({children: []}) // => false 81 | is(node) // => true 82 | is(node, 'strong') // => true 83 | is(node, 'emphasis') // => false 84 | 85 | is(node, node) // => true 86 | is(parent, {type: 'paragraph'}) // => true 87 | is(parent, {type: 'strong'}) // => false 88 | 89 | is(node, test) // => false 90 | is(node, test, 4, parent) // => false 91 | is(node, test, 5, parent) // => true 92 | 93 | function test(node, n) { 94 | return n === 5 95 | } 96 | ``` 97 | 98 | ## API 99 | 100 | This package exports the identifiers [`convert`][api-convert] and 101 | [`is`][api-is]. 102 | There is no default export. 103 | 104 | ### `is(node[, test[, index, parent[, context]]])` 105 | 106 | Check if `node` is a `Node` and whether it passes the given test. 107 | 108 | ###### Parameters 109 | 110 | * `node` (`unknown`, optional) 111 | — thing to check, typically [`Node`][node] 112 | * `test` ([`Test`][api-test], optional) 113 | — a test for a specific element 114 | * `index` (`number`, optional) 115 | — the node’s position in its parent 116 | * `parent` ([`Node`][node], optional) 117 | — the node’s parent 118 | * `context` (`unknown`, optional) 119 | — context object (`this`) to call `test` with 120 | 121 | ###### Returns 122 | 123 | Whether `node` is a [`Node`][node] and passes a test (`boolean`). 124 | 125 | ###### Throws 126 | 127 | When an incorrect `test`, `index`, or `parent` is given. 128 | There is no error thrown when `node` is not a node. 129 | 130 | ### `convert(test)` 131 | 132 | Generate a check from a test. 133 | 134 | Useful if you’re going to test many nodes, for example when creating a 135 | utility where something else passes a compatible test. 136 | 137 | The created function is a bit faster because it expects valid input only: 138 | a `node`, `index`, and `parent`. 139 | 140 | ###### Parameters 141 | 142 | * `test` ([`Test`][api-test], optional) 143 | — a test for a specific node 144 | 145 | ###### Returns 146 | 147 | A check ([`Check`][api-check]). 148 | 149 | ### `Check` 150 | 151 | Check that an arbitrary value is a node (TypeScript type). 152 | 153 | ###### Parameters 154 | 155 | * `this` (`unknown`, optional) 156 | — context object (`this`) to call `test` with 157 | * `node` (`unknown`) 158 | — anything (typically a node) 159 | * `index` (`number`, optional) 160 | — the node’s position in its parent 161 | * `parent` ([`Node`][node], optional) 162 | — the node’s parent 163 | 164 | ###### Returns 165 | 166 | Whether this is a node and passes a test (`boolean`). 167 | 168 | ### `Test` 169 | 170 | Check for an arbitrary node (TypeScript type). 171 | 172 | ###### Type 173 | 174 | ```ts 175 | type Test = 176 | | Array | TestFunction | string> 177 | | Record 178 | | TestFunction 179 | | string 180 | | null 181 | | undefined 182 | ``` 183 | 184 | Checks that the given thing is a node, and then: 185 | 186 | * when `string`, checks that the node has that tag name 187 | * when `function`, see [`TestFunction`][api-test-function] 188 | * when `object`, checks that all keys in test are in node, and that they have 189 | (strictly) equal values 190 | * when `Array`, checks if one of the subtests pass 191 | 192 | ### `TestFunction` 193 | 194 | Check if a node passes a test (TypeScript type). 195 | 196 | ###### Parameters 197 | 198 | * `node` ([`Node`][node]) 199 | — a node 200 | * `index` (`number` or `undefined`) 201 | — the node’s position in its parent 202 | * `parent` ([`Node`][node] or `undefined`) 203 | — the node’s parent 204 | 205 | ###### Returns 206 | 207 | Whether this node passes the test (`boolean`, optional). 208 | 209 | ## Examples 210 | 211 | ### Example of `convert` 212 | 213 | ```js 214 | import {u} from 'unist-builder' 215 | import {convert} from 'unist-util-is' 216 | 217 | const test = convert('leaf') 218 | 219 | const tree = u('tree', [ 220 | u('node', [u('leaf', '1')]), 221 | u('leaf', '2'), 222 | u('node', [u('leaf', '3'), u('leaf', '4')]), 223 | u('leaf', '5') 224 | ]) 225 | 226 | const leafs = tree.children.filter(function (child, index) { 227 | return test(child, index, tree) 228 | }) 229 | 230 | console.log(leafs) 231 | ``` 232 | 233 | Yields: 234 | 235 | ```js 236 | [{type: 'leaf', value: '2'}, {type: 'leaf', value: '5'}] 237 | ``` 238 | 239 | ## Types 240 | 241 | This package is fully typed with [TypeScript][]. 242 | It exports the additional types [`Check`][api-check], 243 | [`Test`][api-test], 244 | [`TestFunction`][api-test-function]. 245 | 246 | ## Compatibility 247 | 248 | Projects maintained by the unified collective are compatible with maintained 249 | versions of Node.js. 250 | 251 | When we cut a new major release, we drop support for unmaintained versions of 252 | Node. 253 | This means we try to keep the current release line, `unist-util-is@^6`, 254 | compatible with Node.js 16. 255 | 256 | ## Related 257 | 258 | * [`unist-util-find-after`](https://github.com/syntax-tree/unist-util-find-after) 259 | — find a node after another node 260 | * [`unist-util-find-before`](https://github.com/syntax-tree/unist-util-find-before) 261 | — find a node before another node 262 | * [`unist-util-find-all-after`](https://github.com/syntax-tree/unist-util-find-all-after) 263 | — find all nodes after another node 264 | * [`unist-util-find-all-before`](https://github.com/syntax-tree/unist-util-find-all-before) 265 | — find all nodes before another node 266 | * [`unist-util-find-all-between`](https://github.com/mrzmmr/unist-util-find-all-between) 267 | — find all nodes between two nodes 268 | * [`unist-util-filter`](https://github.com/syntax-tree/unist-util-filter) 269 | — create a new tree with nodes that pass a check 270 | * [`unist-util-remove`](https://github.com/syntax-tree/unist-util-remove) 271 | — remove nodes from tree 272 | 273 | ## Contribute 274 | 275 | See [`contributing.md`][contributing] in [`syntax-tree/.github`][health] for 276 | ways to get started. 277 | See [`support.md`][support] for ways to get help. 278 | 279 | This project has a [code of conduct][coc]. 280 | By interacting with this repository, organization, or community you agree to 281 | abide by its terms. 282 | 283 | ## License 284 | 285 | [MIT][license] © [Titus Wormer][author] 286 | 287 | 288 | 289 | [api-check]: #check 290 | 291 | [api-convert]: #converttest 292 | 293 | [api-is]: #isnode-test-index-parent-context 294 | 295 | [api-test]: #test 296 | 297 | [api-test-function]: #testfunction 298 | 299 | [author]: https://wooorm.com 300 | 301 | [backers-badge]: https://opencollective.com/unified/backers/badge.svg 302 | 303 | [build]: https://github.com/syntax-tree/unist-util-is/actions 304 | 305 | [build-badge]: https://github.com/syntax-tree/unist-util-is/workflows/main/badge.svg 306 | 307 | [chat]: https://github.com/syntax-tree/unist/discussions 308 | 309 | [chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg 310 | 311 | [coc]: https://github.com/syntax-tree/.github/blob/main/code-of-conduct.md 312 | 313 | [collective]: https://opencollective.com/unified 314 | 315 | [contributing]: https://github.com/syntax-tree/.github/blob/main/contributing.md 316 | 317 | [coverage]: https://codecov.io/github/syntax-tree/unist-util-is 318 | 319 | [coverage-badge]: https://img.shields.io/codecov/c/github/syntax-tree/unist-util-is.svg 320 | 321 | [downloads]: https://www.npmjs.com/package/unist-util-is 322 | 323 | [downloads-badge]: https://img.shields.io/npm/dm/unist-util-is.svg 324 | 325 | [esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 326 | 327 | [esmsh]: https://esm.sh 328 | 329 | [hast-util-is-element]: https://github.com/syntax-tree/hast-util-is-element 330 | 331 | [health]: https://github.com/syntax-tree/.github 332 | 333 | [license]: license 334 | 335 | [node]: https://github.com/syntax-tree/unist#node 336 | 337 | [npm]: https://docs.npmjs.com/cli/install 338 | 339 | [size]: https://bundlejs.com/?q=unist-util-is 340 | 341 | [size-badge]: https://img.shields.io/badge/dynamic/json?label=minzipped%20size&query=$.size.compressedSize&url=https://deno.bundlejs.com/?q=unist-util-is 342 | 343 | [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg 344 | 345 | [support]: https://github.com/syntax-tree/.github/blob/main/support.md 346 | 347 | [typescript]: https://www.typescriptlang.org 348 | 349 | [unist]: https://github.com/syntax-tree/unist 350 | 351 | [unist-util-select]: https://github.com/syntax-tree/unist-util-select 352 | --------------------------------------------------------------------------------