@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.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
+ }