22 * Timing Compiler
33 *
44 * Shared, pure HTML compilation that normalizes timing attributes.
5- * Works in both Node.js and browser (no dependencies, regex-based).
5+ * Works in both Node.js and browser (regex-based, no DOM ).
66 *
77 * Guarantees every timed element gets:
88 * - id on media elements when missing
1313 * this compiler identifies them as "unresolved" so the caller can provide
1414 * durations via an environment-specific resolver (ffprobe, el.duration, etc.)
1515 * and call injectDurations() to complete the compilation.
16+ *
17+ * Relative `data-start` (`intro`, `intro + 0.5`) is not numeric — leave
18+ * `data-end` off so extract can resolve the id-ref later.
1619 */
1720
21+ import { parseNumeric } from "@hyperframes/parsers/composition-contract" ;
1822import {
1923 parseStrictFiniteTimingNumber ,
2024 readElementPlaybackRate ,
2125 readMediaStart ,
2226} from "../runtime/playbackRate.js" ;
23-
2427// ── Types ────────────────────────────────────────────────────────────────
2528
2629export interface UnresolvedElement {
@@ -149,25 +152,26 @@ function compileTag(
149152 result = injectAttr ( result , "data-hf-auto-start" , "" ) ;
150153 startStr = "0" ;
151154 }
152- const start = parseFloat ( startStr ) ;
155+ const start = parseNumeric ( startStr ) ;
153156 const attrReader = { getAttribute : ( name : string ) => getAttr ( result , name ) } ;
154157 const mediaStart = readMediaStart ( attrReader ) ;
155158 const playbackRate = readElementPlaybackRate ( attrReader ) ;
156159
157- // 1. Compute data-end from data-start + data-duration
160+ // 1. Compute data-end from data-start + data-duration. Skip relative id-refs.
158161 if ( ! hasAttr ( result , "data-end" ) ) {
159162 const durationStr = getAttr ( result , "data-duration" ) ;
160163 const duration = parseStrictFiniteTimingNumber ( durationStr ) ;
161164 if ( duration != null ) {
162- const end = start + duration ;
163- result = injectAttr ( result , "data-end" , String ( end ) ) ;
165+ if ( start != null ) {
166+ result = injectAttr ( result , "data-end" , String ( start + duration ) ) ;
167+ }
164168 } else if ( id ) {
165169 // No data-duration: mark as unresolved so caller can provide it
166170 unresolved = {
167171 id,
168172 tagName : isVideo ? "video" : "audio" ,
169173 src : getAttr ( result , "src" ) ?? undefined ,
170- start,
174+ start : start ?? 0 ,
171175 mediaStart,
172176 playbackRate,
173177 } ;
@@ -229,7 +233,7 @@ export function compileTimingAttrs(html: string): CompilationResult {
229233 unresolved . push ( {
230234 id,
231235 tagName : "div" ,
232- start : startStr ? parseFloat ( startStr ) : 0 ,
236+ start : parseNumeric ( startStr ) ?? 0 ,
233237 mediaStart : 0 ,
234238 playbackRate : 1 ,
235239 compositionSrc : compositionSrc ?? undefined ,
@@ -262,11 +266,12 @@ export function injectDurations(html: string, resolutions: ResolvedDuration[]):
262266 result = setAttr ( result , "data-duration" , String ( duration ) ) ;
263267 }
264268
265- // Add data-end if missing
269+ // Add data-end if missing. Skip relative id-refs.
266270 if ( ! hasAttr ( result , "data-end" ) ) {
267- const startStr = getAttr ( result , "data-start" ) ;
268- const start = startStr ? parseFloat ( startStr ) : 0 ;
269- result = injectAttr ( result , "data-end" , String ( start + duration ) ) ;
271+ const start = parseNumeric ( getAttr ( result , "data-start" ) ) ;
272+ if ( start != null ) {
273+ result = injectAttr ( result , "data-end" , String ( start + duration ) ) ;
274+ }
270275 }
271276
272277 return result ;
@@ -307,7 +312,7 @@ export function extractResolvedMedia(html: string): ResolvedMediaElement[] {
307312 id,
308313 tagName : isVideo ? "video" : "audio" ,
309314 src : getAttr ( tag , "src" ) ?? undefined ,
310- start : startStr !== null ? parseFloat ( startStr ) : 0 ,
315+ start : parseNumeric ( startStr ) ?? 0 ,
311316 duration,
312317 mediaStart : readMediaStart ( attrReader ) ,
313318 playbackRate : readElementPlaybackRate ( attrReader ) ,
@@ -331,10 +336,10 @@ export function clampDurations(html: string, clamps: ResolvedDuration[]): string
331336 // Replace data-duration value
332337 tag = tag . replace ( / d a t a - d u r a t i o n = [ " ' ] [ ^ " ' ] * [ " ' ] / , `data-duration="${ duration } "` ) ;
333338
334- // Recompute data-end from data-start + clamped duration
335- const startStr = getAttr ( tag , "data-start" ) ;
336- const start = startStr ? parseFloat ( startStr ) : 0 ;
337- tag = tag . replace ( / d a t a - e n d = [ " ' ] [ ^ " ' ] * [ " ' ] / , `data-end=" ${ start + duration } "` ) ;
339+ const start = parseNumeric ( getAttr ( tag , " data-start" ) ) ;
340+ if ( start != null ) {
341+ tag = tag . replace ( / d a t a - e n d = [ " ' ] [ ^ " ' ] * [ " ' ] / , `data-end=" ${ start + duration } "` ) ;
342+ }
338343
339344 return tag ;
340345 } ) ;
0 commit comments