@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,337 @@
|
|
|
1
|
+
// Native HTML image freezing at the deterministic bundle boundary.
|
|
2
|
+
// Local bytes become opaque resources; projected documents retain exact uses.
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { open } from "node:fs/promises";
|
|
5
|
+
import { extname, isAbsolute, resolve } from "node:path";
|
|
6
|
+
import { parse } from "parse5";
|
|
7
|
+
import { hasAmbientImageAnimation } from "./image_admission.js";
|
|
8
|
+
const IMAGE_EXTENSIONS = new Set([
|
|
9
|
+
".avif",
|
|
10
|
+
".gif",
|
|
11
|
+
".jpeg",
|
|
12
|
+
".jpg",
|
|
13
|
+
".png",
|
|
14
|
+
".svg",
|
|
15
|
+
".webp",
|
|
16
|
+
]);
|
|
17
|
+
const DATA_IMAGE_EXTENSIONS = new Map([
|
|
18
|
+
["image/avif", ".avif"],
|
|
19
|
+
["image/gif", ".gif"],
|
|
20
|
+
["image/jpeg", ".jpeg"],
|
|
21
|
+
["image/png", ".png"],
|
|
22
|
+
["image/svg+xml", ".svg"],
|
|
23
|
+
["image/webp", ".webp"],
|
|
24
|
+
]);
|
|
25
|
+
const RESOURCE_DIRECTORY = "resources";
|
|
26
|
+
/** Invalid or unreadable native image input. */
|
|
27
|
+
export class HtmlImageError extends Error {
|
|
28
|
+
constructor(message, options) {
|
|
29
|
+
super(message, options);
|
|
30
|
+
this.name = "HtmlImageError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Native image bytes exceed the bounded presentation artifact. */
|
|
34
|
+
export class HtmlImageLimitError extends HtmlImageError {
|
|
35
|
+
constructor() {
|
|
36
|
+
super("authored HTML images exceed the presentation output-byte limit");
|
|
37
|
+
this.name = "HtmlImageLimitError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Freezes local `img[src]` bytes and rewrites their authored references. */
|
|
41
|
+
export async function freezeHtmlImages(source, directory, maxOutputBytes) {
|
|
42
|
+
const references = imageReferences(source);
|
|
43
|
+
const files = new Map();
|
|
44
|
+
const sources = new Map();
|
|
45
|
+
const edits = [];
|
|
46
|
+
let retainedBytes = 0;
|
|
47
|
+
for (const reference of references) {
|
|
48
|
+
const inline = inlineImageSource(reference.source);
|
|
49
|
+
if (inline !== undefined) {
|
|
50
|
+
rejectAmbientAnimation("inline image", inline.extension, inline.contents);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const local = localImageSource(reference.source);
|
|
54
|
+
const absolute = resolve(directory, local.path);
|
|
55
|
+
let path = sources.get(absolute);
|
|
56
|
+
if (path === undefined) {
|
|
57
|
+
const contents = await readBoundedImage(absolute, maxOutputBytes);
|
|
58
|
+
rejectAmbientAnimation(local.path, local.extension, contents);
|
|
59
|
+
path = resourcePath(contents, local.extension);
|
|
60
|
+
sources.set(absolute, path);
|
|
61
|
+
if (!files.has(path)) {
|
|
62
|
+
retainedBytes = retainBytes(retainedBytes, contents.byteLength, maxOutputBytes);
|
|
63
|
+
files.set(path, contents);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
edits.push({
|
|
67
|
+
...reference.valueRange,
|
|
68
|
+
replacement: `./${path}${local.suffix}`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const resources = [...files]
|
|
72
|
+
.sort(([left], [right]) => comparePath(left, right))
|
|
73
|
+
.map(([path, contents]) => Object.freeze({ contents, path }));
|
|
74
|
+
return Object.freeze({
|
|
75
|
+
document: applyEdits(source, edits),
|
|
76
|
+
resources: Object.freeze(resources),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/** Selects only frozen images referenced by one projected document. */
|
|
80
|
+
export function projectedImageResources(document, resources) {
|
|
81
|
+
const referenced = new Set(imageReferences(document)
|
|
82
|
+
.map(({ source }) => bundledResourcePath(source))
|
|
83
|
+
.filter((path) => path !== undefined));
|
|
84
|
+
return resources.filter(({ path }) => referenced.has(path));
|
|
85
|
+
}
|
|
86
|
+
function imageReferences(source) {
|
|
87
|
+
const references = [];
|
|
88
|
+
visit(parseDocument(source), (element) => {
|
|
89
|
+
rejectResponsiveImageSource(element);
|
|
90
|
+
if (element.tagName !== "img") {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const attribute = element.attrs.find(({ name }) => name === "src");
|
|
94
|
+
if (attribute === undefined) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const location = element.sourceCodeLocation?.attrs?.["src"];
|
|
98
|
+
if (location === undefined) {
|
|
99
|
+
throw new HtmlImageError("cannot locate src on authored <img>");
|
|
100
|
+
}
|
|
101
|
+
references.push({
|
|
102
|
+
source: attribute.value,
|
|
103
|
+
valueRange: attributeValueRange(source, location),
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
return references;
|
|
107
|
+
}
|
|
108
|
+
function rejectResponsiveImageSource(element) {
|
|
109
|
+
if ((element.tagName === "img" || element.tagName === "source") &&
|
|
110
|
+
element.attrs.some(({ name }) => name === "srcset")) {
|
|
111
|
+
throw new HtmlImageError("authored HTML images must use src; srcset is not yet supported");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function attributeValueRange(source, location) {
|
|
115
|
+
const attribute = source.slice(location.startOffset, location.endOffset);
|
|
116
|
+
const equals = attribute.indexOf("=");
|
|
117
|
+
if (equals === -1) {
|
|
118
|
+
throw new HtmlImageError("local image src must have a value");
|
|
119
|
+
}
|
|
120
|
+
let start = equals + 1;
|
|
121
|
+
while (start < attribute.length && isHtmlSpace(attribute[start])) {
|
|
122
|
+
start += 1;
|
|
123
|
+
}
|
|
124
|
+
const quote = attribute[start];
|
|
125
|
+
return quote === '"' || quote === "'"
|
|
126
|
+
? {
|
|
127
|
+
end: location.endOffset - 1,
|
|
128
|
+
start: location.startOffset + start + 1,
|
|
129
|
+
}
|
|
130
|
+
: {
|
|
131
|
+
end: location.endOffset,
|
|
132
|
+
start: location.startOffset + start,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function localImageSource(source) {
|
|
136
|
+
if (source.length === 0 || source !== source.trim()) {
|
|
137
|
+
throw new HtmlImageError("local image src must be nonempty and trimmed");
|
|
138
|
+
}
|
|
139
|
+
if (source.startsWith("//") || /^[a-z][a-z0-9+.-]*:/iu.test(source)) {
|
|
140
|
+
throw new HtmlImageError("authored HTML images must use a local relative path or data URL");
|
|
141
|
+
}
|
|
142
|
+
const suffixOffset = source.search(/[?#]/u);
|
|
143
|
+
const encodedPath = suffixOffset === -1 ? source : source.slice(0, suffixOffset);
|
|
144
|
+
const suffix = suffixOffset === -1 ? "" : source.slice(suffixOffset);
|
|
145
|
+
const path = decodeImagePath(encodedPath);
|
|
146
|
+
const extension = extname(path).toLowerCase();
|
|
147
|
+
if (!IMAGE_EXTENSIONS.has(extension)) {
|
|
148
|
+
throw new HtmlImageError(`authored HTML image uses unsupported extension ${extension || "(none)"}`);
|
|
149
|
+
}
|
|
150
|
+
return { extension, path, suffix };
|
|
151
|
+
}
|
|
152
|
+
function inlineImageSource(source) {
|
|
153
|
+
if (!/^data:/iu.test(source)) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
const separator = source.indexOf(",");
|
|
157
|
+
if (separator === -1) {
|
|
158
|
+
throw new HtmlImageError("inline image src is not a valid data URL");
|
|
159
|
+
}
|
|
160
|
+
const metadata = source.slice("data:".length, separator).split(";");
|
|
161
|
+
const mime = metadata.shift()?.toLowerCase() ?? "";
|
|
162
|
+
const extension = DATA_IMAGE_EXTENSIONS.get(mime);
|
|
163
|
+
if (extension === undefined) {
|
|
164
|
+
throw new HtmlImageError(`inline image uses unsupported media type ${mime || "(none)"}`);
|
|
165
|
+
}
|
|
166
|
+
const base64 = metadata.some((value) => value.toLowerCase() === "base64");
|
|
167
|
+
const unsupported = metadata.filter((value) => value.length > 0 &&
|
|
168
|
+
value.toLowerCase() !== "base64" &&
|
|
169
|
+
!/^charset=/iu.test(value));
|
|
170
|
+
if (unsupported.length > 0) {
|
|
171
|
+
throw new HtmlImageError("inline image data URL has unsupported metadata");
|
|
172
|
+
}
|
|
173
|
+
const payload = source.slice(separator + 1);
|
|
174
|
+
const contents = base64
|
|
175
|
+
? decodeBase64Image(payload)
|
|
176
|
+
: decodeTextImage(payload, extension);
|
|
177
|
+
return { contents, extension };
|
|
178
|
+
}
|
|
179
|
+
function decodeBase64Image(source) {
|
|
180
|
+
if (!/^(?:[a-z0-9+/]{4})*(?:[a-z0-9+/]{2}==|[a-z0-9+/]{3}=)?$/iu.test(source)) {
|
|
181
|
+
throw new HtmlImageError("inline image data URL has invalid base64");
|
|
182
|
+
}
|
|
183
|
+
return Buffer.from(source, "base64");
|
|
184
|
+
}
|
|
185
|
+
function decodeTextImage(source, extension) {
|
|
186
|
+
if (extension !== ".svg") {
|
|
187
|
+
throw new HtmlImageError("inline binary images must use base64 data URLs");
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
return new TextEncoder().encode(decodeURIComponent(source));
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
throw new HtmlImageError("inline image data URL is not valid UTF-8", {
|
|
194
|
+
cause: error,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function rejectAmbientAnimation(source, extension, contents) {
|
|
199
|
+
if (hasAmbientImageAnimation(extension, contents)) {
|
|
200
|
+
throw new HtmlImageError(`${source} contains ambient animation; use an Onmark frame effect`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function decodeImagePath(source) {
|
|
204
|
+
let path;
|
|
205
|
+
try {
|
|
206
|
+
path = decodeURIComponent(source);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
throw new HtmlImageError("local image src is not a valid URL", {
|
|
210
|
+
cause: error,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (path.includes("\\") || isAbsolute(path)) {
|
|
214
|
+
throw new HtmlImageError("authored HTML images must use a portable relative path");
|
|
215
|
+
}
|
|
216
|
+
return path;
|
|
217
|
+
}
|
|
218
|
+
function bundledResourcePath(source) {
|
|
219
|
+
const suffixOffset = source.search(/[?#]/u);
|
|
220
|
+
const path = suffixOffset === -1 ? source : source.slice(0, suffixOffset);
|
|
221
|
+
const prefix = `./${RESOURCE_DIRECTORY}/`;
|
|
222
|
+
return path.startsWith(prefix) ? path.slice(2) : undefined;
|
|
223
|
+
}
|
|
224
|
+
// ── Frozen bytes
|
|
225
|
+
async function readBoundedImage(path, limit) {
|
|
226
|
+
let file;
|
|
227
|
+
try {
|
|
228
|
+
file = await open(path, "r");
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
throw new HtmlImageError(`cannot open local image ${path}`, {
|
|
232
|
+
cause: error,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const chunks = [];
|
|
237
|
+
let length = 0;
|
|
238
|
+
while (length <= limit) {
|
|
239
|
+
const capacity = Math.min(64 * 1024, limit - length + 1);
|
|
240
|
+
const chunk = Buffer.allocUnsafe(capacity);
|
|
241
|
+
const { bytesRead } = await file.read(chunk, 0, capacity, length);
|
|
242
|
+
if (bytesRead === 0) {
|
|
243
|
+
return Buffer.concat(chunks, length);
|
|
244
|
+
}
|
|
245
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
246
|
+
length += bytesRead;
|
|
247
|
+
}
|
|
248
|
+
throw new HtmlImageLimitError();
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (error instanceof HtmlImageError) {
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
throw new HtmlImageError(`cannot read local image ${path}`, {
|
|
255
|
+
cause: error,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
await file.close();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function retainBytes(retained, added, limit) {
|
|
263
|
+
if (added > limit - retained) {
|
|
264
|
+
throw new HtmlImageLimitError();
|
|
265
|
+
}
|
|
266
|
+
return retained + added;
|
|
267
|
+
}
|
|
268
|
+
function resourcePath(contents, extension) {
|
|
269
|
+
const digest = createHash("sha256").update(contents).digest("hex");
|
|
270
|
+
return `${RESOURCE_DIRECTORY}/${digest}${extension}`;
|
|
271
|
+
}
|
|
272
|
+
// ── Parsed HTML and edits
|
|
273
|
+
function parseDocument(source) {
|
|
274
|
+
const errors = [];
|
|
275
|
+
const document = parse(source, {
|
|
276
|
+
onParseError(error) {
|
|
277
|
+
if (error.code !== "missing-doctype") {
|
|
278
|
+
errors.push(error);
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
sourceCodeLocationInfo: true,
|
|
282
|
+
});
|
|
283
|
+
if (errors.length > 0) {
|
|
284
|
+
throw new HtmlImageError(`cannot freeze images after parse error ${errors[0].code}`);
|
|
285
|
+
}
|
|
286
|
+
return document;
|
|
287
|
+
}
|
|
288
|
+
function visit(node, visitor) {
|
|
289
|
+
const pending = [node];
|
|
290
|
+
while (pending.length > 0) {
|
|
291
|
+
const current = pending.pop();
|
|
292
|
+
if (current === undefined) {
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
if ("tagName" in current) {
|
|
296
|
+
visitor(current);
|
|
297
|
+
}
|
|
298
|
+
if ("childNodes" in current) {
|
|
299
|
+
for (let index = current.childNodes.length - 1; index >= 0; index -= 1) {
|
|
300
|
+
const child = current.childNodes[index];
|
|
301
|
+
if (child !== undefined) {
|
|
302
|
+
pending.push(child);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function applyEdits(source, edits) {
|
|
309
|
+
const ordered = edits.toSorted((left, right) => left.start - right.start);
|
|
310
|
+
const parts = [];
|
|
311
|
+
let cursor = 0;
|
|
312
|
+
for (const edit of ordered) {
|
|
313
|
+
if (edit.start < cursor || edit.end < edit.start) {
|
|
314
|
+
throw new HtmlImageError("authored image edits overlap");
|
|
315
|
+
}
|
|
316
|
+
parts.push(source.slice(cursor, edit.start), edit.replacement);
|
|
317
|
+
cursor = edit.end;
|
|
318
|
+
}
|
|
319
|
+
parts.push(source.slice(cursor));
|
|
320
|
+
return parts.join("");
|
|
321
|
+
}
|
|
322
|
+
function comparePath(left, right) {
|
|
323
|
+
if (left < right) {
|
|
324
|
+
return -1;
|
|
325
|
+
}
|
|
326
|
+
if (left > right) {
|
|
327
|
+
return 1;
|
|
328
|
+
}
|
|
329
|
+
return 0;
|
|
330
|
+
}
|
|
331
|
+
function isHtmlSpace(character) {
|
|
332
|
+
return (character === "\t" ||
|
|
333
|
+
character === "\n" ||
|
|
334
|
+
character === "\f" ||
|
|
335
|
+
character === "\r" ||
|
|
336
|
+
character === " ");
|
|
337
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Deterministic admission for image bytes consumed by browser presentations.
|
|
2
|
+
// Static pixels may cross the bundle boundary; self-advancing formats may not.
|
|
3
|
+
import { parse } from "parse5";
|
|
4
|
+
const PNG_SIGNATURE = Uint8Array.from([
|
|
5
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
6
|
+
]);
|
|
7
|
+
const GIF_HEADERS = ["GIF87a", "GIF89a"];
|
|
8
|
+
const SVG_ANIMATION_ELEMENTS = new Set([
|
|
9
|
+
"animate",
|
|
10
|
+
"animatemotion",
|
|
11
|
+
"animatetransform",
|
|
12
|
+
"foreignobject",
|
|
13
|
+
"image",
|
|
14
|
+
"script",
|
|
15
|
+
"set",
|
|
16
|
+
]);
|
|
17
|
+
const CSS_ANIMATION = /(?:@keyframes|\banimation(?:-[a-z-]+)?\s*:|\btransition(?:-[a-z-]+)?\s*:)/iu;
|
|
18
|
+
/** Reports whether browser wall time could change the decoded image pixels. */
|
|
19
|
+
export function hasAmbientImageAnimation(extension, contents) {
|
|
20
|
+
if (gifFrameCount(contents) > 1 ||
|
|
21
|
+
pngHasAnimationControl(contents) ||
|
|
22
|
+
webpHasAnimation(contents) ||
|
|
23
|
+
avifIsImageSequence(contents)) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return extension === ".svg" || looksLikeSvg(contents)
|
|
27
|
+
? svgHasAnimation(contents)
|
|
28
|
+
: false;
|
|
29
|
+
}
|
|
30
|
+
// ── Raster containers
|
|
31
|
+
function pngHasAnimationControl(contents) {
|
|
32
|
+
if (!startsWith(contents, PNG_SIGNATURE)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
let offset = PNG_SIGNATURE.length;
|
|
36
|
+
while (offset + 12 <= contents.length) {
|
|
37
|
+
const length = readU32BigEndian(contents, offset);
|
|
38
|
+
const type = ascii(contents, offset + 4, 4);
|
|
39
|
+
const next = offset + 12 + length;
|
|
40
|
+
if (!Number.isSafeInteger(next) || next > contents.length) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (type === "acTL") {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (type === "IEND") {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
offset = next;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
function gifFrameCount(contents) {
|
|
54
|
+
if (contents.length < 13 || !GIF_HEADERS.includes(ascii(contents, 0, 6))) {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
let offset = 13 + colorTableBytes(contents[10]);
|
|
58
|
+
let frames = 0;
|
|
59
|
+
while (offset < contents.length) {
|
|
60
|
+
switch (contents[offset]) {
|
|
61
|
+
case 0x2c: {
|
|
62
|
+
if (offset + 10 > contents.length) {
|
|
63
|
+
return frames;
|
|
64
|
+
}
|
|
65
|
+
frames += 1;
|
|
66
|
+
if (frames > 1) {
|
|
67
|
+
return frames;
|
|
68
|
+
}
|
|
69
|
+
offset += 10 + colorTableBytes(contents[offset + 9]);
|
|
70
|
+
if (offset >= contents.length) {
|
|
71
|
+
return frames;
|
|
72
|
+
}
|
|
73
|
+
offset = skipSubBlocks(contents, offset + 1);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case 0x21:
|
|
77
|
+
offset = skipSubBlocks(contents, offset + 2);
|
|
78
|
+
break;
|
|
79
|
+
case 0x3b:
|
|
80
|
+
return frames;
|
|
81
|
+
default:
|
|
82
|
+
return frames;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return frames;
|
|
86
|
+
}
|
|
87
|
+
function colorTableBytes(flags) {
|
|
88
|
+
return (flags & 0x80) === 0 ? 0 : 3 * 2 ** ((flags & 0x07) + 1);
|
|
89
|
+
}
|
|
90
|
+
function skipSubBlocks(contents, initialOffset) {
|
|
91
|
+
let offset = initialOffset;
|
|
92
|
+
while (offset < contents.length) {
|
|
93
|
+
const length = contents[offset];
|
|
94
|
+
offset += 1;
|
|
95
|
+
if (length === 0) {
|
|
96
|
+
return offset;
|
|
97
|
+
}
|
|
98
|
+
offset += length;
|
|
99
|
+
}
|
|
100
|
+
return contents.length;
|
|
101
|
+
}
|
|
102
|
+
function webpHasAnimation(contents) {
|
|
103
|
+
if (ascii(contents, 0, 4) !== "RIFF" || ascii(contents, 8, 4) !== "WEBP") {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
let offset = 12;
|
|
107
|
+
while (offset + 8 <= contents.length) {
|
|
108
|
+
const type = ascii(contents, offset, 4);
|
|
109
|
+
const length = readU32LittleEndian(contents, offset + 4);
|
|
110
|
+
const payload = offset + 8;
|
|
111
|
+
const next = payload + length + (length % 2);
|
|
112
|
+
if (!Number.isSafeInteger(next) || next > contents.length) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (type === "ANIM" || type === "ANMF") {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (type === "VP8X" && length > 0 && (contents[payload] & 0x02) !== 0) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
offset = next;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
function avifIsImageSequence(contents) {
|
|
126
|
+
if (ascii(contents, 4, 4) !== "ftyp" || contents.length < 16) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const boxLength = readU32BigEndian(contents, 0);
|
|
130
|
+
if (boxLength < 16 || boxLength > contents.length) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
for (let offset = 8; offset + 4 <= boxLength; offset += 4) {
|
|
134
|
+
const brand = ascii(contents, offset, 4);
|
|
135
|
+
if (brand === "avis" || brand === "msf1") {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
// ── SVG document
|
|
142
|
+
function svgHasAnimation(contents) {
|
|
143
|
+
let source;
|
|
144
|
+
try {
|
|
145
|
+
source = new TextDecoder("utf-8", { fatal: true }).decode(contents);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
const pending = [parse(source)];
|
|
151
|
+
while (pending.length > 0) {
|
|
152
|
+
const current = pending.pop();
|
|
153
|
+
if (current === undefined) {
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
if ("tagName" in current && svgElementHasAnimation(current)) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
if ("childNodes" in current) {
|
|
160
|
+
for (let index = current.childNodes.length - 1; index >= 0; index -= 1) {
|
|
161
|
+
const child = current.childNodes[index];
|
|
162
|
+
if (child !== undefined) {
|
|
163
|
+
pending.push(child);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
function looksLikeSvg(contents) {
|
|
171
|
+
const prefix = new TextDecoder()
|
|
172
|
+
.decode(contents.subarray(0, 512))
|
|
173
|
+
.trimStart();
|
|
174
|
+
return prefix.startsWith("<svg") || prefix.startsWith("<?xml");
|
|
175
|
+
}
|
|
176
|
+
function svgElementHasAnimation(element) {
|
|
177
|
+
if (SVG_ANIMATION_ELEMENTS.has(element.tagName.toLowerCase())) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
for (const attribute of element.attrs) {
|
|
181
|
+
if (attribute.name.toLowerCase().startsWith("on")) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
if (attribute.name === "style" && cssCanAnimate(attribute.value)) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (element.tagName === "style") {
|
|
189
|
+
return element.childNodes.some((child) => "value" in child && cssCanAnimate(child.value));
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
function cssCanAnimate(source) {
|
|
194
|
+
return source.includes("\\") || CSS_ANIMATION.test(source);
|
|
195
|
+
}
|
|
196
|
+
// ── Byte reading
|
|
197
|
+
function startsWith(contents, prefix) {
|
|
198
|
+
return prefix.every((byte, index) => contents[index] === byte);
|
|
199
|
+
}
|
|
200
|
+
function ascii(contents, offset, length) {
|
|
201
|
+
if (offset < 0 || offset + length > contents.length) {
|
|
202
|
+
return "";
|
|
203
|
+
}
|
|
204
|
+
return String.fromCharCode(...contents.subarray(offset, offset + length));
|
|
205
|
+
}
|
|
206
|
+
function readU32BigEndian(contents, offset) {
|
|
207
|
+
return (contents[offset] * 0x1_00_00_00 +
|
|
208
|
+
(contents[offset + 1] << 16) +
|
|
209
|
+
(contents[offset + 2] << 8) +
|
|
210
|
+
contents[offset + 3]);
|
|
211
|
+
}
|
|
212
|
+
function readU32LittleEndian(contents, offset) {
|
|
213
|
+
return (contents[offset] +
|
|
214
|
+
(contents[offset + 1] << 8) +
|
|
215
|
+
(contents[offset + 2] << 16) +
|
|
216
|
+
contents[offset + 3] * 0x1_00_00_00);
|
|
217
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { BundleError, bundlePresentation, type BundleArtifact, type BundleErrorKind, type BundleFile, type BundleManifest, type BundleOptions, } from "./presentation.js";
|
|
2
|
+
export { BUNDLE_ASSET_DIRECTORY, BUNDLE_ENTRY_POINT, BUNDLE_MANIFEST_FILE, BUNDLE_REGION_DIRECTORY, BUNDLE_TEMPORAL_CAPABILITIES, BUNDLE_VERSION, } from "./generated/bundle-manifest.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Public facade for deterministic presentation bundling.
|
|
2
|
+
export { BundleError, bundlePresentation, } from "./presentation.js";
|
|
3
|
+
export { BUNDLE_ASSET_DIRECTORY, BUNDLE_ENTRY_POINT, BUNDLE_MANIFEST_FILE, BUNDLE_REGION_DIRECTORY, BUNDLE_TEMPORAL_CAPABILITIES, BUNDLE_VERSION, } from "./generated/bundle-manifest.js";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { PresentationFrameBehavior, PresentationTemporalCapability, PresentationVisualCapability } from "../../../runtime/dist/src/index.js";
|
|
2
|
+
import { type BundleFile as WireBundleFile, type BundleManifest as WireBundleManifest } from "./generated/bundle-manifest.js";
|
|
3
|
+
type Immutable<T> = T extends object ? {
|
|
4
|
+
readonly [Key in keyof T]: Immutable<T[Key]>;
|
|
5
|
+
} : T;
|
|
6
|
+
/** Immutable view of one Rust-owned bundle payload entry. */
|
|
7
|
+
export type BundleFile = Immutable<WireBundleFile>;
|
|
8
|
+
/** Immutable view of the versioned Rust-owned bundle manifest. */
|
|
9
|
+
export type BundleManifest = Immutable<WireBundleManifest>;
|
|
10
|
+
/** Explicit inputs for one authored HTML document. */
|
|
11
|
+
export interface BundleOptions {
|
|
12
|
+
/** Authored document or a private snapshot containing its exact bytes. */
|
|
13
|
+
readonly document: string;
|
|
14
|
+
/** Base for inline-module imports when `document` is a private snapshot. */
|
|
15
|
+
readonly resolveDirectory?: string;
|
|
16
|
+
readonly outputDirectory: string;
|
|
17
|
+
readonly maxOutputBytes: number;
|
|
18
|
+
readonly temporalCapability: PresentationTemporalCapability;
|
|
19
|
+
readonly visualCapability: PresentationVisualCapability;
|
|
20
|
+
readonly frameBehavior: PresentationFrameBehavior;
|
|
21
|
+
}
|
|
22
|
+
/** Published directory and its owned immutable manifest snapshot. */
|
|
23
|
+
export interface BundleArtifact {
|
|
24
|
+
readonly directory: string;
|
|
25
|
+
readonly manifest: BundleManifest;
|
|
26
|
+
readonly regions: readonly BundleRegionArtifact[];
|
|
27
|
+
}
|
|
28
|
+
/** One shot-scoped immutable browser artifact within a random-access build. */
|
|
29
|
+
export interface BundleRegionArtifact {
|
|
30
|
+
readonly directory: string;
|
|
31
|
+
readonly manifest: BundleManifest;
|
|
32
|
+
}
|
|
33
|
+
export type BundleErrorKind = "configuration" | "build" | "output" | "outputLimit";
|
|
34
|
+
/** Typed failure from presentation compilation or artifact publication. */
|
|
35
|
+
export declare class BundleError extends Error {
|
|
36
|
+
readonly kind: BundleErrorKind;
|
|
37
|
+
constructor(kind: BundleErrorKind, message: string, cause?: unknown);
|
|
38
|
+
}
|
|
39
|
+
/** Builds one HTML-authored presentation without a parallel entry file. */
|
|
40
|
+
export declare function bundlePresentation(options: BundleOptions): Promise<BundleArtifact>;
|
|
41
|
+
export {};
|