@doc-kit/core 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/package.json +21 -8
- package/src/generators/ast/generate.mjs +5 -4
- package/src/generators/ast-js/generate.mjs +1 -1
- package/src/generators/index.mjs +1 -0
- package/src/generators/json-simple/generate.mjs +3 -3
- package/src/generators/loader.mjs +8 -3
- package/src/generators/metadata/generate.mjs +3 -2
- package/src/generators/metadata/types.d.ts +19 -0
- package/src/generators/metadata/utils/parse.mjs +10 -9
- package/src/generators/metadata/utils/resolveTypes.mjs +3 -2
- package/src/generators/metadata/utils/slugger.mjs +1 -1
- package/src/generators/metadata/utils/transformers.mjs +5 -4
- package/src/generators/metadata/utils/visitors.mjs +4 -3
- package/src/generators/metadata/utils/yaml.mjs +1 -1
- package/src/generators/pipeline.mjs +169 -0
- package/src/generators/types.d.ts +20 -0
- package/src/generators.mjs +19 -12
- package/src/logger/transports/console.mjs +2 -2
- package/src/logger/transports/github.mjs +3 -3
- package/src/logger/utils/colors.mjs +1 -1
- package/src/parsers/markdown.mjs +6 -7
- package/src/threading/chunk-worker.mjs +2 -2
- package/src/threading/index.mjs +1 -1
- package/src/threading/parallel.mjs +1 -1
- package/src/utils/configuration/constants.mjs +13 -0
- package/src/utils/configuration/index.mjs +13 -8
- package/src/utils/configuration/types.d.ts +1 -1
- package/src/utils/generators.mjs +30 -0
- package/src/utils/inline.mjs +82 -0
- package/src/utils/queries/index.mjs +9 -2
- package/src/utils/queries/utils.mjs +35 -11
- package/src/utils/remark-shiki.mjs +29 -0
- package/src/utils/remark.mjs +42 -49
- package/src/utils/signature/parseList.mjs +4 -3
- package/src/utils/type-annotations/hast.mjs +0 -58
- package/src/utils/type-annotations/highlighted.mjs +64 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @doc-kit/core
|
|
2
2
|
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#1090](https://github.com/nodejs/doc-kit/pull/1090) [`0a9dc37`](https://github.com/nodejs/doc-kit/commit/0a9dc37dc96f8044c76c3ca9b35e97895a87d1fd) Thanks [@avivkeller](https://github.com/avivkeller)! - perf: build the site one page at a time, and `all.html` from the module pages
|
|
8
|
+
|
|
9
|
+
- [#1057](https://github.com/nodejs/doc-kit/pull/1057) [`da5d8e6`](https://github.com/nodejs/doc-kit/commit/da5d8e63d0d33fe7a95c46afb8d5140d576827b4) Thanks [@avivkeller](https://github.com/avivkeller)! - feat: `dependent` generators and the `section-pages` generator
|
|
10
|
+
|
|
11
|
+
## 1.0.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [#1023](https://github.com/nodejs/doc-kit/pull/1023) [`27a412a`](https://github.com/nodejs/doc-kit/commit/27a412ac35ab06e977b03b559e6525c0c5c5356c) Thanks [@btea](https://github.com/btea)! - Fix empty paragraphs in API docs when a typed parameter list contains trailing non-parameter items
|
|
16
|
+
|
|
17
|
+
When a loose markdown list in the API docs starts with typed parameters (e.g. `actual`, `expected`, `Returns`) but also contains plain prose bullets (e.g. algorithm complexity notes), the entire list was previously treated as a parameter signature table. The non-parameter items had no name or type, causing them to render as empty `<section>` blocks on the built site.
|
|
18
|
+
|
|
19
|
+
The fix splits the list at the first non-parameter item: typed items become the `FunctionSignature` table, and the remaining items render as regular markdown content.
|
|
20
|
+
|
|
21
|
+
- [#1045](https://github.com/nodejs/doc-kit/pull/1045) [`4a22c85`](https://github.com/nodejs/doc-kit/commit/4a22c85dca917f52c773aba084191da7c622687e) Thanks [@avivkeller](https://github.com/avivkeller)! - MDX nodes in the HTML-string pipelines are now dropped, and do not crash.
|
|
22
|
+
|
|
23
|
+
- [#1049](https://github.com/nodejs/doc-kit/pull/1049) [`8cc1fd0`](https://github.com/nodejs/doc-kit/commit/8cc1fd010ac38b16dd3c9bcd422c0ce0e5eb483d) Thanks [@avivkeller](https://github.com/avivkeller)! - Render markdown summaries as markup instead of as their source
|
|
24
|
+
|
|
3
25
|
## 1.0.0
|
|
4
26
|
|
|
5
27
|
### Major Changes
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doc-kit/core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"description": "The doc-kit engine: the pipeline and shared APIs that power the doc-kit generators and CLI",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/nodejs/doc-kit.git",
|
|
9
9
|
"directory": "packages/core"
|
|
10
10
|
},
|
|
11
|
+
"license": "MIT",
|
|
11
12
|
"main": "./src/generators.mjs",
|
|
12
13
|
"exports": {
|
|
13
14
|
".": "./src/generators.mjs",
|
|
@@ -18,7 +19,19 @@
|
|
|
18
19
|
"./package.json": "./package.json",
|
|
19
20
|
"./shiki.config.mjs": "./shiki.config.mjs",
|
|
20
21
|
"./src/*": "./src/*",
|
|
21
|
-
"./*":
|
|
22
|
+
"./*": [
|
|
23
|
+
"./src/*",
|
|
24
|
+
"./src/*.d.ts"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"imports": {
|
|
28
|
+
"#generators/*": "./src/generators/*",
|
|
29
|
+
"#logger/*": "./src/logger/*",
|
|
30
|
+
"#parsers/*": [
|
|
31
|
+
"./src/parsers/*",
|
|
32
|
+
"./src/parsers/*.d.ts"
|
|
33
|
+
],
|
|
34
|
+
"#utils/*": "./src/utils/*"
|
|
22
35
|
},
|
|
23
36
|
"files": [
|
|
24
37
|
"src",
|
|
@@ -31,7 +44,7 @@
|
|
|
31
44
|
],
|
|
32
45
|
"dependencies": {
|
|
33
46
|
"@actions/core": "^3.0.0",
|
|
34
|
-
"@node-core/rehype-shiki": "^1.
|
|
47
|
+
"@node-core/rehype-shiki": "^1.5.0",
|
|
35
48
|
"@swc/html-wasm": "^1.15.46",
|
|
36
49
|
"@swc/wasm": "^1.15.46",
|
|
37
50
|
"acorn": "^8.17.0",
|
|
@@ -40,7 +53,7 @@
|
|
|
40
53
|
"github-slugger": "^2.0.0",
|
|
41
54
|
"glob-parent": "^6.0.2",
|
|
42
55
|
"hastscript": "^9.0.1",
|
|
43
|
-
"piscina": "^5.3.
|
|
56
|
+
"piscina": "^5.3.1",
|
|
44
57
|
"rehype-stringify": "^10.0.1",
|
|
45
58
|
"remark-gfm": "^4.0.1",
|
|
46
59
|
"remark-mdx": "^3.1.1",
|
|
@@ -48,7 +61,7 @@
|
|
|
48
61
|
"remark-rehype": "^11.1.2",
|
|
49
62
|
"remark-stringify": "^11.0.0",
|
|
50
63
|
"semver": "^7.8.5",
|
|
51
|
-
"shiki": "^4.3
|
|
64
|
+
"shiki": "^4.4.3",
|
|
52
65
|
"tinyglobby": "^0.2.17",
|
|
53
66
|
"unified": "^11.0.5",
|
|
54
67
|
"unist-builder": "^4.0.0",
|
|
@@ -64,9 +77,9 @@
|
|
|
64
77
|
"hast-util-to-string": "^3.0.1"
|
|
65
78
|
},
|
|
66
79
|
"peerDependencies": {
|
|
67
|
-
"@node-core/doc-kit": "2.0.0",
|
|
68
|
-
"@node-core/doc-kit-legacy": "1.0.0",
|
|
69
|
-
"@doc-kit/generator-react": "0.1.0"
|
|
80
|
+
"@node-core/doc-kit": ">=2.0.0",
|
|
81
|
+
"@node-core/doc-kit-legacy": ">=1.0.0",
|
|
82
|
+
"@doc-kit/generator-react": ">=0.1.0"
|
|
70
83
|
},
|
|
71
84
|
"peerDependenciesMeta": {
|
|
72
85
|
"@node-core/doc-kit": {
|
|
@@ -7,11 +7,12 @@ import globParent from 'glob-parent';
|
|
|
7
7
|
import { globSync } from 'tinyglobby';
|
|
8
8
|
import { parse as parseYaml } from 'yaml';
|
|
9
9
|
|
|
10
|
+
import getConfig from '#utils/configuration/index.mjs';
|
|
11
|
+
import { withExt } from '#utils/file.mjs';
|
|
12
|
+
import { QUERIES } from '#utils/queries/index.mjs';
|
|
13
|
+
import { getRemark as remark, getRemarkMdx } from '#utils/remark.mjs';
|
|
14
|
+
|
|
10
15
|
import { STABILITY_INDEX_URL } from './constants.mjs';
|
|
11
|
-
import getConfig from '../../utils/configuration/index.mjs';
|
|
12
|
-
import { withExt } from '../../utils/file.mjs';
|
|
13
|
-
import { QUERIES } from '../../utils/queries/index.mjs';
|
|
14
|
-
import { getRemark as remark, getRemarkMdx } from '../../utils/remark.mjs';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Determines whether a file should be parsed as MDX. A `.mdx` extension opts in
|
|
@@ -6,7 +6,7 @@ import { extname } from 'node:path';
|
|
|
6
6
|
import { parse } from 'acorn';
|
|
7
7
|
import { globSync } from 'tinyglobby';
|
|
8
8
|
|
|
9
|
-
import getConfig from '
|
|
9
|
+
import getConfig from '#utils/configuration/index.mjs';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Process a chunk of JavaScript files in a worker thread.
|
package/src/generators/index.mjs
CHANGED
|
@@ -22,6 +22,7 @@ export const publicGenerators = {
|
|
|
22
22
|
'llms-txt': '@doc-kit/generator-react/llms-txt',
|
|
23
23
|
sitemap: '@doc-kit/generator-react/sitemap',
|
|
24
24
|
html: '@doc-kit/generator-react/html',
|
|
25
|
+
'section-pages': '@doc-kit/generator-react/section-pages',
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
// These ones are special since they don't produce standard output,
|
|
@@ -4,9 +4,9 @@ import { join } from 'node:path';
|
|
|
4
4
|
|
|
5
5
|
import { remove } from 'unist-util-remove';
|
|
6
6
|
|
|
7
|
-
import getConfig from '
|
|
8
|
-
import { writeFile } from '
|
|
9
|
-
import { UNIST } from '
|
|
7
|
+
import getConfig from '#utils/configuration/index.mjs';
|
|
8
|
+
import { writeFile } from '#utils/file.mjs';
|
|
9
|
+
import { UNIST } from '#utils/queries/index.mjs';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Generates the simplified JSON version of the API docs
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { isAbsolute } from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
|
|
6
|
+
import { enforceArray } from '#utils/array.mjs';
|
|
7
|
+
|
|
6
8
|
import { allGenerators } from './index.mjs';
|
|
7
9
|
|
|
8
10
|
/**
|
|
@@ -67,7 +69,7 @@ export const loadGenerator = async specifier => {
|
|
|
67
69
|
|
|
68
70
|
/**
|
|
69
71
|
* Loads the given generators plus the transitive closure of their
|
|
70
|
-
* dependencies (via `dependsOn`).
|
|
72
|
+
* dependencies (via `dependsOn`) and dependents (via `dependent`).
|
|
71
73
|
*
|
|
72
74
|
* @param {string[]} targets - Shorthand names, paths, or import specifiers
|
|
73
75
|
* @returns {Promise<Map<string, GeneratorMetadata>>} Loaded generators keyed
|
|
@@ -87,8 +89,11 @@ export const loadGenerators = async targets => {
|
|
|
87
89
|
const generator = await loadGenerator(specifier);
|
|
88
90
|
generators.set(specifier, generator);
|
|
89
91
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
for (const related of [
|
|
93
|
+
...enforceArray(generator.dependsOn ?? []),
|
|
94
|
+
...enforceArray(generator.dependent ?? []),
|
|
95
|
+
]) {
|
|
96
|
+
queue.push(resolveGeneratorSpecifier(related));
|
|
92
97
|
}
|
|
93
98
|
}
|
|
94
99
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
import getConfig from '#utils/configuration/index.mjs';
|
|
4
|
+
import { loadFromURL } from '#utils/loaders.mjs';
|
|
5
|
+
|
|
3
6
|
import { parseApiDoc } from './utils/parse.mjs';
|
|
4
|
-
import getConfig from '../../utils/configuration/index.mjs';
|
|
5
|
-
import { loadFromURL } from '../../utils/loaders.mjs';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Process a chunk of API doc files in a worker thread.
|
|
@@ -120,6 +120,23 @@ export interface StabilityData extends Data {
|
|
|
120
120
|
*/
|
|
121
121
|
export type StabilityNode = NodeWithData<Blockquote, StabilityData>;
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Describes a chunk page: a section of a module rendered as a page of its own
|
|
125
|
+
* (see the `section-pages` generator). Present on every entry of a chunk page.
|
|
126
|
+
*/
|
|
127
|
+
export interface ChunkInfo {
|
|
128
|
+
/** The `api` of the module the chunk was split from */
|
|
129
|
+
api: string;
|
|
130
|
+
/** The `path` of the module the chunk was split from */
|
|
131
|
+
path: string;
|
|
132
|
+
/** The section's anchor on the full module page */
|
|
133
|
+
slug: string;
|
|
134
|
+
/** The chunk's position within the module, in document order */
|
|
135
|
+
index: number;
|
|
136
|
+
/** The original depth of the chunk's heading */
|
|
137
|
+
depth: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
123
140
|
/**
|
|
124
141
|
* Complete metadata entry for a single API documentation element.
|
|
125
142
|
*
|
|
@@ -136,6 +153,8 @@ export interface MetadataEntry extends YAMLProperties {
|
|
|
136
153
|
synthetic?: boolean;
|
|
137
154
|
/** Whether this page was authored as MDX (JSX-in-Markdown). */
|
|
138
155
|
mdx?: boolean;
|
|
156
|
+
/** Set when this entry belongs to a chunk page rather than a full page. */
|
|
157
|
+
chunk?: ChunkInfo;
|
|
139
158
|
/** Processed heading with metadata */
|
|
140
159
|
heading: HeadingNode;
|
|
141
160
|
/** Stability classification information */
|
|
@@ -9,6 +9,15 @@ import { remove } from 'unist-util-remove';
|
|
|
9
9
|
import { selectAll } from 'unist-util-select';
|
|
10
10
|
import { SKIP, visit } from 'unist-util-visit';
|
|
11
11
|
|
|
12
|
+
import {
|
|
13
|
+
DEPRECATION_HEADING_REGEX,
|
|
14
|
+
IGNORE_STABILITY_STEMS,
|
|
15
|
+
} from '#generators/metadata/constants.mjs';
|
|
16
|
+
import { UNIST } from '#utils/queries/index.mjs';
|
|
17
|
+
import { getRemark as remark } from '#utils/remark.mjs';
|
|
18
|
+
import { relative } from '#utils/url.mjs';
|
|
19
|
+
|
|
20
|
+
import { resolveTypeAnnotations } from './resolveTypes.mjs';
|
|
12
21
|
import createNodeSlugger from './slugger.mjs';
|
|
13
22
|
import { transformNodeToHeading } from './transformers.mjs';
|
|
14
23
|
import {
|
|
@@ -18,14 +27,6 @@ import {
|
|
|
18
27
|
visitTextWithUnixManualNode,
|
|
19
28
|
visitYAML,
|
|
20
29
|
} from './visitors.mjs';
|
|
21
|
-
import { UNIST } from '../../../utils/queries/index.mjs';
|
|
22
|
-
import { getRemark as remark } from '../../../utils/remark.mjs';
|
|
23
|
-
import { relative } from '../../../utils/url.mjs';
|
|
24
|
-
import {
|
|
25
|
-
DEPRECATION_HEADING_REGEX,
|
|
26
|
-
IGNORE_STABILITY_STEMS,
|
|
27
|
-
} from '../constants.mjs';
|
|
28
|
-
import { resolveTypeAnnotations } from './resolveTypes.mjs';
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* This generator generates a flattened list of metadata entries from a API doc
|
|
@@ -87,7 +88,7 @@ export const parseApiDoc = ({ path, tree, mdx = false }, typeMap) => {
|
|
|
87
88
|
// Handles the normalisation URLs that reference to API doc files with .md extension
|
|
88
89
|
visit(tree, UNIST.isMarkdownUrl, node => visitMarkdownLink(node));
|
|
89
90
|
|
|
90
|
-
// If the document has no headings but it has content, we add a fake heading to the top
|
|
91
|
+
// If the document has no headings but it has content, we add a fake heading to the top, it will not be rendered
|
|
91
92
|
if (headingNodes.length === 0 && tree.children.length > 0) {
|
|
92
93
|
tree.children.unshift(createTree('heading', { depth: 1 }, []));
|
|
93
94
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { parseSync } from '@swc/wasm';
|
|
2
2
|
import { visit } from 'unist-util-visit';
|
|
3
3
|
|
|
4
|
+
import logger from '#logger/index.mjs';
|
|
5
|
+
import { walk } from '#utils/swc.mjs';
|
|
6
|
+
|
|
4
7
|
import { lookupTypeName, resolveTypeReference } from './transformers.mjs';
|
|
5
|
-
import logger from '../../../logger/index.mjs';
|
|
6
|
-
import { walk } from '../../../utils/swc.mjs';
|
|
7
8
|
|
|
8
9
|
// `null` is a keyword kind in SWC, unlike most parsers which treat it as a literal type
|
|
9
10
|
const KEYWORDS = new Set([
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import GitHubSlugger, { slug as defaultSlugFn } from 'github-slugger';
|
|
4
4
|
|
|
5
|
-
import { DOC_API_SLUGS_REPLACEMENTS } from '
|
|
5
|
+
import { DOC_API_SLUGS_REPLACEMENTS } from '#generators/metadata/constants.mjs';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Creates a modified version of the GitHub Slugger
|
|
@@ -2,11 +2,12 @@ import {
|
|
|
2
2
|
DOC_MAN_BASE_URL,
|
|
3
3
|
DOC_API_HEADING_TYPES,
|
|
4
4
|
MODULE_QUALIFIED_NAME,
|
|
5
|
-
} from '
|
|
5
|
+
} from '#generators/metadata/constants.mjs';
|
|
6
|
+
import BUILTIN_TYPE_MAP from '#generators/metadata/maps/builtin.json' with { type: 'json' };
|
|
7
|
+
import MDN_TYPE_MAP from '#generators/metadata/maps/mdn.json' with { type: 'json' };
|
|
8
|
+
import { transformNodesToString } from '#utils/unist.mjs';
|
|
9
|
+
|
|
6
10
|
import { slug } from './slugger.mjs';
|
|
7
|
-
import { transformNodesToString } from '../../../utils/unist.mjs';
|
|
8
|
-
import BUILTIN_TYPE_MAP from '../maps/builtin.json' with { type: 'json' };
|
|
9
|
-
import MDN_TYPE_MAP from '../maps/mdn.json' with { type: 'json' };
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* @param {string} text The inner text
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import { SKIP } from 'unist-util-visit';
|
|
4
4
|
|
|
5
|
+
import { QUERIES } from '#utils/queries/index.mjs';
|
|
6
|
+
import { getRemark as remark } from '#utils/remark.mjs';
|
|
7
|
+
import { transformNodesToString } from '#utils/unist.mjs';
|
|
8
|
+
|
|
5
9
|
import { transformUnixManualToLink } from './transformers.mjs';
|
|
6
10
|
import { extractYamlContent, parseYAMLIntoMetadata } from './yaml.mjs';
|
|
7
|
-
import { QUERIES } from '../../../utils/queries/index.mjs';
|
|
8
|
-
import { getRemark as remark } from '../../../utils/remark.mjs';
|
|
9
|
-
import { transformNodesToString } from '../../../utils/unist.mjs';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Updates a Markdown link into a HTML link for API docs
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { enforceArray } from '#utils/array.mjs';
|
|
4
|
+
|
|
5
|
+
import { resolveGeneratorSpecifier } from './loader.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The resolved specifiers of the generators a generator is delivered through.
|
|
9
|
+
*
|
|
10
|
+
* @param {GeneratorMetadata | undefined} generator
|
|
11
|
+
* @returns {string[]}
|
|
12
|
+
*/
|
|
13
|
+
const dependentsOf = generator =>
|
|
14
|
+
enforceArray(generator?.dependent ?? []).map(resolveGeneratorSpecifier);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Collects the generators reached from the given ones through `dependsOn`.
|
|
18
|
+
*
|
|
19
|
+
* @param {string[]} specifiers - Resolved generator specifiers
|
|
20
|
+
* @param {Map<string, GeneratorMetadata>} generators - Loaded generators
|
|
21
|
+
* @returns {Set<string>}
|
|
22
|
+
*/
|
|
23
|
+
const dependencyClosure = (specifiers, generators) => {
|
|
24
|
+
const closure = new Set();
|
|
25
|
+
const stack = [...specifiers];
|
|
26
|
+
|
|
27
|
+
while (stack.length > 0) {
|
|
28
|
+
const specifier = stack.pop();
|
|
29
|
+
|
|
30
|
+
if (closure.has(specifier)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
closure.add(specifier);
|
|
35
|
+
|
|
36
|
+
const { dependsOn } = generators.get(specifier) ?? {};
|
|
37
|
+
|
|
38
|
+
if (dependsOn) {
|
|
39
|
+
stack.push(resolveGeneratorSpecifier(dependsOn));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return closure;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Splices a generator into a dependent's pipeline: walking from the dependent
|
|
48
|
+
* up its dependency chain, the first generator that consumes the splicing
|
|
49
|
+
* generator's own input is rewired to consume the splicing generator instead.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} specifier - The generator to splice in
|
|
52
|
+
* @param {string} dependent - The pipeline to splice it into
|
|
53
|
+
* @param {Map<string, string | undefined>} inputOf - Effective inputs, mutated
|
|
54
|
+
* @param {string} name - The splicing generator's name, for errors
|
|
55
|
+
*/
|
|
56
|
+
const splice = (specifier, dependent, inputOf, name) => {
|
|
57
|
+
const upstream = inputOf.get(specifier);
|
|
58
|
+
const visited = new Set();
|
|
59
|
+
|
|
60
|
+
let current = dependent;
|
|
61
|
+
|
|
62
|
+
while (current !== undefined && current !== specifier) {
|
|
63
|
+
if (visited.has(current)) {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
visited.add(current);
|
|
68
|
+
|
|
69
|
+
if (inputOf.get(current) === upstream) {
|
|
70
|
+
inputOf.set(current, specifier);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
current = inputOf.get(current);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Generator "${name}" declares "${dependent}" as its dependent, but ` +
|
|
79
|
+
`nothing in that pipeline consumes "${upstream ?? 'no input'}", so ` +
|
|
80
|
+
'there is nowhere to splice it in.'
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Follows a generator's `dependent` links to the generators that finally
|
|
86
|
+
* deliver its output. When some of its dependents are already part of the
|
|
87
|
+
* run, only those deliver it; otherwise all of them do.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} specifier - Resolved generator specifier
|
|
90
|
+
* @param {Map<string, GeneratorMetadata>} generators - Loaded generators
|
|
91
|
+
* @param {Set<string>} requested - Generators already part of the run
|
|
92
|
+
* @param {Set<string>} [seen] - Guards against dependent cycles
|
|
93
|
+
* @returns {string[]} Resolved specifiers of the terminal generators
|
|
94
|
+
*/
|
|
95
|
+
const resolveTerminals = (
|
|
96
|
+
specifier,
|
|
97
|
+
generators,
|
|
98
|
+
requested,
|
|
99
|
+
seen = new Set()
|
|
100
|
+
) => {
|
|
101
|
+
if (seen.has(specifier)) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
seen.add(specifier);
|
|
106
|
+
|
|
107
|
+
const dependents = dependentsOf(generators.get(specifier));
|
|
108
|
+
|
|
109
|
+
if (dependents.length === 0) {
|
|
110
|
+
return [specifier];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const active = dependents.filter(dependent => requested.has(dependent));
|
|
114
|
+
|
|
115
|
+
return (active.length > 0 ? active : dependents).flatMap(dependent =>
|
|
116
|
+
resolveTerminals(dependent, generators, requested, seen)
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolves the effective pipeline for a run: which generator each generator
|
|
122
|
+
* reads its input from, and which generators the run finally collects.
|
|
123
|
+
*
|
|
124
|
+
* By default a generator reads from its `dependsOn`. A generator that declares
|
|
125
|
+
* one or more `dependent`s is spliced into each dependent's pipeline (see
|
|
126
|
+
* {@link splice}). Several generators may splice at the same point; each later
|
|
127
|
+
* one is inserted upstream of the earlier ones, forming a chain.
|
|
128
|
+
*
|
|
129
|
+
* A requested generator that declares dependents is delivered through them,
|
|
130
|
+
* so the run collects their output rather than the generator's own: through
|
|
131
|
+
* the dependents that are already part of the run when there are any, and
|
|
132
|
+
* through all of them otherwise.
|
|
133
|
+
*
|
|
134
|
+
* @param {string[]} targets - Requested targets, as resolved specifiers
|
|
135
|
+
* @param {Map<string, GeneratorMetadata>} generators - Loaded generators,
|
|
136
|
+
* including every dependency and dependent of the targets
|
|
137
|
+
* @returns {{ targets: string[], inputOf: Map<string, string | undefined> }}
|
|
138
|
+
* The generators to collect, and each generator's effective input generator
|
|
139
|
+
*/
|
|
140
|
+
export const resolvePipeline = (targets, generators) => {
|
|
141
|
+
/** @type {Map<string, string | undefined>} */
|
|
142
|
+
const inputOf = new Map();
|
|
143
|
+
|
|
144
|
+
for (const [specifier, { dependsOn }] of generators) {
|
|
145
|
+
inputOf.set(
|
|
146
|
+
specifier,
|
|
147
|
+
dependsOn ? resolveGeneratorSpecifier(dependsOn) : undefined
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const [specifier, generator] of generators) {
|
|
152
|
+
for (const dependent of dependentsOf(generator)) {
|
|
153
|
+
splice(specifier, dependent, inputOf, generator.name);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const requested = dependencyClosure(targets, generators);
|
|
158
|
+
const collected = [];
|
|
159
|
+
|
|
160
|
+
for (const target of targets) {
|
|
161
|
+
for (const terminal of resolveTerminals(target, generators, requested)) {
|
|
162
|
+
if (!collected.includes(terminal)) {
|
|
163
|
+
collected.push(terminal);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { targets: collected, inputOf };
|
|
169
|
+
};
|
|
@@ -97,6 +97,26 @@ declare global {
|
|
|
97
97
|
*/
|
|
98
98
|
dependsOn: string | undefined;
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* The generator(s) this generator's output is delivered through, declared
|
|
102
|
+
* as import specifiers. This is the inverse of `dependsOn`: rather than
|
|
103
|
+
* pulling a dependency in, it pushes this generator into another
|
|
104
|
+
* generator's pipeline.
|
|
105
|
+
*
|
|
106
|
+
* The orchestrator splices this generator in front of the first generator
|
|
107
|
+
* in each dependent's dependency chain that consumes the same `dependsOn`
|
|
108
|
+
* as this one. For example, a generator with
|
|
109
|
+
* `dependsOn: '@doc-kit/core/metadata'` and
|
|
110
|
+
* `dependent: '@doc-kit/generator-react/html'` is inserted between
|
|
111
|
+
* `metadata` and `jsx-ast` (the consumer of `metadata` on the way to
|
|
112
|
+
* `html`), so its output is what `jsx-ast` — and therefore `html` — sees.
|
|
113
|
+
*
|
|
114
|
+
* Requesting a generator that declares dependents runs them as well, and
|
|
115
|
+
* the run's result is their output: when some of the dependents are
|
|
116
|
+
* already part of the run, only those; otherwise all of them.
|
|
117
|
+
*/
|
|
118
|
+
dependent?: string | string[];
|
|
119
|
+
|
|
100
120
|
/**
|
|
101
121
|
* Generators are abstract and the different generators have different sort of inputs and outputs.
|
|
102
122
|
* For example, a MDX generator would take the raw AST and output MDX with React Components;
|
package/src/generators.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
loadGenerators,
|
|
6
6
|
resolveGeneratorSpecifier,
|
|
7
7
|
} from './generators/loader.mjs';
|
|
8
|
+
import { resolvePipeline } from './generators/pipeline.mjs';
|
|
8
9
|
import logger from './logger/index.mjs';
|
|
9
10
|
import createWorkerPool from './threading/index.mjs';
|
|
10
11
|
import createParallelWorker from './threading/parallel.mjs';
|
|
@@ -44,9 +45,12 @@ const createGenerator = () => {
|
|
|
44
45
|
*
|
|
45
46
|
* @param {string} specifier - Resolved generator specifier to schedule
|
|
46
47
|
* @param {Map<string, GeneratorMetadata>} generators - Loaded generators
|
|
48
|
+
* @param {Map<string, string | undefined>} inputOf - Each generator's
|
|
49
|
+
* effective input generator (its dependency, unless another generator was
|
|
50
|
+
* spliced in front of it via `dependent`)
|
|
47
51
|
* @param {import('./utils/configuration/types').Configuration} configuration - Runtime options
|
|
48
52
|
*/
|
|
49
|
-
const scheduleGenerator = (specifier, generators, configuration) => {
|
|
53
|
+
const scheduleGenerator = (specifier, generators, inputOf, configuration) => {
|
|
50
54
|
if (cache.has(specifier)) {
|
|
51
55
|
return;
|
|
52
56
|
}
|
|
@@ -54,12 +58,11 @@ const createGenerator = () => {
|
|
|
54
58
|
const generator = generators.get(specifier);
|
|
55
59
|
const { name, generate, hasParallelProcessor } = generator;
|
|
56
60
|
|
|
57
|
-
const dependsOn =
|
|
58
|
-
generator.dependsOn && resolveGeneratorSpecifier(generator.dependsOn);
|
|
61
|
+
const dependsOn = inputOf.get(specifier);
|
|
59
62
|
|
|
60
63
|
// Schedule dependency first
|
|
61
64
|
if (dependsOn && !cache.has(dependsOn)) {
|
|
62
|
-
scheduleGenerator(dependsOn, generators, configuration);
|
|
65
|
+
scheduleGenerator(dependsOn, generators, inputOf, configuration);
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
generatorsLogger.debug(`Scheduling "${name}"`, {
|
|
@@ -104,8 +107,16 @@ const createGenerator = () => {
|
|
|
104
107
|
|
|
105
108
|
// Resolve shorthand names and load the full dependency closure up front,
|
|
106
109
|
// so scheduling below is fully synchronous.
|
|
107
|
-
const
|
|
108
|
-
|
|
110
|
+
const generators = await loadGenerators(
|
|
111
|
+
target.map(resolveGeneratorSpecifier)
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Work out who reads from whom once generators declaring a `dependent`
|
|
115
|
+
// have been spliced in, and which generators the run finally collects.
|
|
116
|
+
const { targets, inputOf } = resolvePipeline(
|
|
117
|
+
target.map(resolveGeneratorSpecifier),
|
|
118
|
+
generators
|
|
119
|
+
);
|
|
109
120
|
|
|
110
121
|
generatorsLogger.debug(`Starting pipeline`, {
|
|
111
122
|
generators: targets.join(', '),
|
|
@@ -114,18 +125,14 @@ const createGenerator = () => {
|
|
|
114
125
|
|
|
115
126
|
// Compute consumer counts up front so dependencies can be evicted as soon
|
|
116
127
|
// as their last consumer runs (must be ready before any generator starts).
|
|
117
|
-
cache.populateConsumerCounts(targets, specifier =>
|
|
118
|
-
const { dependsOn } = generators.get(specifier);
|
|
119
|
-
|
|
120
|
-
return dependsOn && resolveGeneratorSpecifier(dependsOn);
|
|
121
|
-
});
|
|
128
|
+
cache.populateConsumerCounts(targets, specifier => inputOf.get(specifier));
|
|
122
129
|
|
|
123
130
|
// Create worker pool
|
|
124
131
|
pool = createWorkerPool(threads);
|
|
125
132
|
|
|
126
133
|
// Schedule all generators
|
|
127
134
|
for (const specifier of targets) {
|
|
128
|
-
scheduleGenerator(specifier, generators, configuration);
|
|
135
|
+
scheduleGenerator(specifier, generators, inputOf, configuration);
|
|
129
136
|
}
|
|
130
137
|
|
|
131
138
|
// Start all collections in parallel (don't await sequentially). Consuming
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { styleText } from 'node:util';
|
|
4
4
|
|
|
5
|
-
import { prettifyLevel } from '
|
|
6
|
-
import { prettifyTimestamp } from '
|
|
5
|
+
import { prettifyLevel } from '#logger/utils/colors.mjs';
|
|
6
|
+
import { prettifyTimestamp } from '#logger/utils/time.mjs';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Logs a formatted message to stdout for human-friendly CLI output.
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { debug, notice, warning, error } from '@actions/core';
|
|
4
4
|
|
|
5
|
-
import { LogLevel } from '
|
|
6
|
-
import { prettifyLevel } from '
|
|
7
|
-
import { prettifyTimestamp } from '
|
|
5
|
+
import { LogLevel } from '#logger/constants.mjs';
|
|
6
|
+
import { prettifyLevel } from '#logger/utils/colors.mjs';
|
|
7
|
+
import { prettifyTimestamp } from '#logger/utils/time.mjs';
|
|
8
8
|
|
|
9
9
|
const actions = {
|
|
10
10
|
[LogLevel.debug]: debug,
|
package/src/parsers/markdown.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { coerce } from 'semver';
|
|
4
4
|
|
|
5
|
-
import { loadFromURL } from '
|
|
5
|
+
import { loadFromURL } from '#utils/loaders.mjs';
|
|
6
6
|
|
|
7
7
|
// A ReGeX for retrieving Node.js version headers from the CHANGELOG.md
|
|
8
8
|
const NODE_VERSIONS_REGEX = /\* \[Node\.js ([0-9.]+)\]\S+ (.*)\r?\n/g;
|
|
@@ -22,9 +22,7 @@ const NODE_LTS_VERSION_REGEX = /Long Term Support/i;
|
|
|
22
22
|
export const parseChangelog = async path => {
|
|
23
23
|
const changelog = await loadFromURL(path);
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return nodeMajors.map(match => ({
|
|
25
|
+
return Array.from(changelog.matchAll(NODE_VERSIONS_REGEX), match => ({
|
|
28
26
|
version: coerce(match[1]),
|
|
29
27
|
isLts: NODE_LTS_VERSION_REGEX.test(match[2]),
|
|
30
28
|
}));
|
|
@@ -43,7 +41,8 @@ export const parseIndex = async path => {
|
|
|
43
41
|
|
|
44
42
|
const index = await loadFromURL(path);
|
|
45
43
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
return Array.from(index.matchAll(LIST_ITEM_REGEX), ([, section, api]) => ({
|
|
45
|
+
section,
|
|
46
|
+
api,
|
|
47
|
+
}));
|
|
49
48
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { loadGenerator } from '
|
|
2
|
-
import { setConfig } from '
|
|
1
|
+
import { loadGenerator } from '#generators/loader.mjs';
|
|
2
|
+
import { setConfig } from '#utils/configuration/index.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Processes a chunk of items using the specified generator's processChunk method.
|
package/src/threading/index.mjs
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The default `threads` ceiling. Each worker that highlights code holds Shiki's
|
|
5
|
+
* grammars and regex engine (~300MB) on top of the pages it is building, so
|
|
6
|
+
* past a few threads memory, not CPU, is what runs out. `--threads` raises it.
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_MAX_THREADS = 4;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The default number of items each worker task processes.
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_CHUNK_SIZE = 10;
|
|
@@ -11,12 +11,14 @@ import { coerce } from 'semver';
|
|
|
11
11
|
import {
|
|
12
12
|
loadGenerators,
|
|
13
13
|
resolveGeneratorSpecifier,
|
|
14
|
-
} from '
|
|
15
|
-
import logger from '
|
|
16
|
-
import { parseChangelog, parseIndex } from '
|
|
17
|
-
import { enforceArray } from '
|
|
18
|
-
import { leftHandAssign } from '
|
|
19
|
-
import { deepMerge } from '
|
|
14
|
+
} from '#generators/loader.mjs';
|
|
15
|
+
import logger from '#logger/index.mjs';
|
|
16
|
+
import { parseChangelog, parseIndex } from '#parsers/markdown.mjs';
|
|
17
|
+
import { enforceArray } from '#utils/array.mjs';
|
|
18
|
+
import { leftHandAssign } from '#utils/generators.mjs';
|
|
19
|
+
import { deepMerge } from '#utils/misc.mjs';
|
|
20
|
+
|
|
21
|
+
import { DEFAULT_CHUNK_SIZE, DEFAULT_MAX_THREADS } from './constants.mjs';
|
|
20
22
|
|
|
21
23
|
const configExplorer = cosmiconfig('doc-kit');
|
|
22
24
|
|
|
@@ -68,8 +70,11 @@ export const getDefaultConfig = (generators, config) =>
|
|
|
68
70
|
// riscv64 with sv39. Running multiple generators that use wasm in
|
|
69
71
|
// parallel could cause failures to allocate new wasm instance.
|
|
70
72
|
// See also https://github.com/nodejs/node/pull/60591
|
|
71
|
-
threads:
|
|
72
|
-
|
|
73
|
+
threads:
|
|
74
|
+
process.arch === 'riscv64'
|
|
75
|
+
? 1
|
|
76
|
+
: Math.min(cpus().length, DEFAULT_MAX_THREADS),
|
|
77
|
+
chunkSize: DEFAULT_CHUNK_SIZE,
|
|
73
78
|
})
|
|
74
79
|
);
|
|
75
80
|
|
package/src/utils/generators.mjs
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
import { coerce, major } from 'semver';
|
|
4
4
|
|
|
5
|
+
import { transformNodeToString } from './unist.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Retrieves the description of a given API doc entry. It first checks whether
|
|
9
|
+
* the entry has a llm_description property. If not, it extracts the first
|
|
10
|
+
* paragraph from the entry's content.
|
|
11
|
+
*
|
|
12
|
+
* @param {import('../generators/metadata/types').MetadataEntry} entry
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
export const getEntryDescription = entry => {
|
|
16
|
+
if (entry.llm_description) {
|
|
17
|
+
return entry.llm_description.trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const descriptionNode = entry.content.children.find(
|
|
21
|
+
child => child.type === 'paragraph'
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (!descriptionNode) {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
transformNodeToString(descriptionNode)
|
|
30
|
+
// Remove newlines and extra spaces
|
|
31
|
+
.replace(/[\r\n]+/g, '')
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
5
35
|
/**
|
|
6
36
|
* Groups all the API metadata nodes by module (`api` property) so that we can process each different file
|
|
7
37
|
* based on the module it belongs to.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import rehypeStringify from 'rehype-stringify';
|
|
4
|
+
import remarkParse from 'remark-parse';
|
|
5
|
+
import remarkRehype from 'remark-rehype';
|
|
6
|
+
import { unified } from 'unified';
|
|
7
|
+
import { u as createTree } from 'unist-builder';
|
|
8
|
+
import { SKIP, visit } from 'unist-util-visit';
|
|
9
|
+
|
|
10
|
+
import { lazy } from './misc.mjs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Renders a `root` as just its children, since content placed directly on one
|
|
14
|
+
* is otherwise rendered with a line break between each node, which surfaces as
|
|
15
|
+
* stray whitespace.
|
|
16
|
+
*
|
|
17
|
+
* @param {import('mdast-util-to-hast').State} state
|
|
18
|
+
* @param {import('unist').Parent} node
|
|
19
|
+
*/
|
|
20
|
+
const inlineRoot = (state, node) => ({
|
|
21
|
+
type: 'root',
|
|
22
|
+
children: state.all(node),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Retrieves an instance of Remark configured to parse plain markdown, without
|
|
27
|
+
* the extensions that only apply to whole documents.
|
|
28
|
+
*/
|
|
29
|
+
const getInlineParser = lazy(() => unified().use(remarkParse));
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Retrieves an instance of Remark configured to render inline nodes as an HTML
|
|
33
|
+
* string. Raw HTML is dropped rather than passed through, since the result is
|
|
34
|
+
* inserted into the page as-is.
|
|
35
|
+
*/
|
|
36
|
+
const getInlineRenderer = lazy(() =>
|
|
37
|
+
unified()
|
|
38
|
+
.use(remarkRehype, { handlers: { root: inlineRoot } })
|
|
39
|
+
.use(rehypeStringify)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parses a single line of markdown (a change description, a summary, ...) into
|
|
44
|
+
* the inline nodes it is made of.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} markdown - The markdown to parse.
|
|
47
|
+
* @param {boolean} [dropLinks] - Replace links with their text? Anchors cannot
|
|
48
|
+
* nest, so content rendered inside a link must not contain one.
|
|
49
|
+
* @returns {Array<import('mdast').PhrasingContent>} The parsed nodes.
|
|
50
|
+
*/
|
|
51
|
+
export const parseInline = (markdown, dropLinks = false) => {
|
|
52
|
+
const tree = getInlineParser().parse(markdown);
|
|
53
|
+
|
|
54
|
+
if (dropLinks) {
|
|
55
|
+
visit(tree, 'link', (node, index, parent) => {
|
|
56
|
+
parent.children.splice(index, 1, ...node.children);
|
|
57
|
+
|
|
58
|
+
return [SKIP, index];
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const [first] = tree.children;
|
|
63
|
+
|
|
64
|
+
// A single line of prose parses into one paragraph, which is dropped so the
|
|
65
|
+
// nodes can be rendered inline
|
|
66
|
+
return tree.children.length === 1 && first.type === 'paragraph'
|
|
67
|
+
? first.children
|
|
68
|
+
: tree.children;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Renders inline nodes as an HTML string, for the places that hand rendered
|
|
73
|
+
* markup to a component through a data channel rather than as an AST.
|
|
74
|
+
*
|
|
75
|
+
* @param {Array<import('mdast').PhrasingContent>} nodes - The nodes to render.
|
|
76
|
+
* @returns {string} The rendered HTML.
|
|
77
|
+
*/
|
|
78
|
+
export const renderAsHTML = nodes => {
|
|
79
|
+
const renderer = getInlineRenderer();
|
|
80
|
+
|
|
81
|
+
return renderer.stringify(renderer.runSync(createTree('root', nodes)));
|
|
82
|
+
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
import { transformNodesToString } from '
|
|
4
|
-
|
|
3
|
+
import { transformNodesToString } from '#utils/unist.mjs';
|
|
4
|
+
|
|
5
|
+
import { isTypedListItem, isTypedList } from './utils.mjs';
|
|
5
6
|
|
|
6
7
|
// This defines the actual REGEX Queries
|
|
7
8
|
export const QUERIES = {
|
|
@@ -71,6 +72,12 @@ export const UNIST = {
|
|
|
71
72
|
*/
|
|
72
73
|
isLooselyTypedList: list => Boolean(isTypedList(list)),
|
|
73
74
|
|
|
75
|
+
/**
|
|
76
|
+
* @param {import('@types/mdast').ListItem} item
|
|
77
|
+
* @returns {boolean}
|
|
78
|
+
*/
|
|
79
|
+
isTypedListItem,
|
|
80
|
+
|
|
74
81
|
/**
|
|
75
82
|
* @param {import('@types/mdast').List} list
|
|
76
83
|
* @returns {boolean}
|
|
@@ -2,20 +2,17 @@ import { VALID_JAVASCRIPT_PROPERTY } from './constants.mjs';
|
|
|
2
2
|
import { QUERIES } from './index.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Inspects the first phrasing node of a paragraph and returns how confidently
|
|
6
|
+
* it looks like the start of a typed parameter.
|
|
7
|
+
*
|
|
8
|
+
* @param {import('@types/mdast').PhrasingContent | undefined} firstNode
|
|
6
9
|
* @returns {0 | 1 | 2} confidence
|
|
7
10
|
*
|
|
8
|
-
* 0:
|
|
9
|
-
* 1:
|
|
10
|
-
* 2:
|
|
11
|
+
* 0: Not a typed parameter
|
|
12
|
+
* 1: Loosely typed (inlineCode + valid property name)
|
|
13
|
+
* 2: Strongly typed (typed list starter or direct type annotation)
|
|
11
14
|
*/
|
|
12
|
-
|
|
13
|
-
if (!list || list.type !== 'list') {
|
|
14
|
-
return 0;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const firstNode = list.children?.[0]?.children?.[0]?.children[0];
|
|
18
|
-
|
|
15
|
+
const getTypedConfidence = firstNode => {
|
|
19
16
|
if (!firstNode) {
|
|
20
17
|
return 0;
|
|
21
18
|
}
|
|
@@ -43,3 +40,30 @@ export const isTypedList = list => {
|
|
|
43
40
|
|
|
44
41
|
return 0;
|
|
45
42
|
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Checks whether a single list item looks like a typed parameter — i.e. it
|
|
46
|
+
* starts with a property name (`inlineCode`), a Returns/Extends/Type prefix,
|
|
47
|
+
* or a direct type annotation.
|
|
48
|
+
*
|
|
49
|
+
* @param {import('@types/mdast').ListItem} item
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
export const isTypedListItem = item =>
|
|
53
|
+
Boolean(getTypedConfidence(item?.children?.[0]?.children?.[0]));
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {import('@types/mdast').List} list
|
|
57
|
+
* @returns {0 | 1 | 2} confidence
|
|
58
|
+
*
|
|
59
|
+
* 0: This is not a typed list
|
|
60
|
+
* 1: This is a loosely typed list
|
|
61
|
+
* 2: This is a strongly typed list
|
|
62
|
+
*/
|
|
63
|
+
export const isTypedList = list => {
|
|
64
|
+
if (!list || list.type !== 'list') {
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return getTypedConfidence(list.children?.[0]?.children?.[0]?.children?.[0]);
|
|
69
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import rehypeStringify from 'rehype-stringify';
|
|
4
|
+
import remarkParse from 'remark-parse';
|
|
5
|
+
import remarkRehype from 'remark-rehype';
|
|
6
|
+
import { unified } from 'unified';
|
|
7
|
+
|
|
8
|
+
import syntaxHighlighter from './highlighter.mjs';
|
|
9
|
+
import { lazy } from './misc.mjs';
|
|
10
|
+
import { rehypeOptions } from './remark.mjs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Retrieves an instance of Remark configured to output stringified HTML code
|
|
14
|
+
* including parsing Code Boxes with syntax highlighting.
|
|
15
|
+
*
|
|
16
|
+
* This lives apart from `./remark.mjs` because importing it loads Shiki (every
|
|
17
|
+
* grammar plus the regex engine, ~250MB per process). Only the generators that
|
|
18
|
+
* highlight code — `legacy-html` here — should pay for that.
|
|
19
|
+
*/
|
|
20
|
+
export const getRemarkRehypeWithShiki = lazy(() =>
|
|
21
|
+
unified()
|
|
22
|
+
.use(remarkParse)
|
|
23
|
+
// legacy-html gets the minimal (unhighlighted) type rendering
|
|
24
|
+
.use(remarkRehype, rehypeOptions)
|
|
25
|
+
// This is a custom ad-hoc within the Shiki Rehype plugin, used to highlight code
|
|
26
|
+
// and transform them into HAST nodes
|
|
27
|
+
.use(syntaxHighlighter)
|
|
28
|
+
.use(rehypeStringify, { allowDangerousHtml: true })
|
|
29
|
+
);
|
package/src/utils/remark.mjs
CHANGED
|
@@ -8,23 +8,51 @@ import remarkRehype from 'remark-rehype';
|
|
|
8
8
|
import remarkStringify from 'remark-stringify';
|
|
9
9
|
import { unified } from 'unified';
|
|
10
10
|
|
|
11
|
-
import syntaxHighlighter from './highlighter.mjs';
|
|
12
11
|
import { lazy } from './misc.mjs';
|
|
13
12
|
import { typeAnnotationToHast } from './type-annotations/hast.mjs';
|
|
14
13
|
import remarkTypeAnnotations from './type-annotations/remark.mjs';
|
|
15
14
|
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
15
|
+
// Nothing in this module loads Shiki: the `ast` and `metadata` stages (and
|
|
16
|
+
// every worker that runs them) import it, and none of them highlight code.
|
|
17
|
+
// The highlighting pipeline lives in `./remark-shiki.mjs`.
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Renders an MDX JSX element as just its children, so the surrounding prose
|
|
21
|
+
* still renders in HTML-string output.
|
|
22
|
+
*
|
|
23
|
+
* @param {import('mdast-util-to-hast').State} state
|
|
24
|
+
* @param {import('unist').Parent} node
|
|
25
|
+
*/
|
|
26
|
+
const mdxElementToChildren = (state, node) => state.all(node);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Drops a node from HTML-string output.
|
|
30
|
+
*/
|
|
31
|
+
const dropNode = () => undefined;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The `remark-rehype` options shared by the HTML-string pipelines.
|
|
35
|
+
*
|
|
36
|
+
* Existing HTML nodes pass through untouched (they were created during the
|
|
37
|
+
* rehype process), and dangerous HTML is allowed since the Markdown sources
|
|
38
|
+
* are trusted. The MDX node types cannot be rendered to an HTML string (that
|
|
39
|
+
* is the React generators' job): JSX elements degrade to their children so the
|
|
40
|
+
* surrounding prose still renders, and expressions/ESM are dropped.
|
|
41
|
+
*
|
|
42
|
+
* @type {import('remark-rehype').Options}
|
|
43
|
+
*/
|
|
44
|
+
export const rehypeOptions = {
|
|
45
|
+
allowDangerousHtml: true,
|
|
46
|
+
passThrough: ['element'],
|
|
47
|
+
handlers: {
|
|
48
|
+
typeAnnotation: typeAnnotationToHast,
|
|
49
|
+
mdxJsxTextElement: mdxElementToChildren,
|
|
50
|
+
mdxJsxFlowElement: mdxElementToChildren,
|
|
51
|
+
mdxFlowExpression: dropNode,
|
|
52
|
+
mdxTextExpression: dropNode,
|
|
53
|
+
mdxjsEsm: dropNode,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
28
56
|
|
|
29
57
|
/**
|
|
30
58
|
* Retrieves an instance of Remark configured to parse GFM (GitHub Flavored Markdown)
|
|
@@ -57,41 +85,6 @@ export const getRemarkMdx = lazy(() =>
|
|
|
57
85
|
export const getRemarkRehype = lazy(() =>
|
|
58
86
|
unified()
|
|
59
87
|
.use(remarkParse)
|
|
60
|
-
|
|
61
|
-
// as these are nodes we manually created during the rehype process
|
|
62
|
-
// We also allow dangerous HTML to be passed through, since we have HTML within our Markdown
|
|
63
|
-
// and we trust the sources of the Markdown files
|
|
64
|
-
.use(remarkRehype, {
|
|
65
|
-
allowDangerousHtml: true,
|
|
66
|
-
passThrough,
|
|
67
|
-
handlers: { typeAnnotation: typeAnnotationToHast },
|
|
68
|
-
})
|
|
69
|
-
// We allow dangerous HTML to be passed through, since we have HTML within our Markdown
|
|
70
|
-
// and we trust the sources of the Markdown files
|
|
71
|
-
.use(rehypeStringify, { allowDangerousHtml: true })
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Retrieves an instance of Remark configured to output stringified HTML code
|
|
76
|
-
* including parsing Code Boxes with syntax highlighting
|
|
77
|
-
*/
|
|
78
|
-
export const getRemarkRehypeWithShiki = lazy(() =>
|
|
79
|
-
unified()
|
|
80
|
-
.use(remarkParse)
|
|
81
|
-
// We make Rehype ignore existing HTML nodes (just the node itself, not its children)
|
|
82
|
-
// as these are nodes we manually created during the rehype process
|
|
83
|
-
// We also allow dangerous HTML to be passed through, since we have HTML within our Markdown
|
|
84
|
-
// and we trust the sources of the Markdown files
|
|
85
|
-
.use(remarkRehype, {
|
|
86
|
-
allowDangerousHtml: true,
|
|
87
|
-
passThrough,
|
|
88
|
-
// legacy-html gets the minimal (unhighlighted) type rendering
|
|
89
|
-
handlers: { typeAnnotation: typeAnnotationToHast },
|
|
90
|
-
})
|
|
91
|
-
// This is a custom ad-hoc within the Shiki Rehype plugin, used to highlight code
|
|
92
|
-
// and transform them into HAST nodes
|
|
93
|
-
.use(syntaxHighlighter)
|
|
94
|
-
// We allow dangerous HTML to be passed through, since we have HTML within our Markdown
|
|
95
|
-
// and we trust the sources of the Markdown files
|
|
88
|
+
.use(remarkRehype, rehypeOptions)
|
|
96
89
|
.use(rehypeStringify, { allowDangerousHtml: true })
|
|
97
90
|
);
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { leftHandAssign } from '#utils/generators.mjs';
|
|
2
|
+
import { QUERIES, UNIST } from '#utils/queries/index.mjs';
|
|
3
|
+
import { transformNodesToString } from '#utils/unist.mjs';
|
|
4
|
+
|
|
1
5
|
import {
|
|
2
6
|
DEFAULT_EXPRESSION,
|
|
3
7
|
LEADING_HYPHEN,
|
|
4
8
|
NAME_EXPRESSION,
|
|
5
9
|
} from './constants.mjs';
|
|
6
10
|
import parseSignature from './parseSignature.mjs';
|
|
7
|
-
import { leftHandAssign } from '../generators.mjs';
|
|
8
|
-
import { QUERIES, UNIST } from '../queries/index.mjs';
|
|
9
|
-
import { transformNodesToString } from '../unist.mjs';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Extracts and removes a specific pattern from a text string while storing the result in a key of the `current` object.
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
import { highlighter } from '../highlighter.mjs';
|
|
4
|
-
|
|
5
|
-
const [lightTheme, darkTheme] = highlighter.shiki.getLoadedThemes();
|
|
6
|
-
|
|
7
3
|
/**
|
|
8
4
|
* Slices a type's text by its resolved link ranges into hast children —
|
|
9
5
|
* plain text segments interleaved with `<a class="type-link">` anchors.
|
|
@@ -59,57 +55,3 @@ export const typeAnnotationToHast = (state, node) => {
|
|
|
59
55
|
|
|
60
56
|
return state.applyData(node, result);
|
|
61
57
|
};
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Syntax-highlighted mdast→hast handler for `typeAnnotation` nodes, used by
|
|
65
|
-
* the web (JSX) pipeline. The whole type is highlighted as one inline
|
|
66
|
-
* fragment, and each resolved identifier's exact character range is wrapped
|
|
67
|
-
* in an `<a>` via Shiki decorations. Values that are not TypeScript (display
|
|
68
|
-
* names such as `HTTP/2 Headers Object`) are highlighted as plain text, so
|
|
69
|
-
* their prose is not coloured as operators and numeric literals.
|
|
70
|
-
*
|
|
71
|
-
* Falls back to the minimal handler when the type failed to parse or nothing
|
|
72
|
-
* resolved (no point paying for highlighting then).
|
|
73
|
-
*
|
|
74
|
-
* @param {import('mdast-util-to-hast').State} state
|
|
75
|
-
* @param {import('mdast').Node} node
|
|
76
|
-
* @returns {import('hast').Element}
|
|
77
|
-
*/
|
|
78
|
-
export const typeAnnotationToHighlightedHast = (state, node) => {
|
|
79
|
-
const links = node.data?.links ?? [];
|
|
80
|
-
|
|
81
|
-
if (node.data?.parseError || links.length === 0) {
|
|
82
|
-
return typeAnnotationToHast(state, node);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const root = highlighter.shiki.codeToHast(node.value, {
|
|
86
|
-
lang: node.data?.typescript ? 'typescript' : 'text',
|
|
87
|
-
themes: { light: lightTheme, dark: darkTheme },
|
|
88
|
-
decorations: links.map(({ start, end, href }) => ({
|
|
89
|
-
start,
|
|
90
|
-
end,
|
|
91
|
-
tagName: 'a',
|
|
92
|
-
properties: { href, class: 'type-link' },
|
|
93
|
-
alwaysWrap: true,
|
|
94
|
-
})),
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
// codeToHast wraps the highlighted line in <pre><code>; re-shape that into
|
|
98
|
-
// a single inline <code> element ("only the outermost type opens/closes
|
|
99
|
-
// the code fragment")
|
|
100
|
-
const [preElement] = root.children;
|
|
101
|
-
const [codeElement] = preElement.children;
|
|
102
|
-
|
|
103
|
-
const result = {
|
|
104
|
-
type: 'element',
|
|
105
|
-
tagName: 'code',
|
|
106
|
-
properties: {
|
|
107
|
-
class: `${preElement.properties.class} type`,
|
|
108
|
-
},
|
|
109
|
-
children: codeElement.children,
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
state.patch(node, result);
|
|
113
|
-
|
|
114
|
-
return state.applyData(node, result);
|
|
115
|
-
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { highlighter } from '#utils/highlighter.mjs';
|
|
4
|
+
|
|
5
|
+
import { typeAnnotationToHast } from './hast.mjs';
|
|
6
|
+
|
|
7
|
+
// Kept apart from `./hast.mjs` on purpose: importing this module loads Shiki
|
|
8
|
+
// (every grammar plus the regex engine), which only the pipelines that
|
|
9
|
+
// highlight should pay for. The `ast` and `metadata` stages never do.
|
|
10
|
+
const [lightTheme, darkTheme] = highlighter.shiki.getLoadedThemes();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Syntax-highlighted mdast→hast handler for `typeAnnotation` nodes, used by
|
|
14
|
+
* the web (JSX) pipeline. The whole type is highlighted as one inline
|
|
15
|
+
* fragment, and each resolved identifier's exact character range is wrapped
|
|
16
|
+
* in an `<a>` via Shiki decorations. Values that are not TypeScript (display
|
|
17
|
+
* names such as `HTTP/2 Headers Object`) are highlighted as plain text, so
|
|
18
|
+
* their prose is not coloured as operators and numeric literals.
|
|
19
|
+
*
|
|
20
|
+
* Falls back to the minimal handler when the type failed to parse or nothing
|
|
21
|
+
* resolved (no point paying for highlighting then).
|
|
22
|
+
*
|
|
23
|
+
* @param {import('mdast-util-to-hast').State} state
|
|
24
|
+
* @param {import('mdast').Node} node
|
|
25
|
+
* @returns {import('hast').Element}
|
|
26
|
+
*/
|
|
27
|
+
export const typeAnnotationToHighlightedHast = (state, node) => {
|
|
28
|
+
const links = node.data?.links ?? [];
|
|
29
|
+
|
|
30
|
+
if (node.data?.parseError || links.length === 0) {
|
|
31
|
+
return typeAnnotationToHast(state, node);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const root = highlighter.shiki.codeToHast(node.value, {
|
|
35
|
+
lang: node.data?.typescript ? 'typescript' : 'text',
|
|
36
|
+
themes: { light: lightTheme, dark: darkTheme },
|
|
37
|
+
decorations: links.map(({ start, end, href }) => ({
|
|
38
|
+
start,
|
|
39
|
+
end,
|
|
40
|
+
tagName: 'a',
|
|
41
|
+
properties: { href, class: 'type-link' },
|
|
42
|
+
alwaysWrap: true,
|
|
43
|
+
})),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// codeToHast wraps the highlighted line in <pre><code>; re-shape that into
|
|
47
|
+
// a single inline <code> element ("only the outermost type opens/closes
|
|
48
|
+
// the code fragment")
|
|
49
|
+
const [preElement] = root.children;
|
|
50
|
+
const [codeElement] = preElement.children;
|
|
51
|
+
|
|
52
|
+
const result = {
|
|
53
|
+
type: 'element',
|
|
54
|
+
tagName: 'code',
|
|
55
|
+
properties: {
|
|
56
|
+
class: `${preElement.properties.class} type`,
|
|
57
|
+
},
|
|
58
|
+
children: codeElement.children,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
state.patch(node, result);
|
|
62
|
+
|
|
63
|
+
return state.applyData(node, result);
|
|
64
|
+
};
|