@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,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers, no AI, no network, no React.
|
|
3
|
+
* Pure helpers — no AI, no network, no React.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function uid(): string {
|
|
7
|
+
return Math.random().toString(36).slice(2, 10);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const KEBAB = /[^a-zA-Z0-9_-]+/g;
|
|
11
|
+
export function toClassName(s: string): string {
|
|
12
|
+
return (s || "").replace(KEBAB, "-").replace(/^-+|-+$/g, "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function toPlaybackRate(rate: number): number {
|
|
16
|
+
// remotion clamps to (0.0625, 16)
|
|
17
|
+
if (!isFinite(rate) || rate <= 0) return 1;
|
|
18
|
+
return Math.min(16, Math.max(0.0625, rate));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function cssJS(css?: string | Record<string, unknown>): Record<string, unknown> {
|
|
22
|
+
if (!css) return {};
|
|
23
|
+
if (typeof css === "object") return css;
|
|
24
|
+
const out: Record<string, string> = {};
|
|
25
|
+
for (const decl of css.split(";")) {
|
|
26
|
+
const i = decl.indexOf(":");
|
|
27
|
+
if (i < 0) continue;
|
|
28
|
+
const k = decl.slice(0, i).trim();
|
|
29
|
+
const v = decl.slice(i + 1).trim();
|
|
30
|
+
if (!k) continue;
|
|
31
|
+
const camel = k.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
32
|
+
out[camel] = v;
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Walk a stream tree depth-first. Visitor returns false to stop descending.
|
|
39
|
+
*/
|
|
40
|
+
export type StreamNode = { id?: string; type?: string; children?: StreamNode[]; [k: string]: unknown };
|
|
41
|
+
export function walkDown<T extends StreamNode>(
|
|
42
|
+
node: T,
|
|
43
|
+
visit: (n: T, parent: T | null, depth: number) => boolean | void,
|
|
44
|
+
parent: T | null = null,
|
|
45
|
+
depth = 0,
|
|
46
|
+
): void {
|
|
47
|
+
const keep = visit(node, parent, depth);
|
|
48
|
+
if (keep === false) return;
|
|
49
|
+
if (Array.isArray(node.children)) {
|
|
50
|
+
for (const c of node.children) walkDown(c as T, visit, node, depth + 1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compute duration of a stream subtree, in seconds.
|
|
56
|
+
*
|
|
57
|
+
* Compute duration of a stream subtree, in seconds.
|
|
58
|
+
*
|
|
59
|
+
* - rhythm streams use their pre-set durationInSeconds (set by host)
|
|
60
|
+
* - leaf actions use action.end as default
|
|
61
|
+
* - series sums children, subtracting transition overlaps
|
|
62
|
+
* - sequence (parallel) takes max child duration
|
|
63
|
+
* - background children do not contribute
|
|
64
|
+
* - actions[].streamRef is unsupported in lite (no template instancing)
|
|
65
|
+
*/
|
|
66
|
+
export interface DurationStream extends StreamNode {
|
|
67
|
+
type?: string;
|
|
68
|
+
isSeries?: boolean;
|
|
69
|
+
isBackground?: boolean;
|
|
70
|
+
transition?: string;
|
|
71
|
+
transitionTime?: number;
|
|
72
|
+
durationInSeconds?: number;
|
|
73
|
+
actions?: Array<{ start?: number; end?: number; startFrom?: number; endAt?: number }>;
|
|
74
|
+
children?: DurationStream[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getDurationInSeconds(stream: DurationStream, update = true): number {
|
|
78
|
+
if (!stream) return 0;
|
|
79
|
+
|
|
80
|
+
if (stream.type === "rhythm") {
|
|
81
|
+
return stream.durationInSeconds ?? 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// include: if src is set, treat as leaf (duration from action end).
|
|
85
|
+
// Otherwise fall back to inline children (legacy).
|
|
86
|
+
if (stream.type === "include") {
|
|
87
|
+
if (stream.src) {
|
|
88
|
+
// External reference — use action end (like other leaves)
|
|
89
|
+
const last = stream.actions?.[stream.actions.length - 1];
|
|
90
|
+
const d = last?.end ?? 0;
|
|
91
|
+
if (update) stream.durationInSeconds = d;
|
|
92
|
+
return d;
|
|
93
|
+
}
|
|
94
|
+
// Inline children fallback (legacy) — parallel max
|
|
95
|
+
const children = stream.children ?? [];
|
|
96
|
+
if (children.length && update) {
|
|
97
|
+
for (const child of children) {
|
|
98
|
+
getDurationInSeconds(child, update);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const visible = children.filter((c) => !c.isBackground);
|
|
102
|
+
let total = 0;
|
|
103
|
+
for (const c of visible) {
|
|
104
|
+
const d = c.durationInSeconds ?? 0;
|
|
105
|
+
if (d > total) total = d;
|
|
106
|
+
}
|
|
107
|
+
if (update) stream.durationInSeconds = total;
|
|
108
|
+
return total;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// scene is a container (like folder) — falls through to general children logic
|
|
112
|
+
// leaf with actions
|
|
113
|
+
if (!stream.children?.length) {
|
|
114
|
+
const last = stream.actions?.[stream.actions.length - 1];
|
|
115
|
+
const d = last?.end ?? 0;
|
|
116
|
+
if (update) stream.durationInSeconds = d;
|
|
117
|
+
return d;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let total = 0;
|
|
121
|
+
for (const child of stream.children) {
|
|
122
|
+
getDurationInSeconds(child, update);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const visible = stream.children.filter((c) => !c.isBackground);
|
|
126
|
+
if (stream.isSeries) {
|
|
127
|
+
const overlap = stream.transition ? (stream.transitionTime ?? 0.5) : 0;
|
|
128
|
+
for (let i = 0; i < visible.length; i++) {
|
|
129
|
+
const c = visible[i]!;
|
|
130
|
+
const d = c.durationInSeconds ?? 0;
|
|
131
|
+
total += d;
|
|
132
|
+
if (i > 0 && overlap > 0) total -= overlap;
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
for (const c of visible) {
|
|
136
|
+
const d = c.durationInSeconds ?? 0;
|
|
137
|
+
if (d > total) total = d;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (update) stream.durationInSeconds = total;
|
|
142
|
+
return total;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** WebVTT parsing — supports plain "MM:SS.mmm" or "HH:MM:SS.mmm". */
|
|
146
|
+
export const VTT_REG = /^(?:(\d+):)?(\d{1,2}):(\d{2})(?:\.(\d{1,3}))?$/;
|
|
147
|
+
export function vttSecond(t: string): number {
|
|
148
|
+
const m = VTT_REG.exec(t.trim());
|
|
149
|
+
if (!m) return 0;
|
|
150
|
+
const [, h = "0", mm = "0", ss = "0", ms = "0"] = m;
|
|
151
|
+
return Number(h) * 3600 + Number(mm) * 60 + Number(ss) + Number(ms.padEnd(3, "0")) / 1000;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface Cue { startFrom: number; endAt: number; text: string }
|
|
155
|
+
export function parseVTT(src: string): Cue[] {
|
|
156
|
+
const cues: Cue[] = [];
|
|
157
|
+
const blocks = src.replace(/\r\n/g, "\n").split(/\n\n+/);
|
|
158
|
+
for (const b of blocks) {
|
|
159
|
+
const lines = b.split("\n").filter(Boolean);
|
|
160
|
+
const tline = lines.find((l) => l.includes("-->"));
|
|
161
|
+
if (!tline) continue;
|
|
162
|
+
const [a, z] = tline.split("-->").map((s) => s.trim());
|
|
163
|
+
if (!a || !z) continue;
|
|
164
|
+
const text = lines.slice(lines.indexOf(tline) + 1).join("\n").trim();
|
|
165
|
+
cues.push({ startFrom: vttSecond(a), endAt: vttSecond(z), text });
|
|
166
|
+
}
|
|
167
|
+
return cues;
|
|
168
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tween animation resolver for JSX components rendered via react-jsx-parser.
|
|
3
|
+
*
|
|
4
|
+
* Makes `tween(from, to, easing?)` expressions work inside component JSX
|
|
5
|
+
* by providing it as a binding to JsxParser. The tween function uses
|
|
6
|
+
* Remotion's `interpolate()` with the current frame to produce animated values.
|
|
7
|
+
*
|
|
8
|
+
* Usage in markdown JSX:
|
|
9
|
+
* <StatCounter value={tween(0, 100)} />
|
|
10
|
+
* <div style={{ background: tween('#000', '#FFF', 'easeOut') }} />
|
|
11
|
+
* <div style={{ borderRadius: tween(0, 100, 'easeOut') }} />
|
|
12
|
+
*/
|
|
13
|
+
import * as React from "react";
|
|
14
|
+
import { interpolate, useCurrentFrame, Easing } from "remotion";
|
|
15
|
+
|
|
16
|
+
/** Built-in easing name → Remotion easing function. */
|
|
17
|
+
const EASING_MAP: Record<string, ((t: number) => number) | undefined> = {
|
|
18
|
+
linear: undefined,
|
|
19
|
+
ease: Easing.ease,
|
|
20
|
+
easeIn: Easing.in(Easing.ease),
|
|
21
|
+
easeOut: Easing.out(Easing.ease),
|
|
22
|
+
easeInOut: Easing.inOut(Easing.ease),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Check if a value looks like a hex color string. */
|
|
26
|
+
function isHexColor(v: unknown): v is string {
|
|
27
|
+
return typeof v === "string" && /^#[0-9a-fA-F]{3,8}$/.test(v);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Parse a hex color (#RGB, #RRGGBB) to a number. */
|
|
31
|
+
function hexToNumber(hex: string): number {
|
|
32
|
+
const s = hex.replace(/^#/, "");
|
|
33
|
+
if (s.length === 3) {
|
|
34
|
+
return parseInt(s[0]! + s[0] + s[1]! + s[1] + s[2]! + s[2], 16);
|
|
35
|
+
}
|
|
36
|
+
return parseInt(s, 16);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Lerp between two numbers. */
|
|
40
|
+
function lerp(a: number, b: number, t: number): number {
|
|
41
|
+
return a + (b - a) * t;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* React hook that creates tween-related bindings for react-jsx-parser.
|
|
46
|
+
*
|
|
47
|
+
* Returns an object with:
|
|
48
|
+
* - `tween(from, to, easing?)` — frame-interpolated value (number or hex color)
|
|
49
|
+
* - `interpolate` — Remotion's interpolate function (for advanced use)
|
|
50
|
+
*
|
|
51
|
+
* Must be called at the top level of a React component (per Rules of Hooks).
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* const bindings = useTweenBindings(action);
|
|
55
|
+
* <JsxParser bindings={{ ...stream.data, ...bindings }} />
|
|
56
|
+
*/
|
|
57
|
+
export function useTweenBindings(action: { start?: number; end?: number }): Record<string, unknown> {
|
|
58
|
+
const frame = useCurrentFrame();
|
|
59
|
+
const actionDurationFrames = Math.max(1, Math.floor(((action.end ?? 1) - (action.start ?? 0)) * 30));
|
|
60
|
+
|
|
61
|
+
// Per-render cache: cleared each time frame changes.
|
|
62
|
+
// Only lives for one render — avoids re-computing the same tween()
|
|
63
|
+
// call if it appears multiple times in the JSX on the same frame.
|
|
64
|
+
const cacheRef = React.useRef<Map<string, number | string>>(new Map());
|
|
65
|
+
const prevFrameRef = React.useRef(frame);
|
|
66
|
+
|
|
67
|
+
if (prevFrameRef.current !== frame) {
|
|
68
|
+
cacheRef.current = new Map();
|
|
69
|
+
prevFrameRef.current = frame;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const tween = React.useCallback(
|
|
73
|
+
(from: unknown, to: unknown, easing?: string): unknown => {
|
|
74
|
+
const key = `${from},${to},${easing || "linear"}`;
|
|
75
|
+
const cached = cacheRef.current.get(key);
|
|
76
|
+
if (cached !== undefined) return cached;
|
|
77
|
+
|
|
78
|
+
const easingFn = easing ? EASING_MAP[easing] : undefined;
|
|
79
|
+
|
|
80
|
+
// Color tween: interpolate each channel
|
|
81
|
+
if (isHexColor(from) && isHexColor(to)) {
|
|
82
|
+
const fromNum = hexToNumber(from);
|
|
83
|
+
const toNum = hexToNumber(to);
|
|
84
|
+
const t = actionDurationFrames > 0
|
|
85
|
+
? interpolate(frame, [0, actionDurationFrames], [0, 1], {
|
|
86
|
+
extrapolateLeft: "clamp",
|
|
87
|
+
extrapolateRight: "clamp",
|
|
88
|
+
easing: easingFn,
|
|
89
|
+
})
|
|
90
|
+
: 1;
|
|
91
|
+
const r = Math.round(lerp((fromNum >> 16) & 0xff, (toNum >> 16) & 0xff, t));
|
|
92
|
+
const g = Math.round(lerp((fromNum >> 8) & 0xff, (toNum >> 8) & 0xff, t));
|
|
93
|
+
const b = Math.round(lerp(fromNum & 0xff, toNum & 0xff, t));
|
|
94
|
+
const val = `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
95
|
+
cacheRef.current.set(key, val);
|
|
96
|
+
return val;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Numeric tween
|
|
100
|
+
const fromNum = Number(from);
|
|
101
|
+
const toNum = Number(to);
|
|
102
|
+
if (!Number.isFinite(fromNum) || !Number.isFinite(toNum)) {
|
|
103
|
+
return to;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const val = interpolate(frame, [0, actionDurationFrames], [fromNum, toNum], {
|
|
107
|
+
extrapolateLeft: "clamp",
|
|
108
|
+
extrapolateRight: "clamp",
|
|
109
|
+
easing: easingFn,
|
|
110
|
+
});
|
|
111
|
+
cacheRef.current.set(key, val);
|
|
112
|
+
return val;
|
|
113
|
+
},
|
|
114
|
+
[frame, actionDurationFrames],
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return { tween, interpolate };
|
|
118
|
+
}
|