@onmark/cli 0.1.3
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/LICENSE +21 -0
- package/README.md +202 -0
- package/onmark-release.json +388 -0
- package/package.json +52 -0
- package/packages/authoring/dist/src/index.d.ts +3 -0
- package/packages/authoring/dist/src/index.js +4 -0
- package/packages/authoring/dist/src/motion.d.ts +23 -0
- package/packages/authoring/dist/src/motion.js +81 -0
- package/packages/authoring/dist/src/presentation.d.ts +12 -0
- package/packages/authoring/dist/src/presentation.js +226 -0
- package/packages/authoring/dist/src/resource.d.ts +32 -0
- package/packages/authoring/dist/src/resource.js +86 -0
- package/packages/authoring/dist/src/types.d.ts +1 -0
- package/packages/authoring/dist/src/types.js +2 -0
- package/packages/bundler/dist/src/authored_html.d.ts +41 -0
- package/packages/bundler/dist/src/authored_html.js +387 -0
- package/packages/bundler/dist/src/command.d.ts +2 -0
- package/packages/bundler/dist/src/command.js +138 -0
- package/packages/bundler/dist/src/generated/bundle-manifest.d.ts +65 -0
- package/packages/bundler/dist/src/generated/bundle-manifest.js +18 -0
- package/packages/bundler/dist/src/html_image.d.ts +20 -0
- package/packages/bundler/dist/src/html_image.js +337 -0
- package/packages/bundler/dist/src/image_admission.d.ts +2 -0
- package/packages/bundler/dist/src/image_admission.js +217 -0
- package/packages/bundler/dist/src/index.d.ts +2 -0
- package/packages/bundler/dist/src/index.js +3 -0
- package/packages/bundler/dist/src/presentation.d.ts +41 -0
- package/packages/bundler/dist/src/presentation.js +492 -0
- package/packages/launcher/dist/desktop-release.json +21 -0
- package/packages/launcher/dist/src/browser-command.d.ts +2 -0
- package/packages/launcher/dist/src/browser-command.js +43 -0
- package/packages/launcher/dist/src/browser.d.ts +14 -0
- package/packages/launcher/dist/src/browser.js +280 -0
- package/packages/launcher/dist/src/cache.d.ts +8 -0
- package/packages/launcher/dist/src/cache.js +21 -0
- package/packages/launcher/dist/src/capture-environment.d.ts +15 -0
- package/packages/launcher/dist/src/capture-environment.js +145 -0
- package/packages/launcher/dist/src/command.d.ts +2 -0
- package/packages/launcher/dist/src/command.js +58 -0
- package/packages/launcher/dist/src/native.d.ts +20 -0
- package/packages/launcher/dist/src/native.js +31 -0
- package/packages/launcher/dist/src/platform.d.ts +13 -0
- package/packages/launcher/dist/src/platform.js +29 -0
- package/packages/launcher/dist/src/release.d.ts +16 -0
- package/packages/launcher/dist/src/release.js +49 -0
- package/packages/motion-gsap/dist/src/index.d.ts +16 -0
- package/packages/motion-gsap/dist/src/index.js +139 -0
- package/packages/runtime/dist/src/clock.d.ts +10 -0
- package/packages/runtime/dist/src/clock.js +19 -0
- package/packages/runtime/dist/src/generated/browser-request.d.ts +143 -0
- package/packages/runtime/dist/src/generated/browser-request.js +2 -0
- package/packages/runtime/dist/src/generated/browser-response.d.ts +58 -0
- package/packages/runtime/dist/src/generated/browser-response.js +2 -0
- package/packages/runtime/dist/src/generated/bundle-layout.d.ts +7 -0
- package/packages/runtime/dist/src/generated/bundle-layout.js +14 -0
- package/packages/runtime/dist/src/generated/codec.d.ts +8 -0
- package/packages/runtime/dist/src/generated/codec.js +22 -0
- package/packages/runtime/dist/src/generated/runtime-contract.d.ts +7 -0
- package/packages/runtime/dist/src/generated/runtime-contract.js +8 -0
- package/packages/runtime/dist/src/generated/validators.d.ts +13 -0
- package/packages/runtime/dist/src/generated/validators.js +3327 -0
- package/packages/runtime/dist/src/host.d.ts +11 -0
- package/packages/runtime/dist/src/host.js +25 -0
- package/packages/runtime/dist/src/index.d.ts +13 -0
- package/packages/runtime/dist/src/index.js +12 -0
- package/packages/runtime/dist/src/media.d.ts +13 -0
- package/packages/runtime/dist/src/media.js +28 -0
- package/packages/runtime/dist/src/presentation.d.ts +66 -0
- package/packages/runtime/dist/src/presentation.js +379 -0
- package/packages/runtime/dist/src/resource.d.ts +23 -0
- package/packages/runtime/dist/src/resource.js +150 -0
- package/packages/runtime/dist/src/session.d.ts +43 -0
- package/packages/runtime/dist/src/session.js +450 -0
- package/packages/runtime/dist/src/types.d.ts +6 -0
- package/packages/runtime/dist/src/types.js +2 -0
- package/packages/runtime/dist/src/video.d.ts +40 -0
- package/packages/runtime/dist/src/video.js +343 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
// Bounded HTML ingestion and exact motion-module extraction.
|
|
2
|
+
// Parsing identifies source ranges; publication preserves all other bytes.
|
|
3
|
+
import { open } from "node:fs/promises";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { parse } from "parse5";
|
|
6
|
+
import { freezeHtmlImages } from "./html_image.js";
|
|
7
|
+
const MAX_HTML_BYTES = 8 * 1024 * 1024;
|
|
8
|
+
const MOTION_ATTRIBUTE = "data-om-motion";
|
|
9
|
+
const RUNTIME_SCRIPT = '<script type="module" src="./presentation.js"></script>';
|
|
10
|
+
/** Invalid or unreadable authored HTML at the Node bundling boundary. */
|
|
11
|
+
export class AuthoredHtmlError extends Error {
|
|
12
|
+
constructor(message, options) {
|
|
13
|
+
super(message, options);
|
|
14
|
+
this.name = "AuthoredHtmlError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Reads one bounded UTF-8 document and isolates its motion module. */
|
|
18
|
+
export async function readAuthoredHtml(path, maxOutputBytes, resolveDirectory) {
|
|
19
|
+
const absolute = resolve(path);
|
|
20
|
+
const source = await readBoundedSource(absolute);
|
|
21
|
+
const directory = resolveDirectory === undefined
|
|
22
|
+
? dirname(absolute)
|
|
23
|
+
: resolve(resolveDirectory);
|
|
24
|
+
const browserSource = projectBrowserDocument(source);
|
|
25
|
+
const frozen = await freezeHtmlImages(browserSource, directory, maxOutputBytes);
|
|
26
|
+
const extracted = extractMotion(frozen.document);
|
|
27
|
+
return Object.freeze({
|
|
28
|
+
...extracted,
|
|
29
|
+
resources: frozen.resources,
|
|
30
|
+
resolveDirectory: directory,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
// ── Bounded input
|
|
34
|
+
async function readBoundedSource(path) {
|
|
35
|
+
let file;
|
|
36
|
+
try {
|
|
37
|
+
file = await open(path, "r");
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new AuthoredHtmlError(`cannot open authored HTML ${path}`, {
|
|
41
|
+
cause: error,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const bytes = Buffer.allocUnsafe(MAX_HTML_BYTES + 1);
|
|
46
|
+
let length = 0;
|
|
47
|
+
while (length < bytes.length) {
|
|
48
|
+
const { bytesRead } = await file.read(bytes, length, bytes.length - length, length);
|
|
49
|
+
if (bytesRead === 0) {
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
length += bytesRead;
|
|
53
|
+
}
|
|
54
|
+
if (length > MAX_HTML_BYTES) {
|
|
55
|
+
throw new AuthoredHtmlError(`authored HTML exceeds the ${MAX_HTML_BYTES}-byte limit`);
|
|
56
|
+
}
|
|
57
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, length));
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error instanceof AuthoredHtmlError) {
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
throw new AuthoredHtmlError(`cannot read authored HTML ${path}`, {
|
|
64
|
+
cause: error,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
await file.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function extractMotion(source) {
|
|
72
|
+
// Reparse after each source edit because parse5 offsets are not transferable.
|
|
73
|
+
// The final tree owns runtime insertion and projected shot ranges.
|
|
74
|
+
const installed = installRuntime(source);
|
|
75
|
+
const projection = projectShotStructure(installed.document);
|
|
76
|
+
return {
|
|
77
|
+
...installed,
|
|
78
|
+
...projection,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function projectBrowserDocument(source) {
|
|
82
|
+
const document = parseDocument(source);
|
|
83
|
+
const edits = [];
|
|
84
|
+
collectCompilerFacts(document, source, edits);
|
|
85
|
+
return removeRanges(source, edits);
|
|
86
|
+
}
|
|
87
|
+
function installRuntime(source) {
|
|
88
|
+
const document = parseDocument(source);
|
|
89
|
+
const scripts = collectScripts(document);
|
|
90
|
+
const motion = scripts.filter(isMotionScript);
|
|
91
|
+
const unsupported = scripts.filter((script) => !isMotionScript(script));
|
|
92
|
+
if (unsupported.length > 0) {
|
|
93
|
+
throw new AuthoredHtmlError('authored HTML scripts must use type="module" and data-om-motion');
|
|
94
|
+
}
|
|
95
|
+
if (motion.length > 1) {
|
|
96
|
+
throw new AuthoredHtmlError("authored HTML may contain at most one motion module");
|
|
97
|
+
}
|
|
98
|
+
const script = motion[0];
|
|
99
|
+
if (script === undefined) {
|
|
100
|
+
return insertRuntimeScript(source, document);
|
|
101
|
+
}
|
|
102
|
+
const location = script.sourceCodeLocation;
|
|
103
|
+
const startTag = location?.startTag;
|
|
104
|
+
const endTag = location?.endTag;
|
|
105
|
+
if (location == null || startTag === undefined || endTag === undefined) {
|
|
106
|
+
throw new AuthoredHtmlError("the motion module must have explicit opening and closing tags");
|
|
107
|
+
}
|
|
108
|
+
const motionSource = source.slice(startTag.endOffset, endTag.startOffset);
|
|
109
|
+
const browserSource = removeRanges(source, [
|
|
110
|
+
{ end: location.endOffset, start: location.startOffset },
|
|
111
|
+
]);
|
|
112
|
+
return {
|
|
113
|
+
...insertRuntimeScript(browserSource, parseDocument(browserSource)),
|
|
114
|
+
motion: motionSource,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function projectShotStructure(source) {
|
|
118
|
+
const document = parseDocument(source);
|
|
119
|
+
const film = findElement(document, "om-film");
|
|
120
|
+
if (film === undefined) {
|
|
121
|
+
return {
|
|
122
|
+
regions: Object.freeze([]),
|
|
123
|
+
regionStructure: Object.freeze({ scenes: Object.freeze([]) }),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const scenes = childElements(film, "om-scene").map((scene) => {
|
|
127
|
+
const shots = childElements(scene, "om-shot").map((shot) => removableElementRange(source, shot));
|
|
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 })));
|
|
134
|
+
return {
|
|
135
|
+
regions: Object.freeze(regions),
|
|
136
|
+
regionStructure: Object.freeze({ scenes: Object.freeze(scenes) }),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/** Materializes one shot projection without retaining every projected document. */
|
|
140
|
+
export function projectShotDocument(html, region) {
|
|
141
|
+
const selectedScene = html.regionStructure.scenes[region.scene];
|
|
142
|
+
const selectedShot = selectedScene?.shots[region.shot];
|
|
143
|
+
if (selectedScene === undefined || selectedShot === undefined) {
|
|
144
|
+
throw new AuthoredHtmlError("browser projection selects an unknown shot");
|
|
145
|
+
}
|
|
146
|
+
const ranges = [];
|
|
147
|
+
for (const [sceneIndex, scene] of html.regionStructure.scenes.entries()) {
|
|
148
|
+
if (sceneIndex !== region.scene) {
|
|
149
|
+
ranges.push(scene.range);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
for (const [shotIndex, shot] of scene.shots.entries()) {
|
|
153
|
+
if (shotIndex !== region.shot) {
|
|
154
|
+
ranges.push(shot);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return Object.freeze({
|
|
159
|
+
document: removeRanges(html.document, ranges),
|
|
160
|
+
runtimeOffset: projectOffset(html.runtimeOffset, ranges),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function parseDocument(source) {
|
|
164
|
+
const errors = [];
|
|
165
|
+
const document = parse(source, {
|
|
166
|
+
onParseError(error) {
|
|
167
|
+
if (error.code !== "missing-doctype") {
|
|
168
|
+
errors.push(error);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
sourceCodeLocationInfo: true,
|
|
172
|
+
});
|
|
173
|
+
if (errors.length > 0) {
|
|
174
|
+
throw new AuthoredHtmlError(`authored HTML cannot be bundled after parse error ${errors[0].code}`);
|
|
175
|
+
}
|
|
176
|
+
return document;
|
|
177
|
+
}
|
|
178
|
+
function insertRuntimeScript(source, document) {
|
|
179
|
+
const body = findElement(document, "body");
|
|
180
|
+
const endTag = body?.sourceCodeLocation?.endTag;
|
|
181
|
+
if (endTag === undefined) {
|
|
182
|
+
return {
|
|
183
|
+
document: `${source}\n${RUNTIME_SCRIPT}`,
|
|
184
|
+
motion: undefined,
|
|
185
|
+
runtimeOffset: source.length + 1,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const lineStart = source.lastIndexOf("\n", endTag.startOffset - 1) + 1;
|
|
189
|
+
const indentation = source.slice(lineStart, endTag.startOffset);
|
|
190
|
+
if (indentation.trim().length === 0) {
|
|
191
|
+
const before = source.slice(0, lineStart);
|
|
192
|
+
const after = source.slice(lineStart);
|
|
193
|
+
const prefix = `${before}${indentation} `;
|
|
194
|
+
return {
|
|
195
|
+
document: `${prefix}${RUNTIME_SCRIPT}\n${after}`,
|
|
196
|
+
motion: undefined,
|
|
197
|
+
runtimeOffset: prefix.length,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const before = source.slice(0, endTag.startOffset);
|
|
201
|
+
const after = source.slice(endTag.startOffset);
|
|
202
|
+
return {
|
|
203
|
+
document: `${before}\n${RUNTIME_SCRIPT}\n${after}`,
|
|
204
|
+
motion: undefined,
|
|
205
|
+
runtimeOffset: before.length + 1,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
// ── Parsed-tree queries
|
|
209
|
+
function collectScripts(node) {
|
|
210
|
+
const scripts = [];
|
|
211
|
+
visit(node, (element) => {
|
|
212
|
+
if (element.tagName === "script") {
|
|
213
|
+
scripts.push(element);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
return scripts;
|
|
217
|
+
}
|
|
218
|
+
function findElement(node, name) {
|
|
219
|
+
const pending = [node];
|
|
220
|
+
while (pending.length > 0) {
|
|
221
|
+
const current = pending.pop();
|
|
222
|
+
if (current === undefined) {
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
if ("tagName" in current && current.tagName === name) {
|
|
226
|
+
return current;
|
|
227
|
+
}
|
|
228
|
+
if ("childNodes" in current) {
|
|
229
|
+
pushChildren(pending, current.childNodes);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
function childElements(parent, name) {
|
|
235
|
+
return parent.childNodes.filter((child) => "tagName" in child && child.tagName === name);
|
|
236
|
+
}
|
|
237
|
+
function visit(node, visitor) {
|
|
238
|
+
const pending = [node];
|
|
239
|
+
while (pending.length > 0) {
|
|
240
|
+
const current = pending.pop();
|
|
241
|
+
if (current === undefined) {
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
if ("tagName" in current) {
|
|
245
|
+
visitor(current);
|
|
246
|
+
}
|
|
247
|
+
if ("childNodes" in current) {
|
|
248
|
+
pushChildren(pending, current.childNodes);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function pushChildren(pending, children) {
|
|
253
|
+
for (let index = children.length - 1; index >= 0; index -= 1) {
|
|
254
|
+
const child = children[index];
|
|
255
|
+
if (child !== undefined) {
|
|
256
|
+
pending.push(child);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function isMotionScript(element) {
|
|
261
|
+
const attributes = new Map(element.attrs.map(({ name, value }) => [name, value]));
|
|
262
|
+
return (attributes.get("type") === "module" &&
|
|
263
|
+
attributes.has(MOTION_ATTRIBUTE) &&
|
|
264
|
+
!attributes.has("src"));
|
|
265
|
+
}
|
|
266
|
+
function projectOffset(offset, ranges) {
|
|
267
|
+
let projected = offset;
|
|
268
|
+
for (const range of ranges) {
|
|
269
|
+
if (range.start < offset && offset < range.end) {
|
|
270
|
+
throw new AuthoredHtmlError("browser projection cannot remove its runtime entry");
|
|
271
|
+
}
|
|
272
|
+
if (range.end <= offset) {
|
|
273
|
+
projected -= range.end - range.start;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return projected;
|
|
277
|
+
}
|
|
278
|
+
function collectCompilerFacts(node, source, ranges) {
|
|
279
|
+
const pending = [node];
|
|
280
|
+
while (pending.length > 0) {
|
|
281
|
+
const current = pending.pop();
|
|
282
|
+
if (current === undefined) {
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
if ("tagName" in current) {
|
|
286
|
+
if (isCompilerOnlyElement(current.tagName)) {
|
|
287
|
+
ranges.push(removableElementRange(source, current));
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
collectCompilerAttributes(current, source, ranges);
|
|
291
|
+
}
|
|
292
|
+
if ("childNodes" in current) {
|
|
293
|
+
pushChildren(pending, current.childNodes);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function collectCompilerAttributes(element, source, ranges) {
|
|
298
|
+
for (const attribute of element.attrs) {
|
|
299
|
+
if (!isCompilerOnlyAttribute(element.tagName, attribute.name)) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const location = element.sourceCodeLocation?.attrs?.[attribute.name];
|
|
303
|
+
if (location === undefined) {
|
|
304
|
+
throw new AuthoredHtmlError(`browser projection cannot locate ${attribute.name} on <${element.tagName}>`);
|
|
305
|
+
}
|
|
306
|
+
ranges.push(attributeRange(source, location));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function attributeRange(source, location) {
|
|
310
|
+
let start = location.startOffset;
|
|
311
|
+
while (start > 0 && isHtmlSpace(source[start - 1])) {
|
|
312
|
+
start -= 1;
|
|
313
|
+
}
|
|
314
|
+
return { end: location.endOffset, start };
|
|
315
|
+
}
|
|
316
|
+
function isCompilerOnlyElement(tagName) {
|
|
317
|
+
// This closed list mirrors the language boundary, not HTML vocabulary.
|
|
318
|
+
// New spellings require the Rust compiler, specification, and identity
|
|
319
|
+
// conformance to change together before they may disappear from the browser.
|
|
320
|
+
return (tagName === "om-cues" ||
|
|
321
|
+
tagName === "om-music" ||
|
|
322
|
+
tagName === "om-sfx" ||
|
|
323
|
+
tagName === "om-vo");
|
|
324
|
+
}
|
|
325
|
+
function isCompilerOnlyAttribute(tagName, attributeName) {
|
|
326
|
+
switch (tagName) {
|
|
327
|
+
case "om-cta":
|
|
328
|
+
case "om-title":
|
|
329
|
+
return attributeName === "cue" || attributeName === "delay";
|
|
330
|
+
case "om-shot":
|
|
331
|
+
return attributeName === "duration";
|
|
332
|
+
case "video":
|
|
333
|
+
return attributeName === "delay" || attributeName === "src";
|
|
334
|
+
default:
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function isHtmlSpace(character) {
|
|
339
|
+
return (character === "\t" ||
|
|
340
|
+
character === "\n" ||
|
|
341
|
+
character === "\f" ||
|
|
342
|
+
character === "\r" ||
|
|
343
|
+
character === " ");
|
|
344
|
+
}
|
|
345
|
+
function elementRange(element) {
|
|
346
|
+
const location = element.sourceCodeLocation;
|
|
347
|
+
if (location == null) {
|
|
348
|
+
throw new AuthoredHtmlError(`browser projection cannot locate <${element.tagName}>`);
|
|
349
|
+
}
|
|
350
|
+
return { end: location.endOffset, start: location.startOffset };
|
|
351
|
+
}
|
|
352
|
+
function removableElementRange(source, element) {
|
|
353
|
+
const range = elementRange(element);
|
|
354
|
+
// Indentation on an otherwise empty line belongs to the removed element.
|
|
355
|
+
// Retaining it creates a browser text node and invalid source artifacts.
|
|
356
|
+
const lineStart = source.lastIndexOf("\n", range.start - 1) + 1;
|
|
357
|
+
const newline = source.indexOf("\n", range.end);
|
|
358
|
+
const lineEnd = newline === -1 ? source.length : newline + 1;
|
|
359
|
+
const leading = source.slice(lineStart, range.start);
|
|
360
|
+
const trailing = source.slice(range.end, lineEnd);
|
|
361
|
+
if (isHtmlWhitespace(leading) && isHtmlWhitespace(trailing)) {
|
|
362
|
+
return { end: lineEnd, start: lineStart };
|
|
363
|
+
}
|
|
364
|
+
return range;
|
|
365
|
+
}
|
|
366
|
+
function isHtmlWhitespace(value) {
|
|
367
|
+
for (const character of value) {
|
|
368
|
+
if (!isHtmlSpace(character)) {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
function removeRanges(source, ranges) {
|
|
375
|
+
const ordered = ranges.toSorted((left, right) => left.start - right.start);
|
|
376
|
+
const parts = [];
|
|
377
|
+
let cursor = 0;
|
|
378
|
+
for (const range of ordered) {
|
|
379
|
+
if (range.start < cursor || range.end < range.start) {
|
|
380
|
+
throw new AuthoredHtmlError("browser projection ranges overlap");
|
|
381
|
+
}
|
|
382
|
+
parts.push(source.slice(cursor, range.start));
|
|
383
|
+
cursor = range.end;
|
|
384
|
+
}
|
|
385
|
+
parts.push(source.slice(cursor));
|
|
386
|
+
return parts.join("");
|
|
387
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Executable boundary for one bounded presentation build.
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { BundleError, bundlePresentation, } from "./presentation.js";
|
|
6
|
+
import { BUNDLE_FRAME_BEHAVIORS, BUNDLE_TEMPORAL_CAPABILITIES, BUNDLE_VISUAL_CAPABILITIES, } from "./generated/bundle-manifest.js";
|
|
7
|
+
const USAGE = [
|
|
8
|
+
"Usage: onmark-bundle",
|
|
9
|
+
" --html <path>",
|
|
10
|
+
" [--resolve-directory <directory>]",
|
|
11
|
+
" --output <directory>",
|
|
12
|
+
" --max-output-bytes <bytes>",
|
|
13
|
+
" --frame-behavior <perFrame|placementBounded>",
|
|
14
|
+
" --temporal-capability <sequential|randomAccess>",
|
|
15
|
+
" --visual-capability <browserComposite|separableOverlay>",
|
|
16
|
+
"",
|
|
17
|
+
].join("\n");
|
|
18
|
+
// ── Execution ──
|
|
19
|
+
try {
|
|
20
|
+
const command = parseArguments(process.argv.slice(2));
|
|
21
|
+
if (command.kind === "help") {
|
|
22
|
+
process.stdout.write(USAGE);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
await bundlePresentation(command.options);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const failure = commandFailure(error);
|
|
30
|
+
process.stderr.write(`${failure.kind}: ${failure.message}\n`);
|
|
31
|
+
process.exitCode = failure.kind === "configuration" ? 2 : 1;
|
|
32
|
+
}
|
|
33
|
+
// ── Arguments ──
|
|
34
|
+
function parseArguments(arguments_) {
|
|
35
|
+
const values = commandValues(arguments_);
|
|
36
|
+
if (values.help !== undefined) {
|
|
37
|
+
if (arguments_.length !== 1) {
|
|
38
|
+
throw configuration("--help cannot be combined with bundle options");
|
|
39
|
+
}
|
|
40
|
+
return { kind: "help" };
|
|
41
|
+
}
|
|
42
|
+
const outputDirectory = oneValue(values.output, "--output");
|
|
43
|
+
const maxOutputBytes = parseByteLimit(oneValue(values["max-output-bytes"], "--max-output-bytes"));
|
|
44
|
+
const frameBehavior = parseFrameBehavior(oneValue(values["frame-behavior"], "--frame-behavior"));
|
|
45
|
+
const temporalCapability = parseTemporalCapability(oneValue(values["temporal-capability"], "--temporal-capability"));
|
|
46
|
+
const visualCapability = parseVisualCapability(oneValue(values["visual-capability"], "--visual-capability"));
|
|
47
|
+
const controls = {
|
|
48
|
+
frameBehavior,
|
|
49
|
+
maxOutputBytes,
|
|
50
|
+
outputDirectory,
|
|
51
|
+
temporalCapability,
|
|
52
|
+
visualCapability,
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
kind: "bundle",
|
|
56
|
+
options: {
|
|
57
|
+
...controls,
|
|
58
|
+
document: oneValue(values.html, "--html"),
|
|
59
|
+
...optionalResolveDirectory(values["resolve-directory"]),
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function commandValues(arguments_) {
|
|
64
|
+
try {
|
|
65
|
+
return parseArgs({
|
|
66
|
+
args: arguments_,
|
|
67
|
+
allowPositionals: false,
|
|
68
|
+
options: {
|
|
69
|
+
"frame-behavior": { type: "string", multiple: true },
|
|
70
|
+
help: { type: "boolean" },
|
|
71
|
+
html: { type: "string", multiple: true },
|
|
72
|
+
"max-output-bytes": { type: "string", multiple: true },
|
|
73
|
+
output: { type: "string", multiple: true },
|
|
74
|
+
"resolve-directory": { type: "string", multiple: true },
|
|
75
|
+
"temporal-capability": { type: "string", multiple: true },
|
|
76
|
+
"visual-capability": { type: "string", multiple: true },
|
|
77
|
+
},
|
|
78
|
+
strict: true,
|
|
79
|
+
}).values;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
throw configuration("arguments are invalid", error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function optionalResolveDirectory(values) {
|
|
86
|
+
if (values === undefined) {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
return { resolveDirectory: oneValue(values, "--resolve-directory") };
|
|
90
|
+
}
|
|
91
|
+
function parseTemporalCapability(value) {
|
|
92
|
+
const capability = BUNDLE_TEMPORAL_CAPABILITIES.find((candidate) => candidate === value);
|
|
93
|
+
if (capability !== undefined) {
|
|
94
|
+
return capability;
|
|
95
|
+
}
|
|
96
|
+
throw configuration("--temporal-capability must be sequential or randomAccess");
|
|
97
|
+
}
|
|
98
|
+
function parseVisualCapability(value) {
|
|
99
|
+
const capability = BUNDLE_VISUAL_CAPABILITIES.find((candidate) => candidate === value);
|
|
100
|
+
if (capability !== undefined) {
|
|
101
|
+
return capability;
|
|
102
|
+
}
|
|
103
|
+
throw configuration("--visual-capability must be browserComposite or separableOverlay");
|
|
104
|
+
}
|
|
105
|
+
function parseFrameBehavior(value) {
|
|
106
|
+
const behavior = BUNDLE_FRAME_BEHAVIORS.find((candidate) => candidate === value);
|
|
107
|
+
if (behavior !== undefined) {
|
|
108
|
+
return behavior;
|
|
109
|
+
}
|
|
110
|
+
throw configuration("--frame-behavior must be perFrame or placementBounded");
|
|
111
|
+
}
|
|
112
|
+
function oneValue(values, name) {
|
|
113
|
+
const value = values?.[0];
|
|
114
|
+
if (value === undefined) {
|
|
115
|
+
throw configuration(`${name} is required`);
|
|
116
|
+
}
|
|
117
|
+
if (values !== undefined && values.length > 1) {
|
|
118
|
+
throw configuration(`${name} cannot be repeated`);
|
|
119
|
+
}
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
function parseByteLimit(value) {
|
|
123
|
+
const bytes = Number(value);
|
|
124
|
+
if (!Number.isSafeInteger(bytes) || bytes <= 0) {
|
|
125
|
+
throw configuration("--max-output-bytes must be a positive safe integer");
|
|
126
|
+
}
|
|
127
|
+
return bytes;
|
|
128
|
+
}
|
|
129
|
+
// ── Failures ──
|
|
130
|
+
function configuration(message, cause) {
|
|
131
|
+
return new BundleError("configuration", message, cause);
|
|
132
|
+
}
|
|
133
|
+
function commandFailure(error) {
|
|
134
|
+
if (error instanceof BundleError) {
|
|
135
|
+
return error;
|
|
136
|
+
}
|
|
137
|
+
return new BundleError("output", "unexpected bundler failure", error);
|
|
138
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic DOM extent present in one immutable browser artifact.
|
|
3
|
+
*/
|
|
4
|
+
export type PresentationDocumentScope = "wholeFilm" | "renderRegion";
|
|
5
|
+
/**
|
|
6
|
+
* Proven cadence at which browser-owned pixels may change.
|
|
7
|
+
*
|
|
8
|
+
* Unknown presentation code requires `PerFrame`. Placement-bounded pixels may
|
|
9
|
+
* change only when the browser plan changes its active structural placements,
|
|
10
|
+
* so execution may reuse one exact capture between those boundaries.
|
|
11
|
+
*/
|
|
12
|
+
export type PresentationFrameBehavior = "perFrame" | "placementBounded";
|
|
13
|
+
/**
|
|
14
|
+
* Proven relationship between a presentation's output and requested frames.
|
|
15
|
+
*
|
|
16
|
+
* Unknown presentation code requires `Sequential`. Random access is an
|
|
17
|
+
* explicit claim that each requested frame depends only on immutable inputs
|
|
18
|
+
* and that exact frame.
|
|
19
|
+
*/
|
|
20
|
+
export type PresentationTemporalCapability = "sequential" | "randomAccess";
|
|
21
|
+
/**
|
|
22
|
+
* Version of the immutable presentation-bundle contract.
|
|
23
|
+
*/
|
|
24
|
+
export type BundleVersion = 1;
|
|
25
|
+
/**
|
|
26
|
+
* Proven relationship between browser presentation pixels and primary media.
|
|
27
|
+
*
|
|
28
|
+
* Unknown presentation code requires `BrowserComposite`. A separable overlay
|
|
29
|
+
* is an explicit claim that the browser output is a transparent foreground
|
|
30
|
+
* independent of the primary video beneath it.
|
|
31
|
+
*/
|
|
32
|
+
export type PresentationVisualCapability = "browserComposite" | "separableOverlay";
|
|
33
|
+
/**
|
|
34
|
+
* Stable description of one immutable presentation artifact.
|
|
35
|
+
*/
|
|
36
|
+
export interface BundleManifest {
|
|
37
|
+
bundleId: string;
|
|
38
|
+
documentScope: PresentationDocumentScope;
|
|
39
|
+
entryPoint: "index.html";
|
|
40
|
+
/**
|
|
41
|
+
* @minItems 1
|
|
42
|
+
* @maxItems 99999
|
|
43
|
+
*/
|
|
44
|
+
files: [BundleFile, ...BundleFile[]];
|
|
45
|
+
frameBehavior: PresentationFrameBehavior;
|
|
46
|
+
temporalCapability: PresentationTemporalCapability;
|
|
47
|
+
version: BundleVersion;
|
|
48
|
+
visualCapability: PresentationVisualCapability;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* One content-addressed presentation payload file.
|
|
52
|
+
*/
|
|
53
|
+
export interface BundleFile {
|
|
54
|
+
bytes: number;
|
|
55
|
+
path: string;
|
|
56
|
+
sha256: string;
|
|
57
|
+
}
|
|
58
|
+
export declare const BUNDLE_VERSION: 1;
|
|
59
|
+
export declare const BUNDLE_TEMPORAL_CAPABILITIES: readonly ["sequential", "randomAccess"];
|
|
60
|
+
export declare const BUNDLE_VISUAL_CAPABILITIES: readonly ["browserComposite", "separableOverlay"];
|
|
61
|
+
export declare const BUNDLE_FRAME_BEHAVIORS: readonly ["perFrame", "placementBounded"];
|
|
62
|
+
export declare const BUNDLE_ENTRY_POINT: "index.html";
|
|
63
|
+
export declare const BUNDLE_MANIFEST_FILE: "manifest.json";
|
|
64
|
+
export declare const BUNDLE_ASSET_DIRECTORY: "assets/sha256";
|
|
65
|
+
export declare const BUNDLE_REGION_DIRECTORY: "regions";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Generated by `cargo xtask schema`; edit Rust protocol types instead.
|
|
2
|
+
export const BUNDLE_VERSION = 1;
|
|
3
|
+
export const BUNDLE_TEMPORAL_CAPABILITIES = Object.freeze([
|
|
4
|
+
"sequential",
|
|
5
|
+
"randomAccess",
|
|
6
|
+
]);
|
|
7
|
+
export const BUNDLE_VISUAL_CAPABILITIES = Object.freeze([
|
|
8
|
+
"browserComposite",
|
|
9
|
+
"separableOverlay",
|
|
10
|
+
]);
|
|
11
|
+
export const BUNDLE_FRAME_BEHAVIORS = Object.freeze([
|
|
12
|
+
"perFrame",
|
|
13
|
+
"placementBounded",
|
|
14
|
+
]);
|
|
15
|
+
export const BUNDLE_ENTRY_POINT = "index.html";
|
|
16
|
+
export const BUNDLE_MANIFEST_FILE = "manifest.json";
|
|
17
|
+
export const BUNDLE_ASSET_DIRECTORY = "assets/sha256";
|
|
18
|
+
export const BUNDLE_REGION_DIRECTORY = "regions";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** One local image frozen into the presentation artifact. */
|
|
2
|
+
export interface HtmlImageResource {
|
|
3
|
+
readonly contents: Uint8Array;
|
|
4
|
+
readonly path: string;
|
|
5
|
+
}
|
|
6
|
+
/** Invalid or unreadable native image input. */
|
|
7
|
+
export declare class HtmlImageError extends Error {
|
|
8
|
+
constructor(message: string, options?: ErrorOptions);
|
|
9
|
+
}
|
|
10
|
+
/** Native image bytes exceed the bounded presentation artifact. */
|
|
11
|
+
export declare class HtmlImageLimitError extends HtmlImageError {
|
|
12
|
+
constructor();
|
|
13
|
+
}
|
|
14
|
+
/** Freezes local `img[src]` bytes and rewrites their authored references. */
|
|
15
|
+
export declare function freezeHtmlImages(source: string, directory: string, maxOutputBytes: number): Promise<{
|
|
16
|
+
readonly document: string;
|
|
17
|
+
readonly resources: readonly HtmlImageResource[];
|
|
18
|
+
}>;
|
|
19
|
+
/** Selects only frozen images referenced by one projected document. */
|
|
20
|
+
export declare function projectedImageResources(document: string, resources: readonly HtmlImageResource[]): readonly HtmlImageResource[];
|