From c0c0d553f83382c3e1764cdabf7c5ba104d3ee1a Mon Sep 17 00:00:00 2001 From: Christian Murphy Date: Mon, 29 Jul 2019 20:45:39 -0700 Subject: [PATCH] types: add typings for unist-util-visit-parents --- package.json | 15 +- types/index.d.ts | 111 ++++++++++++ types/tsconfig.json | 14 ++ types/tslint.json | 15 ++ types/unist-util-visit-parents.test.ts | 241 +++++++++++++++++++++++++ 5 files changed, 392 insertions(+), 4 deletions(-) create mode 100644 types/index.d.ts create mode 100644 types/tsconfig.json create mode 100644 types/tslint.json create mode 100644 types/unist-util-visit-parents.test.ts diff --git a/package.json b/package.json index 26e1872..7ce7513 100644 --- a/package.json +++ b/package.json @@ -16,13 +16,17 @@ "Titus Wormer (https://wooorm.com)" ], "files": [ - "index.js" + "index.js", + "types/index.d.ts" ], + "types": "types/index.d.ts", "dependencies": { - "unist-util-is": "^3.0.0" + "@types/unist": "^2.0.3", + "unist-util-is": "^4.0.0" }, "devDependencies": { "browserify": "^16.0.0", + "dtslint": "^0.9.0", "nyc": "^14.0.0", "prettier": "^1.0.0", "remark": "^10.0.0", @@ -30,16 +34,19 @@ "remark-preset-wooorm": "^5.0.0", "tape": "^4.0.0", "tinyify": "^2.0.0", + "typescript": "^3.5.3", + "unified": "^8.3.2", "xo": "^0.24.0" }, "scripts": { - "format": "remark . -qfo && prettier --write \"**/*.js\" && xo --fix", + "format": "remark . -qfo && prettier --write \"**/*.{js,ts}\" && xo --fix", "build-bundle": "browserify index.js -s unistUtilVisitParents > unist-util-visit-parents.js", "build-mangle": "browserify index.js -s unistUtilVisitParents -p tinyify > unist-util-visit-parents.min.js", "build": "npm run build-bundle && npm run build-mangle", "test-api": "node test", "test-coverage": "nyc --reporter lcov tape test.js", - "test": "npm run format && npm run build && npm run test-coverage" + "test-types": "dtslint types", + "test": "npm run format && npm run build && npm run test-coverage && npm run test-types" }, "nyc": { "check-coverage": true, diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..53a37c4 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,111 @@ +// TypeScript Version: 3.5 + +import {Node, Parent} from 'unist' +import {Test} from 'unist-util-is' + +declare namespace visitParents { + /** + * Continue traversing as normal + */ + type Continue = true + + /** + * Do not traverse this node’s children + */ + type Skip = 'skip' + + /** + * Stop traversing immediately + */ + type Exit = false + + /** + * Union of the action types + */ + type Action = Continue | Skip | Exit + + /** + * List with one or two values, the first an action, the second an index. + */ + type ActionTuple = [Action, Index] + + /** + * Move to the sibling at index next (after node itself is completely traversed). + * Useful if mutating the tree, such as removing the node the visitor is currently on, + * or any of its previous siblings (or next siblings, in case of reverse) + * Results less than 0 or greater than or equal to children.length stop traversing the parent + */ + type Index = number + + /** + * Invoked when a node (matching test, if given) is found. + * Visitors are free to transform node. + * They can also transform the parent of node (the last of ancestors). + * Replacing node itself, if visit.SKIP is not returned, still causes its descendants to be visited. + * If adding or removing previous siblings (or next siblings, in case of reverse) of node, + * visitor should return a new index (number) to specify the sibling to traverse after node is traversed. + * Adding or removing next siblings of node (or previous siblings, in case of reverse) + * is handled as expected without needing to return a new index. + * Removing the children property of an ancestor still results in them being traversed. + * + * @param node Found node + * @param ancestors Ancestors of node + * @paramType V node type found + * @returns + * When Action is passed, treated as a tuple of [Action] + * When Index is passed, treated as a tuple of [CONTINUE, Index] + * When ActionTuple is passed, + * Note that passing a tuple only makes sense if the action is SKIP. + * If the action is EXIT, that action can be returned. + * If the action is CONTINUE, index can be returned. + */ + type Visitor = ( + node: V, + ancestors: Node[] + ) => void | Action | Index | ActionTuple +} + +declare const visitParents: { + /** + * Visit children of tree which pass a test + * + * @param tree abstract syntax tree to visit + * @param test test node + * @param visitor function to run for each node + * @param reverse visit the tree in reverse, defaults to false + * @typeParam T tree node + * @typeParam V node type found + */ + ( + tree: Node, + test: Test | Array>, + visitor: visitParents.Visitor, + reverse?: boolean + ): void + + /** + * Visit children of a tree + * + * @param tree abstract syntax tree to visit + * @param visitor function to run for each node + * @param reverse visit the tree in reverse, defaults to false + */ + (tree: Node, visitor: visitParents.Visitor, reverse?: boolean): void + + /** + * Continue traversing as normal + */ + CONTINUE: visitParents.Continue + + /** + * Do not traverse this node’s children + */ + SKIP: visitParents.Skip + + /** + * Stop traversing immediately + */ + EXIT: visitParents.Exit +} + +export = visitParents diff --git a/types/tsconfig.json b/types/tsconfig.json new file mode 100644 index 0000000..c371e62 --- /dev/null +++ b/types/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "lib": ["es2015"], + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "baseUrl": ".", + "paths": { + "unist-util-visit-parents": ["index.d.ts"] + } + } +} diff --git a/types/tslint.json b/types/tslint.json new file mode 100644 index 0000000..aa59581 --- /dev/null +++ b/types/tslint.json @@ -0,0 +1,15 @@ +{ + "extends": "dtslint/dtslint.json", + "rules": { + "callable-types": false, + "max-line-length": false, + "no-redundant-jsdoc": false, + "no-void-expression": false, + "only-arrow-functions": false, + "semicolon": false, + "unified-signatures": false, + "whitespace": false, + "interface-over-type-literal": false, + "no-unnecessary-generics": false + } +} diff --git a/types/unist-util-visit-parents.test.ts b/types/unist-util-visit-parents.test.ts new file mode 100644 index 0000000..2af7752 --- /dev/null +++ b/types/unist-util-visit-parents.test.ts @@ -0,0 +1,241 @@ +import {Node, Parent} from 'unist' +import unified = require('unified') +import visit = require('unist-util-visit-parents') + +/*=== setup ===*/ +const sampleTree = { + type: 'root', + children: [ + { + type: 'heading', + depth: 1, + children: [] + } + ] +} + +interface Heading extends Parent { + type: 'heading' + depth: number + children: Node[] +} + +interface Element extends Parent { + type: 'element' + tagName: string + properties: { + [key: string]: unknown + } + content: Node + children: Node[] +} + +const isNode = (node: unknown): node is Node => + typeof node === 'object' && !!node && 'type' in node +const headingTest = (node: unknown): node is Heading => + isNode(node) && node.type === 'heading' +const elementTest = (node: unknown): node is Element => + isNode(node) && node.type === 'element' + +/*=== constants ===*/ +const cont: visit.Continue = visit.CONTINUE +const skip: visit.Skip = visit.SKIP +const exit: visit.Exit = visit.EXIT + +/*=== missing params ===*/ +// $ExpectError +visit() +// $ExpectError +visit(sampleTree) + +/*=== visit without test ===*/ +visit(sampleTree, node => {}) +visit(sampleTree, (node: Node) => {}) +// $ExpectError +visit(sampleTree, (node: Element) => {}) +// $ExpectError +visit(sampleTree, (node: Heading) => {}) + +/*=== visit with type test ===*/ +visit(sampleTree, 'heading', node => {}) +visit(sampleTree, 'heading', (node: Heading) => {}) +// $ExpectError +visit(sampleTree, 'not-a-heading', (node: Heading) => {}) +// $ExpectError +visit(sampleTree, 'element', (node: Heading) => {}) + +visit(sampleTree, 'element', node => {}) +visit(sampleTree, 'element', (node: Element) => {}) +// $ExpectError +visit(sampleTree, 'not-an-element', (node: Element) => {}) +// $ExpectError +visit(sampleTree, 'heading', (node: Element) => {}) + +/*=== visit with object test ===*/ +visit(sampleTree, {type: 'heading'}, node => {}) +visit(sampleTree, {random: 'property'}, node => {}) + +visit(sampleTree, {type: 'heading'}, (node: Heading) => {}) +visit(sampleTree, {type: 'heading', depth: 2}, (node: Heading) => {}) +// $ExpectError +visit(sampleTree, {type: 'element'}, (node: Heading) => {}) +// $ExpectError +visit(sampleTree, {type: 'heading', depth: '2'}, (node: Heading) => {}) + +visit(sampleTree, {type: 'element'}, (node: Element) => {}) +visit(sampleTree, {type: 'element', tagName: 'section'}, (node: Element) => {}) +// $ExpectError +visit(sampleTree, {type: 'heading'}, (node: Element) => {}) +// $ExpectError +visit(sampleTree, {type: 'element', tagName: true}, (node: Element) => {}) + +/*=== visit with function test ===*/ +visit(sampleTree, headingTest, node => {}) +visit(sampleTree, headingTest, (node: Heading) => {}) +// $ExpectError +visit(sampleTree, headingTest, (node: Element) => {}) + +visit(sampleTree, elementTest, node => {}) +visit(sampleTree, elementTest, (node: Element) => {}) +// $ExpectError +visit(sampleTree, elementTest, (node: Heading) => {}) + +/*=== visit with array of tests ===*/ +visit(sampleTree, ['ParagraphNode', {type: 'element'}, headingTest], node => {}) + +/*=== visit returns action ===*/ +visit(sampleTree, 'heading', node => visit.CONTINUE) +visit(sampleTree, 'heading', node => visit.EXIT) +visit(sampleTree, 'heading', node => visit.SKIP) +visit(sampleTree, 'heading', node => true) +visit(sampleTree, 'heading', node => false) +visit(sampleTree, 'heading', node => 'skip') +// $ExpectError +visit(sampleTree, 'heading', node => 'random') + +/*=== visit returns index ===*/ +visit(sampleTree, 'heading', node => 0) +visit(sampleTree, 'heading', node => 1) + +/*=== visit returns tuple ===*/ +visit(sampleTree, 'heading', node => [visit.CONTINUE, 1]) +visit(sampleTree, 'heading', node => [visit.EXIT, 1]) +visit(sampleTree, 'heading', node => [visit.SKIP, 1]) +visit(sampleTree, 'heading', node => [true, 1]) +visit(sampleTree, 'heading', node => [false, 1]) +visit(sampleTree, 'heading', node => ['skip', 1]) +// $ExpectError +visit(sampleTree, 'heading', node => ['skip']) +// $ExpectError +visit(sampleTree, 'heading', node => [1]) +// $ExpectError +visit(sampleTree, 'heading', node => ['random', 1]) + +/*=== usage as unified plugin ===*/ +unified().use(() => sampleTree => { + // duplicates the above type tests but passes in the unified transformer input + + /*=== constants ===*/ + const cont: visit.Continue = visit.CONTINUE + const skip: visit.Skip = visit.SKIP + const exit: visit.Exit = visit.EXIT + + /*=== missing params ===*/ + // $ExpectError + visit() + // $ExpectError + visit(sampleTree) + + /*=== visit without test ===*/ + visit(sampleTree, node => {}) + visit(sampleTree, (node: Node) => {}) + // $ExpectError + visit(sampleTree, (node: Element) => {}) + // $ExpectError + visit(sampleTree, (node: Heading) => {}) + + /*=== visit with type test ===*/ + visit(sampleTree, 'heading', node => {}) + visit(sampleTree, 'heading', (node: Heading) => {}) + // $ExpectError + visit(sampleTree, 'not-a-heading', (node: Heading) => {}) + // $ExpectError + visit(sampleTree, 'element', (node: Heading) => {}) + + visit(sampleTree, 'element', node => {}) + visit(sampleTree, 'element', (node: Element) => {}) + // $ExpectError + visit(sampleTree, 'not-an-element', (node: Element) => {}) + // $ExpectError + visit(sampleTree, 'heading', (node: Element) => {}) + + /*=== visit with object test ===*/ + visit(sampleTree, {type: 'heading'}, node => {}) + visit(sampleTree, {random: 'property'}, node => {}) + + visit(sampleTree, {type: 'heading'}, (node: Heading) => {}) + visit(sampleTree, {type: 'heading', depth: 2}, (node: Heading) => {}) + // $ExpectError + visit(sampleTree, {type: 'element'}, (node: Heading) => {}) + // $ExpectError + visit(sampleTree, {type: 'heading', depth: '2'}, (node: Heading) => {}) + + visit(sampleTree, {type: 'element'}, (node: Element) => {}) + visit( + sampleTree, + {type: 'element', tagName: 'section'}, + (node: Element) => {} + ) + // $ExpectError + visit(sampleTree, {type: 'heading'}, (node: Element) => {}) + // $ExpectError + visit(sampleTree, {type: 'element', tagName: true}, (node: Element) => {}) + + /*=== visit with function test ===*/ + visit(sampleTree, headingTest, node => {}) + visit(sampleTree, headingTest, (node: Heading) => {}) + // $ExpectError + visit(sampleTree, headingTest, (node: Element) => {}) + + visit(sampleTree, elementTest, node => {}) + visit(sampleTree, elementTest, (node: Element) => {}) + // $ExpectError + visit(sampleTree, elementTest, (node: Heading) => {}) + + /*=== visit with array of tests ===*/ + visit( + sampleTree, + ['ParagraphNode', {type: 'element'}, headingTest], + node => {} + ) + + /*=== visit returns action ===*/ + visit(sampleTree, 'heading', node => visit.CONTINUE) + visit(sampleTree, 'heading', node => visit.EXIT) + visit(sampleTree, 'heading', node => visit.SKIP) + visit(sampleTree, 'heading', node => true) + visit(sampleTree, 'heading', node => false) + visit(sampleTree, 'heading', node => 'skip') + // $ExpectError + visit(sampleTree, 'heading', node => 'random') + + /*=== visit returns index ===*/ + visit(sampleTree, 'heading', node => 0) + visit(sampleTree, 'heading', node => 1) + + /*=== visit returns tuple ===*/ + visit(sampleTree, 'heading', node => [visit.CONTINUE, 1]) + visit(sampleTree, 'heading', node => [visit.EXIT, 1]) + visit(sampleTree, 'heading', node => [visit.SKIP, 1]) + visit(sampleTree, 'heading', node => [true, 1]) + visit(sampleTree, 'heading', node => [false, 1]) + visit(sampleTree, 'heading', node => ['skip', 1]) + // $ExpectError + visit(sampleTree, 'heading', node => ['skip']) + // $ExpectError + visit(sampleTree, 'heading', node => [1]) + // $ExpectError + visit(sampleTree, 'heading', node => ['random', 1]) + + return sampleTree +})