html-preview-sandbox 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +163 -0
- package/SECURITY.md +40 -0
- package/THREAT_MODEL.md +51 -0
- package/dist/bridge.d.ts +3 -0
- package/dist/decode.d.ts +12 -0
- package/dist/document.browser.d.ts +2 -0
- package/dist/document.d.ts +2 -0
- package/dist/errors.d.ts +12 -0
- package/dist/index.browser.d.ts +9 -0
- package/dist/index.browser.js +988 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +990 -0
- package/dist/index.js.map +1 -0
- package/dist/policy.d.ts +10 -0
- package/dist/renderer-core.d.ts +4 -0
- package/dist/renderer.browser.d.ts +1 -0
- package/dist/renderer.d.ts +1 -0
- package/dist/sanitize-core.d.ts +5 -0
- package/dist/sanitize.browser.d.ts +4 -0
- package/dist/sanitize.d.ts +4 -0
- package/dist/scrollbar.d.ts +2 -0
- package/dist/types.d.ts +84 -0
- package/docs/BRANCHING.md +94 -0
- package/docs/BROWSER_SUPPORT.md +42 -0
- package/docs/INTEGRATION.md +145 -0
- package/docs/PROJECT_STRUCTURE.md +121 -0
- package/docs/ROADMAP.md +184 -0
- package/docs/SANITIZER_DECISION.md +78 -0
- package/docs/SECURITY_MODEL.md +100 -0
- package/docs//344/275/277/347/224/250/346/226/207/346/241/243.zh-CN.md +204 -0
- package/package.json +86 -0
|
@@ -0,0 +1,988 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var PreviewError = class extends Error {
|
|
3
|
+
constructor(code, message, cause) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "PreviewError";
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.cause = cause;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var ERROR_CODES = {
|
|
11
|
+
OVERSIZED: "OVERSIZED",
|
|
12
|
+
DECODE_FAILED: "DECODE_FAILED",
|
|
13
|
+
EMPTY_AFTER_SANITIZE: "EMPTY_AFTER_SANITIZE",
|
|
14
|
+
RENDER_FAILED: "RENDER_FAILED"
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/decode.ts
|
|
18
|
+
var DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
|
|
19
|
+
var BOM_UTF8 = [239, 187, 191];
|
|
20
|
+
var BOM_UTF16_LE = [255, 254];
|
|
21
|
+
var BOM_UTF16_BE = [254, 255];
|
|
22
|
+
function startsWithBytes(bytes, prefix) {
|
|
23
|
+
if (bytes.length < prefix.length) return false;
|
|
24
|
+
return prefix.every((byte, index) => bytes[index] === byte);
|
|
25
|
+
}
|
|
26
|
+
function decodeWith(bytes, encoding, fatal = false) {
|
|
27
|
+
try {
|
|
28
|
+
return new TextDecoder(encoding, { fatal }).decode(bytes);
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function findDeclaredCharset(head) {
|
|
34
|
+
const metaCharset = head.match(/<meta[^>]+charset\s*=\s*["']?([\w-]+)/i);
|
|
35
|
+
if (metaCharset?.[1]) return metaCharset[1].toLowerCase();
|
|
36
|
+
const contentType = head.match(/charset\s*=\s*["']?([\w-]+)/i);
|
|
37
|
+
if (contentType?.[1]) return contentType[1].toLowerCase();
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
async function normalizeInput(input, options = {}) {
|
|
41
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
42
|
+
if (typeof input === "string") {
|
|
43
|
+
const size = new TextEncoder().encode(input).byteLength;
|
|
44
|
+
if (size > maxBytes) {
|
|
45
|
+
throw new PreviewError(ERROR_CODES.OVERSIZED, `HTML input exceeds ${maxBytes} bytes`);
|
|
46
|
+
}
|
|
47
|
+
return { html: input, encoding: "utf-8", usedBom: false, size };
|
|
48
|
+
}
|
|
49
|
+
let bytes;
|
|
50
|
+
if (input instanceof Uint8Array) {
|
|
51
|
+
bytes = input;
|
|
52
|
+
} else if (input instanceof ArrayBuffer) {
|
|
53
|
+
bytes = new Uint8Array(input);
|
|
54
|
+
} else if (typeof Blob !== "undefined" && input instanceof Blob) {
|
|
55
|
+
if (input.size > maxBytes) {
|
|
56
|
+
throw new PreviewError(ERROR_CODES.OVERSIZED, `HTML input exceeds ${maxBytes} bytes`);
|
|
57
|
+
}
|
|
58
|
+
bytes = new Uint8Array(await input.arrayBuffer());
|
|
59
|
+
} else {
|
|
60
|
+
throw new PreviewError(ERROR_CODES.DECODE_FAILED, "Unsupported preview input");
|
|
61
|
+
}
|
|
62
|
+
if (bytes.byteLength > maxBytes) {
|
|
63
|
+
throw new PreviewError(ERROR_CODES.OVERSIZED, `HTML input exceeds ${maxBytes} bytes`);
|
|
64
|
+
}
|
|
65
|
+
return decodeHtmlBytes(bytes);
|
|
66
|
+
}
|
|
67
|
+
function decodeHtmlBytes(input) {
|
|
68
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
69
|
+
if (startsWithBytes(bytes, BOM_UTF8)) {
|
|
70
|
+
return {
|
|
71
|
+
html: decodeWith(bytes.subarray(BOM_UTF8.length), "utf-8") ?? "",
|
|
72
|
+
encoding: "utf-8",
|
|
73
|
+
usedBom: true,
|
|
74
|
+
size: bytes.byteLength
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (startsWithBytes(bytes, BOM_UTF16_LE)) {
|
|
78
|
+
return {
|
|
79
|
+
html: decodeWith(bytes.subarray(BOM_UTF16_LE.length), "utf-16le") ?? "",
|
|
80
|
+
encoding: "utf-16le",
|
|
81
|
+
usedBom: true,
|
|
82
|
+
size: bytes.byteLength
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (startsWithBytes(bytes, BOM_UTF16_BE)) {
|
|
86
|
+
return {
|
|
87
|
+
html: decodeWith(bytes.subarray(BOM_UTF16_BE.length), "utf-16be") ?? "",
|
|
88
|
+
encoding: "utf-16be",
|
|
89
|
+
usedBom: true,
|
|
90
|
+
size: bytes.byteLength
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const strictUtf8 = decodeWith(bytes, "utf-8", true);
|
|
94
|
+
if (strictUtf8 !== null) {
|
|
95
|
+
return { html: strictUtf8, encoding: "utf-8", usedBom: false, size: bytes.byteLength };
|
|
96
|
+
}
|
|
97
|
+
const head = decodeWith(bytes.subarray(0, 4096), "iso-8859-1") ?? "";
|
|
98
|
+
const declared = findDeclaredCharset(head);
|
|
99
|
+
if (declared && declared !== "utf-8" && declared !== "utf8") {
|
|
100
|
+
const decoded = decodeWith(bytes, declared);
|
|
101
|
+
if (decoded !== null) {
|
|
102
|
+
return { html: decoded, encoding: declared, usedBom: false, size: bytes.byteLength };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
html: decodeWith(bytes, "utf-8") ?? "",
|
|
107
|
+
encoding: "utf-8",
|
|
108
|
+
usedBom: false,
|
|
109
|
+
size: bytes.byteLength
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/sanitize.browser.ts
|
|
114
|
+
import createDOMPurify from "dompurify";
|
|
115
|
+
|
|
116
|
+
// src/sanitize-core.ts
|
|
117
|
+
var DEFAULT_ALLOWED_TAGS = [
|
|
118
|
+
"html",
|
|
119
|
+
"head",
|
|
120
|
+
"body",
|
|
121
|
+
"title",
|
|
122
|
+
"meta",
|
|
123
|
+
"link",
|
|
124
|
+
"style",
|
|
125
|
+
"script",
|
|
126
|
+
"main",
|
|
127
|
+
"section",
|
|
128
|
+
"article",
|
|
129
|
+
"aside",
|
|
130
|
+
"header",
|
|
131
|
+
"footer",
|
|
132
|
+
"nav",
|
|
133
|
+
"div",
|
|
134
|
+
"span",
|
|
135
|
+
"p",
|
|
136
|
+
"br",
|
|
137
|
+
"hr",
|
|
138
|
+
"pre",
|
|
139
|
+
"code",
|
|
140
|
+
"blockquote",
|
|
141
|
+
"h1",
|
|
142
|
+
"h2",
|
|
143
|
+
"h3",
|
|
144
|
+
"h4",
|
|
145
|
+
"h5",
|
|
146
|
+
"h6",
|
|
147
|
+
"ul",
|
|
148
|
+
"ol",
|
|
149
|
+
"li",
|
|
150
|
+
"dl",
|
|
151
|
+
"dt",
|
|
152
|
+
"dd",
|
|
153
|
+
"table",
|
|
154
|
+
"thead",
|
|
155
|
+
"tbody",
|
|
156
|
+
"tfoot",
|
|
157
|
+
"tr",
|
|
158
|
+
"th",
|
|
159
|
+
"td",
|
|
160
|
+
"caption",
|
|
161
|
+
"colgroup",
|
|
162
|
+
"col",
|
|
163
|
+
"a",
|
|
164
|
+
"strong",
|
|
165
|
+
"b",
|
|
166
|
+
"em",
|
|
167
|
+
"i",
|
|
168
|
+
"u",
|
|
169
|
+
"s",
|
|
170
|
+
"small",
|
|
171
|
+
"mark",
|
|
172
|
+
"sub",
|
|
173
|
+
"sup",
|
|
174
|
+
"img",
|
|
175
|
+
"picture",
|
|
176
|
+
"source",
|
|
177
|
+
"audio",
|
|
178
|
+
"video",
|
|
179
|
+
"canvas",
|
|
180
|
+
"form",
|
|
181
|
+
"label",
|
|
182
|
+
"input",
|
|
183
|
+
"button",
|
|
184
|
+
"select",
|
|
185
|
+
"option",
|
|
186
|
+
"optgroup",
|
|
187
|
+
"textarea",
|
|
188
|
+
"fieldset",
|
|
189
|
+
"legend",
|
|
190
|
+
"details",
|
|
191
|
+
"summary",
|
|
192
|
+
"svg",
|
|
193
|
+
"g",
|
|
194
|
+
"defs",
|
|
195
|
+
"symbol",
|
|
196
|
+
"use",
|
|
197
|
+
"desc",
|
|
198
|
+
"rect",
|
|
199
|
+
"circle",
|
|
200
|
+
"ellipse",
|
|
201
|
+
"line",
|
|
202
|
+
"polyline",
|
|
203
|
+
"polygon",
|
|
204
|
+
"path",
|
|
205
|
+
"text",
|
|
206
|
+
"tspan",
|
|
207
|
+
"textPath",
|
|
208
|
+
"linearGradient",
|
|
209
|
+
"radialGradient",
|
|
210
|
+
"stop",
|
|
211
|
+
"pattern",
|
|
212
|
+
"mask",
|
|
213
|
+
"clipPath",
|
|
214
|
+
"filter",
|
|
215
|
+
"feGaussianBlur",
|
|
216
|
+
"feColorMatrix",
|
|
217
|
+
"feOffset",
|
|
218
|
+
"feMerge",
|
|
219
|
+
"feMergeNode",
|
|
220
|
+
"feBlend",
|
|
221
|
+
"feFlood",
|
|
222
|
+
"feComposite",
|
|
223
|
+
"feTurbulence",
|
|
224
|
+
"feDisplacementMap",
|
|
225
|
+
"feDropShadow",
|
|
226
|
+
"marker",
|
|
227
|
+
"animate",
|
|
228
|
+
"animateTransform",
|
|
229
|
+
"animateMotion",
|
|
230
|
+
"set",
|
|
231
|
+
"foreignObject"
|
|
232
|
+
];
|
|
233
|
+
var DISALLOWED_TAGS = ["base", "object", "embed", "applet"];
|
|
234
|
+
var GLOBAL_ATTRS = [
|
|
235
|
+
"class",
|
|
236
|
+
"id",
|
|
237
|
+
"style",
|
|
238
|
+
"title",
|
|
239
|
+
"lang",
|
|
240
|
+
"dir",
|
|
241
|
+
"role",
|
|
242
|
+
"tabindex",
|
|
243
|
+
"hidden",
|
|
244
|
+
"aria-*",
|
|
245
|
+
"data-*"
|
|
246
|
+
];
|
|
247
|
+
var SAFE_INLINE_EVENTS = [
|
|
248
|
+
"onclick",
|
|
249
|
+
"onauxclick",
|
|
250
|
+
"ondblclick",
|
|
251
|
+
"oninput",
|
|
252
|
+
"onchange",
|
|
253
|
+
"onsubmit",
|
|
254
|
+
"onreset",
|
|
255
|
+
"onkeydown",
|
|
256
|
+
"onkeyup",
|
|
257
|
+
"onkeypress",
|
|
258
|
+
"onfocusin",
|
|
259
|
+
"onfocusout"
|
|
260
|
+
];
|
|
261
|
+
var SVG_ATTRS = [
|
|
262
|
+
"x",
|
|
263
|
+
"y",
|
|
264
|
+
"cx",
|
|
265
|
+
"cy",
|
|
266
|
+
"r",
|
|
267
|
+
"rx",
|
|
268
|
+
"ry",
|
|
269
|
+
"x1",
|
|
270
|
+
"y1",
|
|
271
|
+
"x2",
|
|
272
|
+
"y2",
|
|
273
|
+
"width",
|
|
274
|
+
"height",
|
|
275
|
+
"d",
|
|
276
|
+
"points",
|
|
277
|
+
"transform",
|
|
278
|
+
"fill",
|
|
279
|
+
"fill-opacity",
|
|
280
|
+
"fill-rule",
|
|
281
|
+
"stroke",
|
|
282
|
+
"stroke-width",
|
|
283
|
+
"stroke-linecap",
|
|
284
|
+
"stroke-linejoin",
|
|
285
|
+
"stroke-dasharray",
|
|
286
|
+
"stroke-dashoffset",
|
|
287
|
+
"stroke-opacity",
|
|
288
|
+
"stroke-miterlimit",
|
|
289
|
+
"opacity",
|
|
290
|
+
"color",
|
|
291
|
+
"visibility",
|
|
292
|
+
"display",
|
|
293
|
+
"vector-effect",
|
|
294
|
+
"viewBox",
|
|
295
|
+
"preserveAspectRatio",
|
|
296
|
+
"xmlns",
|
|
297
|
+
"xmlns:xlink",
|
|
298
|
+
"href",
|
|
299
|
+
"xlink:href",
|
|
300
|
+
"offset",
|
|
301
|
+
"gradientUnits",
|
|
302
|
+
"gradientTransform",
|
|
303
|
+
"spreadMethod",
|
|
304
|
+
"stop-color",
|
|
305
|
+
"stop-opacity",
|
|
306
|
+
"text-anchor",
|
|
307
|
+
"dominant-baseline",
|
|
308
|
+
"alignment-baseline",
|
|
309
|
+
"font-family",
|
|
310
|
+
"font-size",
|
|
311
|
+
"font-weight",
|
|
312
|
+
"font-style",
|
|
313
|
+
"letter-spacing",
|
|
314
|
+
"word-spacing",
|
|
315
|
+
"text-decoration",
|
|
316
|
+
"dx",
|
|
317
|
+
"dy",
|
|
318
|
+
"rotate",
|
|
319
|
+
"startOffset",
|
|
320
|
+
"textLength",
|
|
321
|
+
"lengthAdjust",
|
|
322
|
+
"clip-path",
|
|
323
|
+
"clip-rule",
|
|
324
|
+
"mask",
|
|
325
|
+
"filter",
|
|
326
|
+
"stdDeviation",
|
|
327
|
+
"in",
|
|
328
|
+
"in2",
|
|
329
|
+
"result",
|
|
330
|
+
"mode",
|
|
331
|
+
"flood-color",
|
|
332
|
+
"flood-opacity",
|
|
333
|
+
"edgeMode",
|
|
334
|
+
"patternUnits",
|
|
335
|
+
"patternTransform",
|
|
336
|
+
"patternContentUnits",
|
|
337
|
+
"markerWidth",
|
|
338
|
+
"markerHeight",
|
|
339
|
+
"refX",
|
|
340
|
+
"refY",
|
|
341
|
+
"orient",
|
|
342
|
+
"markerUnits",
|
|
343
|
+
"marker-start",
|
|
344
|
+
"marker-mid",
|
|
345
|
+
"marker-end",
|
|
346
|
+
"attributeName",
|
|
347
|
+
"attributeType",
|
|
348
|
+
"from",
|
|
349
|
+
"to",
|
|
350
|
+
"by",
|
|
351
|
+
"values",
|
|
352
|
+
"dur",
|
|
353
|
+
"repeatCount",
|
|
354
|
+
"repeatDur",
|
|
355
|
+
"begin",
|
|
356
|
+
"end",
|
|
357
|
+
"calcMode",
|
|
358
|
+
"keyTimes",
|
|
359
|
+
"keySplines",
|
|
360
|
+
"additive",
|
|
361
|
+
"accumulate",
|
|
362
|
+
"fill-mode",
|
|
363
|
+
"restart"
|
|
364
|
+
];
|
|
365
|
+
var ATTRS_BY_TAG = {
|
|
366
|
+
a: ["href", "name", "target", "rel", "download"],
|
|
367
|
+
link: ["rel", "href", "as", "crossorigin", "integrity", "referrerpolicy", "type", "media", "sizes"],
|
|
368
|
+
meta: ["charset", "name", "content", "http-equiv", "property"],
|
|
369
|
+
img: ["src", "alt", "width", "height", "loading", "decoding", "srcset", "sizes"],
|
|
370
|
+
source: ["src", "srcset", "type", "media"],
|
|
371
|
+
input: ["type", "name", "value", "placeholder", "min", "max", "step", "required", "checked", "disabled", "readonly", "pattern", "maxlength"],
|
|
372
|
+
button: ["type", "name", "value", "disabled"],
|
|
373
|
+
select: ["name", "value", "multiple", "disabled", "required"],
|
|
374
|
+
option: ["value", "selected", "disabled"],
|
|
375
|
+
textarea: ["name", "value", "placeholder", "rows", "cols", "maxlength", "required", "disabled", "readonly"],
|
|
376
|
+
form: ["method", "enctype", "novalidate"],
|
|
377
|
+
label: ["for"],
|
|
378
|
+
audio: ["src", "controls", "autoplay", "loop", "muted", "preload"],
|
|
379
|
+
video: ["src", "controls", "autoplay", "loop", "muted", "preload", "poster", "width", "height"],
|
|
380
|
+
script: ["src", "type", "async", "defer", "crossorigin", "integrity", "referrerpolicy", "nomodule", "nonce"]
|
|
381
|
+
};
|
|
382
|
+
var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "srcset", "cite", "xlink:href", "action"]);
|
|
383
|
+
var DEFAULT_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]);
|
|
384
|
+
var MEDIA_TAGS = /* @__PURE__ */ new Set(["img", "audio", "video", "source", "picture"]);
|
|
385
|
+
function emptyReport() {
|
|
386
|
+
return {
|
|
387
|
+
removedTags: [],
|
|
388
|
+
removedAttributes: [],
|
|
389
|
+
removedSchemes: [],
|
|
390
|
+
strippedAll: false
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function increment(list, predicate, create) {
|
|
394
|
+
const existing = list.find(predicate);
|
|
395
|
+
if (existing) {
|
|
396
|
+
existing.count += 1;
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
list.push({ ...create(), count: 1 });
|
|
400
|
+
}
|
|
401
|
+
function reportTag(report, tag) {
|
|
402
|
+
increment(report.removedTags, (item) => item.tag === tag, () => ({ tag }));
|
|
403
|
+
}
|
|
404
|
+
function reportAttr(report, tag, attr) {
|
|
405
|
+
increment(report.removedAttributes, (item) => item.tag === tag && item.attr === attr, () => ({ tag, attr }));
|
|
406
|
+
}
|
|
407
|
+
function reportScheme(report, scheme) {
|
|
408
|
+
increment(report.removedSchemes, (item) => item.scheme === scheme, () => ({ scheme }));
|
|
409
|
+
}
|
|
410
|
+
function normalizeName(name) {
|
|
411
|
+
return String(name || "").toLowerCase();
|
|
412
|
+
}
|
|
413
|
+
function matchesPattern(attr, pattern) {
|
|
414
|
+
const normalizedPattern = normalizeName(pattern);
|
|
415
|
+
return normalizedPattern.endsWith("*") ? attr.startsWith(normalizedPattern.slice(0, -1)) : attr === normalizedPattern;
|
|
416
|
+
}
|
|
417
|
+
function getAllowedTags(options = {}) {
|
|
418
|
+
const tags = new Set(DEFAULT_ALLOWED_TAGS.map(normalizeName));
|
|
419
|
+
if (options.allowScripts === false) tags.delete("script");
|
|
420
|
+
for (const tag of options.extraTags ?? []) tags.add(normalizeName(tag));
|
|
421
|
+
for (const tag of options.dropTags ?? []) tags.delete(normalizeName(tag));
|
|
422
|
+
return tags;
|
|
423
|
+
}
|
|
424
|
+
function getAllowedAttrs(options = {}) {
|
|
425
|
+
return [
|
|
426
|
+
...GLOBAL_ATTRS,
|
|
427
|
+
...options.allowInlineEvents === false ? [] : SAFE_INLINE_EVENTS,
|
|
428
|
+
...SVG_ATTRS,
|
|
429
|
+
...Object.values(ATTRS_BY_TAG).flat(),
|
|
430
|
+
...options.extraAttributes?.["*"] ?? [],
|
|
431
|
+
...Object.values(options.extraAttributes ?? {}).flat()
|
|
432
|
+
];
|
|
433
|
+
}
|
|
434
|
+
function isAllowedAttr(tag, attr, options = {}) {
|
|
435
|
+
if (GLOBAL_ATTRS.some((pattern) => matchesPattern(attr, pattern))) return true;
|
|
436
|
+
if (options.allowInlineEvents !== false && SAFE_INLINE_EVENTS.map(normalizeName).includes(attr)) return true;
|
|
437
|
+
if (SVG_ATTRS.map(normalizeName).includes(attr)) return true;
|
|
438
|
+
if ((ATTRS_BY_TAG[tag] ?? []).map(normalizeName).includes(attr)) return true;
|
|
439
|
+
if ((options.extraAttributes?.["*"] ?? []).map(normalizeName).includes(attr)) return true;
|
|
440
|
+
if ((options.extraAttributes?.[tag] ?? []).map(normalizeName).includes(attr)) return true;
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
function getScheme(raw) {
|
|
444
|
+
const trimmed = String(raw || "").trim();
|
|
445
|
+
const match = trimmed.match(/^([a-zA-Z][\w+.-]*):/);
|
|
446
|
+
return match ? `${match[1].toLowerCase()}:` : "";
|
|
447
|
+
}
|
|
448
|
+
function isAllowedUrl(raw, tag, options = {}) {
|
|
449
|
+
const scheme = getScheme(raw);
|
|
450
|
+
if (!scheme) return true;
|
|
451
|
+
if (DEFAULT_SCHEMES.has(scheme)) return true;
|
|
452
|
+
if (MEDIA_TAGS.has(tag) && (scheme === "data:" || scheme === "blob:")) return true;
|
|
453
|
+
return (options.extraSchemes ?? []).map((item) => item.endsWith(":") ? item.toLowerCase() : `${item.toLowerCase()}:`).includes(scheme);
|
|
454
|
+
}
|
|
455
|
+
function splitSrcsetCandidates(raw) {
|
|
456
|
+
return String(raw || "").split(",").map((candidate) => candidate.trim()).filter(Boolean).map((candidate) => candidate.split(/\s+/)[0]).filter(Boolean);
|
|
457
|
+
}
|
|
458
|
+
function disallowedSrcsetSchemes(raw, tag, options = {}) {
|
|
459
|
+
const schemes = /* @__PURE__ */ new Set();
|
|
460
|
+
for (const url of splitSrcsetCandidates(raw)) {
|
|
461
|
+
if (!isAllowedUrl(url, tag, options)) {
|
|
462
|
+
schemes.add(getScheme(url) || "invalid");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return [...schemes];
|
|
466
|
+
}
|
|
467
|
+
function isDangerousMeta(node) {
|
|
468
|
+
if (normalizeName(node.nodeName) !== "meta") return false;
|
|
469
|
+
const equiv = normalizeName(node.getAttribute("http-equiv"));
|
|
470
|
+
return equiv === "refresh" || equiv === "content-security-policy";
|
|
471
|
+
}
|
|
472
|
+
var ANIMATION_TAGS = /* @__PURE__ */ new Set(["animate", "set", "animatemotion", "animatetransform"]);
|
|
473
|
+
var URL_TARGET_ATTRS = /* @__PURE__ */ new Set(["href", "xlink:href", "src"]);
|
|
474
|
+
function isUrlTargetingAnimation(node) {
|
|
475
|
+
if (!ANIMATION_TAGS.has(normalizeName(node.nodeName))) return false;
|
|
476
|
+
const target = normalizeName(node.getAttribute?.("attributeName"));
|
|
477
|
+
return URL_TARGET_ATTRS.has(target);
|
|
478
|
+
}
|
|
479
|
+
var BLOCKED_LINK_RELS = /* @__PURE__ */ new Set(["dns-prefetch", "preconnect", "prefetch", "prerender"]);
|
|
480
|
+
function isBlockedResourceHintLink(node) {
|
|
481
|
+
if (normalizeName(node.nodeName) !== "link") return false;
|
|
482
|
+
const rel = normalizeName(node.getAttribute?.("rel"));
|
|
483
|
+
if (!rel) return false;
|
|
484
|
+
return rel.split(/\s+/).some((token) => BLOCKED_LINK_RELS.has(token));
|
|
485
|
+
}
|
|
486
|
+
function applyProjectHooks(purifier, report, options) {
|
|
487
|
+
const pendingRemoval = /* @__PURE__ */ new WeakSet();
|
|
488
|
+
purifier.addHook("uponSanitizeElement", (node, data) => {
|
|
489
|
+
const tag = normalizeName(data.tagName || node.nodeName);
|
|
490
|
+
if (DISALLOWED_TAGS.includes(tag)) reportTag(report, tag);
|
|
491
|
+
if (isDangerousMeta(node)) reportAttr(report, "meta", "http-equiv");
|
|
492
|
+
if (isUrlTargetingAnimation(node)) {
|
|
493
|
+
reportTag(report, tag);
|
|
494
|
+
pendingRemoval.add(node);
|
|
495
|
+
}
|
|
496
|
+
if (isBlockedResourceHintLink(node)) {
|
|
497
|
+
reportTag(report, tag);
|
|
498
|
+
pendingRemoval.add(node);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
purifier.addHook("afterSanitizeElements", (node) => {
|
|
502
|
+
if (isDangerousMeta(node) || pendingRemoval.has(node)) {
|
|
503
|
+
node.remove();
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
purifier.addHook("afterSanitizeAttributes", (node) => {
|
|
507
|
+
const tag = normalizeName(node.nodeName);
|
|
508
|
+
if (!node.attributes) return;
|
|
509
|
+
for (const attrNode of [...node.attributes]) {
|
|
510
|
+
const attr = normalizeName(attrNode.name);
|
|
511
|
+
if (!isAllowedAttr(tag, attr, options)) {
|
|
512
|
+
reportAttr(report, tag, attr);
|
|
513
|
+
node.removeAttribute(attrNode.name);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (URL_ATTRS.has(attr) && !isAllowedUrl(attrNode.value, tag, options)) {
|
|
517
|
+
reportAttr(report, tag, attr);
|
|
518
|
+
reportScheme(report, getScheme(attrNode.value) || "invalid");
|
|
519
|
+
node.removeAttribute(attrNode.name);
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (attr === "srcset") {
|
|
523
|
+
const blockedSchemes = disallowedSrcsetSchemes(attrNode.value, tag, options);
|
|
524
|
+
if (blockedSchemes.length) {
|
|
525
|
+
reportAttr(report, tag, attr);
|
|
526
|
+
for (const scheme of blockedSchemes) reportScheme(report, scheme);
|
|
527
|
+
node.removeAttribute(attrNode.name);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
function collectDomPurifyRemoved(purifier, report) {
|
|
534
|
+
for (const item of purifier.removed ?? []) {
|
|
535
|
+
if (item.element) {
|
|
536
|
+
reportTag(report, normalizeName(item.element.nodeName));
|
|
537
|
+
}
|
|
538
|
+
if (item.attribute) {
|
|
539
|
+
reportAttr(report, normalizeName(item.from?.nodeName || "*"), normalizeName(item.attribute.name));
|
|
540
|
+
if (URL_ATTRS.has(normalizeName(item.attribute.name))) {
|
|
541
|
+
reportScheme(report, getScheme(item.attribute.value) || "invalid");
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function createSanitizer(runtimeWindow, createDOMPurify2) {
|
|
547
|
+
return function sanitizeHtml2(rawHtml, options = {}) {
|
|
548
|
+
const purifier = createDOMPurify2(runtimeWindow);
|
|
549
|
+
const report = emptyReport();
|
|
550
|
+
const allowedTags = [...getAllowedTags(options)];
|
|
551
|
+
applyProjectHooks(purifier, report, options);
|
|
552
|
+
const html = purifier.sanitize(rawHtml, {
|
|
553
|
+
WHOLE_DOCUMENT: true,
|
|
554
|
+
// ALLOWED_TAGS/ALLOWED_ATTR are authoritative (they replace DOMPurify's
|
|
555
|
+
// defaults). Per-tag attribute tightening happens in the afterSanitizeAttributes
|
|
556
|
+
// hook; DISALLOWED_TAGS is the hard deny-list.
|
|
557
|
+
ALLOWED_TAGS: allowedTags,
|
|
558
|
+
ALLOWED_ATTR: getAllowedAttrs(options),
|
|
559
|
+
FORBID_TAGS: DISALLOWED_TAGS,
|
|
560
|
+
KEEP_CONTENT: true,
|
|
561
|
+
ALLOW_DATA_ATTR: true,
|
|
562
|
+
ALLOW_ARIA_ATTR: true
|
|
563
|
+
});
|
|
564
|
+
collectDomPurifyRemoved(purifier, report);
|
|
565
|
+
const parser = new runtimeWindow.DOMParser();
|
|
566
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
567
|
+
report.strippedAll = !doc.body.textContent?.trim() && !doc.body.querySelector("script,style,img,svg,canvas,video,audio");
|
|
568
|
+
return { html, report };
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/sanitize.browser.ts
|
|
573
|
+
var sanitizeHtml = createSanitizer(window, createDOMPurify);
|
|
574
|
+
|
|
575
|
+
// src/policy.ts
|
|
576
|
+
var DEFAULT_SCRIPT_HOSTS = [
|
|
577
|
+
"https://cdnjs.cloudflare.com",
|
|
578
|
+
"https://cdn.jsdelivr.net",
|
|
579
|
+
"https://cdn.tailwindcss.com",
|
|
580
|
+
"https://code.jquery.com"
|
|
581
|
+
];
|
|
582
|
+
var DEFAULT_STYLE_HOSTS = [
|
|
583
|
+
"https://cdnjs.cloudflare.com",
|
|
584
|
+
"https://cdn.jsdelivr.net",
|
|
585
|
+
"https://cdn.tailwindcss.com",
|
|
586
|
+
"https://fonts.googleapis.com"
|
|
587
|
+
];
|
|
588
|
+
var DEFAULT_FONT_HOSTS = ["https://fonts.gstatic.com"];
|
|
589
|
+
var DEFAULT_SANDBOX_TOKENS = [
|
|
590
|
+
"allow-scripts",
|
|
591
|
+
"allow-forms",
|
|
592
|
+
"allow-popups",
|
|
593
|
+
"allow-popups-to-escape-sandbox",
|
|
594
|
+
"allow-modals"
|
|
595
|
+
];
|
|
596
|
+
var UNSAFE_SANDBOX_TOKENS = [
|
|
597
|
+
"allow-same-origin",
|
|
598
|
+
"allow-top-navigation",
|
|
599
|
+
"allow-top-navigation-by-user-activation",
|
|
600
|
+
"allow-downloads"
|
|
601
|
+
];
|
|
602
|
+
var DEFAULT_ALLOWED_EXTERNAL_PROTOCOLS = ["http:", "https:", "mailto:", "tel:"];
|
|
603
|
+
function normalizePolicy(policy) {
|
|
604
|
+
if (!policy) return { preset: "strict" };
|
|
605
|
+
if (typeof policy === "string") return { preset: policy };
|
|
606
|
+
return { preset: "strict", ...policy };
|
|
607
|
+
}
|
|
608
|
+
function join(values) {
|
|
609
|
+
return [...new Set(values.filter(Boolean))].join(" ");
|
|
610
|
+
}
|
|
611
|
+
function buildCsp(policy) {
|
|
612
|
+
const normalized = normalizePolicy(policy);
|
|
613
|
+
const preset = normalized.preset ?? "strict";
|
|
614
|
+
const scriptHosts = [...DEFAULT_SCRIPT_HOSTS, ...normalized.scriptHosts ?? []];
|
|
615
|
+
const styleHosts = [...DEFAULT_STYLE_HOSTS, ...normalized.styleHosts ?? []];
|
|
616
|
+
const fontHosts = [...DEFAULT_FONT_HOSTS, ...normalized.fontHosts ?? []];
|
|
617
|
+
const imgHosts = normalized.imgHosts ?? [];
|
|
618
|
+
const connectHosts = normalized.connectHosts ?? [];
|
|
619
|
+
const directives = {
|
|
620
|
+
"default-src": "'none'",
|
|
621
|
+
"script-src": join(["'unsafe-inline'", "'unsafe-eval'", ...scriptHosts]),
|
|
622
|
+
"style-src": join(["'unsafe-inline'", ...styleHosts]),
|
|
623
|
+
"img-src": join(["blob:", "data:", ...imgHosts]),
|
|
624
|
+
"media-src": "blob: data:",
|
|
625
|
+
"font-src": join(["data:", ...fontHosts]),
|
|
626
|
+
"connect-src": "'none'",
|
|
627
|
+
"worker-src": "'none'",
|
|
628
|
+
"frame-src": "blob:",
|
|
629
|
+
"object-src": "'none'",
|
|
630
|
+
"base-uri": "'none'",
|
|
631
|
+
"form-action": "'none'",
|
|
632
|
+
"upgrade-insecure-requests": "",
|
|
633
|
+
"block-all-mixed-content": ""
|
|
634
|
+
};
|
|
635
|
+
if (preset === "balanced") {
|
|
636
|
+
directives["script-src"] = join(["'unsafe-inline'", "'unsafe-eval'", ...scriptHosts, "https://unpkg.com"]);
|
|
637
|
+
directives["style-src"] = join(["'unsafe-inline'", ...styleHosts, "https://unpkg.com"]);
|
|
638
|
+
directives["img-src"] = join(["https:", "blob:", "data:", ...imgHosts]);
|
|
639
|
+
directives["media-src"] = join(["https:", "blob:", "data:"]);
|
|
640
|
+
directives["connect-src"] = connectHosts.length ? join(["https:", ...connectHosts]) : "https:";
|
|
641
|
+
}
|
|
642
|
+
if (preset === "offline") {
|
|
643
|
+
directives["script-src"] = "'unsafe-inline'";
|
|
644
|
+
directives["style-src"] = "'unsafe-inline'";
|
|
645
|
+
directives["img-src"] = "blob: data:";
|
|
646
|
+
directives["media-src"] = "blob: data:";
|
|
647
|
+
directives["font-src"] = "data:";
|
|
648
|
+
directives["connect-src"] = "'none'";
|
|
649
|
+
}
|
|
650
|
+
Object.assign(directives, normalized.directives ?? {});
|
|
651
|
+
return Object.entries(directives).map(([name, value]) => value ? `${name} ${value}` : name).join("; ");
|
|
652
|
+
}
|
|
653
|
+
function injectCspMeta(html, csp) {
|
|
654
|
+
const escaped = csp.replace(/"/g, """);
|
|
655
|
+
const meta = `<meta http-equiv="Content-Security-Policy" content="${escaped}">`;
|
|
656
|
+
if (/<head[^>]*>/i.test(html)) {
|
|
657
|
+
return html.replace(/<head[^>]*>/i, (match) => `${match}${meta}`);
|
|
658
|
+
}
|
|
659
|
+
if (/<html[^>]*>/i.test(html)) {
|
|
660
|
+
return html.replace(/<html[^>]*>/i, (match) => `${match}<head>${meta}</head>`);
|
|
661
|
+
}
|
|
662
|
+
return `<!doctype html><html><head>${meta}</head><body>${html}</body></html>`;
|
|
663
|
+
}
|
|
664
|
+
function getSandboxAttribute(tokens = DEFAULT_SANDBOX_TOKENS, options = {}) {
|
|
665
|
+
const allowUnsafeTokens = options.allowUnsafeTokens === true;
|
|
666
|
+
return [...new Set(tokens)].filter((token) => allowUnsafeTokens || !UNSAFE_SANDBOX_TOKENS.includes(token)).join(" ");
|
|
667
|
+
}
|
|
668
|
+
function isAllowedExternalUrl(url, protocols = DEFAULT_ALLOWED_EXTERNAL_PROTOCOLS) {
|
|
669
|
+
try {
|
|
670
|
+
const parsed = new URL(url);
|
|
671
|
+
const allowedProtocols = protocols.map((protocol) => {
|
|
672
|
+
const normalized = String(protocol || "").toLowerCase();
|
|
673
|
+
return normalized.endsWith(":") ? normalized : `${normalized}:`;
|
|
674
|
+
});
|
|
675
|
+
return allowedProtocols.includes(parsed.protocol);
|
|
676
|
+
} catch {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/bridge.ts
|
|
682
|
+
var BRIDGE_OPEN_EXTERNAL = "html-preview-sandbox:openExternal";
|
|
683
|
+
var BRIDGE_CSP_VIOLATION = "html-preview-sandbox:cspViolation";
|
|
684
|
+
var CSP_VIOLATION_LIMIT = 30;
|
|
685
|
+
function bridgeScript() {
|
|
686
|
+
return `<script>
|
|
687
|
+
(() => {
|
|
688
|
+
const OPEN_EXTERNAL = ${JSON.stringify(BRIDGE_OPEN_EXTERNAL)};
|
|
689
|
+
const CSP_VIOLATION = ${JSON.stringify(BRIDGE_CSP_VIOLATION)};
|
|
690
|
+
const LIMIT = ${CSP_VIOLATION_LIMIT};
|
|
691
|
+
|
|
692
|
+
function post(type, payload) {
|
|
693
|
+
try {
|
|
694
|
+
window.parent.postMessage(Object.assign({ type }, payload), '*');
|
|
695
|
+
} catch (_) {}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function postExternal(href, source) {
|
|
699
|
+
post(OPEN_EXTERNAL, { href: String(href || ''), source });
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
document.addEventListener('click', (event) => {
|
|
703
|
+
let element = event.target;
|
|
704
|
+
while (element && element.nodeName !== 'A') element = element.parentElement;
|
|
705
|
+
if (!element) return;
|
|
706
|
+
|
|
707
|
+
const href = element.getAttribute('href');
|
|
708
|
+
const resolved = element.href || '';
|
|
709
|
+
|
|
710
|
+
if (href == null || href === '' || href.charAt(0) === '#') {
|
|
711
|
+
event.preventDefault();
|
|
712
|
+
event.stopPropagation();
|
|
713
|
+
const fragment = href && href.length > 1 ? href.slice(1) : '';
|
|
714
|
+
if (fragment) {
|
|
715
|
+
let target = null;
|
|
716
|
+
try {
|
|
717
|
+
target = document.getElementById(fragment) || document.querySelector('a[name="' + CSS.escape(fragment) + '"]');
|
|
718
|
+
} catch (_) {}
|
|
719
|
+
if (target && target.scrollIntoView) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
720
|
+
} else if (href === '#') {
|
|
721
|
+
try { window.scrollTo({ top: 0, behavior: 'smooth' }); } catch (_) {}
|
|
722
|
+
}
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Defense in depth: even if a relaxed custom sanitizer let a javascript: URL
|
|
727
|
+
// through, the bridge must not allow the default click to run it.
|
|
728
|
+
if (/^javascript:/i.test(resolved)) {
|
|
729
|
+
event.preventDefault();
|
|
730
|
+
event.stopPropagation();
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
try {
|
|
735
|
+
const url = new URL(resolved);
|
|
736
|
+
if (url.protocol === 'about:') return;
|
|
737
|
+
} catch (_) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
event.preventDefault();
|
|
742
|
+
event.stopPropagation();
|
|
743
|
+
postExternal(resolved, 'link');
|
|
744
|
+
}, true);
|
|
745
|
+
|
|
746
|
+
window.open = (url) => {
|
|
747
|
+
if (url) postExternal(url, 'window.open');
|
|
748
|
+
return null;
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
const seen = new Set();
|
|
752
|
+
let sent = 0;
|
|
753
|
+
document.addEventListener('securitypolicyviolation', (event) => {
|
|
754
|
+
if (sent >= LIMIT) return;
|
|
755
|
+
const key = (event.effectiveDirective || event.violatedDirective || '') + '|' + (event.blockedURI || '');
|
|
756
|
+
if (seen.has(key)) return;
|
|
757
|
+
seen.add(key);
|
|
758
|
+
sent += 1;
|
|
759
|
+
post(CSP_VIOLATION, {
|
|
760
|
+
blockedURI: String(event.blockedURI || ''),
|
|
761
|
+
violatedDirective: String(event.violatedDirective || ''),
|
|
762
|
+
effectiveDirective: String(event.effectiveDirective || ''),
|
|
763
|
+
sourceFile: String(event.sourceFile || ''),
|
|
764
|
+
lineNumber: Number(event.lineNumber || 0),
|
|
765
|
+
columnNumber: Number(event.columnNumber || 0),
|
|
766
|
+
disposition: String(event.disposition || ''),
|
|
767
|
+
sample: String(event.sample || '').slice(0, 120),
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
})();
|
|
771
|
+
<\/script>`;
|
|
772
|
+
}
|
|
773
|
+
function injectBridgeScript(html) {
|
|
774
|
+
const script = bridgeScript();
|
|
775
|
+
const cspMeta = /<meta[^>]*http-equiv\s*=\s*["']Content-Security-Policy["'][^>]*>/i;
|
|
776
|
+
if (cspMeta.test(html)) {
|
|
777
|
+
return html.replace(cspMeta, (match) => `${match}${script}`);
|
|
778
|
+
}
|
|
779
|
+
if (/<\/head>/i.test(html)) {
|
|
780
|
+
return html.replace(/<\/head>/i, `${script}</head>`);
|
|
781
|
+
}
|
|
782
|
+
if (/<body[^>]*>/i.test(html)) {
|
|
783
|
+
return html.replace(/<body[^>]*>/i, (match) => `${match}${script}`);
|
|
784
|
+
}
|
|
785
|
+
return `${html}${script}`;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/scrollbar.ts
|
|
789
|
+
var AUTHOR_SCROLLBAR_RE = /::-webkit-scrollbar|scrollbar-width|scrollbar-color/i;
|
|
790
|
+
var SCROLLBAR_STYLE = `<style data-html-preview-sandbox-scrollbar>
|
|
791
|
+
:root {
|
|
792
|
+
color-scheme: light;
|
|
793
|
+
}
|
|
794
|
+
html, body {
|
|
795
|
+
min-height: 100%;
|
|
796
|
+
}
|
|
797
|
+
* {
|
|
798
|
+
scrollbar-width: thin;
|
|
799
|
+
scrollbar-color: rgba(96, 112, 128, 0.45) transparent;
|
|
800
|
+
}
|
|
801
|
+
*::-webkit-scrollbar {
|
|
802
|
+
width: 10px;
|
|
803
|
+
height: 10px;
|
|
804
|
+
}
|
|
805
|
+
*::-webkit-scrollbar-thumb {
|
|
806
|
+
background: rgba(96, 112, 128, 0.38);
|
|
807
|
+
border-radius: 999px;
|
|
808
|
+
border: 2px solid transparent;
|
|
809
|
+
background-clip: content-box;
|
|
810
|
+
}
|
|
811
|
+
</style>`;
|
|
812
|
+
function hasAuthorScrollbarStyle(html) {
|
|
813
|
+
return AUTHOR_SCROLLBAR_RE.test(html || "");
|
|
814
|
+
}
|
|
815
|
+
function injectScrollbarStyle(html) {
|
|
816
|
+
if (hasAuthorScrollbarStyle(html)) return html;
|
|
817
|
+
if (/<\/head>/i.test(html)) {
|
|
818
|
+
return html.replace(/<\/head>/i, `${SCROLLBAR_STYLE}</head>`);
|
|
819
|
+
}
|
|
820
|
+
if (/<body[^>]*>/i.test(html)) {
|
|
821
|
+
return html.replace(/<body[^>]*>/i, (match) => `${match}${SCROLLBAR_STYLE}`);
|
|
822
|
+
}
|
|
823
|
+
return `${html}${SCROLLBAR_STYLE}`;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/document.browser.ts
|
|
827
|
+
async function createHtmlDocument(input, options = {}) {
|
|
828
|
+
const decoded = await normalizeInput(input, { maxBytes: options.maxBytes });
|
|
829
|
+
const sanitized = sanitizeHtml(decoded.html, options.sanitize);
|
|
830
|
+
if (!sanitized.html.trim()) {
|
|
831
|
+
sanitized.report.strippedAll = true;
|
|
832
|
+
throw new PreviewError(ERROR_CODES.EMPTY_AFTER_SANITIZE, "HTML is empty after sanitization");
|
|
833
|
+
}
|
|
834
|
+
const csp = buildCsp(options.csp);
|
|
835
|
+
const withCsp = injectCspMeta(sanitized.html, csp);
|
|
836
|
+
const withBridge = injectBridgeScript(withCsp);
|
|
837
|
+
const html = options.injectScrollbarStyle === false ? withBridge : injectScrollbarStyle(withBridge);
|
|
838
|
+
return {
|
|
839
|
+
html,
|
|
840
|
+
encoding: decoded.encoding,
|
|
841
|
+
sanitizeReport: sanitized.report
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// src/renderer-core.ts
|
|
846
|
+
function toMessage(error) {
|
|
847
|
+
return error instanceof Error ? error.message : String(error);
|
|
848
|
+
}
|
|
849
|
+
function toPreviewError(error) {
|
|
850
|
+
return error instanceof PreviewError ? error : new PreviewError(ERROR_CODES.RENDER_FAILED, toMessage(error), error);
|
|
851
|
+
}
|
|
852
|
+
function createPreviewFactory(createHtmlDocument2) {
|
|
853
|
+
return function createPreview2(container, options = {}) {
|
|
854
|
+
if (!container || typeof container.appendChild !== "function") {
|
|
855
|
+
throw new TypeError("createPreview requires a container HTMLElement");
|
|
856
|
+
}
|
|
857
|
+
let currentOptions = { ...options };
|
|
858
|
+
let iframe = null;
|
|
859
|
+
let destroyed = false;
|
|
860
|
+
let lastHtml = "";
|
|
861
|
+
function removeIframe() {
|
|
862
|
+
if (iframe?.parentNode) iframe.parentNode.removeChild(iframe);
|
|
863
|
+
iframe = null;
|
|
864
|
+
}
|
|
865
|
+
function ensureIframe() {
|
|
866
|
+
removeIframe();
|
|
867
|
+
iframe = document.createElement("iframe");
|
|
868
|
+
iframe.setAttribute("sandbox", getSandboxAttribute(currentOptions.sandboxTokens ?? DEFAULT_SANDBOX_TOKENS, {
|
|
869
|
+
allowUnsafeTokens: currentOptions.allowUnsafeSandboxTokens
|
|
870
|
+
}));
|
|
871
|
+
iframe.setAttribute("referrerpolicy", "no-referrer");
|
|
872
|
+
iframe.setAttribute("title", "HTML preview sandbox");
|
|
873
|
+
iframe.style.width = "100%";
|
|
874
|
+
iframe.style.height = "100%";
|
|
875
|
+
iframe.style.border = "0";
|
|
876
|
+
iframe.style.display = "block";
|
|
877
|
+
iframe.style.background = "#fff";
|
|
878
|
+
container.appendChild(iframe);
|
|
879
|
+
return iframe;
|
|
880
|
+
}
|
|
881
|
+
function handleMessage(event) {
|
|
882
|
+
const data = event?.data;
|
|
883
|
+
if (!data || typeof data !== "object") return;
|
|
884
|
+
if (!iframe || event.source !== iframe.contentWindow) return;
|
|
885
|
+
if ("type" in data && data.type === BRIDGE_CSP_VIOLATION) {
|
|
886
|
+
currentOptions.onCspViolation?.({
|
|
887
|
+
effectiveDirective: String(data.effectiveDirective || data.violatedDirective || ""),
|
|
888
|
+
blockedURI: String(data.blockedURI || ""),
|
|
889
|
+
sourceFile: String(data.sourceFile || ""),
|
|
890
|
+
lineNumber: Number(data.lineNumber || 0),
|
|
891
|
+
sample: String(data.sample || "")
|
|
892
|
+
});
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if ("type" in data && data.type === BRIDGE_OPEN_EXTERNAL) {
|
|
896
|
+
const href = String(data.href || "");
|
|
897
|
+
const source = data.source === "window.open" ? "window.open" : "link";
|
|
898
|
+
const context = { source };
|
|
899
|
+
if (!isAllowedExternal(href, context)) {
|
|
900
|
+
currentOptions.logger?.warn?.(`Blocked external URL: ${href}`);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
currentOptions.onOpenExternal?.(href, context);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
function isAllowedExternal(url, context) {
|
|
907
|
+
if (!isAllowedExternalUrl(url, currentOptions.externalProtocols)) return false;
|
|
908
|
+
if (!currentOptions.allowExternalUrl) return true;
|
|
909
|
+
try {
|
|
910
|
+
return currentOptions.allowExternalUrl(url, context) === true;
|
|
911
|
+
} catch (error) {
|
|
912
|
+
currentOptions.logger?.warn?.(`External URL policy threw: ${toMessage(error)}`);
|
|
913
|
+
return false;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function remountLastHtml() {
|
|
917
|
+
if (!lastHtml) return;
|
|
918
|
+
const target = ensureIframe();
|
|
919
|
+
target.srcdoc = lastHtml;
|
|
920
|
+
}
|
|
921
|
+
window.addEventListener("message", handleMessage);
|
|
922
|
+
return {
|
|
923
|
+
async render(input) {
|
|
924
|
+
if (destroyed) throw new Error("Preview has been destroyed");
|
|
925
|
+
try {
|
|
926
|
+
const result = await createHtmlDocument2(input, currentOptions);
|
|
927
|
+
currentOptions.onSanitize?.(result.sanitizeReport);
|
|
928
|
+
lastHtml = result.html;
|
|
929
|
+
const target = ensureIframe();
|
|
930
|
+
target.srcdoc = result.html;
|
|
931
|
+
return result;
|
|
932
|
+
} catch (error) {
|
|
933
|
+
currentOptions.onError?.(toPreviewError(error));
|
|
934
|
+
throw error;
|
|
935
|
+
}
|
|
936
|
+
},
|
|
937
|
+
updateOptions(patch) {
|
|
938
|
+
currentOptions = { ...currentOptions, ...patch };
|
|
939
|
+
},
|
|
940
|
+
notifyNavigationAttempt(url, context = { source: "navigation" }) {
|
|
941
|
+
const href = String(url || "");
|
|
942
|
+
if (!href) return;
|
|
943
|
+
currentOptions.onNavigationAttempt?.(href, context);
|
|
944
|
+
remountLastHtml();
|
|
945
|
+
if (isAllowedExternal(href, context)) {
|
|
946
|
+
currentOptions.onOpenExternal?.(href, context);
|
|
947
|
+
} else {
|
|
948
|
+
currentOptions.logger?.warn?.(`Blocked navigation URL: ${href}`);
|
|
949
|
+
}
|
|
950
|
+
},
|
|
951
|
+
destroy() {
|
|
952
|
+
destroyed = true;
|
|
953
|
+
window.removeEventListener("message", handleMessage);
|
|
954
|
+
removeIframe();
|
|
955
|
+
lastHtml = "";
|
|
956
|
+
},
|
|
957
|
+
get iframe() {
|
|
958
|
+
return iframe;
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// src/renderer.browser.ts
|
|
965
|
+
var createPreview = createPreviewFactory(createHtmlDocument);
|
|
966
|
+
export {
|
|
967
|
+
BRIDGE_CSP_VIOLATION,
|
|
968
|
+
BRIDGE_OPEN_EXTERNAL,
|
|
969
|
+
DEFAULT_ALLOWED_EXTERNAL_PROTOCOLS,
|
|
970
|
+
DEFAULT_MAX_BYTES,
|
|
971
|
+
DEFAULT_SANDBOX_TOKENS,
|
|
972
|
+
ERROR_CODES,
|
|
973
|
+
PreviewError,
|
|
974
|
+
UNSAFE_SANDBOX_TOKENS,
|
|
975
|
+
buildCsp,
|
|
976
|
+
createHtmlDocument,
|
|
977
|
+
createPreview,
|
|
978
|
+
decodeHtmlBytes,
|
|
979
|
+
getSandboxAttribute,
|
|
980
|
+
hasAuthorScrollbarStyle,
|
|
981
|
+
injectBridgeScript,
|
|
982
|
+
injectCspMeta,
|
|
983
|
+
injectScrollbarStyle,
|
|
984
|
+
isAllowedExternalUrl,
|
|
985
|
+
normalizeInput,
|
|
986
|
+
sanitizeHtml
|
|
987
|
+
};
|
|
988
|
+
//# sourceMappingURL=index.browser.js.map
|