From d63b8a6a4fee15f96e0c324169216e129ffbd034 Mon Sep 17 00:00:00 2001 From: Christian Murphy Date: Wed, 31 Jul 2019 08:40:57 -0700 Subject: [PATCH] Add typings Related-to: unifiedjs/unified#65. Related-to: syntax-tree/unist-util-visit-parents#7. Closes GH-15. Reviewed-by: Junyoung Choi Reviewed-by: Titus Wormer --- package.json | 16 ++- types/index.d.ts | 88 ++++++++++++ types/tsconfig.json | 14 ++ types/tslint.json | 15 +++ types/unist-util-visit-tests.ts | 231 ++++++++++++++++++++++++++++++++ 5 files changed, 360 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-tests.ts diff --git a/package.json b/package.json index 44b8bd4..1ce8d3a 100644 --- a/package.json +++ b/package.json @@ -25,13 +25,18 @@ "Richard Gibson " ], "files": [ - "index.js" + "index.js", + "types/index.d.ts" ], + "types": "types/index.d.ts", "dependencies": { - "unist-util-visit-parents": "^2.0.0" + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0", + "unist-util-visit-parents": "^3.0.0" }, "devDependencies": { "browserify": "^16.0.0", + "dtslint": "^0.9.0", "nyc": "^14.0.0", "prettier": "^1.0.0", "remark": "^10.0.0", @@ -39,16 +44,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 . -s unistUtilVisit > unist-util-visit.js", "build-mangle": "browserify . -s unistUtilVisit -p tinyify > unist-util-visit.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..09a9c37 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,88 @@ +// TypeScript Version: 3.5 + +import {Node, Parent} from 'unist' +import {Test} from 'unist-util-is' +import { + Action, + ActionTuple, + Continue, + Exit, + Index, + Skip +} from 'unist-util-visit-parents' + +declare namespace visit { + /** + * Invoked when a node (matching test, if given) is found. + * Visitors are free to transform node. + * They can also transform the parent of node. + * 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 the parent still result in them being traversed. + * + * @param node Found node + * @param index Position of found node within Parent + * @param parent Parent of found 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, + index: number, + parent: Node + ) => void | Action | Index | ActionTuple +} + +declare const visit: { + /** + * 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: visit.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: visit.Visitor, reverse?: boolean): void + + /** + * Continue traversing as normal + */ + CONTINUE: Continue + + /** + * Do not traverse this node’s children + */ + SKIP: Skip + + /** + * Stop traversing immediately + */ + EXIT: Exit +} + +export = visit diff --git a/types/tsconfig.json b/types/tsconfig.json new file mode 100644 index 0000000..4d5222d --- /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": ["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-tests.ts b/types/unist-util-visit-tests.ts new file mode 100644 index 0000000..d066383 --- /dev/null +++ b/types/unist-util-visit-tests.ts @@ -0,0 +1,231 @@ +import {Node, Parent} from 'unist' +import unified = require('unified') +import visit = require('unist-util-visit') + +/*=== 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' + +/*=== 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 + + /*=== 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 +})