@lalalic/markcut 2.9.0 → 3.1.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/package.json +1 -1
- package/skills/markcut/SKILL.md +7 -0
- package/skills/markcut/docs/components.md +45 -2
- package/skills/markcut/docs/map-dynamic-camera.md +244 -0
- package/skills/markcut/docs/markdown-descriptive.md +7 -2
- package/src/components/Markdown.tsx +138 -24
- package/src/components/Mermaid.tsx +223 -22
- package/src/context/EventContext.tsx +3 -0
- package/src/descriptive/compiler.ts +105 -29
- package/src/descriptive/dsl.ts +42 -5
- package/src/descriptive/markdown.ts +23 -0
- package/src/descriptive/resolve.test.ts +5 -5
- package/src/descriptive/resolve.ts +51 -12
- package/src/player/bundle/player.js +751 -143
- package/src/player/pipeline.mjs +130 -32
- package/src/player/pipeline.ts +5 -4
- package/src/player/server.mjs +22 -42
- package/src/render/cli.mjs +54 -3
- package/src/render/validate-assets.mjs +140 -0
- package/src/schema/index.ts +58 -2
- package/src/spots/cli.mjs +266 -0
- 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 +501 -127
- package/src/utils/index.ts +14 -2
- package/src/utils/tween.ts +49 -1
- package/tests/dsl.test.ts +43 -0
- package/tests/fixtures/map-dynamic.json +52 -0
- package/tests/fixtures/md/animate-diagrams.md +42 -0
- package/tests/fixtures/md/electricity-grow.md +130 -0
- package/tests/fixtures/md/map-all-views.md +28 -0
- package/tests/md-descriptive.test.ts +58 -0
- package/tests/render.test.ts +1 -0
- package/tests/schema.test.ts +58 -1
- package/tests/validate-assets.test.ts +106 -0
|
@@ -755,7 +755,7 @@ describe("resolveMediaDurations", () => {
|
|
|
755
755
|
{ type: "video", src: "nonexistent.mp4", duration: 5 },
|
|
756
756
|
],
|
|
757
757
|
};
|
|
758
|
-
const result = await resolveMediaDurations(root);
|
|
758
|
+
const result = await resolveMediaDurations(root, { baseDir: tmpDir });
|
|
759
759
|
expect(result).not.toBe(root);
|
|
760
760
|
expect(result.children[0]!.duration).toBe(5);
|
|
761
761
|
});
|
|
@@ -767,7 +767,7 @@ describe("resolveMediaDurations", () => {
|
|
|
767
767
|
{ type: "video", src: "anything.mp4", duration: 7 },
|
|
768
768
|
],
|
|
769
769
|
};
|
|
770
|
-
const result = await resolveMediaDurations(root);
|
|
770
|
+
const result = await resolveMediaDurations(root, { baseDir: tmpDir });
|
|
771
771
|
expect(result.children[0]!.duration).toBe(7);
|
|
772
772
|
});
|
|
773
773
|
|
|
@@ -778,7 +778,7 @@ describe("resolveMediaDurations", () => {
|
|
|
778
778
|
{ type: "video", src: "anything.mp4", startFrom: 2, endAt: 5 },
|
|
779
779
|
],
|
|
780
780
|
};
|
|
781
|
-
const result = await resolveMediaDurations(root);
|
|
781
|
+
const result = await resolveMediaDurations(root, { baseDir: tmpDir });
|
|
782
782
|
expect((result.children[0] as any).endAt).toBe(5);
|
|
783
783
|
expect((result.children[0] as any).duration).toBeUndefined();
|
|
784
784
|
});
|
|
@@ -791,7 +791,7 @@ describe("resolveMediaDurations", () => {
|
|
|
791
791
|
{ type: "image", src: "photo.jpg" },
|
|
792
792
|
],
|
|
793
793
|
};
|
|
794
|
-
const result = await resolveMediaDurations(root, { skip: /\.jpg$/ });
|
|
794
|
+
const result = await resolveMediaDurations(root, { baseDir: tmpDir, skip: /\.jpg$/ });
|
|
795
795
|
// .jpg skipped, .mp4 tried but file doesn't exist so duration stays undefined
|
|
796
796
|
expect(result.children[0]!.duration).toBeUndefined();
|
|
797
797
|
expect(result.children[1]!.duration).toBeUndefined();
|
|
@@ -805,7 +805,7 @@ describe("resolveMediaDurations", () => {
|
|
|
805
805
|
],
|
|
806
806
|
};
|
|
807
807
|
const originalChildren = root.children[0];
|
|
808
|
-
const result = await resolveMediaDurations(root);
|
|
808
|
+
const result = await resolveMediaDurations(root, { baseDir: tmpDir });
|
|
809
809
|
expect(result).not.toBe(root);
|
|
810
810
|
expect(root.children[0]).toBe(originalChildren);
|
|
811
811
|
});
|
|
@@ -46,8 +46,9 @@ function firstWords(text: string, n: number): string {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export interface ResolveMediaOptions {
|
|
49
|
-
/**
|
|
50
|
-
|
|
49
|
+
/** Source file's folder — every asset path is resolved/emitted relative to it.
|
|
50
|
+
* Always set by the CLI/server (dirname of the source file); no fallback. */
|
|
51
|
+
baseDir: string;
|
|
51
52
|
/** Skip nodes whose src matches this regex */
|
|
52
53
|
skip?: RegExp;
|
|
53
54
|
}
|
|
@@ -56,7 +57,7 @@ export interface ResolveMediaOptions {
|
|
|
56
57
|
* Probe actual media duration via ffprobe.
|
|
57
58
|
* Returns duration in seconds, or null if probe fails.
|
|
58
59
|
*/
|
|
59
|
-
function probeDuration(src: string, baseDir
|
|
60
|
+
function probeDuration(src: string, baseDir: string): number | null {
|
|
60
61
|
const absPath = resolveSrc(src, baseDir);
|
|
61
62
|
try {
|
|
62
63
|
const out = execSync(
|
|
@@ -70,9 +71,37 @@ function probeDuration(src: string, baseDir?: string): number | null {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
function resolveSrc(src: string, baseDir
|
|
74
|
+
function resolveSrc(src: string, baseDir: string): string {
|
|
74
75
|
if (/^(https?:|file:|\/)/.test(src)) return src;
|
|
75
|
-
return resolvePath(baseDir
|
|
76
|
+
return resolvePath(baseDir, src);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Convert every generated asset path that lives under `baseDir` (the source
|
|
81
|
+
* .md folder) into a path relative to it, so the compiled JSON references
|
|
82
|
+
* every asset from the md folder's perspective. Render serves that folder via
|
|
83
|
+
* --public-dir and the preview server serves it as the document root — no
|
|
84
|
+
* render-time normalization needed. Paths outside baseDir (or remote) stay
|
|
85
|
+
* unchanged.
|
|
86
|
+
*
|
|
87
|
+
* Applied once at the end of resolveAll — all resolvers keep emitting
|
|
88
|
+
* absolute paths internally (needed for ffprobe/whisper), and this single
|
|
89
|
+
* walk relativizes them for the final JSON.
|
|
90
|
+
*/
|
|
91
|
+
function relativizeAssetsUnder(node: any, baseDir: string): any {
|
|
92
|
+
if (!node || typeof node !== "object") return node;
|
|
93
|
+
if (Array.isArray(node)) return node.map((v) => relativizeAssetsUnder(v, baseDir));
|
|
94
|
+
const out: Record<string, any> = {};
|
|
95
|
+
for (const [k, v] of Object.entries(node)) {
|
|
96
|
+
if (typeof v === "string" && v.startsWith(baseDir + "/")) {
|
|
97
|
+
out[k] = v.slice(baseDir.length + 1);
|
|
98
|
+
} else if (v && typeof v === "object") {
|
|
99
|
+
out[k] = relativizeAssetsUnder(v, baseDir);
|
|
100
|
+
} else {
|
|
101
|
+
out[k] = v;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
76
105
|
}
|
|
77
106
|
|
|
78
107
|
/**
|
|
@@ -111,7 +140,7 @@ export function resolveMediaSrc(
|
|
|
111
140
|
src: string,
|
|
112
141
|
targetWidth: number,
|
|
113
142
|
targetHeight: number,
|
|
114
|
-
baseDir
|
|
143
|
+
baseDir: string,
|
|
115
144
|
): string {
|
|
116
145
|
// If no pattern, direct resolve
|
|
117
146
|
if (!src.includes("${")) {
|
|
@@ -157,7 +186,7 @@ export function resolveMediaSrc(
|
|
|
157
186
|
*/
|
|
158
187
|
export async function resolveMediaSrcs(
|
|
159
188
|
root: DescriptiveRoot,
|
|
160
|
-
options: ResolveMediaOptions
|
|
189
|
+
options: ResolveMediaOptions,
|
|
161
190
|
): Promise<DescriptiveRoot> {
|
|
162
191
|
const clone: DescriptiveRoot = JSON.parse(JSON.stringify(root));
|
|
163
192
|
const baseDir = options.baseDir;
|
|
@@ -190,7 +219,7 @@ export async function resolveMediaSrcs(
|
|
|
190
219
|
*/
|
|
191
220
|
export async function resolveMediaDurations(
|
|
192
221
|
root: DescriptiveRoot,
|
|
193
|
-
options: ResolveMediaOptions
|
|
222
|
+
options: ResolveMediaOptions,
|
|
194
223
|
): Promise<DescriptiveRoot> {
|
|
195
224
|
const clone: DescriptiveRoot = JSON.parse(JSON.stringify(root));
|
|
196
225
|
const baseDir = options.baseDir;
|
|
@@ -411,7 +440,8 @@ export async function resolveScripts(
|
|
|
411
440
|
}
|
|
412
441
|
if (!generated) continue;
|
|
413
442
|
|
|
414
|
-
// Set resolved src (
|
|
443
|
+
// Set resolved src (absolute for reliable probing; the final
|
|
444
|
+
// relativizeAssetsUnder pass in resolveAll makes it md-folder-relative)
|
|
415
445
|
node.src = resolvePath(generated);
|
|
416
446
|
delete node.script;
|
|
417
447
|
}
|
|
@@ -485,6 +515,8 @@ export async function resolveSubtitles(
|
|
|
485
515
|
const effectiveOffset = nodeStart + actionStart;
|
|
486
516
|
|
|
487
517
|
if (node.type === "audio" && node.src) {
|
|
518
|
+
// node.src is still absolute here (relativization happens at the end of
|
|
519
|
+
// resolveAll), so whisper/existsSync can read the file directly.
|
|
488
520
|
clips.push({ audioSrc: node.src, offset: effectiveOffset, speaker: node.speaker });
|
|
489
521
|
}
|
|
490
522
|
|
|
@@ -937,10 +969,10 @@ export interface ResolveAllOptions extends ResolveMediaOptions {
|
|
|
937
969
|
*/
|
|
938
970
|
export async function resolveIncludes(
|
|
939
971
|
root: DescriptiveRoot,
|
|
940
|
-
options: ResolveAllOptions
|
|
972
|
+
options: ResolveAllOptions,
|
|
941
973
|
): Promise<DescriptiveRoot> {
|
|
942
974
|
const clone: DescriptiveRoot = JSON.parse(JSON.stringify(root));
|
|
943
|
-
const baseDir = options.baseDir
|
|
975
|
+
const baseDir = options.baseDir;
|
|
944
976
|
const outputDir = options.includeOutputDir ?? join(baseDir, ".markcut", "generated", "includes");
|
|
945
977
|
mkdirSync(outputDir, { recursive: true });
|
|
946
978
|
|
|
@@ -1076,7 +1108,7 @@ export async function resolveIncludes(
|
|
|
1076
1108
|
*/
|
|
1077
1109
|
export async function resolveAll(
|
|
1078
1110
|
root: DescriptiveRoot,
|
|
1079
|
-
options: ResolveAllOptions
|
|
1111
|
+
options: ResolveAllOptions,
|
|
1080
1112
|
): Promise<DescriptiveRoot> {
|
|
1081
1113
|
let result = root;
|
|
1082
1114
|
|
|
@@ -1180,5 +1212,12 @@ export async function resolveAll(
|
|
|
1180
1212
|
});
|
|
1181
1213
|
}
|
|
1182
1214
|
|
|
1215
|
+
// Final step: emit every generated asset path relative to the source .md
|
|
1216
|
+
// folder so the compiled JSON carries md-folder-relative paths. Render
|
|
1217
|
+
// serves that folder via --public-dir and the preview server serves it as
|
|
1218
|
+
// the document root — no render-time normalization needed. baseDir is
|
|
1219
|
+
// always the source file's folder (set by the CLI/server) — no fallback.
|
|
1220
|
+
result = relativizeAssetsUnder(result, options.baseDir);
|
|
1221
|
+
|
|
1183
1222
|
return result;
|
|
1184
1223
|
}
|