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