@doc-kit/core 0.0.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 +16 -0
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/package.json +88 -0
- package/shiki.config.mjs +50 -0
- package/src/caching.mjs +183 -0
- package/src/generators/ast/README.md +8 -0
- package/src/generators/ast/constants.mjs +1 -0
- package/src/generators/ast/generate.mjs +125 -0
- package/src/generators/ast/index.mjs +20 -0
- package/src/generators/ast/types.d.ts +7 -0
- package/src/generators/ast-js/README.md +8 -0
- package/src/generators/ast-js/generate.mjs +56 -0
- package/src/generators/ast-js/index.mjs +24 -0
- package/src/generators/ast-js/types.d.ts +5 -0
- package/src/generators/index.mjs +48 -0
- package/src/generators/json-simple/README.md +9 -0
- package/src/generators/json-simple/generate.mjs +33 -0
- package/src/generators/json-simple/index.mjs +23 -0
- package/src/generators/json-simple/types.d.ts +6 -0
- package/src/generators/loader.mjs +96 -0
- package/src/generators/metadata/README.md +8 -0
- package/src/generators/metadata/constants.mjs +68 -0
- package/src/generators/metadata/generate.mjs +40 -0
- package/src/generators/metadata/index.mjs +21 -0
- package/src/generators/metadata/maps/builtin.json +16 -0
- package/src/generators/metadata/maps/mdn.json +1114 -0
- package/src/generators/metadata/types.d.ts +145 -0
- package/src/generators/metadata/utils/parse.mjs +160 -0
- package/src/generators/metadata/utils/resolveTypes.mjs +204 -0
- package/src/generators/metadata/utils/slugger.mjs +39 -0
- package/src/generators/metadata/utils/transformers.mjs +109 -0
- package/src/generators/metadata/utils/visitors.mjs +134 -0
- package/src/generators/metadata/utils/yaml.mjs +58 -0
- package/src/generators/types.d.ts +129 -0
- package/src/generators.mjs +145 -0
- package/src/logger/constants.mjs +34 -0
- package/src/logger/index.mjs +27 -0
- package/src/logger/logger.mjs +177 -0
- package/src/logger/transports/console.mjs +54 -0
- package/src/logger/transports/github.mjs +42 -0
- package/src/logger/transports/index.mjs +11 -0
- package/src/logger/types.d.ts +29 -0
- package/src/logger/utils/colors.mjs +18 -0
- package/src/logger/utils/time.mjs +21 -0
- package/src/parsers/markdown.mjs +49 -0
- package/src/parsers/types.d.ts +7 -0
- package/src/threading/chunk-worker.mjs +23 -0
- package/src/threading/index.mjs +27 -0
- package/src/threading/parallel.mjs +140 -0
- package/src/utils/array.mjs +7 -0
- package/src/utils/configuration/index.mjs +263 -0
- package/src/utils/configuration/templates.mjs +21 -0
- package/src/utils/configuration/types.d.ts +64 -0
- package/src/utils/file.mjs +20 -0
- package/src/utils/generators.mjs +71 -0
- package/src/utils/highlighter.mjs +198 -0
- package/src/utils/html-minifier.mjs +12 -0
- package/src/utils/loaders.mjs +48 -0
- package/src/utils/misc.mjs +72 -0
- package/src/utils/queries/constants.mjs +3 -0
- package/src/utils/queries/index.mjs +93 -0
- package/src/utils/queries/utils.mjs +45 -0
- package/src/utils/remark.mjs +97 -0
- package/src/utils/signature/constants.mjs +12 -0
- package/src/utils/signature/parseList.mjs +133 -0
- package/src/utils/signature/parseSignature.mjs +208 -0
- package/src/utils/signature/types.d.ts +66 -0
- package/src/utils/swc.mjs +34 -0
- package/src/utils/type-annotations/hast.mjs +115 -0
- package/src/utils/type-annotations/mdast.mjs +75 -0
- package/src/utils/type-annotations/remark.mjs +24 -0
- package/src/utils/type-annotations/syntax.mjs +188 -0
- package/src/utils/unist.mjs +75 -0
- package/src/utils/url.mjs +27 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { PARAM_EXPRESSION } from './constants.mjs';
|
|
4
|
+
|
|
5
|
+
const OPTIONAL_LEVEL_CHANGES = { '[': 1, ']': -1 };
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {Number} depth
|
|
9
|
+
* @param {String} char
|
|
10
|
+
* @returns {Number}
|
|
11
|
+
*/
|
|
12
|
+
const updateDepth = (depth, char) =>
|
|
13
|
+
depth + (OPTIONAL_LEVEL_CHANGES[char] || 0);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} parameterName
|
|
17
|
+
* @param {number} optionalDepth
|
|
18
|
+
* @returns {[string, number, boolean]}
|
|
19
|
+
*/
|
|
20
|
+
export function parseNameAndOptionalStatus(parameterName, optionalDepth) {
|
|
21
|
+
// Let's check if the parameter is optional & grab its name at the same time.
|
|
22
|
+
// We need to see if there's any leading brackets in front of the parameter
|
|
23
|
+
// name. While we're doing that, we can also get the index where the
|
|
24
|
+
// parameter's name actually starts at.
|
|
25
|
+
|
|
26
|
+
// Find the starting index where the name begins
|
|
27
|
+
const startingIdx = [...parameterName].findIndex(
|
|
28
|
+
char => !OPTIONAL_LEVEL_CHANGES[char]
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// Update optionalDepth based on leading brackets
|
|
32
|
+
optionalDepth = [...parameterName.slice(0, startingIdx)].reduce(
|
|
33
|
+
updateDepth,
|
|
34
|
+
optionalDepth
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Find the ending index where the name ends
|
|
38
|
+
const endingIdx = [...parameterName].findLastIndex(
|
|
39
|
+
char => !OPTIONAL_LEVEL_CHANGES[char]
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Extract the actual parameter name
|
|
43
|
+
const actualName = parameterName.slice(startingIdx, endingIdx + 1);
|
|
44
|
+
const isParameterOptional = optionalDepth > 0;
|
|
45
|
+
|
|
46
|
+
// Update optionalDepth based on trailing brackets
|
|
47
|
+
// These apply to the NEXT parameter
|
|
48
|
+
optionalDepth = [...parameterName.slice(endingIdx + 1)].reduce(
|
|
49
|
+
updateDepth,
|
|
50
|
+
optionalDepth
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return [actualName, optionalDepth, isParameterOptional];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} parameterName
|
|
58
|
+
* @returns {[string, string | undefined]}
|
|
59
|
+
*/
|
|
60
|
+
export function parseDefaultValue(parameterName) {
|
|
61
|
+
/**
|
|
62
|
+
* @type {string | undefined}
|
|
63
|
+
*/
|
|
64
|
+
let defaultValue;
|
|
65
|
+
const equalSignPos = parameterName.indexOf('=');
|
|
66
|
+
|
|
67
|
+
if (equalSignPos !== -1) {
|
|
68
|
+
// We do have a default value, let's extract it
|
|
69
|
+
defaultValue = parameterName.substring(equalSignPos).trim();
|
|
70
|
+
|
|
71
|
+
// Let's remove the default value from the parameter name
|
|
72
|
+
parameterName = parameterName.substring(0, equalSignPos);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return [parameterName, defaultValue];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {string} parameterName
|
|
80
|
+
* @param {number} index
|
|
81
|
+
* @param {Array<import('./types.d.ts').ParameterList>} markdownParameters
|
|
82
|
+
* @returns {import('./types.d.ts').Parameter}
|
|
83
|
+
*/
|
|
84
|
+
export function findParameter(parameterName, index, markdownParameters) {
|
|
85
|
+
const parameter = markdownParameters[index];
|
|
86
|
+
if (parameter?.name === parameterName) {
|
|
87
|
+
return parameter;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Method likely has multiple signatures, something like
|
|
91
|
+
// `new Console(stdout[, stderr][, ignoreErrors])` and `new Console(options)`
|
|
92
|
+
// Try to find the parameter that this is being shared with
|
|
93
|
+
for (const property of markdownParameters) {
|
|
94
|
+
if (property.name === parameterName) {
|
|
95
|
+
return property;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const matchingOption = property.options?.find(
|
|
99
|
+
option => option.name === parameterName
|
|
100
|
+
);
|
|
101
|
+
if (matchingOption) {
|
|
102
|
+
return { ...matchingOption };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Default return if no matches are found
|
|
107
|
+
return { name: parameterName };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {string[]} declaredParameters
|
|
112
|
+
* @param {Array<import('./types.d.ts').ParameterList>} markdownParameters
|
|
113
|
+
*/
|
|
114
|
+
export function parseParameters(declaredParameters, markdownParameters) {
|
|
115
|
+
/**
|
|
116
|
+
* @type {Array<import('./types.d.ts').Parameter>}
|
|
117
|
+
*/
|
|
118
|
+
let parameters = [];
|
|
119
|
+
|
|
120
|
+
let optionalDepth = 0;
|
|
121
|
+
|
|
122
|
+
declaredParameters.forEach((parameterName, i) => {
|
|
123
|
+
/**
|
|
124
|
+
* @example 'length]]'
|
|
125
|
+
* @example 'arrayBuffer['
|
|
126
|
+
* @example '[sources['
|
|
127
|
+
* @example 'end'
|
|
128
|
+
*/
|
|
129
|
+
parameterName = parameterName.trim();
|
|
130
|
+
|
|
131
|
+
// We need to do three things here:
|
|
132
|
+
// 1. Determine the declared parameters' name
|
|
133
|
+
// 2. Determine if the parameter is optional
|
|
134
|
+
// 3. Determine if the parameter has a default value
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* This will handle the first and second thing for us
|
|
138
|
+
* @type {boolean}
|
|
139
|
+
*/
|
|
140
|
+
let isParameterOptional;
|
|
141
|
+
[parameterName, optionalDepth, isParameterOptional] =
|
|
142
|
+
parseNameAndOptionalStatus(parameterName, optionalDepth);
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Now let's work on the third thing
|
|
146
|
+
* @type {string | undefined}
|
|
147
|
+
*/
|
|
148
|
+
let defaultValue;
|
|
149
|
+
[parameterName, defaultValue] = parseDefaultValue(parameterName);
|
|
150
|
+
|
|
151
|
+
const parameter = findParameter(parameterName, i, markdownParameters);
|
|
152
|
+
|
|
153
|
+
if (isParameterOptional) {
|
|
154
|
+
parameter.optional = true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (defaultValue) {
|
|
158
|
+
parameter.default = defaultValue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
parameters.push(parameter);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return parameters;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {string} textRaw Something like `new buffer.Blob([sources[, options]])`
|
|
169
|
+
* @param {Array<import('./types.d.ts').ParameterList} markdownParameters The properties in the AST
|
|
170
|
+
* @returns {import('./types.d.ts').MethodSignature | undefined}
|
|
171
|
+
*/
|
|
172
|
+
export default (textRaw, markdownParameters) => {
|
|
173
|
+
/**
|
|
174
|
+
* @type {import('./types.d.ts').MethodSignature}
|
|
175
|
+
*/
|
|
176
|
+
const signature = { params: [] };
|
|
177
|
+
|
|
178
|
+
// Find the return/extends value & filter it out
|
|
179
|
+
markdownParameters = markdownParameters.filter(value => {
|
|
180
|
+
if (value.name === 'return' || value.name === 'extends') {
|
|
181
|
+
signature[value.name] = value;
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return true;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Extract the parameters from the method's declaration
|
|
190
|
+
* @example `[sources[, options]]`
|
|
191
|
+
*/
|
|
192
|
+
let [, declaredParameters] =
|
|
193
|
+
textRaw.replace(/^`|`$/g, '').match(PARAM_EXPRESSION) || [];
|
|
194
|
+
|
|
195
|
+
if (!declaredParameters) {
|
|
196
|
+
return signature;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @type {string[]}
|
|
201
|
+
* @example ['sources[,', 'options]]']
|
|
202
|
+
*/
|
|
203
|
+
declaredParameters = declaredParameters.split(',');
|
|
204
|
+
|
|
205
|
+
signature.params = parseParameters(declaredParameters, markdownParameters);
|
|
206
|
+
|
|
207
|
+
return signature;
|
|
208
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a parameter for methods or functions.
|
|
3
|
+
*/
|
|
4
|
+
export interface Parameter {
|
|
5
|
+
/**
|
|
6
|
+
* The name of the parameter.
|
|
7
|
+
*/
|
|
8
|
+
name: string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Indicates if the parameter is optional.
|
|
12
|
+
*/
|
|
13
|
+
optional?: boolean;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The default value for the parameter.
|
|
17
|
+
*/
|
|
18
|
+
default?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Represents a method signature, including its parameters and return type.
|
|
23
|
+
*/
|
|
24
|
+
export interface MethodSignature {
|
|
25
|
+
/**
|
|
26
|
+
* A list of parameters for the method.
|
|
27
|
+
*/
|
|
28
|
+
params: Parameter[];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The return type of the method.
|
|
32
|
+
*/
|
|
33
|
+
return?: Parameter;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Represents a list of parameters.
|
|
38
|
+
*/
|
|
39
|
+
export interface ParameterList {
|
|
40
|
+
/**
|
|
41
|
+
* Raw parameter description
|
|
42
|
+
*/
|
|
43
|
+
textRaw: string;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A short description of the parameter.
|
|
47
|
+
*/
|
|
48
|
+
desc?: string;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The name of the parameter.
|
|
52
|
+
*/
|
|
53
|
+
name: string;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The type of the parameter (E.G. string, boolean).
|
|
57
|
+
*/
|
|
58
|
+
type?: string;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The default value.
|
|
62
|
+
*/
|
|
63
|
+
default?: string;
|
|
64
|
+
|
|
65
|
+
options?: ParameterList;
|
|
66
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TODO(@avivkeller): Once oxc-parser supports WASM, or the
|
|
4
|
+
// bindings we need, remove this in favor of it. It's much
|
|
5
|
+
// faster.
|
|
6
|
+
//
|
|
7
|
+
// For now, however, this is our walker :-)
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {unknown} node
|
|
11
|
+
* @param {((node: { type: string }) => void) | Record<string, (node: never) => void>} visitors
|
|
12
|
+
*/
|
|
13
|
+
export const walk = (node, visitors) => {
|
|
14
|
+
if (node === null || typeof node !== 'object') {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (Array.isArray(node)) {
|
|
19
|
+
node.forEach(child => walk(child, visitors));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (typeof visitors === 'function') {
|
|
24
|
+
visitors(node);
|
|
25
|
+
} else {
|
|
26
|
+
visitors[node.type]?.(node);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const [key, value] of Object.entries(node)) {
|
|
30
|
+
if (key !== 'span' && key !== 'loc') {
|
|
31
|
+
walk(value, visitors);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { highlighter } from '../highlighter.mjs';
|
|
4
|
+
|
|
5
|
+
const [lightTheme, darkTheme] = highlighter.shiki.getLoadedThemes();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Slices a type's text by its resolved link ranges into hast children —
|
|
9
|
+
* plain text segments interleaved with `<a class="type-link">` anchors.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} value The type text
|
|
12
|
+
* @param {Array<{ start: number, end: number, href: string }>} links Sorted, disjoint ranges
|
|
13
|
+
* @returns {Array<import('hast').ElementContent>}
|
|
14
|
+
*/
|
|
15
|
+
const buildLinkedChildren = (value, links) => {
|
|
16
|
+
const children = [];
|
|
17
|
+
let cursor = 0;
|
|
18
|
+
|
|
19
|
+
for (const { start, end, href } of links) {
|
|
20
|
+
if (start > cursor) {
|
|
21
|
+
children.push({ type: 'text', value: value.slice(cursor, start) });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
children.push({
|
|
25
|
+
type: 'element',
|
|
26
|
+
tagName: 'a',
|
|
27
|
+
properties: { href, className: ['type-link'] },
|
|
28
|
+
children: [{ type: 'text', value: value.slice(start, end) }],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
cursor = end;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (cursor < value.length) {
|
|
35
|
+
children.push({ type: 'text', value: value.slice(cursor) });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return children;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Minimal mdast→hast handler for `typeAnnotation` nodes: one
|
|
43
|
+
* `<code class="type">` whose resolved identifiers are plain `<a>` links.
|
|
44
|
+
* No syntax highlighting — used by the legacy generators.
|
|
45
|
+
*
|
|
46
|
+
* @param {import('mdast-util-to-hast').State} state
|
|
47
|
+
* @param {import('mdast').Node} node
|
|
48
|
+
* @returns {import('hast').Element}
|
|
49
|
+
*/
|
|
50
|
+
export const typeAnnotationToHast = (state, node) => {
|
|
51
|
+
const result = {
|
|
52
|
+
type: 'element',
|
|
53
|
+
tagName: 'code',
|
|
54
|
+
properties: { className: ['type'] },
|
|
55
|
+
children: buildLinkedChildren(node.value, node.data?.links ?? []),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
state.patch(node, result);
|
|
59
|
+
|
|
60
|
+
return state.applyData(node, result);
|
|
61
|
+
};
|
|
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,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// A backslash escape before ASCII punctuation (CommonMark's escapable set).
|
|
4
|
+
// The tokenizer captures the raw source, so legacy escapes like `{string\[]}`
|
|
5
|
+
// and the mandatory `\|` inside GFM table cells must be decoded here.
|
|
6
|
+
const CHARACTER_ESCAPE = /\\([!-/:-@[-`{-~])/g;
|
|
7
|
+
|
|
8
|
+
// A single union separator, with any surrounding whitespace. Lookarounds
|
|
9
|
+
// exclude `||` (logical OR in default-value prose).
|
|
10
|
+
const UNION_SEPARATOR = /(?<!\|)\s*\|\s*(?!\|)/g;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates the mdast-util-from-markdown extension that compiles
|
|
14
|
+
* `typeAnnotation` tokens into `{ type: 'typeAnnotation', value }` nodes.
|
|
15
|
+
*
|
|
16
|
+
* The stored `value` is the canonical, single-line, unescaped TypeScript type
|
|
17
|
+
* text with union separators spaced (`A | B`); link offsets computed later
|
|
18
|
+
* are relative to it.
|
|
19
|
+
*
|
|
20
|
+
* @returns {import('mdast-util-from-markdown').Extension}
|
|
21
|
+
*/
|
|
22
|
+
export const typeAnnotationFromMarkdown = () => ({
|
|
23
|
+
enter: {
|
|
24
|
+
/**
|
|
25
|
+
* @this {import('mdast-util-from-markdown').CompileContext}
|
|
26
|
+
* @param {import('micromark-util-types').Token} token
|
|
27
|
+
*/
|
|
28
|
+
typeAnnotation(token) {
|
|
29
|
+
this.enter({ type: 'typeAnnotation', value: '' }, token);
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
exit: {
|
|
33
|
+
/**
|
|
34
|
+
* @this {import('mdast-util-from-markdown').CompileContext}
|
|
35
|
+
* @param {import('micromark-util-types').Token} token
|
|
36
|
+
*/
|
|
37
|
+
typeAnnotationValue(token) {
|
|
38
|
+
const node = this.stack.at(-1);
|
|
39
|
+
const chunk = this.sliceSerialize(token);
|
|
40
|
+
|
|
41
|
+
// Chunks are split by interior line endings; re-join with a space
|
|
42
|
+
node.value = node.value ? `${node.value} ${chunk}` : chunk;
|
|
43
|
+
},
|
|
44
|
+
/**
|
|
45
|
+
* @this {import('mdast-util-from-markdown').CompileContext}
|
|
46
|
+
* @param {import('micromark-util-types').Token} token
|
|
47
|
+
*/
|
|
48
|
+
typeAnnotation(token) {
|
|
49
|
+
const node = this.stack.at(-1);
|
|
50
|
+
|
|
51
|
+
node.value = node.value
|
|
52
|
+
.replace(/\s+/g, ' ')
|
|
53
|
+
.trim()
|
|
54
|
+
.replace(CHARACTER_ESCAPE, '$1')
|
|
55
|
+
.replace(UNION_SEPARATOR, ' | ');
|
|
56
|
+
|
|
57
|
+
this.exit(token);
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Creates the mdast-util-to-markdown extension that serializes
|
|
64
|
+
* `typeAnnotation` nodes back to their `{...}` source form.
|
|
65
|
+
*
|
|
66
|
+
* @returns {import('mdast-util-to-markdown').Options}
|
|
67
|
+
*/
|
|
68
|
+
export const typeAnnotationToMarkdown = () => ({
|
|
69
|
+
handlers: {
|
|
70
|
+
/**
|
|
71
|
+
* @param {{ value: string }} node
|
|
72
|
+
*/
|
|
73
|
+
typeAnnotation: node => `{${node.value}}`,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
typeAnnotationFromMarkdown,
|
|
5
|
+
typeAnnotationToMarkdown,
|
|
6
|
+
} from './mdast.mjs';
|
|
7
|
+
import { typeAnnotationSyntax } from './syntax.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Remark plugin that teaches the parser to treat any balanced `{...}` span in
|
|
11
|
+
* text as a `typeAnnotation` node whose value is a TypeScript type expression.
|
|
12
|
+
*
|
|
13
|
+
* Only registered on the non-MDX pipeline — in MDX, `{...}` is a real
|
|
14
|
+
* expression and is handled by remark-mdx instead.
|
|
15
|
+
*
|
|
16
|
+
* @this {import('unified').Processor}
|
|
17
|
+
*/
|
|
18
|
+
export default function remarkTypeAnnotations() {
|
|
19
|
+
const data = this.data();
|
|
20
|
+
|
|
21
|
+
(data.micromarkExtensions ??= []).push(typeAnnotationSyntax());
|
|
22
|
+
(data.fromMarkdownExtensions ??= []).push(typeAnnotationFromMarkdown());
|
|
23
|
+
(data.toMarkdownExtensions ??= []).push(typeAnnotationToMarkdown());
|
|
24
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const LEFT_CURLY_BRACE = '{'.charCodeAt(0);
|
|
4
|
+
const RIGHT_CURLY_BRACE = '}'.charCodeAt(0);
|
|
5
|
+
const DOLLAR_SIGN = '$'.charCodeAt(0);
|
|
6
|
+
const SPACE = ' '.charCodeAt(0);
|
|
7
|
+
|
|
8
|
+
// The type starters that are punctuation: `_` and `$` (identifiers), the
|
|
9
|
+
// openers of parenthesized, tuple, object, generic and string-literal types,
|
|
10
|
+
// the leading `-` of a negative numeric literal, the leading `&`/`|` of an
|
|
11
|
+
// intersection or union, and `\` (a legacy escape, decoded further down the
|
|
12
|
+
// pipeline). Alphanumeric and non-ASCII starters are handled by `isTypeStart`.
|
|
13
|
+
const TYPE_STARTER_PUNCTUATION = new Set(
|
|
14
|
+
[...'_$([{<\'"`-&|\\'].map(character => character.charCodeAt(0))
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* micromark preprocesses `\r`, `\n` and `\r\n` into virtual codes below -2
|
|
19
|
+
* (and represents EOF as `null`).
|
|
20
|
+
*
|
|
21
|
+
* @param {import('micromark-util-types').Code} code
|
|
22
|
+
*/
|
|
23
|
+
const isLineEnding = code => code !== null && code < -2;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A tab is preprocessed into -2, followed by -1 for each column it expands to.
|
|
27
|
+
*
|
|
28
|
+
* @param {import('micromark-util-types').Code} code
|
|
29
|
+
*/
|
|
30
|
+
const isSpace = code => code === SPACE || code === -1 || code === -2;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {import('micromark-util-types').Code} code
|
|
34
|
+
*/
|
|
35
|
+
const isAsciiAlphanumeric = code =>
|
|
36
|
+
(code >= 0x30 && code <= 0x39) || // 0-9
|
|
37
|
+
(code >= 0x41 && code <= 0x5a) || // A-Z
|
|
38
|
+
(code >= 0x61 && code <= 0x7a); // a-z
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether a type expression may begin with this character: an identifier or
|
|
42
|
+
* numeric literal (alphanumeric or non-ASCII) or one of the punctuation
|
|
43
|
+
* starters above. Prose that happens to sit between two braces (as in url.md's
|
|
44
|
+
* `U+007B ({), and U+007D (}).`) starts with punctuation that no type can, so
|
|
45
|
+
* this keeps such spans out.
|
|
46
|
+
*
|
|
47
|
+
* @param {import('micromark-util-types').Code} code
|
|
48
|
+
*/
|
|
49
|
+
const isTypeStart = code =>
|
|
50
|
+
code !== null &&
|
|
51
|
+
(isAsciiAlphanumeric(code) ||
|
|
52
|
+
// Non-ASCII identifier characters
|
|
53
|
+
code >= 0x80 ||
|
|
54
|
+
TYPE_STARTER_PUNCTUATION.has(code));
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A `{` directly after `$` is prose about template literals, not a type
|
|
58
|
+
* annotation.
|
|
59
|
+
*
|
|
60
|
+
* @param {import('micromark-util-types').Code} code
|
|
61
|
+
*/
|
|
62
|
+
const previous = code => code !== DOLLAR_SIGN;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Tokenizes a balanced `{...}` span as a single `typeAnnotation` token.
|
|
66
|
+
*
|
|
67
|
+
* The outer braces become `typeAnnotationMarker` tokens; everything between
|
|
68
|
+
* them becomes `typeAnnotationValue` chunks (one per line). An unbalanced or
|
|
69
|
+
* empty span, or one whose content cannot start a type, fails the construct,
|
|
70
|
+
* so the `{` falls back to literal text.
|
|
71
|
+
*
|
|
72
|
+
* @type {import('micromark-util-types').Tokenizer}
|
|
73
|
+
*/
|
|
74
|
+
function tokenizeTypeAnnotation(effects, ok, nok) {
|
|
75
|
+
// Nesting depth of inner `{`/`}` pairs (the wrapping pair is not counted)
|
|
76
|
+
let depth = 0;
|
|
77
|
+
// `{}` (no content at all) must not become an annotation
|
|
78
|
+
let hasContent = false;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* At the opening `{`.
|
|
82
|
+
*
|
|
83
|
+
* @type {import('micromark-util-types').State}
|
|
84
|
+
*/
|
|
85
|
+
const start = code => {
|
|
86
|
+
effects.enter('typeAnnotation');
|
|
87
|
+
effects.enter('typeAnnotationMarker');
|
|
88
|
+
effects.consume(code);
|
|
89
|
+
effects.exit('typeAnnotationMarker');
|
|
90
|
+
|
|
91
|
+
return between;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* At a position where a value chunk may start: right after the opening
|
|
96
|
+
* brace or after an interior line ending.
|
|
97
|
+
*
|
|
98
|
+
* @type {import('micromark-util-types').State}
|
|
99
|
+
*/
|
|
100
|
+
const between = code => {
|
|
101
|
+
if (code === null) {
|
|
102
|
+
return nok(code);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (code === RIGHT_CURLY_BRACE && depth === 0) {
|
|
106
|
+
return hasContent ? finish(code) : nok(code);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (isLineEnding(code)) {
|
|
110
|
+
effects.enter('lineEnding');
|
|
111
|
+
effects.consume(code);
|
|
112
|
+
effects.exit('lineEnding');
|
|
113
|
+
|
|
114
|
+
return between;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
effects.enter('typeAnnotationValue');
|
|
118
|
+
|
|
119
|
+
return value(code);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Inside a value chunk.
|
|
124
|
+
*
|
|
125
|
+
* @type {import('micromark-util-types').State}
|
|
126
|
+
*/
|
|
127
|
+
const value = code => {
|
|
128
|
+
if (
|
|
129
|
+
code === null ||
|
|
130
|
+
isLineEnding(code) ||
|
|
131
|
+
(code === RIGHT_CURLY_BRACE && depth === 0)
|
|
132
|
+
) {
|
|
133
|
+
effects.exit('typeAnnotationValue');
|
|
134
|
+
|
|
135
|
+
return between(code);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!hasContent && !isSpace(code)) {
|
|
139
|
+
if (!isTypeStart(code)) {
|
|
140
|
+
return nok(code);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
hasContent = true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (code === LEFT_CURLY_BRACE) {
|
|
147
|
+
depth++;
|
|
148
|
+
} else if (code === RIGHT_CURLY_BRACE) {
|
|
149
|
+
depth--;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
effects.consume(code);
|
|
153
|
+
|
|
154
|
+
return value;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* At the closing `}`.
|
|
159
|
+
*
|
|
160
|
+
* @type {import('micromark-util-types').State}
|
|
161
|
+
*/
|
|
162
|
+
const finish = code => {
|
|
163
|
+
effects.enter('typeAnnotationMarker');
|
|
164
|
+
effects.consume(code);
|
|
165
|
+
effects.exit('typeAnnotationMarker');
|
|
166
|
+
effects.exit('typeAnnotation');
|
|
167
|
+
|
|
168
|
+
return ok;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return start;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Creates the micromark syntax extension that recognizes `{...}` type
|
|
176
|
+
* annotations in text.
|
|
177
|
+
*
|
|
178
|
+
* @returns {import('micromark-util-types').Extension}
|
|
179
|
+
*/
|
|
180
|
+
export const typeAnnotationSyntax = () => ({
|
|
181
|
+
text: {
|
|
182
|
+
[LEFT_CURLY_BRACE]: {
|
|
183
|
+
name: 'typeAnnotation',
|
|
184
|
+
tokenize: tokenizeTypeAnnotation,
|
|
185
|
+
previous,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
});
|