@tinycloud/share-sdk 1.2.0-beta.5 → 1.2.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/html-bundle.cjs +975 -0
- package/dist/html-bundle.cjs.map +1 -0
- package/dist/html-bundle.d.cts +136 -0
- package/dist/html-bundle.d.ts +136 -0
- package/dist/html-bundle.js +930 -0
- package/dist/html-bundle.js.map +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/share-index.cjs.map +1 -1
- package/dist/share-index.js.map +1 -1
- package/dist/share-root.cjs +96 -0
- package/dist/share-root.cjs.map +1 -1
- package/dist/share-root.d.cts +74 -12
- package/dist/share-root.d.ts +74 -12
- package/dist/share-root.js +94 -0
- package/dist/share-root.js.map +1 -1
- package/package.json +8 -1
|
@@ -0,0 +1,975 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/html-bundle/index.ts
|
|
21
|
+
var html_bundle_exports = {};
|
|
22
|
+
__export(html_bundle_exports, {
|
|
23
|
+
ARTIFACT_ENTRY: () => ARTIFACT_ENTRY,
|
|
24
|
+
ArtifactBundleError: () => ArtifactBundleError,
|
|
25
|
+
MAX_ARTIFACT_ATTRIBUTES_PER_ELEMENT: () => MAX_ARTIFACT_ATTRIBUTES_PER_ELEMENT,
|
|
26
|
+
MAX_ARTIFACT_COMMENTS: () => MAX_ARTIFACT_COMMENTS,
|
|
27
|
+
MAX_ARTIFACT_CSS_IMPORT_DEPTH: () => MAX_ARTIFACT_CSS_IMPORT_DEPTH,
|
|
28
|
+
MAX_ARTIFACT_FILES: () => MAX_ARTIFACT_FILES,
|
|
29
|
+
MAX_ARTIFACT_REFERENCES: () => MAX_ARTIFACT_REFERENCES,
|
|
30
|
+
MAX_ARTIFACT_RENDER_BYTES: () => MAX_ARTIFACT_RENDER_BYTES,
|
|
31
|
+
MAX_ARTIFACT_SOURCE_BYTES: () => MAX_ARTIFACT_SOURCE_BYTES,
|
|
32
|
+
MAX_ARTIFACT_TEXT_BYTES: () => MAX_ARTIFACT_TEXT_BYTES,
|
|
33
|
+
artifactMediaType: () => artifactMediaType,
|
|
34
|
+
assertSelfContainedPage: () => assertSelfContainedPage,
|
|
35
|
+
canonicalArtifactPath: () => canonicalArtifactPath,
|
|
36
|
+
detectHtmlArtifact: () => detectHtmlArtifact,
|
|
37
|
+
inlineHtmlFolder: () => inlineHtmlFolder,
|
|
38
|
+
isPassiveArtifactPath: () => isPassiveArtifactPath,
|
|
39
|
+
normalizeCssEscapes: () => normalizeCssEscapes,
|
|
40
|
+
prepareHtmlArtifact: () => prepareHtmlArtifact,
|
|
41
|
+
resolveArtifactReference: () => resolveArtifactReference
|
|
42
|
+
});
|
|
43
|
+
module.exports = __toCommonJS(html_bundle_exports);
|
|
44
|
+
|
|
45
|
+
// src/filename-policy.ts
|
|
46
|
+
var UNSAFE_FILENAME_CODE_POINT = /[\p{Cc}\p{Cf}\p{Cs}\u2028\u2029]/u;
|
|
47
|
+
function hasUnsafeFilenameCodePoint(value) {
|
|
48
|
+
return UNSAFE_FILENAME_CODE_POINT.test(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/html-bundle/bundle.ts
|
|
52
|
+
var ARTIFACT_ENTRY = "index.html";
|
|
53
|
+
var MAX_ARTIFACT_FILES = 1e3;
|
|
54
|
+
var MAX_ARTIFACT_SOURCE_BYTES = 100 * 1024 * 1024;
|
|
55
|
+
var MAX_ARTIFACT_RENDER_BYTES = 10 * 1024 * 1024;
|
|
56
|
+
var MAX_ARTIFACT_TEXT_BYTES = 5 * 1024 * 1024;
|
|
57
|
+
var MAX_ARTIFACT_REFERENCES = 2e3;
|
|
58
|
+
var MAX_ARTIFACT_CSS_IMPORT_DEPTH = 16;
|
|
59
|
+
var MAX_ARTIFACT_COMMENTS = 2e4;
|
|
60
|
+
var MAX_ARTIFACT_ATTRIBUTES_PER_ELEMENT = 5e3;
|
|
61
|
+
var ArtifactBundleError = class extends Error {
|
|
62
|
+
/**
|
|
63
|
+
* `path` is the bundle file being processed when the refusal happened; `reference` is the reference text inside it
|
|
64
|
+
* that was refused, when there is one. Both are the folder's own text: print them through a terminal-safe printer.
|
|
65
|
+
*/
|
|
66
|
+
constructor(kind, message, path, reference) {
|
|
67
|
+
super(message);
|
|
68
|
+
this.kind = kind;
|
|
69
|
+
this.path = path;
|
|
70
|
+
this.reference = reference;
|
|
71
|
+
this.name = "ArtifactBundleError";
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var MIME_BY_EXTENSION = Object.freeze({
|
|
75
|
+
html: "text/html;charset=utf-8",
|
|
76
|
+
htm: "text/html;charset=utf-8",
|
|
77
|
+
css: "text/css;charset=utf-8",
|
|
78
|
+
js: "text/javascript;charset=utf-8",
|
|
79
|
+
json: "application/json;charset=utf-8",
|
|
80
|
+
txt: "text/plain;charset=utf-8",
|
|
81
|
+
md: "text/markdown;charset=utf-8",
|
|
82
|
+
svg: "image/svg+xml",
|
|
83
|
+
png: "image/png",
|
|
84
|
+
jpg: "image/jpeg",
|
|
85
|
+
jpeg: "image/jpeg",
|
|
86
|
+
gif: "image/gif",
|
|
87
|
+
webp: "image/webp",
|
|
88
|
+
avif: "image/avif",
|
|
89
|
+
ico: "image/x-icon",
|
|
90
|
+
woff: "font/woff",
|
|
91
|
+
woff2: "font/woff2",
|
|
92
|
+
ttf: "font/ttf",
|
|
93
|
+
otf: "font/otf",
|
|
94
|
+
mp3: "audio/mpeg",
|
|
95
|
+
wav: "audio/wav",
|
|
96
|
+
mp4: "video/mp4",
|
|
97
|
+
webm: "video/webm",
|
|
98
|
+
pdf: "application/pdf"
|
|
99
|
+
});
|
|
100
|
+
var SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
101
|
+
var ENCODED_PATH_ALIAS = /%2f|%5c|%2e/i;
|
|
102
|
+
var SAFE_MEDIA_QUERY = /^[a-z0-9\s,().:%\-+/]+$/i;
|
|
103
|
+
var UNSAFE_CLASSIC_SCRIPT = /\b(?:fetch|XMLHttpRequest|WebSocket|EventSource|Worker|SharedWorker|importScripts|sendBeacon)\s*\(|\b(?:window|self|globalThis|top|parent|opener)\s*\.\s*open\s*\(|\b(?:window|document|globalThis|self|top|parent|opener)\s*(?:\.\s*location|\[\s*["']location["']\s*\])|\blocation\s*(?:=|(?:\.\s*|\[\s*["'])href(?:["']\s*\])?|\.assign\s*\(|\.replace\s*\(|\.reload\s*\()|\bimport\s*\(|\beval\s*\(|\bWebAssembly\b/;
|
|
104
|
+
function fail(kind, detail) {
|
|
105
|
+
throw new ArtifactBundleError(kind, detail);
|
|
106
|
+
}
|
|
107
|
+
function defaultParseHtml(html) {
|
|
108
|
+
if (typeof DOMParser === "undefined") {
|
|
109
|
+
throw new TypeError("html-bundle needs a DOM parser: pass parseHtml outside a browser");
|
|
110
|
+
}
|
|
111
|
+
return new DOMParser().parseFromString(html, "text/html");
|
|
112
|
+
}
|
|
113
|
+
function canonicalArtifactPath(input) {
|
|
114
|
+
const value = input.normalize("NFC");
|
|
115
|
+
const encodedLength = new TextEncoder().encode(value).byteLength;
|
|
116
|
+
if (value.length === 0 || encodedLength > 4096 || value.startsWith("/") || value.endsWith("/") || value.includes("\\") || hasUnsafeFilenameCodePoint(value) || ENCODED_PATH_ALIAS.test(value)) {
|
|
117
|
+
fail("malformed", "artifact path is not canonical");
|
|
118
|
+
}
|
|
119
|
+
const segments = value.split("/");
|
|
120
|
+
if (segments.some((segment) => segment.length === 0 || segment === "." || segment === ".." || new TextEncoder().encode(segment).byteLength > 255)) {
|
|
121
|
+
fail("malformed", "artifact path is not canonical");
|
|
122
|
+
}
|
|
123
|
+
return segments.join("/");
|
|
124
|
+
}
|
|
125
|
+
function artifactMediaType(path, supplied) {
|
|
126
|
+
const normalized = supplied?.split(";")[0]?.trim().toLowerCase();
|
|
127
|
+
if (normalized !== void 0 && /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(normalized)) {
|
|
128
|
+
return supplied.trim();
|
|
129
|
+
}
|
|
130
|
+
const extension = path.split(".").at(-1)?.toLowerCase() ?? "";
|
|
131
|
+
return MIME_BY_EXTENSION[extension] ?? "application/octet-stream";
|
|
132
|
+
}
|
|
133
|
+
function resolveArtifactReference(basePath, reference) {
|
|
134
|
+
const raw = trimUrl(reference);
|
|
135
|
+
if (raw.length === 0) return { path: canonicalArtifactPath(basePath), fragment: "", fragmentOnly: true };
|
|
136
|
+
if (raw.startsWith("#")) {
|
|
137
|
+
return { path: canonicalArtifactPath(basePath), fragment: raw, fragmentOnly: true };
|
|
138
|
+
}
|
|
139
|
+
if (raw.startsWith("//") || raw.startsWith("/") || SCHEME.test(raw)) {
|
|
140
|
+
fail("unsupported", "external artifact reference");
|
|
141
|
+
}
|
|
142
|
+
const hashAt = raw.indexOf("#");
|
|
143
|
+
const queryAt = raw.indexOf("?");
|
|
144
|
+
const end = [hashAt, queryAt].filter((value) => value >= 0).reduce((min, value) => Math.min(min, value), raw.length);
|
|
145
|
+
const pathname = raw.slice(0, end);
|
|
146
|
+
const fragment = hashAt >= 0 ? raw.slice(hashAt) : "";
|
|
147
|
+
if (pathname.length === 0) return { path: canonicalArtifactPath(basePath), fragment, fragmentOnly: true };
|
|
148
|
+
if (pathname.includes("\\") || hasUnsafeFilenameCodePoint(pathname) || ENCODED_PATH_ALIAS.test(pathname)) {
|
|
149
|
+
fail("malformed", "artifact reference is not canonical");
|
|
150
|
+
}
|
|
151
|
+
const output = basePath.split("/").slice(0, -1);
|
|
152
|
+
for (const segment of pathname.normalize("NFC").split("/")) {
|
|
153
|
+
if (segment === "" || segment === ".") continue;
|
|
154
|
+
if (segment === "..") {
|
|
155
|
+
if (output.length === 0) fail("malformed", "artifact reference escapes its bundle");
|
|
156
|
+
output.pop();
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
output.push(segment);
|
|
160
|
+
}
|
|
161
|
+
return { path: canonicalArtifactPath(output.join("/")), fragment, fragmentOnly: false };
|
|
162
|
+
}
|
|
163
|
+
function detectHtmlArtifact(paths) {
|
|
164
|
+
const seen = /* @__PURE__ */ new Set();
|
|
165
|
+
let rootCount = 0;
|
|
166
|
+
for (const rawPath of paths) {
|
|
167
|
+
const path = canonicalArtifactPath(rawPath);
|
|
168
|
+
const key = path.toLowerCase();
|
|
169
|
+
if (seen.has(key)) fail("malformed", "artifact paths collide after canonicalization");
|
|
170
|
+
seen.add(key);
|
|
171
|
+
if (path === ARTIFACT_ENTRY) rootCount += 1;
|
|
172
|
+
}
|
|
173
|
+
if (rootCount === 1) return { kind: "html", entry: ARTIFACT_ENTRY };
|
|
174
|
+
return { kind: "folder", reason: "missing-root-index" };
|
|
175
|
+
}
|
|
176
|
+
function toBase64(bytes) {
|
|
177
|
+
let binary = "";
|
|
178
|
+
const chunk = 32768;
|
|
179
|
+
for (let offset = 0; offset < bytes.length; offset += chunk) {
|
|
180
|
+
binary += String.fromCharCode(...bytes.subarray(offset, offset + chunk));
|
|
181
|
+
}
|
|
182
|
+
return btoa(binary);
|
|
183
|
+
}
|
|
184
|
+
function chargeInlinedBytes(context, bytes) {
|
|
185
|
+
context.counter.inlinedBytes += bytes;
|
|
186
|
+
if (context.counter.inlinedBytes > MAX_ARTIFACT_RENDER_BYTES) fail("limit", "artifact inlined resources exceed the render limit");
|
|
187
|
+
}
|
|
188
|
+
function asDataUrl(file, context) {
|
|
189
|
+
if (file.bytes.byteLength > MAX_ARTIFACT_RENDER_BYTES) fail("limit", "artifact resource exceeds render limit");
|
|
190
|
+
const prefix = `data:${artifactMediaType(file.path, file.mediaType)};base64,`;
|
|
191
|
+
chargeInlinedBytes(context, prefix.length + 4 * Math.ceil(file.bytes.byteLength / 3));
|
|
192
|
+
return `${prefix}${toBase64(file.bytes)}`;
|
|
193
|
+
}
|
|
194
|
+
function decodeText(file, context) {
|
|
195
|
+
if (file.bytes.byteLength > MAX_ARTIFACT_TEXT_BYTES) fail("limit", "artifact text file exceeds render limit");
|
|
196
|
+
if (context !== void 0) chargeInlinedBytes(context, file.bytes.byteLength);
|
|
197
|
+
try {
|
|
198
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(file.bytes);
|
|
199
|
+
} catch {
|
|
200
|
+
return fail("malformed", "artifact text is not utf-8");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function assertClassicScript(source) {
|
|
204
|
+
if (UNSAFE_CLASSIC_SCRIPT.test(source)) fail("unsupported", "artifact script uses a blocked browser capability");
|
|
205
|
+
}
|
|
206
|
+
function normalizeMediaQuery(media, detail = "artifact uses an unsupported stylesheet media query") {
|
|
207
|
+
const value = media?.trim() ?? "";
|
|
208
|
+
if (value.length === 0 || value.toLowerCase() === "all") return null;
|
|
209
|
+
if (/[\u0000-\u001f\u007f<>{};"'`\\]/.test(value) || !SAFE_MEDIA_QUERY.test(value)) fail("unsupported", detail);
|
|
210
|
+
return value;
|
|
211
|
+
}
|
|
212
|
+
function wrapMediaCss(source, media, detail) {
|
|
213
|
+
const value = normalizeMediaQuery(media, detail);
|
|
214
|
+
if (value === null) return source;
|
|
215
|
+
return `@media ${value} {
|
|
216
|
+
${source}
|
|
217
|
+
}`;
|
|
218
|
+
}
|
|
219
|
+
function classifyLinkRelation(rel) {
|
|
220
|
+
const tokens = rel?.trim().toLowerCase().split(/\s+/).filter(Boolean) ?? [];
|
|
221
|
+
if (tokens.length === 0) return null;
|
|
222
|
+
const tokenSet = new Set(tokens);
|
|
223
|
+
if (tokenSet.has("stylesheet") && [...tokenSet].every((token) => token === "stylesheet" || token === "alternate")) return "stylesheet";
|
|
224
|
+
if ((tokenSet.has("icon") || tokenSet.has("apple-touch-icon")) && [...tokenSet].every((token) => token === "icon" || token === "apple-touch-icon" || token === "shortcut")) return "icon";
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
var SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
228
|
+
function documentRoots(doc) {
|
|
229
|
+
const roots = [doc];
|
|
230
|
+
for (let index = 0; index < roots.length; index += 1) {
|
|
231
|
+
for (const template of roots[index].querySelectorAll("template")) roots.push(template.content);
|
|
232
|
+
}
|
|
233
|
+
return roots;
|
|
234
|
+
}
|
|
235
|
+
function queryAll(roots, selector) {
|
|
236
|
+
return roots.flatMap((root) => [...root.querySelectorAll(selector)]);
|
|
237
|
+
}
|
|
238
|
+
var URL_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
239
|
+
"href",
|
|
240
|
+
"src",
|
|
241
|
+
"srcset",
|
|
242
|
+
"imagesrcset",
|
|
243
|
+
"poster",
|
|
244
|
+
"action",
|
|
245
|
+
"formaction",
|
|
246
|
+
"cite",
|
|
247
|
+
"data",
|
|
248
|
+
"codebase",
|
|
249
|
+
"longdesc",
|
|
250
|
+
"usemap",
|
|
251
|
+
"ping",
|
|
252
|
+
"background",
|
|
253
|
+
"manifest",
|
|
254
|
+
"icon",
|
|
255
|
+
"archive",
|
|
256
|
+
"classid",
|
|
257
|
+
"profile",
|
|
258
|
+
"lowsrc",
|
|
259
|
+
"dynsrc",
|
|
260
|
+
"attributionsrc",
|
|
261
|
+
"itemtype",
|
|
262
|
+
"itemid"
|
|
263
|
+
]);
|
|
264
|
+
function trimUrl(value) {
|
|
265
|
+
return value.replace(/^[\u0000-\u0020]+|[\u0000-\u0020]+$/g, "");
|
|
266
|
+
}
|
|
267
|
+
function urlText(value) {
|
|
268
|
+
return trimUrl(value).replace(/[\t\n\r]/g, "");
|
|
269
|
+
}
|
|
270
|
+
function isFragmentOnly(trimmed) {
|
|
271
|
+
return /^#[^\u0000-\u0020\u007f-\u009f\\]*$/.test(trimmed);
|
|
272
|
+
}
|
|
273
|
+
var INLINE_DATA_URL = /^data:;base64(?:,[A-Za-z0-9+/=]*)?(?:#[^\u0000-\u0020\\]*)?$/;
|
|
274
|
+
function isInlineReference(value) {
|
|
275
|
+
const text = urlText(value);
|
|
276
|
+
return isFragmentOnly(text) || INLINE_DATA_URL.test(text);
|
|
277
|
+
}
|
|
278
|
+
function assertInlineSrcset(value) {
|
|
279
|
+
for (const raw of value.split(/[\t\n\f\r ]+/)) {
|
|
280
|
+
const token = raw.replace(/,+$/, "");
|
|
281
|
+
if (token === "" || /^\d+(?:\.\d+)?[wx]$/i.test(token)) continue;
|
|
282
|
+
if (!isInlineReference(token)) throw new ArtifactBundleError("unsupported", "artifact page still references a resource", void 0, token);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
var JAVASCRIPT_TYPES = /* @__PURE__ */ new Set([
|
|
286
|
+
"application/ecmascript",
|
|
287
|
+
"application/javascript",
|
|
288
|
+
"application/x-ecmascript",
|
|
289
|
+
"application/x-javascript",
|
|
290
|
+
"text/ecmascript",
|
|
291
|
+
"text/javascript",
|
|
292
|
+
"text/javascript1.0",
|
|
293
|
+
"text/javascript1.1",
|
|
294
|
+
"text/javascript1.2",
|
|
295
|
+
"text/javascript1.3",
|
|
296
|
+
"text/javascript1.4",
|
|
297
|
+
"text/javascript1.5",
|
|
298
|
+
"text/jscript",
|
|
299
|
+
"text/livescript",
|
|
300
|
+
"text/x-ecmascript",
|
|
301
|
+
"text/x-javascript"
|
|
302
|
+
]);
|
|
303
|
+
function isJavaScriptType(value) {
|
|
304
|
+
const type = (value ?? "").replace(/^[\t\n\f\r ]+|[\t\n\f\r ]+$/g, "").toLowerCase();
|
|
305
|
+
if (type === "") return true;
|
|
306
|
+
return JAVASCRIPT_TYPES.has(type.split(";")[0].replace(/[\t\n\f\r ]+$/g, ""));
|
|
307
|
+
}
|
|
308
|
+
var BLOCKED_ELEMENTS = "base, form, iframe, frame, frameset, object, embed, portal, noscript, plaintext, xmp, noembed, noframes, applet, area[href], link:not([href]), [ping]";
|
|
309
|
+
var DATA_URI = /data:[a-z0-9!#$&^_.+/-]*(?:;[a-z0-9!#$&^_.+=-]+)*;base64,[A-Za-z0-9+/=]*/gi;
|
|
310
|
+
function withoutDataUris(html) {
|
|
311
|
+
return html.replace(DATA_URI, "data:;base64,");
|
|
312
|
+
}
|
|
313
|
+
var TEXT_ATTRIBUTES = /^(?:alt|title|placeholder|value|label|content|name|id|class|lang|summary|abbr|datetime|pattern|xmlns(?::[\w.-]+)?|aria-[\w.:-]*|data-[\w.:-]*)$/;
|
|
314
|
+
function looksLikeUrl(raw) {
|
|
315
|
+
const value = raw.replace(/\\/g, "/");
|
|
316
|
+
return /(?:^|[^a-z0-9+.-])(?:https?|ftps?|wss?|file|javascript|vbscript|blob|about|view-source|filesystem):/i.test(value) || /(?:^|[\s,;(="'])\/\/\S/.test(value) || /:\/\//.test(value) || /^\s*\.{0,2}\/[^\s]/.test(value);
|
|
317
|
+
}
|
|
318
|
+
var NAMED_CHARACTERS = {
|
|
319
|
+
amp: "&",
|
|
320
|
+
quot: '"',
|
|
321
|
+
apos: "'",
|
|
322
|
+
lt: "<",
|
|
323
|
+
gt: ">",
|
|
324
|
+
nbsp: "\xA0",
|
|
325
|
+
colon: ":",
|
|
326
|
+
sol: "/",
|
|
327
|
+
bsol: "\\",
|
|
328
|
+
period: ".",
|
|
329
|
+
comma: ",",
|
|
330
|
+
semi: ";",
|
|
331
|
+
equals: "=",
|
|
332
|
+
num: "#",
|
|
333
|
+
quest: "?",
|
|
334
|
+
lpar: "(",
|
|
335
|
+
rpar: ")",
|
|
336
|
+
tab: " ",
|
|
337
|
+
newline: "\n"
|
|
338
|
+
};
|
|
339
|
+
function decodeAttributeValue(value) {
|
|
340
|
+
return value.replace(/&(?:#[xX]([0-9a-fA-F]{1,6})|#([0-9]{1,7})|([A-Za-z]{2,8}));?/g, (whole, hex, decimal, name) => {
|
|
341
|
+
if (name !== void 0) return NAMED_CHARACTERS[name.toLowerCase()] ?? whole;
|
|
342
|
+
const point = Number.parseInt(hex ?? decimal ?? "", hex === void 0 ? 10 : 16);
|
|
343
|
+
return point > 0 && point <= 1114111 && !(point >= 55296 && point <= 57343) ? String.fromCodePoint(point) : "\uFFFD";
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
var MARKUP_BLOCKED_NAMES = /* @__PURE__ */ new Set(["base", "form", "iframe", "frame", "frameset", "object", "embed", "portal", "noscript", "plaintext", "xmp", "noembed", "noframes", "applet"]);
|
|
347
|
+
function unsupportedOutput(detail, reference) {
|
|
348
|
+
throw new ArtifactBundleError("unsupported", detail, void 0, reference);
|
|
349
|
+
}
|
|
350
|
+
function isHrefAnimation(tag, attributeName, value) {
|
|
351
|
+
return /^(?:animate|set|animatetransform|animatemotion)$/i.test(tag) && attributeName.toLowerCase() === "attributename" && /^(?:xlink:)?href$/i.test(trimUrl(value));
|
|
352
|
+
}
|
|
353
|
+
function checkAttribute(name, decoded) {
|
|
354
|
+
const local = name.slice(name.lastIndexOf(":") + 1);
|
|
355
|
+
if (/^on/.test(name)) unsupportedOutput("artifact output contains a blocked html feature");
|
|
356
|
+
if (name === "style") {
|
|
357
|
+
assertOnlyInlineCssReferences(decoded);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (local === "srcset" || local === "imagesrcset") {
|
|
361
|
+
assertInlineSrcset(decoded);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (URL_ATTRIBUTES.has(local)) {
|
|
365
|
+
if (!isInlineReference(decoded)) unsupportedOutput("artifact page still references a resource", decoded);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (TEXT_ATTRIBUTES.test(name)) return;
|
|
369
|
+
if (looksLikeUrl(urlText(decoded))) unsupportedOutput("artifact page still references a resource", decoded);
|
|
370
|
+
if (/url\s*\(|image-set|@import|\\/i.test(decoded) || /(?:^|[^\w-])src\s*\(/i.test(decoded)) assertOnlyInlineCssReferences(decoded);
|
|
371
|
+
}
|
|
372
|
+
function isOwnPolicyMeta(httpEquiv, content) {
|
|
373
|
+
return httpEquiv.trim().toLowerCase() === "content-security-policy" && content === INNER_CSP;
|
|
374
|
+
}
|
|
375
|
+
var HTML_SPACE = "\\t\\n\\f\\r ";
|
|
376
|
+
var TAG_NAME = new RegExp(`[^${HTML_SPACE}/>]*`, "y");
|
|
377
|
+
var ATTRIBUTE_NAME = new RegExp(`=?[^${HTML_SPACE}/>=]*`, "y");
|
|
378
|
+
var UNQUOTED_VALUE = new RegExp(`[^${HTML_SPACE}>]*`, "y");
|
|
379
|
+
var SKIPPED = new RegExp(`[${HTML_SPACE}/]`);
|
|
380
|
+
var SPACE = new RegExp(`[${HTML_SPACE}]`);
|
|
381
|
+
var ODD_TAG_NAME = /[\u00a0\u1680\u180e\u2000-\u200f\u2028-\u202f\u205f-\u2064\u3000\ufeff]/;
|
|
382
|
+
var COMMENT_END = /--!?>/g;
|
|
383
|
+
var SCRIPT_END = /<\/script/gi;
|
|
384
|
+
var STYLE_END = /<\/style/gi;
|
|
385
|
+
function scanMarkup(html, options) {
|
|
386
|
+
const checkReferences = options.references;
|
|
387
|
+
const length = html.length;
|
|
388
|
+
let index = 0;
|
|
389
|
+
let comments = 0;
|
|
390
|
+
const malformed = (detail) => {
|
|
391
|
+
if (checkReferences) unsupportedOutput(detail);
|
|
392
|
+
return true;
|
|
393
|
+
};
|
|
394
|
+
const countComment = () => {
|
|
395
|
+
comments += 1;
|
|
396
|
+
if (comments > MAX_ARTIFACT_COMMENTS) fail("limit", "artifact page has too many comments");
|
|
397
|
+
};
|
|
398
|
+
while (index < length) {
|
|
399
|
+
const open = html.indexOf("<", index);
|
|
400
|
+
if (open === -1) return;
|
|
401
|
+
index = open + 1;
|
|
402
|
+
const next = html[index] ?? "";
|
|
403
|
+
if (html.startsWith("!--", index)) {
|
|
404
|
+
countComment();
|
|
405
|
+
let end;
|
|
406
|
+
if (html[index + 3] === ">") end = index + 4;
|
|
407
|
+
else if (html.startsWith("->", index + 3)) end = index + 5;
|
|
408
|
+
else {
|
|
409
|
+
COMMENT_END.lastIndex = index + 3;
|
|
410
|
+
const close = COMMENT_END.exec(html);
|
|
411
|
+
if (close === null) {
|
|
412
|
+
if (malformed("artifact output has an unterminated comment")) return;
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
end = close.index + close[0].length;
|
|
416
|
+
}
|
|
417
|
+
index = end;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (next === "!" || next === "?" || next === "/") {
|
|
421
|
+
if (next === "?" || next === "!" && !/^!doctype/i.test(html.slice(index, index + 8))) countComment();
|
|
422
|
+
const close = html.indexOf(">", index);
|
|
423
|
+
if (close === -1) {
|
|
424
|
+
if (malformed("artifact output has an unterminated tag")) return;
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
index = close + 1;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (!/[A-Za-z]/.test(next)) continue;
|
|
431
|
+
TAG_NAME.lastIndex = index;
|
|
432
|
+
const rawTag = TAG_NAME.exec(html)[0];
|
|
433
|
+
const tag = rawTag.toLowerCase();
|
|
434
|
+
index = TAG_NAME.lastIndex;
|
|
435
|
+
if (checkReferences) {
|
|
436
|
+
if (ODD_TAG_NAME.test(rawTag)) unsupportedOutput("artifact output has a tag name with an unexpected character");
|
|
437
|
+
if (MARKUP_BLOCKED_NAMES.has(tag)) unsupportedOutput("artifact output contains a blocked html feature");
|
|
438
|
+
}
|
|
439
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
440
|
+
let attributeCount = 0;
|
|
441
|
+
for (; ; ) {
|
|
442
|
+
while (index < length && SKIPPED.test(html[index])) index += 1;
|
|
443
|
+
if (index >= length) {
|
|
444
|
+
if (malformed("artifact output has an unterminated tag")) return;
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (html[index] === ">") {
|
|
448
|
+
index += 1;
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
ATTRIBUTE_NAME.lastIndex = index;
|
|
452
|
+
const rawName = ATTRIBUTE_NAME.exec(html)[0];
|
|
453
|
+
index = ATTRIBUTE_NAME.lastIndex;
|
|
454
|
+
while (index < length && SPACE.test(html[index])) index += 1;
|
|
455
|
+
let rawValue = "";
|
|
456
|
+
if (html[index] === "=") {
|
|
457
|
+
index += 1;
|
|
458
|
+
while (index < length && SPACE.test(html[index])) index += 1;
|
|
459
|
+
const quote = html[index];
|
|
460
|
+
if (quote === '"' || quote === "'") {
|
|
461
|
+
const close = html.indexOf(quote, index + 1);
|
|
462
|
+
if (close === -1) {
|
|
463
|
+
if (malformed("artifact output has an unterminated attribute")) return;
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
rawValue = html.slice(index + 1, close);
|
|
467
|
+
index = close + 1;
|
|
468
|
+
} else {
|
|
469
|
+
UNQUOTED_VALUE.lastIndex = index;
|
|
470
|
+
rawValue = UNQUOTED_VALUE.exec(html)[0];
|
|
471
|
+
index = UNQUOTED_VALUE.lastIndex;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (rawName.length > 0) {
|
|
475
|
+
attributeCount += 1;
|
|
476
|
+
if (attributeCount > MAX_ARTIFACT_ATTRIBUTES_PER_ELEMENT) fail("limit", "artifact element has too many attributes");
|
|
477
|
+
if (!checkReferences) continue;
|
|
478
|
+
const name = rawName.toLowerCase();
|
|
479
|
+
const decoded = decodeAttributeValue(rawValue);
|
|
480
|
+
checkAttribute(name, decoded);
|
|
481
|
+
if (isHrefAnimation(tag, name, decoded)) unsupportedOutput("artifact svg animates a resource reference");
|
|
482
|
+
if (!attributes.has(name)) attributes.set(name, decoded);
|
|
483
|
+
} else if (index < length && html[index] !== ">" && !SKIPPED.test(html[index])) {
|
|
484
|
+
index += 1;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (checkReferences) {
|
|
488
|
+
if (tag === "area" && attributes.has("href") || attributes.has("ping") || tag === "link" && !attributes.has("href")) {
|
|
489
|
+
unsupportedOutput("artifact output contains a blocked html feature");
|
|
490
|
+
}
|
|
491
|
+
if (tag === "meta" && attributes.has("http-equiv") && !isOwnPolicyMeta(attributes.get("http-equiv"), attributes.get("content") ?? null)) {
|
|
492
|
+
unsupportedOutput("artifact output contains a meta http-equiv", attributes.get("http-equiv"));
|
|
493
|
+
}
|
|
494
|
+
if (tag === "script" && !isJavaScriptType(attributes.get("type"))) {
|
|
495
|
+
unsupportedOutput("artifact output holds a script type that is not JavaScript", attributes.get("type"));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (tag === "script") {
|
|
499
|
+
SCRIPT_END.lastIndex = index;
|
|
500
|
+
const close = SCRIPT_END.exec(html);
|
|
501
|
+
if (close === null) return;
|
|
502
|
+
index = close.index;
|
|
503
|
+
} else if (tag === "style" && checkReferences) {
|
|
504
|
+
STYLE_END.lastIndex = index;
|
|
505
|
+
const close = STYLE_END.exec(html);
|
|
506
|
+
assertOnlyInlineCssReferences(html.slice(index, close === null ? length : close.index));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function assertSelfContainedPage(output, options = {}) {
|
|
511
|
+
const page = withoutDataUris(output);
|
|
512
|
+
if (options.only !== "dom") scanMarkup(page, { references: true });
|
|
513
|
+
if (options.only === "bytes") return;
|
|
514
|
+
const doc = (options.parseHtml ?? defaultParseHtml)(page);
|
|
515
|
+
const roots = documentRoots(doc);
|
|
516
|
+
if (queryAll(roots, BLOCKED_ELEMENTS).length > 0) fail("unsupported", "artifact output contains a blocked html feature");
|
|
517
|
+
for (const meta of queryAll(roots, "meta[http-equiv]")) {
|
|
518
|
+
if (!isOwnPolicyMeta(meta.getAttribute("http-equiv") ?? "", meta.getAttribute("content"))) fail("unsupported", "artifact output contains a meta http-equiv");
|
|
519
|
+
}
|
|
520
|
+
if (queryAll(roots, "script[src]").length > 0) fail("unsupported", "artifact output still loads a script");
|
|
521
|
+
for (const script of queryAll(roots, "script")) {
|
|
522
|
+
if (!isJavaScriptType(script.getAttribute("type"))) fail("unsupported", "artifact output holds a script type that is not JavaScript");
|
|
523
|
+
}
|
|
524
|
+
for (const style of queryAll(roots, "style")) assertOnlyInlineCssReferences(style.textContent ?? "");
|
|
525
|
+
for (const element of queryAll(roots, "*")) {
|
|
526
|
+
const inSvg = element.namespaceURI === SVG_NAMESPACE;
|
|
527
|
+
if (inSvg && element.localName === "script") fail("unsupported", "artifact svg contains a script");
|
|
528
|
+
for (const name of element.getAttributeNames()) {
|
|
529
|
+
const lower = name.toLowerCase();
|
|
530
|
+
const local = lower.slice(lower.lastIndexOf(":") + 1);
|
|
531
|
+
const value = element.getAttribute(name) ?? "";
|
|
532
|
+
if (/^on/.test(lower)) fail("unsupported", "artifact output contains a blocked html feature");
|
|
533
|
+
if (URL_ATTRIBUTES.has(local)) {
|
|
534
|
+
if (local === "srcset" || local === "imagesrcset") assertInlineSrcset(value);
|
|
535
|
+
else if (!isInlineReference(value)) throw new ArtifactBundleError("unsupported", "artifact page still references a resource", void 0, value);
|
|
536
|
+
} else if (lower !== "style" && !TEXT_ATTRIBUTES.test(lower) && looksLikeUrl(urlText(value))) {
|
|
537
|
+
throw new ArtifactBundleError("unsupported", "artifact page still references a resource", void 0, value);
|
|
538
|
+
}
|
|
539
|
+
if (lower === "style" || inSvg) assertOnlyInlineCssReferences(value);
|
|
540
|
+
if (isHrefAnimation(element.localName, lower, value)) fail("unsupported", "artifact svg animates a resource reference");
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function verifySelfContained(output, context) {
|
|
545
|
+
assertSelfContainedPage(output, { parseHtml: context.parseHtml });
|
|
546
|
+
}
|
|
547
|
+
function rejectSvgResourceReferences(roots) {
|
|
548
|
+
for (const element of queryAll(roots, "*")) {
|
|
549
|
+
if (element.namespaceURI !== SVG_NAMESPACE) continue;
|
|
550
|
+
const animatesHref = /^(?:animate|set|animatetransform|animatemotion)$/i.test(element.localName) && element.getAttributeNames().some((name) => name.toLowerCase() === "attributename" && /^(?:xlink:)?href$/i.test(trimUrl(element.getAttribute(name) ?? "")));
|
|
551
|
+
if (animatesHref) fail("unsupported", "artifact svg animates a resource reference");
|
|
552
|
+
if (element.localName === "script") fail("unsupported", "artifact svg contains a script");
|
|
553
|
+
for (const attributeName of element.getAttributeNames()) {
|
|
554
|
+
if (attributeName !== "href" && !attributeName.endsWith(":href")) continue;
|
|
555
|
+
if (element.localName === "a" && attributeName === "href") continue;
|
|
556
|
+
if (!isFragmentOnly(urlText(element.getAttribute(attributeName) ?? ""))) fail("unsupported", "artifact svg references a resource");
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function rejectEventHandlerAttributes(roots) {
|
|
561
|
+
for (const element of queryAll(roots, "*")) {
|
|
562
|
+
for (const attributeName of element.getAttributeNames()) {
|
|
563
|
+
if (/^on/i.test(attributeName)) fail("unsupported", "artifact contains a blocked html feature");
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function replaceAsync(input, expression, replacer) {
|
|
568
|
+
const matches = [...input.matchAll(expression)];
|
|
569
|
+
return matches.reduceRight(
|
|
570
|
+
async (next, match) => {
|
|
571
|
+
const value = await next;
|
|
572
|
+
const replacement = await replacer(...match.map((entry) => entry ?? ""));
|
|
573
|
+
return `${value.slice(0, match.index)}${replacement}${value.slice((match.index ?? 0) + match[0].length)}`;
|
|
574
|
+
},
|
|
575
|
+
Promise.resolve(input)
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
function newDeferred() {
|
|
579
|
+
const bytes = new Uint8Array(8);
|
|
580
|
+
if (globalThis.crypto?.getRandomValues !== void 0) globalThis.crypto.getRandomValues(bytes);
|
|
581
|
+
else for (let index = 0; index < bytes.length; index += 1) bytes[index] = Math.floor(Math.random() * 256);
|
|
582
|
+
return { token: Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""), urls: [] };
|
|
583
|
+
}
|
|
584
|
+
function parkUrl(deferred, url) {
|
|
585
|
+
deferred.urls.push(url);
|
|
586
|
+
return `data:;base64,TCPH${deferred.token}x${deferred.urls.length - 1}x`;
|
|
587
|
+
}
|
|
588
|
+
function restoreParkedUrls(page, deferred) {
|
|
589
|
+
if (deferred.urls.length === 0) return page;
|
|
590
|
+
const pattern = new RegExp(`data:;base64,TCPH${deferred.token}x(\\d+)x`, "g");
|
|
591
|
+
return page.replace(pattern, (_whole, index) => deferred.urls[Number(index)]);
|
|
592
|
+
}
|
|
593
|
+
function resolveReferenceFrom(basePath, reference) {
|
|
594
|
+
try {
|
|
595
|
+
return resolveArtifactReference(basePath, reference);
|
|
596
|
+
} catch (error) {
|
|
597
|
+
if (error instanceof ArtifactBundleError) throw new ArtifactBundleError(error.kind, error.message, basePath, reference);
|
|
598
|
+
throw error;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function requiredFile(context, basePath, reference) {
|
|
602
|
+
context.counter.references += 1;
|
|
603
|
+
if (context.counter.references > MAX_ARTIFACT_REFERENCES) fail("limit", "artifact has too many resource references");
|
|
604
|
+
const resolved = resolveReferenceFrom(basePath, reference);
|
|
605
|
+
const file = context.files.get(resolved.path);
|
|
606
|
+
if (file === void 0) throw new ArtifactBundleError("missing", "artifact resource is missing", basePath, reference);
|
|
607
|
+
return { file, resolved };
|
|
608
|
+
}
|
|
609
|
+
var CSS_REFERENCE_NAMES = /* @__PURE__ */ new Set(["url", "image-set", "-webkit-image-set", "src", "import"]);
|
|
610
|
+
function isCssIdentifierCode(code) {
|
|
611
|
+
return code >= 128 || code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95 || code === 45;
|
|
612
|
+
}
|
|
613
|
+
function normalizeCssEscapes(source) {
|
|
614
|
+
let output = "";
|
|
615
|
+
let index = 0;
|
|
616
|
+
const length = source.length;
|
|
617
|
+
while (index < length) {
|
|
618
|
+
const char = source[index];
|
|
619
|
+
if (char === "/" && source[index + 1] === "*") {
|
|
620
|
+
const end = source.indexOf("*/", index + 2);
|
|
621
|
+
const stop = end === -1 ? length : end + 2;
|
|
622
|
+
output += source.slice(index, stop);
|
|
623
|
+
index = stop;
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (char === '"' || char === "'") {
|
|
627
|
+
let cursor = index + 1;
|
|
628
|
+
while (cursor < length && source[cursor] !== char && source[cursor] !== "\n") cursor += source[cursor] === "\\" ? 2 : 1;
|
|
629
|
+
const stop = Math.min(source[cursor] === char ? cursor + 1 : cursor, length);
|
|
630
|
+
output += source.slice(index, stop);
|
|
631
|
+
index = stop;
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
if (char === "\\" || isCssIdentifierCode(source.charCodeAt(index))) {
|
|
635
|
+
let cursor = index;
|
|
636
|
+
let decoded = "";
|
|
637
|
+
let escaped = false;
|
|
638
|
+
while (cursor < length) {
|
|
639
|
+
if (source[cursor] === "\\") {
|
|
640
|
+
const next = source[cursor + 1];
|
|
641
|
+
if (next === void 0) {
|
|
642
|
+
decoded += "\uFFFD";
|
|
643
|
+
escaped = true;
|
|
644
|
+
cursor += 1;
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
if (next === "\n" || next === "\r" || next === "\f") break;
|
|
648
|
+
escaped = true;
|
|
649
|
+
const hex = /^[0-9a-fA-F]{1,6}/.exec(source.slice(cursor + 1, cursor + 7));
|
|
650
|
+
if (hex !== null) {
|
|
651
|
+
const point = Number.parseInt(hex[0], 16);
|
|
652
|
+
cursor += 1 + hex[0].length;
|
|
653
|
+
if (source[cursor] === "\r" && source[cursor + 1] === "\n") cursor += 2;
|
|
654
|
+
else if (/[ \t\n\r\f]/.test(source[cursor] ?? "")) cursor += 1;
|
|
655
|
+
decoded += point === 0 || point > 1114111 || point >= 55296 && point <= 57343 ? "\uFFFD" : String.fromCodePoint(point);
|
|
656
|
+
} else {
|
|
657
|
+
const point = source.codePointAt(cursor + 1);
|
|
658
|
+
decoded += String.fromCodePoint(point);
|
|
659
|
+
cursor += 1 + (point > 65535 ? 2 : 1);
|
|
660
|
+
}
|
|
661
|
+
} else if (isCssIdentifierCode(source.charCodeAt(cursor))) {
|
|
662
|
+
decoded += source[cursor];
|
|
663
|
+
cursor += 1;
|
|
664
|
+
} else break;
|
|
665
|
+
}
|
|
666
|
+
const raw = source.slice(index, cursor);
|
|
667
|
+
const startsReference = escaped && (source[cursor] === "(" || output.endsWith("@")) && CSS_REFERENCE_NAMES.has(decoded.toLowerCase());
|
|
668
|
+
output += startsReference ? decoded : raw;
|
|
669
|
+
index = cursor;
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
output += char;
|
|
673
|
+
index += 1;
|
|
674
|
+
}
|
|
675
|
+
return output;
|
|
676
|
+
}
|
|
677
|
+
function assertOnlyInlineCssReferences(css) {
|
|
678
|
+
const text = normalizeCssEscapes(css);
|
|
679
|
+
if (/@import/i.test(text)) fail("unsupported", "artifact css still imports a stylesheet");
|
|
680
|
+
if (/(?:^|[^\w-])(?:-webkit-)?image-set\s*\(/i.test(text) || /(?:^|[^\w-])src\s*\(/i.test(text)) fail("unsupported", "artifact css uses an unsupported resource function");
|
|
681
|
+
const opened = text.match(/url\(/gi)?.length ?? 0;
|
|
682
|
+
let read = 0;
|
|
683
|
+
for (const match of text.matchAll(/url\([ \t\n\r\f]*(?:"([^"]*)"|'([^']*)'|([^)"' \t\n\r\f]*))[ \t\n\r\f]*\)/gi)) {
|
|
684
|
+
read += 1;
|
|
685
|
+
const value = match[1] ?? match[2] ?? match[3] ?? "";
|
|
686
|
+
if (!isInlineReference(value) || value.includes("\\")) {
|
|
687
|
+
throw new ArtifactBundleError("unsupported", "artifact css references a resource", void 0, value);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (read !== opened) fail("unsupported", "artifact css has a url() that cannot be read");
|
|
691
|
+
}
|
|
692
|
+
async function transformCss(sourceText, basePath, context) {
|
|
693
|
+
if (context.cssStack.length > MAX_ARTIFACT_CSS_IMPORT_DEPTH) fail("limit", "artifact stylesheet import depth exceeded");
|
|
694
|
+
const source = normalizeCssEscapes(sourceText);
|
|
695
|
+
const imported = await replaceAsync(
|
|
696
|
+
source,
|
|
697
|
+
/@import\s+(?:url\(\s*(?:(["'])([^"']+)\1|([^"')\s]+))\s*\)|(["'])([^"']+)\4)\s*([^;]*);/gi,
|
|
698
|
+
async (_whole, _urlQuote, quotedUrl, unquotedUrl, _quote, quotedReference, media) => {
|
|
699
|
+
const reference = quotedUrl || unquotedUrl || quotedReference;
|
|
700
|
+
if (/^(?:layer|supports)\b/i.test(media.trim()) || /\b(?:layer|supports)\b/i.test(media)) fail("unsupported", "artifact uses an unsupported stylesheet import condition");
|
|
701
|
+
const { file, resolved } = requiredFile(context, basePath, reference);
|
|
702
|
+
if (context.cssStack.includes(resolved.path)) fail("unsupported", "artifact stylesheet import cycle");
|
|
703
|
+
const css = await transformCss(decodeText(file, context), resolved.path, { ...context, cssStack: [...context.cssStack, resolved.path] });
|
|
704
|
+
return wrapMediaCss(css, media, "artifact uses an unsupported stylesheet import condition");
|
|
705
|
+
}
|
|
706
|
+
);
|
|
707
|
+
return replaceAsync(
|
|
708
|
+
imported,
|
|
709
|
+
/url\(\s*(["']?)([^"')]+)\1\s*\)/gi,
|
|
710
|
+
async (_whole, _quote, reference) => {
|
|
711
|
+
if (trimUrl(reference).startsWith("#")) return `url("${trimUrl(reference)}")`;
|
|
712
|
+
const { file, resolved } = requiredFile(context, basePath, reference);
|
|
713
|
+
const dataUrl = asDataUrl(file, context);
|
|
714
|
+
return `url("${context.deferred === void 0 ? dataUrl : parkUrl(context.deferred, dataUrl)}${resolved.fragment}")`;
|
|
715
|
+
}
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
function serializeSrcset(value, basePath, context) {
|
|
719
|
+
return value.split(",").map((candidate) => {
|
|
720
|
+
const trimmed = candidate.trim();
|
|
721
|
+
const separator = trimmed.search(/\s/);
|
|
722
|
+
const reference = separator === -1 ? trimmed : trimmed.slice(0, separator);
|
|
723
|
+
const descriptor = separator === -1 ? "" : trimmed.slice(separator).trim();
|
|
724
|
+
const { file, resolved } = requiredFile(context, basePath, reference);
|
|
725
|
+
return `${asDataUrl(file, context)}${resolved.fragment}${descriptor.length === 0 ? "" : ` ${descriptor}`}`;
|
|
726
|
+
}).join(", ");
|
|
727
|
+
}
|
|
728
|
+
var CHILD_BOOTSTRAP = `(function(){
|
|
729
|
+
"use strict";
|
|
730
|
+
document.addEventListener("click",function(event){
|
|
731
|
+
var target=event.target instanceof Element?event.target.closest("a[data-tc-artifact-path]"):null;
|
|
732
|
+
if(!target)return;
|
|
733
|
+
event.preventDefault();
|
|
734
|
+
window.parent.postMessage({type:"navigate",path:target.getAttribute("data-tc-artifact-path"),fragment:target.getAttribute("data-tc-artifact-fragment")||""},"*");
|
|
735
|
+
},true);
|
|
736
|
+
window.addEventListener("message",function(event){
|
|
737
|
+
if(event.source!==window.parent||event.origin!=="null"||!event.data||event.data.type!=="artifact-fragment"||typeof event.data.fragment!=="string")return;
|
|
738
|
+
var fragment=event.data.fragment;
|
|
739
|
+
if(!/^#[^\\u0000-\\u001f\\u007f]{1,1024}$/.test(fragment))return;
|
|
740
|
+
try{var target=document.getElementById(decodeURIComponent(fragment.slice(1)));if(target)target.scrollIntoView();}catch(_){}
|
|
741
|
+
});
|
|
742
|
+
window.parent.postMessage({type:"artifact-ready"},"*");
|
|
743
|
+
})();`;
|
|
744
|
+
var INNER_CSP = [
|
|
745
|
+
"default-src 'none'",
|
|
746
|
+
"script-src 'unsafe-inline'",
|
|
747
|
+
"style-src 'unsafe-inline'",
|
|
748
|
+
"img-src data: blob:",
|
|
749
|
+
"font-src data: blob:",
|
|
750
|
+
"media-src data: blob:",
|
|
751
|
+
"connect-src 'none'",
|
|
752
|
+
"frame-src 'none'",
|
|
753
|
+
"child-src 'none'",
|
|
754
|
+
"object-src 'none'",
|
|
755
|
+
"base-uri 'none'",
|
|
756
|
+
"form-action 'none'"
|
|
757
|
+
].join("; ");
|
|
758
|
+
async function transformHtml(path, file, context) {
|
|
759
|
+
const text = decodeText(file);
|
|
760
|
+
scanMarkup(text, { references: false });
|
|
761
|
+
const doc = context.parseHtml(text);
|
|
762
|
+
const roots = documentRoots(doc);
|
|
763
|
+
const deferred = newDeferred();
|
|
764
|
+
if (doc.querySelector("parsererror") !== null) fail("malformed", "artifact html is malformed");
|
|
765
|
+
if (queryAll(roots, BLOCKED_ELEMENTS).length > 0) {
|
|
766
|
+
fail("unsupported", "artifact contains a blocked html feature");
|
|
767
|
+
}
|
|
768
|
+
rejectEventHandlerAttributes(roots);
|
|
769
|
+
rejectSvgResourceReferences(roots);
|
|
770
|
+
const inlineStyles = queryAll(roots, "style");
|
|
771
|
+
for (const meta of queryAll(roots, "meta[http-equiv]")) meta.remove();
|
|
772
|
+
for (const link of queryAll(roots, "link[href]")) {
|
|
773
|
+
const relation = classifyLinkRelation(link.getAttribute("rel"));
|
|
774
|
+
if (relation === "stylesheet") {
|
|
775
|
+
const { file: stylesheet, resolved } = requiredFile(context, path, link.getAttribute("href") ?? "");
|
|
776
|
+
const style = doc.createElement("style");
|
|
777
|
+
style.textContent = wrapMediaCss(await transformCss(decodeText(stylesheet, context), resolved.path, { ...context, cssStack: [resolved.path] }), link.getAttribute("media"));
|
|
778
|
+
link.replaceWith(style);
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
if (relation === "icon") {
|
|
782
|
+
const { file: icon, resolved } = requiredFile(context, path, link.getAttribute("href") ?? "");
|
|
783
|
+
link.href = `${asDataUrl(icon, context)}${resolved.fragment}`;
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
fail("unsupported", "artifact contains an unsupported link relation");
|
|
787
|
+
}
|
|
788
|
+
for (const style of inlineStyles) {
|
|
789
|
+
style.textContent = wrapMediaCss(await transformCss(style.textContent ?? "", path, { ...context, cssStack: [] }), style.getAttribute("media"));
|
|
790
|
+
}
|
|
791
|
+
for (const element of queryAll(roots, "[style]")) {
|
|
792
|
+
const value = element.getAttribute("style");
|
|
793
|
+
if (value !== null) element.setAttribute("style", await transformCss(value, path, { ...context, cssStack: [], deferred }));
|
|
794
|
+
}
|
|
795
|
+
for (const script of queryAll(roots, "script")) {
|
|
796
|
+
if ((script.getAttribute("type") ?? "").trim().toLowerCase() === "module" || script.hasAttribute("async") || script.hasAttribute("defer")) {
|
|
797
|
+
fail("unsupported", "artifact uses an unsupported script mode");
|
|
798
|
+
}
|
|
799
|
+
if (!isJavaScriptType(script.getAttribute("type"))) fail("unsupported", "artifact uses a script type that is not JavaScript");
|
|
800
|
+
let source = script.textContent ?? "";
|
|
801
|
+
const reference = script.getAttribute("src");
|
|
802
|
+
if (reference !== null) {
|
|
803
|
+
const { file: scriptFile } = requiredFile(context, path, reference);
|
|
804
|
+
source = decodeText(scriptFile, context);
|
|
805
|
+
script.removeAttribute("src");
|
|
806
|
+
}
|
|
807
|
+
assertClassicScript(source);
|
|
808
|
+
script.textContent = source.replace(/<\/script/gi, "<\\/script");
|
|
809
|
+
}
|
|
810
|
+
const resourceAttributes = [
|
|
811
|
+
["img[src]", "src"],
|
|
812
|
+
["source[src]", "src"],
|
|
813
|
+
["video[src]", "src"],
|
|
814
|
+
["video[poster]", "poster"],
|
|
815
|
+
["audio[src]", "src"],
|
|
816
|
+
["track[src]", "src"],
|
|
817
|
+
["input[type=image][src]", "src"]
|
|
818
|
+
];
|
|
819
|
+
for (const [selector, attribute] of resourceAttributes) {
|
|
820
|
+
for (const element of queryAll(roots, selector)) {
|
|
821
|
+
const reference = element.getAttribute(attribute);
|
|
822
|
+
if (reference === null) continue;
|
|
823
|
+
const { file: resource, resolved } = requiredFile(context, path, reference);
|
|
824
|
+
element.setAttribute(attribute, `${asDataUrl(resource, context)}${resolved.fragment}`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
for (const element of queryAll(roots, "[srcset]")) {
|
|
828
|
+
const value = element.getAttribute("srcset");
|
|
829
|
+
if (value !== null) element.setAttribute("srcset", serializeSrcset(value, path, context));
|
|
830
|
+
}
|
|
831
|
+
for (const anchor of queryAll(roots, "a[href]")) {
|
|
832
|
+
anchor.removeAttribute("target");
|
|
833
|
+
anchor.removeAttribute("download");
|
|
834
|
+
const reference = anchor.getAttribute("href") ?? "";
|
|
835
|
+
if (reference.startsWith("#")) continue;
|
|
836
|
+
const resolved = resolveReferenceFrom(path, reference);
|
|
837
|
+
if (context.singlePage) throw new ArtifactBundleError("unsupported", "artifact links to another page", path, reference);
|
|
838
|
+
if (!context.files.has(resolved.path) || !/\.html?$/i.test(resolved.path)) fail("unsupported", "artifact link leaves its html bundle");
|
|
839
|
+
anchor.href = "#";
|
|
840
|
+
anchor.dataset.tcArtifactPath = resolved.path;
|
|
841
|
+
if (resolved.fragment.length > 0) anchor.dataset.tcArtifactFragment = resolved.fragment;
|
|
842
|
+
}
|
|
843
|
+
const head = doc.head ?? doc.documentElement.insertBefore(doc.createElement("head"), doc.body);
|
|
844
|
+
const csp = doc.createElement("meta");
|
|
845
|
+
csp.httpEquiv = "Content-Security-Policy";
|
|
846
|
+
csp.content = INNER_CSP;
|
|
847
|
+
const bootstrap = doc.createElement("script");
|
|
848
|
+
bootstrap.textContent = CHILD_BOOTSTRAP;
|
|
849
|
+
head.prepend(bootstrap);
|
|
850
|
+
head.prepend(csp);
|
|
851
|
+
const output = restoreParkedUrls(`<!DOCTYPE html>
|
|
852
|
+
${doc.documentElement.outerHTML}`, deferred);
|
|
853
|
+
verifySelfContained(output, context);
|
|
854
|
+
return output;
|
|
855
|
+
}
|
|
856
|
+
async function prepareHtmlArtifact(input, options = {}) {
|
|
857
|
+
if (input.length === 0) fail("malformed", "artifact bundle is empty");
|
|
858
|
+
if (input.length > MAX_ARTIFACT_FILES) fail("limit", "artifact has too many files");
|
|
859
|
+
const files = /* @__PURE__ */ new Map();
|
|
860
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
861
|
+
let sourceBytes = 0;
|
|
862
|
+
for (const candidate of input) {
|
|
863
|
+
let path;
|
|
864
|
+
try {
|
|
865
|
+
path = canonicalArtifactPath(candidate.path);
|
|
866
|
+
} catch (error) {
|
|
867
|
+
if (error instanceof ArtifactBundleError) throw new ArtifactBundleError(error.kind, error.message, candidate.path);
|
|
868
|
+
throw error;
|
|
869
|
+
}
|
|
870
|
+
const alias = path.toLowerCase();
|
|
871
|
+
if (aliases.has(alias)) throw new ArtifactBundleError("malformed", "artifact paths collide after canonicalization", path);
|
|
872
|
+
aliases.add(alias);
|
|
873
|
+
sourceBytes += candidate.bytes.byteLength;
|
|
874
|
+
if (!Number.isSafeInteger(sourceBytes) || sourceBytes > MAX_ARTIFACT_SOURCE_BYTES) throw new ArtifactBundleError("limit", "artifact source exceeds limit", path);
|
|
875
|
+
files.set(path, { ...candidate, path });
|
|
876
|
+
}
|
|
877
|
+
const detection = detectHtmlArtifact([...files.keys()]);
|
|
878
|
+
if (detection.kind !== "html") fail("malformed", "artifact root index is missing");
|
|
879
|
+
const renderBytes = [...files.values()].filter((file) => /^(?:text\/|application\/(?:javascript|json)|image\/svg\+xml)/i.test(artifactMediaType(file.path, file.mediaType))).reduce((total, file) => total + file.bytes.byteLength, 0);
|
|
880
|
+
if (renderBytes > MAX_ARTIFACT_RENDER_BYTES) fail("limit", "artifact render aggregate exceeds limit");
|
|
881
|
+
const context = {
|
|
882
|
+
files,
|
|
883
|
+
cssStack: [],
|
|
884
|
+
counter: { references: 0, inlinedBytes: 0 },
|
|
885
|
+
parseHtml: options.parseHtml ?? defaultParseHtml,
|
|
886
|
+
singlePage: options.singlePage === true
|
|
887
|
+
};
|
|
888
|
+
const pages = {};
|
|
889
|
+
let renderedBytes = 0;
|
|
890
|
+
for (const [path, file] of files) {
|
|
891
|
+
if (!/\.html?$/i.test(path)) continue;
|
|
892
|
+
if (context.singlePage && path !== ARTIFACT_ENTRY) continue;
|
|
893
|
+
let page;
|
|
894
|
+
try {
|
|
895
|
+
page = await transformHtml(path, file, context);
|
|
896
|
+
} catch (error) {
|
|
897
|
+
if (error instanceof ArtifactBundleError && error.path === void 0) throw new ArtifactBundleError(error.kind, error.message, path, error.reference);
|
|
898
|
+
throw error;
|
|
899
|
+
}
|
|
900
|
+
renderedBytes += new TextEncoder().encode(page).byteLength;
|
|
901
|
+
if (!Number.isSafeInteger(renderedBytes) || renderedBytes > MAX_ARTIFACT_RENDER_BYTES) {
|
|
902
|
+
fail("limit", "artifact rendered output exceeds limit");
|
|
903
|
+
}
|
|
904
|
+
pages[path] = page;
|
|
905
|
+
}
|
|
906
|
+
return { entry: ARTIFACT_ENTRY, pages: Object.freeze(pages), fileCount: files.size, sourceBytes };
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// src/html-bundle/folder.ts
|
|
910
|
+
function isPassiveArtifactPath(canonicalPath) {
|
|
911
|
+
const lower = canonicalPath.toLowerCase();
|
|
912
|
+
return lower === "manifest.json" || lower.startsWith("knowledge/");
|
|
913
|
+
}
|
|
914
|
+
function canonicalWithPath(path) {
|
|
915
|
+
try {
|
|
916
|
+
return canonicalArtifactPath(path);
|
|
917
|
+
} catch (error) {
|
|
918
|
+
if (error instanceof ArtifactBundleError) throw new ArtifactBundleError(error.kind, error.message, path);
|
|
919
|
+
throw error;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
async function inlineHtmlFolder(files, options = {}) {
|
|
923
|
+
if (files.length === 0) throw new ArtifactBundleError("malformed", "artifact bundle is empty");
|
|
924
|
+
if (files.length > MAX_ARTIFACT_FILES) throw new ArtifactBundleError("limit", "artifact has too many files");
|
|
925
|
+
const canonical = /* @__PURE__ */ new Map();
|
|
926
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
927
|
+
let sourceBytes = 0;
|
|
928
|
+
for (const candidate of files) {
|
|
929
|
+
const path = canonicalWithPath(candidate.path);
|
|
930
|
+
const alias = path.toLowerCase();
|
|
931
|
+
if (aliases.has(alias)) throw new ArtifactBundleError("malformed", "artifact paths collide after canonicalization", path);
|
|
932
|
+
aliases.set(alias, path);
|
|
933
|
+
sourceBytes += candidate.bytes.byteLength;
|
|
934
|
+
if (!Number.isSafeInteger(sourceBytes) || sourceBytes > MAX_ARTIFACT_SOURCE_BYTES) {
|
|
935
|
+
throw new ArtifactBundleError("limit", "artifact source exceeds limit", path);
|
|
936
|
+
}
|
|
937
|
+
canonical.set(path, { ...candidate, path });
|
|
938
|
+
}
|
|
939
|
+
if (!canonical.has(ARTIFACT_ENTRY)) throw new ArtifactBundleError("missing", "artifact root index is missing", ARTIFACT_ENTRY);
|
|
940
|
+
const active = [];
|
|
941
|
+
const passive = [];
|
|
942
|
+
for (const file of canonical.values()) (isPassiveArtifactPath(file.path) ? passive : active).push(file);
|
|
943
|
+
const prepared = await prepareHtmlArtifact(active, { ...options, singlePage: true });
|
|
944
|
+
passive.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
945
|
+
return {
|
|
946
|
+
entry: ARTIFACT_ENTRY,
|
|
947
|
+
html: new TextEncoder().encode(prepared.pages[ARTIFACT_ENTRY]),
|
|
948
|
+
passive,
|
|
949
|
+
fileCount: canonical.size,
|
|
950
|
+
sourceBytes
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
954
|
+
0 && (module.exports = {
|
|
955
|
+
ARTIFACT_ENTRY,
|
|
956
|
+
ArtifactBundleError,
|
|
957
|
+
MAX_ARTIFACT_ATTRIBUTES_PER_ELEMENT,
|
|
958
|
+
MAX_ARTIFACT_COMMENTS,
|
|
959
|
+
MAX_ARTIFACT_CSS_IMPORT_DEPTH,
|
|
960
|
+
MAX_ARTIFACT_FILES,
|
|
961
|
+
MAX_ARTIFACT_REFERENCES,
|
|
962
|
+
MAX_ARTIFACT_RENDER_BYTES,
|
|
963
|
+
MAX_ARTIFACT_SOURCE_BYTES,
|
|
964
|
+
MAX_ARTIFACT_TEXT_BYTES,
|
|
965
|
+
artifactMediaType,
|
|
966
|
+
assertSelfContainedPage,
|
|
967
|
+
canonicalArtifactPath,
|
|
968
|
+
detectHtmlArtifact,
|
|
969
|
+
inlineHtmlFolder,
|
|
970
|
+
isPassiveArtifactPath,
|
|
971
|
+
normalizeCssEscapes,
|
|
972
|
+
prepareHtmlArtifact,
|
|
973
|
+
resolveArtifactReference
|
|
974
|
+
});
|
|
975
|
+
//# sourceMappingURL=html-bundle.cjs.map
|