@lalalic/markcut 2.8.0 → 3.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/B] +2 -0
- package/README.md +29 -0
- package/package.json +1 -1
- package/skills/markcut/SKILL.md +12 -45
- package/skills/markcut/docs/components.md +89 -0
- package/skills/markcut/docs/markdown-descriptive.md +17 -2
- package/skills/markcut/docs/sound-effects.md +45 -0
- package/src/components/Markdown.tsx +138 -24
- package/src/components/Mermaid.tsx +223 -22
- package/src/config.mjs +2 -2
- package/src/context/EventContext.tsx +3 -0
- package/src/descriptive/compiler.ts +68 -29
- package/src/descriptive/markdown.ts +20 -0
- package/src/player/browser.tsx +95 -5
- package/src/player/bundle/player.js +1078 -629
- package/src/player/components/EditControls.tsx +6 -3
- package/src/player/components/EditMessagePanel.tsx +96 -0
- package/src/player/components/HeaderBar.tsx +9 -11
- package/src/player/components/index.ts +1 -0
- package/src/player/pipeline.mjs +72 -21
- package/src/player/server-shared.mjs +4 -1
- package/src/player/server.mjs +202 -42
- package/src/render/cli.mjs +1 -1
- package/src/schema/index.ts +4 -1
- package/src/types/Component.tsx +27 -1
- package/src/types/Effect.tsx +13 -6
- package/src/types/Folder.tsx +1 -1
- package/src/types/Map.tsx +51 -1
- package/src/utils/index.ts +14 -2
- package/tests/fixtures/md/animate-diagrams.md +40 -0
- package/tests/fixtures/md/electricity-grow.md +130 -0
- package/tests/tmp/vision-1785081637127-video/videos/.normalized/segments/test-clip_0to3_seg_1100to3000.mp4 +0 -0
- package/tests/tmp/vision-1785081637127-video/videos/.normalized/test-clip_0to3.mp4 +0 -0
- package/tests/tmp/vision-1785081637127-video/videos/.normalized/test-clip_audio.mp3 +0 -0
- package/tests/tmp/vision-1785081637127-video/videos/metadata.json +9 -0
- package/tests/tmp/vision-1785081637127-video/videos/test-clip.mp4 +0 -0
- package/tests/tmp/vision-1785081637127-video/videos/test-clip.vtt +5 -0
- package/tests/tmp/vision-1784830584961/images/.normalized/test-photo_384.jpg +0 -0
- package/tests/tmp/vision-1784830584961/images/metadata.json +0 -8
- package/tests/tmp/vision-1784830584961/images/test-photo.png +0 -0
- package/tmp/frontmatter-test.ts +0 -21
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { delayRender, continueRender } from "remotion";
|
|
3
2
|
import mermaid from "mermaid";
|
|
4
3
|
|
|
5
4
|
/**
|
|
@@ -10,13 +9,23 @@ import mermaid from "mermaid";
|
|
|
10
9
|
* <Mermaid source={diagram} theme="dark" />
|
|
11
10
|
*
|
|
12
11
|
* Props:
|
|
13
|
-
* source
|
|
14
|
-
* theme
|
|
15
|
-
*
|
|
12
|
+
* source — Mermaid diagram definition string
|
|
13
|
+
* theme — Mermaid theme: "default" | "dark" | "forest" | "neutral" (default: "dark")
|
|
14
|
+
* className — Optional container className
|
|
15
|
+
* style — Container inline style
|
|
16
|
+
* highlight — Node name(s) to highlight. String or array of strings.
|
|
17
|
+
* Toggles CSS class `highlight` on matching SVG elements.
|
|
18
|
+
* The diagram source should define the class, e.g.:
|
|
19
|
+
* classDef highlight fill:#ffd700,stroke:#ff6600,stroke-width:3px
|
|
20
|
+
* animateEdges — Edge(s) to animate with a flowing dash effect.
|
|
21
|
+
* true = animate all edges.
|
|
22
|
+
* string[] = animate specific edges by source->target alias,
|
|
23
|
+
* e.g. ["A->B", "D->C"]. Edge paths are found via Mermaid's
|
|
24
|
+
* SVG id pattern: {prefix}-L_{source}_{target}_{index}.
|
|
25
|
+
* Adds CSS class `edge-animated` with a stroke-dashoffset
|
|
26
|
+
* keyframe animation.
|
|
16
27
|
*
|
|
17
28
|
* The diagram is rendered asynchronously via the mermaid library.
|
|
18
|
-
* Uses delayRender/continueRender to ensure the SVG is ready before
|
|
19
|
-
* the frame is captured by Remotion.
|
|
20
29
|
*
|
|
21
30
|
* Built-in — no imports or frontmatter registration needed.
|
|
22
31
|
*/
|
|
@@ -24,20 +33,147 @@ export interface MermaidProps {
|
|
|
24
33
|
source?: string;
|
|
25
34
|
children?: string;
|
|
26
35
|
theme?: "default" | "dark" | "forest" | "neutral";
|
|
27
|
-
|
|
36
|
+
className?: string;
|
|
37
|
+
style?: React.CSSProperties;
|
|
38
|
+
highlight?: string | string[];
|
|
39
|
+
animateEdges?: boolean | string[];
|
|
28
40
|
}
|
|
29
41
|
|
|
30
42
|
let initialized = false;
|
|
31
43
|
|
|
32
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Recursively extract plain text from JsxParser children.
|
|
46
|
+
* JsxParser may wrap template literals in nested React elements.
|
|
47
|
+
*/
|
|
48
|
+
function extractText(x: unknown): string {
|
|
49
|
+
if (typeof x === "string") return x;
|
|
50
|
+
if (Array.isArray(x)) return x.map(extractText).join("");
|
|
51
|
+
if (x && typeof x === "object" && "props" in (x as any)) {
|
|
52
|
+
return extractText((x as any).props?.children);
|
|
53
|
+
}
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Find the SVG element representing a named node.
|
|
59
|
+
* Strategy: search <text> + <title> elements by textContent,
|
|
60
|
+
* walk up to the containing <g> cluster.
|
|
61
|
+
*/
|
|
62
|
+
function findNodeGroup(svg: SVGSVGElement, name: string): Element | null {
|
|
63
|
+
// Strategy 1: SVG id containing the alias (e.g. `flowchart-A-1234` for "A")
|
|
64
|
+
// Mermaid embeds node aliases in auto-generated IDs.
|
|
65
|
+
const byId = svg.querySelector(`[id*="-${CSS.escape(name)}-"]`);
|
|
66
|
+
if (byId) {
|
|
67
|
+
let el: Element | null = byId;
|
|
68
|
+
while (el && el.tagName !== "g") el = el.parentElement;
|
|
69
|
+
return el || byId;
|
|
70
|
+
}
|
|
71
|
+
// Strategy 2: <text> elements whose text starts with the name
|
|
72
|
+
for (const t of svg.querySelectorAll<SVGTextElement>("text")) {
|
|
73
|
+
const text = t.textContent?.trim() ?? "";
|
|
74
|
+
if (text.startsWith(name)) {
|
|
75
|
+
let el: Element | null = t;
|
|
76
|
+
while (el && el.tagName !== "g") el = el.parentElement;
|
|
77
|
+
return el || t;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Strategy 3: <title> elements (class diagram titles, state labels)
|
|
81
|
+
for (const t of svg.querySelectorAll("title")) {
|
|
82
|
+
if ((t.textContent?.trim() ?? "").startsWith(name) && t.parentElement) {
|
|
83
|
+
return t.parentElement;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Apply edge animation to matching paths in the rendered Mermaid SVG.
|
|
91
|
+
* Mermaid edge paths have IDs like `{prefix}-L_{source}_{target}_{index}`.
|
|
92
|
+
*
|
|
93
|
+
* @param svg - The rendered SVG element
|
|
94
|
+
* @param spec - true = animate all edges, string[] = specific edges by "A->B" pattern
|
|
95
|
+
*/
|
|
96
|
+
function applyEdgeAnimation(svg: SVGSVGElement, spec: boolean | string[] | undefined): void {
|
|
97
|
+
if (!spec) return;
|
|
98
|
+
|
|
99
|
+
const allEdges = Array.from(svg.querySelectorAll<SVGPathElement>(
|
|
100
|
+
'path[id*="-L_"]',
|
|
101
|
+
));
|
|
102
|
+
|
|
103
|
+
if (spec === true) {
|
|
104
|
+
// Animate all edges
|
|
105
|
+
for (const path of allEdges) {
|
|
106
|
+
path.classList.add("edge-animated");
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// spec is string[] — parse patterns like "A->B"
|
|
112
|
+
for (const pattern of spec) {
|
|
113
|
+
const match = pattern.match(/^(\w+)\s*->\s*(\w+)$/);
|
|
114
|
+
if (!match) continue;
|
|
115
|
+
const source = match[1]!;
|
|
116
|
+
const target = match[2]!;
|
|
117
|
+
|
|
118
|
+
// Find matching path by ID pattern: *L_{source}_{target}_*
|
|
119
|
+
const suffix = `L_${source}_${target}_`;
|
|
120
|
+
for (const path of allEdges) {
|
|
121
|
+
if (path.id.includes(suffix)) {
|
|
122
|
+
path.classList.add("edge-animated");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function Mermaid({
|
|
129
|
+
children,
|
|
130
|
+
source: sourceProp,
|
|
131
|
+
theme = "dark",
|
|
132
|
+
className,
|
|
133
|
+
style,
|
|
134
|
+
highlight,
|
|
135
|
+
animateEdges,
|
|
136
|
+
}: MermaidProps) {
|
|
33
137
|
const ref = React.useRef<HTMLDivElement>(null);
|
|
34
|
-
const
|
|
138
|
+
const renderedRef = React.useRef(false);
|
|
139
|
+
const styleRef = React.useRef<HTMLStyleElement | null>(null);
|
|
35
140
|
|
|
141
|
+
// Inject edge animation CSS once
|
|
36
142
|
React.useEffect(() => {
|
|
143
|
+
if (!styleRef.current) {
|
|
144
|
+
const el = document.createElement("style");
|
|
145
|
+
el.textContent = `
|
|
146
|
+
@keyframes mermaid-edge-flow {
|
|
147
|
+
to { stroke-dashoffset: -24; }
|
|
148
|
+
}
|
|
149
|
+
.edge-animated {
|
|
150
|
+
stroke-dasharray: 8 6 !important;
|
|
151
|
+
animation: mermaid-edge-flow 0.5s linear infinite !important;
|
|
152
|
+
}
|
|
153
|
+
`;
|
|
154
|
+
document.head.appendChild(el);
|
|
155
|
+
styleRef.current = el;
|
|
156
|
+
}
|
|
157
|
+
return () => {
|
|
158
|
+
if (styleRef.current) {
|
|
159
|
+
styleRef.current.remove();
|
|
160
|
+
styleRef.current = null;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
165
|
+
// Resolve source string: children from JsxParser may be nested React
|
|
166
|
+
// elements wrapping template literals.
|
|
167
|
+
const source = React.useMemo(
|
|
168
|
+
() => extractText(sourceProp ?? children),
|
|
169
|
+
[sourceProp, children],
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Render effect — runs once when source/theme changes
|
|
173
|
+
React.useEffect(() => {
|
|
174
|
+
let cancelled = false;
|
|
37
175
|
if (!source || !ref.current) return;
|
|
38
176
|
|
|
39
|
-
// Initialize once — mermaid.initialize is idempotent but we guard to avoid
|
|
40
|
-
// redundant config writes on re-renders.
|
|
41
177
|
if (!initialized) {
|
|
42
178
|
mermaid.initialize({
|
|
43
179
|
startOnLoad: false,
|
|
@@ -52,25 +188,90 @@ export function Mermaid({ children, source=children, theme = "dark", className}:
|
|
|
52
188
|
mermaid
|
|
53
189
|
.render(id, source)
|
|
54
190
|
.then((result) => {
|
|
55
|
-
if (ref.current)
|
|
56
|
-
|
|
191
|
+
if (cancelled || !ref.current) return;
|
|
192
|
+
ref.current.innerHTML = result.svg;
|
|
193
|
+
const svg = ref.current.querySelector<SVGSVGElement>("svg");
|
|
194
|
+
if (svg) {
|
|
195
|
+
svg.removeAttribute("width");
|
|
196
|
+
svg.removeAttribute("height");
|
|
197
|
+
svg.style.width = "100%";
|
|
198
|
+
svg.style.height = "100%";
|
|
199
|
+
|
|
200
|
+
// Apply initial highlight now that SVG exists.
|
|
201
|
+
// The highlight effect below only fires on prop changes, so the
|
|
202
|
+
// initial highlight would be missed if SVG wasn't ready yet.
|
|
203
|
+
if (highlight) {
|
|
204
|
+
const names = Array.isArray(highlight) ? highlight : [highlight];
|
|
205
|
+
for (const name of names) {
|
|
206
|
+
const node = findNodeGroup(svg, name);
|
|
207
|
+
if (node) node.classList.add("highlight");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Apply edge animation now that SVG exists.
|
|
212
|
+
applyEdgeAnimation(svg, animateEdges);
|
|
213
|
+
}
|
|
214
|
+
renderedRef.current = true;
|
|
57
215
|
})
|
|
58
216
|
.catch((err) => {
|
|
217
|
+
if (cancelled) return;
|
|
59
218
|
console.error("Mermaid error:", err);
|
|
60
|
-
// Show error inline so the user can diagnose diagram syntax
|
|
61
219
|
if (ref.current) {
|
|
62
220
|
ref.current.innerHTML = `<div style="color:#f87171;padding:1em;border:2px dashed #f87171;border-radius:8px;font-family:monospace;font-size:14px;">
|
|
63
221
|
<strong>⚠ Mermaid Error</strong><br/>${String(err).replace(/</g, "<").replace(/>/g, ">")}
|
|
64
222
|
</div>`;
|
|
65
223
|
}
|
|
66
|
-
continueRender(handle);
|
|
67
224
|
});
|
|
68
|
-
}, [source, theme, handle]);
|
|
69
225
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
226
|
+
return () => { cancelled = true; };
|
|
227
|
+
}, [source, theme]);
|
|
228
|
+
|
|
229
|
+
// Highlight effect — toggles CSS class `highlight` on matching nodes.
|
|
230
|
+
// The diagram source must define this class with desired styles, e.g.:
|
|
231
|
+
// classDef highlight fill:#ffd700,stroke:#ff6600,stroke-width:3px
|
|
232
|
+
React.useEffect(() => {
|
|
233
|
+
const svg = ref.current?.querySelector<SVGSVGElement>("svg");
|
|
234
|
+
if (!svg) return;
|
|
235
|
+
|
|
236
|
+
// Remove class from all nodes
|
|
237
|
+
svg.querySelectorAll(".highlight").forEach((el) => {
|
|
238
|
+
el.classList.remove("highlight");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Apply class to current highlight target(s)
|
|
242
|
+
const names = Array.isArray(highlight) ? highlight : (highlight ? [highlight] : []);
|
|
243
|
+
for (const name of names) {
|
|
244
|
+
const node = findNodeGroup(svg, name);
|
|
245
|
+
if (node) {
|
|
246
|
+
node.classList.add("highlight");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}, [highlight]);
|
|
250
|
+
|
|
251
|
+
// AnimateEdges effect — re-applies edge animation on prop changes.
|
|
252
|
+
// Runs in addition to the initial application in the render callback.
|
|
253
|
+
React.useEffect(() => {
|
|
254
|
+
const svg = ref.current?.querySelector<SVGSVGElement>("svg");
|
|
255
|
+
if (!svg) return;
|
|
256
|
+
|
|
257
|
+
// Remove animated class from all edges first
|
|
258
|
+
svg.querySelectorAll(".edge-animated").forEach((el) => {
|
|
259
|
+
el.classList.remove("edge-animated");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Re-apply to current spec
|
|
263
|
+
applyEdgeAnimation(svg, animateEdges);
|
|
264
|
+
}, [animateEdges]);
|
|
265
|
+
|
|
266
|
+
// Default: center the SVG in the container. User style overrides individual properties.
|
|
267
|
+
const containerStyle: React.CSSProperties = {
|
|
268
|
+
display: "flex",
|
|
269
|
+
justifyContent: "center",
|
|
270
|
+
alignItems: "center",
|
|
271
|
+
width: "100%",
|
|
272
|
+
height: "100%",
|
|
273
|
+
...style,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
return <div ref={ref} className={className} style={containerStyle} />;
|
|
76
277
|
}
|
package/src/config.mjs
CHANGED
|
@@ -85,9 +85,9 @@ export const DEFAULT_VTT_SAMPLE_INTERVAL = Number(process.env.MARKCUT_VTT_SAMPLE
|
|
|
85
85
|
|
|
86
86
|
|
|
87
87
|
/** Speech-to-text CLI. Override via --stt flag or MARKCUT_STT_CLI env var. */
|
|
88
|
-
export const DEFAULT_STT_CLI = args.cliOverrides.stt || process.env.MARKCUT_STT_CLI || 'uvx --from openai-whisper whisper "{input}" --output_format vtt --output_dir "{output}"';
|
|
88
|
+
export const DEFAULT_STT_CLI = args.cliOverrides.stt || process.env.MARKCUT_STT_CLI || 'uvx --from openai-whisper whisper "{input}" --output_format vtt --output_dir "{output}" --word_timestamps True --max_line_count 1 --max_line_width 14';
|
|
89
89
|
/** Text-to-speech CLI. Override via --tts flag or MARKCUT_TTS_CLI env var. */
|
|
90
|
-
export const DEFAULT_TTS_CLI = args.cliOverrides.tts || process.env.MARKCUT_TTS_CLI || 'uvx edge-tts --voice "
|
|
90
|
+
export const DEFAULT_TTS_CLI = args.cliOverrides.tts || process.env.MARKCUT_TTS_CLI || 'uvx edge-tts --voice "zh-CN-YunxiNeural" --text "{input}" --write-media "{output}"';
|
|
91
91
|
/** Default agent CLI. Override via --agent flag or MARKCUT_AGENT_CLI env var. */
|
|
92
92
|
export const DEFAULT_AGENT_CLI = args.cliOverrides.agent || process.env.MARKCUT_AGENT_CLI || 'npx pi -p {prompt}';
|
|
93
93
|
/** Default edit agent CLI. Override via --edit-cli flag or MARKCUT_EDIT_CLI env var. */
|
|
@@ -74,6 +74,9 @@ export function EventProvider({ children }: { children: React.ReactNode }) {
|
|
|
74
74
|
const keys = Object.keys(scope);
|
|
75
75
|
const vals = Object.values(scope);
|
|
76
76
|
try {
|
|
77
|
+
// Debug: log the exact code string
|
|
78
|
+
const codeChars = Array.from(code).map((c: string) => c.charCodeAt(0));
|
|
79
|
+
console.log(`EventContext.debug: keys=${JSON.stringify(keys)}, codeLen=${code.length}, codeChars=${JSON.stringify(codeChars)}, codeStr=${JSON.stringify(code)}`);
|
|
77
80
|
const fn = new Function(...keys, code);
|
|
78
81
|
fn(...vals);
|
|
79
82
|
console.info(`Event evaluation succeeded: "${code}"`);
|
|
@@ -147,6 +147,8 @@ export interface DescriptiveMap extends DescriptiveBaseNode {
|
|
|
147
147
|
zoom?: number;
|
|
148
148
|
center?: { lat: number; lng: number };
|
|
149
149
|
mapType?: "roadmap" | "satellite" | "hybrid" | "terrain";
|
|
150
|
+
language?: string;
|
|
151
|
+
region?: string;
|
|
150
152
|
travelMode?: "DRIVING" | "WALKING" | "BICYCLING" | "TRANSIT";
|
|
151
153
|
routeMarker?: string;
|
|
152
154
|
}
|
|
@@ -446,15 +448,21 @@ function wrapWithEffects(
|
|
|
446
448
|
const absEnd = innerStream.end ?? result.duration;
|
|
447
449
|
const duration = absEnd - absStart;
|
|
448
450
|
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
451
|
+
// For background nodes without explicit end, keep original timing
|
|
452
|
+
// (start/end undefined) so parent back-propagation fills the correct
|
|
453
|
+
// scene duration. The effect wrapper handles animation timing via
|
|
454
|
+
// durationInSeconds and the parent fills end for proper visibility span.
|
|
455
|
+
// Other nodes: reset to relative timing so the effect wrapper owns
|
|
456
|
+
// the absolute positioning in the parent timeline.
|
|
457
|
+
const isBgNoEnd = innerStream.isBackground && innerStream.end == null;
|
|
458
|
+
const resetStream = isBgNoEnd
|
|
459
|
+
? { ...innerStream }
|
|
460
|
+
: {
|
|
461
|
+
...innerStream,
|
|
462
|
+
start: 0,
|
|
463
|
+
end: duration,
|
|
464
|
+
durationInSeconds: duration,
|
|
465
|
+
} as any;
|
|
458
466
|
|
|
459
467
|
// Build nested effect wrappers from innermost → outermost.
|
|
460
468
|
// The outermost effect uses the original absolute timing;
|
|
@@ -474,14 +482,18 @@ function wrapWithEffects(
|
|
|
474
482
|
id: uid(),
|
|
475
483
|
type: "effect",
|
|
476
484
|
animation: spec.animation,
|
|
485
|
+
animationDurationSeconds: spec.duration,
|
|
477
486
|
durationInSeconds: spec.duration,
|
|
478
487
|
animationTimingFunction: spec.animationTimingFunction,
|
|
479
488
|
animationIterationCount: spec.animationIterationCount ?? 1,
|
|
480
489
|
customKeyframes: spec.customKeyframes,
|
|
481
490
|
children: [currentStream],
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
491
|
+
// For background inner nodes: propagate start/end as-is so parent
|
|
492
|
+
// back-propagation fills the correct scene duration. The effect's
|
|
493
|
+
// durationInSeconds (animation spec) controls animation timing.
|
|
494
|
+
start: isOutermost && isBgNoEnd ? innerStream.start : effStart,
|
|
495
|
+
end: isOutermost && isBgNoEnd ? undefined : effEnd,
|
|
496
|
+
visible: innerStream.visible ?? true,
|
|
485
497
|
...pickOn(node),
|
|
486
498
|
} as Effect;
|
|
487
499
|
}
|
|
@@ -490,7 +502,10 @@ function wrapWithEffects(
|
|
|
490
502
|
}
|
|
491
503
|
|
|
492
504
|
function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | DescriptiveScene | DescriptiveInclude>, ctx: CompileContext, parentKind: "series" | "parallel" | "transitionSeries"): CompileResult {
|
|
493
|
-
|
|
505
|
+
// Only set id when explicitly provided — auto-generated uids would register
|
|
506
|
+
// in EventContext and may create invalid JS identifiers (starting with digit).
|
|
507
|
+
const id = node.id;
|
|
508
|
+
const hasExplicitId = node.id != null;
|
|
494
509
|
|
|
495
510
|
// Background nodes without explicit duration/endAt: let parent fill timing later
|
|
496
511
|
const hasOwnDuration = typeof node.duration === "number" || typeof node.endAt === "number";
|
|
@@ -504,7 +519,7 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
|
|
|
504
519
|
const end = duration != null ? start! + duration : undefined;
|
|
505
520
|
|
|
506
521
|
const base = {
|
|
507
|
-
id,
|
|
522
|
+
...(id ? { id } : {}),
|
|
508
523
|
style: node.style,
|
|
509
524
|
visible: node.visible ?? true,
|
|
510
525
|
isBackground: node.isBackground,
|
|
@@ -555,11 +570,10 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
|
|
|
555
570
|
"type", "jsx", "id", "instruction", "style", "visible",
|
|
556
571
|
"isBackground", "duration", "start", "on",
|
|
557
572
|
]);
|
|
558
|
-
const bindings: Record<string,
|
|
573
|
+
const bindings: Record<string, unknown> = {};
|
|
559
574
|
for (const key of Object.keys(node)) {
|
|
560
575
|
if (!KNOWN_COMPONENT_KEYS.has(key)) {
|
|
561
|
-
|
|
562
|
-
if (typeof val === "string") bindings[key] = val;
|
|
576
|
+
bindings[key] = (node as any)[key];
|
|
563
577
|
}
|
|
564
578
|
}
|
|
565
579
|
|
|
@@ -593,6 +607,8 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
|
|
|
593
607
|
zoom: node.zoom ?? 10,
|
|
594
608
|
center: node.center,
|
|
595
609
|
mapType: node.mapType ?? "roadmap",
|
|
610
|
+
language: node.language,
|
|
611
|
+
region: node.region,
|
|
596
612
|
travelMode: node.travelMode ?? "DRIVING",
|
|
597
613
|
routeMarker: node.routeMarker ?? "🚗",
|
|
598
614
|
googleMapsApiKey: ctx.googleMapsApiKey,
|
|
@@ -686,14 +702,27 @@ function compileScene(
|
|
|
686
702
|
: aggregateDuration(compiledChildren, sceneKind, resolved.time);
|
|
687
703
|
const localDuration = Math.max(node.duration ?? 0, sceneContentDuration);
|
|
688
704
|
|
|
689
|
-
// Back-propagate parent duration to
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
705
|
+
// Back-propagate parent duration to nodes without own timing.
|
|
706
|
+
// Also recurses into effect wrappers so nested background children
|
|
707
|
+
// (wrapped by effects) get the correct scene duration.
|
|
708
|
+
function backpropagate(stream: Record<string, any>, dur: number): void {
|
|
709
|
+
if (stream.end == null) {
|
|
710
|
+
stream.end = dur;
|
|
711
|
+
if (stream.durationInSeconds == null) {
|
|
712
|
+
stream.durationInSeconds = dur;
|
|
713
|
+
}
|
|
714
|
+
if (stream.start == null) stream.start = 0;
|
|
715
|
+
}
|
|
716
|
+
// Recursively walk into effect wrapper children
|
|
717
|
+
if (stream.type === "effect" && Array.isArray(stream.children)) {
|
|
718
|
+
for (const child of stream.children) {
|
|
719
|
+
backpropagate(child, dur);
|
|
720
|
+
}
|
|
695
721
|
}
|
|
696
722
|
}
|
|
723
|
+
for (const c of compiledChildren) {
|
|
724
|
+
backpropagate(c.stream, localDuration);
|
|
725
|
+
}
|
|
697
726
|
|
|
698
727
|
const start = parentKind === "parallel" ? Math.max(0, node.start ?? 0) : 0;
|
|
699
728
|
const end = start + localDuration;
|
|
@@ -833,14 +862,24 @@ function compileContainer(node: DescriptiveContainer, ctx: CompileContext, paren
|
|
|
833
862
|
const children = compileChildren(node.children, ctx, node.type);
|
|
834
863
|
const duration = aggregateDuration(children, node.type, resolved.time);
|
|
835
864
|
|
|
836
|
-
// Back-propagate parent duration to
|
|
837
|
-
|
|
838
|
-
if (
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
865
|
+
// Back-propagate parent duration to nodes without own timing.
|
|
866
|
+
function backpropagate(stream: Record<string, any>, dur: number): void {
|
|
867
|
+
if (stream.end == null) {
|
|
868
|
+
stream.end = dur;
|
|
869
|
+
if (stream.durationInSeconds == null) {
|
|
870
|
+
stream.durationInSeconds = dur;
|
|
871
|
+
}
|
|
872
|
+
if (stream.start == null) stream.start = 0;
|
|
873
|
+
}
|
|
874
|
+
if (stream.type === "effect" && Array.isArray(stream.children)) {
|
|
875
|
+
for (const child of stream.children) {
|
|
876
|
+
backpropagate(child, dur);
|
|
877
|
+
}
|
|
842
878
|
}
|
|
843
879
|
}
|
|
880
|
+
for (const c of children) {
|
|
881
|
+
backpropagate(c.stream, duration);
|
|
882
|
+
}
|
|
844
883
|
|
|
845
884
|
const stream: Folder = {
|
|
846
885
|
id,
|
|
@@ -38,6 +38,7 @@ const TYPE_TOKENS: Record<string, string> = {
|
|
|
38
38
|
video: "video",
|
|
39
39
|
audio: "audio",
|
|
40
40
|
component: "component",
|
|
41
|
+
event: "event",
|
|
41
42
|
rhythm: "rhythm",
|
|
42
43
|
include: "include",
|
|
43
44
|
map: "map",
|
|
@@ -251,6 +252,23 @@ function parseNodeLine(content: string, lineNum?: number): DescriptiveNode {
|
|
|
251
252
|
preserveVariantAttrs(node, attrs);
|
|
252
253
|
return node;
|
|
253
254
|
}
|
|
255
|
+
case "event": {
|
|
256
|
+
// Event-only stub: compiles to a component with empty JSX (renders nothing),
|
|
257
|
+
// just fires events on other registered components via `on`.
|
|
258
|
+
const node: DescriptiveComponent = {
|
|
259
|
+
type: "component",
|
|
260
|
+
id: attrs.id as any,
|
|
261
|
+
jsx: "",
|
|
262
|
+
duration: attrs.duration as any,
|
|
263
|
+
start: attrs.start as any,
|
|
264
|
+
instruction: attrs.instruction as any,
|
|
265
|
+
style: attrs.style as any,
|
|
266
|
+
effects: attrs.effects as any,
|
|
267
|
+
on: attrs.on as any,
|
|
268
|
+
};
|
|
269
|
+
preserveVariantAttrs(node, attrs);
|
|
270
|
+
return node;
|
|
271
|
+
}
|
|
254
272
|
case "rhythm": {
|
|
255
273
|
const src = firstPositional ?? (attrs.src as string | undefined);
|
|
256
274
|
if (!src) throw new DslError("rhythm requires src", ctx);
|
|
@@ -327,6 +345,8 @@ function parseNodeLine(content: string, lineNum?: number): DescriptiveNode {
|
|
|
327
345
|
zoom: attrs.zoom as any,
|
|
328
346
|
center: attrs.center as any,
|
|
329
347
|
mapType: attrs.mapType as any,
|
|
348
|
+
language: (attrs.language as any) ?? (attrs.lang as any),
|
|
349
|
+
region: attrs.region as any,
|
|
330
350
|
instruction: attrs.instruction as any,
|
|
331
351
|
visible: attrs.visible as any,
|
|
332
352
|
isBackground: attrs.isBackground as any,
|