@lalalic/markcut 1.0.0

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.
Files changed (98) hide show
  1. package/.env.example +27 -0
  2. package/.github/user-steer.md +9 -0
  3. package/.vscode/settings.json +3 -0
  4. package/AGENTS.md +271 -0
  5. package/README.md +219 -0
  6. package/SKILL.md +209 -0
  7. package/docs/dynamic-components.md +191 -0
  8. package/docs/edit-mode.md +220 -0
  9. package/docs/json-descriptive.md +539 -0
  10. package/docs/label-mode.md +110 -0
  11. package/docs/markdown-descriptive.md +751 -0
  12. package/docs/templates.md +52 -0
  13. package/package.json +64 -0
  14. package/remotion.config.ts +5 -0
  15. package/scripts/artlist-dl.mjs +190 -0
  16. package/scripts/build-pipeline.sh +19 -0
  17. package/scripts/build-player.sh +20 -0
  18. package/src/Root.tsx +55 -0
  19. package/src/config.mjs +88 -0
  20. package/src/context/EventContext.tsx +168 -0
  21. package/src/context/index.tsx +42 -0
  22. package/src/descriptive/compiler.test.ts +1135 -0
  23. package/src/descriptive/compiler.ts +1230 -0
  24. package/src/descriptive/dsl.ts +455 -0
  25. package/src/descriptive/markdown.test.ts +866 -0
  26. package/src/descriptive/markdown.ts +674 -0
  27. package/src/descriptive/resolve.test.ts +951 -0
  28. package/src/descriptive/resolve.ts +891 -0
  29. package/src/entry.tsx +163 -0
  30. package/src/index.ts +4 -0
  31. package/src/player/browser.tsx +356 -0
  32. package/src/player/bundle/player.js +60259 -0
  33. package/src/player/bundler.mjs +269 -0
  34. package/src/player/label-server.mjs +599 -0
  35. package/src/player/pipeline.mjs +11123 -0
  36. package/src/player/pipeline.ts +117 -0
  37. package/src/player/server-shared.mjs +144 -0
  38. package/src/player/server.mjs +1006 -0
  39. package/src/render/cli-tools.ts +177 -0
  40. package/src/render/cli.mjs +628 -0
  41. package/src/schema/index.ts +259 -0
  42. package/src/types/Audio.tsx +56 -0
  43. package/src/types/Component.tsx +135 -0
  44. package/src/types/Effect.tsx +88 -0
  45. package/src/types/Folder.tsx +180 -0
  46. package/src/types/FrameSyncStyle.tsx +51 -0
  47. package/src/types/Image.tsx +51 -0
  48. package/src/types/Include.tsx +394 -0
  49. package/src/types/Map.tsx +252 -0
  50. package/src/types/Rhythm.tsx +58 -0
  51. package/src/types/Scene.tsx +42 -0
  52. package/src/types/Subtitle.tsx +218 -0
  53. package/src/types/Video.tsx +70 -0
  54. package/src/types/keyframes.ts +454 -0
  55. package/src/utils/__tests__/vtt.test.ts +129 -0
  56. package/src/utils/index.ts +168 -0
  57. package/src/utils/tween.ts +118 -0
  58. package/src/vision/cli.mjs +1187 -0
  59. package/src/vision/vision_prompts.md +67 -0
  60. package/tests/dsl.test.ts +317 -0
  61. package/tests/fixtures/audio.json +25 -0
  62. package/tests/fixtures/basic.json +27 -0
  63. package/tests/fixtures/component-all.json +38 -0
  64. package/tests/fixtures/components.json +38 -0
  65. package/tests/fixtures/effects.json +64 -0
  66. package/tests/fixtures/full.json +51 -0
  67. package/tests/fixtures/map.json +23 -0
  68. package/tests/fixtures/md/all-nodes.md +28 -0
  69. package/tests/fixtures/md/basic.md +6 -0
  70. package/tests/fixtures/md/component-imports.md +20 -0
  71. package/tests/fixtures/md/edge-cases.md +33 -0
  72. package/tests/fixtures/md/effects.md +17 -0
  73. package/tests/fixtures/md/frontmatter.md +20 -0
  74. package/tests/fixtures/md/full-feature.md +58 -0
  75. package/tests/fixtures/md/imports-block.md +19 -0
  76. package/tests/fixtures/md/include-main.md +11 -0
  77. package/tests/fixtures/md/include-sub.md +25 -0
  78. package/tests/fixtures/md/jsx-code-fence.md +21 -0
  79. package/tests/fixtures/md/map.md +11 -0
  80. package/tests/fixtures/md/nested-scenes.md +25 -0
  81. package/tests/fixtures/md/rhythm.md +17 -0
  82. package/tests/fixtures/md/scenes.md +16 -0
  83. package/tests/fixtures/md/tween.md +11 -0
  84. package/tests/fixtures/md/vars-test.md +6 -0
  85. package/tests/fixtures/scenes.json +40 -0
  86. package/tests/fixtures/subtitle.json +59 -0
  87. package/tests/fixtures/subvideo.json +59 -0
  88. package/tests/fixtures/templates/courseware.md +351 -0
  89. package/tests/fixtures/tween-visual.json +28 -0
  90. package/tests/fixtures/video-series.json +54 -0
  91. package/tests/md-descriptive.test.ts +742 -0
  92. package/tests/render.test.ts +985 -0
  93. package/tests/schema.test.ts +68 -0
  94. package/tests/server.test.ts +308 -0
  95. package/tests/utils.ts +391 -0
  96. package/tests/vitest.config.ts +18 -0
  97. package/tests/vitest.integration.config.ts +16 -0
  98. package/tsconfig.json +20 -0
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Unit tests for the stream-tree schema.
3
+ *
4
+ * Fast (no rendering). Guards against silent data loss from zod's default
5
+ * `.strip()` behavior — specifically the `root.imports` component-registry
6
+ * field that the player server sets after bundling frontmatter imports.
7
+ */
8
+ import { describe, it, expect } from "vitest";
9
+ import { root, component } from "../src/schema/index";
10
+
11
+ describe("root.imports — component registry passthrough", () => {
12
+ it("preserves a string bundle URL through root.parse()", () => {
13
+ // This is what the player server sets after bundling frontmatter imports.
14
+ const parsed = root.parse({
15
+ type: "root",
16
+ imports: "http://localhost:3001/.component-cache/abc123.js",
17
+ children: [],
18
+ });
19
+ expect(parsed.imports).toBe("http://localhost:3001/.component-cache/abc123.js");
20
+ });
21
+
22
+ it("preserves an inline component map (programmatic API)", () => {
23
+ const Hello = () => null;
24
+ const parsed = root.parse({
25
+ type: "root",
26
+ imports: { Hello, BarChart: () => null },
27
+ children: [],
28
+ });
29
+ expect(parsed.imports).toBeDefined();
30
+ expect(Object.keys(parsed.imports as object).sort()).toEqual(["BarChart", "Hello"]);
31
+ });
32
+
33
+ it("defaults imports to undefined when absent", () => {
34
+ const parsed = root.parse({ type: "root", children: [] });
35
+ expect(parsed.imports).toBeUndefined();
36
+ });
37
+ });
38
+
39
+ describe("component node", () => {
40
+ it("requires a jsx usage expression", () => {
41
+ const c = component.parse({
42
+ type: "component",
43
+ jsx: "<BarChart data={[{value: 1}]} />",
44
+ actions: [{ start: 0, end: 2 }],
45
+ });
46
+ expect(c.type).toBe("component");
47
+ expect(c.jsx).toContain("BarChart");
48
+ });
49
+
50
+ it("carries data bindings for the JSX scope", () => {
51
+ const c = component.parse({
52
+ type: "component",
53
+ jsx: "<Hello name={who} />",
54
+ data: { who: "World" },
55
+ actions: [{ start: 0, end: 1 }],
56
+ });
57
+ expect(c.data).toEqual({ who: "World" });
58
+ });
59
+
60
+ it("rejects a component without jsx", () => {
61
+ expect(() =>
62
+ component.parse({
63
+ type: "component",
64
+ actions: [{ start: 0, end: 1 }],
65
+ }),
66
+ ).toThrow();
67
+ });
68
+ });
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Integration tests for the multi-variant player server.
3
+ *
4
+ * Tests:
5
+ * - Variant config parsing from CLI args (--variant flags)
6
+ * - Directory helpers (variantDir, compiledCacheFile)
7
+ * - URL path variant parsing
8
+ * - HTTP API endpoints with variant query param
9
+ * - HTML page variant injection
10
+ * - Sequential compilation (no parallel pipeline runs)
11
+ */
12
+ import { describe, expect, it, afterEach } from "vitest";
13
+ import { spawn, ChildProcess } from "node:child_process";
14
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
15
+ import { join, resolve } from "node:path";
16
+ import { tmpdir } from "node:os";
17
+ import { realpathSync } from "node:fs";
18
+ import http from "node:http";
19
+
20
+ // ─── Helpers ──────────────────────────────────────────────────────────────
21
+
22
+ const testDir = mkdtempSync(join(realpathSync(tmpdir()), "server-test-"));
23
+ let portCounter = 4000;
24
+
25
+ /** Get a unique port for each test */
26
+ function nextPort(): number {
27
+ return portCounter++;
28
+ }
29
+
30
+ /** Create a minimal compiled stream tree JSON fixture */
31
+ function makeFixture(name: string): string {
32
+ const fixture = {
33
+ root: {
34
+ type: "root",
35
+ id: "root",
36
+ width: 640,
37
+ height: 480,
38
+ fps: 30,
39
+ isSeries: true,
40
+ children: [
41
+ {
42
+ type: "folder",
43
+ id: `scene-${name}`,
44
+ name: name,
45
+ isSeries: false,
46
+ children: [
47
+ {
48
+ type: "component",
49
+ id: `comp-${name}`,
50
+ jsx: `<div>${name}</div>`,
51
+ actions: [{ start: 0, end: 3 }],
52
+ },
53
+ ],
54
+ },
55
+ ],
56
+ },
57
+ };
58
+ const path = join(testDir, `${name}.json`);
59
+ writeFileSync(path, JSON.stringify(fixture, null, 2));
60
+ return path;
61
+ }
62
+
63
+ /** Fetch a URL and return response status + parsed body */
64
+ async function fetchUrl(url: string): Promise<{ status: number; body: any; text: string }> {
65
+ return new Promise((resolve, reject) => {
66
+ http.get(url, (res) => {
67
+ let data = "";
68
+ res.on("data", (chunk) => (data += chunk));
69
+ res.on("end", () => {
70
+ try {
71
+ resolve({
72
+ status: res.statusCode!,
73
+ body: JSON.parse(data),
74
+ text: data,
75
+ });
76
+ } catch {
77
+ resolve({ status: res.statusCode!, body: null, text: data });
78
+ }
79
+ });
80
+ }).on("error", reject);
81
+ });
82
+ }
83
+
84
+ /** Spawn the server with specific --variant flags and wait for it to be ready.
85
+ * Returns a cleanup function that kills the process. */
86
+ async function startServer(
87
+ fixturePath: string,
88
+ variants: string[],
89
+ port: number,
90
+ ): Promise<() => void> {
91
+ const args = [
92
+ "src/player/server.mjs",
93
+ fixturePath,
94
+ "--port",
95
+ String(port),
96
+ ...variants.flatMap((v) => ["--variant", v]),
97
+ ];
98
+ const proc = spawn("node", args, {
99
+ cwd: resolve(__dirname, ".."),
100
+ stdio: ["ignore", "pipe", "pipe"],
101
+ });
102
+
103
+ return new Promise((resolvePromise, reject) => {
104
+ let output = "";
105
+ const timeout = setTimeout(() => {
106
+ proc.kill();
107
+ reject(new Error(`Server did not start on port ${port}. Output:\n${output}`));
108
+ }, 20000);
109
+
110
+ const onData = (d: Buffer) => {
111
+ output += d.toString();
112
+ if (output.includes("Player ready")) {
113
+ clearTimeout(timeout);
114
+ resolvePromise(() => { proc.kill(); });
115
+ }
116
+ };
117
+
118
+ proc.stdout!.on("data", onData);
119
+ proc.stderr!.on("data", onData);
120
+
121
+ proc.on("error", (err) => {
122
+ clearTimeout(timeout);
123
+ reject(err);
124
+ });
125
+ });
126
+ }
127
+
128
+ // ─── Tests ────────────────────────────────────────────────────────────────
129
+
130
+ describe("variant config parsing", () => {
131
+ it("defaults to single 'default' variant when no --variant flags", async () => {
132
+ const fixture = makeFixture("noflag");
133
+ const port = nextPort();
134
+ const stop = await startServer(fixture, [], port);
135
+ try {
136
+ const info = await fetchUrl(`http://localhost:${port}/api/video-info`);
137
+ expect(info.status).toBe(200);
138
+ expect(info.body.variants).toEqual(["default"]);
139
+ expect(info.body.variant).toBe("default");
140
+ } finally {
141
+ stop();
142
+ }
143
+ });
144
+
145
+ it("parses single --variant flag", async () => {
146
+ const fixture = makeFixture("single");
147
+ const port = nextPort();
148
+ const stop = await startServer(fixture, ["youtube"], port);
149
+ try {
150
+ const info = await fetchUrl(`http://localhost:${port}/api/video-info`);
151
+ expect(info.status).toBe(200);
152
+ expect(info.body.variants).toEqual(["youtube"]);
153
+ // /api/video-info without ?variant= defaults to "default"
154
+ expect(info.body.variant).toBe("default");
155
+ // Verify with explicit variant query
156
+ const infoYt = await fetchUrl(`http://localhost:${port}/api/video-info?variant=youtube`);
157
+ expect(infoYt.body.variant).toBe("youtube");
158
+ } finally {
159
+ stop();
160
+ }
161
+ });
162
+
163
+ it("parses multiple --variant flags", async () => {
164
+ const fixture = makeFixture("multi");
165
+ const port = nextPort();
166
+ const stop = await startServer(fixture, ["default", "zh-tiktok", "youtube"], port);
167
+ try {
168
+ const info = await fetchUrl(`http://localhost:${port}/api/video-info`);
169
+ expect(info.status).toBe(200);
170
+ expect(info.body.variants).toEqual(["default", "zh-tiktok", "youtube"]);
171
+ } finally {
172
+ stop();
173
+ }
174
+ });
175
+
176
+ it("creates dash-separated chain from compound variant label", async () => {
177
+ const fixture = makeFixture("compound");
178
+ const port = nextPort();
179
+ const stop = await startServer(fixture, ["zh-tiktok", "en-tiktok"], port);
180
+ try {
181
+ const info = await fetchUrl(`http://localhost:${port}/api/video-info`);
182
+ expect(info.status).toBe(200);
183
+ expect(info.body.variants).toEqual(["zh-tiktok", "en-tiktok"]);
184
+ } finally {
185
+ stop();
186
+ }
187
+ });
188
+ });
189
+
190
+ describe("variant HTTP endpoints", () => {
191
+ it("serves default variant at /api/video-data (no query param)", async () => {
192
+ const fixture = makeFixture("ep-noq");
193
+ const port = nextPort();
194
+ const stop = await startServer(fixture, ["default", "zh-tiktok"], port);
195
+ try {
196
+ const res = await fetchUrl(`http://localhost:${port}/api/video-data`);
197
+ expect(res.status).toBe(200);
198
+ expect(res.body.type).toBe("root");
199
+ expect(res.body.id).toBe("root");
200
+ } finally {
201
+ stop();
202
+ }
203
+ });
204
+
205
+ it("serves variant-specific data at /api/video-data?variant=", async () => {
206
+ const fixture = makeFixture("ep-vspec");
207
+ const port = nextPort();
208
+ const stop = await startServer(fixture, ["default", "zh-tiktok"], port);
209
+ try {
210
+ // Default variant
211
+ const def = await fetchUrl(`http://localhost:${port}/api/video-data?variant=default`);
212
+ expect(def.status).toBe(200);
213
+ expect(def.body.id).toBe("root");
214
+
215
+ // zh-tiktok variant
216
+ const zh = await fetchUrl(`http://localhost:${port}/api/video-data?variant=zh-tiktok`);
217
+ expect(zh.status).toBe(200);
218
+ expect(zh.body.id).toBe("root");
219
+ } finally {
220
+ stop();
221
+ }
222
+ });
223
+
224
+ it("returns 500 for unknown variant", async () => {
225
+ const fixture = makeFixture("ep-unk");
226
+ const port = nextPort();
227
+ const stop = await startServer(fixture, ["default"], port);
228
+ try {
229
+ const res = await fetchUrl(`http://localhost:${port}/api/video-data?variant=nonexistent`);
230
+ expect(res.status).toBe(500);
231
+ } finally {
232
+ stop();
233
+ }
234
+ });
235
+
236
+ it("serves variant scenes at /api/scenes", async () => {
237
+ const fixture = makeFixture("ep-scn");
238
+ const port = nextPort();
239
+ const stop = await startServer(fixture, ["default"], port);
240
+ try {
241
+ const res = await fetchUrl(`http://localhost:${port}/api/scenes`);
242
+ expect(res.status).toBe(200);
243
+ expect(Array.isArray(res.body)).toBe(true);
244
+ } finally {
245
+ stop();
246
+ }
247
+ });
248
+ });
249
+
250
+ describe("HTML page variant injection", () => {
251
+ it("injects window.VARIANT for default variant at /", async () => {
252
+ const fixture = makeFixture("html-def");
253
+ const port = nextPort();
254
+ const stop = await startServer(fixture, ["default"], port);
255
+ try {
256
+ const res = await fetchUrl(`http://localhost:${port}/`);
257
+ expect(res.status).toBe(200);
258
+ expect(res.text).toContain('window.VARIANT = "default"');
259
+ } finally {
260
+ stop();
261
+ }
262
+ });
263
+
264
+ it("injects window.VARIANT for variant path /zh-tiktok", async () => {
265
+ const fixture = makeFixture("html-zh");
266
+ const port = nextPort();
267
+ const stop = await startServer(fixture, ["default", "zh-tiktok"], port);
268
+ try {
269
+ const res = await fetchUrl(`http://localhost:${port}/zh-tiktok`);
270
+ expect(res.status).toBe(200);
271
+ expect(res.text).toContain('window.VARIANT = "zh-tiktok"');
272
+ } finally {
273
+ stop();
274
+ }
275
+ });
276
+
277
+ it("shows variant switcher bar with all variant links", async () => {
278
+ const fixture = makeFixture("html-bar");
279
+ const port = nextPort();
280
+ const stop = await startServer(fixture, ["default", "zh-tiktok", "youtube"], port);
281
+ try {
282
+ const res = await fetchUrl(`http://localhost:${port}/`);
283
+ expect(res.status).toBe(200);
284
+ expect(res.text).toContain('href="/"');
285
+ expect(res.text).toContain('href="/zh-tiktok"');
286
+ expect(res.text).toContain('href="/youtube"');
287
+ } finally {
288
+ stop();
289
+ }
290
+ });
291
+ });
292
+
293
+ describe("sequential compilation", () => {
294
+ it("compiles multiple variants and serves all via API", async () => {
295
+ const fixture = makeFixture("seq");
296
+ const port = nextPort();
297
+ const stop = await startServer(fixture, ["v1", "v2", "v3"], port);
298
+ try {
299
+ for (const v of ["v1", "v2", "v3"]) {
300
+ const res = await fetchUrl(`http://localhost:${port}/api/video-data?variant=${v}`);
301
+ expect(res.status).toBe(200);
302
+ expect(res.body.type).toBe("root");
303
+ }
304
+ } finally {
305
+ stop();
306
+ }
307
+ });
308
+ });