@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.
- package/.env.example +27 -0
- package/.github/user-steer.md +9 -0
- package/.vscode/settings.json +3 -0
- package/AGENTS.md +271 -0
- package/README.md +219 -0
- package/SKILL.md +209 -0
- package/docs/dynamic-components.md +191 -0
- package/docs/edit-mode.md +220 -0
- package/docs/json-descriptive.md +539 -0
- package/docs/label-mode.md +110 -0
- package/docs/markdown-descriptive.md +751 -0
- package/docs/templates.md +52 -0
- package/package.json +64 -0
- package/remotion.config.ts +5 -0
- package/scripts/artlist-dl.mjs +190 -0
- package/scripts/build-pipeline.sh +19 -0
- package/scripts/build-player.sh +20 -0
- package/src/Root.tsx +55 -0
- package/src/config.mjs +88 -0
- package/src/context/EventContext.tsx +168 -0
- package/src/context/index.tsx +42 -0
- package/src/descriptive/compiler.test.ts +1135 -0
- package/src/descriptive/compiler.ts +1230 -0
- package/src/descriptive/dsl.ts +455 -0
- package/src/descriptive/markdown.test.ts +866 -0
- package/src/descriptive/markdown.ts +674 -0
- package/src/descriptive/resolve.test.ts +951 -0
- package/src/descriptive/resolve.ts +891 -0
- package/src/entry.tsx +163 -0
- package/src/index.ts +4 -0
- package/src/player/browser.tsx +356 -0
- package/src/player/bundle/player.js +60259 -0
- package/src/player/bundler.mjs +269 -0
- package/src/player/label-server.mjs +599 -0
- package/src/player/pipeline.mjs +11123 -0
- package/src/player/pipeline.ts +117 -0
- package/src/player/server-shared.mjs +144 -0
- package/src/player/server.mjs +1006 -0
- package/src/render/cli-tools.ts +177 -0
- package/src/render/cli.mjs +628 -0
- package/src/schema/index.ts +259 -0
- package/src/types/Audio.tsx +56 -0
- package/src/types/Component.tsx +135 -0
- package/src/types/Effect.tsx +88 -0
- package/src/types/Folder.tsx +180 -0
- package/src/types/FrameSyncStyle.tsx +51 -0
- package/src/types/Image.tsx +51 -0
- package/src/types/Include.tsx +394 -0
- package/src/types/Map.tsx +252 -0
- package/src/types/Rhythm.tsx +58 -0
- package/src/types/Scene.tsx +42 -0
- package/src/types/Subtitle.tsx +218 -0
- package/src/types/Video.tsx +70 -0
- package/src/types/keyframes.ts +454 -0
- package/src/utils/__tests__/vtt.test.ts +129 -0
- package/src/utils/index.ts +168 -0
- package/src/utils/tween.ts +118 -0
- package/src/vision/cli.mjs +1187 -0
- package/src/vision/vision_prompts.md +67 -0
- package/tests/dsl.test.ts +317 -0
- package/tests/fixtures/audio.json +25 -0
- package/tests/fixtures/basic.json +27 -0
- package/tests/fixtures/component-all.json +38 -0
- package/tests/fixtures/components.json +38 -0
- package/tests/fixtures/effects.json +64 -0
- package/tests/fixtures/full.json +51 -0
- package/tests/fixtures/map.json +23 -0
- package/tests/fixtures/md/all-nodes.md +28 -0
- package/tests/fixtures/md/basic.md +6 -0
- package/tests/fixtures/md/component-imports.md +20 -0
- package/tests/fixtures/md/edge-cases.md +33 -0
- package/tests/fixtures/md/effects.md +17 -0
- package/tests/fixtures/md/frontmatter.md +20 -0
- package/tests/fixtures/md/full-feature.md +58 -0
- package/tests/fixtures/md/imports-block.md +19 -0
- package/tests/fixtures/md/include-main.md +11 -0
- package/tests/fixtures/md/include-sub.md +25 -0
- package/tests/fixtures/md/jsx-code-fence.md +21 -0
- package/tests/fixtures/md/map.md +11 -0
- package/tests/fixtures/md/nested-scenes.md +25 -0
- package/tests/fixtures/md/rhythm.md +17 -0
- package/tests/fixtures/md/scenes.md +16 -0
- package/tests/fixtures/md/tween.md +11 -0
- package/tests/fixtures/md/vars-test.md +6 -0
- package/tests/fixtures/scenes.json +40 -0
- package/tests/fixtures/subtitle.json +59 -0
- package/tests/fixtures/subvideo.json +59 -0
- package/tests/fixtures/templates/courseware.md +351 -0
- package/tests/fixtures/tween-visual.json +28 -0
- package/tests/fixtures/video-series.json +54 -0
- package/tests/md-descriptive.test.ts +742 -0
- package/tests/render.test.ts +985 -0
- package/tests/schema.test.ts +68 -0
- package/tests/server.test.ts +308 -0
- package/tests/utils.ts +391 -0
- package/tests/vitest.config.ts +18 -0
- package/tests/vitest.integration.config.ts +16 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { parseMarkdownDescriptive } from "./markdown";
|
|
3
|
+
import { compileDescriptiveRoot } from "./compiler";
|
|
4
|
+
|
|
5
|
+
describe("parseMarkdownDescriptive", () => {
|
|
6
|
+
it("supports nested scenes by heading depth", () => {
|
|
7
|
+
const doc = `# video\nlayout:series\n## Parent\n- image src:p.jpg duration:2\n### Child\n- image src:c.jpg duration:1`;
|
|
8
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
9
|
+
|
|
10
|
+
const parent = parsed.children[0]! as any;
|
|
11
|
+
expect(parent.type).toBe("scene");
|
|
12
|
+
expect(parent.children).toHaveLength(2);
|
|
13
|
+
|
|
14
|
+
const child = parent.children[1]!;
|
|
15
|
+
expect(child.type).toBe("scene");
|
|
16
|
+
expect(child.children[0]!.type).toBe("image");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("reads scene metadata from lines below heading", () => {
|
|
20
|
+
const doc = `# video\nlayout:series\n## Hook\nlayout:parallel instruction:"Fast opener"\n- image src:cover.jpg duration:2`;
|
|
21
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
22
|
+
|
|
23
|
+
const scene = parsed.children[0]! as any;
|
|
24
|
+
expect(scene.type).toBe("scene");
|
|
25
|
+
expect(scene.name).toBe("Hook");
|
|
26
|
+
expect(scene.layout).toBe("parallel");
|
|
27
|
+
expect(scene.instruction).toBe("Fast opener");
|
|
28
|
+
expect(scene.children[0]!!.type).toBe("image");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
it("accepts full-word layout values", () => {
|
|
34
|
+
const doc = `# video\nlayout:series\n## Intro\nlayout:parallel\n- parallel\n - image src:p.jpg duration:2\n## Journey\nlayout:transitionSeries transition:fade transitionTime:0.4\n- image src:a.jpg duration:2\n- image src:b.jpg duration:2`;
|
|
35
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
36
|
+
|
|
37
|
+
expect(parsed.layout).toBe("series");
|
|
38
|
+
|
|
39
|
+
const intro = parsed.children[0]! as any;
|
|
40
|
+
expect(intro.layout).toBe("parallel");
|
|
41
|
+
expect(intro.children[0]!.type).toBe("parallel");
|
|
42
|
+
|
|
43
|
+
const journey = parsed.children[1]! as any;
|
|
44
|
+
expect(journey.layout).toBe("transitionSeries");
|
|
45
|
+
expect(journey.transition).toBe("fade");
|
|
46
|
+
expect(journey.transitionTime).toBe(0.4);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("accepts merged transition syntax: transition:fade(0.5)", () => {
|
|
50
|
+
const doc = `# video\nlayout:series\n## Scene\nlayout:transitionSeries transition:fade(0.5)\n- image src:a.jpg duration:2\n- image src:b.jpg duration:2`;
|
|
51
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
52
|
+
|
|
53
|
+
const scene = parsed.children[0]! as any;
|
|
54
|
+
expect(scene.layout).toBe("transitionSeries");
|
|
55
|
+
expect(scene.transition).toBe("fade");
|
|
56
|
+
expect(scene.transitionTime).toBe(0.5);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("accepts merged transition syntax: transition:fade(0.3) with explicit transitionTime override", () => {
|
|
60
|
+
const doc = `# video\nlayout:series\n## Scene\nlayout:transitionSeries transition:fade(0.3) transitionTime:0.8\n- image src:a.jpg duration:2\n- image src:b.jpg duration:2`;
|
|
61
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
62
|
+
|
|
63
|
+
const scene = parsed.children[0]! as any;
|
|
64
|
+
expect(scene.layout).toBe("transitionSeries");
|
|
65
|
+
expect(scene.transition).toBe("fade");
|
|
66
|
+
// Separate transitionTime overrides the inline time
|
|
67
|
+
expect(scene.transitionTime).toBe(0.8);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("accepts full-word type tokens", () => {
|
|
71
|
+
const doc = `# video\nlayout:series\n## Intro\nlayout:parallel\n- image src:cover.jpg duration:2\n- video src:clip.mp4 startFrom:1 endAt:3\n- audio src:bgm.mp3 duration:3 volume:0.5\n- component duration:2 jsx:"<AnimatedHeadline />"\n- image src:card.jpg duration:1 effects:[fadeIn]\n- map duration:2 waypoints:[37.77,-122.41,"SF";34.05,-118.24,"LA"]`;
|
|
72
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
73
|
+
|
|
74
|
+
const scene = parsed.children[0]! as any;
|
|
75
|
+
expect(scene.children[0]!!.type).toBe("image");
|
|
76
|
+
expect(scene.children[1]!!.type).toBe("video");
|
|
77
|
+
expect(scene.children[2]!!.type).toBe("audio");
|
|
78
|
+
expect(scene.children[3]!!.type).toBe("component");
|
|
79
|
+
expect(scene.children[4]!!.type).toBe("image");
|
|
80
|
+
expect(scene.children[4]!!.effects).toEqual(["fadeIn"]);
|
|
81
|
+
expect(scene.children[5]!!.type).toBe("map");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("accepts full-word keys and normalizes enum aliases in keyed values", () => {
|
|
85
|
+
const doc = `# video\nlayout:series width:1080 height:1920 fps:30\n## Intro\nlayout:parallel instruction:"desc"\n- image src:cover.jpg duration:2`;
|
|
86
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
87
|
+
|
|
88
|
+
expect(parsed.layout).toBe("series");
|
|
89
|
+
expect(parsed.width).toBe(1080);
|
|
90
|
+
|
|
91
|
+
const intro = parsed.children[0]! as any;
|
|
92
|
+
expect(intro.layout).toBe("parallel");
|
|
93
|
+
expect(intro.instruction).toBe("desc");
|
|
94
|
+
|
|
95
|
+
const leaf = intro.children[0]!;
|
|
96
|
+
expect(leaf.type).toBe("image");
|
|
97
|
+
expect(leaf.src).toBe("cover.jpg");
|
|
98
|
+
expect(leaf.duration).toBe(2);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("parses root metadata: width, height, fps, layout", () => {
|
|
102
|
+
const doc = `# video
|
|
103
|
+
width:1920 height:1080 fps:30 layout:series
|
|
104
|
+
## Scene
|
|
105
|
+
- image src:cover.jpg duration:2`;
|
|
106
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
107
|
+
|
|
108
|
+
expect(parsed.width).toBe(1920);
|
|
109
|
+
expect(parsed.height).toBe(1080);
|
|
110
|
+
expect(parsed.fps).toBe(30);
|
|
111
|
+
expect(parsed.layout).toBe("series");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("parses root instruction and metadata fields", () => {
|
|
115
|
+
const doc = `# video
|
|
116
|
+
instruction:"A cool travel video" metadata:"summer 2026"
|
|
117
|
+
## Scene
|
|
118
|
+
- image src:a.jpg duration:2`;
|
|
119
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
120
|
+
|
|
121
|
+
expect(parsed.instruction).toBe("A cool travel video");
|
|
122
|
+
expect(parsed.metadata).toBe("summer 2026");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("parses key:value syntax correctly", () => {
|
|
126
|
+
const doc = `# video
|
|
127
|
+
width:1920 height:1080 fps:30 layout:series
|
|
128
|
+
## Scene
|
|
129
|
+
- image src:a.jpg duration:2`;
|
|
130
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
131
|
+
|
|
132
|
+
expect(parsed.width).toBe(1920);
|
|
133
|
+
expect(parsed.height).toBe(1080);
|
|
134
|
+
expect(parsed.fps).toBe(30);
|
|
135
|
+
expect(parsed.layout).toBe("series");
|
|
136
|
+
|
|
137
|
+
const scene = parsed.children[0]! as any;
|
|
138
|
+
expect(scene.children[0]!!.type).toBe("image");
|
|
139
|
+
expect(scene.children[0]!!.src).toBe("a.jpg");
|
|
140
|
+
expect(scene.children[0]!!.duration).toBe(2);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("throws on unrecognized token", () => {
|
|
144
|
+
const doc = `# video
|
|
145
|
+
## Scene
|
|
146
|
+
- image src:a.jpg duration:2 zoom`;
|
|
147
|
+
expect(() => parseMarkdownDescriptive(doc)).toThrow();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("parses component nodes with props", () => {
|
|
151
|
+
const doc = `# video
|
|
152
|
+
layout:series
|
|
153
|
+
## Demo
|
|
154
|
+
- component duration:3 jsx:"<AnimatedHeadline text='Hello' gradient />"`;
|
|
155
|
+
const parsed = parseMarkdownDescriptive(doc, );
|
|
156
|
+
|
|
157
|
+
const scene = parsed.children[0]! as any;
|
|
158
|
+
const c = scene.children[0]!!;
|
|
159
|
+
expect(c.type).toBe("component");
|
|
160
|
+
expect(c.duration).toBe(3);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("parses effects:[...] on leaf nodes", () => {
|
|
164
|
+
const doc = `# video
|
|
165
|
+
## Scene
|
|
166
|
+
- image src:card.jpg duration:1 effects:[fadeIn]`;
|
|
167
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
168
|
+
|
|
169
|
+
const scene = parsed.children[0]! as any;
|
|
170
|
+
const img = scene.children[0]!!;
|
|
171
|
+
expect(img.type).toBe("image");
|
|
172
|
+
expect(img.effects).toEqual(["fadeIn"]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("parses effects:[fadeIn] on leaf nodes", () => {
|
|
176
|
+
const doc = `# video
|
|
177
|
+
## Scene
|
|
178
|
+
- image src:card.jpg duration:2 effects:[fadeIn]`;
|
|
179
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
180
|
+
|
|
181
|
+
const scene = parsed.children[0]! as any;
|
|
182
|
+
const img = scene.children[0]!! as any;
|
|
183
|
+
expect(img.type).toBe("image");
|
|
184
|
+
expect(img.effects).toEqual(["fadeIn"]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("parses multiple effects and parameterized syntax", () => {
|
|
188
|
+
const doc = `# video
|
|
189
|
+
## Scene
|
|
190
|
+
- image src:card.jpg duration:3 effects:[fadeIn, bounceIn(1, ease-out, 2)]`;
|
|
191
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
192
|
+
|
|
193
|
+
const scene = parsed.children[0]! as any;
|
|
194
|
+
const img = scene.children[0]!! as any;
|
|
195
|
+
expect(img.effects).toHaveLength(2);
|
|
196
|
+
expect(img.effects![0]).toBe("fadeIn");
|
|
197
|
+
expect(img.effects![1]).toMatch(/^bounceIn\(/);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("parses effects on component nodes", () => {
|
|
201
|
+
const doc = `# video
|
|
202
|
+
## Demo
|
|
203
|
+
- component duration:3 jsx:"<Headline />" effects:[fadeIn]`;
|
|
204
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
205
|
+
|
|
206
|
+
const scene = parsed.children[0]! as any;
|
|
207
|
+
const c = scene.children[0]!! as any;
|
|
208
|
+
expect(c.type).toBe("component");
|
|
209
|
+
expect(c.effects).toEqual(["fadeIn"]);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("parses effects with comma-separated positional params — all three", () => {
|
|
213
|
+
const doc = `# video
|
|
214
|
+
## Scene
|
|
215
|
+
- image src:card.jpg duration:3 effects:[fadeIn(1.5, ease-out, 2)]`;
|
|
216
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
217
|
+
|
|
218
|
+
const scene = parsed.children[0]! as any;
|
|
219
|
+
const img = scene.children[0]!! as any;
|
|
220
|
+
expect(img.effects).toHaveLength(1);
|
|
221
|
+
expect(img.effects![0]).toBe("fadeIn(1.5, ease-out, 2)");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("parses effects with comma-separated positional params — duration only", () => {
|
|
225
|
+
const doc = `# video
|
|
226
|
+
## Scene
|
|
227
|
+
- image src:card.jpg duration:4 effects:[fadeIn(2)]`;
|
|
228
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
229
|
+
|
|
230
|
+
const scene = parsed.children[0]! as any;
|
|
231
|
+
const img = scene.children[0]!! as any;
|
|
232
|
+
expect(img.effects![0]).toBe("fadeIn(2)");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("parses map node with waypoints", () => {
|
|
236
|
+
const doc = `# video
|
|
237
|
+
layout:series
|
|
238
|
+
## Route
|
|
239
|
+
- map duration:4 waypoints:[37.77,-122.41,"SF";34.05,-118.24,"LA"]`;
|
|
240
|
+
const parsed = parseMarkdownDescriptive(doc, );
|
|
241
|
+
|
|
242
|
+
const scene = parsed.children[0]! as any;
|
|
243
|
+
const m = scene.children[0]!!;
|
|
244
|
+
expect(m.type).toBe("map");
|
|
245
|
+
expect(m.duration).toBe(4);
|
|
246
|
+
expect(m.waypoints).toHaveLength(2);
|
|
247
|
+
expect(m.waypoints[0].lat).toBe(37.77);
|
|
248
|
+
expect(m.waypoints[0].label).toBe("SF");
|
|
249
|
+
expect(m.waypoints[1].label).toBe("LA");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("parses include node", () => {
|
|
253
|
+
const doc = `# video
|
|
254
|
+
layout:series
|
|
255
|
+
## Section
|
|
256
|
+
- include src:child.json duration:4`;
|
|
257
|
+
const parsed = parseMarkdownDescriptive(doc, );
|
|
258
|
+
|
|
259
|
+
const scene = parsed.children[0]! as any;
|
|
260
|
+
const inc = scene.children[0]!!;
|
|
261
|
+
expect(inc.type).toBe("include");
|
|
262
|
+
expect(inc.src).toBe("child.json");
|
|
263
|
+
expect(inc.duration).toBe(4);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("parses audio node", () => {
|
|
267
|
+
const doc = `# video
|
|
268
|
+
layout:series
|
|
269
|
+
## Scene
|
|
270
|
+
- audio src:bgm.mp3 duration:4 volume:0.5`;
|
|
271
|
+
const parsed = parseMarkdownDescriptive(doc, );
|
|
272
|
+
|
|
273
|
+
const scene = parsed.children[0]! as any;
|
|
274
|
+
const a = scene.children[0]!!;
|
|
275
|
+
expect(a.type).toBe("audio");
|
|
276
|
+
expect(a.src).toBe("bgm.mp3");
|
|
277
|
+
expect(a.duration).toBe(4);
|
|
278
|
+
expect(a.volume).toBe(0.5);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("parses rhythm node", () => {
|
|
282
|
+
const doc = `# video
|
|
283
|
+
layout:series
|
|
284
|
+
## Beat
|
|
285
|
+
- rhythm src:track.mp3 duration:5`;
|
|
286
|
+
const parsed = parseMarkdownDescriptive(doc, );
|
|
287
|
+
|
|
288
|
+
const scene = parsed.children[0]! as any;
|
|
289
|
+
const r = scene.children[0]!!;
|
|
290
|
+
expect(r.type).toBe("rhythm");
|
|
291
|
+
expect(r.src).toBe("track.mp3");
|
|
292
|
+
expect(r.duration).toBe(5);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("parses quoted strings with special characters", () => {
|
|
296
|
+
const doc = `# video
|
|
297
|
+
## Scene
|
|
298
|
+
layout:parallel
|
|
299
|
+
- image src:a.jpg duration:2`;
|
|
300
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
301
|
+
|
|
302
|
+
const scene = parsed.children[0]! as any;
|
|
303
|
+
expect(scene.children[0].type).toBe("image");
|
|
304
|
+
expect(scene.children[0].src).toBe("a.jpg");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("handles empty input gracefully", () => {
|
|
308
|
+
const parsed = parseMarkdownDescriptive("", );
|
|
309
|
+
expect(parsed.children).toEqual([]);
|
|
310
|
+
});
|
|
311
|
+
it("parses image with prompt instead of src", () => {
|
|
312
|
+
const doc = `# video
|
|
313
|
+
width:640 height:480 layout:series
|
|
314
|
+
## Scene
|
|
315
|
+
layout:parallel
|
|
316
|
+
- image prompt:"a beautiful sunset over mountains" duration:3`;
|
|
317
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
318
|
+
const img = parsed.children[0]!.children[0];
|
|
319
|
+
expect(img.type).toBe("image");
|
|
320
|
+
expect(img.prompt).toBe("a beautiful sunset over mountains");
|
|
321
|
+
expect(img.src).toBeUndefined();
|
|
322
|
+
expect(img.duration).toBe(3);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("parses video with prompt instead of src", () => {
|
|
326
|
+
const doc = `# video
|
|
327
|
+
width:640 height:480 layout:series
|
|
328
|
+
## Scene
|
|
329
|
+
layout:parallel
|
|
330
|
+
- video prompt:"waves crashing on beach" duration:5`;
|
|
331
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
332
|
+
const vid = parsed.children[0]!.children[0];
|
|
333
|
+
expect(vid.type).toBe("video");
|
|
334
|
+
expect(vid.prompt).toBe("waves crashing on beach");
|
|
335
|
+
expect(vid.src).toBeUndefined();
|
|
336
|
+
expect(vid.duration).toBe(5);
|
|
337
|
+
});
|
|
338
|
+
it("round-trips markdown → parse → compile", () => {
|
|
339
|
+
const doc = `# video
|
|
340
|
+
width:1080 height:1920 fps:30 layout:series
|
|
341
|
+
## Hook
|
|
342
|
+
layout:parallel instruction:"Visual opening"
|
|
343
|
+
- image src:cover.jpg duration:3
|
|
344
|
+
- video src:clip.mp4 startFrom:1 endAt:4
|
|
345
|
+
## End
|
|
346
|
+
layout:parallel
|
|
347
|
+
- image src:final.jpg duration:2`;
|
|
348
|
+
|
|
349
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
350
|
+
const compiled = compileDescriptiveRoot(parsed);
|
|
351
|
+
|
|
352
|
+
expect(compiled.type).toBe("root");
|
|
353
|
+
expect(compiled.width).toBe(1080);
|
|
354
|
+
expect(compiled.height).toBe(1920);
|
|
355
|
+
expect(compiled.isSeries).toBe(true);
|
|
356
|
+
expect(compiled.children).toHaveLength(2);
|
|
357
|
+
|
|
358
|
+
const hook = compiled.children[0]! as any;
|
|
359
|
+
expect(hook.type).toBe("scene");
|
|
360
|
+
expect(hook.children).toHaveLength(2);
|
|
361
|
+
|
|
362
|
+
const end = compiled.children[1]! as any;
|
|
363
|
+
expect(end.type).toBe("scene");
|
|
364
|
+
expect(end.durationInSeconds).toBe(2);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe("frontmatter", () => {
|
|
368
|
+
it("frontmatter does not affect root config", () => {
|
|
369
|
+
const doc = `---
|
|
370
|
+
width: 1080
|
|
371
|
+
height: 1920
|
|
372
|
+
fps: 30
|
|
373
|
+
---
|
|
374
|
+
# video
|
|
375
|
+
width:640 height:480 fps:24 layout:series
|
|
376
|
+
## Scene
|
|
377
|
+
- image src:a.jpg duration:1`;
|
|
378
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
379
|
+
// Frontmatter is metadata only — root config comes from key-value line
|
|
380
|
+
expect(parsed.width).toBe(640);
|
|
381
|
+
expect(parsed.height).toBe(480);
|
|
382
|
+
expect(parsed.fps).toBe(24);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("parses imports from ~~~js imports block with multiple imports", () => {
|
|
386
|
+
const doc = `---
|
|
387
|
+
width: 640
|
|
388
|
+
height: 480
|
|
389
|
+
---
|
|
390
|
+
# video
|
|
391
|
+
## Demo
|
|
392
|
+
- component duration:1 jsx:"<ComA />"
|
|
393
|
+
|
|
394
|
+
~~~js imports
|
|
395
|
+
import { ComA } from "npm:stat-counter"
|
|
396
|
+
import { ComB } from "github:foo/bar/src/Logo.tsx"
|
|
397
|
+
import { ComC } from "https://cdn.example.com/banner.js"
|
|
398
|
+
~~~`;
|
|
399
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
400
|
+
// imports removed from component schema — now at root level
|
|
401
|
+
// imports removed from component schema — now at root level
|
|
402
|
+
|
|
403
|
+
// Compiler resolves from: specs onto component nodes
|
|
404
|
+
const compiled = compileDescriptiveRoot(parsed);
|
|
405
|
+
const scene = compiled.children[0]! as any;
|
|
406
|
+
const c = scene.children[0]!!;
|
|
407
|
+
// imports removed from component schema — now at root level
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("parses JSON array imports is no longer supported — use ~~~js imports instead", () => {
|
|
411
|
+
// JSON array imports in frontmatter are no longer supported.
|
|
412
|
+
// Use ~~~js imports code blocks instead.
|
|
413
|
+
const doc = `# video
|
|
414
|
+
- component duration:1 jsx:"<ComA />"
|
|
415
|
+
|
|
416
|
+
~~~js imports
|
|
417
|
+
import { ComA } from "npm:pkg"
|
|
418
|
+
import { ComB } from "npm:other"
|
|
419
|
+
~~~`;
|
|
420
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
421
|
+
// imports removed from component schema — now at root level
|
|
422
|
+
// imports removed from component schema — now at root level
|
|
423
|
+
// imports removed from component schema — now at root level
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("parses inline component definitions from ~~~js imports block", () => {
|
|
427
|
+
const doc = `# video
|
|
428
|
+
## Demo
|
|
429
|
+
- component duration:1 jsx:"<Badge />"
|
|
430
|
+
|
|
431
|
+
~~~js imports
|
|
432
|
+
export function Badge({ text }) {
|
|
433
|
+
return <span>{text}</span>
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
import { Card } from "npm:card"
|
|
437
|
+
~~~`;
|
|
438
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
439
|
+
// imports removed from component schema — now at root level
|
|
440
|
+
// imports removed from component schema — now at root level
|
|
441
|
+
// imports removed from component schema — now at root level
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("does not treat unrelated ``` code blocks as components", () => {
|
|
445
|
+
const doc = `# video
|
|
446
|
+
## Scene
|
|
447
|
+
- image src:a.jpg duration:1
|
|
448
|
+
|
|
449
|
+
\`\`\`bash
|
|
450
|
+
npm install foo
|
|
451
|
+
\`\`\`
|
|
452
|
+
|
|
453
|
+
\`\`\`jsx
|
|
454
|
+
export default function Unnamed() { return null; }
|
|
455
|
+
\`\`\`
|
|
456
|
+
`;
|
|
457
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
458
|
+
// bash block ignored; jsx block without name also ignored
|
|
459
|
+
// imports removed from component schema — now at root level
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it("supports inline jsx: key on a component node (usage JSX)", () => {
|
|
463
|
+
const doc = `# video
|
|
464
|
+
## Scene
|
|
465
|
+
- component duration:1 jsx:"<Foo />"`;
|
|
466
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
467
|
+
const scene = parsed.children[0]! as any;
|
|
468
|
+
const c = scene.children[0]!!;
|
|
469
|
+
expect(c.jsx).toBe("<Foo />");
|
|
470
|
+
expect(c.jsx).toContain("Foo");
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("component node can have jsx: only (no componentName)", () => {
|
|
474
|
+
const doc = `# video
|
|
475
|
+
## Scene
|
|
476
|
+
- component duration:1 jsx:"<Greeting name='World' />"
|
|
477
|
+
|
|
478
|
+
~~~js imports
|
|
479
|
+
import { Greeting } from "npm:greeting"
|
|
480
|
+
~~~`;
|
|
481
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
482
|
+
const scene = parsed.children[0] as any;
|
|
483
|
+
const c = scene.children[0];
|
|
484
|
+
expect(c.jsx).toBe("<Greeting name='World' />");
|
|
485
|
+
expect(c.jsx).toContain("Greeting");
|
|
486
|
+
|
|
487
|
+
const compiled = compileDescriptiveRoot(parsed);
|
|
488
|
+
const compiledScene = compiled.children[0]! as any;
|
|
489
|
+
const cc = compiledScene.children[0]!;
|
|
490
|
+
expect(cc.jsx).toBe("<Greeting name='World' />");
|
|
491
|
+
// imports removed from component schema — now at root level
|
|
492
|
+
// imports removed from component schema — now at root level
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("imports from ~~~js imports block resolve onto component nodes", () => {
|
|
496
|
+
const doc = `# video
|
|
497
|
+
## Scene
|
|
498
|
+
- component duration:1 jsx:"<Logo />"
|
|
499
|
+
|
|
500
|
+
~~~js imports
|
|
501
|
+
import { Logo } from "npm:logo-pkg"
|
|
502
|
+
~~~`;
|
|
503
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
504
|
+
// imports removed from component schema — now at root level
|
|
505
|
+
|
|
506
|
+
const compiled = compileDescriptiveRoot(parsed);
|
|
507
|
+
const scene = compiled.children[0]! as any;
|
|
508
|
+
// imports removed from component schema — now at root level
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// ── Import source types ──────────────────────────────────────────────
|
|
512
|
+
|
|
513
|
+
it("~~~js imports with from:npm resolves to esm.sh", () => {
|
|
514
|
+
const doc = `# video
|
|
515
|
+
## Scene
|
|
516
|
+
- component duration:1 jsx:"<CompA />"
|
|
517
|
+
|
|
518
|
+
~~~js imports
|
|
519
|
+
import { CompA } from "npm:some-pkg"
|
|
520
|
+
~~~`;
|
|
521
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
522
|
+
// imports removed from component schema — now at root level
|
|
523
|
+
const compiled = compileDescriptiveRoot(parsed);
|
|
524
|
+
// imports removed from component schema — now at root level
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it("~~~js imports with from:npm@version", () => {
|
|
528
|
+
const doc = `# video
|
|
529
|
+
## Scene
|
|
530
|
+
- component duration:1 jsx:"<Chart />"
|
|
531
|
+
- component duration:1 jsx:"<Chart />"
|
|
532
|
+
|
|
533
|
+
~~~js imports
|
|
534
|
+
import { Chart } from "npm:chart-js@4.5.0"
|
|
535
|
+
~~~`;
|
|
536
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
537
|
+
// imports removed from component schema — now at root level
|
|
538
|
+
const compiled = compileDescriptiveRoot(parsed);
|
|
539
|
+
const [byName, byJsx] = (compiled.children[0]! as any).children;
|
|
540
|
+
// imports removed from component schema — now at root level
|
|
541
|
+
// imports removed from component schema — now at root level
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("~~~js imports with from:git:user/repo@branch/path", () => {
|
|
545
|
+
const doc = `# video
|
|
546
|
+
## Scene
|
|
547
|
+
- component duration:1 jsx:"<Badge />"
|
|
548
|
+
|
|
549
|
+
~~~js imports
|
|
550
|
+
import { Badge } from "git:myorg/badge-component@master/src/Badge.tsx"
|
|
551
|
+
~~~`;
|
|
552
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
553
|
+
const compiled = compileDescriptiveRoot(parsed);
|
|
554
|
+
// imports removed from component schema — now at root level
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it("~~~js imports with from:github:user/repo", () => {
|
|
558
|
+
const doc = `# video
|
|
559
|
+
## Scene
|
|
560
|
+
- component duration:1 jsx:"<Logo />"
|
|
561
|
+
|
|
562
|
+
~~~js imports
|
|
563
|
+
import { Logo } from "github:team/logo-assets"
|
|
564
|
+
~~~`;
|
|
565
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
566
|
+
const compiled = compileDescriptiveRoot(parsed);
|
|
567
|
+
// imports removed from component schema — now at root level
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it("~~~js imports with from:https URL", () => {
|
|
571
|
+
const doc = `# video
|
|
572
|
+
## Scene
|
|
573
|
+
- component duration:1 jsx:"<Widget />"
|
|
574
|
+
|
|
575
|
+
~~~js imports
|
|
576
|
+
import { Widget } from "https://cdn.example.com/widget.mjs"
|
|
577
|
+
~~~`;
|
|
578
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
579
|
+
const compiled = compileDescriptiveRoot(parsed);
|
|
580
|
+
// imports removed from component schema — now at root level
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("~~~js imports with from:http URL", () => {
|
|
584
|
+
const doc = `# video
|
|
585
|
+
## Scene
|
|
586
|
+
- component duration:1 jsx:"<DevUI />"
|
|
587
|
+
|
|
588
|
+
~~~js imports
|
|
589
|
+
import { DevUI } from "http://localhost:5173/src/components/DevPanel.tsx"
|
|
590
|
+
~~~`;
|
|
591
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
592
|
+
const compiled = compileDescriptiveRoot(parsed);
|
|
593
|
+
// imports removed from component schema — now at root level
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it("~~~js imports with from:local relative path", () => {
|
|
597
|
+
const doc = `# video
|
|
598
|
+
## Scene
|
|
599
|
+
- component duration:1 jsx:"<LocalComp />"
|
|
600
|
+
|
|
601
|
+
~~~js imports
|
|
602
|
+
import { LocalComp } from "./components/MyWidget.tsx"
|
|
603
|
+
~~~`;
|
|
604
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
605
|
+
const compiled = compileDescriptiveRoot(parsed);
|
|
606
|
+
// imports removed from component schema — now at root level
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it("~~~js imports with from:absolute path", () => {
|
|
610
|
+
const doc = `# video
|
|
611
|
+
## Scene
|
|
612
|
+
- component duration:1 jsx:"<Helper />"
|
|
613
|
+
|
|
614
|
+
~~~js imports
|
|
615
|
+
import { Helper } from "/Users/me/lib/helper.tsx"
|
|
616
|
+
~~~`;
|
|
617
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
618
|
+
const compiled = compileDescriptiveRoot(parsed);
|
|
619
|
+
// imports removed from component schema — now at root level
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// ── Import entry variants ────────────────────────────────────────────
|
|
623
|
+
|
|
624
|
+
it("~~~js imports with from: + exports:", () => {
|
|
625
|
+
// exports is not directly represented in import statements;
|
|
626
|
+
// the default export is used. This is a compiler concern.
|
|
627
|
+
const doc = `# video
|
|
628
|
+
## Scene
|
|
629
|
+
- component duration:1 jsx:"<Counter />"
|
|
630
|
+
|
|
631
|
+
~~~js imports
|
|
632
|
+
import { Counter } from "npm:stat-counter"
|
|
633
|
+
~~~`;
|
|
634
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
635
|
+
// imports removed from component schema — now at root level
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it("~~~js imports with inline function definition", () => {
|
|
639
|
+
const doc = `# video
|
|
640
|
+
## Scene
|
|
641
|
+
- component duration:1 jsx:"<Greeting />"
|
|
642
|
+
|
|
643
|
+
~~~js imports
|
|
644
|
+
export function Greeting({ name }) {
|
|
645
|
+
return <h1>{name}</h1>
|
|
646
|
+
}
|
|
647
|
+
~~~`;
|
|
648
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
649
|
+
// imports removed from component schema — now at root level
|
|
650
|
+
// imports removed from component schema — now at root level
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// ── Component node usage modes ────────────────────────────────────────
|
|
654
|
+
|
|
655
|
+
it("component node resolved from ~~~js imports", () => {
|
|
656
|
+
const doc = `# video
|
|
657
|
+
## Scene
|
|
658
|
+
- component duration:2 jsx:"<StatBox value={10} label='Score' />"
|
|
659
|
+
|
|
660
|
+
~~~js imports
|
|
661
|
+
import { StatBox } from "npm:stat-box"
|
|
662
|
+
~~~`;
|
|
663
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
664
|
+
const compiled = compileDescriptiveRoot(parsed);
|
|
665
|
+
const c = (compiled.children[0]! as any).children[0]!;
|
|
666
|
+
// imports removed from component schema — now at root level
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it("component node with jsx only (no componentName)", () => {
|
|
670
|
+
const doc = `# video
|
|
671
|
+
## Scene
|
|
672
|
+
- component duration:2 jsx:"<Greeting name='World' />"`;
|
|
673
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
674
|
+
const c = (parsed.children[0]! as any).children[0]!;
|
|
675
|
+
expect(c.jsx).toBe("<Greeting name='World' />");
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it("component node with jsx referencing imported components", () => {
|
|
679
|
+
const doc = `# video
|
|
680
|
+
## Scene
|
|
681
|
+
- component duration:2 jsx:"<Header title='Page' /><Footer />"
|
|
682
|
+
|
|
683
|
+
~~~js imports
|
|
684
|
+
import { Header } from "npm:header-lib"
|
|
685
|
+
import { Footer } from "git:org/footer@main"
|
|
686
|
+
~~~`;
|
|
687
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
688
|
+
const compiled = compileDescriptiveRoot(parsed);
|
|
689
|
+
const c = (compiled.children[0]! as any).children[0]!;
|
|
690
|
+
expect(c.jsx).toBe("<Header title='Page' /><Footer />");
|
|
691
|
+
// imports removed from component schema — now at root level
|
|
692
|
+
// imports removed from component schema — now at root level
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it("component node with both componentName and jsx", () => {
|
|
696
|
+
const doc = `# video
|
|
697
|
+
## Scene
|
|
698
|
+
- component duration:2 jsx:"<Widget mode='dark' />"`;
|
|
699
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
700
|
+
const c = (parsed.children[0]! as any).children[0]!;
|
|
701
|
+
expect(c.jsx).toBe("<Widget mode='dark' />");
|
|
702
|
+
// props merged into jsx expression
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
// ── Multiple imports + mixed component nodes ──────────────────────────
|
|
706
|
+
|
|
707
|
+
it("multiple imports with mixed sources in one ~~~js imports block", () => {
|
|
708
|
+
const doc = `# video
|
|
709
|
+
## Scene
|
|
710
|
+
- component duration:1 jsx:"<Counter />"
|
|
711
|
+
- component duration:1 jsx:"<Logo />"
|
|
712
|
+
- component duration:1 jsx:"<Badge />"
|
|
713
|
+
- component duration:1 jsx:"<Chart />"
|
|
714
|
+
|
|
715
|
+
~~~js imports
|
|
716
|
+
import { Counter } from "npm:counter-lib"
|
|
717
|
+
import { Logo } from "github:org/design-system/src/Logo.tsx"
|
|
718
|
+
import { Chart } from "https://cdn.example.com/chart.js"
|
|
719
|
+
|
|
720
|
+
export function Badge({ label }) {
|
|
721
|
+
return <span>{label}</span>
|
|
722
|
+
}
|
|
723
|
+
~~~`;
|
|
724
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
725
|
+
// imports removed from component schema — now at root level
|
|
726
|
+
// imports removed from component schema — now at root level
|
|
727
|
+
// imports removed from component schema — now at root level
|
|
728
|
+
// imports removed from component schema — now at root level
|
|
729
|
+
|
|
730
|
+
const compiled = compileDescriptiveRoot(parsed);
|
|
731
|
+
const children = (compiled.children[0]! as any).children;
|
|
732
|
+
// imports removed from component schema — now at root level
|
|
733
|
+
// imports removed from component schema — now at root level
|
|
734
|
+
// imports removed from component schema — now at root level
|
|
735
|
+
// imports removed from component schema — now at root level
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it("jsx usage node with no frontmatter imports (host-registered fallback)", () => {
|
|
739
|
+
const doc = `# video
|
|
740
|
+
## Scene
|
|
741
|
+
- component duration:2 jsx:"<AnimatedHeadline text='Hello' />"`;
|
|
742
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
743
|
+
// No imports → jsx will reference host-registered components at runtime
|
|
744
|
+
const compiled = compileDescriptiveRoot(parsed);
|
|
745
|
+
const c = (compiled.children[0]! as any).children[0]!;
|
|
746
|
+
expect(c.jsx).toBe("<AnimatedHeadline text='Hello' />");
|
|
747
|
+
// imports removed from component schema — now at root level
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it("component node with pipe-delimited jsx body on bullet line", () => {
|
|
751
|
+
// jsx can contain spaces, commas, quotes — the pipe | syntax on the bullet
|
|
752
|
+
const doc = `# video
|
|
753
|
+
## Scene
|
|
754
|
+
- component duration:2 jsx:"<Counter value={42} suffix='%' />"`;
|
|
755
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
756
|
+
const c = (parsed.children[0]! as any).children[0]!;
|
|
757
|
+
expect(c.jsx).toBe("<Counter value={42} suffix='%' />");
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
it("empty frontmatter (just --- line pairs) is ignored", () => {
|
|
761
|
+
const doc = `---
|
|
762
|
+
---
|
|
763
|
+
# video
|
|
764
|
+
## Scene
|
|
765
|
+
- image src:a.jpg duration:1`;
|
|
766
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
767
|
+
// imports removed from component schema — now at root level
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
// ── Script / audio alias ─────────────────────────────────────────────
|
|
772
|
+
|
|
773
|
+
describe("script — audio alias", () => {
|
|
774
|
+
it("inline audio script:key on audio node", () => {
|
|
775
|
+
const doc = `# video
|
|
776
|
+
## S
|
|
777
|
+
layout:parallel
|
|
778
|
+
- audio src:bg.mp3 duration:3 volume:0.5 script:"inline narration"`;
|
|
779
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
780
|
+
const node = (parsed.children[0] as any).children[0];
|
|
781
|
+
expect(node.type).toBe("audio");
|
|
782
|
+
expect(node.src).toBe("bg.mp3");
|
|
783
|
+
expect(node.script).toBe("inline narration");
|
|
784
|
+
expect(node.volume).toBe(0.5);
|
|
785
|
+
expect(node.duration).toBe(3);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it("standalone - script bullet creates audio node with script", () => {
|
|
789
|
+
const doc = `# video
|
|
790
|
+
## S
|
|
791
|
+
layout:parallel
|
|
792
|
+
- script "standalone narration"`;
|
|
793
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
794
|
+
const node = (parsed.children[0] as any).children[0];
|
|
795
|
+
expect(node.type).toBe("audio");
|
|
796
|
+
expect(node.script).toBe("standalone narration");
|
|
797
|
+
expect(node.volume).toBe(1); // default
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
it("standalone - script bullet supports audio keys", () => {
|
|
801
|
+
const doc = `# video
|
|
802
|
+
## S
|
|
803
|
+
layout:parallel
|
|
804
|
+
- script "narration" volume:0.3 start:2 duration:5 foreground:true isBackground:true`;
|
|
805
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
806
|
+
const node = (parsed.children[0] as any).children[0];
|
|
807
|
+
expect(node.type).toBe("audio");
|
|
808
|
+
expect(node.script).toBe("narration");
|
|
809
|
+
expect(node.volume).toBe(0.3);
|
|
810
|
+
expect(node.start).toBe(2);
|
|
811
|
+
expect(node.duration).toBe(5);
|
|
812
|
+
expect(node.foreground).toBe(true);
|
|
813
|
+
expect(node.isBackground).toBe(true);
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
it("code fence ~~~script sets script on audio node", () => {
|
|
817
|
+
const doc = `# video
|
|
818
|
+
## S
|
|
819
|
+
layout:parallel
|
|
820
|
+
- audio src:bg.mp3 duration:3
|
|
821
|
+
~~~script
|
|
822
|
+
code fence content
|
|
823
|
+
~~~`;
|
|
824
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
825
|
+
const node = (parsed.children[0] as any).children[0];
|
|
826
|
+
expect(node.type).toBe("audio");
|
|
827
|
+
expect(node.src).toBe("bg.mp3");
|
|
828
|
+
expect(node.script).toBe("code fence content");
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
it("throws when standalone - script has no text", () => {
|
|
832
|
+
const doc = `# video
|
|
833
|
+
## S
|
|
834
|
+
layout:parallel
|
|
835
|
+
- script`;
|
|
836
|
+
expect(() => parseMarkdownDescriptive(doc)).toThrow("script requires text content");
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it("compiles script audio node through full pipeline", () => {
|
|
840
|
+
const doc = `# video
|
|
841
|
+
## S
|
|
842
|
+
layout:parallel
|
|
843
|
+
- audio src:bg.mp3 duration:3 script:"tts text"`;
|
|
844
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
845
|
+
const compiled = compileDescriptiveRoot(parsed);
|
|
846
|
+
const child = (compiled.children[0] as any).children[0];
|
|
847
|
+
expect(child.type).toBe("audio");
|
|
848
|
+
expect(child.src).toBe("bg.mp3");
|
|
849
|
+
// script field is not in the compiled schema — it's consumed by resolveScripts
|
|
850
|
+
expect(child.script).toBeUndefined();
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it("compiles standalone - script through full pipeline", () => {
|
|
854
|
+
const doc = `# video
|
|
855
|
+
## S
|
|
856
|
+
layout:parallel
|
|
857
|
+
- script "tts text"`;
|
|
858
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
859
|
+
const compiled = compileDescriptiveRoot(parsed);
|
|
860
|
+
const child = (compiled.children[0] as any).children[0];
|
|
861
|
+
expect(child.type).toBe("audio");
|
|
862
|
+
// No src yet — resolveScripts will set it later from script field
|
|
863
|
+
expect(child.src).toBeUndefined();
|
|
864
|
+
});
|
|
865
|
+
});
|
|
866
|
+
});
|