@hypit/caption-fine 0.0.0-stage → 0.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/dist/style.js ADDED
@@ -0,0 +1,340 @@
1
+ import { sealCaptionStyle } from "@hypit/hypit/caption";
2
+ import { assertFontArtifactRef } from "@hypit/hypit/media";
3
+ import { canonicalStringify } from "@hypit/hypit/protocol";
4
+ import { fineCaptionOneShotMotions, fineCaptionEditableDefaults, fineCaptionOptionalRecipeProperties, fineCaptionRequiredRecipeProperties, } from "./recipe.js";
5
+ export const FINE_CAPTION_FAMILY = "@hypit/caption-fine@1";
6
+ const ALLOWED_PROPERTIES = new Set([
7
+ ...fineCaptionRequiredRecipeProperties,
8
+ ...fineCaptionOptionalRecipeProperties,
9
+ ]);
10
+ function required(value, name) {
11
+ if (!Object.hasOwn(value.properties, name))
12
+ throw new Error(`Fine Caption Recipe requires ${name}`);
13
+ return value.properties[name];
14
+ }
15
+ function number(value, name, fallback) {
16
+ const property = Object.hasOwn(value.properties, name) ? value.properties[name] : fallback;
17
+ if (typeof property !== "number" || !Number.isFinite(property)) {
18
+ throw new Error(`Fine Caption Recipe ${name} must be a number`);
19
+ }
20
+ return property;
21
+ }
22
+ function integer(value, name, fallback) {
23
+ const property = number(value, name, fallback);
24
+ if (!Number.isSafeInteger(property))
25
+ throw new Error(`Fine Caption Recipe ${name} must be an integer`);
26
+ return property;
27
+ }
28
+ function string(value, name, fallback) {
29
+ const property = Object.hasOwn(value.properties, name) ? value.properties[name] : fallback;
30
+ if (typeof property !== "string" || !property.trim()) {
31
+ throw new Error(`Fine Caption Recipe ${name} must be a string`);
32
+ }
33
+ return property.trim();
34
+ }
35
+ function choice(value, name, choices, fallback) {
36
+ const property = string(value, name, fallback);
37
+ if (!choices.includes(property))
38
+ throw new Error(`Fine Caption Recipe ${name} is invalid`);
39
+ return property;
40
+ }
41
+ function color(value, name, fallback) {
42
+ const property = string(value, name, fallback);
43
+ if (!/^#[0-9a-f]{6}(?:[0-9a-f]{2})?$/iu.test(property)) {
44
+ throw new Error(`Fine Caption Recipe ${name} must be an RGB or RGBA hex color`);
45
+ }
46
+ return property;
47
+ }
48
+ function padding(value) {
49
+ const parts = value.trim().split(/\s+/u).map(Number);
50
+ if ((parts.length !== 1 && parts.length !== 2) || parts.some((item) => !Number.isFinite(item) || item < 0)) {
51
+ throw new Error("Fine Caption Recipe padding must contain one or two non-negative pixel numbers");
52
+ }
53
+ return { y: parts[0], x: parts[1] ?? parts[0] };
54
+ }
55
+ function optionalGradient(recipe, prefix, base) {
56
+ const fromName = `${prefix}gradient-from`;
57
+ const toName = `${prefix}gradient-to`;
58
+ const hasFrom = Object.hasOwn(recipe.properties, fromName);
59
+ const hasTo = Object.hasOwn(recipe.properties, toName);
60
+ const hasAngle = Object.hasOwn(recipe.properties, `${prefix}gradient-angle`);
61
+ if (!hasFrom && !hasTo && !hasAngle)
62
+ return prefix === "active-" ? undefined : base;
63
+ if (!hasFrom || !hasTo) {
64
+ throw new Error(`Fine Caption Recipe ${prefix}gradient requires both from and to colors`);
65
+ }
66
+ return {
67
+ from: color(recipe, fromName),
68
+ to: color(recipe, toName),
69
+ angleDeg: number(recipe, `${prefix}gradient-angle`, base?.angleDeg ?? 90),
70
+ };
71
+ }
72
+ function glyphPaint(recipe, prefix, base) {
73
+ const fallback = (key) => base?.[key];
74
+ const baseShadow = fallback("shadow");
75
+ const baseGlow = fallback("glow");
76
+ const baseStroke = fallback("stroke");
77
+ const baseLongShadow = fallback("longShadow");
78
+ const gradient = optionalGradient(recipe, prefix, base?.gradient);
79
+ return {
80
+ fill: color(recipe, `${prefix}fill`, prefix === "active-" ? "#FFD54A" : base?.fill),
81
+ ...(gradient === undefined ? {} : { gradient }),
82
+ opacity: number(recipe, `${prefix}opacity`, base?.opacity ?? fineCaptionEditableDefaults.opacity),
83
+ stroke: {
84
+ color: color(recipe, `${prefix}stroke-color`, baseStroke?.color ?? "#000000"),
85
+ widthPx: number(recipe, `${prefix}stroke-width`, baseStroke?.widthPx ?? fineCaptionEditableDefaults["stroke-width"]),
86
+ },
87
+ shadow: {
88
+ color: color(recipe, `${prefix}shadow-color`, baseShadow?.color ?? "#000000"),
89
+ opacity: number(recipe, `${prefix}shadow-opacity`, baseShadow?.opacity ?? fineCaptionEditableDefaults["shadow-opacity"]),
90
+ offsetXPx: number(recipe, `${prefix}shadow-x`, baseShadow?.offsetXPx ?? 0),
91
+ offsetYPx: number(recipe, `${prefix}shadow-y`, baseShadow?.offsetYPx ?? 0),
92
+ blurPx: number(recipe, `${prefix}shadow-blur`, baseShadow?.blurPx ?? 0),
93
+ spreadPx: number(recipe, `${prefix}shadow-spread`, baseShadow?.spreadPx ?? 0),
94
+ },
95
+ longShadow: {
96
+ color: color(recipe, `${prefix}long-shadow-color`, baseLongShadow?.color ?? "#000000"),
97
+ opacity: number(recipe, `${prefix}long-shadow-opacity`, baseLongShadow?.opacity ?? 0),
98
+ distancePx: number(recipe, `${prefix}long-shadow-distance`, baseLongShadow?.distancePx ?? 0),
99
+ angleDeg: number(recipe, `${prefix}long-shadow-angle`, baseLongShadow?.angleDeg ?? 45),
100
+ },
101
+ glow: {
102
+ color: color(recipe, `${prefix}glow-color`, baseGlow?.color ?? "#FFFFFF"),
103
+ opacity: number(recipe, `${prefix}glow-opacity`, baseGlow?.opacity ?? fineCaptionEditableDefaults["glow-opacity"]),
104
+ blurPx: number(recipe, `${prefix}glow-blur`, baseGlow?.blurPx ?? 0),
105
+ spreadPx: number(recipe, `${prefix}glow-spread`, baseGlow?.spreadPx ?? 0),
106
+ },
107
+ };
108
+ }
109
+ function assertColor(value, label) {
110
+ if (!/^#[0-9a-f]{6}(?:[0-9a-f]{2})?$/iu.test(value))
111
+ throw new Error(`${label} is invalid`);
112
+ }
113
+ function assertOpacity(value, label) {
114
+ if (!Number.isFinite(value) || value < 0 || value > 1)
115
+ throw new Error(`${label} must be between 0 and 1`);
116
+ }
117
+ function assertPaint(value, label) {
118
+ assertColor(value.fill, `${label} fill`);
119
+ assertOpacity(value.opacity, `${label} opacity`);
120
+ assertColor(value.stroke.color, `${label} stroke color`);
121
+ assertColor(value.shadow.color, `${label} shadow color`);
122
+ assertColor(value.longShadow.color, `${label} long shadow color`);
123
+ assertColor(value.glow.color, `${label} glow color`);
124
+ assertOpacity(value.shadow.opacity, `${label} shadow opacity`);
125
+ assertOpacity(value.longShadow.opacity, `${label} long shadow opacity`);
126
+ assertOpacity(value.glow.opacity, `${label} glow opacity`);
127
+ for (const [name, metric] of [
128
+ ["stroke width", value.stroke.widthPx], ["shadow blur", value.shadow.blurPx],
129
+ ["long shadow distance", value.longShadow.distancePx], ["glow blur", value.glow.blurPx],
130
+ ["glow spread", value.glow.spreadPx],
131
+ ]) {
132
+ if (!Number.isFinite(metric) || metric < 0)
133
+ throw new Error(`${label} ${name} is invalid`);
134
+ }
135
+ if (!Number.isFinite(value.shadow.offsetXPx) || !Number.isFinite(value.shadow.offsetYPx)
136
+ || !Number.isFinite(value.shadow.spreadPx)) {
137
+ throw new Error(`${label} shadow offset is invalid`);
138
+ }
139
+ if (!Number.isFinite(value.longShadow.angleDeg))
140
+ throw new Error(`${label} long shadow angle is invalid`);
141
+ if (value.gradient !== undefined) {
142
+ assertColor(value.gradient.from, `${label} gradient from`);
143
+ assertColor(value.gradient.to, `${label} gradient to`);
144
+ if (!Number.isFinite(value.gradient.angleDeg))
145
+ throw new Error(`${label} gradient angle is invalid`);
146
+ }
147
+ }
148
+ export function fineCaptionParameters(recipe, exactFonts) {
149
+ for (const name of fineCaptionRequiredRecipeProperties)
150
+ required(recipe, name);
151
+ const unknown = Object.keys(recipe.properties).filter((name) => !ALLOWED_PROPERTIES.has(name));
152
+ if (unknown.length > 0)
153
+ throw new Error(`Fine Caption Recipe contains unknown property ${unknown[0]}`);
154
+ const pad = padding(string(recipe, "padding"));
155
+ const activeBoxPadding = padding(string(recipe, "active-box-padding", "0"));
156
+ const fontSizePx = number(recipe, "size");
157
+ const basePaint = glyphPaint(recipe, "");
158
+ const cueEnter = choice(recipe, "cue-enter", fineCaptionOneShotMotions, "none");
159
+ const parameters = {
160
+ stackingOrder: integer(recipe, "stack-order"),
161
+ placement: {
162
+ x: number(recipe, "x"),
163
+ y: number(recipe, "y"),
164
+ width: number(recipe, "width"),
165
+ ...(Object.hasOwn(recipe.properties, "height") ? { height: number(recipe, "height") } : {}),
166
+ anchorX: choice(recipe, "anchor-x", ["left", "center", "right"], "left"),
167
+ anchorY: choice(recipe, "anchor-y", ["top", "center", "bottom"], "top"),
168
+ },
169
+ layout: {
170
+ textAlign: choice(recipe, "align", ["left", "center", "right"]),
171
+ blockAlign: choice(recipe, "block-align", ["start", "center", "end"], "center"),
172
+ direction: choice(recipe, "direction", ["ltr", "rtl"], "ltr"),
173
+ inlineSize: choice(recipe, "inline-size", ["hug", "fixed"], "hug"),
174
+ wrap: choice(recipe, "wrap", ["word", "grapheme"], "word"),
175
+ ...(Object.hasOwn(recipe.properties, "max-lines") ? { maxLines: integer(recipe, "max-lines") } : {}),
176
+ ...(Object.hasOwn(recipe.properties, "max-words-per-line")
177
+ ? { maxWordsPerLine: integer(recipe, "max-words-per-line") } : {}),
178
+ lineHeight: number(recipe, "line-height"),
179
+ letterSpacingPx: number(recipe, "letter-spacing", 0),
180
+ wordGapPx: number(recipe, "word-gap", fontSizePx * 0.25),
181
+ },
182
+ typography: {
183
+ fontSizePx,
184
+ kerning: choice(recipe, "kerning", ["auto", "normal", "none"], "auto"),
185
+ variantCaps: choice(recipe, "caps", ["normal", "small-caps", "all-small-caps"], "normal"),
186
+ textTransform: choice(recipe, "text-transform", ["none", "uppercase", "lowercase", "capitalize"], "none"),
187
+ exactFonts: [...exactFonts],
188
+ },
189
+ basePaint,
190
+ activePaint: glyphPaint(recipe, "active-", basePaint),
191
+ underline: {
192
+ mode: choice(recipe, "underline", ["off", "always"], fineCaptionEditableDefaults["underline"]),
193
+ color: color(recipe, "underline-color", basePaint.fill),
194
+ thicknessPx: number(recipe, "underline-thickness", 2),
195
+ offsetPx: number(recipe, "underline-offset", 4),
196
+ },
197
+ activeUnderline: {
198
+ mode: choice(recipe, "active-underline", ["off", "current", "trail"], fineCaptionEditableDefaults["active-underline"]),
199
+ color: color(recipe, "active-underline-color", "#FFD54A"),
200
+ thicknessPx: number(recipe, "active-underline-thickness", 3),
201
+ offsetPx: number(recipe, "active-underline-offset", 4),
202
+ },
203
+ cueBox: {
204
+ background: color(recipe, "background"),
205
+ borderColor: color(recipe, "border-color", "#00000000"),
206
+ borderWidthPx: number(recipe, "border-width", 0),
207
+ paddingXPx: pad.x,
208
+ paddingYPx: pad.y,
209
+ radiusPx: number(recipe, "radius"),
210
+ shadow: {
211
+ color: color(recipe, "cue-shadow-color", "#000000"),
212
+ opacity: number(recipe, "cue-shadow-opacity", 0),
213
+ offsetXPx: number(recipe, "cue-shadow-x", 0),
214
+ offsetYPx: number(recipe, "cue-shadow-y", 0),
215
+ blurPx: number(recipe, "cue-shadow-blur", 0),
216
+ spreadPx: number(recipe, "cue-shadow-spread", 0),
217
+ },
218
+ },
219
+ karaoke: {
220
+ mode: choice(recipe, "karaoke", ["off", "current", "trail"], fineCaptionEditableDefaults["karaoke"]),
221
+ transition: choice(recipe, "karaoke-transition", ["step", "wipe"], fineCaptionEditableDefaults["karaoke-transition"]),
222
+ },
223
+ activeBox: {
224
+ mode: choice(recipe, "active-box", ["off", "current", "trail"], fineCaptionEditableDefaults["active-box"]),
225
+ continuity: choice(recipe, "active-box-continuity", ["isolated", "joined"], "isolated"),
226
+ background: color(recipe, "active-box-background", "#FFD54A"),
227
+ borderColor: color(recipe, "active-box-border-color", "#00000000"),
228
+ borderWidthPx: number(recipe, "active-box-border-width", 0),
229
+ paddingXPx: activeBoxPadding.x,
230
+ paddingYPx: activeBoxPadding.y,
231
+ radiusPx: number(recipe, "active-box-radius", 8),
232
+ enter: choice(recipe, "active-box-enter", fineCaptionOneShotMotions, "none"),
233
+ exit: choice(recipe, "active-box-exit", fineCaptionOneShotMotions, "none"),
234
+ transitionFrames: integer(recipe, "active-box-transition-frames", 0),
235
+ },
236
+ motion: {
237
+ cueEnter,
238
+ cueExit: choice(recipe, "cue-exit", fineCaptionOneShotMotions, "none"),
239
+ cueEnterFrames: integer(recipe, "cue-enter-frames", 0),
240
+ cueExitFrames: integer(recipe, "cue-exit-frames", 0),
241
+ ...(Object.hasOwn(recipe.properties, "cue-enter-start-scale")
242
+ ? { cueEnterStartScale: number(recipe, "cue-enter-start-scale") } : {}),
243
+ atomEnter: choice(recipe, "atom-enter", fineCaptionOneShotMotions, "none"),
244
+ atomEnterFrames: integer(recipe, "atom-enter-frames", 0),
245
+ atomExit: choice(recipe, "atom-exit", fineCaptionOneShotMotions, "none"),
246
+ atomExitFrames: integer(recipe, "atom-exit-frames", 0),
247
+ atomReveal: choice(recipe, "atom-reveal", ["all", "on-start", "typewriter"], "all"),
248
+ activeResponse: choice(recipe, "active-response", fineCaptionOneShotMotions, "none"),
249
+ activeResponseFrames: integer(recipe, "active-response-frames", 6),
250
+ activeScale: number(recipe, "active-scale", 1.08),
251
+ slideDistancePx: number(recipe, "slide-distance", 24),
252
+ loop: choice(recipe, "loop", ["none", "shake", "wobble", "glow-pulse", "breathe", "float", "pulse", "flicker"], "none"),
253
+ loopTarget: choice(recipe, "loop-target", ["cue", "active-atom"], "cue"),
254
+ loopPeriodFrames: integer(recipe, "loop-period-frames", 12),
255
+ loopIntensity: number(recipe, "loop-intensity", 1),
256
+ },
257
+ timing: {
258
+ leadFrames: integer(recipe, "lead-frames", 0),
259
+ tailFrames: integer(recipe, "tail-frames", 0),
260
+ handoff: choice(recipe, "handoff", ["cut", "overlap"], "cut"),
261
+ },
262
+ };
263
+ assertFineCaptionParameters(parameters);
264
+ return parameters;
265
+ }
266
+ export function assertFineCaptionParameters(value) {
267
+ if (!Number.isSafeInteger(value.stackingOrder) || value.stackingOrder < 0) {
268
+ throw new Error("Fine Caption stacking order is invalid");
269
+ }
270
+ const nonNegative = [
271
+ value.placement.x, value.placement.y, value.placement.width, value.typography.fontSizePx,
272
+ value.layout.lineHeight, value.layout.wordGapPx, value.cueBox.paddingXPx,
273
+ value.cueBox.paddingYPx, value.cueBox.radiusPx, value.cueBox.borderWidthPx,
274
+ value.underline.thicknessPx, value.underline.offsetPx, value.activeUnderline.thicknessPx,
275
+ value.activeUnderline.offsetPx, value.activeBox.borderWidthPx, value.activeBox.paddingXPx,
276
+ value.activeBox.paddingYPx, value.activeBox.radiusPx, value.activeBox.transitionFrames,
277
+ value.motion.cueEnterFrames, value.motion.cueExitFrames, value.motion.atomEnterFrames, value.motion.atomExitFrames,
278
+ value.motion.activeResponseFrames,
279
+ value.motion.slideDistancePx, value.motion.loopPeriodFrames, value.motion.loopIntensity,
280
+ value.timing.leadFrames, value.timing.tailFrames,
281
+ value.cueBox.shadow.blurPx,
282
+ ];
283
+ if (nonNegative.some((item) => !Number.isFinite(item) || item < 0)
284
+ || value.placement.x > 1 || value.placement.y > 1 || value.placement.width <= 0 || value.placement.width > 1
285
+ || (value.placement.height !== undefined && (!Number.isFinite(value.placement.height) || value.placement.height <= 0 || value.placement.height > 1))
286
+ || value.typography.fontSizePx <= 0 || value.layout.lineHeight <= 0
287
+ || !Number.isSafeInteger(value.motion.cueEnterFrames) || !Number.isSafeInteger(value.motion.cueExitFrames)
288
+ || !Number.isSafeInteger(value.motion.atomEnterFrames) || !Number.isSafeInteger(value.motion.atomExitFrames)
289
+ || !Number.isSafeInteger(value.motion.loopPeriodFrames)
290
+ || !Number.isSafeInteger(value.motion.activeResponseFrames)
291
+ || !Number.isSafeInteger(value.activeBox.transitionFrames) || value.motion.loopPeriodFrames <= 0
292
+ || !Number.isSafeInteger(value.timing.leadFrames) || !Number.isSafeInteger(value.timing.tailFrames)
293
+ || (value.layout.maxLines !== undefined && (!Number.isSafeInteger(value.layout.maxLines) || value.layout.maxLines <= 0))
294
+ || (value.layout.maxWordsPerLine !== undefined && (!Number.isSafeInteger(value.layout.maxWordsPerLine) || value.layout.maxWordsPerLine <= 0))
295
+ || (value.motion.cueEnterStartScale !== undefined
296
+ && (!Number.isFinite(value.motion.cueEnterStartScale) || value.motion.cueEnterStartScale < 0))
297
+ || value.motion.activeScale <= 0
298
+ || !Number.isFinite(value.motion.activeScale) || !Number.isFinite(value.layout.letterSpacingPx)) {
299
+ throw new Error("Fine Caption parameters contain invalid numeric bounds");
300
+ }
301
+ if (value.motion.cueEnter === "none"
302
+ && value.motion.cueEnterStartScale !== undefined) {
303
+ throw new Error("Fine Caption Cue entrance modifiers require a Cue entrance motion");
304
+ }
305
+ if (value.typography.exactFonts.length === 0)
306
+ throw new Error("Fine Caption exact Font stack is empty");
307
+ const faces = new Set();
308
+ for (const [index, font] of value.typography.exactFonts.entries()) {
309
+ assertFontArtifactRef(font, `Fine Caption exact Font ${index + 1}`);
310
+ const identity = canonicalStringify(font);
311
+ if (faces.has(identity))
312
+ throw new Error("Fine Caption exact Font stack contains a duplicate face");
313
+ faces.add(identity);
314
+ }
315
+ if (value.layout.maxLines !== undefined && value.layout.maxWordsPerLine === undefined) {
316
+ throw new Error("Fine Caption max-lines requires max-words-per-line so its rows are structural");
317
+ }
318
+ assertColor(value.cueBox.background, "Fine Caption background");
319
+ assertColor(value.cueBox.borderColor, "Fine Caption border color");
320
+ assertColor(value.cueBox.shadow.color, "Fine Caption Cue shadow color");
321
+ assertOpacity(value.cueBox.shadow.opacity, "Fine Caption Cue shadow opacity");
322
+ if (![value.cueBox.shadow.offsetXPx, value.cueBox.shadow.offsetYPx, value.cueBox.shadow.spreadPx]
323
+ .every(Number.isFinite))
324
+ throw new Error("Fine Caption Cue shadow contains an invalid metric");
325
+ assertColor(value.underline.color, "Fine Caption underline color");
326
+ assertColor(value.activeUnderline.color, "Fine Caption active underline color");
327
+ assertColor(value.activeBox.background, "Fine Caption active box background");
328
+ assertColor(value.activeBox.borderColor, "Fine Caption active box border color");
329
+ assertPaint(value.basePaint, "Fine Caption base Paint");
330
+ assertPaint(value.activePaint, "Fine Caption active Paint");
331
+ }
332
+ export function fineCaptionStyle(id, recipe, exactFonts) {
333
+ return sealCaptionStyle({
334
+ id,
335
+ rendering: {
336
+ family: FINE_CAPTION_FAMILY,
337
+ parameters: fineCaptionParameters(recipe, exactFonts),
338
+ },
339
+ });
340
+ }
@@ -0,0 +1,3 @@
1
+ import type { StructuredSurfaceHandler } from "@hypit/hypit/markup";
2
+ export declare const decodeFineCaptionStyleSurface: StructuredSurfaceHandler;
3
+ export declare const decodeFineCaptionTrackSurface: StructuredSurfaceHandler;
@@ -0,0 +1,171 @@
1
+ import { captionProducers, captionTypes } from "@hypit/hypit/caption";
2
+ import { assertFontArtifactRef, assertFontStackRef, mediaTypes } from "@hypit/hypit/media";
3
+ import { timelineTypes } from "@hypit/hypit/timeline";
4
+ import { regionEvidenceTypes } from "@hypit/hypit/region-evidence";
5
+ import { spatialTypes } from "@hypit/hypit/spatial";
6
+ import { recipeType } from "@hypit/hypit/recipe";
7
+ import { sealGraphFragment } from "@hypit/hypit/author";
8
+ import { assertEmptyElement, optionalTextAttribute } from "@hypit/hypit/markup";
9
+ import { resolveTemporalContext, resolveTemporalWindowReference, temporalWindowAttributeNames } from "@hypit/hypit/temporal/markup";
10
+ import { temporalTypes } from "@hypit/hypit/temporal";
11
+ import { compositionTypes } from "@hypit/hypit/composition";
12
+ import { captionFineProducers, captionFineTypes } from "./manifest.js";
13
+ const input = (name) => ({ kind: "fragment-input", name });
14
+ const operation = (id) => ({ kind: "fragment-operation", operation: id });
15
+ import { fineCaptionStyle } from "./style.js";
16
+ function sameType(left, right) {
17
+ return left.module.name === right.module.name && left.module.version === right.module.version && left.name === right.name;
18
+ }
19
+ function attributes(element, required, optional = []) {
20
+ const actual = Object.keys(element.attributes);
21
+ const allowed = new Set([...required, ...optional]);
22
+ if (required.some((name) => element.attributes[name] === undefined) || actual.some((name) => !allowed.has(name))) {
23
+ const suffix = optional.length === 0 ? "" : `, with optional ${optional.join(", ")}`;
24
+ throw new Error(`${element.name} requires ${required.join(", ")}${suffix}`);
25
+ }
26
+ }
27
+ function stringAttribute(element, name) {
28
+ const value = element.attributes[name];
29
+ if (typeof value !== "string" || !value.trim())
30
+ throw new Error(`${element.name}.${name} must be a non-empty string`);
31
+ return value.trim();
32
+ }
33
+ function reference(element, name, expected, resolveReference) {
34
+ const raw = element.attributes[name];
35
+ if (typeof raw !== "object" || raw.kind !== "reference") {
36
+ throw new Error(`${element.name}.${name} must be a whole-value reference`);
37
+ }
38
+ const value = resolveReference(raw.path);
39
+ if (value === undefined)
40
+ throw new Error(`${element.name}.${name} cannot resolve ${raw.path}`);
41
+ if (!sameType(value.type, expected))
42
+ throw new Error(`${element.name}.${name} has the wrong type`);
43
+ return value;
44
+ }
45
+ function inline(referenceValue, subject) {
46
+ if (referenceValue.record?.value.kind !== "inline")
47
+ throw new Error(`${subject} must reference an authored inline Record`);
48
+ return referenceValue.record.value.value;
49
+ }
50
+ function localName(name) {
51
+ const colon = name.lastIndexOf(":");
52
+ return colon < 0 ? name : name.slice(colon + 1);
53
+ }
54
+ function exactFonts(element, resolveReference) {
55
+ const result = [];
56
+ const primary = element.attributes.font;
57
+ if (primary === undefined)
58
+ throw new Error(`${element.name} requires font`);
59
+ if (typeof primary !== "object" || primary.kind !== "reference") {
60
+ throw new Error(`${element.name}.font must be a whole-value reference`);
61
+ }
62
+ const resolved = resolveReference(primary.path);
63
+ if (resolved === undefined)
64
+ throw new Error(`${element.name}.font cannot resolve ${primary.path}`);
65
+ if (sameType(resolved.type, mediaTypes.fontArtifact)) {
66
+ result.push(inline(resolved, `${element.name}.font`));
67
+ }
68
+ else if (sameType(resolved.type, mediaTypes.fontStack)) {
69
+ const stack = inline(resolved, `${element.name}.font`);
70
+ assertFontStackRef(stack, `${element.name}.font`);
71
+ result.push(...stack.faces);
72
+ }
73
+ else {
74
+ throw new Error(`${element.name}.font has the wrong type`);
75
+ }
76
+ for (const child of element.children) {
77
+ if (child.kind === "text") {
78
+ if (child.value.trim())
79
+ throw new Error(`${element.name} accepts only Fallback children`);
80
+ continue;
81
+ }
82
+ if (localName(child.name) !== "Fallback")
83
+ throw new Error(`${element.name} accepts only Fallback children`);
84
+ attributes(child, ["font"]);
85
+ if (child.children.some((nested) => nested.kind === "element" || nested.value.trim())) {
86
+ throw new Error(`${child.name} does not accept children`);
87
+ }
88
+ result.push(inline(reference(child, "font", mediaTypes.fontArtifact, resolveReference), `${child.name}.font`));
89
+ }
90
+ for (const [index, font] of result.entries())
91
+ assertFontArtifactRef(font, `${element.name}.font.${index + 1}`);
92
+ return result;
93
+ }
94
+ export const decodeFineCaptionStyleSurface = ({ element, resolveReference }) => {
95
+ attributes(element, ["id", "recipe", "font"]);
96
+ const id = stringAttribute(element, "id");
97
+ const recipe = inline(reference(element, "recipe", recipeType, resolveReference), `${element.name}.recipe`);
98
+ const style = fineCaptionStyle(id, recipe, exactFonts(element, resolveReference));
99
+ return {
100
+ records: [{ id, type: captionTypes.style, value: { kind: "inline", value: style }, range: element.range }],
101
+ components: [],
102
+ fragments: [],
103
+ };
104
+ };
105
+ export const decodeFineCaptionTrackSurface = ({ element, resolveReference }) => {
106
+ attributes(element, ["id", "document", "timing", "timeline", "within"], ["regions"]);
107
+ const id = stringAttribute(element, "id");
108
+ const document = reference(element, "document", captionTypes.document, resolveReference);
109
+ const timing = reference(element, "timing", captionTypes.timing, resolveReference);
110
+ const within = reference(element, "within", spatialTypes.frame, resolveReference);
111
+ const context = resolveTemporalContext({ element, resolveReference });
112
+ const regions = element.attributes.regions === undefined ? undefined : reference(element, "regions", regionEvidenceTypes.evidence, resolveReference);
113
+ const records = [{ id: `${id}.header`, type: captionTypes.header,
114
+ value: { kind: "inline", value: { id } }, range: element.range }];
115
+ const components = [];
116
+ const fragments = [];
117
+ const inputs = [{ name: "document", type: captionTypes.document }, { name: "timing", type: captionTypes.timing }, { name: "timeline", type: timelineTypes.timeline }, { name: "within", type: spatialTypes.frame }, { name: "header", type: captionTypes.header }];
118
+ const bindings = { document: document.ref, timing: timing.ref, timeline: context.timeline.ref, within: within.ref, header: { kind: "record", id: `${id}.header` } };
119
+ const operations = [{ id: "create", producer: captionProducers.create,
120
+ inputs: { document: input("document"), header: input("header") }, result: { kind: "output", name: "program" } }];
121
+ let previous = "create", index = 0;
122
+ for (const child of element.children) {
123
+ if (child.kind === "text") {
124
+ if (child.value.trim())
125
+ throw new Error("Caption accepts Use children.");
126
+ continue;
127
+ }
128
+ if (localName(child.name) !== "Use")
129
+ throw new Error("Caption accepts Use children.");
130
+ attributes(child, ["style"], ["id", "role", ...temporalWindowAttributeNames]);
131
+ assertEmptyElement(child);
132
+ index += 1;
133
+ const useId = optionalTextAttribute(child, "id") ?? `${id}.use.${index}`;
134
+ const window = child.attributes.during === undefined
135
+ ? undefined
136
+ : resolveTemporalWindowReference({ element: child, resolveReference });
137
+ const style = reference(child, "style", captionTypes.style, resolveReference);
138
+ const role = optionalTextAttribute(child, "role");
139
+ const filterId = `${useId}.filter`;
140
+ records.push({ id: filterId, type: captionTypes.filter, value: { kind: "inline", value: {
141
+ id: useId, ...(role === undefined ? {} : { role }),
142
+ } }, range: child.range });
143
+ const key = `use-${index}`;
144
+ inputs.push(...(window === undefined ? [] : [{ name: `${key}-window`, type: temporalTypes.window }]), { name: `${key}-style`, type: captionTypes.style }, { name: `${key}-filter`, type: captionTypes.filter });
145
+ if (window !== undefined)
146
+ bindings[`${key}-window`] = window.ref;
147
+ bindings[`${key}-style`] = style.ref;
148
+ bindings[`${key}-filter`] = { kind: "record", id: filterId };
149
+ operations.push({ id: key, producer: window === undefined ? captionProducers.appendUnbounded : captionProducers.append,
150
+ inputs: { program: operation(previous), ...(window === undefined ? {} : { window: input(`${key}-window`) }),
151
+ style: input(`${key}-style`), filter: input(`${key}-filter`) }, result: { kind: "output", name: "program" } });
152
+ previous = key;
153
+ }
154
+ operations.push({ id: "schedule", producer: captionFineProducers.schedule,
155
+ inputs: { timing: input("timing"), document: input("document"), program: operation(previous) }, result: { kind: "output", name: "schedule" } });
156
+ if (regions !== undefined) {
157
+ inputs.push({ name: "regions", type: regionEvidenceTypes.evidence });
158
+ bindings.regions = regions.ref;
159
+ }
160
+ operations.push({ id: "render", producer: regions === undefined ? captionFineProducers.render : captionFineProducers.renderWithRegions,
161
+ inputs: { schedule: operation("schedule"), document: input("document"), timeline: input("timeline"), within: input("within"), program: operation(previous), ...(regions === undefined ? {} : { regions: input("regions") }) }, result: { kind: "output", name: "track" } });
162
+ const collector = sealGraphFragment({ inputs, operations, exports: [
163
+ { name: "program", type: captionTypes.program, root: operation(previous) },
164
+ { name: "schedule", type: captionFineTypes.schedule, root: operation("schedule") },
165
+ { name: "visual", type: compositionTypes.visualTrack, root: operation("render") },
166
+ ] });
167
+ fragments.push(collector);
168
+ components.push({ id, fragment: collector.id, inputs: bindings,
169
+ outputs: { program: `${id}.program`, schedule: `${id}.schedule`, visual: `${id}.visual` }, range: element.range });
170
+ return { records, components, fragments, exports: [`${id}.program`, `${id}.schedule`, `${id}.visual`] };
171
+ };