From f13ce9393afec9c0d9cdb573327848b2a3e827a2 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Sun, 25 Jul 2021 18:00:13 +0200 Subject: [PATCH 1/3] =?UTF-8?q?Add=20types=20to=20narrow=20what=E2=80=99s?= =?UTF-8?q?=20given=20to=20`visitor`=20based=20on=20`tree`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.js | 6 ++-- index.test-d.ts | 73 ++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index 771f122..aeb0985 100644 --- a/index.js +++ b/index.js @@ -31,9 +31,9 @@ export {CONTINUE, SKIP, EXIT} export const visit = /** * @type {( - * ((tree: Node, test: T['type']|Partial|import('unist-util-is').TestFunctionPredicate|Array.|import('unist-util-is').TestFunctionPredicate>, visitor: Visitor, reverse?: boolean) => void) & - * ((tree: Node, test: Test, visitor: Visitor, reverse?: boolean) => void) & - * ((tree: Node, visitor: Visitor, reverse?: boolean) => void) + * ((tree: Node, test: Needle['type']|Partial|import('unist-util-is').TestFunctionPredicate|Array.|import('unist-util-is').TestFunctionPredicate>, visitor: Visitor, reverse?: boolean) => void) & + * ((tree: Tree, test: Test, visitor: Visitor>, reverse?: boolean) => void) & + * ((tree: Tree, visitor: Visitor>, reverse?: boolean) => void) * )} */ ( diff --git a/index.test-d.ts b/index.test-d.ts index f117507..f05a967 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,7 +1,8 @@ /* eslint-disable @typescript-eslint/no-confusing-void-expression, @typescript-eslint/no-empty-function */ +import assert from 'node:assert' import {expectError} from 'tsd' -import {Node, Parent} from 'unist' +import {Node, Parent, Literal} from 'unist' import {visit, SKIP, EXIT, CONTINUE} from './index.js' /* Setup */ @@ -10,12 +11,6 @@ const sampleTree = { children: [{type: 'heading', depth: 1, children: []}] } -interface Heading extends Parent { - type: 'heading' - depth: number - children: Node[] -} - interface Element extends Parent { type: 'element' tagName: string @@ -24,6 +19,40 @@ interface Element extends Parent { children: Node[] } +interface Root extends Parent { + type: 'root' + children: Flow[] +} + +type Flow = Blockquote | Heading | Paragraph + +interface Blockquote extends Parent { + type: 'blockquote' + children: Flow[] +} + +interface Heading extends Parent { + type: 'heading' + depth: number + children: Phrasing[] +} + +interface Paragraph extends Parent { + type: 'paragraph' + children: Phrasing[] +} + +type Phrasing = Text | Emphasis + +interface Emphasis extends Parent { + type: 'emphasis' + children: Phrasing[] +} + +interface Text extends Literal { + type: 'text' + value: string +} const isNode = (node: unknown): node is Node => typeof node === 'object' && node !== null && 'type' in node const headingTest = (node: unknown): node is Heading => @@ -101,3 +130,33 @@ visit(sampleTree, 'heading', (_) => [SKIP, 1]) visit(sampleTree, 'heading', (_) => [SKIP]) expectError(visit(sampleTree, 'heading', (_) => [1])) expectError(visit(sampleTree, 'heading', (_) => ['random', 1])) + +/* Should infer children from the given tree. */ + +const typedTree: Root = { + type: 'root', + children: [ + { + type: 'blockquote', + children: [{type: 'paragraph', children: [{type: 'text', value: 'a'}]}] + }, + { + type: 'paragraph', + children: [ + { + type: 'emphasis', + children: [{type: 'emphasis', children: [{type: 'text', value: 'b'}]}] + }, + {type: 'text', value: 'c'} + ] + } + ] +} + +visit(typedTree, (_: Root | Flow | Phrasing) => {}) +const blockquote = typedTree.children[0] +assert(blockquote.type === 'blockquote') +visit(blockquote, (_: Flow | Phrasing) => {}) +const paragraph = typedTree.children[1] +assert(paragraph.type === 'paragraph') +visit(paragraph, (_: Paragraph | Phrasing) => {}) From 20caa53735c319d39c9cb0cb738e8e67f2c992d1 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 28 Jul 2021 10:46:38 +0200 Subject: [PATCH 2/3] Update for changes in `unist-util-visit-parents` --- index.js | 23 ++++--- index.test-d.ts | 179 +++++++++++++++++++++++++++--------------------- 2 files changed, 115 insertions(+), 87 deletions(-) diff --git a/index.js b/index.js index aeb0985..9598d2d 100644 --- a/index.js +++ b/index.js @@ -28,22 +28,27 @@ import {visitParents, CONTINUE, SKIP, EXIT} from 'unist-util-visit-parents' export {CONTINUE, SKIP, EXIT} +/** + * Visit children of tree which pass a test + * + * @param tree Abstract syntax tree to walk + * @param test Test, optional + * @param visitor Function to run for each node + * @param reverse Fisit the tree in reverse, defaults to false + */ export const visit = /** * @type {( - * ((tree: Node, test: Needle['type']|Partial|import('unist-util-is').TestFunctionPredicate|Array.|import('unist-util-is').TestFunctionPredicate>, visitor: Visitor, reverse?: boolean) => void) & - * ((tree: Tree, test: Test, visitor: Visitor>, reverse?: boolean) => void) & - * ((tree: Tree, visitor: Visitor>, reverse?: boolean) => void) + * ((tree: Tree, test: Check, visitor: Visitor, Check>>, reverse?: boolean) => void) & + * ((tree: Tree, visitor: Visitor>, reverse?: boolean) => void) * )} */ ( /** - * Visit children of tree which pass a test - * - * @param {Node} tree Abstract syntax tree to walk - * @param {Test} test test Test node - * @param {Visitor} visitor Function to run for each node - * @param {boolean} [reverse] Fisit the tree in reverse, defaults to false + * @param {Node} tree + * @param {Test} test + * @param {Visitor} visitor + * @param {boolean} [reverse] */ function (tree, test, visitor, reverse) { if (typeof test === 'function' && typeof visitor !== 'function') { diff --git a/index.test-d.ts b/index.test-d.ts index f05a967..fa13772 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,16 +1,36 @@ /* eslint-disable @typescript-eslint/no-confusing-void-expression, @typescript-eslint/no-empty-function */ -import assert from 'node:assert' -import {expectError} from 'tsd' +import {expectError, expectType} from 'tsd' import {Node, Parent, Literal} from 'unist' +import {is} from 'unist-util-is' import {visit, SKIP, EXIT, CONTINUE} from './index.js' /* Setup */ -const sampleTree = { +const sampleTree: Root = { type: 'root', children: [{type: 'heading', depth: 1, children: []}] } +const complexTree: Root = { + type: 'root', + children: [ + { + type: 'blockquote', + children: [{type: 'paragraph', children: [{type: 'text', value: 'a'}]}] + }, + { + type: 'paragraph', + children: [ + { + type: 'emphasis', + children: [{type: 'emphasis', children: [{type: 'text', value: 'b'}]}] + }, + {type: 'text', value: 'c'} + ] + } + ] +} + interface Element extends Parent { type: 'element' tagName: string @@ -19,6 +39,8 @@ interface Element extends Parent { children: Node[] } +type Content = Flow | Phrasing + interface Root extends Parent { type: 'root' children: Flow[] @@ -53,6 +75,7 @@ interface Text extends Literal { type: 'text' value: string } + const isNode = (node: unknown): node is Node => typeof node === 'object' && node !== null && 'type' in node const headingTest = (node: unknown): node is Heading => @@ -65,98 +88,98 @@ expectError(visit()) expectError(visit(sampleTree)) /* Visit without test. */ -visit(sampleTree, (_) => {}) -visit(sampleTree, (_: Node) => {}) -expectError(visit(sampleTree, (_: Element) => {})) -expectError(visit(sampleTree, (_: Heading) => {})) +visit(sampleTree, (node) => { + expectType(node) +}) /* Visit with type test. */ -visit(sampleTree, 'heading', (_) => {}) -visit(sampleTree, 'heading', (_: Heading) => {}) -expectError(visit(sampleTree, 'not-a-heading', (_: Heading) => {})) -expectError(visit(sampleTree, 'element', (_: Heading) => {})) - -visit(sampleTree, 'element', (_) => {}) -visit(sampleTree, 'element', (_: Element) => {}) -expectError(visit(sampleTree, 'not-an-element', (_: Element) => {})) +visit(sampleTree, 'heading', (node) => { + expectType(node) +}) +visit(sampleTree, 'element', (node) => { + // Not in tree. + expectType(node) +}) expectError(visit(sampleTree, 'heading', (_: Element) => {})) /* Visit with object test. */ -visit(sampleTree, {type: 'heading'}, (_) => {}) -visit(sampleTree, {random: 'property'}, (_) => {}) - -visit(sampleTree, {type: 'heading'}, (_: Heading) => {}) -visit(sampleTree, {type: 'heading', depth: 2}, (_: Heading) => {}) -expectError(visit(sampleTree, {type: 'element'}, (_: Heading) => {})) -expectError( - visit(sampleTree, {type: 'heading', depth: '2'}, (_: Heading) => {}) -) - -visit(sampleTree, {type: 'element'}, (_: Element) => {}) -visit(sampleTree, {type: 'element', tagName: 'section'}, (_: Element) => {}) - -expectError(visit(sampleTree, {type: 'heading'}, (_: Element) => {})) - -expectError( - visit(sampleTree, {type: 'element', tagName: true}, (_: Element) => {}) -) +visit(sampleTree, {depth: 1}, (node) => { + expectType(node) +}) +visit(sampleTree, {random: 'property'}, (node) => { + expectType(node) +}) +visit(sampleTree, {type: 'heading', depth: '2'}, (node) => { + // Not in tree. + expectType(node) +}) +visit(sampleTree, {tagName: 'section'}, (node) => { + // Not in tree. + expectType(node) +}) +visit(sampleTree, {type: 'element', tagName: 'section'}, (node) => { + // Not in tree. + expectType(node) +}) /* Visit with function test. */ -visit(sampleTree, headingTest, (_) => {}) -visit(sampleTree, headingTest, (_: Heading) => {}) +visit(sampleTree, headingTest, (node) => { + expectType(node) +}) expectError(visit(sampleTree, headingTest, (_: Element) => {})) - -visit(sampleTree, elementTest, (_) => {}) -visit(sampleTree, elementTest, (_: Element) => {}) -expectError(visit(sampleTree, elementTest, (_: Heading) => {})) +visit(sampleTree, elementTest, (node) => { + // Not in tree. + expectType(node) +}) /* Visit with array of tests. */ -visit(sampleTree, ['ParagraphNode', {type: 'element'}, headingTest], (_) => {}) +visit(sampleTree, ['heading', {depth: 1}, headingTest], (node) => { + // Unfortunately TS casts things in arrays too vague. + expectType(node) +}) /* Visit returns action. */ -visit(sampleTree, 'heading', (_) => CONTINUE) -visit(sampleTree, 'heading', (_) => EXIT) -visit(sampleTree, 'heading', (_) => SKIP) -expectError(visit(sampleTree, 'heading', (_) => 'random')) +visit(sampleTree, () => CONTINUE) +visit(sampleTree, () => EXIT) +visit(sampleTree, () => SKIP) +expectError(visit(sampleTree, () => 'random')) /* Visit returns index. */ -visit(sampleTree, 'heading', (_) => 0) -visit(sampleTree, 'heading', (_) => 1) +visit(sampleTree, () => 0) +visit(sampleTree, () => 1) /* Visit returns tuple. */ -visit(sampleTree, 'heading', (_) => [CONTINUE, 1]) -visit(sampleTree, 'heading', (_) => [EXIT, 1]) -visit(sampleTree, 'heading', (_) => [SKIP, 1]) -visit(sampleTree, 'heading', (_) => [SKIP]) -expectError(visit(sampleTree, 'heading', (_) => [1])) -expectError(visit(sampleTree, 'heading', (_) => ['random', 1])) +visit(sampleTree, () => [CONTINUE, 1]) +visit(sampleTree, () => [EXIT, 1]) +visit(sampleTree, () => [SKIP, 1]) +visit(sampleTree, () => [SKIP]) +expectError(visit(sampleTree, () => [1])) +expectError(visit(sampleTree, () => ['random', 1])) /* Should infer children from the given tree. */ - -const typedTree: Root = { - type: 'root', - children: [ - { - type: 'blockquote', - children: [{type: 'paragraph', children: [{type: 'text', value: 'a'}]}] - }, - { - type: 'paragraph', - children: [ - { - type: 'emphasis', - children: [{type: 'emphasis', children: [{type: 'text', value: 'b'}]}] - }, - {type: 'text', value: 'c'} - ] - } - ] +visit(complexTree, (node) => { + expectType(node) +}) + +const blockquote = complexTree.children[0] +if (is
(blockquote, 'blockquote')) { + visit(blockquote, (node) => { + expectType(node) + }) } -visit(typedTree, (_: Root | Flow | Phrasing) => {}) -const blockquote = typedTree.children[0] -assert(blockquote.type === 'blockquote') -visit(blockquote, (_: Flow | Phrasing) => {}) -const paragraph = typedTree.children[1] -assert(paragraph.type === 'paragraph') -visit(paragraph, (_: Paragraph | Phrasing) => {}) +const paragraph = complexTree.children[1] +if (is(paragraph, 'paragraph')) { + visit(paragraph, (node) => { + expectType(node) + }) + + const child = paragraph.children[1] + + if (is(child, 'emphasis')) { + visit(child, 'blockquote', (node) => { + // `blockquote` does not exist in phrasing. + expectType(node) + }) + } +} From ae8e0d156d0f55dd8dc6c9ec3e42f4273d31be71 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 30 Jul 2021 10:01:41 +0200 Subject: [PATCH 3/3] Update `unist-util-visit-parents` --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b1d4f39..ac9e735 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^4.0.0" + "unist-util-visit-parents": "^5.0.0" }, "devDependencies": { "@types/tape": "^4.0.0",