@markdy/mdx 0.7.15
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/LICENSE +1 -0
- package/README.md +48 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +276 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @markdy/mdx
|
|
2
|
+
|
|
3
|
+
MDX integration for MarkdyScript with Lighthouse-safe defaults:
|
|
4
|
+
|
|
5
|
+
- `remarkMarkdy` converts fenced code blocks (` ```markdy `) into a player component.
|
|
6
|
+
- `MarkdyPlayer` hydrates only when visible and lazy-loads `@markdy/renderer-dom`.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pnpm add @markdy/mdx react react-dom
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
// mdx config
|
|
18
|
+
import { remarkMarkdy } from "@markdy/mdx";
|
|
19
|
+
|
|
20
|
+
export default {
|
|
21
|
+
remarkPlugins: [[remarkMarkdy, { componentName: "MarkdyPlayer" }]],
|
|
22
|
+
};
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
// shared MDX components map
|
|
27
|
+
import { MarkdyPlayer } from "@markdy/mdx";
|
|
28
|
+
|
|
29
|
+
export const mdxComponents = {
|
|
30
|
+
MarkdyPlayer,
|
|
31
|
+
};
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then write Markdown:
|
|
35
|
+
|
|
36
|
+
````md
|
|
37
|
+
```markdy width=800 height=400 bg="#f8fafc" autoplay=false loop=false
|
|
38
|
+
scene width=800 height=400 bg=#f8fafc
|
|
39
|
+
actor label = text("Hello, MDX") at (80, 180) size 48
|
|
40
|
+
@0.2: label.fade_in(dur=0.6)
|
|
41
|
+
```
|
|
42
|
+
````
|
|
43
|
+
|
|
44
|
+
## Performance Notes
|
|
45
|
+
|
|
46
|
+
- Default transform options set `autoplay=false`, `loop=false`, `progressBar=false`.
|
|
47
|
+
- Runtime player does not import renderer code until the block enters viewport.
|
|
48
|
+
- Placeholder is SSR-safe and keeps stable layout ratio to avoid CLS.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { Root } from 'mdast';
|
|
3
|
+
|
|
4
|
+
type MarkdyPlayerProps = {
|
|
5
|
+
code: string;
|
|
6
|
+
width?: number | string;
|
|
7
|
+
height?: number | string;
|
|
8
|
+
bg?: string;
|
|
9
|
+
assets?: Record<string, string>;
|
|
10
|
+
autoplay?: boolean | string;
|
|
11
|
+
loop?: boolean | string;
|
|
12
|
+
copyright?: boolean | string;
|
|
13
|
+
progressBar?: boolean | string;
|
|
14
|
+
className?: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
};
|
|
18
|
+
declare function MarkdyPlayer({ code, width, height, bg, assets, autoplay, loop, copyright, progressBar, className, title, description, }: MarkdyPlayerProps): react.JSX.Element;
|
|
19
|
+
|
|
20
|
+
type MarkdyFenceDefaults = Partial<{
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
bg: string;
|
|
24
|
+
autoplay: boolean;
|
|
25
|
+
loop: boolean;
|
|
26
|
+
copyright: boolean;
|
|
27
|
+
progressBar: boolean;
|
|
28
|
+
title: string;
|
|
29
|
+
description: string;
|
|
30
|
+
}>;
|
|
31
|
+
type RemarkMarkdyOptions = {
|
|
32
|
+
componentName?: string;
|
|
33
|
+
defaults?: MarkdyFenceDefaults;
|
|
34
|
+
};
|
|
35
|
+
declare function remarkMarkdy(opts?: RemarkMarkdyOptions): (tree: Root) => void;
|
|
36
|
+
|
|
37
|
+
export { MarkdyPlayer, type MarkdyPlayerProps, type RemarkMarkdyOptions, remarkMarkdy };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// src/player.tsx
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
function scheduleBackgroundTask(work) {
|
|
5
|
+
if (typeof window === "undefined") {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
if ("requestIdleCallback" in window) {
|
|
9
|
+
const requestIdleCallbackFn = window.requestIdleCallback;
|
|
10
|
+
requestIdleCallbackFn(() => work(), { timeout: 1500 });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
globalThis.setTimeout(work, 0);
|
|
14
|
+
}
|
|
15
|
+
function coerceBoolean(value, fallback) {
|
|
16
|
+
if (typeof value === "boolean") return value;
|
|
17
|
+
if (typeof value === "string") {
|
|
18
|
+
if (value.toLowerCase() === "true") return true;
|
|
19
|
+
if (value.toLowerCase() === "false") return false;
|
|
20
|
+
}
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
function coerceNumber(value, fallback) {
|
|
24
|
+
if (typeof value === "number") return value;
|
|
25
|
+
if (typeof value === "string") {
|
|
26
|
+
const parsed = Number(value);
|
|
27
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
28
|
+
}
|
|
29
|
+
return fallback;
|
|
30
|
+
}
|
|
31
|
+
function MarkdyPlayer({
|
|
32
|
+
code,
|
|
33
|
+
width = 800,
|
|
34
|
+
height = 400,
|
|
35
|
+
bg = "#ffffff",
|
|
36
|
+
assets = {},
|
|
37
|
+
autoplay = false,
|
|
38
|
+
loop = false,
|
|
39
|
+
copyright = false,
|
|
40
|
+
progressBar = false,
|
|
41
|
+
className,
|
|
42
|
+
title = "Markdy animation",
|
|
43
|
+
description
|
|
44
|
+
}) {
|
|
45
|
+
const resolvedWidth = coerceNumber(width, 800);
|
|
46
|
+
const resolvedHeight = coerceNumber(height, 400);
|
|
47
|
+
const resolvedAutoplay = coerceBoolean(autoplay, false);
|
|
48
|
+
const resolvedLoop = coerceBoolean(loop, false);
|
|
49
|
+
const resolvedCopyright = coerceBoolean(copyright, false);
|
|
50
|
+
const resolvedProgressBar = coerceBoolean(progressBar, false);
|
|
51
|
+
const rootRef = useRef(null);
|
|
52
|
+
const playerRef = useRef(null);
|
|
53
|
+
const hydratedRef = useRef(false);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (typeof window === "undefined") return;
|
|
56
|
+
const root = rootRef.current;
|
|
57
|
+
if (!root || hydratedRef.current) return;
|
|
58
|
+
let disposed = false;
|
|
59
|
+
let observer = null;
|
|
60
|
+
const doHydrate = (forceAutoplay = false) => {
|
|
61
|
+
if (disposed || hydratedRef.current) return;
|
|
62
|
+
hydratedRef.current = true;
|
|
63
|
+
root.dataset.markdyInit = "hydrating";
|
|
64
|
+
scheduleBackgroundTask(() => {
|
|
65
|
+
void (async () => {
|
|
66
|
+
try {
|
|
67
|
+
const renderer = await import("@markdy/renderer-dom");
|
|
68
|
+
if (disposed) return;
|
|
69
|
+
const createPlayer = renderer.createPlayer;
|
|
70
|
+
root.innerHTML = "";
|
|
71
|
+
playerRef.current = createPlayer({
|
|
72
|
+
container: root,
|
|
73
|
+
code,
|
|
74
|
+
assets,
|
|
75
|
+
autoplay: forceAutoplay || resolvedAutoplay,
|
|
76
|
+
loop: resolvedLoop,
|
|
77
|
+
copyright: resolvedCopyright,
|
|
78
|
+
progressBar: resolvedProgressBar
|
|
79
|
+
});
|
|
80
|
+
root.dataset.markdyInit = "done";
|
|
81
|
+
root.removeAttribute("aria-busy");
|
|
82
|
+
} catch (error) {
|
|
83
|
+
hydratedRef.current = false;
|
|
84
|
+
root.dataset.markdyInit = "error";
|
|
85
|
+
root.removeAttribute("aria-busy");
|
|
86
|
+
console.error("Failed to hydrate MarkdyPlayer", error);
|
|
87
|
+
}
|
|
88
|
+
})();
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
observer = new IntersectionObserver(
|
|
92
|
+
(entries) => {
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
if (!entry.isIntersecting) continue;
|
|
95
|
+
observer?.unobserve(root);
|
|
96
|
+
doHydrate(false);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
{ threshold: 0.2 }
|
|
100
|
+
);
|
|
101
|
+
observer.observe(root);
|
|
102
|
+
root.dataset.markdyInit = "pending";
|
|
103
|
+
const onClick = (event) => {
|
|
104
|
+
const target = event.target;
|
|
105
|
+
if (!(target instanceof Element)) return;
|
|
106
|
+
if (!target.closest("[data-markdy-placeholder]")) return;
|
|
107
|
+
observer?.unobserve(root);
|
|
108
|
+
doHydrate(true);
|
|
109
|
+
};
|
|
110
|
+
root.addEventListener("click", onClick);
|
|
111
|
+
return () => {
|
|
112
|
+
disposed = true;
|
|
113
|
+
observer?.disconnect();
|
|
114
|
+
root.removeEventListener("click", onClick);
|
|
115
|
+
playerRef.current?.destroy();
|
|
116
|
+
playerRef.current = null;
|
|
117
|
+
};
|
|
118
|
+
}, [assets, code, resolvedAutoplay, resolvedCopyright, resolvedLoop, resolvedProgressBar]);
|
|
119
|
+
return /* @__PURE__ */ jsxs(
|
|
120
|
+
"div",
|
|
121
|
+
{
|
|
122
|
+
ref: rootRef,
|
|
123
|
+
className,
|
|
124
|
+
role: "img",
|
|
125
|
+
"aria-label": title,
|
|
126
|
+
"aria-busy": "true",
|
|
127
|
+
style: {
|
|
128
|
+
maxWidth: `${width}px`,
|
|
129
|
+
width: "100%",
|
|
130
|
+
aspectRatio: `${resolvedWidth}/${resolvedHeight}`,
|
|
131
|
+
overflow: "hidden"
|
|
132
|
+
},
|
|
133
|
+
children: [
|
|
134
|
+
/* @__PURE__ */ jsx(
|
|
135
|
+
"button",
|
|
136
|
+
{
|
|
137
|
+
"data-markdy-placeholder": "true",
|
|
138
|
+
type: "button",
|
|
139
|
+
"aria-label": `Play ${title}`,
|
|
140
|
+
style: {
|
|
141
|
+
width: "100%",
|
|
142
|
+
height: "100%",
|
|
143
|
+
background: bg,
|
|
144
|
+
border: "none",
|
|
145
|
+
padding: 0,
|
|
146
|
+
display: "flex",
|
|
147
|
+
alignItems: "center",
|
|
148
|
+
justifyContent: "center",
|
|
149
|
+
cursor: "pointer"
|
|
150
|
+
},
|
|
151
|
+
children: /* @__PURE__ */ jsx(
|
|
152
|
+
"span",
|
|
153
|
+
{
|
|
154
|
+
style: {
|
|
155
|
+
fontFamily: "sans-serif",
|
|
156
|
+
fontSize: "12px",
|
|
157
|
+
color: "#8a8a8a",
|
|
158
|
+
letterSpacing: "0.04em",
|
|
159
|
+
pointerEvents: "none"
|
|
160
|
+
},
|
|
161
|
+
children: "\u25B6 markdy"
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
),
|
|
166
|
+
/* @__PURE__ */ jsx("noscript", { children: /* @__PURE__ */ jsx(
|
|
167
|
+
"div",
|
|
168
|
+
{
|
|
169
|
+
style: {
|
|
170
|
+
width: "100%",
|
|
171
|
+
height: "100%",
|
|
172
|
+
background: bg,
|
|
173
|
+
display: "flex",
|
|
174
|
+
alignItems: "center",
|
|
175
|
+
justifyContent: "center"
|
|
176
|
+
},
|
|
177
|
+
children: /* @__PURE__ */ jsx(
|
|
178
|
+
"p",
|
|
179
|
+
{
|
|
180
|
+
style: {
|
|
181
|
+
fontFamily: "sans-serif",
|
|
182
|
+
fontSize: "14px",
|
|
183
|
+
color: "#666666",
|
|
184
|
+
margin: 0,
|
|
185
|
+
padding: "1rem",
|
|
186
|
+
textAlign: "center"
|
|
187
|
+
},
|
|
188
|
+
children: description ?? title
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
) })
|
|
193
|
+
]
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/remark.ts
|
|
199
|
+
import { visit } from "unist-util-visit";
|
|
200
|
+
var MARKDY_LANGS = /* @__PURE__ */ new Set(["markdy", "markdyscript"]);
|
|
201
|
+
var META_TOKEN_RE = /([A-Za-z_]\w*)=("[^"]*"|'[^']*'|[^\s]+)/g;
|
|
202
|
+
function parseMetaValue(raw) {
|
|
203
|
+
if (raw === "true") return true;
|
|
204
|
+
if (raw === "false") return false;
|
|
205
|
+
if (raw.startsWith('"') && raw.endsWith('"') || raw.startsWith("'") && raw.endsWith("'")) {
|
|
206
|
+
return raw.slice(1, -1);
|
|
207
|
+
}
|
|
208
|
+
const asNumber = Number(raw);
|
|
209
|
+
if (!Number.isNaN(asNumber)) return asNumber;
|
|
210
|
+
return raw;
|
|
211
|
+
}
|
|
212
|
+
function normalizeMetaKey(key) {
|
|
213
|
+
if (key === "progress_bar") return "progressBar";
|
|
214
|
+
return key;
|
|
215
|
+
}
|
|
216
|
+
function parseMeta(meta) {
|
|
217
|
+
if (!meta) return {};
|
|
218
|
+
const out = {};
|
|
219
|
+
for (const match of meta.matchAll(META_TOKEN_RE)) {
|
|
220
|
+
const key = normalizeMetaKey(match[1]);
|
|
221
|
+
out[key] = parseMetaValue(match[2]);
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
function toAttribute(name, value) {
|
|
226
|
+
return {
|
|
227
|
+
type: "mdxJsxAttribute",
|
|
228
|
+
name,
|
|
229
|
+
value: String(value)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function toCodeAttribute(code) {
|
|
233
|
+
return {
|
|
234
|
+
type: "mdxJsxAttribute",
|
|
235
|
+
name: "code",
|
|
236
|
+
value: code
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function remarkMarkdy(opts = {}) {
|
|
240
|
+
const componentName = opts.componentName ?? "MarkdyPlayer";
|
|
241
|
+
const defaultProps = {
|
|
242
|
+
autoplay: false,
|
|
243
|
+
loop: false,
|
|
244
|
+
progressBar: false,
|
|
245
|
+
...opts.defaults
|
|
246
|
+
};
|
|
247
|
+
return (tree) => {
|
|
248
|
+
visit(tree, "code", (node, index, parent) => {
|
|
249
|
+
if (index === void 0 || !parent) return;
|
|
250
|
+
const codeNode = node;
|
|
251
|
+
const lang = codeNode.lang?.toLowerCase() ?? "";
|
|
252
|
+
if (!MARKDY_LANGS.has(lang)) return;
|
|
253
|
+
const parsedMeta = parseMeta(codeNode.meta);
|
|
254
|
+
const mergedProps = {
|
|
255
|
+
...defaultProps,
|
|
256
|
+
...parsedMeta
|
|
257
|
+
};
|
|
258
|
+
const attributes = [toCodeAttribute(codeNode.value)];
|
|
259
|
+
for (const [key, value] of Object.entries(mergedProps)) {
|
|
260
|
+
if (value === void 0) continue;
|
|
261
|
+
attributes.push(toAttribute(key, value));
|
|
262
|
+
}
|
|
263
|
+
const replacement = {
|
|
264
|
+
type: "mdxJsxFlowElement",
|
|
265
|
+
name: componentName,
|
|
266
|
+
attributes,
|
|
267
|
+
children: []
|
|
268
|
+
};
|
|
269
|
+
parent.children[index] = replacement;
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
export {
|
|
274
|
+
MarkdyPlayer,
|
|
275
|
+
remarkMarkdy
|
|
276
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@markdy/mdx",
|
|
3
|
+
"version": "0.7.15",
|
|
4
|
+
"description": "MDX integration plugin and lightweight React player for Markdy.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"markdy",
|
|
23
|
+
"mdx",
|
|
24
|
+
"remark",
|
|
25
|
+
"react",
|
|
26
|
+
"animation"
|
|
27
|
+
],
|
|
28
|
+
"author": "Hoang Yell <hoangyell@gmail.com> (https://hoangyell.com)",
|
|
29
|
+
"homepage": "https://markdy.com",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/HoangYell/markdy-com.git",
|
|
33
|
+
"directory": "packages/mdx"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/HoangYell/markdy-com/issues"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"unist-util-visit": "^5.0.0",
|
|
43
|
+
"@markdy/renderer-dom": "0.7.15"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": ">=18.0.0",
|
|
47
|
+
"react-dom": ">=18.0.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/mdast": "^4.0.4",
|
|
51
|
+
"@types/react": "^19.2.2",
|
|
52
|
+
"@types/react-dom": "^19.2.2",
|
|
53
|
+
"react": "^19.2.0",
|
|
54
|
+
"react-dom": "^19.2.0",
|
|
55
|
+
"tsup": "^8.5.1",
|
|
56
|
+
"typescript": "^5.9.3"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsup",
|
|
60
|
+
"test": "echo \"No tests yet\"",
|
|
61
|
+
"typecheck": "tsc --noEmit",
|
|
62
|
+
"lint": "tsc --noEmit"
|
|
63
|
+
}
|
|
64
|
+
}
|