Skip to content

Commit e2f026e

Browse files
committed
fix(skills): extend roots without explicit duration
fixes reported:1785307750.289819:transitions-extend-tail-root-duration-contract-mismatch; PR #2859 and unrelated claims remain unmodified.
1 parent 6c185f2 commit e2f026e

2 files changed

Lines changed: 75 additions & 5 deletions

File tree

skills/faceless-explainer/scripts/transitions.mjs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,16 +117,18 @@ function extendFrameTail(hyperframesDir, frame, baseDuration, targetDuration, di
117117
let extended = 0;
118118
const rewritten = html.replace(/<([A-Za-z][\w:-]*)\b([^>]*)>/g, (tag, name, attrs) => {
119119
const durationMatch = attrs.match(/\bdata-duration="([\d.]+)"/);
120-
if (!durationMatch) return tag;
121-
const duration = Number(durationMatch[1]);
122-
if (!Number.isFinite(duration)) return tag;
123-
124120
const compositionMatch = attrs.match(/\bdata-composition-id="([^"]+)"/);
125121
if (compositionMatch?.[1] === compId && !foundRoot) {
126122
foundRoot = true;
127-
return tag.replace(/\bdata-duration="[\d.]+"/, `data-duration="${targetDuration}"`);
123+
return durationMatch
124+
? tag.replace(/\bdata-duration="[\d.]+"/, `data-duration="${targetDuration}"`)
125+
: tag.replace(/(\s*\/?>)$/, ` data-duration="${targetDuration}"$1`);
128126
}
129127

128+
if (!durationMatch) return tag;
129+
const duration = Number(durationMatch[1]);
130+
if (!Number.isFinite(duration)) return tag;
131+
130132
if (name.toLowerCase() === "audio") return tag;
131133
const startMatch = attrs.match(/\bdata-start="([\d.]+)"/);
132134
if (!startMatch) return tag;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import assert from "node:assert/strict";
2+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3+
import { tmpdir } from "node:os";
4+
import { dirname, join } from "node:path";
5+
import { spawnSync } from "node:child_process";
6+
import test from "node:test";
7+
8+
const script = new URL("./transitions.mjs", import.meta.url).pathname;
9+
10+
function write(filePath, contents) {
11+
mkdirSync(dirname(filePath), { recursive: true });
12+
writeFileSync(filePath, contents);
13+
}
14+
15+
test("inject extends a documented frame root that omits data-duration", (t) => {
16+
const project = mkdtempSync(join(tmpdir(), "faceless-transitions-"));
17+
t.after(() => rmSync(project, { force: true, recursive: true }));
18+
19+
write(
20+
join(project, "STORYBOARD.md"),
21+
`---
22+
format: 1920x1080
23+
---
24+
25+
## Frame 1 — First
26+
27+
- duration: 2s
28+
- transition_in: cut
29+
- status: animated
30+
- src: compositions/frames/01-a.html
31+
32+
## Frame 2 — Second
33+
34+
- duration: 2s
35+
- transition_in: crossfade
36+
- status: animated
37+
- src: compositions/frames/02-b.html
38+
`,
39+
);
40+
write(
41+
join(project, "index.html"),
42+
`<!doctype html><html><body>
43+
<div id="root" data-composition-id="main" data-duration="4"></div>
44+
<div id="el-01-a" data-start="0" data-duration="2" data-track-index="0"></div>
45+
<div id="el-02-b" data-start="2" data-duration="2" data-track-index="0"></div>
46+
<script>window.__timelines={}; window.__timelines["main"] = gsap.timeline({ paused: true });</script>
47+
</body></html>`,
48+
);
49+
write(
50+
join(project, "compositions/frames/01-a.html"),
51+
`<template><div id="root" data-composition-id="01-a"><div class="clip" data-start="0" data-duration="2" data-track-index="0">first</div></div></template>`,
52+
);
53+
write(
54+
join(project, "compositions/frames/02-b.html"),
55+
`<template><div id="root" data-composition-id="02-b"><div class="clip" data-start="0" data-duration="2" data-track-index="0">second</div></div></template>`,
56+
);
57+
58+
const result = spawnSync(
59+
process.execPath,
60+
[script, "inject", "--storyboard", join(project, "STORYBOARD.md"), "--hyperframes", project],
61+
{ encoding: "utf8" },
62+
);
63+
64+
assert.equal(result.status, 0, result.stderr);
65+
const outgoing = readFileSync(join(project, "compositions/frames/01-a.html"), "utf8");
66+
assert.match(outgoing, /data-composition-id="01-a" data-duration="2.5"/);
67+
assert.match(outgoing, /class="clip"[^>]*data-duration="2.5"/);
68+
});

0 commit comments

Comments
 (0)