@temelj/mdx 0.8.0 → 0.8.1
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/dist/compiler.d.ts +17 -15
- package/dist/compiler.js +67 -68
- package/dist/mod.d.ts +13 -3
- package/dist/mod.js +1 -1
- package/dist/plugins/heading-id/plugin.d.ts +8 -5
- package/dist/plugins/heading-id/plugin.js +18 -15
- package/dist/plugins/mod.d.ts +8 -4
- package/dist/plugins/remove-imports-exports.d.ts +7 -4
- package/dist/plugins/remove-imports-exports.js +8 -8
- package/dist/plugins/syntax-highlight/code-meta.d.ts +10 -7
- package/dist/plugins/syntax-highlight/code-meta.js +41 -41
- package/dist/plugins/syntax-highlight/plugin.d.ts +10 -8
- package/dist/plugins/syntax-highlight/plugin.js +134 -122
- package/dist/plugins/tree-processor/plugin.d.ts +8 -6
- package/dist/plugins/tree-processor/plugin.js +20 -17
- package/dist/types.d.ts +1 -2
- package/dist/types.js +0 -1
- package/package.json +9 -4
- package/.turbo/turbo-build.log +0 -4
- package/compiler.test.ts +0 -117
- package/compiler.ts +0 -124
- package/mod.ts +0 -3
- package/plugins/heading-id/plugin.ts +0 -42
- package/plugins/mod.ts +0 -4
- package/plugins/remove-imports-exports.ts +0 -18
- package/plugins/syntax-highlight/code-meta.test.ts +0 -22
- package/plugins/syntax-highlight/code-meta.ts +0 -76
- package/plugins/syntax-highlight/plugin.ts +0 -266
- package/plugins/tree-processor/plugin.ts +0 -54
- package/tsconfig.json +0 -18
- package/types.ts +0 -3
|
@@ -1,135 +1,147 @@
|
|
|
1
1
|
import { toString as hastToString } from "hast-util-to-string";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
codeToHast,
|
|
4
|
+
ShikiError
|
|
5
|
+
} from "shiki";
|
|
3
6
|
import { visit } from "unist-util-visit";
|
|
4
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
transformerNotationHighlight
|
|
9
|
+
} from "@shikijs/transformers";
|
|
5
10
|
import { numericRangeContains } from "@temelj/iterator";
|
|
6
11
|
import { extractCodeMeta } from "./code-meta";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
12
|
+
const syntaxHighlightPlugin = (options = {}) => {
|
|
13
|
+
return async (tree) => {
|
|
14
|
+
const promises = [];
|
|
15
|
+
async function visitor(node, index, parent) {
|
|
16
|
+
if (!parent || index === null || node.tagName !== "pre") {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const codeNode = node.children[0];
|
|
20
|
+
if (!codeNode || codeNode.type !== "element" || codeNode.tagName !== "code" || !codeNode.properties) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const lang = getNodeLanguage(codeNode);
|
|
24
|
+
if (typeof lang !== "string") {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const meta = extractCodeMeta(codeNode);
|
|
28
|
+
const sourceCode = hastToString(codeNode).trim();
|
|
29
|
+
const hastOptions = {
|
|
30
|
+
tokenizeTimeLimit: 1e4,
|
|
31
|
+
lang,
|
|
32
|
+
themes: {
|
|
33
|
+
light: "github-light-default",
|
|
34
|
+
dark: "github-dark-default"
|
|
35
|
+
},
|
|
36
|
+
...options.shikiHastOptions ?? {}
|
|
37
|
+
};
|
|
38
|
+
if (hastOptions.transformers === void 0) {
|
|
39
|
+
hastOptions.transformers = [];
|
|
40
|
+
}
|
|
41
|
+
if (options.highlight) {
|
|
42
|
+
hastOptions.transformers.push(
|
|
43
|
+
transformerNotationHighlight({
|
|
44
|
+
...options.highlight.transformer ?? {},
|
|
45
|
+
matchAlgorithm: "v3"
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
let lineCount = 0;
|
|
50
|
+
hastOptions.transformers.push({
|
|
51
|
+
code(node2) {
|
|
52
|
+
let lineIndex = 0;
|
|
53
|
+
for (const line of node2.children.filter(
|
|
54
|
+
(node3) => node3.type === "element"
|
|
55
|
+
)) {
|
|
56
|
+
line.properties["data-line"] = lineIndex + 1;
|
|
42
57
|
if (options.highlight) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
if (meta.highlight && numericRangeContains(meta.highlight.lineRange, lineIndex)) {
|
|
59
|
+
this.addClassToHast(
|
|
60
|
+
line,
|
|
61
|
+
options.highlight.transformer?.classActiveLine ?? "highlighted"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (options.lineNumbers && meta.showLineNumbers && (meta.showLineNumbers.lineRange.length === 0 || numericRangeContains(meta.showLineNumbers.lineRange, lineIndex))) {
|
|
66
|
+
this.addClassToHast(
|
|
67
|
+
line,
|
|
68
|
+
options.lineNumbers.className ?? "line-number"
|
|
69
|
+
);
|
|
47
70
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
line.properties["data-line"] = lineIndex + 1;
|
|
54
|
-
if (options.highlight) {
|
|
55
|
-
if (meta.highlight &&
|
|
56
|
-
numericRangeContains(meta.highlight.lineRange, lineIndex)) {
|
|
57
|
-
this.addClassToHast(line, options.highlight.transformer?.classActiveLine ??
|
|
58
|
-
"highlighted");
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
if (options.lineNumbers &&
|
|
62
|
-
meta.showLineNumbers &&
|
|
63
|
-
(meta.showLineNumbers.lineRange.length === 0 ||
|
|
64
|
-
numericRangeContains(meta.showLineNumbers.lineRange, lineIndex))) {
|
|
65
|
-
this.addClassToHast(line, options.lineNumbers.className ?? "line-number");
|
|
66
|
-
}
|
|
67
|
-
if (options.commandLine &&
|
|
68
|
-
meta.commandLine &&
|
|
69
|
-
(meta.commandLine.commandRange.length === 0 ||
|
|
70
|
-
numericRangeContains(meta.commandLine.commandRange, lineIndex))) {
|
|
71
|
-
this.addClassToHast(line, options.commandLine.className ?? "line-command");
|
|
72
|
-
}
|
|
73
|
-
lineIndex++;
|
|
74
|
-
}
|
|
75
|
-
lineCount = lineIndex;
|
|
76
|
-
if (meta.showLineNumbers) {
|
|
77
|
-
if (typeof node.properties.style !== "string") {
|
|
78
|
-
node.properties.style = "";
|
|
79
|
-
}
|
|
80
|
-
node.properties.style += `--${options.lineNumbers?.widthVariableName ?? "line-number-width"}:${1 + Math.floor(Math.log10(this.lines.length))}ch;`;
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
try {
|
|
85
|
-
const hast = await codeToHast(sourceCode, hastOptions);
|
|
86
|
-
// Insert highlighted code into parent
|
|
87
|
-
const hastPre = hast.children[0];
|
|
88
|
-
const properties = {
|
|
89
|
-
...hastPre.properties,
|
|
90
|
-
dataFileName: meta.fileName,
|
|
91
|
-
};
|
|
92
|
-
if (options.includeDataAttributes) {
|
|
93
|
-
if (options.includeDataAttributes.includes("language")) {
|
|
94
|
-
properties.dataLanguage = lang;
|
|
95
|
-
}
|
|
96
|
-
if (options.includeDataAttributes.includes("source-code")) {
|
|
97
|
-
properties.dataSourceCode = sourceCode;
|
|
98
|
-
}
|
|
99
|
-
if (options.includeDataAttributes.includes("line-count")) {
|
|
100
|
-
properties.dataLineCount = lineCount;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
parent.children.splice(index, 1, {
|
|
104
|
-
...hastPre,
|
|
105
|
-
properties,
|
|
106
|
-
});
|
|
71
|
+
if (options.commandLine && meta.commandLine && (meta.commandLine.commandRange.length === 0 || numericRangeContains(meta.commandLine.commandRange, lineIndex))) {
|
|
72
|
+
this.addClassToHast(
|
|
73
|
+
line,
|
|
74
|
+
options.commandLine.className ?? "line-command"
|
|
75
|
+
);
|
|
107
76
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
throw error;
|
|
77
|
+
lineIndex++;
|
|
78
|
+
}
|
|
79
|
+
lineCount = lineIndex;
|
|
80
|
+
if (meta.showLineNumbers) {
|
|
81
|
+
if (typeof node2.properties.style !== "string") {
|
|
82
|
+
node2.properties.style = "";
|
|
116
83
|
}
|
|
84
|
+
node2.properties.style += `--${options.lineNumbers?.widthVariableName ?? "line-number-width"}:${1 + Math.floor(Math.log10(this.lines.length))}ch;`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
try {
|
|
89
|
+
const hast = await codeToHast(sourceCode, hastOptions);
|
|
90
|
+
const hastPre = hast.children[0];
|
|
91
|
+
const properties = {
|
|
92
|
+
...hastPre.properties,
|
|
93
|
+
dataFileName: meta.fileName
|
|
94
|
+
};
|
|
95
|
+
if (options.includeDataAttributes) {
|
|
96
|
+
if (options.includeDataAttributes.includes("language")) {
|
|
97
|
+
properties.dataLanguage = lang;
|
|
98
|
+
}
|
|
99
|
+
if (options.includeDataAttributes.includes("source-code")) {
|
|
100
|
+
properties.dataSourceCode = sourceCode;
|
|
101
|
+
}
|
|
102
|
+
if (options.includeDataAttributes.includes("line-count")) {
|
|
103
|
+
properties.dataLineCount = lineCount;
|
|
104
|
+
}
|
|
117
105
|
}
|
|
118
|
-
|
|
119
|
-
|
|
106
|
+
parent.children.splice(index, 1, {
|
|
107
|
+
...hastPre,
|
|
108
|
+
properties
|
|
120
109
|
});
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof ShikiError) {
|
|
112
|
+
if (error.message.includes(
|
|
113
|
+
"Language `math` is not included in this bundle"
|
|
114
|
+
)) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
visit(
|
|
122
|
+
tree,
|
|
123
|
+
"element",
|
|
124
|
+
(node, index, parent) => {
|
|
125
|
+
promises.push(visitor(node, index, parent));
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
await Promise.all(promises);
|
|
129
|
+
return tree;
|
|
130
|
+
};
|
|
124
131
|
};
|
|
125
132
|
function getNodeLanguage(node) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return language;
|
|
133
|
+
if (!Array.isArray(node.properties.className)) {
|
|
134
|
+
return void 0;
|
|
135
|
+
}
|
|
136
|
+
const prefix = "language-";
|
|
137
|
+
const language = node.properties.className.find(
|
|
138
|
+
(name) => typeof name === "string" && name.startsWith(prefix)
|
|
139
|
+
);
|
|
140
|
+
if (typeof language === "string") {
|
|
141
|
+
return language.slice(prefix.length);
|
|
142
|
+
}
|
|
143
|
+
return language;
|
|
135
144
|
}
|
|
145
|
+
export {
|
|
146
|
+
syntaxHighlightPlugin
|
|
147
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
1
|
+
import { Plugin } from 'unified';
|
|
2
|
+
import { Element, Node } from 'hast';
|
|
3
|
+
|
|
4
|
+
type MdxTreeProcessor = (node: Element, index: number, parent: Element) => void | Promise<void>;
|
|
4
5
|
/**
|
|
5
6
|
* Options for {@linkcode treeProcessorPlugin}.
|
|
6
7
|
*/
|
|
@@ -12,7 +13,8 @@ type TreeProcessorPluginOptions = {
|
|
|
12
13
|
* A plugin that allows to process MDX tree nodes.
|
|
13
14
|
* It can be async or sync.
|
|
14
15
|
*/
|
|
15
|
-
|
|
16
|
+
declare const treeProcessorPlugin: Plugin<[
|
|
16
17
|
TreeProcessorPluginOptions?
|
|
17
|
-
],
|
|
18
|
-
|
|
18
|
+
], Node, Node>;
|
|
19
|
+
|
|
20
|
+
export { type MdxTreeProcessor, treeProcessorPlugin };
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { visit } from "unist-util-visit";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
2
|
+
const treeProcessorPlugin = ({ process } = { process: () => {
|
|
3
|
+
} }) => {
|
|
4
|
+
return async (tree) => {
|
|
5
|
+
const promises = [];
|
|
6
|
+
visit(
|
|
7
|
+
tree,
|
|
8
|
+
"element",
|
|
9
|
+
function visitor(node, index, parent) {
|
|
10
|
+
const p = process(node, index, parent);
|
|
11
|
+
if (p instanceof Promise || Object.prototype.toString.call(p) === "[object Promise]") {
|
|
12
|
+
promises.push(p);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
);
|
|
16
|
+
await Promise.allSettled(promises);
|
|
17
|
+
return tree;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export {
|
|
21
|
+
treeProcessorPlugin
|
|
19
22
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export type { Element as HastElement, Node as HastNode };
|
|
1
|
+
export { Element as HastElement, Node as HastNode } from 'hast';
|
package/dist/types.js
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@temelj/mdx",
|
|
3
3
|
"description": "MDX processing utilities.",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "0.8.
|
|
5
|
+
"version": "0.8.1",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/mod.js",
|
|
8
8
|
"types": "./dist/mod.d.ts",
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
"import": "./dist/mod.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
15
18
|
"dependencies": {
|
|
16
19
|
"@mdx-js/mdx": "^3.1.1",
|
|
17
20
|
"@shikijs/transformers": "^3.15.0",
|
|
@@ -29,16 +32,18 @@
|
|
|
29
32
|
"vfile": "^6.0.3",
|
|
30
33
|
"vfile-matter": "^5.0.1",
|
|
31
34
|
"zod": "^4.1.12",
|
|
32
|
-
"@temelj/iterator": "0.8.
|
|
35
|
+
"@temelj/iterator": "0.8.1"
|
|
33
36
|
},
|
|
34
37
|
"devDependencies": {
|
|
35
38
|
"@types/mdast": "^4.0.4",
|
|
36
|
-
"@types/node": "24.10.
|
|
39
|
+
"@types/node": "24.10.1",
|
|
40
|
+
"tsup": "^8.5.1",
|
|
41
|
+
"shx": "^0.4.0",
|
|
37
42
|
"typescript": "^5.9.3",
|
|
38
43
|
"vitest": "^4.0.8"
|
|
39
44
|
},
|
|
40
45
|
"scripts": {
|
|
41
|
-
"build": "
|
|
46
|
+
"build": "tsup",
|
|
42
47
|
"test": "vitest run",
|
|
43
48
|
"typecheck": "tsc --noEmit",
|
|
44
49
|
"clean": "shx rm -rf dist"
|
package/.turbo/turbo-build.log
DELETED
package/compiler.test.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "vitest";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
|
|
4
|
-
import { MdxCompiler } from "./compiler";
|
|
5
|
-
import { headingIdPlugin } from "./plugins/heading-id/plugin";
|
|
6
|
-
import { syntaxHighlightPlugin } from "./plugins/syntax-highlight/plugin";
|
|
7
|
-
import { treeProcessorPlugin } from "./plugins/tree-processor/plugin";
|
|
8
|
-
|
|
9
|
-
test("mdx - compile", async () => {
|
|
10
|
-
let headingCount = 0;
|
|
11
|
-
const compiler = new MdxCompiler()
|
|
12
|
-
.withRehypePlugin(headingIdPlugin, {
|
|
13
|
-
prefix: "h-",
|
|
14
|
-
})
|
|
15
|
-
.withRehypePlugin(treeProcessorPlugin, {
|
|
16
|
-
process: (element) => {
|
|
17
|
-
if (element.tagName.startsWith("h")) {
|
|
18
|
-
headingCount++;
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
})
|
|
22
|
-
.withRehypePlugin(syntaxHighlightPlugin, {
|
|
23
|
-
includeDataAttributes: ["language", "source-code", "line-count"],
|
|
24
|
-
highlight: {
|
|
25
|
-
transformer: { classActiveLine: "hl" },
|
|
26
|
-
},
|
|
27
|
-
lineNumbers: {},
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const frontmatterSchema = z.object({
|
|
31
|
-
title: z.string(),
|
|
32
|
-
x: z.optional(z.number()),
|
|
33
|
-
b: z.optional(z.boolean()).default(true),
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const artifact = await compiler.compile(
|
|
37
|
-
`
|
|
38
|
-
---
|
|
39
|
-
title: Test
|
|
40
|
-
x: 42
|
|
41
|
-
---
|
|
42
|
-
|
|
43
|
-
# Hello
|
|
44
|
-
## Title 2
|
|
45
|
-
## Duplicate
|
|
46
|
-
## Duplicate
|
|
47
|
-
|
|
48
|
-
\`\`\`ts {"highlight":"2", "showLineNumbers":true}
|
|
49
|
-
type Mdx = never; // [!code highlight]
|
|
50
|
-
const x1 = 1;
|
|
51
|
-
\`\`\`
|
|
52
|
-
`.trim(),
|
|
53
|
-
{},
|
|
54
|
-
frontmatterSchema,
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
expect(headingCount).toBe(4);
|
|
58
|
-
expect(artifact.frontmatter.title).toBe("Test");
|
|
59
|
-
expect(artifact.frontmatter.x).toBe(42);
|
|
60
|
-
expect(artifact.frontmatter.b).toBe(true);
|
|
61
|
-
|
|
62
|
-
await expect(() =>
|
|
63
|
-
compiler.compile("", {}, frontmatterSchema),
|
|
64
|
-
).rejects.toThrow(z.ZodError);
|
|
65
|
-
|
|
66
|
-
const value = artifact.compiled;
|
|
67
|
-
expect(typeof value === "string").toBe(true);
|
|
68
|
-
expect(value?.includes('"Hello"')).toBe(true);
|
|
69
|
-
expect(value?.includes('"h-title-2"')).toBe(true);
|
|
70
|
-
|
|
71
|
-
expect(value?.includes('"data-language": "ts"')).toBe(true);
|
|
72
|
-
expect(value?.includes('"data-line-count": "2"')).toBe(true);
|
|
73
|
-
expect(value?.includes('"data-source-code": "')).toBe(true);
|
|
74
|
-
|
|
75
|
-
expect(
|
|
76
|
-
value?.includes('"data-line": "1"') && value?.includes('"data-line": "2"'),
|
|
77
|
-
).toBe(true);
|
|
78
|
-
expect(value?.includes('className: "line hl line-number"')).toBe(true);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("mdx - parse frontmatter", async () => {
|
|
82
|
-
async function compile<TFrontmatterSchema extends z.ZodSchema>(
|
|
83
|
-
frontmatter: Record<string, unknown>,
|
|
84
|
-
schema: TFrontmatterSchema,
|
|
85
|
-
): Promise<z.output<TFrontmatterSchema>> {
|
|
86
|
-
const compiler = new MdxCompiler();
|
|
87
|
-
|
|
88
|
-
const artifact = await compiler.compile(
|
|
89
|
-
`
|
|
90
|
-
---
|
|
91
|
-
${JSON.stringify(frontmatter)}
|
|
92
|
-
---
|
|
93
|
-
`.trim(),
|
|
94
|
-
{},
|
|
95
|
-
schema,
|
|
96
|
-
);
|
|
97
|
-
return artifact.frontmatter;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const fm1 = await compile(
|
|
101
|
-
{ x: 42 },
|
|
102
|
-
z.object({
|
|
103
|
-
x: z.number(),
|
|
104
|
-
}),
|
|
105
|
-
);
|
|
106
|
-
const _fm1type: { x: number } = fm1;
|
|
107
|
-
expect(fm1).toStrictEqual({ x: 42 });
|
|
108
|
-
|
|
109
|
-
const fm2 = await compile(
|
|
110
|
-
{ s: "abc" },
|
|
111
|
-
z.object({
|
|
112
|
-
s: z.string(),
|
|
113
|
-
}),
|
|
114
|
-
);
|
|
115
|
-
const _fm2type: { s: string } = fm2;
|
|
116
|
-
expect(fm2).toStrictEqual({ s: "abc" });
|
|
117
|
-
});
|
package/compiler.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
compile as compileMdxJs,
|
|
3
|
-
type CompileOptions as MdxJsCompileOptions,
|
|
4
|
-
} from "@mdx-js/mdx";
|
|
5
|
-
import remarkFrontmatterPlugin from "remark-frontmatter";
|
|
6
|
-
import remarkGfmPlugin from "remark-gfm";
|
|
7
|
-
import { VFile } from "vfile";
|
|
8
|
-
import { matter } from "vfile-matter";
|
|
9
|
-
import type { Pluggable, PluggableList, Plugin } from "unified";
|
|
10
|
-
import type { z } from "zod";
|
|
11
|
-
|
|
12
|
-
import type { HastNode } from "./types";
|
|
13
|
-
|
|
14
|
-
export { remarkFrontmatterPlugin, remarkGfmPlugin };
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Represents the source content for an MDX document.
|
|
18
|
-
*/
|
|
19
|
-
export type MdxSource = string | Uint8Array;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Options for compiling an MDX document.
|
|
23
|
-
*/
|
|
24
|
-
export interface MdxCompileOptions {
|
|
25
|
-
frontmatterOnly?: boolean | undefined;
|
|
26
|
-
mdxOptions?: MdxJsCompileOptions;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* The result of compiling an MDX document.
|
|
31
|
-
* Compiled content is optional, because sometimes we only need the frontmatter.
|
|
32
|
-
*/
|
|
33
|
-
export interface MdxArtifact<TFrontmatter = Record<string, unknown>> {
|
|
34
|
-
compiled?: string | undefined;
|
|
35
|
-
frontmatter: TFrontmatter;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* A reusable compiler for compiling MDX.
|
|
40
|
-
*/
|
|
41
|
-
export class MdxCompiler {
|
|
42
|
-
private readonly mdxOptions: MdxJsCompileOptions;
|
|
43
|
-
private readonly remarkPlugins: PluggableList;
|
|
44
|
-
private readonly rehypePlugins: PluggableList;
|
|
45
|
-
|
|
46
|
-
public constructor(mdxOptions: MdxJsCompileOptions = {}) {
|
|
47
|
-
this.mdxOptions = mdxOptions;
|
|
48
|
-
this.remarkPlugins = [remarkFrontmatterPlugin, remarkGfmPlugin];
|
|
49
|
-
this.rehypePlugins = [];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
public withRemarkPlugin<TOptions>(
|
|
53
|
-
plugin: Plugin<[TOptions?], HastNode, HastNode>,
|
|
54
|
-
options?: TOptions,
|
|
55
|
-
): MdxCompiler {
|
|
56
|
-
if (options === undefined) {
|
|
57
|
-
this.remarkPlugins.push(plugin as Pluggable);
|
|
58
|
-
} else {
|
|
59
|
-
this.remarkPlugins.push([plugin, options] as Pluggable);
|
|
60
|
-
}
|
|
61
|
-
return this;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
public withRehypePlugin<TOptions>(
|
|
65
|
-
plugin: Plugin<[TOptions?], HastNode, HastNode>,
|
|
66
|
-
options?: TOptions,
|
|
67
|
-
): MdxCompiler {
|
|
68
|
-
if (options === undefined) {
|
|
69
|
-
this.rehypePlugins.push(plugin as Pluggable);
|
|
70
|
-
} else {
|
|
71
|
-
this.rehypePlugins.push([plugin, options] as Pluggable);
|
|
72
|
-
}
|
|
73
|
-
return this;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
public async compile<TFrontmatterSchema extends z.ZodSchema>(
|
|
77
|
-
source: MdxSource,
|
|
78
|
-
{ frontmatterOnly = false, mdxOptions }: MdxCompileOptions = {},
|
|
79
|
-
frontmatterSchema?: TFrontmatterSchema,
|
|
80
|
-
): Promise<MdxArtifact<z.output<TFrontmatterSchema>>> {
|
|
81
|
-
const vfile = new VFile({
|
|
82
|
-
value: source,
|
|
83
|
-
// Needed so vfile doesn't need to access fs.
|
|
84
|
-
cwd: "/",
|
|
85
|
-
path: "/",
|
|
86
|
-
message: {},
|
|
87
|
-
messages: {},
|
|
88
|
-
});
|
|
89
|
-
matter(vfile, { strip: true });
|
|
90
|
-
|
|
91
|
-
const remarkPlugins = [
|
|
92
|
-
...this.remarkPlugins,
|
|
93
|
-
...(this.mdxOptions?.remarkPlugins ?? []),
|
|
94
|
-
...(mdxOptions?.remarkPlugins ?? []),
|
|
95
|
-
];
|
|
96
|
-
const rehypePlugins = [
|
|
97
|
-
...this.rehypePlugins,
|
|
98
|
-
...(this.mdxOptions?.rehypePlugins ?? []),
|
|
99
|
-
...(mdxOptions?.rehypePlugins ?? []),
|
|
100
|
-
];
|
|
101
|
-
|
|
102
|
-
let compiled: string | undefined;
|
|
103
|
-
if (!frontmatterOnly) {
|
|
104
|
-
const compiledFile = await compileMdxJs(vfile, {
|
|
105
|
-
outputFormat: "function-body",
|
|
106
|
-
...(this.mdxOptions ?? {}),
|
|
107
|
-
...(mdxOptions ?? {}),
|
|
108
|
-
remarkPlugins,
|
|
109
|
-
rehypePlugins,
|
|
110
|
-
});
|
|
111
|
-
compiled = compiledFile.value.toString();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const frontmatter =
|
|
115
|
-
frontmatterSchema === undefined
|
|
116
|
-
? (vfile.data.matter ?? {})
|
|
117
|
-
: frontmatterSchema.parse(vfile.data.matter ?? {});
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
compiled,
|
|
121
|
-
frontmatter: frontmatter as z.output<TFrontmatterSchema>,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
}
|
package/mod.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import GithubSlugger from "github-slugger";
|
|
2
|
-
import { toString as hastToString } from "hast-util-to-string";
|
|
3
|
-
import { visit } from "unist-util-visit";
|
|
4
|
-
import type { Plugin } from "unified";
|
|
5
|
-
|
|
6
|
-
import type { HastElement, HastNode } from "../../types";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Options for {@linkcode headingIdPlugin}.
|
|
10
|
-
*/
|
|
11
|
-
export interface HeadingIdPluginOptions {
|
|
12
|
-
/** Prefix for title slugs. */
|
|
13
|
-
prefix?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* A rehype plugin that adds IDs to heading tags.
|
|
18
|
-
*/
|
|
19
|
-
export const headingIdPlugin: Plugin<
|
|
20
|
-
[HeadingIdPluginOptions?],
|
|
21
|
-
HastNode,
|
|
22
|
-
HastNode
|
|
23
|
-
> = ({ prefix } = {}) => {
|
|
24
|
-
const slugger = new GithubSlugger();
|
|
25
|
-
return (tree) => {
|
|
26
|
-
slugger.reset();
|
|
27
|
-
visit(
|
|
28
|
-
tree,
|
|
29
|
-
"element",
|
|
30
|
-
function visitor(
|
|
31
|
-
node: HastElement,
|
|
32
|
-
index: number,
|
|
33
|
-
parent: HastElement,
|
|
34
|
-
): void {
|
|
35
|
-
if (!parent || index === null || !node.tagName.startsWith("h")) return;
|
|
36
|
-
const id = (prefix ?? "") + slugger.slug(hastToString(node));
|
|
37
|
-
node.properties.id = id;
|
|
38
|
-
},
|
|
39
|
-
);
|
|
40
|
-
return tree;
|
|
41
|
-
};
|
|
42
|
-
};
|