@hyperframes/parsers 0.8.144 → 0.8.146
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/dist/assetResolution.js +0 -1
- package/dist/assets.js +0 -1
- package/dist/colorGradingContract.js +0 -1
- package/dist/composition.js +0 -1
- package/dist/compositionContract.js +0 -1
- package/dist/compositionDuration.js +0 -1
- package/dist/ffBinaries.d.ts +5 -4
- package/dist/ffBinaries.js +40 -10
- package/dist/gsapConstants.js +0 -1
- package/dist/gsapParser.js +0 -1
- package/dist/gsapParserAcorn.js +0 -1
- package/dist/gsapParserExports.js +0 -1
- package/dist/gsapWriterAcorn.js +0 -1
- package/dist/hfIds.js +0 -1
- package/dist/htmlAttributeSpans.js +0 -1
- package/dist/index.js +1 -2
- package/dist/mediaDuration.js +0 -1
- package/dist/mediaDurationFixtures.js +0 -1
- package/dist/slideshow.js +1 -2
- package/dist/springEase.js +0 -1
- package/dist/subCompositionValidity.js +0 -1
- package/dist/topLevelElements.js +0 -1
- package/package.json +2 -2
- package/dist/assetResolution.js.map +0 -1
- package/dist/assets.js.map +0 -1
- package/dist/colorGradingContract.js.map +0 -1
- package/dist/composition.js.map +0 -1
- package/dist/compositionContract.js.map +0 -1
- package/dist/compositionDuration.js.map +0 -1
- package/dist/ffBinaries.js.map +0 -1
- package/dist/gsapConstants.js.map +0 -1
- package/dist/gsapParser.js.map +0 -1
- package/dist/gsapParserAcorn.js.map +0 -1
- package/dist/gsapParserExports.js.map +0 -1
- package/dist/gsapWriterAcorn.js.map +0 -1
- package/dist/hfIds.js.map +0 -1
- package/dist/htmlAttributeSpans.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/mediaDuration.js.map +0 -1
- package/dist/mediaDurationFixtures.js.map +0 -1
- package/dist/slideshow.js.map +0 -1
- package/dist/springEase.js.map +0 -1
- package/dist/subCompositionValidity.js.map +0 -1
- package/dist/topLevelElements.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/compositionContract.ts","../src/mediaDuration.ts"],"sourcesContent":["/**\n * Browser-safe authored composition contract.\n *\n * Source HTML is authored with data-start + data-duration + data-track-index.\n * data-end is compiler-derived and data-layer is legacy input only. Readers\n * accept legacy documents; writers always emit the canonical representation.\n */\n\nexport const COMPOSITION_CONTRACT_VERSION = 1 as const;\n\nexport const COMPOSITION_ATTRIBUTES = Object.freeze({\n start: \"data-start\",\n duration: \"data-duration\",\n trackIndex: \"data-track-index\",\n derivedEnd: \"data-end\",\n legacyTrack: \"data-layer\",\n} as const);\n\nexport const CANONICAL_AUTHORED_TIMING_ATTRIBUTES = Object.freeze([\n COMPOSITION_ATTRIBUTES.start,\n COMPOSITION_ATTRIBUTES.duration,\n COMPOSITION_ATTRIBUTES.trackIndex,\n] as const);\n\nexport const DERIVED_TIMING_ATTRIBUTES = Object.freeze([\n COMPOSITION_ATTRIBUTES.derivedEnd,\n] as const);\n\nexport const LEGACY_TIMING_ATTRIBUTES = Object.freeze([\n COMPOSITION_ATTRIBUTES.derivedEnd,\n COMPOSITION_ATTRIBUTES.legacyTrack,\n] as const);\n\nexport type ReferenceExpression =\n | { kind: \"absolute\"; value: number }\n | { kind: \"reference\"; refId: string; offset: number };\n\nexport type ClipTimingDiagnosticCode =\n | \"invalid-start\"\n | \"unresolved-start-reference\"\n | \"invalid-duration\"\n | \"invalid-end\"\n | \"end-before-start\"\n | \"deprecated-end\"\n | \"conflicting-end\"\n | \"invalid-track-index\"\n | \"deprecated-layer\"\n | \"conflicting-layer\";\n\nexport interface ClipTimingDiagnostic {\n code: ClipTimingDiagnosticCode;\n attribute: string;\n value: string | null;\n}\n\nexport interface ClipAttributeReader {\n getAttribute(name: string): string | null;\n}\n\nexport interface ClipAttributeWriter extends ClipAttributeReader {\n setAttribute(name: string, value: string): void;\n removeAttribute(name: string): void;\n}\n\nexport interface ReadClipTimingOptions {\n /** Resolve a reference to the referenced clip's absolute end time. */\n resolveReferenceEnd?: (refId: string) => number | null | undefined;\n /** Start used when data-start is absent. Defaults to zero. */\n defaultStart?: number | null;\n}\n\nexport interface ClipTiming {\n startExpression: ReferenceExpression | null;\n start: number | null;\n duration: number | null;\n end: number | null;\n trackIndex: number;\n durationSource: \"duration\" | \"legacy-end\" | \"missing\" | \"invalid\";\n trackSource: \"track-index\" | \"legacy-layer\" | \"default\" | \"invalid\";\n diagnostics: ClipTimingDiagnostic[];\n}\n\nexport interface ClipTimingUpdate {\n start?: number | string | ReferenceExpression;\n duration?: number | null;\n trackIndex?: number | null;\n}\n\nexport class ClipTimingWriteError extends Error {\n readonly code: \"invalid-start\" | \"invalid-duration\" | \"invalid-track-index\";\n\n constructor(code: ClipTimingWriteError[\"code\"], message: string) {\n super(message);\n this.name = \"ClipTimingWriteError\";\n this.code = code;\n }\n}\n\n/** Parse a value to a finite number, or null if it is absent/invalid. */\nexport function parseNumeric(value: string | null | undefined): number | null {\n if (value == null || value.trim() === \"\") return null;\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : null;\n}\n\nconst REFERENCE_ID_PATTERN = /^[A-Za-z0-9_.:-]+$/;\n\nfunction isAsciiDigitAt(value: string, index: number): boolean {\n const code = value.charCodeAt(index);\n return code >= 48 && code <= 57;\n}\n\nfunction skipDigitsLeft(value: string, start: number): number {\n let cursor = start;\n while (cursor >= 0 && isAsciiDigitAt(value, cursor)) cursor--;\n return cursor;\n}\n\nfunction skipWhitespaceLeft(value: string, start: number): number {\n let cursor = start;\n while (cursor >= 0 && (value[cursor] ?? \"\").trim() === \"\") cursor--;\n return cursor;\n}\n\nfunction findMagnitudeStart(value: string): number | null {\n const last = value.length - 1;\n if (!isAsciiDigitAt(value, last)) return null;\n let cursor = skipDigitsLeft(value, last);\n if (value[cursor] === \".\") cursor = skipDigitsLeft(value, cursor - 1);\n return cursor + 1;\n}\n\nfunction parseReferenceOffset(\n value: string,\n): { refId: string; operator: \"+\" | \"-\"; magnitude: number } | null {\n const magnitudeStart = findMagnitudeStart(value);\n if (magnitudeStart == null) return null;\n const operatorIndex = skipWhitespaceLeft(value, magnitudeStart - 1);\n const operator = value[operatorIndex];\n if (operator !== \"+\" && operator !== \"-\") return null;\n\n const refId = value.slice(0, operatorIndex).trim();\n if (!REFERENCE_ID_PATTERN.test(refId)) return null;\n const magnitude = Number(value.slice(magnitudeStart));\n if (!Number.isFinite(magnitude)) return null;\n return { refId, operator, magnitude };\n}\n\n/**\n * Parse the data-start grammar: absolute seconds, `clip-id`, or\n * `clip-id +/- offset`, where references resolve to the referenced clip's end.\n */\nexport function parseStartExpression(raw: string | null | undefined): ReferenceExpression | null {\n const normalized = (raw ?? \"\").trim();\n if (!normalized) return null;\n const absolute = parseNumeric(normalized);\n if (absolute != null) return { kind: \"absolute\", value: absolute };\n if (REFERENCE_ID_PATTERN.test(normalized)) {\n return { kind: \"reference\", refId: normalized, offset: 0 };\n }\n const reference = parseReferenceOffset(normalized);\n if (!reference) return null;\n return {\n kind: \"reference\",\n refId: reference.refId,\n offset: reference.operator === \"-\" ? -reference.magnitude : reference.magnitude,\n };\n}\n\nfunction pushDiagnostic(\n diagnostics: ClipTimingDiagnostic[],\n code: ClipTimingDiagnosticCode,\n attribute: string,\n value: string | null,\n): void {\n diagnostics.push({ code, attribute, value });\n}\n\nfunction resolveStart(\n expression: ReferenceExpression | null,\n rawStart: string | null,\n options: ReadClipTimingOptions,\n diagnostics: ClipTimingDiagnostic[],\n): number | null {\n if (rawStart == null || rawStart.trim() === \"\") {\n return options.defaultStart === undefined ? 0 : options.defaultStart;\n }\n if (!expression) {\n pushDiagnostic(diagnostics, \"invalid-start\", COMPOSITION_ATTRIBUTES.start, rawStart);\n return null;\n }\n if (expression.kind === \"absolute\") return Math.max(0, expression.value);\n const referencedEnd = options.resolveReferenceEnd?.(expression.refId);\n if (referencedEnd == null || !Number.isFinite(referencedEnd)) {\n pushDiagnostic(\n diagnostics,\n \"unresolved-start-reference\",\n COMPOSITION_ATTRIBUTES.start,\n rawStart,\n );\n return null;\n }\n return Math.max(0, referencedEnd + expression.offset);\n}\n\ntype DurationRead = Pick<ClipTiming, \"duration\" | \"end\" | \"durationSource\">;\ntype TrackRead = Pick<ClipTiming, \"trackIndex\" | \"trackSource\">;\n\n// Float pairs like `data-start=\"0.1\" + data-duration=\"0.2\"` add to\n// `0.30000000000000004`, so the compiler-emitted `data-end` string can be a few\n// ulps off the sum a reader computes from the canonical inputs. Any drift below\n// this ceiling is treated as equal for the derived-end reconciliation check —\n// well below one 60fps frame (~16.67 ms) and orders of magnitude above the\n// worst realistic IEEE-754 residual (~2e-16 s).\nconst DERIVED_END_EQUALITY_EPSILON_SECONDS = 1e-9;\n\nfunction derivedEndsAreConsistent(parsedEnd: number, canonicalEnd: number): boolean {\n return Math.abs(parsedEnd - canonicalEnd) <= DERIVED_END_EQUALITY_EPSILON_SECONDS;\n}\n\n// Reconcile a `data-end` attribute paired with canonical `data-duration`.\n//\n// The compiler (`compileTimingAttrs` in @hyperframes/core) writes\n// `data-end = data-start + data-duration` into the bundled HTML so the runtime\n// can key off a single attribute. That derived attribute is legitimate — it is\n// not a legacy authoring shape. Treating it as `deprecated-end` here made the\n// linter fire `deprecated_data_end` on the compiler's own output whenever\n// StaticGuard re-validated a bundled composition, producing a false-positive\n// cluster reported in the field (n=25+ across cli-feedback crons 61-68).\n//\n// We keep `deprecated-end` for the truly legacy shape (see `readLegacyEnd` —\n// `data-end` present without `data-duration`) and for the *conflicting* case\n// where an author left a stale `data-end` behind a canonical duration edit; a\n// stale end must not be silently overridden without a diagnostic.\nfunction diagnoseDerivedEnd(\n rawEnd: string,\n canonicalEnd: number | null,\n diagnostics: ClipTimingDiagnostic[],\n): void {\n const parsedEnd = parseNumeric(rawEnd);\n if (parsedEnd == null) {\n pushDiagnostic(diagnostics, \"deprecated-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n pushDiagnostic(diagnostics, \"invalid-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n return;\n }\n if (canonicalEnd == null) {\n // Canonical duration exists but resolved end is unknown (e.g. unresolved\n // reference start). Preserve prior behavior — flag as deprecated so authors\n // remove the stale companion attribute.\n pushDiagnostic(diagnostics, \"deprecated-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n return;\n }\n if (derivedEndsAreConsistent(parsedEnd, canonicalEnd)) {\n // Compiler-derived (or manually consistent) `data-end` alongside\n // `data-duration` — silent. No diagnostic.\n return;\n }\n pushDiagnostic(diagnostics, \"deprecated-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n pushDiagnostic(diagnostics, \"conflicting-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n}\n\nfunction readCanonicalDuration(\n rawDuration: string,\n start: number | null,\n diagnostics: ClipTimingDiagnostic[],\n): DurationRead {\n const duration = parseNumeric(rawDuration);\n if (duration == null || duration < 0) {\n pushDiagnostic(diagnostics, \"invalid-duration\", COMPOSITION_ATTRIBUTES.duration, rawDuration);\n return { duration: null, end: null, durationSource: \"invalid\" };\n }\n return {\n duration,\n end: start == null ? null : start + duration,\n durationSource: \"duration\",\n };\n}\n\nfunction readLegacyEnd(\n rawEnd: string,\n start: number | null,\n diagnostics: ClipTimingDiagnostic[],\n): DurationRead {\n pushDiagnostic(diagnostics, \"deprecated-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n const end = parseNumeric(rawEnd);\n if (end == null) {\n pushDiagnostic(diagnostics, \"invalid-end\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n return { duration: null, end: null, durationSource: \"invalid\" };\n }\n if (start == null) return { duration: null, end, durationSource: \"legacy-end\" };\n if (end < start) {\n pushDiagnostic(diagnostics, \"end-before-start\", COMPOSITION_ATTRIBUTES.derivedEnd, rawEnd);\n return { duration: null, end: null, durationSource: \"invalid\" };\n }\n return { duration: end - start, end, durationSource: \"legacy-end\" };\n}\n\nfunction readDuration(\n attributes: ClipAttributeReader,\n start: number | null,\n diagnostics: ClipTimingDiagnostic[],\n): DurationRead {\n const rawDuration = attributes.getAttribute(COMPOSITION_ATTRIBUTES.duration);\n const rawEnd = attributes.getAttribute(COMPOSITION_ATTRIBUTES.derivedEnd);\n if (rawDuration == null) {\n return rawEnd == null\n ? { duration: null, end: null, durationSource: \"missing\" }\n : readLegacyEnd(rawEnd, start, diagnostics);\n }\n const canonical = readCanonicalDuration(rawDuration, start, diagnostics);\n if (rawEnd != null) diagnoseDerivedEnd(rawEnd, canonical.end, diagnostics);\n return canonical;\n}\n\nfunction readTrackValue(\n rawValue: string,\n attribute: string,\n source: \"track-index\" | \"legacy-layer\",\n diagnostics: ClipTimingDiagnostic[],\n): TrackRead {\n const trackIndex = parseNumeric(rawValue);\n if (trackIndex == null || !Number.isInteger(trackIndex)) {\n pushDiagnostic(diagnostics, \"invalid-track-index\", attribute, rawValue);\n return { trackIndex: 0, trackSource: \"invalid\" };\n }\n return { trackIndex, trackSource: source };\n}\n\nfunction readTrack(\n attributes: ClipAttributeReader,\n diagnostics: ClipTimingDiagnostic[],\n): TrackRead {\n const rawTrack = attributes.getAttribute(COMPOSITION_ATTRIBUTES.trackIndex);\n const rawLayer = attributes.getAttribute(COMPOSITION_ATTRIBUTES.legacyTrack);\n if (rawTrack == null && rawLayer == null) return { trackIndex: 0, trackSource: \"default\" };\n if (rawTrack == null && rawLayer != null) {\n pushDiagnostic(diagnostics, \"deprecated-layer\", COMPOSITION_ATTRIBUTES.legacyTrack, rawLayer);\n return readTrackValue(\n rawLayer,\n COMPOSITION_ATTRIBUTES.legacyTrack,\n \"legacy-layer\",\n diagnostics,\n );\n }\n const canonical = readTrackValue(\n rawTrack ?? \"\",\n COMPOSITION_ATTRIBUTES.trackIndex,\n \"track-index\",\n diagnostics,\n );\n if (rawLayer == null) return canonical;\n pushDiagnostic(diagnostics, \"deprecated-layer\", COMPOSITION_ATTRIBUTES.legacyTrack, rawLayer);\n const parsedLayer = parseNumeric(rawLayer);\n if (parsedLayer != null && parsedLayer !== canonical.trackIndex) {\n pushDiagnostic(diagnostics, \"conflicting-layer\", COMPOSITION_ATTRIBUTES.legacyTrack, rawLayer);\n }\n return canonical;\n}\n\n/** Read canonical timing, accepting legacy attributes without preferring them. */\nexport function readClipTiming(\n attributes: ClipAttributeReader,\n options: ReadClipTimingOptions = {},\n): ClipTiming {\n const diagnostics: ClipTimingDiagnostic[] = [];\n const rawStart = attributes.getAttribute(COMPOSITION_ATTRIBUTES.start);\n const startExpression = parseStartExpression(rawStart);\n const start = resolveStart(startExpression, rawStart, options, diagnostics);\n const duration = readDuration(attributes, start, diagnostics);\n const track = readTrack(attributes, diagnostics);\n return { startExpression, start, ...duration, ...track, diagnostics };\n}\n\nfunction serializeStartNumber(start: number): string {\n if (!Number.isFinite(start)) {\n throw new ClipTimingWriteError(\"invalid-start\", \"start must be finite\");\n }\n return String(start);\n}\n\nfunction serializeStartReference(start: ReferenceExpression): string {\n if (start.kind === \"absolute\") return serializeStartNumber(start.value);\n if (!start.refId || !Number.isFinite(start.offset)) {\n throw new ClipTimingWriteError(\n \"invalid-start\",\n \"reference start must have an id and finite offset\",\n );\n }\n if (start.offset === 0) return start.refId;\n return `${start.refId} ${start.offset < 0 ? \"-\" : \"+\"} ${Math.abs(start.offset)}`;\n}\n\nfunction serializeStart(start: ClipTimingUpdate[\"start\"]): string {\n if (typeof start === \"number\") return serializeStartNumber(start);\n if (typeof start === \"object\" && start != null) return serializeStartReference(start);\n if (typeof start === \"string\" && parseStartExpression(start)) return start.trim();\n throw new ClipTimingWriteError(\"invalid-start\", `invalid start expression: ${start ?? \"\"}`);\n}\n\nfunction writeDuration(\n attributes: ClipAttributeWriter,\n duration: number | null | undefined,\n current: ClipTiming,\n): void {\n if (duration === null) {\n attributes.removeAttribute(COMPOSITION_ATTRIBUTES.duration);\n return;\n }\n if (duration === undefined) {\n if (\n attributes.getAttribute(COMPOSITION_ATTRIBUTES.duration) == null &&\n current.duration != null\n ) {\n attributes.setAttribute(COMPOSITION_ATTRIBUTES.duration, String(current.duration));\n }\n return;\n }\n if (!Number.isFinite(duration) || duration < 0) {\n throw new ClipTimingWriteError(\n \"invalid-duration\",\n \"duration must be a finite, non-negative number\",\n );\n }\n attributes.setAttribute(COMPOSITION_ATTRIBUTES.duration, String(duration));\n}\n\nfunction writeTrack(\n attributes: ClipAttributeWriter,\n trackIndex: number | null | undefined,\n current: ClipTiming,\n): void {\n if (trackIndex === null) {\n attributes.removeAttribute(COMPOSITION_ATTRIBUTES.trackIndex);\n return;\n }\n if (trackIndex === undefined) {\n if (\n attributes.getAttribute(COMPOSITION_ATTRIBUTES.trackIndex) == null &&\n current.trackSource === \"legacy-layer\"\n ) {\n attributes.setAttribute(COMPOSITION_ATTRIBUTES.trackIndex, String(current.trackIndex));\n }\n return;\n }\n if (!Number.isFinite(trackIndex) || !Number.isInteger(trackIndex)) {\n throw new ClipTimingWriteError(\"invalid-track-index\", \"trackIndex must be a finite integer\");\n }\n attributes.setAttribute(COMPOSITION_ATTRIBUTES.trackIndex, String(trackIndex));\n}\n\n/** Write only canonical authored timing and remove derived/legacy source attributes. */\nexport function writeClipTiming(\n attributes: ClipAttributeWriter,\n update: ClipTimingUpdate,\n): ClipTiming {\n const current = readClipTiming(attributes);\n if (update.start !== undefined) {\n attributes.setAttribute(COMPOSITION_ATTRIBUTES.start, serializeStart(update.start));\n }\n writeDuration(attributes, update.duration, current);\n writeTrack(attributes, update.trackIndex, current);\n // A legacy end paired with an unresolved reference start cannot be converted\n // to data-duration without a reference resolver. On a duration-omitting edit\n // (for example, moving only the track), preserve that sole duration source\n // instead of canonicalizing it into data loss.\n const preserveUnresolvedLegacyEnd =\n update.duration === undefined &&\n attributes.getAttribute(COMPOSITION_ATTRIBUTES.duration) == null &&\n attributes.getAttribute(COMPOSITION_ATTRIBUTES.derivedEnd) != null &&\n current.duration == null;\n if (!preserveUnresolvedLegacyEnd) {\n attributes.removeAttribute(COMPOSITION_ATTRIBUTES.derivedEnd);\n }\n attributes.removeAttribute(COMPOSITION_ATTRIBUTES.legacyTrack);\n return readClipTiming(attributes);\n}\n","/** The one rule for a media element's timeline length; hosts only supply the source length. */\n\nimport { parseNumeric } from \"./compositionContract.js\";\n\nexport type MediaTag = \"video\" | \"audio\" | \"img\";\n\n/** Reads a named attribute's raw string value. Works for a DOM Element's `getAttribute`\n * (browser or linkedom) or a plain attrs-record lookup -- callers pick whichever they have. */\nexport type AttrReader = (name: string) => string | null | undefined;\n\nconst nonNegative = (raw: string | null | undefined): number | null => {\n const value = parseNumeric(raw);\n return value !== null && value >= 0 ? value : null;\n};\n\n/** Playback offset into the source: `data-playback-start`, falling back to the older\n * `data-media-start`; an invalid or negative value falls through to the next. One reader for\n * every host -- the runtime and `htmlParser` used to read this two different ways. */\nexport function readMediaOffsetSeconds(getAttr: AttrReader): number {\n return (\n nonNegative(getAttr(\"data-playback-start\")) ?? nonNegative(getAttr(\"data-media-start\")) ?? 0\n );\n}\n\n/** The one bound on a clip's playback rate; core re-exports it for the rate lane. */\nexport const MIN_PLAYBACK_RATE = 0.1;\nexport const MAX_PLAYBACK_RATE = 10;\n\n/** A playback rate clamped to the bounds above; anything non-positive or non-finite is 1. */\nexport function clampPlaybackRate(raw: number): number {\n return Number.isFinite(raw) && raw > 0\n ? Math.max(MIN_PLAYBACK_RATE, Math.min(MAX_PLAYBACK_RATE, raw))\n : 1;\n}\n\n/** `data-playback-rate`, parsed like the browser parses a native rate (`\"2x\"` is 2), else\n * `fallback` (a media element's own `defaultPlaybackRate` in the browser), clamped. */\nexport function readPlaybackRate(getAttr: AttrReader, fallback = 1): number {\n const authored = Number.parseFloat(getAttr(\"data-playback-rate\") ?? \"\");\n return clampPlaybackRate(Number.isFinite(authored) && authored > 0 ? authored : fallback);\n}\n\n/** `data-duration` when it is a positive number, else null. */\nexport function readDataDurationSeconds(getAttr: AttrReader): number | null {\n const duration = parseNumeric(getAttr(\"data-duration\"));\n return duration !== null && duration > 0 ? duration : null;\n}\n\n/** An authored trim: `data-duration` if positive, else `data-end - start` if `data-end` is\n * authored, else null (nothing authored). */\nexport function readAuthoredDurationSeconds(\n getAttr: AttrReader,\n startSeconds: number,\n): number | null {\n const duration = readDataDurationSeconds(getAttr);\n if (duration !== null) return duration;\n const end = parseNumeric(getAttr(\"data-end\"));\n return end !== null ? end - startSeconds : null;\n}\n\n/** The dropped-image default: what Studio gives a freshly dropped image before any trim.\n * One owner; everything needing an untrimmed image length imports this. */\nexport const DEFAULT_IMAGE_TIMELINE_DURATION_SECONDS = 3;\n\n/** The arithmetic alone: no attribute reading, no DOM, no probing. */\nexport function resolveNaturalDurationSeconds(\n sourceDurationSeconds: number,\n mediaStartSeconds: number,\n playbackRate: number,\n): number | null {\n if (!Number.isFinite(sourceDurationSeconds)) return null;\n const remaining = Math.max(0, sourceDurationSeconds - mediaStartSeconds);\n return remaining / clampPlaybackRate(playbackRate);\n}\n\nexport type MediaDurationSource = \"authored\" | \"media\" | \"default\" | \"pending\";\n\nexport interface MediaDurationResult {\n seconds: number | null;\n source: MediaDurationSource;\n /** Set only when source is \"pending\": why nothing could be resolved yet. */\n reason?: string;\n}\n\nexport interface ResolveMediaDurationInput {\n tag: MediaTag;\n /** From `readAuthoredDurationSeconds`, or null when nothing is authored. */\n authoredDurationSeconds: number | null;\n /** The source's own length once a probe has answered, else null. Ignored for `img`. */\n sourceDurationSeconds: number | null;\n mediaStartSeconds: number;\n playbackRate: number;\n}\n\nexport function resolveMediaDuration(input: ResolveMediaDurationInput): MediaDurationResult {\n const { tag, authoredDurationSeconds, sourceDurationSeconds, mediaStartSeconds, playbackRate } =\n input;\n if (authoredDurationSeconds !== null && authoredDurationSeconds > 0) {\n return { seconds: authoredDurationSeconds, source: \"authored\" };\n }\n if (tag === \"img\") {\n return { seconds: DEFAULT_IMAGE_TIMELINE_DURATION_SECONDS, source: \"default\" };\n }\n if (sourceDurationSeconds === null) {\n return { seconds: null, source: \"pending\", reason: \"source duration not yet probed\" };\n }\n const seconds = resolveNaturalDurationSeconds(\n sourceDurationSeconds,\n mediaStartSeconds,\n playbackRate,\n );\n if (seconds === null) {\n return { seconds: null, source: \"pending\", reason: \"source reported a non-finite duration\" };\n }\n return { seconds, source: \"media\" };\n}\n\n/** Readers not yet converted to `resolveMediaDuration`; each reader PR removes its entry, so this only shrinks. */\nexport const PENDING_MEDIA_DURATION_READERS = [\n \"core/src/runtime/init.ts (visibility end, clock, seek, WebAudio scheduling)\",\n \"core/src/compiler/timingCompiler.ts compileTag\",\n \"core/src/compiler/htmlCompiler.ts compileHtml\",\n \"producer/src/services/htmlCompiler.ts resolveMediaDuration\",\n \"engine/src/services/videoFrameExtractor.ts (extraction window)\",\n \"engine/src/services/audioMixer.ts\",\n \"parsers/src/htmlParser.ts (defaults to 5s, reads only data-media-start)\",\n \"studio (timelineDOM.ts, timelineElementHelpers.ts, useTimelineSyncCallbacks.ts)\",\n] as const;\n"],"mappings":";AAUO,IAAM,yBAAyB,OAAO,OAAO;AAAA,EAClD,OAAO;AAAA,EACP,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,aAAa;AACf,CAAU;AAEH,IAAM,uCAAuC,OAAO,OAAO;AAAA,EAChE,uBAAuB;AAAA,EACvB,uBAAuB;AAAA,EACvB,uBAAuB;AACzB,CAAU;AAEH,IAAM,4BAA4B,OAAO,OAAO;AAAA,EACrD,uBAAuB;AACzB,CAAU;AAEH,IAAM,2BAA2B,OAAO,OAAO;AAAA,EACpD,uBAAuB;AAAA,EACvB,uBAAuB;AACzB,CAAU;AAoEH,SAAS,aAAa,OAAiD;AAC5E,MAAI,SAAS,QAAQ,MAAM,KAAK,MAAM,GAAI,QAAO;AACjD,QAAM,SAAS,OAAO,KAAK;AAC3B,SAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAC5C;;;AC7FA,IAAM,cAAc,CAAC,QAAkD;AACrE,QAAM,QAAQ,aAAa,GAAG;AAC9B,SAAO,UAAU,QAAQ,SAAS,IAAI,QAAQ;AAChD;AAKO,SAAS,uBAAuB,SAA6B;AAClE,SACE,YAAY,QAAQ,qBAAqB,CAAC,KAAK,YAAY,QAAQ,kBAAkB,CAAC,KAAK;AAE/F;AAGO,IAAM,oBAAoB;AAC1B,IAAM,oBAAoB;AAG1B,SAAS,kBAAkB,KAAqB;AACrD,SAAO,OAAO,SAAS,GAAG,KAAK,MAAM,IACjC,KAAK,IAAI,mBAAmB,KAAK,IAAI,mBAAmB,GAAG,CAAC,IAC5D;AACN;AAIO,SAAS,iBAAiB,SAAqB,WAAW,GAAW;AAC1E,QAAM,WAAW,OAAO,WAAW,QAAQ,oBAAoB,KAAK,EAAE;AACtE,SAAO,kBAAkB,OAAO,SAAS,QAAQ,KAAK,WAAW,IAAI,WAAW,QAAQ;AAC1F;AAGO,SAAS,wBAAwB,SAAoC;AAC1E,QAAM,WAAW,aAAa,QAAQ,eAAe,CAAC;AACtD,SAAO,aAAa,QAAQ,WAAW,IAAI,WAAW;AACxD;AAIO,SAAS,4BACd,SACA,cACe;AACf,QAAM,WAAW,wBAAwB,OAAO;AAChD,MAAI,aAAa,KAAM,QAAO;AAC9B,QAAM,MAAM,aAAa,QAAQ,UAAU,CAAC;AAC5C,SAAO,QAAQ,OAAO,MAAM,eAAe;AAC7C;AAIO,IAAM,0CAA0C;AAGhD,SAAS,8BACd,uBACA,mBACA,cACe;AACf,MAAI,CAAC,OAAO,SAAS,qBAAqB,EAAG,QAAO;AACpD,QAAM,YAAY,KAAK,IAAI,GAAG,wBAAwB,iBAAiB;AACvE,SAAO,YAAY,kBAAkB,YAAY;AACnD;AAqBO,SAAS,qBAAqB,OAAuD;AAC1F,QAAM,EAAE,KAAK,yBAAyB,uBAAuB,mBAAmB,aAAa,IAC3F;AACF,MAAI,4BAA4B,QAAQ,0BAA0B,GAAG;AACnE,WAAO,EAAE,SAAS,yBAAyB,QAAQ,WAAW;AAAA,EAChE;AACA,MAAI,QAAQ,OAAO;AACjB,WAAO,EAAE,SAAS,yCAAyC,QAAQ,UAAU;AAAA,EAC/E;AACA,MAAI,0BAA0B,MAAM;AAClC,WAAO,EAAE,SAAS,MAAM,QAAQ,WAAW,QAAQ,iCAAiC;AAAA,EACtF;AACA,QAAM,UAAU;AAAA,IACd;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,MAAI,YAAY,MAAM;AACpB,WAAO,EAAE,SAAS,MAAM,QAAQ,WAAW,QAAQ,wCAAwC;AAAA,EAC7F;AACA,SAAO,EAAE,SAAS,QAAQ,QAAQ;AACpC;AAGO,IAAM,iCAAiC;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/mediaDurationFixtures.ts"],"sourcesContent":["import type { MediaDurationResult, MediaTag } from \"./mediaDuration.js\";\n\n/** The one fixture table every media-length reader is proven against: an authored\n * element plus the source length a probe would report, and the `expected` result. */\nexport interface MediaDurationFixture {\n name: string;\n tag: MediaTag;\n attrs: Readonly<Record<string, string>>;\n sourceDurationSeconds: number | null;\n expected: MediaDurationResult;\n}\n\nexport const MEDIA_DURATION_FIXTURES: readonly MediaDurationFixture[] = [\n {\n name: \"authored data-duration wins over a known source length\",\n tag: \"video\",\n attrs: { \"data-start\": \"0\", \"data-duration\": \"4\" },\n sourceDurationSeconds: 20,\n expected: { seconds: 4, source: \"authored\" },\n },\n {\n name: \"video with only data-start resolves to the source length\",\n tag: \"video\",\n attrs: { \"data-start\": \"0\" },\n sourceDurationSeconds: 12,\n expected: { seconds: 12, source: \"media\" },\n },\n {\n name: \"video resolves to source minus offset over rate\",\n tag: \"video\",\n attrs: { \"data-start\": \"0\", \"data-playback-start\": \"5\", \"data-playback-rate\": \"2\" },\n sourceDurationSeconds: 20,\n expected: { seconds: 7.5, source: \"media\" },\n },\n {\n name: \"the older data-media-start offset still applies\",\n tag: \"audio\",\n attrs: { \"data-start\": \"0\", \"data-media-start\": \"4\" },\n sourceDurationSeconds: 10,\n expected: { seconds: 6, source: \"media\" },\n },\n {\n name: \"an offset past the end of the source is a known zero span\",\n tag: \"audio\",\n attrs: { \"data-start\": \"0\", \"data-media-start\": \"12\" },\n sourceDurationSeconds: 10,\n expected: { seconds: 0, source: \"media\" },\n },\n {\n name: \"a rate above the 10x bound is clamped\",\n tag: \"video\",\n attrs: { \"data-start\": \"0\", \"data-playback-rate\": \"20\" },\n sourceDurationSeconds: 10,\n expected: { seconds: 1, source: \"media\" },\n },\n {\n name: \"a native-style rate like 2x reads as 2\",\n tag: \"video\",\n attrs: { \"data-start\": \"0\", \"data-playback-rate\": \"2x\" },\n sourceDurationSeconds: 10,\n expected: { seconds: 5, source: \"media\" },\n },\n {\n name: \"a zero authored duration is not authored\",\n tag: \"video\",\n attrs: { \"data-start\": \"0\", \"data-duration\": \"0\" },\n sourceDurationSeconds: 8,\n expected: { seconds: 8, source: \"media\" },\n },\n {\n name: \"video with no probed source length is pending, not a guess\",\n tag: \"video\",\n attrs: { \"data-start\": \"0\" },\n sourceDurationSeconds: null,\n expected: { seconds: null, source: \"pending\", reason: \"source duration not yet probed\" },\n },\n {\n name: \"image with no authored duration takes the dropped-image default\",\n tag: \"img\",\n attrs: { \"data-start\": \"0\" },\n sourceDurationSeconds: null,\n expected: { seconds: 3, source: \"default\" },\n },\n {\n name: \"image with an authored duration is trimmed like any other media\",\n tag: \"img\",\n attrs: { \"data-start\": \"0\", \"data-duration\": \"1.5\" },\n sourceDurationSeconds: null,\n expected: { seconds: 1.5, source: \"authored\" },\n },\n];\n"],"mappings":";AAYO,IAAM,0BAA2D;AAAA,EACtE;AAAA,IACE,MAAM;AAAA,IACN,KAAK;AAAA,IACL,OAAO,EAAE,cAAc,KAAK,iBAAiB,IAAI;AAAA,IACjD,uBAAuB;AAAA,IACvB,UAAU,EAAE,SAAS,GAAG,QAAQ,WAAW;AAAA,EAC7C;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,KAAK;AAAA,IACL,OAAO,EAAE,cAAc,IAAI;AAAA,IAC3B,uBAAuB;AAAA,IACvB,UAAU,EAAE,SAAS,IAAI,QAAQ,QAAQ;AAAA,EAC3C;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,KAAK;AAAA,IACL,OAAO,EAAE,cAAc,KAAK,uBAAuB,KAAK,sBAAsB,IAAI;AAAA,IAClF,uBAAuB;AAAA,IACvB,UAAU,EAAE,SAAS,KAAK,QAAQ,QAAQ;AAAA,EAC5C;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,KAAK;AAAA,IACL,OAAO,EAAE,cAAc,KAAK,oBAAoB,IAAI;AAAA,IACpD,uBAAuB;AAAA,IACvB,UAAU,EAAE,SAAS,GAAG,QAAQ,QAAQ;AAAA,EAC1C;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,KAAK;AAAA,IACL,OAAO,EAAE,cAAc,KAAK,oBAAoB,KAAK;AAAA,IACrD,uBAAuB;AAAA,IACvB,UAAU,EAAE,SAAS,GAAG,QAAQ,QAAQ;AAAA,EAC1C;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,KAAK;AAAA,IACL,OAAO,EAAE,cAAc,KAAK,sBAAsB,KAAK;AAAA,IACvD,uBAAuB;AAAA,IACvB,UAAU,EAAE,SAAS,GAAG,QAAQ,QAAQ;AAAA,EAC1C;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,KAAK;AAAA,IACL,OAAO,EAAE,cAAc,KAAK,sBAAsB,KAAK;AAAA,IACvD,uBAAuB;AAAA,IACvB,UAAU,EAAE,SAAS,GAAG,QAAQ,QAAQ;AAAA,EAC1C;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,KAAK;AAAA,IACL,OAAO,EAAE,cAAc,KAAK,iBAAiB,IAAI;AAAA,IACjD,uBAAuB;AAAA,IACvB,UAAU,EAAE,SAAS,GAAG,QAAQ,QAAQ;AAAA,EAC1C;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,KAAK;AAAA,IACL,OAAO,EAAE,cAAc,IAAI;AAAA,IAC3B,uBAAuB;AAAA,IACvB,UAAU,EAAE,SAAS,MAAM,QAAQ,WAAW,QAAQ,iCAAiC;AAAA,EACzF;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,KAAK;AAAA,IACL,OAAO,EAAE,cAAc,IAAI;AAAA,IAC3B,uBAAuB;AAAA,IACvB,UAAU,EAAE,SAAS,GAAG,QAAQ,UAAU;AAAA,EAC5C;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,KAAK;AAAA,IACL,OAAO,EAAE,cAAc,KAAK,iBAAiB,MAAM;AAAA,IACnD,uBAAuB;AAAA,IACvB,UAAU,EAAE,SAAS,KAAK,QAAQ,WAAW;AAAA,EAC/C;AACF;","names":[]}
|
package/dist/slideshow.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/slideshow/slideshow.types.ts","../src/slideshow/parseSlideshow.ts","../src/slideshow/sceneId.ts"],"sourcesContent":["// packages/core/src/slideshow/slideshow.types.ts\n\n/** Current manifest schema version. Stamped on persist so future schema\n * changes can detect and migrate older islands. */\nexport const SLIDESHOW_MANIFEST_VERSION = 1;\n\n/** Raw author-facing shapes parsed from the JSON island. */\nexport interface SlideshowManifest {\n /** Schema version (absent on pre-versioning islands → treat as 1). */\n version?: number;\n slides: SlideRef[];\n slideSequences?: SlideSequence[];\n}\n\nexport interface SlideRef {\n sceneId: string;\n startTime?: number;\n endTime?: number;\n notes?: string;\n fragments?: number[];\n hotspots?: SlideHotspot[];\n /**\n * When true, the slide's first `<video>` plays automatically on enter (the\n * presenter lands on the slide and the clip plays). The slideshow still holds\n * — it never auto-advances — so the presenter clicks Next when ready.\n * Defaults to false. Use it when the video is the slide's primary content and\n * its natural end is the cue to advance, not for background/ambient clips.\n */\n autoplay?: boolean;\n // Reserved — TTS deferred. Parsed and carried, never consumed.\n ttsScript?: string;\n ttsAudioUrl?: string;\n ttsDurationMs?: number;\n}\n\nexport interface SlideHotspot {\n id: string;\n label: string;\n target: string; // references a SlideSequence.id\n region?: { x: number; y: number; w: number; h: number }; // % of slide\n}\n\nexport interface SlideSequence {\n id: string;\n label: string;\n slides: SlideRef[];\n}\n\n/** A slide with its time range resolved from the matching scene. */\nexport interface ResolvedSlide extends SlideRef {\n start: number;\n end: number;\n fragments: number[]; // always present, sorted, defaulted to []\n hotspots: SlideHotspot[]; // always present, defaulted to []\n}\n\nexport interface ResolvedSlideSequence {\n id: string;\n label: string;\n slides: ResolvedSlide[];\n}\n\nexport interface ResolvedSlideshow {\n slides: ResolvedSlide[];\n sequences: Record<string, ResolvedSlideSequence>; // keyed by sequence id\n}\n","// packages/core/src/slideshow/parseSlideshow.ts\nimport type {\n SlideshowManifest,\n SlideRef,\n SlideHotspot,\n ResolvedSlide,\n ResolvedSlideshow,\n ResolvedSlideSequence,\n} from \"./slideshow.types\";\n\nexport const SLIDESHOW_ISLAND_TYPE = \"application/hyperframes-slideshow+json\";\n\n/**\n * Builds the island <script> matcher. Capture group 1 = inner JSON.\n *\n * Factory (fresh RegExp per call) on purpose: a RegExp with the `g` flag carries\n * a mutable `lastIndex`, so callers that need `g` must call this each time rather\n * than caching a shared instance.\n */\nexport function slideshowIslandRegex(flags = \"i\"): RegExp {\n // Escape ALL regex metacharacters in the constant (CodeQL flags the incomplete\n // `\\` escape). The constant has none today, but a complete escape is correct.\n const escaped = SLIDESHOW_ISLAND_TYPE.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return new RegExp(`<script[^>]*type=[\"']${escaped}[\"'][^>]*>([\\\\s\\\\S]*?)<\\\\/script>`, flags);\n}\n\ninterface SceneRange {\n id: string;\n start: number;\n duration: number;\n}\n\n/** Extract the JSON island from composition HTML. Returns null if absent. */\nexport function parseSlideshowManifest(html: string): SlideshowManifest | null {\n // Match <script type=\"application/hyperframes-slideshow+json\"> ... </script>\n const re = slideshowIslandRegex(\"i\");\n const match = re.exec(html);\n if (!match || match[1] === undefined) return null;\n const raw = match[1].trim();\n if (raw.length === 0) return null;\n const parsed: unknown = JSON.parse(raw);\n if (!isManifest(parsed)) {\n throw new Error(\"slideshow island is not a valid SlideshowManifest\");\n }\n return parsed;\n}\n\nfunction isOptionalNumberArray(v: unknown): boolean {\n return v === undefined || (Array.isArray(v) && v.every((n) => typeof n === \"number\"));\n}\n\nfunction isOptionalBoolean(v: unknown): v is boolean | undefined {\n return v === undefined || typeof v === \"boolean\";\n}\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nfunction isHotspotRegion(v: unknown): boolean {\n return (\n isRecord(v) &&\n [v[\"x\"], v[\"y\"], v[\"w\"], v[\"h\"]].every(\n (coordinate) => typeof coordinate === \"number\" && Number.isFinite(coordinate),\n )\n );\n}\n\nfunction isSlideHotspot(v: unknown): v is SlideHotspot {\n return (\n isRecord(v) &&\n typeof v[\"id\"] === \"string\" &&\n typeof v[\"label\"] === \"string\" &&\n typeof v[\"target\"] === \"string\" &&\n (v[\"region\"] === undefined || isHotspotRegion(v[\"region\"]))\n );\n}\n\nfunction isSlideRef(v: unknown): v is SlideRef {\n if (!isRecord(v)) return false;\n const r = v;\n if (typeof r[\"sceneId\"] !== \"string\") return false;\n if (!isOptionalNumberArray(r[\"fragments\"])) return false;\n if (\n r[\"hotspots\"] !== undefined &&\n (!Array.isArray(r[\"hotspots\"]) || !r[\"hotspots\"].every(isSlideHotspot))\n )\n return false;\n if (!isOptionalBoolean(r[\"autoplay\"])) return false;\n return true;\n}\n\nfunction isSlideSequence(v: unknown): boolean {\n if (typeof v !== \"object\" || v === null) return false;\n const s = v as Record<string, unknown>;\n return (\n typeof s[\"id\"] === \"string\" &&\n typeof s[\"label\"] === \"string\" &&\n Array.isArray(s[\"slides\"]) &&\n s[\"slides\"].every(isSlideRef)\n );\n}\n\nfunction isManifest(v: unknown): v is SlideshowManifest {\n if (typeof v !== \"object\" || v === null || Array.isArray(v)) return false;\n const o = v as Record<string, unknown>;\n if (!Array.isArray(o[\"slides\"]) || !o[\"slides\"].every(isSlideRef)) return false;\n if (o[\"slideSequences\"] !== undefined) {\n if (!Array.isArray(o[\"slideSequences\"]) || !o[\"slideSequences\"].every(isSlideSequence))\n return false;\n }\n return true;\n}\n\nfunction missingBoundError(sceneId: string, missing: \"startTime\" | \"endTime\"): string {\n const present = missing === \"startTime\" ? \"endTime\" : \"startTime\";\n return `slide \"${sceneId}\" sets ${present} but ${missing} cannot be resolved (no scene \"${sceneId}\")`;\n}\n\n// fallow-ignore-next-line complexity\nfunction resolveTimeRange(\n ref: SlideRef,\n scene: SceneRange | undefined,\n errors: string[],\n): { start: number; end: number } {\n const { startTime, endTime, sceneId } = ref;\n\n // Both explicit — use them directly, no scene needed.\n if (startTime !== undefined && endTime !== undefined) {\n return { start: startTime, end: endTime };\n }\n\n // Neither explicit — resolve both from scene.\n if (startTime === undefined && endTime === undefined) {\n if (!scene) {\n errors.push(`slide references unresolved sceneId \"${sceneId}\"`);\n return { start: 0, end: 0 };\n }\n return { start: scene.start, end: scene.start + scene.duration };\n }\n\n // Exactly one bound explicit — fill from scene, or report a clear error.\n if (!scene) {\n const missing = startTime === undefined ? \"startTime\" : \"endTime\";\n errors.push(missingBoundError(sceneId, missing));\n const bound = startTime ?? endTime ?? 0;\n return { start: bound, end: bound };\n }\n\n return {\n start: startTime ?? scene.start,\n end: endTime ?? scene.start + scene.duration,\n };\n}\n\nfunction validateFragments(\n sceneId: string,\n fragments: number[],\n start: number,\n end: number,\n errors: string[],\n): void {\n for (const f of fragments) {\n if (f < start || f > end) {\n errors.push(`slide \"${sceneId}\" fragment ${f} is outside range [${start}, ${end}]`);\n }\n }\n}\n\nfunction resolveSlide(\n ref: SlideRef,\n sceneById: Map<string, SceneRange>,\n errors: string[],\n): ResolvedSlide {\n const scene = sceneById.get(ref.sceneId);\n const { start, end } = resolveTimeRange(ref, scene, errors);\n if (ref.startTime !== undefined && ref.endTime !== undefined && end <= start) {\n errors.push(`slide \"${ref.sceneId}\" has endTime (${end}) <= startTime (${start})`);\n }\n const fragments = [...new Set(ref.fragments ?? [])].sort((a, b) => a - b);\n validateFragments(ref.sceneId, fragments, start, end, errors);\n return { ...ref, start, end, fragments, hotspots: ref.hotspots ?? [] };\n}\n\nexport function resolveSlideshow(\n manifest: SlideshowManifest,\n scenes: SceneRange[],\n): { resolved: ResolvedSlideshow; errors: string[] } {\n const errors: string[] = [];\n const sceneById = new Map(scenes.map((s) => [s.id, s]));\n\n const sequences: Record<string, ResolvedSlideSequence> = {};\n for (const seq of manifest.slideSequences ?? []) {\n // Flag duplicate sequence ids rather than silently overwriting the earlier one.\n if (Object.prototype.hasOwnProperty.call(sequences, seq.id)) {\n errors.push(`duplicate slideSequence id \"${seq.id}\" — only the last definition is kept`);\n }\n sequences[seq.id] = {\n id: seq.id,\n label: seq.label,\n slides: seq.slides.map((s) => resolveSlide(s, sceneById, errors)),\n };\n }\n\n const slides = manifest.slides.map((s) => resolveSlide(s, sceneById, errors));\n\n // Validate hotspot targets.\n const allSlides = [...slides, ...Object.values(sequences).flatMap((s) => s.slides)];\n for (const slide of allSlides) {\n for (const h of slide.hotspots) {\n const seq = sequences[h.target];\n if (!seq) {\n errors.push(`hotspot \"${h.id}\" targets unknown sequence \"${h.target}\"`);\n } else if (seq.slides.length === 0) {\n errors.push(`hotspot \"${h.id}\" targets empty sequence \"${h.target}\"`);\n }\n }\n }\n\n // Validate no main-line overlap (sorted by start; adjacent compare).\n const ordered = [...slides].sort((a, b) => a.start - b.start);\n for (let i = 1; i < ordered.length; i++) {\n const prev = ordered[i - 1];\n const curr = ordered[i];\n if (prev !== undefined && curr !== undefined && curr.start < prev.end) {\n errors.push(`main-line slides \"${prev.sceneId}\" and \"${curr.sceneId}\" overlap`);\n }\n }\n\n return { resolved: { slides, sequences }, errors };\n}\n","// packages/core/src/slideshow/sceneId.ts\n\n/**\n * Whether a composition id names a \"scene-like\" composition — i.e. a real slide\n * scene, not the root timeline (`main`) or a non-scene overlay (captions, ambient\n * layers). Shared by the runtime scene-window computation and the slideshow lint\n * rule so the two can never drift.\n */\nexport function isSceneLikeCompositionId(compositionId: string): boolean {\n const normalized = compositionId.trim().toLowerCase();\n if (!normalized || normalized === \"main\") return false;\n if (normalized.includes(\"caption\")) return false;\n if (normalized.includes(\"ambient\")) return false;\n return true;\n}\n"],"mappings":"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","names":[]}
|
package/dist/springEase.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/springEase.ts"],"sourcesContent":["/**\n * Damped harmonic oscillator solver for GSAP CustomEase spring curves.\n *\n * Generates an SVG path data string compatible with `CustomEase.create(id, data)`.\n * The solver supports underdamped (bouncy), critically damped, and overdamped\n * spring configurations. Output is normalized to x ∈ [0,1] with y starting at 0\n * and settling to 1.\n */\n\nexport interface SpringPreset {\n name: string;\n label: string;\n mass: number;\n stiffness: number;\n damping: number;\n}\n\nexport const SPRING_PRESETS: SpringPreset[] = [\n { name: \"spring-gentle\", label: \"Gentle\", mass: 1, stiffness: 100, damping: 15 },\n { name: \"spring-bouncy\", label: \"Bouncy\", mass: 1, stiffness: 180, damping: 12 },\n { name: \"spring-stiff\", label: \"Stiff\", mass: 1, stiffness: 300, damping: 20 },\n { name: \"spring-wobbly\", label: \"Wobbly\", mass: 1, stiffness: 120, damping: 8 },\n { name: \"spring-heavy\", label: \"Heavy\", mass: 3, stiffness: 200, damping: 20 },\n];\n\n/**\n * Solve a damped harmonic oscillator and return a GSAP CustomEase data string.\n *\n * The output is an SVG path (`M0,0 L... L...`) that CustomEase.create() accepts.\n * The curve is normalized so x spans [0,1] and the spring settles at y = 1.\n *\n * @param mass - Spring mass (> 0)\n * @param stiffness - Spring stiffness constant (> 0)\n * @param damping - Damping coefficient (> 0)\n * @param steps - Number of sample points (default 120)\n */\nexport function generateSpringEaseData(\n mass: number,\n stiffness: number,\n damping: number,\n steps = 120,\n): string {\n const w0 = Math.sqrt(stiffness / mass);\n const zeta = damping / (2 * Math.sqrt(stiffness * mass));\n\n // Determine simulation duration: time until oscillation settles within threshold of 1.0.\n // Underdamped: ~5 time constants. Critically/overdamped: characteristic decay time.\n let settleDuration: number;\n if (zeta < 1) {\n settleDuration = Math.min(5 / (zeta * w0), 10);\n } else {\n const decayRate = zeta * w0 - w0 * Math.sqrt(zeta * zeta - 1);\n settleDuration = Math.min(4 / Math.max(decayRate, 0.01), 10);\n }\n const simDuration = Math.max(settleDuration, 1);\n\n const segments: string[] = [\"M0,0\"];\n\n for (let i = 1; i <= steps; i++) {\n const t = i / steps;\n const simT = t * simDuration;\n let value: number;\n\n if (zeta < 1) {\n // Underdamped — oscillates before settling\n const wd = w0 * Math.sqrt(1 - zeta * zeta);\n value =\n 1 -\n Math.exp(-zeta * w0 * simT) *\n (Math.cos(wd * simT) + ((zeta * w0) / wd) * Math.sin(wd * simT));\n } else if (zeta === 1) {\n // Critically damped — fastest approach without oscillation\n value = 1 - (1 + w0 * simT) * Math.exp(-w0 * simT);\n } else {\n // Overdamped — slow exponential approach\n const s1 = -w0 * (zeta - Math.sqrt(zeta * zeta - 1));\n const s2 = -w0 * (zeta + Math.sqrt(zeta * zeta - 1));\n value = 1 + (s1 * Math.exp(s2 * simT) - s2 * Math.exp(s1 * simT)) / (s2 - s1);\n }\n\n segments.push(`${t.toFixed(4)},${value.toFixed(4)}`);\n }\n\n // Force exact endpoint\n segments[segments.length - 1] = \"1,1\";\n\n return `${segments[0]} L${segments.slice(1).join(\" \")}`;\n}\n"],"mappings":";AAiBO,IAAM,iBAAiC;AAAA,EAC5C,EAAE,MAAM,iBAAiB,OAAO,UAAU,MAAM,GAAG,WAAW,KAAK,SAAS,GAAG;AAAA,EAC/E,EAAE,MAAM,iBAAiB,OAAO,UAAU,MAAM,GAAG,WAAW,KAAK,SAAS,GAAG;AAAA,EAC/E,EAAE,MAAM,gBAAgB,OAAO,SAAS,MAAM,GAAG,WAAW,KAAK,SAAS,GAAG;AAAA,EAC7E,EAAE,MAAM,iBAAiB,OAAO,UAAU,MAAM,GAAG,WAAW,KAAK,SAAS,EAAE;AAAA,EAC9E,EAAE,MAAM,gBAAgB,OAAO,SAAS,MAAM,GAAG,WAAW,KAAK,SAAS,GAAG;AAC/E;AAaO,SAAS,uBACd,MACA,WACA,SACA,QAAQ,KACA;AACR,QAAM,KAAK,KAAK,KAAK,YAAY,IAAI;AACrC,QAAM,OAAO,WAAW,IAAI,KAAK,KAAK,YAAY,IAAI;AAItD,MAAI;AACJ,MAAI,OAAO,GAAG;AACZ,qBAAiB,KAAK,IAAI,KAAK,OAAO,KAAK,EAAE;AAAA,EAC/C,OAAO;AACL,UAAM,YAAY,OAAO,KAAK,KAAK,KAAK,KAAK,OAAO,OAAO,CAAC;AAC5D,qBAAiB,KAAK,IAAI,IAAI,KAAK,IAAI,WAAW,IAAI,GAAG,EAAE;AAAA,EAC7D;AACA,QAAM,cAAc,KAAK,IAAI,gBAAgB,CAAC;AAE9C,QAAM,WAAqB,CAAC,MAAM;AAElC,WAAS,IAAI,GAAG,KAAK,OAAO,KAAK;AAC/B,UAAM,IAAI,IAAI;AACd,UAAM,OAAO,IAAI;AACjB,QAAI;AAEJ,QAAI,OAAO,GAAG;AAEZ,YAAM,KAAK,KAAK,KAAK,KAAK,IAAI,OAAO,IAAI;AACzC,cACE,IACA,KAAK,IAAI,CAAC,OAAO,KAAK,IAAI,KACvB,KAAK,IAAI,KAAK,IAAI,IAAM,OAAO,KAAM,KAAM,KAAK,IAAI,KAAK,IAAI;AAAA,IACpE,WAAW,SAAS,GAAG;AAErB,cAAQ,KAAK,IAAI,KAAK,QAAQ,KAAK,IAAI,CAAC,KAAK,IAAI;AAAA,IACnD,OAAO;AAEL,YAAM,KAAK,CAAC,MAAM,OAAO,KAAK,KAAK,OAAO,OAAO,CAAC;AAClD,YAAM,KAAK,CAAC,MAAM,OAAO,KAAK,KAAK,OAAO,OAAO,CAAC;AAClD,cAAQ,KAAK,KAAK,KAAK,IAAI,KAAK,IAAI,IAAI,KAAK,KAAK,IAAI,KAAK,IAAI,MAAM,KAAK;AAAA,IAC5E;AAEA,aAAS,KAAK,GAAG,EAAE,QAAQ,CAAC,CAAC,IAAI,MAAM,QAAQ,CAAC,CAAC,EAAE;AAAA,EACrD;AAGA,WAAS,SAAS,SAAS,CAAC,IAAI;AAEhC,SAAO,GAAG,SAAS,CAAC,CAAC,KAAK,SAAS,MAAM,CAAC,EAAE,KAAK,GAAG,CAAC;AACvD;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/subCompositionValidity.ts"],"sourcesContent":["/**\n * Shared \"is this sub-composition file usable?\" check.\n *\n * `data-composition-src` files are authored by AI agents far more often than\n * by humans clicking a UI. The dominant real-world failure is a scene worker\n * that dies mid-write (or a step that references a scene before writing it),\n * leaving an empty or partial `compositions/scene-*.html` on disk. Historically\n * this surfaced in three different ways depending on which code path touched\n * the file first:\n *\n * 1. A raw crash inside linkedom's `Document.head` getter — destructuring\n * `firstElementChild` off a `null` `documentElement` — when the file is\n * empty or contains no parseable markup.\n * 2. An actionable-but-late `Error` thrown deep inside the render compiler\n * (see git history: #1364), which aborted the whole render.\n * 3. A silent skip (see git history: #1678) that drops the scene from the\n * output with only a `console.warn`, producing a materially broken\n * video (missing scene, no error surfaced anywhere) with no clear\n * signal to the caller.\n *\n * This module gives every consumer (lint, render pre-flight, the tolerant\n * inliner) a single, shared definition of \"usable\" so they can never\n * disagree about whether a given file would render something. It lives in\n * `@hyperframes/parsers` (rather than `@hyperframes/core`, where it\n * originated) because `@hyperframes/lint` needs it too, and `lint` cannot\n * depend on `core` — `core` already depends on `lint` — so this shared,\n * dependency-free check lives in the common ancestor package both `core`\n * and `lint` already depend on.\n *\n * `inlineSubCompositions.ts` (in `@hyperframes/core`) intentionally stays\n * tolerant (skip + continue) for the preview/studio bundling path, where\n * partial content while iterating is expected. `lint` and the render\n * pre-flight check (`packages/producer/src/services/htmlCompiler.ts`) use\n * this helper to fail loudly and name the exact offending file, because a\n * render that silently drops a scene is strictly worse than a render that\n * refuses to start.\n */\n\nexport type SubCompositionValidityReason =\n | \"empty\"\n | \"unparsable\"\n | \"no-content\"\n | \"no-composition-root\";\n\nexport interface SubCompositionValidity {\n ok: boolean;\n /** Present when `ok` is false. */\n reason?: SubCompositionValidityReason;\n /** Human-readable detail suitable for direct inclusion in an error message. */\n detail?: string;\n}\n\n/** Minimal shape both linkedom's `Document` and `happy-dom`'s satisfy. */\nexport interface ParsableDocumentLike {\n documentElement: { outerHTML?: string } | null;\n body?: { innerHTML?: string | null } | null;\n querySelector(selector: string): { innerHTML?: string | null } | null;\n}\n\n/** Resolve authored content without invoking linkedom's synthetic body getter. */\nexport function resolveSubCompositionContent(html: string, document: ParsableDocumentLike) {\n const template = document.querySelector(\"template\");\n if (template) return { contentHtml: template.innerHTML ?? \"\", hasTemplate: true };\n\n const body = document.querySelector(\"body\");\n if (body) return { contentHtml: body.innerHTML ?? \"\", hasTemplate: false };\n\n // Browsers create a body for bare fragments; linkedom keeps their top-level nodes.\n const contentHtml = document.querySelector(\"html\") ? \"\" : html;\n return { contentHtml, hasTemplate: false };\n}\n\n/**\n * Check whether `html` (the raw file contents resolved for a\n * `data-composition-src` reference) is non-empty and parses to a document\n * that actually contains renderable content.\n *\n * Mirrors the content-detection steps in `inlineSubCompositions` exactly\n * (resolve → parse → resolve template, body, or fragment content → parse that →\n * confirm a `[data-composition-id]` root exists in it), so a file that\n * passes this check is guaranteed to produce non-empty output from the\n * inliner, and a file that fails it is guaranteed to hit one of the\n * inliner's `onMissingComposition` branches.\n *\n * @param html Raw file contents, or `null`/`undefined` if the file could not\n * be read (e.g. missing from disk). Callers should distinguish \"missing\"\n * from \"empty\" in their own error message using a separate existence\n * check — this function only inspects content.\n * @param parseHtml Parse an HTML string into a document. Pass linkedom's\n * `parseHTML(html).document` or the core bundler's `parseHTMLContent`.\n */\nexport function checkSubCompositionUsability(\n html: string | null | undefined,\n parseHtml: (html: string) => ParsableDocumentLike,\n): SubCompositionValidity {\n if (html == null || !html.trim()) {\n return {\n ok: false,\n reason: \"empty\",\n detail: \"the file is empty (0 bytes or whitespace-only)\",\n };\n }\n\n const compDoc = parseHtml(html);\n if (!compDoc.documentElement) {\n return {\n ok: false,\n reason: \"unparsable\",\n detail: \"the file's contents could not be parsed as HTML\",\n };\n }\n\n const { contentHtml } = resolveSubCompositionContent(html, compDoc);\n if (!contentHtml.trim()) {\n return {\n ok: false,\n reason: \"no-content\",\n detail: \"the file has no <template> or <body> content to render\",\n };\n }\n\n const contentDoc = parseHtml(contentHtml);\n if (!contentDoc.documentElement) {\n return {\n ok: false,\n reason: \"unparsable\",\n detail: \"the file's resolved contents could not be parsed as HTML\",\n };\n }\n\n // The content must contain an actual composition root — the element the\n // inliner looks for (`contentDoc.querySelector(\"[data-composition-id]\")`)\n // to know what to inject into the host. Well-formed but marker-free HTML\n // (e.g. an AI-authored placeholder like `<body><p>TODO</p></body>`) parses\n // fine and has non-empty content, but has nothing for the inliner to find.\n if (!contentDoc.querySelector(\"[data-composition-id]\")) {\n return {\n ok: false,\n reason: \"no-composition-root\",\n detail: \"the file's resolved content has no element with a data-composition-id attribute\",\n };\n }\n\n return { ok: true };\n}\n"],"mappings":";AA4DO,SAAS,6BAA6B,MAAc,UAAgC;AACzF,QAAM,WAAW,SAAS,cAAc,UAAU;AAClD,MAAI,SAAU,QAAO,EAAE,aAAa,SAAS,aAAa,IAAI,aAAa,KAAK;AAEhF,QAAM,OAAO,SAAS,cAAc,MAAM;AAC1C,MAAI,KAAM,QAAO,EAAE,aAAa,KAAK,aAAa,IAAI,aAAa,MAAM;AAGzE,QAAM,cAAc,SAAS,cAAc,MAAM,IAAI,KAAK;AAC1D,SAAO,EAAE,aAAa,aAAa,MAAM;AAC3C;AAqBO,SAAS,6BACd,MACA,WACwB;AACxB,MAAI,QAAQ,QAAQ,CAAC,KAAK,KAAK,GAAG;AAChC,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV;AAAA,EACF;AAEA,QAAM,UAAU,UAAU,IAAI;AAC9B,MAAI,CAAC,QAAQ,iBAAiB;AAC5B,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV;AAAA,EACF;AAEA,QAAM,EAAE,YAAY,IAAI,6BAA6B,MAAM,OAAO;AAClE,MAAI,CAAC,YAAY,KAAK,GAAG;AACvB,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV;AAAA,EACF;AAEA,QAAM,aAAa,UAAU,WAAW;AACxC,MAAI,CAAC,WAAW,iBAAiB;AAC/B,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV;AAAA,EACF;AAOA,MAAI,CAAC,WAAW,cAAc,uBAAuB,GAAG;AACtD,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV;AAAA,EACF;AAEA,SAAO,EAAE,IAAI,KAAK;AACpB;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/topLevelElements.ts"],"sourcesContent":["/** Structural view of a markup node; lint (source tags) and Studio (DOM) adapt to it. */\nexport type StructureNode<N = unknown> = {\n tag: string;\n attrs: Readonly<Record<string, string | undefined>>;\n children: readonly N[];\n};\n\nexport type TrackKind = \"video\" | \"audio\" | \"captions\" | \"graphics\";\nexport type TrackKindSource = \"attribute\" | \"tag\" | \"legacy-captions\" | \"sub-composition\";\n\nconst NON_RENDERED_TAGS = new Set([\"script\", \"style\", \"template\", \"noscript\", \"link\", \"meta\"]);\nconst MEDIA_TAGS = new Set([\"video\", \"audio\", \"img\"]);\nconst TRACK_KINDS: readonly string[] = [\"video\", \"audio\", \"captions\", \"graphics\"];\n\nexport function isSubCompositionHost(node: StructureNode): boolean {\n return (\n node.attrs[\"data-composition-src\"] !== undefined ||\n node.attrs[\"data-composition-id\"] !== undefined\n );\n}\n\n/** Mirrors the runtime's clip selector; a data-track-index layer container without a duration is not a clip. */\nexport function isTimedElement(node: StructureNode): boolean {\n const { attrs } = node;\n return (\n attrs[\"data-start\"] !== undefined ||\n (attrs[\"data-track-index\"] !== undefined && attrs[\"data-duration\"] !== undefined) ||\n MEDIA_TAGS.has(node.tag.toLowerCase())\n );\n}\n\n/**\n * The elements that own a timeline row: a sub-composition host or timed element is one row\n * and is never descended into; untimed wrappers are descended. The root itself is not a row.\n */\nexport function topLevelElements<N extends StructureNode<N>>(root: N): N[] {\n const rows: N[] = [];\n const visit = (node: N): void => {\n if (NON_RENDERED_TAGS.has(node.tag.toLowerCase())) return;\n if (isSubCompositionHost(node) || isTimedElement(node)) rows.push(node);\n else node.children.forEach(visit);\n };\n root.children.forEach(visit);\n return rows;\n}\n\nexport function trackKindOf(node: StructureNode): { kind: TrackKind; source: TrackKindSource } {\n const explicit = node.attrs[\"data-track-kind\"]?.trim().toLowerCase();\n if (explicit && TRACK_KINDS.includes(explicit))\n return { kind: explicit as TrackKind, source: \"attribute\" };\n const tag = node.tag.toLowerCase();\n if (tag === \"video\" || tag === \"audio\") return { kind: tag, source: \"tag\" };\n if (isSubCompositionHost(node)) {\n const legacy =\n node.attrs[\"data-composition-id\"] === \"captions\" ||\n /(?:^|\\s)caption[-_]/.test(node.attrs.class ?? \"\");\n return legacy\n ? { kind: \"captions\", source: \"legacy-captions\" }\n : { kind: \"graphics\", source: \"sub-composition\" };\n }\n return { kind: \"graphics\", source: \"tag\" };\n}\n"],"mappings":";AAUA,IAAM,oBAAoB,oBAAI,IAAI,CAAC,UAAU,SAAS,YAAY,YAAY,QAAQ,MAAM,CAAC;AAC7F,IAAM,aAAa,oBAAI,IAAI,CAAC,SAAS,SAAS,KAAK,CAAC;AACpD,IAAM,cAAiC,CAAC,SAAS,SAAS,YAAY,UAAU;AAEzE,SAAS,qBAAqB,MAA8B;AACjE,SACE,KAAK,MAAM,sBAAsB,MAAM,UACvC,KAAK,MAAM,qBAAqB,MAAM;AAE1C;AAGO,SAAS,eAAe,MAA8B;AAC3D,QAAM,EAAE,MAAM,IAAI;AAClB,SACE,MAAM,YAAY,MAAM,UACvB,MAAM,kBAAkB,MAAM,UAAa,MAAM,eAAe,MAAM,UACvE,WAAW,IAAI,KAAK,IAAI,YAAY,CAAC;AAEzC;AAMO,SAAS,iBAA6C,MAAc;AACzE,QAAM,OAAY,CAAC;AACnB,QAAM,QAAQ,CAAC,SAAkB;AAC/B,QAAI,kBAAkB,IAAI,KAAK,IAAI,YAAY,CAAC,EAAG;AACnD,QAAI,qBAAqB,IAAI,KAAK,eAAe,IAAI,EAAG,MAAK,KAAK,IAAI;AAAA,QACjE,MAAK,SAAS,QAAQ,KAAK;AAAA,EAClC;AACA,OAAK,SAAS,QAAQ,KAAK;AAC3B,SAAO;AACT;AAEO,SAAS,YAAY,MAAmE;AAC7F,QAAM,WAAW,KAAK,MAAM,iBAAiB,GAAG,KAAK,EAAE,YAAY;AACnE,MAAI,YAAY,YAAY,SAAS,QAAQ;AAC3C,WAAO,EAAE,MAAM,UAAuB,QAAQ,YAAY;AAC5D,QAAM,MAAM,KAAK,IAAI,YAAY;AACjC,MAAI,QAAQ,WAAW,QAAQ,QAAS,QAAO,EAAE,MAAM,KAAK,QAAQ,MAAM;AAC1E,MAAI,qBAAqB,IAAI,GAAG;AAC9B,UAAM,SACJ,KAAK,MAAM,qBAAqB,MAAM,cACtC,sBAAsB,KAAK,KAAK,MAAM,SAAS,EAAE;AACnD,WAAO,SACH,EAAE,MAAM,YAAY,QAAQ,kBAAkB,IAC9C,EAAE,MAAM,YAAY,QAAQ,kBAAkB;AAAA,EACpD;AACA,SAAO,EAAE,MAAM,YAAY,QAAQ,MAAM;AAC3C;","names":[]}
|