@onmark/cli 0.3.0 → 0.5.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/onmark-release.json +94 -64
- package/package.json +4 -4
- package/packages/authoring/dist/src/frame-motion.d.ts +10 -2
- package/packages/authoring/dist/src/frame-motion.js +29 -6
- package/packages/authoring/dist/src/index.d.ts +2 -2
- package/packages/authoring/dist/src/motion.d.ts +16 -4
- package/packages/authoring/dist/src/presentation.js +160 -6
- package/packages/authoring/dist/src/types.d.ts +1 -1
- package/packages/bundler/dist/src/authored_html.d.ts +19 -7
- package/packages/bundler/dist/src/authored_html.js +109 -21
- package/packages/bundler/dist/src/bundle_projection.d.ts +9 -0
- package/packages/bundler/dist/src/bundle_projection.js +66 -0
- package/packages/bundler/dist/src/command.js +38 -14
- package/packages/bundler/dist/src/generated/bundle-manifest.d.ts +4 -5
- package/packages/bundler/dist/src/generated/bundle-manifest.js +1 -0
- package/packages/bundler/dist/src/generated/bundle-projection-validator.d.ts +7 -0
- package/packages/bundler/dist/src/generated/bundle-projection-validator.js +432 -0
- package/packages/bundler/dist/src/generated/bundle-projection.d.ts +25 -0
- package/packages/bundler/dist/src/generated/bundle-projection.js +2 -0
- package/packages/bundler/dist/src/index.d.ts +1 -1
- package/packages/bundler/dist/src/presentation.d.ts +14 -7
- package/packages/bundler/dist/src/presentation.js +71 -30
- package/packages/motion-gsap/dist/src/index.d.ts +10 -2
- package/packages/motion-gsap/dist/src/index.js +27 -5
- package/packages/runtime/dist/src/generated/browser-request.d.ts +19 -2
- package/packages/runtime/dist/src/generated/browser-response.d.ts +46 -1
- package/packages/runtime/dist/src/generated/bundle-layout.d.ts +1 -1
- package/packages/runtime/dist/src/generated/bundle-layout.js +1 -0
- package/packages/runtime/dist/src/generated/runtime-contract.d.ts +4 -0
- package/packages/runtime/dist/src/generated/runtime-contract.js +4 -0
- package/packages/runtime/dist/src/generated/validators.d.ts +2 -2
- package/packages/runtime/dist/src/generated/validators.js +1482 -572
- package/packages/runtime/dist/src/index.d.ts +4 -4
- package/packages/runtime/dist/src/index.js +1 -1
- package/packages/runtime/dist/src/presentation.d.ts +18 -3
- package/packages/runtime/dist/src/presentation.js +112 -7
- package/packages/runtime/dist/src/session.d.ts +7 -3
- package/packages/runtime/dist/src/session.js +77 -20
- package/packages/runtime/dist/src/types.d.ts +3 -2
|
@@ -9,6 +9,7 @@ const ELEMENTS = Object.freeze({
|
|
|
9
9
|
scene: "om-scene",
|
|
10
10
|
shot: "om-shot",
|
|
11
11
|
title: "om-title",
|
|
12
|
+
transition: "om-transition",
|
|
12
13
|
video: "video",
|
|
13
14
|
});
|
|
14
15
|
// Whole-film artifacts may contain semantic nodes absent from a projected
|
|
@@ -17,6 +18,7 @@ const VISIBILITY_RULE = [
|
|
|
17
18
|
"[data-om-node][hidden],",
|
|
18
19
|
"om-film > om-scene:not([data-om-node]),",
|
|
19
20
|
"om-scene > om-shot:not([data-om-node]),",
|
|
21
|
+
"om-scene > om-transition,",
|
|
20
22
|
"om-shot > :is(video, om-title, om-cta):not([data-om-node]) {",
|
|
21
23
|
" display: none !important;",
|
|
22
24
|
"}",
|
|
@@ -32,6 +34,7 @@ export function createDomPresentationBindings(options) {
|
|
|
32
34
|
bindFilm: document.bindFilm.bind(document),
|
|
33
35
|
bindScene: document.bindScene.bind(document),
|
|
34
36
|
bindShot: document.bindShot.bind(document),
|
|
37
|
+
bindTransition: document.bindTransition.bind(document),
|
|
35
38
|
bindVideo: document.bindVideo.bind(document),
|
|
36
39
|
bindOverlay: document.bindOverlay.bind(document),
|
|
37
40
|
async bindExtensions(plan) {
|
|
@@ -87,15 +90,31 @@ class AuthoredDocument {
|
|
|
87
90
|
}
|
|
88
91
|
bindVideo(placement) {
|
|
89
92
|
const element = requiredNode(this.#nodeIndex(), placement.node, "video", ELEMENTS.video);
|
|
90
|
-
const bound = bindElement(element, placement.node);
|
|
91
93
|
element.muted = true;
|
|
92
94
|
element.playsInline = true;
|
|
93
95
|
this.#record("video", element, placement.node, placement.interval);
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
+
return bindVideoElement(element, placement.node, this.#videoSource(placement));
|
|
97
|
+
}
|
|
98
|
+
bindTransition(placement) {
|
|
99
|
+
const nodes = this.#nodeIndex();
|
|
100
|
+
const element = requiredNode(nodes, placement.node, "transition", ELEMENTS.transition);
|
|
101
|
+
const outgoingElement = requiredNodeId(nodes, placement.outgoingShotId, "outgoing shot", ELEMENTS.shot);
|
|
102
|
+
const incomingElement = requiredNodeId(nodes, placement.incomingShotId, "incoming shot", ELEMENTS.shot);
|
|
103
|
+
const bound = bindElement(element, placement.node);
|
|
104
|
+
this.#targets.push(Object.freeze({
|
|
105
|
+
element,
|
|
106
|
+
incomingElement,
|
|
107
|
+
interval: placement.interval,
|
|
108
|
+
kind: "transition",
|
|
109
|
+
node: placement.node,
|
|
110
|
+
outgoingElement,
|
|
111
|
+
}));
|
|
112
|
+
return Object.freeze({
|
|
113
|
+
dispose: bound.dispose,
|
|
96
114
|
element,
|
|
97
|
-
|
|
98
|
-
|
|
115
|
+
incomingElement,
|
|
116
|
+
outgoingElement,
|
|
117
|
+
});
|
|
99
118
|
}
|
|
100
119
|
bindOverlay(placement) {
|
|
101
120
|
if (placement.kind === "caption") {
|
|
@@ -157,7 +176,13 @@ function collectAuthoredNodes(document, plan) {
|
|
|
157
176
|
const browserVideoShots = new Set(plan.videos.map(({ shotId }) => shotId));
|
|
158
177
|
for (const scene of semanticChildren(film, ELEMENTS.scene)) {
|
|
159
178
|
indexed.push(scene);
|
|
160
|
-
|
|
179
|
+
const structure = semanticChildren(scene, `${ELEMENTS.shot}, ${ELEMENTS.transition}`);
|
|
180
|
+
for (const element of structure) {
|
|
181
|
+
if (element.matches(ELEMENTS.transition)) {
|
|
182
|
+
indexed.push(element);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const shot = element;
|
|
161
186
|
indexed.push(shot);
|
|
162
187
|
// Browser Plans assign dense semantic preorder IDs. The shot just
|
|
163
188
|
// admitted is therefore the parent ID carried by projected videos.
|
|
@@ -170,6 +195,9 @@ function collectAuthoredNodes(document, plan) {
|
|
|
170
195
|
}
|
|
171
196
|
return Object.freeze({ film, elements: Object.freeze(indexed) });
|
|
172
197
|
}
|
|
198
|
+
function requiredNodeId(nodes, nodeId, role, selector) {
|
|
199
|
+
return requiredNode(nodes, { nodeId }, role, selector);
|
|
200
|
+
}
|
|
173
201
|
function requiredNode(nodes, node, role, selector) {
|
|
174
202
|
const element = nodes.elements[node.nodeId];
|
|
175
203
|
if (element === undefined) {
|
|
@@ -205,6 +233,132 @@ function bindElement(element, node, release) {
|
|
|
205
233
|
},
|
|
206
234
|
};
|
|
207
235
|
}
|
|
236
|
+
function bindVideoElement(element, node, source) {
|
|
237
|
+
const bound = bindElement(element, node);
|
|
238
|
+
const authoredVisibility = element.style.visibility;
|
|
239
|
+
let layoutVisible = false;
|
|
240
|
+
return {
|
|
241
|
+
element,
|
|
242
|
+
source,
|
|
243
|
+
setVisible: bound.setVisible,
|
|
244
|
+
setLayoutVisible(visible) {
|
|
245
|
+
if (visible) {
|
|
246
|
+
element.style.visibility = "hidden";
|
|
247
|
+
bound.setVisible(true);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
bound.setVisible(false);
|
|
251
|
+
element.style.visibility = authoredVisibility;
|
|
252
|
+
}
|
|
253
|
+
layoutVisible = visible;
|
|
254
|
+
},
|
|
255
|
+
measureLayout() {
|
|
256
|
+
if (!layoutVisible) {
|
|
257
|
+
throw new Error("video layout requires a visible layout-only element");
|
|
258
|
+
}
|
|
259
|
+
return measureVideoLayout(element, node.nodeId);
|
|
260
|
+
},
|
|
261
|
+
dispose() {
|
|
262
|
+
bound.setVisible(false);
|
|
263
|
+
element.style.visibility = authoredVisibility;
|
|
264
|
+
bound.dispose();
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function measureVideoLayout(element, nodeId) {
|
|
269
|
+
requireStaticVideoStyle(element);
|
|
270
|
+
const rectangle = pixelRectangle(element.getBoundingClientRect());
|
|
271
|
+
const style = getComputedStyle(element);
|
|
272
|
+
return Object.freeze({
|
|
273
|
+
nodeId,
|
|
274
|
+
objectFit: objectFit(style.objectFit),
|
|
275
|
+
objectPosition: objectPosition(style.objectPosition),
|
|
276
|
+
rectangle,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
function requireStaticVideoStyle(element) {
|
|
280
|
+
const style = getComputedStyle(element);
|
|
281
|
+
const unsupported = [
|
|
282
|
+
["border-radius", style.borderRadius, "0px"],
|
|
283
|
+
["filter", style.filter, "none"],
|
|
284
|
+
["mix-blend-mode", style.mixBlendMode, "normal"],
|
|
285
|
+
["opacity", style.opacity, "1"],
|
|
286
|
+
["transform", style.transform, "none"],
|
|
287
|
+
];
|
|
288
|
+
for (const [name, actual, expected] of unsupported) {
|
|
289
|
+
if (actual !== expected) {
|
|
290
|
+
throw new Error(`layout-only video requires ${name}: ${expected}, found ${actual}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
for (const name of [
|
|
294
|
+
"borderBottomWidth",
|
|
295
|
+
"borderLeftWidth",
|
|
296
|
+
"borderRightWidth",
|
|
297
|
+
"borderTopWidth",
|
|
298
|
+
"paddingBottom",
|
|
299
|
+
"paddingLeft",
|
|
300
|
+
"paddingRight",
|
|
301
|
+
"paddingTop",
|
|
302
|
+
]) {
|
|
303
|
+
if (style[name] !== "0px") {
|
|
304
|
+
throw new Error(`layout-only video requires ${cssName(name)}: 0`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (style.clipPath !== "none") {
|
|
308
|
+
throw new Error("layout-only video requires clip-path: none");
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function pixelRectangle(rectangle) {
|
|
312
|
+
const values = [rectangle.x, rectangle.y, rectangle.width, rectangle.height];
|
|
313
|
+
if (devicePixelRatio !== 1 ||
|
|
314
|
+
!values.every(Number.isSafeInteger) ||
|
|
315
|
+
rectangle.x < 0 ||
|
|
316
|
+
rectangle.y < 0 ||
|
|
317
|
+
rectangle.width <= 0 ||
|
|
318
|
+
rectangle.height <= 0 ||
|
|
319
|
+
rectangle.right > document.documentElement.clientWidth ||
|
|
320
|
+
rectangle.bottom > document.documentElement.clientHeight) {
|
|
321
|
+
throw new Error("layout-only video requires a positive, pixel-aligned viewport rectangle");
|
|
322
|
+
}
|
|
323
|
+
return Object.freeze({
|
|
324
|
+
height: rectangle.height,
|
|
325
|
+
width: rectangle.width,
|
|
326
|
+
x: rectangle.x,
|
|
327
|
+
y: rectangle.y,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
function objectFit(value) {
|
|
331
|
+
if (value === "fill" || value === "contain" || value === "cover") {
|
|
332
|
+
return value;
|
|
333
|
+
}
|
|
334
|
+
throw new Error(`layout-only video does not support object-fit: ${value}`);
|
|
335
|
+
}
|
|
336
|
+
function objectPosition(value) {
|
|
337
|
+
const [x, y, ...rest] = value.trim().split(/\s+/u);
|
|
338
|
+
if (x === undefined || y === undefined || rest.length > 0) {
|
|
339
|
+
throw new Error("layout-only video requires two percentage positions");
|
|
340
|
+
}
|
|
341
|
+
return Object.freeze({
|
|
342
|
+
x: percentageMillionths(x),
|
|
343
|
+
y: percentageMillionths(y),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function percentageMillionths(value) {
|
|
347
|
+
const match = /^(?<whole>[0-9]{1,3})(?:\.(?<fraction>[0-9]{1,4}))?%$/u.exec(value);
|
|
348
|
+
if (match?.groups === undefined) {
|
|
349
|
+
throw new Error("layout-only video object-position must use exact percentages");
|
|
350
|
+
}
|
|
351
|
+
const whole = Number(match.groups["whole"]);
|
|
352
|
+
const fraction = Number((match.groups["fraction"] ?? "").padEnd(4, "0"));
|
|
353
|
+
const millionths = whole * 10_000 + fraction;
|
|
354
|
+
if (millionths > 1_000_000) {
|
|
355
|
+
throw new Error("layout-only video object-position must lie between 0% and 100%");
|
|
356
|
+
}
|
|
357
|
+
return millionths;
|
|
358
|
+
}
|
|
359
|
+
function cssName(value) {
|
|
360
|
+
return value.replace(/[A-Z]/gu, (character) => `-${character.toLowerCase()}`);
|
|
361
|
+
}
|
|
208
362
|
function requireAuthoredId(element, node) {
|
|
209
363
|
const expected = node.authoredId ?? "";
|
|
210
364
|
if (element.id !== expected) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export type { PresentationExtension, PresentationExtensionContext, PresentationTarget, PresentationTargetKind, } from "./motion.js";
|
|
1
|
+
export type { PresentationExtension, PresentationExtensionContext, PresentationElementTarget, PresentationElementTargetKind, PresentationTarget, PresentationTargetKind, PresentationTransitionTarget, } from "./motion.js";
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
+
import type { PresentationVisualCapability } from "../../../runtime/dist/src/index.js";
|
|
1
2
|
import { type HtmlImageResource } from "./html_image.js";
|
|
2
3
|
/** Authored document bytes and optional inline module prepared for esbuild. */
|
|
3
4
|
export interface AuthoredHtml {
|
|
4
5
|
readonly document: string;
|
|
5
6
|
readonly motion: string | undefined;
|
|
7
|
+
readonly visualCapability: PresentationVisualCapability | undefined;
|
|
6
8
|
readonly resources: readonly HtmlImageResource[];
|
|
7
|
-
readonly
|
|
9
|
+
readonly shots: readonly AuthoredHtmlShot[];
|
|
8
10
|
readonly regionStructure: AuthoredHtmlRegionStructure;
|
|
9
11
|
readonly resolveDirectory: string;
|
|
10
12
|
readonly runtimeOffset: number;
|
|
11
13
|
}
|
|
12
|
-
/** One shot
|
|
13
|
-
export interface
|
|
14
|
+
/** One dense screenplay-order shot within the authored document. */
|
|
15
|
+
export interface AuthoredHtmlShot {
|
|
14
16
|
readonly scene: number;
|
|
15
17
|
readonly shot: number;
|
|
16
18
|
}
|
|
@@ -20,9 +22,19 @@ export interface AuthoredHtmlRegionStructure {
|
|
|
20
22
|
}
|
|
21
23
|
export interface AuthoredHtmlScene {
|
|
22
24
|
readonly range: SourceRange;
|
|
23
|
-
readonly shots: readonly
|
|
25
|
+
readonly shots: readonly AuthoredHtmlShotRange[];
|
|
26
|
+
readonly transitions: readonly AuthoredHtmlTransitionRange[];
|
|
24
27
|
}
|
|
25
|
-
|
|
28
|
+
export interface AuthoredHtmlShotRange {
|
|
29
|
+
readonly index: number;
|
|
30
|
+
readonly range: SourceRange;
|
|
31
|
+
}
|
|
32
|
+
export interface AuthoredHtmlTransitionRange {
|
|
33
|
+
readonly incomingShot: number;
|
|
34
|
+
readonly outgoingShot: number;
|
|
35
|
+
readonly range: SourceRange;
|
|
36
|
+
}
|
|
37
|
+
/** One materialized render-region browser document. */
|
|
26
38
|
export interface AuthoredHtmlRegionDocument {
|
|
27
39
|
readonly document: string;
|
|
28
40
|
readonly runtimeOffset: number;
|
|
@@ -33,8 +45,8 @@ export declare class AuthoredHtmlError extends Error {
|
|
|
33
45
|
}
|
|
34
46
|
/** Reads one bounded UTF-8 document and isolates its motion module. */
|
|
35
47
|
export declare function readAuthoredHtml(path: string, maxOutputBytes: number, resolveDirectory?: string): Promise<AuthoredHtml>;
|
|
36
|
-
/** Materializes one
|
|
37
|
-
export declare function
|
|
48
|
+
/** Materializes one Rust-planned region without retaining every projection. */
|
|
49
|
+
export declare function projectRegionDocument(html: AuthoredHtml, shotIndices: readonly number[]): AuthoredHtmlRegionDocument;
|
|
38
50
|
export interface SourceRange {
|
|
39
51
|
readonly end: number;
|
|
40
52
|
readonly start: number;
|
|
@@ -6,6 +6,7 @@ import { parse } from "parse5";
|
|
|
6
6
|
import { freezeHtmlImages } from "./html_image.js";
|
|
7
7
|
const MAX_HTML_BYTES = 8 * 1024 * 1024;
|
|
8
8
|
const MOTION_ATTRIBUTE = "data-om-motion";
|
|
9
|
+
const VISUAL_CAPABILITY_META = "onmark:visual-capability";
|
|
9
10
|
const RUNTIME_SCRIPT = '<script type="module" src="./presentation.js"></script>';
|
|
10
11
|
/** Invalid or unreadable authored HTML at the Node bundling boundary. */
|
|
11
12
|
export class AuthoredHtmlError extends Error {
|
|
@@ -18,6 +19,7 @@ export class AuthoredHtmlError extends Error {
|
|
|
18
19
|
export async function readAuthoredHtml(path, maxOutputBytes, resolveDirectory) {
|
|
19
20
|
const absolute = resolve(path);
|
|
20
21
|
const source = await readBoundedSource(absolute);
|
|
22
|
+
const visualCapability = declaredVisualCapability(parseDocument(source));
|
|
21
23
|
const directory = resolveDirectory === undefined
|
|
22
24
|
? dirname(absolute)
|
|
23
25
|
: resolve(resolveDirectory);
|
|
@@ -28,6 +30,7 @@ export async function readAuthoredHtml(path, maxOutputBytes, resolveDirectory) {
|
|
|
28
30
|
...extracted,
|
|
29
31
|
resources: frozen.resources,
|
|
30
32
|
resolveDirectory: directory,
|
|
33
|
+
visualCapability,
|
|
31
34
|
});
|
|
32
35
|
}
|
|
33
36
|
// ── Bounded input
|
|
@@ -119,39 +122,81 @@ function projectShotStructure(source) {
|
|
|
119
122
|
const film = findElement(document, "om-film");
|
|
120
123
|
if (film === undefined) {
|
|
121
124
|
return {
|
|
122
|
-
|
|
125
|
+
shots: Object.freeze([]),
|
|
123
126
|
regionStructure: Object.freeze({ scenes: Object.freeze([]) }),
|
|
124
127
|
};
|
|
125
128
|
}
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
return Object.freeze({
|
|
129
|
-
range: removableElementRange(source, scene),
|
|
130
|
-
shots: Object.freeze(shots),
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
const regions = scenes.flatMap((scene, sceneIndex) => scene.shots.map((_, shotIndex) => Object.freeze({ scene: sceneIndex, shot: shotIndex })));
|
|
129
|
+
const shots = [];
|
|
130
|
+
const scenes = childElements(film, "om-scene").map((scene, sceneIndex) => projectSceneStructure(source, scene, sceneIndex, shots));
|
|
134
131
|
return {
|
|
135
|
-
|
|
132
|
+
shots: Object.freeze(shots),
|
|
136
133
|
regionStructure: Object.freeze({ scenes: Object.freeze(scenes) }),
|
|
137
134
|
};
|
|
138
135
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
136
|
+
function projectSceneStructure(source, scene, sceneIndex, documentShots) {
|
|
137
|
+
const shots = [];
|
|
138
|
+
const transitions = [];
|
|
139
|
+
let outgoingShot;
|
|
140
|
+
let pendingTransition;
|
|
141
|
+
for (const element of directElements(scene)) {
|
|
142
|
+
if (element.tagName === "om-transition") {
|
|
143
|
+
if (outgoingShot === undefined || pendingTransition !== undefined) {
|
|
144
|
+
throw transitionProjectionError();
|
|
145
|
+
}
|
|
146
|
+
pendingTransition = removableElementRange(source, element);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (element.tagName !== "om-shot") {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const index = documentShots.length;
|
|
153
|
+
const shot = Object.freeze({ scene: sceneIndex, shot: shots.length });
|
|
154
|
+
documentShots.push(shot);
|
|
155
|
+
shots.push(Object.freeze({
|
|
156
|
+
index,
|
|
157
|
+
range: removableElementRange(source, element),
|
|
158
|
+
}));
|
|
159
|
+
if (pendingTransition !== undefined && outgoingShot !== undefined) {
|
|
160
|
+
transitions.push(Object.freeze({
|
|
161
|
+
incomingShot: index,
|
|
162
|
+
outgoingShot,
|
|
163
|
+
range: pendingTransition,
|
|
164
|
+
}));
|
|
165
|
+
pendingTransition = undefined;
|
|
166
|
+
}
|
|
167
|
+
outgoingShot = index;
|
|
168
|
+
}
|
|
169
|
+
if (pendingTransition !== undefined) {
|
|
170
|
+
throw transitionProjectionError();
|
|
145
171
|
}
|
|
172
|
+
return Object.freeze({
|
|
173
|
+
range: removableElementRange(source, scene),
|
|
174
|
+
shots: Object.freeze(shots),
|
|
175
|
+
transitions: Object.freeze(transitions),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function transitionProjectionError() {
|
|
179
|
+
return new AuthoredHtmlError("compiled transition boundaries cannot be projected into browser regions");
|
|
180
|
+
}
|
|
181
|
+
/** Materializes one Rust-planned region without retaining every projection. */
|
|
182
|
+
export function projectRegionDocument(html, shotIndices) {
|
|
183
|
+
const selectedShots = selectedShotSet(html, shotIndices);
|
|
146
184
|
const ranges = [];
|
|
147
|
-
for (const
|
|
148
|
-
if (
|
|
185
|
+
for (const scene of html.regionStructure.scenes) {
|
|
186
|
+
if (!scene.shots.some((shot) => selectedShots.has(shot.index))) {
|
|
149
187
|
ranges.push(scene.range);
|
|
150
188
|
continue;
|
|
151
189
|
}
|
|
152
|
-
for (const
|
|
153
|
-
if (
|
|
154
|
-
ranges.push(shot);
|
|
190
|
+
for (const shot of scene.shots) {
|
|
191
|
+
if (!selectedShots.has(shot.index)) {
|
|
192
|
+
ranges.push(shot.range);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const transition of scene.transitions) {
|
|
196
|
+
const retained = selectedShots.has(transition.outgoingShot) &&
|
|
197
|
+
selectedShots.has(transition.incomingShot);
|
|
198
|
+
if (!retained) {
|
|
199
|
+
ranges.push(transition.range);
|
|
155
200
|
}
|
|
156
201
|
}
|
|
157
202
|
}
|
|
@@ -160,6 +205,16 @@ export function projectShotDocument(html, region) {
|
|
|
160
205
|
runtimeOffset: projectOffset(html.runtimeOffset, ranges),
|
|
161
206
|
});
|
|
162
207
|
}
|
|
208
|
+
function selectedShotSet(html, shotIndices) {
|
|
209
|
+
const selected = new Set();
|
|
210
|
+
for (const index of shotIndices) {
|
|
211
|
+
if (html.shots[index] === undefined) {
|
|
212
|
+
throw new AuthoredHtmlError(`browser projection selects unknown shot ${index}`);
|
|
213
|
+
}
|
|
214
|
+
selected.add(index);
|
|
215
|
+
}
|
|
216
|
+
return selected;
|
|
217
|
+
}
|
|
163
218
|
function parseDocument(source) {
|
|
164
219
|
const errors = [];
|
|
165
220
|
const document = parse(source, {
|
|
@@ -215,6 +270,33 @@ function collectScripts(node) {
|
|
|
215
270
|
});
|
|
216
271
|
return scripts;
|
|
217
272
|
}
|
|
273
|
+
function declaredVisualCapability(document) {
|
|
274
|
+
const declarations = [];
|
|
275
|
+
visit(document, (element) => {
|
|
276
|
+
if (element.tagName !== "meta") {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const attributes = new Map(element.attrs.map(({ name, value }) => [name, value]));
|
|
280
|
+
if (attributes.get("name") === VISUAL_CAPABILITY_META) {
|
|
281
|
+
declarations.push(attributes.get("content") ?? "");
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
if (declarations.length > 1) {
|
|
285
|
+
throw new AuthoredHtmlError(`authored HTML may contain at most one ${VISUAL_CAPABILITY_META} declaration`);
|
|
286
|
+
}
|
|
287
|
+
const [declaration] = declarations;
|
|
288
|
+
if (declaration === undefined) {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
switch (declaration) {
|
|
292
|
+
case "browserComposite":
|
|
293
|
+
case "separableBackdrop":
|
|
294
|
+
case "separableOverlay":
|
|
295
|
+
return declaration;
|
|
296
|
+
default:
|
|
297
|
+
throw new AuthoredHtmlError(`${VISUAL_CAPABILITY_META} must be browserComposite, separableBackdrop, or separableOverlay`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
218
300
|
function findElement(node, name) {
|
|
219
301
|
const pending = [node];
|
|
220
302
|
while (pending.length > 0) {
|
|
@@ -234,6 +316,9 @@ function findElement(node, name) {
|
|
|
234
316
|
function childElements(parent, name) {
|
|
235
317
|
return parent.childNodes.filter((child) => "tagName" in child && child.tagName === name);
|
|
236
318
|
}
|
|
319
|
+
function directElements(parent) {
|
|
320
|
+
return parent.childNodes.filter((child) => "tagName" in child);
|
|
321
|
+
}
|
|
237
322
|
function visit(node, visitor) {
|
|
238
323
|
const pending = [node];
|
|
239
324
|
while (pending.length > 0) {
|
|
@@ -328,9 +413,12 @@ function isCompilerOnlyAttribute(tagName, attributeName) {
|
|
|
328
413
|
case "om-title":
|
|
329
414
|
return attributeName === "cue" || attributeName === "delay";
|
|
330
415
|
case "om-shot":
|
|
416
|
+
case "om-transition":
|
|
331
417
|
return attributeName === "duration";
|
|
332
418
|
case "video":
|
|
333
419
|
return (attributeName === "delay" ||
|
|
420
|
+
attributeName === "hold-last" ||
|
|
421
|
+
attributeName === "plays" ||
|
|
334
422
|
attributeName === "speed" ||
|
|
335
423
|
attributeName === "src" ||
|
|
336
424
|
attributeName === "trim");
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BundleProjection } from "./generated/bundle-projection.js";
|
|
2
|
+
/** Invalid or unreadable projection data at the bundler process boundary. */
|
|
3
|
+
export declare class BundleProjectionError extends Error {
|
|
4
|
+
constructor(message: string, options?: ErrorOptions);
|
|
5
|
+
}
|
|
6
|
+
/** Reads and validates one bounded Rust-owned projection snapshot. */
|
|
7
|
+
export declare function readBundleProjection(path: string): Promise<BundleProjection>;
|
|
8
|
+
/** Decodes one checked projection at an in-process or file boundary. */
|
|
9
|
+
export declare function decodeBundleProjection(value: unknown): BundleProjection;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Bounded reader for the Rust-owned render-region projection contract.
|
|
2
|
+
// It validates process input before authored HTML is projected or published.
|
|
3
|
+
import { open } from "node:fs/promises";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { validateBundleProjection } from "./generated/bundle-projection-validator.js";
|
|
6
|
+
const MAX_PROJECTION_BYTES = 1024 * 1024;
|
|
7
|
+
/** Invalid or unreadable projection data at the bundler process boundary. */
|
|
8
|
+
export class BundleProjectionError extends Error {
|
|
9
|
+
constructor(message, options) {
|
|
10
|
+
super(message, options);
|
|
11
|
+
this.name = "BundleProjectionError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Reads and validates one bounded Rust-owned projection snapshot. */
|
|
15
|
+
export async function readBundleProjection(path) {
|
|
16
|
+
const absolute = resolve(path);
|
|
17
|
+
let file;
|
|
18
|
+
try {
|
|
19
|
+
file = await open(absolute, "r");
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
throw new BundleProjectionError(`cannot open bundle projection ${absolute}`, { cause: error });
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const bytes = await readBoundedProjection(file);
|
|
26
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
27
|
+
return decodeBundleProjection(value);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error instanceof BundleProjectionError) {
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
throw new BundleProjectionError(`cannot read bundle projection ${absolute}`, { cause: error });
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await file.close();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Decodes one checked projection at an in-process or file boundary. */
|
|
40
|
+
export function decodeBundleProjection(value) {
|
|
41
|
+
if (!validateBundleProjection(value)) {
|
|
42
|
+
throw invalidProjection();
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
async function readBoundedProjection(file) {
|
|
47
|
+
const bytes = Buffer.allocUnsafe(MAX_PROJECTION_BYTES + 1);
|
|
48
|
+
let length = 0;
|
|
49
|
+
while (length < bytes.length) {
|
|
50
|
+
const { bytesRead } = await file.read(bytes, length, bytes.length - length, length);
|
|
51
|
+
if (bytesRead === 0) {
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
length += bytesRead;
|
|
55
|
+
}
|
|
56
|
+
if (length > MAX_PROJECTION_BYTES) {
|
|
57
|
+
throw new BundleProjectionError(`bundle projection exceeds the ${MAX_PROJECTION_BYTES}-byte limit`);
|
|
58
|
+
}
|
|
59
|
+
return bytes.subarray(0, length);
|
|
60
|
+
}
|
|
61
|
+
function invalidProjection() {
|
|
62
|
+
const error = validateBundleProjection.errors?.[0];
|
|
63
|
+
const path = error?.instancePath ?? "";
|
|
64
|
+
const detail = error?.message ?? "does not match its schema";
|
|
65
|
+
return new BundleProjectionError(`bundle projection${path} ${detail}`.trim());
|
|
66
|
+
}
|
|
@@ -3,21 +3,23 @@
|
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
import { BundleError, bundlePresentation, } from "./presentation.js";
|
|
6
|
+
import { BundleProjectionError, readBundleProjection, } from "./bundle_projection.js";
|
|
6
7
|
import { BUNDLE_FRAME_BEHAVIORS, BUNDLE_TEMPORAL_CAPABILITIES, BUNDLE_VISUAL_CAPABILITIES, } from "./generated/bundle-manifest.js";
|
|
7
8
|
const USAGE = [
|
|
8
9
|
"Usage: onmark-bundle",
|
|
9
10
|
" --html <path>",
|
|
10
11
|
" [--resolve-directory <directory>]",
|
|
12
|
+
" [--projection <path> # required for randomAccess]",
|
|
11
13
|
" --output <directory>",
|
|
12
14
|
" --max-output-bytes <bytes>",
|
|
13
15
|
" --frame-behavior <perFrame|placementBounded>",
|
|
14
16
|
" --temporal-capability <sequential|randomAccess>",
|
|
15
|
-
" --visual-capability <browserComposite|separableOverlay>",
|
|
17
|
+
" [--visual-capability <browserComposite|separableBackdrop|separableOverlay>]",
|
|
16
18
|
"",
|
|
17
19
|
].join("\n");
|
|
18
20
|
// ── Execution ──
|
|
19
21
|
try {
|
|
20
|
-
const command = parseArguments(process.argv.slice(2));
|
|
22
|
+
const command = await parseArguments(process.argv.slice(2));
|
|
21
23
|
if (command.kind === "help") {
|
|
22
24
|
process.stdout.write(USAGE);
|
|
23
25
|
}
|
|
@@ -31,7 +33,7 @@ catch (error) {
|
|
|
31
33
|
process.exitCode = failure.kind === "configuration" ? 2 : 1;
|
|
32
34
|
}
|
|
33
35
|
// ── Arguments ──
|
|
34
|
-
function parseArguments(arguments_) {
|
|
36
|
+
async function parseArguments(arguments_) {
|
|
35
37
|
const values = commandValues(arguments_);
|
|
36
38
|
if (values.help !== undefined) {
|
|
37
39
|
if (arguments_.length !== 1) {
|
|
@@ -43,21 +45,30 @@ function parseArguments(arguments_) {
|
|
|
43
45
|
const maxOutputBytes = parseByteLimit(oneValue(values["max-output-bytes"], "--max-output-bytes"));
|
|
44
46
|
const frameBehavior = parseFrameBehavior(oneValue(values["frame-behavior"], "--frame-behavior"));
|
|
45
47
|
const temporalCapability = parseTemporalCapability(oneValue(values["temporal-capability"], "--temporal-capability"));
|
|
46
|
-
const visualCapability =
|
|
48
|
+
const visualCapability = optionalVisualCapability(values["visual-capability"]);
|
|
47
49
|
const controls = {
|
|
48
50
|
frameBehavior,
|
|
49
51
|
maxOutputBytes,
|
|
50
52
|
outputDirectory,
|
|
51
|
-
|
|
52
|
-
visualCapability,
|
|
53
|
+
...visualCapability,
|
|
53
54
|
};
|
|
55
|
+
const common = {
|
|
56
|
+
...controls,
|
|
57
|
+
document: oneValue(values.html, "--html"),
|
|
58
|
+
...optionalResolveDirectory(values["resolve-directory"]),
|
|
59
|
+
};
|
|
60
|
+
if (temporalCapability === "sequential") {
|
|
61
|
+
rejectValue(values.projection, "--projection requires randomAccess");
|
|
62
|
+
return {
|
|
63
|
+
kind: "bundle",
|
|
64
|
+
options: { ...common, temporalCapability },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const projectionPath = oneValue(values.projection, "--projection");
|
|
68
|
+
const projection = await readBundleProjection(projectionPath);
|
|
54
69
|
return {
|
|
55
70
|
kind: "bundle",
|
|
56
|
-
options: {
|
|
57
|
-
...controls,
|
|
58
|
-
document: oneValue(values.html, "--html"),
|
|
59
|
-
...optionalResolveDirectory(values["resolve-directory"]),
|
|
60
|
-
},
|
|
71
|
+
options: { ...common, projection, temporalCapability },
|
|
61
72
|
};
|
|
62
73
|
}
|
|
63
74
|
function commandValues(arguments_) {
|
|
@@ -71,6 +82,7 @@ function commandValues(arguments_) {
|
|
|
71
82
|
html: { type: "string", multiple: true },
|
|
72
83
|
"max-output-bytes": { type: "string", multiple: true },
|
|
73
84
|
output: { type: "string", multiple: true },
|
|
85
|
+
projection: { type: "string", multiple: true },
|
|
74
86
|
"resolve-directory": { type: "string", multiple: true },
|
|
75
87
|
"temporal-capability": { type: "string", multiple: true },
|
|
76
88
|
"visual-capability": { type: "string", multiple: true },
|
|
@@ -95,12 +107,16 @@ function parseTemporalCapability(value) {
|
|
|
95
107
|
}
|
|
96
108
|
throw configuration("--temporal-capability must be sequential or randomAccess");
|
|
97
109
|
}
|
|
98
|
-
function
|
|
110
|
+
function optionalVisualCapability(values) {
|
|
111
|
+
if (values === undefined) {
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
const value = oneValue(values, "--visual-capability");
|
|
99
115
|
const capability = BUNDLE_VISUAL_CAPABILITIES.find((candidate) => candidate === value);
|
|
100
116
|
if (capability !== undefined) {
|
|
101
|
-
return capability;
|
|
117
|
+
return { visualCapability: capability };
|
|
102
118
|
}
|
|
103
|
-
throw configuration("--visual-capability must be browserComposite or separableOverlay");
|
|
119
|
+
throw configuration("--visual-capability must be browserComposite, separableBackdrop, or separableOverlay");
|
|
104
120
|
}
|
|
105
121
|
function parseFrameBehavior(value) {
|
|
106
122
|
const behavior = BUNDLE_FRAME_BEHAVIORS.find((candidate) => candidate === value);
|
|
@@ -119,6 +135,11 @@ function oneValue(values, name) {
|
|
|
119
135
|
}
|
|
120
136
|
return value;
|
|
121
137
|
}
|
|
138
|
+
function rejectValue(values, message) {
|
|
139
|
+
if (values !== undefined) {
|
|
140
|
+
throw configuration(message);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
122
143
|
function parseByteLimit(value) {
|
|
123
144
|
const bytes = Number(value);
|
|
124
145
|
if (!Number.isSafeInteger(bytes) || bytes <= 0) {
|
|
@@ -134,5 +155,8 @@ function commandFailure(error) {
|
|
|
134
155
|
if (error instanceof BundleError) {
|
|
135
156
|
return error;
|
|
136
157
|
}
|
|
158
|
+
if (error instanceof BundleProjectionError) {
|
|
159
|
+
return configuration(error.message, error);
|
|
160
|
+
}
|
|
137
161
|
return new BundleError("output", "unexpected bundler failure", error);
|
|
138
162
|
}
|