diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fe284ad..fb63387 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,15 +7,15 @@ jobs: name: ${{matrix.node}} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: dcodeIO/setup-node-nvm@master + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: node-version: ${{matrix.node}} - run: npm install - run: npm test - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 strategy: matrix: node: - - lts/erbium + - lts/gallium - node diff --git a/.npmrc b/.npmrc index 43c97e7..9951b11 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ package-lock=false +ignore-scripts=true diff --git a/index.js b/index.js index 10ce584..5281ca3 100644 --- a/index.js +++ b/index.js @@ -1,103 +1,9 @@ /** - * @typedef {import('unist').Node} Node - * @typedef {import('unist').Parent} Parent - * @typedef {import('unist-util-is').Test} Test - */ - -/** - * Options for unist util filter - * - * @typedef {Object} FilterOptions - * @property {boolean} [cascade=true] Whether to drop parent nodes if they had children, but all their children were filtered out. - */ - -import {convert} from 'unist-util-is' - -const own = {}.hasOwnProperty - -/** - * Create a new tree consisting of copies of all nodes that pass test. - * The tree is walked in preorder (NLR), visiting the node itself, then its head, etc. + * @typedef {import('./lib/index.js').Options} Options * - * @param tree Tree to filter. - * @param options Configuration (optional). - * @param test is-compatible test (such as a type). - * @returns Given `tree` or `null` if it didn’t pass `test`. + * @typedef {Options} FilterOptions + * Deprecated, use `Options`. */ -export const filter = - /** - * @type {( - * ((node: Tree, options: FilterOptions, test: Check) => import('./complex-types').Matches) & - * ((node: Tree, test: Check) => import('./complex-types').Matches) & - * ((node: Tree, options?: FilterOptions) => Tree) - * )} - */ - ( - /** - * @param {Node} tree - * @param {FilterOptions} options - * @param {Test} test - * @returns {Node|null} - */ - function (tree, options, test) { - const is = convert(test || options) - const cascade = - options.cascade === undefined || options.cascade === null - ? true - : options.cascade - - return preorder(tree) - - /** - * @param {Node} node - * @param {number|undefined} [index] - * @param {Parent|undefined} [parent] - * @returns {Node|null} - */ - function preorder(node, index, parent) { - /** @type {Array.} */ - const children = [] - /** @type {number} */ - let childIndex - /** @type {Node} */ - let result - /** @type {string} */ - let key - - if (!is(node, index, parent)) return null - - // @ts-expect-error: Looks like a parent. - if (node.children) { - childIndex = -1 - - // @ts-expect-error Looks like a parent. - while (++childIndex < node.children.length) { - // @ts-expect-error Looks like a parent. - result = preorder(node.children[childIndex], childIndex, node) - - if (result) { - children.push(result) - } - } - - // @ts-expect-error Looks like a parent. - if (cascade && node.children.length > 0 && children.length === 0) - return null - } - - // Create a shallow clone, using the new children. - /** @type {typeof node} */ - // @ts-expect-error all the fields will be copied over. - const next = {} - - for (key in node) { - if (own.call(node, key)) { - // @ts-expect-error: Looks like a record. - next[key] = key === 'children' ? children : node[key] - } - } +// To do: next major: remove `FilterOptions`. - return next - } - } - ) +export {filter} from './lib/index.js' diff --git a/index.test-d.ts b/index.test-d.ts index f65fc92..fc5e936 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -5,7 +5,7 @@ import {filter} from './index.js' const root: Root = {type: 'root', children: []} /* eslint-disable @typescript-eslint/consistent-type-assertions */ -const justANode = {type: 'whatever'} as Node +const justSomeNode = {type: 'whatever'} as Node const headingOrParagraph = { type: 'paragraph', children: [] @@ -44,15 +44,15 @@ expectType( // Abstract types. // These don’t work well. // Use strict nodes types. -expectType(filter(justANode)) -expectType(filter(justANode, '???')) -expectType(filter(justANode, {cascade: false}, '???')) +expectType(filter(justSomeNode)) +expectType(filter(justSomeNode, '???')) +expectType(filter(justSomeNode, {cascade: false}, '???')) expectType( - filter(justANode, {cascade: false}, () => Math.random() > 0.5) + filter(justSomeNode, {cascade: false}, () => Math.random() > 0.5) ) expectType( filter( - justANode, + justSomeNode, {cascade: false}, (node: Node): node is Heading => node.type === 'heading' ) diff --git a/complex-types.d.ts b/lib/complex-types.d.ts similarity index 100% rename from complex-types.d.ts rename to lib/complex-types.d.ts diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..ab888fc --- /dev/null +++ b/lib/index.js @@ -0,0 +1,114 @@ +/** + * @typedef {import('unist').Node} Node + * @typedef {import('unist').Parent} Parent + * @typedef {import('unist-util-is').Test} Test + * + * @typedef Options + * Configuration (optional). + * @property {boolean | null | undefined} [cascade=true] + * Whether to drop parent nodes if they had children, but all their children + * were filtered out. + */ + +import {convert} from 'unist-util-is' + +const own = {}.hasOwnProperty + +/** + * Create a new `tree` of copies of all nodes that pass `test`. + * + * The tree is walked in *preorder* (NLR), visiting the node itself, then its + * head, etc. + * + * @param tree + * Tree to filter. + * @param options + * Configuration (optional). + * @param test + * `unist-util-is` compatible test. + * @returns + * New filtered tree. + * + * `null` is returned if `tree` itself didn’t pass the test, or is cascaded + * away. + */ +export const filter = + /** + * @type {( + * ((node: Tree, options: Options | null | undefined, test: Check | null | undefined) => import('./complex-types.js').Matches) & + * ((node: Tree, test: Check) => import('./complex-types.js').Matches) & + * ((node: Tree, options?: Options | null | undefined) => Tree) + * )} + */ + ( + /** + * @param {Node} tree + * @param {Options | Test | null | undefined} [options] + * @param {Test | null | undefined} [test] + * @returns {Node | null} + */ + function (tree, options, test) { + const is = convert(test || options) + /** @type {boolean | null | undefined} */ + const cascadeRaw = + options && typeof options === 'object' && 'cascade' in options + ? /** @type {boolean | null | undefined} */ (options.cascade) + : undefined + const cascade = + cascadeRaw === undefined || cascadeRaw === null ? true : cascadeRaw + + return preorder(tree) + + /** + * @param {Node} node + * Current node. + * @param {number | undefined} [index] + * Index of `node` in `parent`. + * @param {Parent | undefined} [parent] + * Parent node. + * @returns {Node | null} + * Shallow copy of `node`. + */ + function preorder(node, index, parent) { + /** @type {Array} */ + const children = [] + + if (!is(node, index, parent)) return null + + // @ts-expect-error: Looks like a parent. + if (node.children) { + let childIndex = -1 + + // @ts-expect-error Looks like a parent. + while (++childIndex < node.children.length) { + // @ts-expect-error Looks like a parent. + const result = preorder(node.children[childIndex], childIndex, node) + + if (result) { + children.push(result) + } + } + + // @ts-expect-error Looks like a parent. + if (cascade && node.children.length > 0 && children.length === 0) + return null + } + + // Create a shallow clone, using the new children. + /** @type {typeof node} */ + // @ts-expect-error all the fields will be copied over. + const next = {} + /** @type {string} */ + let key + + for (key in node) { + if (own.call(node, key)) { + // @ts-expect-error: Looks like a record. + next[key] = key === 'children' ? children : node[key] + } + } + + return next + } + } + ) diff --git a/package.json b/package.json index de955e6..1611346 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "unist-util-filter", - "version": "4.0.0", + "version": "4.0.1", "description": "unist utility to create a new tree with nodes that pass a filter", "license": "MIT", "keywords": [ @@ -29,7 +29,7 @@ "main": "index.js", "types": "index.d.ts", "files": [ - "complex-types.d.ts", + "lib/", "index.d.ts", "index.js" ], @@ -40,25 +40,23 @@ }, "devDependencies": { "@types/mdast": "^3.0.0", - "@types/tape": "^4.0.0", + "@types/node": "^18.0.0", "c8": "^7.0.0", "prettier": "^2.0.0", - "remark-cli": "^9.0.0", - "remark-preset-wooorm": "^8.0.0", - "rimraf": "^3.0.0", - "tape": "^5.0.0", - "tsd": "^0.17.0", + "remark-cli": "^11.0.0", + "remark-preset-wooorm": "^9.0.0", + "tsd": "^0.25.0", "type-coverage": "^2.0.0", "typescript": "^4.0.0", "unist-builder": "^3.0.0", - "xo": "^0.42.0" + "xo": "^0.53.0" }, "scripts": { "prepack": "npm run build && npm run format", - "build": "rimraf \"{index,test}.d.ts\" && tsc && tsd && type-coverage", + "build": "tsc --build --clean && tsc --build && tsd && type-coverage", "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", - "test-api": "node test.js", - "test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node test.js", + "test-api": "node --conditions development test.js", + "test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", "test": "npm run build && npm run format && npm run test-coverage" }, "prettier": { @@ -70,7 +68,10 @@ "trailingComma": "none" }, "xo": { - "prettier": true + "prettier": true, + "ignore": [ + "index.test-d.ts" + ] }, "remarkConfig": { "plugins": [ @@ -84,7 +85,7 @@ "ignoreCatch": true, "#": "needed `any`s", "ignoreFiles": [ - "complex-types.d.ts" + "lib/complex-types.d.ts" ] } } diff --git a/readme.md b/readme.md index 388c143..02a5d7a 100644 --- a/readme.md +++ b/readme.md @@ -8,20 +8,61 @@ [![Backers][backers-badge]][collective] [![Chat][chat-badge]][chat] -[**unist**][unist] utility to create a new tree with all nodes that pass the -given test. +[unist][] utility to create a new tree with only nodes that pass a test. -## Install +## Contents + +* [What is this?](#what-is-this) +* [When should I use this?](#when-should-i-use-this) +* [Install](#install) +* [Use](#use) +* [API](#api) + * [`filter(tree[, options][, test])`](#filtertree-options-test) + * [`Options`](#options) +* [Types](#types) +* [Compatibility](#compatibility) +* [Related](#related) +* [Contribute](#contribute) +* [License](#license) + +## What is this? + +This is a small utility that helps you clean a tree. + +## When should I use this? -This package is [ESM only](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c): -Node 12+ is needed to use it and it must be `import`ed instead of `require`d. +You can use this utility to remove things from a tree. +This utility is very similar to [`unist-util-remove`][unist-util-remove], which +changes the given tree. +Modifying a tree like that utility does is much faster on larger documents. -[npm][]: +You can also walk the tree with [`unist-util-visit`][unist-util-visit] to remove +nodes. +To create trees, use [`unist-builder`][unist-builder]. + +## Install + +This package is [ESM only][esm]. +In Node.js (version 14.14+ and 16.0+), install with [npm][]: ```sh npm install unist-util-filter ``` +In Deno with [`esm.sh`][esmsh]: + +```js +import {filter} from 'https://esm.sh/unist-util-filter@4' +``` + +In browsers with [`esm.sh`][esmsh]: + +```html + +``` + ## Use ```js @@ -30,7 +71,7 @@ import {filter} from 'unist-util-filter' const tree = u('root', [ u('leaf', '1'), - u('node', [u('leaf', '2'), u('node', [u('leaf', '3')])]), + u('parent', [u('leaf', '2'), u('parent', [u('leaf', '3')])]), u('leaf', '4') ]) @@ -45,7 +86,7 @@ Yields: { type: 'root', children: [ - {type: 'node', children: [{type: 'leaf', value: '2'}]}, + {type: 'parent', children: [{type: 'leaf', value: '2'}]}, {type: 'leaf', value: '4'} ] } @@ -53,49 +94,72 @@ Yields: ## API -This package exports the following identifiers: `filter`. +This package exports the identifier [`filter`][filter]. There is no default export. ### `filter(tree[, options][, test])` -Create a new [tree][] consisting of copies of all nodes that pass `test`. -The tree is walked in [preorder][] (NLR), visiting the node itself, then its -[head][], etc. +Create a new `tree` of copies of all nodes that pass `test`. + +The tree is walked in *[preorder][]* (NLR), visiting the node itself, then its +head, etc. ###### Parameters -* `tree` ([`Node?`][node]) - — [Tree][] to filter -* `options.cascade` (`boolean`, default: `true`) - — Whether to drop parent nodes if they had children, but all their children - were filtered out -* `test` ([`Test`][is], optional) — [`is`][is]-compatible test (such as a - [type][]) +* `tree` ([`Node`][node]) + — tree to filter +* `options` ([`Options`][options], optional) + — configuration +* `test` ([`Test`][test], optional) + — `unist-util-is` compatible test ###### Returns -[`Node?`][node] — New filtered [tree][]. +New filtered tree ([`Node`][node] or `null`). + `null` is returned if `tree` itself didn’t pass the test, or is cascaded away. +### `Options` + +Configuration (TypeScript type). + +###### Fields + +* `cascade` (`boolean`, default: `true`) + — whether to drop parent nodes if they had children, but all their + children were filtered out + +## Types + +This package is fully typed with [TypeScript][]. +It exports the additional type [`Options`][options]. + +## Compatibility + +Projects maintained by the unified collective are compatible with all maintained +versions of Node.js. +As of now, that is Node.js 14.14+ and 16.0+. +Our projects sometimes work with older versions, but this is not guaranteed. + ## Related * [`unist-util-visit`](https://github.com/syntax-tree/unist-util-visit) - — Recursively walk over nodes + — walk the tree * [`unist-util-visit-parents`](https://github.com/syntax-tree/unist-util-visit-parents) - — Like `visit`, but with a stack of parents + — walk the tree with a stack of parents * [`unist-util-map`](https://github.com/syntax-tree/unist-util-map) - — Create a new tree with all nodes mapped by a given function + — create a new tree with all nodes mapped by a given function * [`unist-util-flatmap`](https://gitlab.com/staltz/unist-util-flatmap) - — Create a new tree by mapping (to an array) by a given function + — create a new tree by mapping (to an array) by a given function * [`unist-util-remove`](https://github.com/syntax-tree/unist-util-remove) - — Remove nodes from a tree that pass a test + — remove nodes from a tree that pass a test * [`unist-util-select`](https://github.com/syntax-tree/unist-util-select) - — Select nodes with CSS-like selectors + — select nodes with CSS-like selectors ## Contribute -See [`contributing.md` in `syntax-tree/.github`][contributing] for ways to get -started. +See [`contributing.md`][contributing] in [`syntax-tree/.github`][health] for +ways to get started. See [`support.md`][support] for ways to get help. This project has a [code of conduct][coc]. @@ -136,24 +200,36 @@ abide by its terms. [npm]: https://docs.npmjs.com/cli/install +[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c + +[esmsh]: https://esm.sh + +[typescript]: https://www.typescriptlang.org + [license]: license +[health]: https://github.com/syntax-tree/.github + +[contributing]: https://github.com/syntax-tree/.github/blob/HEAD/contributing.md + +[support]: https://github.com/syntax-tree/.github/blob/HEAD/support.md + +[coc]: https://github.com/syntax-tree/.github/blob/HEAD/code-of-conduct.md + [unist]: https://github.com/syntax-tree/unist [node]: https://github.com/syntax-tree/unist#node -[tree]: https://github.com/syntax-tree/unist#tree - [preorder]: https://www.geeksforgeeks.org/tree-traversals-inorder-preorder-and-postorder/ -[head]: https://github.com/syntax-tree/unist#head +[unist-util-remove]: https://github.com/syntax-tree/unist-util-remove -[type]: https://github.com/syntax-tree/unist#type +[unist-util-visit]: https://github.com/syntax-tree/unist-util-visit -[is]: https://github.com/syntax-tree/unist-util-is +[unist-builder]: https://github.com/syntax-tree/unist-builder -[contributing]: https://github.com/syntax-tree/.github/blob/HEAD/contributing.md +[test]: https://github.com/syntax-tree/unist-util-is#test -[support]: https://github.com/syntax-tree/.github/blob/HEAD/support.md +[filter]: #filtertree-options-test -[coc]: https://github.com/syntax-tree/.github/blob/HEAD/code-of-conduct.md +[options]: #options diff --git a/test.js b/test.js index a790aff..2e0e639 100644 --- a/test.js +++ b/test.js @@ -3,19 +3,27 @@ * @typedef {import('unist').Parent} Parent */ -import test from 'tape' +import assert from 'node:assert/strict' +import test from 'node:test' import {u} from 'unist-builder' import {filter} from './index.js' +import * as mod from './index.js' -test('should not traverse into children of filtered out nodes', (t) => { +test('filter', () => { + assert.deepEqual( + Object.keys(mod).sort(), + ['filter'], + 'should expose the public api' + ) +}) + +test('should not traverse into children of filtered out nodes', () => { const tree = u('root', [u('node', [u('leaf', '1')]), u('leaf', '2')]) /** @type {Record} */ const types = {} - t.deepEqual(filter(tree, predicate), u('root', [u('leaf', '2')])) - t.deepEqual(types, {root: 1, node: 1, leaf: 1}) - - t.end() + assert.deepEqual(filter(tree, predicate), u('root', [u('leaf', '2')])) + assert.deepEqual(types, {root: 1, node: 1, leaf: 1}) /** * @param {Node} node @@ -26,25 +34,21 @@ test('should not traverse into children of filtered out nodes', (t) => { } }) -test('should return `null` if root node is filtered out', (t) => { +test('should return `null` if root node is filtered out', () => { const tree = u('root', [u('node', [u('leaf', '1')]), u('leaf', '2')]) - t.deepEqual(filter(tree, predicate), null) - - t.end() + assert.deepEqual(filter(tree, predicate), null) function predicate() { return false } }) -test('should cascade-remove parent nodes', (t) => { +test('should cascade-remove parent nodes', () => { const tree = u('root', [u('node', [u('leaf', '1')]), u('leaf', '2')]) - t.deepEqual(filter(tree, notOne), u('root', [u('leaf', '2')])) - t.deepEqual(filter(tree, notLeaf), null) - - t.end() + assert.deepEqual(filter(tree, notOne), u('root', [u('leaf', '2')])) + assert.deepEqual(filter(tree, notLeaf), null) /** * @param {Node} node @@ -62,20 +66,18 @@ test('should cascade-remove parent nodes', (t) => { } }) -test('should not cascade-remove nodes that were empty initially', (t) => { +test('should not cascade-remove nodes that were empty initially', () => { const tree = u('node', [u('node', []), u('node', [u('leaf')])]) - t.deepEqual(filter(tree, 'node'), u('node', [u('node', [])])) - - t.end() + assert.deepEqual(filter(tree, 'node'), u('node', [u('node', [])])) }) -test('should call iterator with `index` and `parent` args', (t) => { +test('should call iterator with `index` and `parent` args', () => { const tree = u('root', [u('node', [u('leaf', '1')]), u('leaf', '2')]) - /** @type {Array.<[Node, number|null|undefined, Parent|null|undefined]>} */ + /** @type {Array<[Node, number|null|undefined, Parent|null|undefined]>} */ const callLog = [] - t.deepEqual( + assert.deepEqual( filter(tree, (a, b, c) => { callLog.push([a, b, c]) return true @@ -83,47 +85,41 @@ test('should call iterator with `index` and `parent` args', (t) => { tree ) - t.deepEqual(callLog, [ + assert.deepEqual(callLog, [ [tree, undefined, undefined], [tree.children[0], 0, tree], // @ts-expect-error yeah, it exists. [tree.children[0].children[0], 0, tree.children[0]], [tree.children[1], 1, tree] ]) - - t.end() }) -test('should support type and node tests', (t) => { +test('should support type and node tests', () => { const tree = u('node', [u('node', [u('leaf', '1')]), u('leaf', '2')]) - t.deepEqual(filter(tree, 'node'), null) - t.deepEqual( + assert.deepEqual(filter(tree, 'node'), null) + assert.deepEqual( filter(tree, {cascade: false}, 'node'), u('node', [u('node', [])]) ) - t.deepEqual(filter(tree, {cascade: false}, 'leaf'), null) - - t.end() + assert.deepEqual(filter(tree, {cascade: false}, 'leaf'), null) }) -test('opts.cascade', (t) => { +test('opts.cascade', () => { const tree = u('root', [u('node', [u('leaf', '1')]), u('leaf', '2')]) - t.deepEqual( + assert.deepEqual( filter(tree, {cascade: true}, predicate), null, 'opts.cascade = true' ) - t.deepEqual( + assert.deepEqual( filter(tree, {cascade: false}, predicate), u('root', [u('node', [])]), 'opts.cascade = false' ) - t.end() - /** * @param {Node} node */ @@ -132,21 +128,19 @@ test('opts.cascade', (t) => { } }) -test('example from README', (t) => { +test('example from README', () => { const tree = u('root', [ u('leaf', '1'), u('node', [u('leaf', '2'), u('node', [u('leaf', '3')])]), u('leaf', '4') ]) - t.deepEqual( + assert.deepEqual( filter(tree, predicate), u('root', [u('node', [u('leaf', '2')]), u('leaf', '4')]), 'example from readme' ) - t.end() - /** * @param {Node} node */ diff --git a/tsconfig.json b/tsconfig.json index c7673fb..aae4e06 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,17 @@ { - "include": ["*.js"], + "include": ["**/*.js", "lib/complex-types.d.ts"], + "exclude": ["coverage/", "node_modules/"], "compilerOptions": { - "target": "ES2020", - "lib": ["ES2020"], - "module": "ES2020", - "moduleResolution": "node", - "allowJs": true, "checkJs": true, "declaration": true, "emitDeclarationOnly": true, - "allowSyntheticDefaultImports": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "lib": ["es2020"], + "module": "node16", + "newLine": "lf", "skipLibCheck": true, - "strictNullChecks": true, - "strict": true + "strict": true, + "target": "es2020" } }