Skip to content

Commit 429bcdb

Browse files
committed
Refactor
- Use `unist-util-visit`, no need for `unist-util-visit-parents`. - Extract the common functions of the visitor check and the language assert to a shared file - For the async visitor, create a thunk inside of the visitor so we keep all the type narrowing instead of having to do it again
1 parent 2abdc9b commit 429bcdb

File tree

5 files changed

+104
-94
lines changed

5 files changed

+104
-94
lines changed

web/package-lock.json

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@
3838
"react-tooltip": "^4.5.1",
3939
"react-transition-group": "^4.4.5",
4040
"tailwindcss": "^3.2.4",
41-
"ts-blank-space": "^0.6.1",
42-
"unist-util-visit-parents": "^6.0.1"
41+
"ts-blank-space": "^0.6.1"
4342
},
4443
"devDependencies": {
4544
"@docusaurus/module-type-aliases": "^3.7.0",

web/src/remark/auto-js-code.ts

Lines changed: 55 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -37,91 +37,75 @@ Output:
3737

3838
import type * as md from 'mdast'
3939
import type {} from 'mdast-util-mdx' // Type-only empty import to register MDX types into mdast
40-
import assert from 'node:assert/strict'
4140
import * as path from 'node:path'
4241
import * as prettier from 'prettier'
4342
import { blankSourceFile } from 'ts-blank-space'
4443
import * as ts from 'typescript'
4544
import type { Plugin } from 'unified'
46-
import { visitParents } from 'unist-util-visit-parents'
45+
import { visit } from 'unist-util-visit'
46+
import { assertSupportedLanguage, shouldVisitNode } from './util/code-blocks'
4747

4848
// Wrapped in \b to denote a word boundary
4949
const META_FLAG_REGEX = /\bauto-js\b/
50-
const SUPPORTED_LANGS = new Set(['ts', 'tsx'])
50+
const SUPPORTED_LANGS = new Set(['ts', 'tsx'] as const)
5151

5252
const autoJSCodePlugin: Plugin<[], md.Root> = () => async (tree, file) => {
53-
const nodesToProcess = new Set<{ node: md.Code; ancestors: md.Parents[] }>()
54-
55-
visitParents(tree, 'code', (node, ancestors) => {
56-
if (node.meta && META_FLAG_REGEX.test(node.meta)) {
57-
if (!node.lang) {
58-
file.fail('No language specified', { place: node.position })
59-
}
60-
if (!SUPPORTED_LANGS.has(node.lang)) {
61-
file.fail(`Unsupported language: ${node.lang}`, {
62-
place: node.position,
63-
})
53+
const asyncFns: (() => Promise<void>)[] = []
54+
55+
visit(tree, shouldVisitNode(META_FLAG_REGEX), (node, idx, parent) => {
56+
assertSupportedLanguage(node, file, SUPPORTED_LANGS)
57+
58+
// We save the computation for later
59+
// because `visit` does not allow async visitors.
60+
61+
asyncFns.push(async () => {
62+
// Remove our flag from the meta so other plugins don't trip up
63+
const newMeta = node.meta.replace(META_FLAG_REGEX, '')
64+
65+
const jsCodeBlock = await makeJsCodeBlock(newMeta, node, {
66+
location: file.path,
67+
})
68+
const tsCodeBlock = await makeTsCodeBlock(newMeta, node, {
69+
location: file.path,
70+
})
71+
72+
// The specific structure of the new node was retrieved by copy-pasting
73+
// an example into the MDX playground and inspecting the AST.
74+
// https://mdxjs.com/playground
75+
const newNode: md.RootContent = {
76+
type: 'mdxJsxFlowElement',
77+
name: 'Tabs',
78+
attributes: [
79+
{ type: 'mdxJsxAttribute', name: 'groupId', value: 'js-ts' },
80+
],
81+
children: [
82+
{
83+
type: 'mdxJsxFlowElement',
84+
name: 'TabItem',
85+
attributes: [
86+
{ type: 'mdxJsxAttribute', name: 'value', value: 'js' },
87+
{ type: 'mdxJsxAttribute', name: 'label', value: 'JavaScript' },
88+
],
89+
children: [jsCodeBlock],
90+
},
91+
{
92+
type: 'mdxJsxFlowElement',
93+
name: 'TabItem',
94+
attributes: [
95+
{ type: 'mdxJsxAttribute', name: 'value', value: 'ts' },
96+
{ type: 'mdxJsxAttribute', name: 'label', value: 'TypeScript' },
97+
],
98+
children: [tsCodeBlock],
99+
},
100+
],
64101
}
65102

66-
// We put these aside for processing later
67-
// because `visitParents` does not allow
68-
// async visitors.
69-
nodesToProcess.add({ node, ancestors })
70-
}
71-
})
72-
73-
for (const { node, ancestors } of nodesToProcess) {
74-
const parent = ancestors.at(-1)
75-
assert(parent) // The node is never a `Root` node, so it will always have a parent
76-
assert(node.meta && node.lang) // Already checked in the visitor
77-
78-
// Remove our flag from the meta so other plugins don't trip up
79-
const newMeta = node.meta.replace(META_FLAG_REGEX, '')
80-
81-
const jsCodeBlock = await makeJsCodeBlock(newMeta, node, {
82-
location: file.path,
83-
})
84-
const tsCodeBlock = await makeTsCodeBlock(newMeta, node, {
85-
location: file.path,
103+
// Replace input node for the new ones in the parent's children array
104+
parent.children.splice(idx, 1, newNode)
86105
})
106+
})
87107

88-
// The specific structure of the new node was retrieved by copy-pasting
89-
// an example into the MDX playground and inspecting the AST.
90-
// https://mdxjs.com/playground
91-
const newNode: md.RootContent = {
92-
type: 'mdxJsxFlowElement',
93-
name: 'Tabs',
94-
attributes: [
95-
{ type: 'mdxJsxAttribute', name: 'groupId', value: 'js-ts' },
96-
],
97-
children: [
98-
{
99-
type: 'mdxJsxFlowElement',
100-
name: 'TabItem',
101-
attributes: [
102-
{ type: 'mdxJsxAttribute', name: 'value', value: 'js' },
103-
{ type: 'mdxJsxAttribute', name: 'label', value: 'JavaScript' },
104-
],
105-
children: [jsCodeBlock],
106-
},
107-
{
108-
type: 'mdxJsxFlowElement',
109-
name: 'TabItem',
110-
attributes: [
111-
{ type: 'mdxJsxAttribute', name: 'value', value: 'ts' },
112-
{ type: 'mdxJsxAttribute', name: 'label', value: 'TypeScript' },
113-
],
114-
children: [tsCodeBlock],
115-
},
116-
],
117-
}
118-
119-
const idx = parent.children.findIndex((someNode) => someNode === node)
120-
assert(idx !== -1, "Node not found in parent's children")
121-
122-
// Replace input node for the new ones in the parent's children array
123-
parent.children.splice(idx, 1, newNode)
124-
}
108+
await Promise.all(asyncFns.map((fn) => fn()))
125109
}
126110

127111
export default autoJSCodePlugin

web/src/remark/code-with-hole.ts

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,33 +24,25 @@ Output:
2424

2525
import type * as md from 'mdast'
2626
import type { Plugin } from 'unified'
27-
import { visitParents } from 'unist-util-visit-parents'
27+
import { visit } from 'unist-util-visit'
28+
import { assertSupportedLanguage, shouldVisitNode } from './util/code-blocks'
2829

2930
// Wrapped in \b to denote a word boundary
3031
const META_FLAG_REGEX = /\bwith-hole\b/
3132
const HOLE_IDENTIFIER_REGEX = /\bhole\b/
3233
const HOLE_REPLACEMENT = '/* ... */'
3334

34-
const SUPPORTED_LANGS = new Set(['js', 'jsx', 'ts', 'tsx'])
35+
const SUPPORTED_LANGS = new Set(['js', 'jsx', 'ts', 'tsx'] as const)
3536

3637
const codeWithHolePlugin: Plugin<[], md.Root> = () => (tree, file) => {
37-
visitParents(tree, 'code', (node) => {
38-
if (node.meta && META_FLAG_REGEX.test(node.meta)) {
39-
if (!node.lang) {
40-
file.fail('No language specified', { place: node.position })
41-
}
42-
if (!SUPPORTED_LANGS.has(node.lang)) {
43-
file.fail(`Unsupported language: ${node.lang}`, {
44-
place: node.position,
45-
})
46-
}
47-
48-
// Remove our flag from the meta so other plugins don't trip up
49-
node.meta = node.meta.replace(META_FLAG_REGEX, '')
50-
51-
// Replace hole with ellipsis
52-
node.value = node.value.replace(HOLE_IDENTIFIER_REGEX, HOLE_REPLACEMENT)
53-
}
38+
visit(tree, shouldVisitNode(META_FLAG_REGEX), (node) => {
39+
assertSupportedLanguage(node, file, SUPPORTED_LANGS)
40+
41+
// Remove our flag from the meta so other plugins don't trip up
42+
node.meta = node.meta.replace(META_FLAG_REGEX, '')
43+
44+
// Replace hole with ellipsis
45+
node.value = node.value.replace(HOLE_IDENTIFIER_REGEX, HOLE_REPLACEMENT)
5446
})
5547
}
5648

web/src/remark/util/code-blocks.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type * as md from 'mdast'
2+
import type { VFile } from 'vfile'
3+
4+
/**
5+
* Creates a "check fn" for `unist-util-visit` that
6+
* checks if a node is a code block with a specific meta flag.
7+
*/
8+
export function shouldVisitNode(metaFlag: RegExp) {
9+
return (node: md.Nodes): node is md.Code & { meta: string } =>
10+
node.type === 'code' && node.meta && metaFlag.test(node.meta)
11+
}
12+
13+
/**
14+
* Checks that the code block's language is supported.
15+
* If not, it throws an error with the block's position
16+
* (through VFile#fail)
17+
*/
18+
export function assertSupportedLanguage<T extends string>(
19+
node: md.Code,
20+
file: VFile,
21+
supportedLanguages: Set<T>
22+
): asserts node is md.Code & { lang: T } {
23+
if (!node.lang) {
24+
file.fail(
25+
`No language specified. Please use one of: ${[...supportedLanguages].join(', ')}`,
26+
{ place: node.position }
27+
)
28+
}
29+
30+
if (!(supportedLanguages as Set<string>).has(node.lang)) {
31+
file.fail(
32+
`Unsupported language: ${node.lang}. Please use one of: ${[...supportedLanguages].join(', ')}`,
33+
{ place: node.position }
34+
)
35+
}
36+
}

0 commit comments

Comments
 (0)