@doc-kit/core 1.0.1 → 1.2.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +2 -1
  3. package/package.json +11 -6
  4. package/src/generators/index.mjs +3 -0
  5. package/src/generators/json/README.md +151 -0
  6. package/src/generators/json/constants.mjs +57 -0
  7. package/src/generators/json/generate.mjs +65 -0
  8. package/src/generators/json/generated/schema.d.ts +351 -0
  9. package/src/generators/json/index.mjs +27 -0
  10. package/src/generators/json/schema.json +529 -0
  11. package/src/generators/json/types.d.ts +29 -0
  12. package/src/generators/json/utils/document.mjs +58 -0
  13. package/src/generators/json/utils/entry.mjs +87 -0
  14. package/src/generators/json/utils/lifecycle.mjs +61 -0
  15. package/src/generators/json/utils/markdown.mjs +57 -0
  16. package/src/generators/json/utils/node.mjs +153 -0
  17. package/src/generators/json/utils/schema.mjs +15 -0
  18. package/src/generators/json/utils/signature.mjs +185 -0
  19. package/src/generators/json/utils/types.mjs +28 -0
  20. package/src/generators/json-all/README.md +31 -0
  21. package/src/generators/json-all/constants.mjs +5 -0
  22. package/src/generators/json-all/generate.mjs +35 -0
  23. package/src/generators/json-all/index.mjs +25 -0
  24. package/src/generators/json-all/schema.json +22 -0
  25. package/src/generators/json-all/types.d.ts +20 -0
  26. package/src/generators/json-simple/generate.mjs +2 -7
  27. package/src/generators/loader.mjs +8 -3
  28. package/src/generators/metadata/maps/mdn.json +9 -2
  29. package/src/generators/metadata/types.d.ts +24 -0
  30. package/src/generators/pipeline.mjs +169 -0
  31. package/src/generators/types.d.ts +20 -0
  32. package/src/generators.mjs +19 -12
  33. package/src/logger/logger.mjs +8 -0
  34. package/src/parsers/markdown.mjs +5 -6
  35. package/src/threading/chunk-worker.mjs +7 -0
  36. package/src/threading/index.mjs +1 -0
  37. package/src/utils/configuration/constants.mjs +13 -0
  38. package/src/utils/configuration/index.mjs +7 -2
  39. package/src/utils/file.mjs +10 -0
  40. package/src/utils/hierarchy.mjs +50 -0
  41. package/src/utils/overloads.mjs +54 -0
  42. package/src/utils/queries/index.mjs +2 -0
  43. package/src/utils/queries/utils.mjs +15 -0
  44. package/src/utils/remark-shiki.mjs +29 -0
  45. package/src/utils/remark.mjs +27 -49
  46. package/src/utils/signature/constants.mjs +3 -0
  47. package/src/utils/signature/extractListItem.mjs +140 -0
  48. package/src/utils/stability.mjs +24 -0
  49. package/src/utils/type-annotations/hast.mjs +0 -58
  50. package/src/utils/type-annotations/highlighted.mjs +64 -0
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ // Heading types that document a callable and therefore may appear several times
4
+ // in a row as overloaded signatures of the same function.
5
+ const OVERLOADABLE_TYPES = new Set(['method', 'ctor', 'classMethod']);
6
+
7
+ /**
8
+ * Two headings document the same function (i.e. are overloads of one another)
9
+ * when they sit at the same depth and share the same resolved name and type.
10
+ *
11
+ * @param {import('../generators/metadata/types').HeadingNode} a
12
+ * @param {import('../generators/metadata/types').HeadingNode} b
13
+ */
14
+ const isSameFunction = (a, b) =>
15
+ a.depth === b.depth &&
16
+ a.data.type === b.data.type &&
17
+ a.data.name === b.data.name;
18
+
19
+ /**
20
+ * Flags overloaded function headings so consumers can present a single entry
21
+ * per function.
22
+ *
23
+ * Node.js documents each overload of a function as its own heading (e.g. the
24
+ * five `new Buffer(...)` signatures). This marks the 2nd..nth heading of each
25
+ * such run with `isOverload`, and with `overloadOf` pointing at the slug of
26
+ * the run's first heading. The first (most stable) heading is left as-is.
27
+ *
28
+ * @param {Array<import('../generators/metadata/types').MetadataEntry>} entries - Entries, in document order.
29
+ * @returns {Array<import('../generators/metadata/types').MetadataEntry>} The same entries (mutated).
30
+ */
31
+ export const annotateOverloads = entries => {
32
+ for (let i = 0; i < entries.length; i++) {
33
+ const { heading } = entries[i];
34
+
35
+ if (!OVERLOADABLE_TYPES.has(heading.data.type)) {
36
+ continue;
37
+ }
38
+
39
+ // Flag each following heading that overloads the same function.
40
+ let end = i + 1;
41
+ while (
42
+ end < entries.length &&
43
+ isSameFunction(entries[end].heading, heading)
44
+ ) {
45
+ entries[end].heading.data.isOverload = true;
46
+ entries[end].heading.data.overloadOf = heading.data.slug;
47
+ end++;
48
+ }
49
+
50
+ i = end - 1;
51
+ }
52
+
53
+ return entries;
54
+ };
@@ -4,6 +4,8 @@ import { transformNodesToString } from '#utils/unist.mjs';
4
4
 
5
5
  import { isTypedListItem, isTypedList } from './utils.mjs';
6
6
 
7
+ export { splitTypedItems } from './utils.mjs';
8
+
7
9
  // This defines the actual REGEX Queries
8
10
  export const QUERIES = {
9
11
  // Fixes the references to Markdown pages into the API documentation
@@ -67,3 +67,18 @@ export const isTypedList = list => {
67
67
 
68
68
  return getTypedConfidence(list.children?.[0]?.children?.[0]?.children?.[0]);
69
69
  };
70
+
71
+ /**
72
+ * Splits a typed list's items into the leading typed ones and the rest:
73
+ * prose bullets that happen to share the list in the source markdown.
74
+ *
75
+ * @param {import('@types/mdast').List} list
76
+ * @returns {{ typed: Array<import('@types/mdast').ListItem>, rest: Array<import('@types/mdast').ListItem> }}
77
+ */
78
+ export const splitTypedItems = ({ children }) => {
79
+ const end = children.findIndex(item => !isTypedListItem(item));
80
+
81
+ return end === -1
82
+ ? { typed: children, rest: [] }
83
+ : { typed: children.slice(0, end), rest: children.slice(end) };
84
+ };
@@ -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
+ );
@@ -8,13 +8,13 @@ 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
- // Nodes the rehype pipelines pass through untouched.
17
- const passThrough = ['element'];
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
18
 
19
19
  /**
20
20
  * Renders an MDX JSX element as just its children, so the surrounding prose
@@ -30,15 +30,28 @@ const mdxElementToChildren = (state, node) => state.all(node);
30
30
  */
31
31
  const dropNode = () => undefined;
32
32
 
33
- // The HTML-string pipelines cannot render MDX nodes (rendering those is the
34
- // React generators' job): JSX elements degrade to their children so the
35
- // surrounding prose still renders, and expressions/ESM are dropped.
36
- const mdxToHastHandlers = {
37
- mdxJsxTextElement: mdxElementToChildren,
38
- mdxJsxFlowElement: mdxElementToChildren,
39
- mdxFlowExpression: dropNode,
40
- mdxTextExpression: dropNode,
41
- mdxjsEsm: dropNode,
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
+ },
42
55
  };
43
56
 
44
57
  /**
@@ -63,7 +76,7 @@ export const getRemark = lazy(() =>
63
76
  * `{` for type annotations that MDX would otherwise try to parse.
64
77
  */
65
78
  export const getRemarkMdx = lazy(() =>
66
- unified().use(remarkParse).use(remarkMdx).use(remarkGfm)
79
+ unified().use(remarkParse).use(remarkMdx).use(remarkGfm).use(remarkStringify)
67
80
  );
68
81
 
69
82
  /**
@@ -72,41 +85,6 @@ export const getRemarkMdx = lazy(() =>
72
85
  export const getRemarkRehype = lazy(() =>
73
86
  unified()
74
87
  .use(remarkParse)
75
- // We make Rehype ignore existing HTML nodes (just the node itself, not its children)
76
- // as these are nodes we manually created during the rehype process
77
- // We also allow dangerous HTML to be passed through, since we have HTML within our Markdown
78
- // and we trust the sources of the Markdown files
79
- .use(remarkRehype, {
80
- allowDangerousHtml: true,
81
- passThrough,
82
- handlers: { typeAnnotation: typeAnnotationToHast, ...mdxToHastHandlers },
83
- })
84
- // We allow dangerous HTML to be passed through, since we have HTML within our Markdown
85
- // and we trust the sources of the Markdown files
86
- .use(rehypeStringify, { allowDangerousHtml: true })
87
- );
88
-
89
- /**
90
- * Retrieves an instance of Remark configured to output stringified HTML code
91
- * including parsing Code Boxes with syntax highlighting
92
- */
93
- export const getRemarkRehypeWithShiki = lazy(() =>
94
- unified()
95
- .use(remarkParse)
96
- // We make Rehype ignore existing HTML nodes (just the node itself, not its children)
97
- // as these are nodes we manually created during the rehype process
98
- // We also allow dangerous HTML to be passed through, since we have HTML within our Markdown
99
- // and we trust the sources of the Markdown files
100
- .use(remarkRehype, {
101
- allowDangerousHtml: true,
102
- passThrough,
103
- // legacy-html gets the minimal (unhighlighted) type rendering
104
- handlers: { typeAnnotation: typeAnnotationToHast, ...mdxToHastHandlers },
105
- })
106
- // This is a custom ad-hoc within the Shiki Rehype plugin, used to highlight code
107
- // and transform them into HAST nodes
108
- .use(syntaxHighlighter)
109
- // We allow dangerous HTML to be passed through, since we have HTML within our Markdown
110
- // and we trust the sources of the Markdown files
88
+ .use(remarkRehype, rehypeOptions)
111
89
  .use(rehypeStringify, { allowDangerousHtml: true })
112
90
  );
@@ -4,6 +4,9 @@ export const NAME_EXPRESSION = /^['`"]?([^'`": {]+)['`"]?\s*:?\s*/;
4
4
  // Checks if there's a leading hyphen
5
5
  export const LEADING_HYPHEN = /^-\s*/;
6
6
 
7
+ // The separators between a parameter's type and its description
8
+ export const TRIMMABLE_PADDING_REGEX = /^[\s:-]+/;
9
+
7
10
  // Grabs the default value if present
8
11
  export const DEFAULT_EXPRESSION = /\s*\*\*Default:\*\*\s*([^]+)$/i;
9
12
 
@@ -0,0 +1,140 @@
1
+ 'use strict';
2
+
3
+ import { QUERIES, UNIST } from '#utils/queries/index.mjs';
4
+ import { transformNodesToString } from '#utils/unist.mjs';
5
+
6
+ import { DEFAULT_EXPRESSION, TRIMMABLE_PADDING_REGEX } from './constants.mjs';
7
+
8
+ // The text of the `**Default:**` marker introducing a default value
9
+ const DEFAULT_MARKER = /^default:$/i;
10
+
11
+ /**
12
+ * A typed list item taken apart into its parts, none of them stringified, so
13
+ * each consumer can render the description its own way.
14
+ *
15
+ * @typedef {object} ExtractedListItem
16
+ * @property {string | undefined} name The parameter name (the leading code span), if any
17
+ * @property {'Returns' | 'Extends' | 'Type' | undefined} prefix The special prefix the item starts with, if any
18
+ * @property {import('mdast').Node | undefined} annotation The leading `typeAnnotation` node, if any
19
+ * @property {Array<import('mdast').PhrasingContent>} text The rest of the first paragraph: the description,
20
+ * with leading separators trimmed and any `**Default:**` marker still in place
21
+ * @property {Array<import('mdast').BlockContent>} blocks The item's further block content, excluding its nested typed list
22
+ * @property {string | undefined} default The `**Default:**` value as written (code span included), if any
23
+ * @property {Array<import('mdast').ListItem>} items The items of the nested typed list, if any
24
+ */
25
+
26
+ /**
27
+ * Replaces the first node's text, or drops the node when nothing is left.
28
+ *
29
+ * @param {Array<import('mdast').PhrasingContent>} nodes
30
+ * @param {string} value The new text of the first node
31
+ */
32
+ const replaceLeadingText = (nodes, value) => {
33
+ if (value.trim()) {
34
+ nodes[0] = { ...nodes[0], value };
35
+ } else {
36
+ nodes.shift();
37
+ }
38
+ };
39
+
40
+ /**
41
+ * Takes a typed list item apart: `` `name` {Type} Description. **Default:** `value` ``,
42
+ * or one of the `Returns:`, `Extends:` and `Type:` forms.
43
+ *
44
+ * @param {import('mdast').ListItem} item
45
+ * @returns {ExtractedListItem}
46
+ */
47
+ export const extractListItem = item => {
48
+ const [paragraph, ...rest] = item.children ?? [];
49
+ const text = [...(paragraph?.children ?? [])];
50
+
51
+ const result = {
52
+ name: undefined,
53
+ prefix: undefined,
54
+ annotation: undefined,
55
+ text,
56
+ blocks: [],
57
+ default: undefined,
58
+ items: [],
59
+ };
60
+
61
+ const [first] = text;
62
+
63
+ if (first?.type === 'inlineCode') {
64
+ result.name = first.value.trimEnd();
65
+ text.shift();
66
+ } else if (first?.type === 'text') {
67
+ const match = first.value.match(QUERIES.typedListStarters);
68
+
69
+ if (match) {
70
+ result.prefix = match[1];
71
+ replaceLeadingText(text, first.value.slice(match[0].length));
72
+ }
73
+ }
74
+
75
+ // The whitespace between the name and the type
76
+ if (text[0]?.type === 'text' && !text[0].value.trim()) {
77
+ text.shift();
78
+ }
79
+
80
+ if (text[0]?.type === 'typeAnnotation') {
81
+ result.annotation = text.shift();
82
+ }
83
+
84
+ // Leading separators: `- description`, `: description`
85
+ if (text[0]?.type === 'text') {
86
+ replaceLeadingText(
87
+ text,
88
+ text[0].value.replace(TRIMMABLE_PADDING_REGEX, '')
89
+ );
90
+ }
91
+
92
+ result.default = DEFAULT_EXPRESSION.exec(transformNodesToString(text))?.[1]
93
+ .trim()
94
+ .replace(/\.$/, '');
95
+
96
+ for (const node of rest) {
97
+ if (result.items.length === 0 && UNIST.isLooselyTypedList(node)) {
98
+ result.items = node.children;
99
+ } else {
100
+ result.blocks.push(node);
101
+ }
102
+ }
103
+
104
+ return result;
105
+ };
106
+
107
+ /**
108
+ * Drops the `**Default:**` marker, and everything after it, from an item's
109
+ * description.
110
+ *
111
+ * @param {Array<import('mdast').PhrasingContent>} nodes The item's description
112
+ * @returns {Array<import('mdast').PhrasingContent>} The description without its default value
113
+ */
114
+ export const removeDefault = nodes => {
115
+ const index = nodes.findIndex(
116
+ node =>
117
+ node.type === 'strong' &&
118
+ DEFAULT_MARKER.test(transformNodesToString(node.children).trim())
119
+ );
120
+
121
+ if (index === -1) {
122
+ return nodes;
123
+ }
124
+
125
+ const kept = nodes.slice(0, index);
126
+ const last = kept.at(-1);
127
+
128
+ // The whitespace that separated the description from its default
129
+ if (last?.type === 'text') {
130
+ const value = last.value.trimEnd();
131
+
132
+ kept.pop();
133
+
134
+ if (value) {
135
+ kept.push({ ...last, value });
136
+ }
137
+ }
138
+
139
+ return kept;
140
+ };
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ import { slice } from 'mdast-util-slice-markdown';
4
+ import { toString } from 'mdast-util-to-string';
5
+
6
+ import { QUERIES } from './queries/index.mjs';
7
+
8
+ /**
9
+ * The content of a stability blockquote without its `Stability: N - `
10
+ * prefix, as a copy of the blockquote. The prefix may have been turned into a
11
+ * link by the AST stage; the separator goes with it.
12
+ *
13
+ * @param {import('../generators/metadata/types').StabilityNode} node
14
+ * @returns {import('mdast').Blockquote | null} The copy, or `null` when nothing follows the prefix
15
+ */
16
+ export const removeStabilityPrefix = node => {
17
+ const text = toString(node.children[0]);
18
+ const match = QUERIES.stabilityIndex.exec(text);
19
+ const start = match ? text.length - match[2].length : 0;
20
+
21
+ return slice(node, start, undefined, {
22
+ textHandling: { boundaries: 'preserve' },
23
+ }).node;
24
+ };
@@ -1,9 +1,5 @@
1
1
  'use strict';
2
2
 
3
- import { highlighter } from '#utils/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
+ };