@reslide-dev/mdx 0.0.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/index.d.mts +4326 -0
- package/dist/index.mjs +310 -0
- package/package.json +49 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import remarkFrontmatter from "remark-frontmatter";
|
|
2
|
+
import { visit } from "unist-util-visit";
|
|
3
|
+
import { compile } from "@mdx-js/mdx";
|
|
4
|
+
import remarkDirective from "remark-directive";
|
|
5
|
+
//#region src/remark-slides.ts
|
|
6
|
+
function parseYamlString(value) {
|
|
7
|
+
const options = {};
|
|
8
|
+
for (const line of value.split("\n")) {
|
|
9
|
+
const match = line.match(/^(\w[\w-]*):\s*(.+)$/);
|
|
10
|
+
if (match) options[match[1]] = match[2].trim();
|
|
11
|
+
}
|
|
12
|
+
return options;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Try to extract frontmatter-like options from a slide section's first nodes.
|
|
16
|
+
*
|
|
17
|
+
* Two patterns are recognized:
|
|
18
|
+
* 1. `yaml` node (first slide, parsed by remark-frontmatter)
|
|
19
|
+
* 2. Setext heading created by remark when `---\nkey: val\n---` appears
|
|
20
|
+
* after a thematicBreak separator
|
|
21
|
+
*/
|
|
22
|
+
function tryExtractOptionsFromNodes(nodes) {
|
|
23
|
+
if (nodes[0]?.type === "yaml") return {
|
|
24
|
+
options: parseYamlString(nodes[0].value),
|
|
25
|
+
contentStart: 1
|
|
26
|
+
};
|
|
27
|
+
if (nodes[0]?.type === "heading") {
|
|
28
|
+
const heading = nodes[0];
|
|
29
|
+
if (heading.children.length === 1 && heading.children[0]?.type === "text") {
|
|
30
|
+
const text = heading.children[0].value;
|
|
31
|
+
if (text.split("\n").every((line) => /^(\w[\w-]*):\s*.+$/.test(line.trim()))) return {
|
|
32
|
+
options: parseYamlString(text),
|
|
33
|
+
contentStart: 1
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Remark plugin that splits MDX content at `---` (thematic breaks)
|
|
41
|
+
* into `<Slide>` components wrapped in a `<Deck>`.
|
|
42
|
+
*
|
|
43
|
+
* Internally enables remark-frontmatter for the first YAML block.
|
|
44
|
+
* Subsequent slide frontmatters are detected via setext heading pattern.
|
|
45
|
+
*/
|
|
46
|
+
function remarkSlides() {
|
|
47
|
+
this.use(remarkFrontmatter, ["yaml"]);
|
|
48
|
+
return (tree) => {
|
|
49
|
+
const slides = [];
|
|
50
|
+
let current = [];
|
|
51
|
+
for (const node of tree.children) if (node.type === "thematicBreak") {
|
|
52
|
+
slides.push(current);
|
|
53
|
+
current = [];
|
|
54
|
+
} else current.push(node);
|
|
55
|
+
slides.push(current);
|
|
56
|
+
const slideElements = [];
|
|
57
|
+
for (const slideContent of slides) {
|
|
58
|
+
if (slideContent.length === 0) continue;
|
|
59
|
+
const extracted = tryExtractOptionsFromNodes(slideContent);
|
|
60
|
+
const options = extracted?.options ?? {};
|
|
61
|
+
const contentStart = extracted?.contentStart ?? 0;
|
|
62
|
+
const content = slideContent.slice(contentStart);
|
|
63
|
+
if (content.length === 0 && Object.keys(options).length === 0) continue;
|
|
64
|
+
const clickCount = countClickDirectives(content);
|
|
65
|
+
const slideIndex = slideElements.length;
|
|
66
|
+
const attrs = [];
|
|
67
|
+
if (options.layout) attrs.push({
|
|
68
|
+
type: "mdxJsxAttribute",
|
|
69
|
+
name: "layout",
|
|
70
|
+
value: options.layout
|
|
71
|
+
});
|
|
72
|
+
if (options.class) attrs.push({
|
|
73
|
+
type: "mdxJsxAttribute",
|
|
74
|
+
name: "className",
|
|
75
|
+
value: options.class
|
|
76
|
+
});
|
|
77
|
+
const slideChildren = [...content];
|
|
78
|
+
if (clickCount > 0) slideChildren.unshift({
|
|
79
|
+
type: "mdxJsxFlowElement",
|
|
80
|
+
name: "ClickSteps",
|
|
81
|
+
attributes: [{
|
|
82
|
+
type: "mdxJsxAttribute",
|
|
83
|
+
name: "slideIndex",
|
|
84
|
+
value: {
|
|
85
|
+
type: "mdxJsxAttributeValueExpression",
|
|
86
|
+
value: String(slideIndex),
|
|
87
|
+
data: { estree: createNumericExpression(slideIndex) }
|
|
88
|
+
}
|
|
89
|
+
}, {
|
|
90
|
+
type: "mdxJsxAttribute",
|
|
91
|
+
name: "count",
|
|
92
|
+
value: {
|
|
93
|
+
type: "mdxJsxAttributeValueExpression",
|
|
94
|
+
value: String(clickCount),
|
|
95
|
+
data: { estree: createNumericExpression(clickCount) }
|
|
96
|
+
}
|
|
97
|
+
}],
|
|
98
|
+
children: []
|
|
99
|
+
});
|
|
100
|
+
slideElements.push({
|
|
101
|
+
type: "mdxJsxFlowElement",
|
|
102
|
+
name: "Slide",
|
|
103
|
+
attributes: attrs,
|
|
104
|
+
children: slideChildren
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
tree.children = [{
|
|
108
|
+
type: "mdxJsxFlowElement",
|
|
109
|
+
name: "Deck",
|
|
110
|
+
attributes: [],
|
|
111
|
+
children: slideElements
|
|
112
|
+
}];
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function countClickDirectives(nodes) {
|
|
116
|
+
let count = 0;
|
|
117
|
+
function walk(node) {
|
|
118
|
+
const n = node;
|
|
119
|
+
if ((n.type === "leafDirective" || n.type === "containerDirective") && n.name === "click") count++;
|
|
120
|
+
if (n.children && Array.isArray(n.children)) for (const child of n.children) walk(child);
|
|
121
|
+
}
|
|
122
|
+
for (const node of nodes) walk(node);
|
|
123
|
+
return count;
|
|
124
|
+
}
|
|
125
|
+
function createNumericExpression(value) {
|
|
126
|
+
return {
|
|
127
|
+
type: "Program",
|
|
128
|
+
sourceType: "module",
|
|
129
|
+
body: [{
|
|
130
|
+
type: "ExpressionStatement",
|
|
131
|
+
expression: {
|
|
132
|
+
type: "Literal",
|
|
133
|
+
value,
|
|
134
|
+
raw: String(value)
|
|
135
|
+
}
|
|
136
|
+
}]
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/remark-click.ts
|
|
141
|
+
/**
|
|
142
|
+
* Remark plugin that converts `::click` directives (from remark-directive)
|
|
143
|
+
* into `<Click>` MDX JSX components.
|
|
144
|
+
*
|
|
145
|
+
* Supports both leaf (`::click`) and container (`::: click ... :::`) forms.
|
|
146
|
+
* Auto-increments the `at` attribute for sequential click steps.
|
|
147
|
+
*/
|
|
148
|
+
function remarkClick() {
|
|
149
|
+
return (tree) => {
|
|
150
|
+
let stepCounter = 0;
|
|
151
|
+
visit(tree, (node, index, parent) => {
|
|
152
|
+
if (node.type !== "leafDirective" && node.type !== "containerDirective") return;
|
|
153
|
+
if (node.name !== "click") return;
|
|
154
|
+
if (index == null || !parent) return;
|
|
155
|
+
stepCounter++;
|
|
156
|
+
const attrs = [{
|
|
157
|
+
type: "mdxJsxAttribute",
|
|
158
|
+
name: "at",
|
|
159
|
+
value: {
|
|
160
|
+
type: "mdxJsxAttributeValueExpression",
|
|
161
|
+
value: String(stepCounter),
|
|
162
|
+
data: { estree: {
|
|
163
|
+
type: "Program",
|
|
164
|
+
sourceType: "module",
|
|
165
|
+
body: [{
|
|
166
|
+
type: "ExpressionStatement",
|
|
167
|
+
expression: {
|
|
168
|
+
type: "Literal",
|
|
169
|
+
value: stepCounter,
|
|
170
|
+
raw: String(stepCounter)
|
|
171
|
+
}
|
|
172
|
+
}]
|
|
173
|
+
} }
|
|
174
|
+
}
|
|
175
|
+
}];
|
|
176
|
+
if (node.type === "leafDirective") {
|
|
177
|
+
const siblings = parent.children;
|
|
178
|
+
const gathered = [];
|
|
179
|
+
let i = index + 1;
|
|
180
|
+
while (i < siblings.length) {
|
|
181
|
+
const sibling = siblings[i];
|
|
182
|
+
if ((sibling.type === "leafDirective" || sibling.type === "containerDirective") && sibling.name === "click") break;
|
|
183
|
+
if (sibling.type === "thematicBreak") break;
|
|
184
|
+
gathered.push(siblings[i]);
|
|
185
|
+
i++;
|
|
186
|
+
}
|
|
187
|
+
if (gathered.length > 0) siblings.splice(index + 1, gathered.length);
|
|
188
|
+
parent.children[index] = {
|
|
189
|
+
type: "mdxJsxFlowElement",
|
|
190
|
+
name: "Click",
|
|
191
|
+
attributes: attrs,
|
|
192
|
+
children: gathered
|
|
193
|
+
};
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
parent.children[index] = {
|
|
197
|
+
type: "mdxJsxFlowElement",
|
|
198
|
+
name: "Click",
|
|
199
|
+
attributes: attrs,
|
|
200
|
+
children: node.children
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/remark-mark.ts
|
|
207
|
+
/**
|
|
208
|
+
* Remark plugin that converts `:mark[text]{.style}` directives
|
|
209
|
+
* into `<Mark>` MDX JSX components.
|
|
210
|
+
*
|
|
211
|
+
* Supports styles: highlight, underline, circle
|
|
212
|
+
* Supports color via additional class: .orange, .red, .blue, etc.
|
|
213
|
+
*
|
|
214
|
+
* Example: `:mark[important text]{.highlight.orange}`
|
|
215
|
+
* Produces: `<Mark type="highlight" color="orange">important text</Mark>`
|
|
216
|
+
*/
|
|
217
|
+
function remarkMark() {
|
|
218
|
+
return (tree) => {
|
|
219
|
+
visit(tree, (node, index, parent) => {
|
|
220
|
+
if (node.type !== "textDirective") return;
|
|
221
|
+
if (node.name !== "mark") return;
|
|
222
|
+
if (index == null || !parent) return;
|
|
223
|
+
const classes = (node.attributes?.class ?? "").split(/\s+/).filter(Boolean);
|
|
224
|
+
const markTypes = [
|
|
225
|
+
"highlight",
|
|
226
|
+
"underline",
|
|
227
|
+
"circle"
|
|
228
|
+
];
|
|
229
|
+
let type = "highlight";
|
|
230
|
+
let color;
|
|
231
|
+
for (const cls of classes) if (markTypes.includes(cls)) type = cls;
|
|
232
|
+
else if (cls) color = cls;
|
|
233
|
+
const attrs = [{
|
|
234
|
+
type: "mdxJsxAttribute",
|
|
235
|
+
name: "type",
|
|
236
|
+
value: type
|
|
237
|
+
}];
|
|
238
|
+
if (color) attrs.push({
|
|
239
|
+
type: "mdxJsxAttribute",
|
|
240
|
+
name: "color",
|
|
241
|
+
value: color
|
|
242
|
+
});
|
|
243
|
+
parent.children[index] = {
|
|
244
|
+
type: "mdxJsxTextElement",
|
|
245
|
+
name: "Mark",
|
|
246
|
+
attributes: attrs,
|
|
247
|
+
children: node.children
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src/compile.ts
|
|
254
|
+
/**
|
|
255
|
+
* Parse frontmatter from MDX source without compiling.
|
|
256
|
+
* Useful for extracting metadata in listing pages.
|
|
257
|
+
*/
|
|
258
|
+
function parseSlideMetadata(source) {
|
|
259
|
+
const fm = parseFrontmatter(source);
|
|
260
|
+
const slideCount = countSlides(source);
|
|
261
|
+
return {
|
|
262
|
+
...fm,
|
|
263
|
+
slideCount
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Compile MDX slides source to JavaScript code.
|
|
268
|
+
* The returned code can be evaluated on the client with `ReslideEmbed`.
|
|
269
|
+
*
|
|
270
|
+
* Usage (server side):
|
|
271
|
+
* ```ts
|
|
272
|
+
* import { compileMdxSlides } from '@reslide/mdx/compile'
|
|
273
|
+
* const { code, metadata } = await compileMdxSlides(mdxSource)
|
|
274
|
+
* ```
|
|
275
|
+
*/
|
|
276
|
+
async function compileMdxSlides(source, options) {
|
|
277
|
+
const metadata = parseSlideMetadata(source);
|
|
278
|
+
const result = await compile(source, {
|
|
279
|
+
outputFormat: "function-body",
|
|
280
|
+
remarkPlugins: [
|
|
281
|
+
remarkDirective,
|
|
282
|
+
remarkSlides,
|
|
283
|
+
remarkClick,
|
|
284
|
+
remarkMark,
|
|
285
|
+
...options?.remarkPlugins ?? []
|
|
286
|
+
],
|
|
287
|
+
rehypePlugins: options?.rehypePlugins ?? [],
|
|
288
|
+
providerImportSource: void 0
|
|
289
|
+
});
|
|
290
|
+
return {
|
|
291
|
+
code: String(result),
|
|
292
|
+
metadata
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function parseFrontmatter(source) {
|
|
296
|
+
const match = source.match(/^---\n([\s\S]*?)\n---/);
|
|
297
|
+
if (!match) return {};
|
|
298
|
+
const result = {};
|
|
299
|
+
for (const line of match[1].split("\n")) {
|
|
300
|
+
const m = line.match(/^(\w[\w-]*):\s*(.+)$/);
|
|
301
|
+
if (m) result[m[1]] = m[2].replace(/^["']|["']$/g, "").trim();
|
|
302
|
+
}
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
function countSlides(source) {
|
|
306
|
+
const parts = source.replace(/^---\n[\s\S]*?\n---\n?/, "").split(/\n---\n/);
|
|
307
|
+
return Math.max(1, parts.filter((p) => p.trim().length > 0).length);
|
|
308
|
+
}
|
|
309
|
+
//#endregion
|
|
310
|
+
export { compileMdxSlides, parseSlideMetadata, remarkClick, remarkMark, remarkSlides };
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reslide-dev/mdx",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Remark/rehype plugins for reslide MDX preprocessing",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.mjs",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "vp pack",
|
|
16
|
+
"dev": "vp pack --watch",
|
|
17
|
+
"test": "vp test"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@mdx-js/mdx": "^3.1.1",
|
|
21
|
+
"remark-frontmatter": "^5.0.0",
|
|
22
|
+
"unist-util-visit": "^5.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/mdast": "catalog:",
|
|
26
|
+
"@types/unist": "catalog:",
|
|
27
|
+
"remark": "^15.0.0",
|
|
28
|
+
"remark-directive": "^3.0.0",
|
|
29
|
+
"remark-frontmatter": "catalog:",
|
|
30
|
+
"remark-mdx": "^3.1.0",
|
|
31
|
+
"unified": "catalog:",
|
|
32
|
+
"vite-plus": "catalog:",
|
|
33
|
+
"vitest": "catalog:"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"remark-directive": "^3.0.0"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"inlinedDependencies": {
|
|
42
|
+
"@types/mdast": "4.0.4",
|
|
43
|
+
"@types/unist": "3.0.3",
|
|
44
|
+
"trough": "2.2.0",
|
|
45
|
+
"unified": "11.0.5",
|
|
46
|
+
"vfile": "6.0.3",
|
|
47
|
+
"vfile-message": "4.0.3"
|
|
48
|
+
}
|
|
49
|
+
}
|