@mermanjs/web-render 0.8.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +231 -0
- package/README.md +37 -0
- package/THIRD_PARTY_LICENSES/cose-base-v1/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/cose-base-v2/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/cytoscape/LICENSE +19 -0
- package/THIRD_PARTY_LICENSES/cytoscape-cose-bilkent/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/cytoscape-fcose/LICENSE +19 -0
- package/THIRD_PARTY_LICENSES/d3-shape/LICENSE +13 -0
- package/THIRD_PARTY_LICENSES/dagre/LICENSE +19 -0
- package/THIRD_PARTY_LICENSES/dompurify/LICENSE +202 -0
- package/THIRD_PARTY_LICENSES/eclipse-elk/LICENSE.md +264 -0
- package/THIRD_PARTY_LICENSES/elkjs/LICENSE.md +264 -0
- package/THIRD_PARTY_LICENSES/fmin/LICENSE +27 -0
- package/THIRD_PARTY_LICENSES/graphlib/LICENSE +19 -0
- package/THIRD_PARTY_LICENSES/katex-fonts/FONT_NOTICE.txt +12 -0
- package/THIRD_PARTY_LICENSES/katex-fonts/OFL.txt +85 -0
- package/THIRD_PARTY_LICENSES/layout-base-v1/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/layout-base-v2/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/mermaid/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/ratex/LICENSE +27 -0
- package/THIRD_PARTY_LICENSES/ratex/THIRD_PARTY_NOTICES.txt +19 -0
- package/THIRD_PARTY_LICENSES/rough-rs/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/roughjs/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/rust-cargo-dependencies.json +2101 -0
- package/THIRD_PARTY_LICENSES/sanitize-url/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/venn-js/LICENSE +22 -0
- package/THIRD_PARTY_LICENSES/zenuml-core/LICENSE +21 -0
- package/THIRD_PARTY_NOTICES.md +326 -0
- package/artifacts/provenance.json +342 -0
- package/artifacts/wasm/merman_wasm.d.ts +110 -0
- package/artifacts/wasm/merman_wasm.js +897 -0
- package/artifacts/wasm/merman_wasm_bg.wasm +0 -0
- package/artifacts/wasm/merman_wasm_bg.wasm.d.ts +35 -0
- package/dist/generated/binding-contract.js +497 -0
- package/dist/generated/binding-contract.js.map +1 -0
- package/dist/generated/capability-surface.d.ts +118 -0
- package/dist/generated/capability-surface.d.ts.map +1 -0
- package/dist/generated/capability-surface.js +171 -0
- package/dist/generated/capability-surface.js.map +1 -0
- package/dist/generated/diagram-catalog.d.ts +2 -0
- package/dist/generated/diagram-catalog.d.ts.map +1 -0
- package/dist/generated/diagram-catalog.js +40 -0
- package/dist/generated/diagram-catalog.js.map +1 -0
- package/dist/generated/resource-contract.d.ts +31 -0
- package/dist/generated/resource-contract.d.ts.map +1 -0
- package/dist/generated/resource-contract.js +193 -0
- package/dist/generated/resource-contract.js.map +1 -0
- package/dist/generated/text-measurement-abi.d.ts +154 -0
- package/dist/generated/text-measurement-abi.d.ts.map +1 -0
- package/dist/generated/text-measurement-abi.js +50 -0
- package/dist/generated/text-measurement-abi.js.map +1 -0
- package/dist/generated/token-descriptor.d.ts +205 -0
- package/dist/generated/token-descriptor.d.ts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/package-entries/render.d.ts +37 -0
- package/dist/package-entries/render.d.ts.map +1 -0
- package/dist/package-entries/render.js +73 -0
- package/dist/package-entries/render.js.map +1 -0
- package/dist/public-catalog.d.ts +127 -0
- package/dist/public-catalog.d.ts.map +1 -0
- package/dist/public-catalog.js +130 -0
- package/dist/public-catalog.js.map +1 -0
- package/dist/public-types.d.ts +764 -0
- package/dist/public-types.d.ts.map +1 -0
- package/dist/runtime-analysis.d.ts +11 -0
- package/dist/runtime-analysis.d.ts.map +1 -0
- package/dist/runtime-ascii.d.ts +6 -0
- package/dist/runtime-ascii.d.ts.map +1 -0
- package/dist/runtime-core.d.ts +18 -0
- package/dist/runtime-core.d.ts.map +1 -0
- package/dist/runtime-core.js +946 -0
- package/dist/runtime-core.js.map +1 -0
- package/dist/runtime-editor.d.ts +16 -0
- package/dist/runtime-editor.d.ts.map +1 -0
- package/dist/runtime-render.d.ts +13 -0
- package/dist/runtime-render.d.ts.map +1 -0
- package/dist/runtime-render.js +577 -0
- package/dist/runtime-render.js.map +1 -0
- package/dist/runtime-state.d.ts +16 -0
- package/dist/runtime-state.d.ts.map +1 -0
- package/dist/runtime-state.js +27 -0
- package/dist/runtime-state.js.map +1 -0
- package/dist/surface-runtime.js +45 -0
- package/dist/surface-runtime.js.map +1 -0
- package/dist/svg-safety-policy.js +1692 -0
- package/dist/svg-safety-policy.js.map +1 -0
- package/dist/svg-safety.d.ts +27 -0
- package/dist/svg-safety.d.ts.map +1 -0
- package/dist/svg-safety.js +101 -0
- package/dist/svg-safety.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,1692 @@
|
|
|
1
|
+
const ACTIVE_SVG_ELEMENTS = new Set([
|
|
2
|
+
"script",
|
|
3
|
+
"iframe",
|
|
4
|
+
"object",
|
|
5
|
+
"embed",
|
|
6
|
+
"applet",
|
|
7
|
+
"form",
|
|
8
|
+
"link",
|
|
9
|
+
"audio",
|
|
10
|
+
"video",
|
|
11
|
+
"canvas",
|
|
12
|
+
"animate",
|
|
13
|
+
"animatemotion",
|
|
14
|
+
"animatetransform",
|
|
15
|
+
"discard",
|
|
16
|
+
"mpath",
|
|
17
|
+
"set",
|
|
18
|
+
]);
|
|
19
|
+
const SVG_ELEMENTS = new Set([
|
|
20
|
+
"a",
|
|
21
|
+
"circle",
|
|
22
|
+
"clippath",
|
|
23
|
+
"defs",
|
|
24
|
+
"desc",
|
|
25
|
+
"ellipse",
|
|
26
|
+
"feblend",
|
|
27
|
+
"fecolormatrix",
|
|
28
|
+
"fecomponenttransfer",
|
|
29
|
+
"fecomposite",
|
|
30
|
+
"feconvolvematrix",
|
|
31
|
+
"fediffuselighting",
|
|
32
|
+
"fedisplacementmap",
|
|
33
|
+
"fedistantlight",
|
|
34
|
+
"fedropshadow",
|
|
35
|
+
"feflood",
|
|
36
|
+
"fefunca",
|
|
37
|
+
"fefuncb",
|
|
38
|
+
"fefuncg",
|
|
39
|
+
"fefuncr",
|
|
40
|
+
"fegaussianblur",
|
|
41
|
+
"feimage",
|
|
42
|
+
"femerge",
|
|
43
|
+
"femergenode",
|
|
44
|
+
"femorphology",
|
|
45
|
+
"feoffset",
|
|
46
|
+
"fepointlight",
|
|
47
|
+
"fespecularlighting",
|
|
48
|
+
"fespotlight",
|
|
49
|
+
"fetile",
|
|
50
|
+
"feturbulence",
|
|
51
|
+
"filter",
|
|
52
|
+
"foreignobject",
|
|
53
|
+
"g",
|
|
54
|
+
"image",
|
|
55
|
+
"line",
|
|
56
|
+
"lineargradient",
|
|
57
|
+
"marker",
|
|
58
|
+
"mask",
|
|
59
|
+
"metadata",
|
|
60
|
+
"path",
|
|
61
|
+
"pattern",
|
|
62
|
+
"polygon",
|
|
63
|
+
"polyline",
|
|
64
|
+
"radialgradient",
|
|
65
|
+
"rect",
|
|
66
|
+
"stop",
|
|
67
|
+
"style",
|
|
68
|
+
"svg",
|
|
69
|
+
"switch",
|
|
70
|
+
"symbol",
|
|
71
|
+
"text",
|
|
72
|
+
"textpath",
|
|
73
|
+
"title",
|
|
74
|
+
"tspan",
|
|
75
|
+
"use",
|
|
76
|
+
"view",
|
|
77
|
+
]);
|
|
78
|
+
const FOREIGN_OBJECT_LABEL_ELEMENTS = new Set([
|
|
79
|
+
"a",
|
|
80
|
+
"div",
|
|
81
|
+
"span",
|
|
82
|
+
"p",
|
|
83
|
+
"br",
|
|
84
|
+
"b",
|
|
85
|
+
"strong",
|
|
86
|
+
"i",
|
|
87
|
+
"em",
|
|
88
|
+
"s",
|
|
89
|
+
"u",
|
|
90
|
+
"small",
|
|
91
|
+
"sub",
|
|
92
|
+
"sup",
|
|
93
|
+
"code",
|
|
94
|
+
"pre",
|
|
95
|
+
]);
|
|
96
|
+
const FOREIGN_OBJECT_INTERACTIVE_ATTRIBUTES = new Set([
|
|
97
|
+
"autofocus",
|
|
98
|
+
"contenteditable",
|
|
99
|
+
"draggable",
|
|
100
|
+
"tabindex",
|
|
101
|
+
]);
|
|
102
|
+
const MAX_EMBEDDED_IMAGE_BYTES = 16 * 1024 * 1024;
|
|
103
|
+
const MAX_TOTAL_EMBEDDED_IMAGE_BYTES = 32 * 1024 * 1024;
|
|
104
|
+
const MAX_EMBEDDED_IMAGE_ENCODED_BYTES = 24 * 1024 * 1024;
|
|
105
|
+
const MAX_TOTAL_EMBEDDED_IMAGE_ENCODED_BYTES = 44 * 1024 * 1024;
|
|
106
|
+
const MAX_EMBEDDED_IMAGE_PIXELS = 16 * 1024 * 1024;
|
|
107
|
+
const MAX_TOTAL_EMBEDDED_IMAGE_PIXELS = 32 * 1024 * 1024;
|
|
108
|
+
const MAX_SAFE_SVG_SOURCE_CODE_UNITS = 64 * 1024 * 1024;
|
|
109
|
+
const MAX_SAFE_SVG_SOURCE_UTF8_BYTES = 64 * 1024 * 1024;
|
|
110
|
+
const MAX_SAFE_SVG_ATTRIBUTE_CODE_UNITS = 25 * 1024 * 1024;
|
|
111
|
+
const PNG_ANIMATION_CHUNKS = new Set(["acTL", "fcTL", "fdAT"]);
|
|
112
|
+
const PNG_STATIC_CHUNKS = new Set([
|
|
113
|
+
"IHDR",
|
|
114
|
+
"PLTE",
|
|
115
|
+
"IDAT",
|
|
116
|
+
"IEND",
|
|
117
|
+
"tRNS",
|
|
118
|
+
"cHRM",
|
|
119
|
+
"gAMA",
|
|
120
|
+
"iCCP",
|
|
121
|
+
"sBIT",
|
|
122
|
+
"sRGB",
|
|
123
|
+
"cICP",
|
|
124
|
+
"mDCV",
|
|
125
|
+
"iTXt",
|
|
126
|
+
"tEXt",
|
|
127
|
+
"zTXt",
|
|
128
|
+
"bKGD",
|
|
129
|
+
"hIST",
|
|
130
|
+
"pHYs",
|
|
131
|
+
"sPLT",
|
|
132
|
+
"eXIf",
|
|
133
|
+
"tIME",
|
|
134
|
+
]);
|
|
135
|
+
const PNG_COMPRESSED_METADATA_CHUNKS = new Set(["iCCP", "zTXt"]);
|
|
136
|
+
const RASTER_DATA_IMAGE_URL = /^data:image\/(png|gif|jpe?g|webp);base64,([a-z0-9+/]*={0,2})$/i;
|
|
137
|
+
const CSS_HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
138
|
+
const URL_SCHEME = /^[a-z][a-z0-9+.-]*:/;
|
|
139
|
+
const MERMAID_NAVIGATION_SCHEMES = new Set([
|
|
140
|
+
"callto:",
|
|
141
|
+
"cid:",
|
|
142
|
+
"ftp:",
|
|
143
|
+
"ftps:",
|
|
144
|
+
"http:",
|
|
145
|
+
"https:",
|
|
146
|
+
"mailto:",
|
|
147
|
+
"matrix:",
|
|
148
|
+
"sms:",
|
|
149
|
+
"tel:",
|
|
150
|
+
"xmpp:",
|
|
151
|
+
]);
|
|
152
|
+
const RAW_URL_ATTRIBUTES = new Set([
|
|
153
|
+
"action",
|
|
154
|
+
"background",
|
|
155
|
+
"cite",
|
|
156
|
+
"data",
|
|
157
|
+
"formaction",
|
|
158
|
+
"href",
|
|
159
|
+
"longdesc",
|
|
160
|
+
"manifest",
|
|
161
|
+
"ping",
|
|
162
|
+
"poster",
|
|
163
|
+
"profile",
|
|
164
|
+
"src",
|
|
165
|
+
"usemap",
|
|
166
|
+
]);
|
|
167
|
+
const SVG_URL_REFERENCE_ATTRIBUTES = new Set([
|
|
168
|
+
"clip-path",
|
|
169
|
+
"color-profile",
|
|
170
|
+
"cursor",
|
|
171
|
+
"fill",
|
|
172
|
+
"filter",
|
|
173
|
+
"marker",
|
|
174
|
+
"marker-end",
|
|
175
|
+
"marker-mid",
|
|
176
|
+
"marker-start",
|
|
177
|
+
"mask",
|
|
178
|
+
"stroke",
|
|
179
|
+
]);
|
|
180
|
+
const SELF_CONTAINED_SVG_POLICY = Object.freeze({
|
|
181
|
+
anchorNavigation: "fragment-only",
|
|
182
|
+
});
|
|
183
|
+
const NAVIGABLE_SVG_POLICY = Object.freeze({
|
|
184
|
+
anchorNavigation: "mermaid-compatible",
|
|
185
|
+
});
|
|
186
|
+
export function assertSelfContainedSvgWithMessagePrefix(svg, messagePrefix) {
|
|
187
|
+
const scanner = new SvgSafetyScanner(svg, messagePrefix, SELF_CONTAINED_SVG_POLICY);
|
|
188
|
+
return scanner.assertSafe();
|
|
189
|
+
}
|
|
190
|
+
export function assertNavigableSvgWithMessagePrefix(svg, messagePrefix) {
|
|
191
|
+
const scanner = new SvgSafetyScanner(svg, messagePrefix, NAVIGABLE_SVG_POLICY);
|
|
192
|
+
return scanner.assertSafe();
|
|
193
|
+
}
|
|
194
|
+
class SvgSafetyScanner {
|
|
195
|
+
constructor(source, messagePrefix, policy) {
|
|
196
|
+
this.source = source;
|
|
197
|
+
this.messagePrefix = messagePrefix;
|
|
198
|
+
this.policy = policy;
|
|
199
|
+
this.cursor = 0;
|
|
200
|
+
this.sawRoot = false;
|
|
201
|
+
this.rootDepth = 0;
|
|
202
|
+
this.rootClosedAt = null;
|
|
203
|
+
this.foreignObjectDepth = 0;
|
|
204
|
+
this.embeddedImageEncodedBytes = 0;
|
|
205
|
+
this.embeddedImageBytes = 0;
|
|
206
|
+
this.embeddedImagePixels = 0;
|
|
207
|
+
this.hasFragmentReferences = false;
|
|
208
|
+
this.elementStack = [];
|
|
209
|
+
}
|
|
210
|
+
assertSafe() {
|
|
211
|
+
if (this.source.length > MAX_SAFE_SVG_SOURCE_CODE_UNITS ||
|
|
212
|
+
!utf8LengthAtMost(this.source, MAX_SAFE_SVG_SOURCE_UTF8_BYTES)) {
|
|
213
|
+
throw this.error("SVG output exceeds the source byte limit.");
|
|
214
|
+
}
|
|
215
|
+
while (this.cursor < this.source.length) {
|
|
216
|
+
const tag = this.nextTag();
|
|
217
|
+
if (!tag) {
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
if (this.rootClosedAt !== null) {
|
|
221
|
+
throw this.error("malformed SVG output.");
|
|
222
|
+
}
|
|
223
|
+
if (tag.kind === "end") {
|
|
224
|
+
const elementName = localName(tag.name);
|
|
225
|
+
if (!this.sawRoot || this.elementStack.length === 0) {
|
|
226
|
+
throw this.error("malformed SVG output.");
|
|
227
|
+
}
|
|
228
|
+
const openElementName = this.elementStack.pop();
|
|
229
|
+
if (openElementName !== elementName) {
|
|
230
|
+
throw this.error("malformed SVG output.");
|
|
231
|
+
}
|
|
232
|
+
if (elementName === "foreignobject" && this.foreignObjectDepth > 0) {
|
|
233
|
+
this.foreignObjectDepth -= 1;
|
|
234
|
+
}
|
|
235
|
+
this.rootDepth = this.elementStack.length;
|
|
236
|
+
if (this.rootDepth === 0) {
|
|
237
|
+
this.rootClosedAt = tag.end;
|
|
238
|
+
}
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const elementName = localName(tag.name);
|
|
242
|
+
const inForeignObject = this.foreignObjectDepth > 0;
|
|
243
|
+
if (!this.sawRoot) {
|
|
244
|
+
this.sawRoot = true;
|
|
245
|
+
if (elementName !== "svg") {
|
|
246
|
+
throw this.error("non-SVG output.");
|
|
247
|
+
}
|
|
248
|
+
if (!tag.selfClosing) {
|
|
249
|
+
this.elementStack.push(elementName);
|
|
250
|
+
this.rootDepth = this.elementStack.length;
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
this.rootClosedAt = tag.end;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else if (!tag.selfClosing) {
|
|
257
|
+
this.elementStack.push(elementName);
|
|
258
|
+
this.rootDepth = this.elementStack.length;
|
|
259
|
+
}
|
|
260
|
+
this.assertSafeElementName(elementName, inForeignObject);
|
|
261
|
+
this.assertSafeAttributes(elementName, tag.attributes, inForeignObject || elementName === "foreignobject");
|
|
262
|
+
if (elementName === "foreignobject" && !tag.selfClosing) {
|
|
263
|
+
this.foreignObjectDepth += 1;
|
|
264
|
+
}
|
|
265
|
+
if (elementName === "style" && !tag.selfClosing) {
|
|
266
|
+
const styleEnd = findClosingStyle(this.source, tag.end);
|
|
267
|
+
if (styleEnd === null) {
|
|
268
|
+
throw this.error("malformed SVG output.");
|
|
269
|
+
}
|
|
270
|
+
const styleText = this.source.slice(tag.end, styleEnd);
|
|
271
|
+
this.assertSafeCss(styleText);
|
|
272
|
+
this.cursor = styleEnd;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (!this.sawRoot) {
|
|
276
|
+
throw this.error("non-SVG output.");
|
|
277
|
+
}
|
|
278
|
+
if (this.rootClosedAt === null || this.rootDepth !== 0 || this.elementStack.length !== 0) {
|
|
279
|
+
throw this.error("malformed SVG output.");
|
|
280
|
+
}
|
|
281
|
+
this.assertOnlyIgnorableRootTail(this.rootClosedAt);
|
|
282
|
+
if (this.foreignObjectDepth !== 0) {
|
|
283
|
+
throw this.error("malformed SVG output.");
|
|
284
|
+
}
|
|
285
|
+
return Object.freeze({ hasFragmentReferences: this.hasFragmentReferences });
|
|
286
|
+
}
|
|
287
|
+
nextTag() {
|
|
288
|
+
while (true) {
|
|
289
|
+
const start = this.source.indexOf("<", this.cursor);
|
|
290
|
+
if (start < 0) {
|
|
291
|
+
this.cursor = this.source.length;
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
if (!this.sawRoot && !isOnlyWhitespace(this.source.slice(this.cursor, start))) {
|
|
295
|
+
throw this.error("non-SVG output.");
|
|
296
|
+
}
|
|
297
|
+
if (this.source.startsWith("<!--", start)) {
|
|
298
|
+
this.cursor = this.consumeUntil(start + 4, "-->");
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (this.source.startsWith("<?", start)) {
|
|
302
|
+
this.cursor = this.consumeUntil(start + 2, "?>");
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (this.source.startsWith("<![CDATA[", start)) {
|
|
306
|
+
this.cursor = this.consumeUntil(start + 9, "]]>");
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (this.source.startsWith("<!", start)) {
|
|
310
|
+
throw this.error("SVG with unsupported declarations.");
|
|
311
|
+
}
|
|
312
|
+
const tag = this.parseTag(start);
|
|
313
|
+
this.cursor = tag.end;
|
|
314
|
+
return tag;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
parseTag(start) {
|
|
318
|
+
let cursor = start + 1;
|
|
319
|
+
let kind = "start";
|
|
320
|
+
if (this.source[cursor] === "/") {
|
|
321
|
+
kind = "end";
|
|
322
|
+
cursor += 1;
|
|
323
|
+
}
|
|
324
|
+
cursor = skipWhitespace(this.source, cursor);
|
|
325
|
+
const nameStart = cursor;
|
|
326
|
+
while (cursor < this.source.length && !isNameTerminator(this.source[cursor] ?? "")) {
|
|
327
|
+
cursor += 1;
|
|
328
|
+
}
|
|
329
|
+
const name = this.source.slice(nameStart, cursor);
|
|
330
|
+
if (!name) {
|
|
331
|
+
throw this.error("malformed SVG output.");
|
|
332
|
+
}
|
|
333
|
+
const attributes = [];
|
|
334
|
+
while (cursor < this.source.length) {
|
|
335
|
+
cursor = skipWhitespace(this.source, cursor);
|
|
336
|
+
const char = this.source[cursor];
|
|
337
|
+
if (char === ">") {
|
|
338
|
+
return { kind, name, attributes, end: cursor + 1, selfClosing: false };
|
|
339
|
+
}
|
|
340
|
+
if (char === "/" && this.source[cursor + 1] === ">") {
|
|
341
|
+
return { kind, name, attributes, end: cursor + 2, selfClosing: true };
|
|
342
|
+
}
|
|
343
|
+
if (kind === "end") {
|
|
344
|
+
throw this.error("malformed SVG output.");
|
|
345
|
+
}
|
|
346
|
+
const attributeStart = cursor;
|
|
347
|
+
while (cursor < this.source.length &&
|
|
348
|
+
!isAttributeNameTerminator(this.source[cursor] ?? "")) {
|
|
349
|
+
cursor += 1;
|
|
350
|
+
}
|
|
351
|
+
const attributeName = this.source.slice(attributeStart, cursor);
|
|
352
|
+
if (!attributeName) {
|
|
353
|
+
throw this.error("malformed SVG output.");
|
|
354
|
+
}
|
|
355
|
+
cursor = skipWhitespace(this.source, cursor);
|
|
356
|
+
let value = "";
|
|
357
|
+
if (this.source[cursor] === "=") {
|
|
358
|
+
cursor += 1;
|
|
359
|
+
cursor = skipWhitespace(this.source, cursor);
|
|
360
|
+
const quote = this.source[cursor];
|
|
361
|
+
if (quote === '"' || quote === "'") {
|
|
362
|
+
const valueStart = cursor + 1;
|
|
363
|
+
const valueEnd = this.source.indexOf(quote, valueStart);
|
|
364
|
+
if (valueEnd < 0) {
|
|
365
|
+
throw this.error("malformed SVG output.");
|
|
366
|
+
}
|
|
367
|
+
value = this.source.slice(valueStart, valueEnd);
|
|
368
|
+
cursor = valueEnd + 1;
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
const valueStart = cursor;
|
|
372
|
+
while (cursor < this.source.length &&
|
|
373
|
+
!isUnquotedValueTerminator(this.source[cursor] ?? "")) {
|
|
374
|
+
cursor += 1;
|
|
375
|
+
}
|
|
376
|
+
value = this.source.slice(valueStart, cursor);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
attributes.push({ name: attributeName, value });
|
|
380
|
+
}
|
|
381
|
+
throw this.error("malformed SVG output.");
|
|
382
|
+
}
|
|
383
|
+
assertSafeElementName(name, inForeignObject) {
|
|
384
|
+
if (ACTIVE_SVG_ELEMENTS.has(name)) {
|
|
385
|
+
throw this.error("SVG with active embedded content.");
|
|
386
|
+
}
|
|
387
|
+
if (inForeignObject) {
|
|
388
|
+
if (!FOREIGN_OBJECT_LABEL_ELEMENTS.has(name)) {
|
|
389
|
+
throw this.error("SVG with unsupported foreignObject content.");
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (!SVG_ELEMENTS.has(name)) {
|
|
394
|
+
throw this.error("SVG with unsupported element content.");
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
assertSafeAttributes(elementName, attributes, inForeignObject) {
|
|
398
|
+
for (const attribute of attributes) {
|
|
399
|
+
if (attribute.value.length > MAX_SAFE_SVG_ATTRIBUTE_CODE_UNITS) {
|
|
400
|
+
throw this.error("SVG attribute exceeds the raw value limit.");
|
|
401
|
+
}
|
|
402
|
+
const name = attribute.name.toLowerCase();
|
|
403
|
+
const nameWithoutNamespace = localName(name);
|
|
404
|
+
const value = decodeXmlEntities(attribute.value);
|
|
405
|
+
if (nameWithoutNamespace.startsWith("on")) {
|
|
406
|
+
throw this.error("SVG with event handler attributes.");
|
|
407
|
+
}
|
|
408
|
+
if (elementName === "a") {
|
|
409
|
+
this.assertSafeAnchorInteractionAttribute(nameWithoutNamespace, value);
|
|
410
|
+
}
|
|
411
|
+
if (nameWithoutNamespace === "base") {
|
|
412
|
+
throw this.error("SVG with base URL attributes.");
|
|
413
|
+
}
|
|
414
|
+
if (nameWithoutNamespace === "srcset") {
|
|
415
|
+
this.assertSafeSrcset(value);
|
|
416
|
+
}
|
|
417
|
+
if (RAW_URL_ATTRIBUTES.has(nameWithoutNamespace)) {
|
|
418
|
+
this.assertSafeUrl(attribute.value, "attribute", elementName, nameWithoutNamespace);
|
|
419
|
+
}
|
|
420
|
+
if (SVG_URL_REFERENCE_ATTRIBUTES.has(nameWithoutNamespace)) {
|
|
421
|
+
this.assertSafeUrlReferences(value, nameWithoutNamespace);
|
|
422
|
+
}
|
|
423
|
+
if (nameWithoutNamespace === "style") {
|
|
424
|
+
this.assertSafeCss(value);
|
|
425
|
+
}
|
|
426
|
+
if (inForeignObject &&
|
|
427
|
+
FOREIGN_OBJECT_INTERACTIVE_ATTRIBUTES.has(nameWithoutNamespace)) {
|
|
428
|
+
throw this.error("SVG with interactive foreignObject attributes.");
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
assertSafeAnchorInteractionAttribute(name, value) {
|
|
433
|
+
if (name === "download") {
|
|
434
|
+
throw this.error("SVG with unsupported navigation download attributes.");
|
|
435
|
+
}
|
|
436
|
+
if (name === "ping" || name === "attributionsrc") {
|
|
437
|
+
throw this.error("SVG with unsupported navigation tracking attributes.");
|
|
438
|
+
}
|
|
439
|
+
if (name === "target") {
|
|
440
|
+
const trimmedTarget = value.trim();
|
|
441
|
+
if (value !== trimmedTarget) {
|
|
442
|
+
throw this.error("SVG with malformed navigation target attributes.");
|
|
443
|
+
}
|
|
444
|
+
const target = trimmedTarget.toLowerCase();
|
|
445
|
+
const allowedTarget = target === "" ||
|
|
446
|
+
(this.policy.anchorNavigation === "mermaid-compatible" &&
|
|
447
|
+
(target === "_self" ||
|
|
448
|
+
target === "_blank" ||
|
|
449
|
+
target === "_parent" ||
|
|
450
|
+
target === "_top"));
|
|
451
|
+
if (!allowedTarget) {
|
|
452
|
+
throw this.error("SVG with unsupported navigation target attributes.");
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (name === "rel" &&
|
|
456
|
+
value
|
|
457
|
+
.toLowerCase()
|
|
458
|
+
.split(/\s+/u)
|
|
459
|
+
.includes("opener")) {
|
|
460
|
+
throw this.error("SVG with unsafe navigation relationship attributes.");
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
assertSafeUrl(value, source, elementName, attributeName) {
|
|
464
|
+
const normalized = source === "attribute" ? decodeXmlEntities(value) : value;
|
|
465
|
+
const decoded = normalized;
|
|
466
|
+
const compact = removeAsciiWhitespaceAndControl(normalized).toLowerCase();
|
|
467
|
+
const trimmed = normalized.trim().toLowerCase();
|
|
468
|
+
if (compact.startsWith("#")) {
|
|
469
|
+
this.hasFragmentReferences = true;
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (source === "attribute" &&
|
|
473
|
+
elementName === "a" &&
|
|
474
|
+
attributeName === "href" &&
|
|
475
|
+
this.policy.anchorNavigation === "mermaid-compatible") {
|
|
476
|
+
this.assertSafeAnchorNavigationUrl(value, decoded, normalized, compact, trimmed);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (compact.startsWith("data:")) {
|
|
480
|
+
if (source !== "attribute" ||
|
|
481
|
+
attributeName !== "href" ||
|
|
482
|
+
(elementName !== "image" && elementName !== "feimage")) {
|
|
483
|
+
throw this.error("SVG with unsafe embedded resource references.");
|
|
484
|
+
}
|
|
485
|
+
this.assertSafeEmbeddedRasterDataUrl(normalized);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (containsCharacterReference(normalized)) {
|
|
489
|
+
throw this.error(source === "css"
|
|
490
|
+
? "SVG with unsafe CSS character references."
|
|
491
|
+
: "SVG with unsafe URL character references.");
|
|
492
|
+
}
|
|
493
|
+
if (trimmed.startsWith("http://") ||
|
|
494
|
+
trimmed.startsWith("https://") ||
|
|
495
|
+
trimmed.startsWith("//") ||
|
|
496
|
+
!URL_SCHEME.test(compact)) {
|
|
497
|
+
throw this.error(source === "css"
|
|
498
|
+
? "SVG with external CSS resource references."
|
|
499
|
+
: "SVG with external resource references.");
|
|
500
|
+
}
|
|
501
|
+
throw this.error(source === "css"
|
|
502
|
+
? "SVG with unsafe CSS URL references."
|
|
503
|
+
: "SVG with unsafe URL attributes.");
|
|
504
|
+
}
|
|
505
|
+
assertSafeAnchorNavigationUrl(rawValue, decodedValue, normalized, compact, trimmed) {
|
|
506
|
+
// Navigation URLs are intentionally stricter than ordinary URL parsing:
|
|
507
|
+
// whitespace, CSS escapes, and character references must not be able to
|
|
508
|
+
// disguise the scheme or turn a relative URL into an ambient navigation.
|
|
509
|
+
if (decodedValue !== normalized ||
|
|
510
|
+
containsCharacterReference(rawValue.replace(/&/gi, "")) ||
|
|
511
|
+
normalized !== normalized.trim() ||
|
|
512
|
+
compact !== normalized.toLowerCase()) {
|
|
513
|
+
throw this.error("SVG with unsafe navigation URL attributes.");
|
|
514
|
+
}
|
|
515
|
+
if (!trimmed) {
|
|
516
|
+
throw this.error("SVG with malformed navigation URL attributes.");
|
|
517
|
+
}
|
|
518
|
+
if (trimmed.startsWith("//") || trimmed.includes("\\")) {
|
|
519
|
+
throw this.error("SVG with ambient navigation URL attributes.");
|
|
520
|
+
}
|
|
521
|
+
const schemeMatch = URL_SCHEME.exec(trimmed);
|
|
522
|
+
if (!schemeMatch) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const scheme = schemeMatch[0].toLowerCase();
|
|
526
|
+
if (!MERMAID_NAVIGATION_SCHEMES.has(scheme)) {
|
|
527
|
+
throw this.error("SVG with unsafe navigation URL attributes.");
|
|
528
|
+
}
|
|
529
|
+
if (scheme !== "http:" && scheme !== "https:" && scheme !== "ftp:" && scheme !== "ftps:") {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
let parsed;
|
|
533
|
+
try {
|
|
534
|
+
parsed = new URL(trimmed);
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
throw this.error("SVG with malformed navigation URL attributes.");
|
|
538
|
+
}
|
|
539
|
+
if (!parsed.hostname) {
|
|
540
|
+
throw this.error("SVG with malformed navigation URL attributes.");
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
assertSafeEmbeddedRasterDataUrl(value) {
|
|
544
|
+
const parsed = parseRasterDataUrl(value);
|
|
545
|
+
if (!parsed) {
|
|
546
|
+
throw this.error("SVG with malformed embedded raster data URL.");
|
|
547
|
+
}
|
|
548
|
+
if (parsed.encodedBytes > MAX_EMBEDDED_IMAGE_ENCODED_BYTES) {
|
|
549
|
+
throw this.error("SVG embedded raster exceeds the per-image encoded byte limit.");
|
|
550
|
+
}
|
|
551
|
+
if (parsed.encodedBytes >
|
|
552
|
+
MAX_TOTAL_EMBEDDED_IMAGE_ENCODED_BYTES - this.embeddedImageEncodedBytes) {
|
|
553
|
+
throw this.error("SVG embedded rasters exceed the aggregate encoded byte limit.");
|
|
554
|
+
}
|
|
555
|
+
if (parsed.decodedBytes > MAX_EMBEDDED_IMAGE_BYTES) {
|
|
556
|
+
throw this.error("SVG embedded raster exceeds the per-image byte limit.");
|
|
557
|
+
}
|
|
558
|
+
if (parsed.decodedBytes > MAX_TOTAL_EMBEDDED_IMAGE_BYTES - this.embeddedImageBytes) {
|
|
559
|
+
throw this.error("SVG embedded rasters exceed the aggregate byte limit.");
|
|
560
|
+
}
|
|
561
|
+
const bytes = decodeBase64(parsed.payload, parsed.decodedBytes);
|
|
562
|
+
if (!bytes) {
|
|
563
|
+
throw this.error("SVG with malformed embedded raster data URL.");
|
|
564
|
+
}
|
|
565
|
+
const inspected = inspectRasterImage(bytes);
|
|
566
|
+
if (!inspected) {
|
|
567
|
+
throw this.error("SVG with malformed embedded raster image.");
|
|
568
|
+
}
|
|
569
|
+
if (inspected.format !== parsed.declaredFormat) {
|
|
570
|
+
throw this.error("SVG embedded raster MIME type does not match its file format.");
|
|
571
|
+
}
|
|
572
|
+
if (inspected.animated) {
|
|
573
|
+
throw this.error("SVG with animated or multi-frame embedded raster image.");
|
|
574
|
+
}
|
|
575
|
+
if (inspected.width > Math.floor(MAX_EMBEDDED_IMAGE_PIXELS / inspected.height)) {
|
|
576
|
+
throw this.error("SVG embedded raster exceeds the per-image pixel limit.");
|
|
577
|
+
}
|
|
578
|
+
const pixels = inspected.width * inspected.height;
|
|
579
|
+
if (pixels > MAX_TOTAL_EMBEDDED_IMAGE_PIXELS - this.embeddedImagePixels) {
|
|
580
|
+
throw this.error("SVG embedded rasters exceed the aggregate pixel limit.");
|
|
581
|
+
}
|
|
582
|
+
this.embeddedImageEncodedBytes += parsed.encodedBytes;
|
|
583
|
+
this.embeddedImageBytes += parsed.decodedBytes;
|
|
584
|
+
this.embeddedImagePixels += pixels;
|
|
585
|
+
}
|
|
586
|
+
assertSafeSrcset(value) {
|
|
587
|
+
if (value.trim().length === 0) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
throw this.error("SVG with srcset resource references.");
|
|
591
|
+
}
|
|
592
|
+
assertSafeCss(css) {
|
|
593
|
+
const normalized = decodeCssEscapes(decodeXmlEntities(css));
|
|
594
|
+
const withoutComments = stripCssComments(normalized);
|
|
595
|
+
const lower = withoutComments.toLowerCase();
|
|
596
|
+
if (containsCharacterReference(withoutComments)) {
|
|
597
|
+
throw this.error("SVG with unsafe CSS character references.");
|
|
598
|
+
}
|
|
599
|
+
if (lower.includes("@import")) {
|
|
600
|
+
throw this.error("SVG with external CSS resource references.");
|
|
601
|
+
}
|
|
602
|
+
if (containsShadowScopingSelector(lower)) {
|
|
603
|
+
throw this.error("SVG with unsafe shadow CSS selectors.");
|
|
604
|
+
}
|
|
605
|
+
if (containsCssFunction(lower, "image-set") ||
|
|
606
|
+
containsCssFunction(lower, "-webkit-image-set")) {
|
|
607
|
+
throw this.error("SVG with external CSS resource references.");
|
|
608
|
+
}
|
|
609
|
+
if (containsViewportEscapingCssDeclaration(lower)) {
|
|
610
|
+
throw this.error("SVG with viewport-escaping CSS.");
|
|
611
|
+
}
|
|
612
|
+
let cursor = 0;
|
|
613
|
+
while (cursor < lower.length) {
|
|
614
|
+
const urlIndex = lower.indexOf("url", cursor);
|
|
615
|
+
if (urlIndex < 0) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
cursor = urlIndex + "url".length;
|
|
619
|
+
cursor = skipWhitespace(lower, cursor);
|
|
620
|
+
if (lower[cursor] !== "(") {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
const valueStart = cursor + 1;
|
|
624
|
+
const valueEnd = lower.indexOf(")", valueStart);
|
|
625
|
+
if (valueEnd < 0) {
|
|
626
|
+
throw this.error("malformed SVG CSS.");
|
|
627
|
+
}
|
|
628
|
+
const rawValue = withoutComments.slice(valueStart, valueEnd).trim();
|
|
629
|
+
const unquoted = (rawValue.startsWith('"') && rawValue.endsWith('"')) ||
|
|
630
|
+
(rawValue.startsWith("'") && rawValue.endsWith("'"))
|
|
631
|
+
? rawValue.slice(1, -1)
|
|
632
|
+
: rawValue;
|
|
633
|
+
this.assertSafeUrl(unquoted, "css");
|
|
634
|
+
cursor = valueEnd + 1;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
assertSafeUrlReferences(value, attributeName) {
|
|
638
|
+
const normalized = decodeCssEscapes(decodeXmlEntities(value));
|
|
639
|
+
const lower = normalized.toLowerCase();
|
|
640
|
+
let cursor = 0;
|
|
641
|
+
let sawUrlReference = false;
|
|
642
|
+
while (cursor < lower.length) {
|
|
643
|
+
const urlIndex = lower.indexOf("url", cursor);
|
|
644
|
+
if (urlIndex < 0) {
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
cursor = urlIndex + "url".length;
|
|
648
|
+
cursor = skipWhitespace(lower, cursor);
|
|
649
|
+
if (lower[cursor] !== "(") {
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
sawUrlReference = true;
|
|
653
|
+
const valueStart = cursor + 1;
|
|
654
|
+
const valueEnd = lower.indexOf(")", valueStart);
|
|
655
|
+
if (valueEnd < 0) {
|
|
656
|
+
throw this.error("malformed SVG URL references.");
|
|
657
|
+
}
|
|
658
|
+
const rawValue = normalized.slice(valueStart, valueEnd).trim();
|
|
659
|
+
const unquoted = (rawValue.startsWith('"') && rawValue.endsWith('"')) ||
|
|
660
|
+
(rawValue.startsWith("'") && rawValue.endsWith("'"))
|
|
661
|
+
? rawValue.slice(1, -1)
|
|
662
|
+
: rawValue;
|
|
663
|
+
this.assertSafeUrl(unquoted, "css");
|
|
664
|
+
cursor = valueEnd + 1;
|
|
665
|
+
}
|
|
666
|
+
if (sawUrlReference) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const compact = removeAsciiWhitespaceAndControl(normalized).toLowerCase();
|
|
670
|
+
if (!compact || compact === "none") {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (compact.startsWith("#")) {
|
|
674
|
+
if (attributeName === "fill" || attributeName === "stroke") {
|
|
675
|
+
if (!CSS_HEX_COLOR.test(compact)) {
|
|
676
|
+
throw this.error("SVG with malformed hexadecimal paint values.");
|
|
677
|
+
}
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
this.hasFragmentReferences = true;
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (compact.startsWith("//") || compact.startsWith("/") || URL_SCHEME.test(compact)) {
|
|
684
|
+
this.assertSafeUrl(normalized, "css");
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
assertOnlyIgnorableRootTail(start) {
|
|
688
|
+
let cursor = start;
|
|
689
|
+
while (cursor < this.source.length) {
|
|
690
|
+
cursor = skipWhitespace(this.source, cursor);
|
|
691
|
+
if (cursor >= this.source.length) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (this.source.startsWith("<!--", cursor)) {
|
|
695
|
+
cursor = this.consumeUntil(cursor + 4, "-->");
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
throw this.error("malformed SVG output.");
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
consumeUntil(start, terminator) {
|
|
702
|
+
const end = this.source.indexOf(terminator, start);
|
|
703
|
+
if (end < 0) {
|
|
704
|
+
throw this.error("malformed SVG output.");
|
|
705
|
+
}
|
|
706
|
+
return end + terminator.length;
|
|
707
|
+
}
|
|
708
|
+
error(suffix) {
|
|
709
|
+
return new Error(`${this.messagePrefix} ${suffix}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function parseRasterDataUrl(value) {
|
|
713
|
+
const match = RASTER_DATA_IMAGE_URL.exec(value);
|
|
714
|
+
if (!match) {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
const mime = match[1]?.toLowerCase();
|
|
718
|
+
const payload = match[2];
|
|
719
|
+
if (!mime || !payload || payload.length % 4 !== 0) {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
const firstPadding = payload.indexOf("=");
|
|
723
|
+
if (firstPadding >= 0 && firstPadding < payload.length - 2) {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
const padding = payload.endsWith("==") ? 2 : payload.endsWith("=") ? 1 : 0;
|
|
727
|
+
const decodedBytes = (payload.length / 4) * 3 - padding;
|
|
728
|
+
if (!Number.isSafeInteger(decodedBytes) || decodedBytes <= 0) {
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
declaredFormat: mime === "jpg" ? "jpeg" : mime,
|
|
733
|
+
payload,
|
|
734
|
+
encodedBytes: payload.length,
|
|
735
|
+
decodedBytes,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function decodeBase64(payload, decodedBytes) {
|
|
739
|
+
const output = new Uint8Array(decodedBytes);
|
|
740
|
+
let outputCursor = 0;
|
|
741
|
+
for (let cursor = 0; cursor < payload.length; cursor += 4) {
|
|
742
|
+
const a = base64Value(payload[cursor] ?? "");
|
|
743
|
+
const b = base64Value(payload[cursor + 1] ?? "");
|
|
744
|
+
const cChar = payload[cursor + 2] ?? "";
|
|
745
|
+
const dChar = payload[cursor + 3] ?? "";
|
|
746
|
+
const isLast = cursor + 4 === payload.length;
|
|
747
|
+
if (a < 0 || b < 0) {
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
output[outputCursor] = (a << 2) | (b >> 4);
|
|
751
|
+
outputCursor += 1;
|
|
752
|
+
if (cChar === "=") {
|
|
753
|
+
if (!isLast || dChar !== "=" || (b & 0x0f) !== 0) {
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
const c = base64Value(cChar);
|
|
759
|
+
if (c < 0 || outputCursor >= decodedBytes) {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
output[outputCursor] = ((b & 0x0f) << 4) | (c >> 2);
|
|
763
|
+
outputCursor += 1;
|
|
764
|
+
if (dChar === "=") {
|
|
765
|
+
if (!isLast || (c & 0x03) !== 0) {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
const d = base64Value(dChar);
|
|
771
|
+
if (d < 0 || outputCursor >= decodedBytes) {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
output[outputCursor] = ((c & 0x03) << 6) | d;
|
|
775
|
+
outputCursor += 1;
|
|
776
|
+
}
|
|
777
|
+
return outputCursor === decodedBytes ? output : null;
|
|
778
|
+
}
|
|
779
|
+
function base64Value(char) {
|
|
780
|
+
const code = char.charCodeAt(0);
|
|
781
|
+
if (code >= 65 && code <= 90) {
|
|
782
|
+
return code - 65;
|
|
783
|
+
}
|
|
784
|
+
if (code >= 97 && code <= 122) {
|
|
785
|
+
return code - 97 + 26;
|
|
786
|
+
}
|
|
787
|
+
if (code >= 48 && code <= 57) {
|
|
788
|
+
return code - 48 + 52;
|
|
789
|
+
}
|
|
790
|
+
if (char === "+") {
|
|
791
|
+
return 62;
|
|
792
|
+
}
|
|
793
|
+
if (char === "/") {
|
|
794
|
+
return 63;
|
|
795
|
+
}
|
|
796
|
+
return -1;
|
|
797
|
+
}
|
|
798
|
+
function inspectRasterImage(bytes) {
|
|
799
|
+
if (hasBytes(bytes, 0, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
|
|
800
|
+
return inspectPng(bytes);
|
|
801
|
+
}
|
|
802
|
+
if (asciiAt(bytes, 0, "GIF87a") || asciiAt(bytes, 0, "GIF89a")) {
|
|
803
|
+
return inspectGif(bytes);
|
|
804
|
+
}
|
|
805
|
+
if (hasBytes(bytes, 0, [0xff, 0xd8])) {
|
|
806
|
+
return inspectJpeg(bytes);
|
|
807
|
+
}
|
|
808
|
+
if (asciiAt(bytes, 0, "RIFF") && asciiAt(bytes, 8, "WEBP")) {
|
|
809
|
+
return inspectWebp(bytes);
|
|
810
|
+
}
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
function inspectPng(bytes) {
|
|
814
|
+
let cursor = 8;
|
|
815
|
+
let width = 0;
|
|
816
|
+
let height = 0;
|
|
817
|
+
let sawHeader = false;
|
|
818
|
+
let sawPalette = false;
|
|
819
|
+
let sawImageData = false;
|
|
820
|
+
let imageDataEnded = false;
|
|
821
|
+
let sawEnd = false;
|
|
822
|
+
let animated = false;
|
|
823
|
+
while (cursor < bytes.length) {
|
|
824
|
+
if (bytes.length - cursor < 12) {
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
const length = readU32Be(bytes, cursor);
|
|
828
|
+
const type = asciiSlice(bytes, cursor + 4, 4);
|
|
829
|
+
if (length === null || type === null || length > bytes.length - cursor - 12) {
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
if (!PNG_STATIC_CHUNKS.has(type) && !PNG_ANIMATION_CHUNKS.has(type)) {
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
const dataStart = cursor + 8;
|
|
836
|
+
const next = dataStart + length + 4;
|
|
837
|
+
if (!sawHeader) {
|
|
838
|
+
if (type !== "IHDR" || length !== 13) {
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
const parsedWidth = readU32Be(bytes, dataStart);
|
|
842
|
+
const parsedHeight = readU32Be(bytes, dataStart + 4);
|
|
843
|
+
if (parsedWidth === null ||
|
|
844
|
+
parsedHeight === null ||
|
|
845
|
+
parsedWidth === 0 ||
|
|
846
|
+
parsedHeight === 0 ||
|
|
847
|
+
!validPngHeader(bytes.subarray(dataStart, dataStart + length))) {
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
width = parsedWidth;
|
|
851
|
+
height = parsedHeight;
|
|
852
|
+
sawHeader = true;
|
|
853
|
+
}
|
|
854
|
+
else if (type === "IHDR") {
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
if (PNG_ANIMATION_CHUNKS.has(type)) {
|
|
858
|
+
animated = true;
|
|
859
|
+
}
|
|
860
|
+
if (PNG_COMPRESSED_METADATA_CHUNKS.has(type)) {
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
if (type === "iTXt") {
|
|
864
|
+
const compressed = inspectPngInternationalTextCompression(bytes, dataStart, length);
|
|
865
|
+
if (compressed === null || compressed) {
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
if (type === "PLTE") {
|
|
870
|
+
if (sawPalette || sawImageData || length === 0 || length % 3 !== 0 || length > 768) {
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
sawPalette = true;
|
|
874
|
+
}
|
|
875
|
+
else if (type === "IDAT") {
|
|
876
|
+
if (imageDataEnded) {
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
sawImageData = true;
|
|
880
|
+
}
|
|
881
|
+
else if (type === "IEND") {
|
|
882
|
+
if (length !== 0 || next !== bytes.length) {
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
sawEnd = true;
|
|
886
|
+
cursor = next;
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
889
|
+
else if (sawImageData) {
|
|
890
|
+
imageDataEnded = true;
|
|
891
|
+
}
|
|
892
|
+
cursor = next;
|
|
893
|
+
}
|
|
894
|
+
return sawHeader && sawImageData && sawEnd && cursor === bytes.length
|
|
895
|
+
? { format: "png", width, height, animated }
|
|
896
|
+
: null;
|
|
897
|
+
}
|
|
898
|
+
function validPngHeader(header) {
|
|
899
|
+
const bitDepth = header[8];
|
|
900
|
+
const colorType = header[9];
|
|
901
|
+
const allowedDepths = colorType === 0
|
|
902
|
+
? [1, 2, 4, 8, 16]
|
|
903
|
+
: colorType === 2
|
|
904
|
+
? [8, 16]
|
|
905
|
+
: colorType === 3
|
|
906
|
+
? [1, 2, 4, 8]
|
|
907
|
+
: colorType === 4 || colorType === 6
|
|
908
|
+
? [8, 16]
|
|
909
|
+
: [];
|
|
910
|
+
return (bitDepth !== undefined &&
|
|
911
|
+
allowedDepths.includes(bitDepth) &&
|
|
912
|
+
header[10] === 0 &&
|
|
913
|
+
header[11] === 0 &&
|
|
914
|
+
(header[12] === 0 || header[12] === 1));
|
|
915
|
+
}
|
|
916
|
+
function inspectPngInternationalTextCompression(bytes, start, length) {
|
|
917
|
+
const end = start + length;
|
|
918
|
+
const keywordEnd = findByte(bytes, start, end, 0);
|
|
919
|
+
if (keywordEnd === null || keywordEnd === start || keywordEnd - start > 79) {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
const compressionFlag = bytes[keywordEnd + 1];
|
|
923
|
+
const compressionMethod = bytes[keywordEnd + 2];
|
|
924
|
+
if ((compressionFlag !== 0 && compressionFlag !== 1) ||
|
|
925
|
+
compressionMethod !== 0) {
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
const languageEnd = findByte(bytes, keywordEnd + 3, end, 0);
|
|
929
|
+
if (languageEnd === null) {
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
const translatedKeywordEnd = findByte(bytes, languageEnd + 1, end, 0);
|
|
933
|
+
if (translatedKeywordEnd === null) {
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
return compressionFlag === 1;
|
|
937
|
+
}
|
|
938
|
+
function findByte(bytes, start, end, expected) {
|
|
939
|
+
for (let index = start; index < end; index += 1) {
|
|
940
|
+
if (bytes[index] === expected) {
|
|
941
|
+
return index;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return null;
|
|
945
|
+
}
|
|
946
|
+
function inspectGif(bytes) {
|
|
947
|
+
if (bytes.length < 14) {
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
const width = readU16Le(bytes, 6);
|
|
951
|
+
const height = readU16Le(bytes, 8);
|
|
952
|
+
const packed = bytes[10];
|
|
953
|
+
if (width === null || height === null || packed === undefined || width === 0 || height === 0) {
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
let cursor = 13;
|
|
957
|
+
const hasGlobalColorTable = (packed & 0x80) !== 0;
|
|
958
|
+
if (hasGlobalColorTable) {
|
|
959
|
+
const colorTableBytes = 3 * 2 ** ((packed & 0x07) + 1);
|
|
960
|
+
if (colorTableBytes > bytes.length - cursor) {
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
cursor += colorTableBytes;
|
|
964
|
+
}
|
|
965
|
+
let imageDescriptors = 0;
|
|
966
|
+
let animationSignaled = false;
|
|
967
|
+
let pendingGraphicControl = false;
|
|
968
|
+
let sawTrailer = false;
|
|
969
|
+
while (cursor < bytes.length) {
|
|
970
|
+
const block = bytes[cursor];
|
|
971
|
+
cursor += 1;
|
|
972
|
+
if (block === 0x3b) {
|
|
973
|
+
sawTrailer = cursor === bytes.length;
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
if (block === 0x21) {
|
|
977
|
+
const label = bytes[cursor];
|
|
978
|
+
if (label === undefined) {
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
cursor += 1;
|
|
982
|
+
if (label === 0xf9) {
|
|
983
|
+
const control = bytes[cursor + 1];
|
|
984
|
+
if (pendingGraphicControl ||
|
|
985
|
+
bytes[cursor] !== 4 ||
|
|
986
|
+
control === undefined ||
|
|
987
|
+
(control & 0xe0) !== 0 ||
|
|
988
|
+
((control >> 2) & 0x07) > 3 ||
|
|
989
|
+
(control & 0x02) !== 0 ||
|
|
990
|
+
bytes[cursor + 5] !== 0) {
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
pendingGraphicControl = true;
|
|
994
|
+
cursor += 6;
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
if (label === 0xfe) {
|
|
998
|
+
const next = skipGifSubBlocks(bytes, cursor);
|
|
999
|
+
if (next === null) {
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
cursor = next;
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
if (label !== 0xff || bytes[cursor] !== 11) {
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
const application = asciiSlice(bytes, cursor + 1, 11);
|
|
1009
|
+
const next = skipGifSubBlocks(bytes, cursor);
|
|
1010
|
+
if (next === null ||
|
|
1011
|
+
(application !== "NETSCAPE2.0" && application !== "ANIMEXTS1.0")) {
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
animationSignaled = true;
|
|
1015
|
+
cursor = next;
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
if (block !== 0x2c || bytes.length - cursor < 9) {
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
imageDescriptors += 1;
|
|
1022
|
+
const imageLeft = readU16Le(bytes, cursor);
|
|
1023
|
+
const imageTop = readU16Le(bytes, cursor + 2);
|
|
1024
|
+
const imageWidth = readU16Le(bytes, cursor + 4);
|
|
1025
|
+
const imageHeight = readU16Le(bytes, cursor + 6);
|
|
1026
|
+
const imagePacked = bytes[cursor + 8];
|
|
1027
|
+
if (imageLeft === null ||
|
|
1028
|
+
imageTop === null ||
|
|
1029
|
+
imageWidth === null ||
|
|
1030
|
+
imageHeight === null ||
|
|
1031
|
+
imagePacked === undefined ||
|
|
1032
|
+
(imagePacked & 0x18) !== 0 ||
|
|
1033
|
+
imageWidth === 0 ||
|
|
1034
|
+
imageHeight === 0 ||
|
|
1035
|
+
imageLeft + imageWidth > width ||
|
|
1036
|
+
imageTop + imageHeight > height) {
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
cursor += 9;
|
|
1040
|
+
const hasLocalColorTable = (imagePacked & 0x80) !== 0;
|
|
1041
|
+
if (hasLocalColorTable) {
|
|
1042
|
+
const colorTableBytes = 3 * 2 ** ((imagePacked & 0x07) + 1);
|
|
1043
|
+
if (colorTableBytes > bytes.length - cursor) {
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
1046
|
+
cursor += colorTableBytes;
|
|
1047
|
+
}
|
|
1048
|
+
const minimumCodeSize = bytes[cursor];
|
|
1049
|
+
if ((!hasGlobalColorTable && !hasLocalColorTable) ||
|
|
1050
|
+
minimumCodeSize === undefined ||
|
|
1051
|
+
minimumCodeSize < 2 ||
|
|
1052
|
+
minimumCodeSize > 8) {
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
cursor += 1;
|
|
1056
|
+
const next = skipGifSubBlocks(bytes, cursor);
|
|
1057
|
+
if (next === null) {
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
cursor = next;
|
|
1061
|
+
pendingGraphicControl = false;
|
|
1062
|
+
}
|
|
1063
|
+
return sawTrailer && imageDescriptors > 0 && !pendingGraphicControl
|
|
1064
|
+
? {
|
|
1065
|
+
format: "gif",
|
|
1066
|
+
width,
|
|
1067
|
+
height,
|
|
1068
|
+
animated: animationSignaled || imageDescriptors > 1,
|
|
1069
|
+
}
|
|
1070
|
+
: null;
|
|
1071
|
+
}
|
|
1072
|
+
function skipGifSubBlocks(bytes, start) {
|
|
1073
|
+
let cursor = start;
|
|
1074
|
+
while (cursor < bytes.length) {
|
|
1075
|
+
const length = bytes[cursor];
|
|
1076
|
+
if (length === undefined) {
|
|
1077
|
+
return null;
|
|
1078
|
+
}
|
|
1079
|
+
cursor += 1;
|
|
1080
|
+
if (length === 0) {
|
|
1081
|
+
return cursor;
|
|
1082
|
+
}
|
|
1083
|
+
if (length > bytes.length - cursor) {
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
cursor += length;
|
|
1087
|
+
}
|
|
1088
|
+
return null;
|
|
1089
|
+
}
|
|
1090
|
+
function inspectJpeg(bytes) {
|
|
1091
|
+
let cursor = 2;
|
|
1092
|
+
let width = 0;
|
|
1093
|
+
let height = 0;
|
|
1094
|
+
let sawFrame = false;
|
|
1095
|
+
let sawScan = false;
|
|
1096
|
+
while (cursor < bytes.length) {
|
|
1097
|
+
if (bytes[cursor] !== 0xff) {
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1100
|
+
const markerStart = cursor;
|
|
1101
|
+
while (bytes[cursor] === 0xff) {
|
|
1102
|
+
cursor += 1;
|
|
1103
|
+
}
|
|
1104
|
+
const marker = bytes[cursor];
|
|
1105
|
+
if (marker === undefined || marker === 0x00 || marker === 0xff) {
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
cursor += 1;
|
|
1109
|
+
if (marker === 0xd9) {
|
|
1110
|
+
return sawFrame && sawScan && height > 0 && cursor === bytes.length
|
|
1111
|
+
? { format: "jpeg", width, height, animated: false }
|
|
1112
|
+
: null;
|
|
1113
|
+
}
|
|
1114
|
+
if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
const segmentLength = readU16Be(bytes, cursor);
|
|
1118
|
+
if (segmentLength === null || segmentLength < 2 || segmentLength > bytes.length - cursor) {
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
const segmentStart = cursor + 2;
|
|
1122
|
+
const segmentEnd = cursor + segmentLength;
|
|
1123
|
+
if (isJpegStartOfFrame(marker)) {
|
|
1124
|
+
if (sawFrame || segmentLength < 8) {
|
|
1125
|
+
return null;
|
|
1126
|
+
}
|
|
1127
|
+
const parsedHeight = readU16Be(bytes, segmentStart + 1);
|
|
1128
|
+
const parsedWidth = readU16Be(bytes, segmentStart + 3);
|
|
1129
|
+
if (parsedWidth === null ||
|
|
1130
|
+
parsedHeight === null ||
|
|
1131
|
+
parsedWidth === 0) {
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
width = parsedWidth;
|
|
1135
|
+
height = parsedHeight;
|
|
1136
|
+
sawFrame = true;
|
|
1137
|
+
}
|
|
1138
|
+
else if (marker === 0xdc) {
|
|
1139
|
+
const definedHeight = readU16Be(bytes, segmentStart);
|
|
1140
|
+
if (segmentLength !== 4 ||
|
|
1141
|
+
!sawFrame ||
|
|
1142
|
+
!sawScan ||
|
|
1143
|
+
height !== 0 ||
|
|
1144
|
+
definedHeight === null ||
|
|
1145
|
+
definedHeight === 0) {
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
height = definedHeight;
|
|
1149
|
+
}
|
|
1150
|
+
cursor = segmentEnd;
|
|
1151
|
+
if (marker !== 0xda) {
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
sawScan = true;
|
|
1155
|
+
while (cursor < bytes.length) {
|
|
1156
|
+
const markerPrefix = bytes.indexOf(0xff, cursor);
|
|
1157
|
+
if (markerPrefix < 0 || markerPrefix + 1 >= bytes.length) {
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
const next = bytes[markerPrefix + 1];
|
|
1161
|
+
if (next === 0x00 || (next !== undefined && next >= 0xd0 && next <= 0xd7)) {
|
|
1162
|
+
cursor = markerPrefix + 2;
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
cursor = markerPrefix;
|
|
1166
|
+
break;
|
|
1167
|
+
}
|
|
1168
|
+
if (cursor === markerStart) {
|
|
1169
|
+
return null;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return null;
|
|
1173
|
+
}
|
|
1174
|
+
function isJpegStartOfFrame(marker) {
|
|
1175
|
+
return (marker >= 0xc0 &&
|
|
1176
|
+
marker <= 0xcf &&
|
|
1177
|
+
marker !== 0xc4 &&
|
|
1178
|
+
marker !== 0xc8 &&
|
|
1179
|
+
marker !== 0xcc);
|
|
1180
|
+
}
|
|
1181
|
+
function inspectWebp(bytes) {
|
|
1182
|
+
if (bytes.length < 20) {
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
const riffLength = readU32Le(bytes, 4);
|
|
1186
|
+
if (riffLength === null || riffLength + 8 !== bytes.length) {
|
|
1187
|
+
return null;
|
|
1188
|
+
}
|
|
1189
|
+
let cursor = 12;
|
|
1190
|
+
let width = 0;
|
|
1191
|
+
let height = 0;
|
|
1192
|
+
let sawDimensions = false;
|
|
1193
|
+
let sawExtendedHeader = false;
|
|
1194
|
+
let sawImageData = false;
|
|
1195
|
+
let sawAlpha = false;
|
|
1196
|
+
let animated = false;
|
|
1197
|
+
while (cursor < bytes.length) {
|
|
1198
|
+
if (bytes.length - cursor < 8) {
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
const chunkType = asciiSlice(bytes, cursor, 4);
|
|
1202
|
+
const chunkLength = readU32Le(bytes, cursor + 4);
|
|
1203
|
+
if (chunkType === null || chunkLength === null || chunkLength > bytes.length - cursor - 8) {
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
const dataStart = cursor + 8;
|
|
1207
|
+
const paddedLength = chunkLength + (chunkLength & 1);
|
|
1208
|
+
if (paddedLength > bytes.length - dataStart) {
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
if ((chunkLength & 1) !== 0 && bytes[dataStart + chunkLength] !== 0) {
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
if (cursor === 12 &&
|
|
1215
|
+
chunkType !== "VP8X" &&
|
|
1216
|
+
chunkType !== "VP8 " &&
|
|
1217
|
+
chunkType !== "VP8L") {
|
|
1218
|
+
return null;
|
|
1219
|
+
}
|
|
1220
|
+
if (cursor > 12 && !sawExtendedHeader) {
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
if (chunkType === "VP8X") {
|
|
1224
|
+
if (cursor !== 12 || sawDimensions || chunkLength !== 10) {
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
const flags = bytes[dataStart];
|
|
1228
|
+
const parsedWidth = readU24Le(bytes, dataStart + 4);
|
|
1229
|
+
const parsedHeight = readU24Le(bytes, dataStart + 7);
|
|
1230
|
+
if (flags === undefined ||
|
|
1231
|
+
(flags & 0xc1) !== 0 ||
|
|
1232
|
+
bytes[dataStart + 1] !== 0 ||
|
|
1233
|
+
bytes[dataStart + 2] !== 0 ||
|
|
1234
|
+
bytes[dataStart + 3] !== 0 ||
|
|
1235
|
+
parsedWidth === null ||
|
|
1236
|
+
parsedHeight === null ||
|
|
1237
|
+
(parsedWidth + 1) * (parsedHeight + 1) > 0xffffffff) {
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
1240
|
+
width = parsedWidth + 1;
|
|
1241
|
+
height = parsedHeight + 1;
|
|
1242
|
+
animated = (flags & 0x02) !== 0;
|
|
1243
|
+
sawDimensions = true;
|
|
1244
|
+
sawExtendedHeader = true;
|
|
1245
|
+
}
|
|
1246
|
+
else if (chunkType === "VP8 ") {
|
|
1247
|
+
if (chunkLength < 10 || sawImageData) {
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
const frameTag = bytes[dataStart];
|
|
1251
|
+
if (frameTag === undefined ||
|
|
1252
|
+
(frameTag & 0x01) !== 0 ||
|
|
1253
|
+
!hasBytes(bytes, dataStart + 3, [0x9d, 0x01, 0x2a])) {
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
const parsedWidth = readU16Le(bytes, dataStart + 6);
|
|
1257
|
+
const parsedHeight = readU16Le(bytes, dataStart + 8);
|
|
1258
|
+
if (parsedWidth === null || parsedHeight === null) {
|
|
1259
|
+
return null;
|
|
1260
|
+
}
|
|
1261
|
+
const frameWidth = parsedWidth & 0x3fff;
|
|
1262
|
+
const frameHeight = parsedHeight & 0x3fff;
|
|
1263
|
+
if (frameWidth === 0 || frameHeight === 0) {
|
|
1264
|
+
return null;
|
|
1265
|
+
}
|
|
1266
|
+
if (sawExtendedHeader) {
|
|
1267
|
+
if (frameWidth !== width || frameHeight !== height) {
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
else if (!sawDimensions) {
|
|
1272
|
+
width = frameWidth;
|
|
1273
|
+
height = frameHeight;
|
|
1274
|
+
sawDimensions = true;
|
|
1275
|
+
}
|
|
1276
|
+
sawImageData = true;
|
|
1277
|
+
}
|
|
1278
|
+
else if (chunkType === "VP8L") {
|
|
1279
|
+
if (chunkLength < 5 || sawImageData || sawAlpha || bytes[dataStart] !== 0x2f) {
|
|
1280
|
+
return null;
|
|
1281
|
+
}
|
|
1282
|
+
const b1 = bytes[dataStart + 1];
|
|
1283
|
+
const b2 = bytes[dataStart + 2];
|
|
1284
|
+
const b3 = bytes[dataStart + 3];
|
|
1285
|
+
const b4 = bytes[dataStart + 4];
|
|
1286
|
+
if (b1 === undefined ||
|
|
1287
|
+
b2 === undefined ||
|
|
1288
|
+
b3 === undefined ||
|
|
1289
|
+
b4 === undefined ||
|
|
1290
|
+
(b4 & 0xe0) !== 0) {
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
const frameWidth = 1 + b1 + ((b2 & 0x3f) << 8);
|
|
1294
|
+
const frameHeight = 1 + (b2 >> 6) + (b3 << 2) + ((b4 & 0x0f) << 10);
|
|
1295
|
+
if (sawExtendedHeader) {
|
|
1296
|
+
if (frameWidth !== width || frameHeight !== height) {
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
else if (!sawDimensions) {
|
|
1301
|
+
width = frameWidth;
|
|
1302
|
+
height = frameHeight;
|
|
1303
|
+
sawDimensions = true;
|
|
1304
|
+
}
|
|
1305
|
+
sawImageData = true;
|
|
1306
|
+
}
|
|
1307
|
+
else if (chunkType === "ANIM" || chunkType === "ANMF") {
|
|
1308
|
+
animated = true;
|
|
1309
|
+
}
|
|
1310
|
+
else if (chunkType === "ALPH") {
|
|
1311
|
+
if (!sawExtendedHeader || sawAlpha || sawImageData || chunkLength === 0) {
|
|
1312
|
+
return null;
|
|
1313
|
+
}
|
|
1314
|
+
sawAlpha = true;
|
|
1315
|
+
}
|
|
1316
|
+
else if (chunkType !== "ICCP" &&
|
|
1317
|
+
chunkType !== "EXIF" &&
|
|
1318
|
+
chunkType !== "XMP ") {
|
|
1319
|
+
return null;
|
|
1320
|
+
}
|
|
1321
|
+
cursor = dataStart + paddedLength;
|
|
1322
|
+
}
|
|
1323
|
+
return cursor === bytes.length && sawDimensions && (sawImageData || animated)
|
|
1324
|
+
? { format: "webp", width, height, animated }
|
|
1325
|
+
: null;
|
|
1326
|
+
}
|
|
1327
|
+
function readU16Be(bytes, offset) {
|
|
1328
|
+
const a = bytes[offset];
|
|
1329
|
+
const b = bytes[offset + 1];
|
|
1330
|
+
return a === undefined || b === undefined ? null : a * 0x100 + b;
|
|
1331
|
+
}
|
|
1332
|
+
function readU16Le(bytes, offset) {
|
|
1333
|
+
const a = bytes[offset];
|
|
1334
|
+
const b = bytes[offset + 1];
|
|
1335
|
+
return a === undefined || b === undefined ? null : a + b * 0x100;
|
|
1336
|
+
}
|
|
1337
|
+
function readU24Le(bytes, offset) {
|
|
1338
|
+
const a = bytes[offset];
|
|
1339
|
+
const b = bytes[offset + 1];
|
|
1340
|
+
const c = bytes[offset + 2];
|
|
1341
|
+
return a === undefined || b === undefined || c === undefined
|
|
1342
|
+
? null
|
|
1343
|
+
: a + b * 0x100 + c * 0x10000;
|
|
1344
|
+
}
|
|
1345
|
+
function readU32Be(bytes, offset) {
|
|
1346
|
+
const a = bytes[offset];
|
|
1347
|
+
const b = bytes[offset + 1];
|
|
1348
|
+
const c = bytes[offset + 2];
|
|
1349
|
+
const d = bytes[offset + 3];
|
|
1350
|
+
return a === undefined || b === undefined || c === undefined || d === undefined
|
|
1351
|
+
? null
|
|
1352
|
+
: a * 0x1000000 + b * 0x10000 + c * 0x100 + d;
|
|
1353
|
+
}
|
|
1354
|
+
function readU32Le(bytes, offset) {
|
|
1355
|
+
const a = bytes[offset];
|
|
1356
|
+
const b = bytes[offset + 1];
|
|
1357
|
+
const c = bytes[offset + 2];
|
|
1358
|
+
const d = bytes[offset + 3];
|
|
1359
|
+
return a === undefined || b === undefined || c === undefined || d === undefined
|
|
1360
|
+
? null
|
|
1361
|
+
: a + b * 0x100 + c * 0x10000 + d * 0x1000000;
|
|
1362
|
+
}
|
|
1363
|
+
function hasBytes(bytes, offset, expected) {
|
|
1364
|
+
return expected.every((value, index) => bytes[offset + index] === value);
|
|
1365
|
+
}
|
|
1366
|
+
function asciiAt(bytes, offset, expected) {
|
|
1367
|
+
return asciiSlice(bytes, offset, expected.length) === expected;
|
|
1368
|
+
}
|
|
1369
|
+
function asciiSlice(bytes, offset, length) {
|
|
1370
|
+
if (offset < 0 || length < 0 || length > bytes.length - offset) {
|
|
1371
|
+
return null;
|
|
1372
|
+
}
|
|
1373
|
+
let value = "";
|
|
1374
|
+
for (let index = 0; index < length; index += 1) {
|
|
1375
|
+
const byte = bytes[offset + index];
|
|
1376
|
+
if (byte === undefined || byte > 0x7f) {
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1379
|
+
value += String.fromCharCode(byte);
|
|
1380
|
+
}
|
|
1381
|
+
return value;
|
|
1382
|
+
}
|
|
1383
|
+
function stripCssComments(css) {
|
|
1384
|
+
let output = "";
|
|
1385
|
+
let cursor = 0;
|
|
1386
|
+
while (cursor < css.length) {
|
|
1387
|
+
const commentStart = css.indexOf("/*", cursor);
|
|
1388
|
+
if (commentStart < 0) {
|
|
1389
|
+
output += css.slice(cursor);
|
|
1390
|
+
break;
|
|
1391
|
+
}
|
|
1392
|
+
output += css.slice(cursor, commentStart);
|
|
1393
|
+
const commentEnd = css.indexOf("*/", commentStart + 2);
|
|
1394
|
+
if (commentEnd < 0) {
|
|
1395
|
+
break;
|
|
1396
|
+
}
|
|
1397
|
+
cursor = commentEnd + 2;
|
|
1398
|
+
}
|
|
1399
|
+
return output;
|
|
1400
|
+
}
|
|
1401
|
+
function containsCssFunction(css, name) {
|
|
1402
|
+
let cursor = 0;
|
|
1403
|
+
while (cursor < css.length) {
|
|
1404
|
+
const index = css.indexOf(name, cursor);
|
|
1405
|
+
if (index < 0) {
|
|
1406
|
+
return false;
|
|
1407
|
+
}
|
|
1408
|
+
cursor = index + name.length;
|
|
1409
|
+
const before = index === 0 ? "" : css[index - 1] ?? "";
|
|
1410
|
+
const after = css[cursor] ?? "";
|
|
1411
|
+
if (!isCssIdentifierChar(before) &&
|
|
1412
|
+
!isCssIdentifierChar(after) &&
|
|
1413
|
+
css[skipWhitespace(css, cursor)] === "(") {
|
|
1414
|
+
return true;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return false;
|
|
1418
|
+
}
|
|
1419
|
+
function containsShadowScopingSelector(css) {
|
|
1420
|
+
return css.includes(":host") || css.includes("::slotted");
|
|
1421
|
+
}
|
|
1422
|
+
function containsViewportEscapingCssDeclaration(css) {
|
|
1423
|
+
return cssDeclarationBlocks(css).some((block) => {
|
|
1424
|
+
if (/(?:^|[;{\s])position\s*:\s*(?:fixed|sticky)\b/.test(block)) {
|
|
1425
|
+
return true;
|
|
1426
|
+
}
|
|
1427
|
+
return (/(?:^|[;{\s])position\s*:\s*absolute\b/.test(block) &&
|
|
1428
|
+
/(?:^|[;{\s])(?:inset(?:-(?:block|inline)(?:-(?:start|end))?)?|top|right|bottom|left)\s*:/.test(block));
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
function cssDeclarationBlocks(css) {
|
|
1432
|
+
const blocks = [];
|
|
1433
|
+
let cursor = 0;
|
|
1434
|
+
while (cursor < css.length) {
|
|
1435
|
+
const open = css.indexOf("{", cursor);
|
|
1436
|
+
if (open < 0) {
|
|
1437
|
+
break;
|
|
1438
|
+
}
|
|
1439
|
+
const close = css.indexOf("}", open + 1);
|
|
1440
|
+
if (close < 0) {
|
|
1441
|
+
break;
|
|
1442
|
+
}
|
|
1443
|
+
blocks.push(css.slice(open + 1, close));
|
|
1444
|
+
cursor = close + 1;
|
|
1445
|
+
}
|
|
1446
|
+
return blocks.length > 0 ? blocks : [css];
|
|
1447
|
+
}
|
|
1448
|
+
function decodeCssEscapes(value) {
|
|
1449
|
+
let output = "";
|
|
1450
|
+
for (let cursor = 0; cursor < value.length; cursor += 1) {
|
|
1451
|
+
const char = value[cursor];
|
|
1452
|
+
if (char !== "\\") {
|
|
1453
|
+
output += char;
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
const next = value[cursor + 1];
|
|
1457
|
+
if (next === undefined) {
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
if (next === "\n" || next === "\r" || next === "\f") {
|
|
1461
|
+
cursor += next === "\r" && value[cursor + 2] === "\n" ? 2 : 1;
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
let hex = "";
|
|
1465
|
+
let hexCursor = cursor + 1;
|
|
1466
|
+
while (hexCursor < value.length && hex.length < 6 && isHexDigit(value[hexCursor] ?? "")) {
|
|
1467
|
+
hex += value[hexCursor];
|
|
1468
|
+
hexCursor += 1;
|
|
1469
|
+
}
|
|
1470
|
+
if (hex.length > 0) {
|
|
1471
|
+
output += codePointToString(Number.parseInt(hex, 16), "");
|
|
1472
|
+
if (value[hexCursor] === "\r" && value[hexCursor + 1] === "\n") {
|
|
1473
|
+
hexCursor += 2;
|
|
1474
|
+
}
|
|
1475
|
+
else if (isWhitespace(value[hexCursor] ?? "")) {
|
|
1476
|
+
hexCursor += 1;
|
|
1477
|
+
}
|
|
1478
|
+
cursor = hexCursor - 1;
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
output += next;
|
|
1482
|
+
cursor += 1;
|
|
1483
|
+
}
|
|
1484
|
+
return output;
|
|
1485
|
+
}
|
|
1486
|
+
function decodeXmlEntities(value) {
|
|
1487
|
+
let output = "";
|
|
1488
|
+
let cursor = 0;
|
|
1489
|
+
while (cursor < value.length) {
|
|
1490
|
+
const ampersand = value.indexOf("&", cursor);
|
|
1491
|
+
if (ampersand < 0) {
|
|
1492
|
+
output += value.slice(cursor);
|
|
1493
|
+
break;
|
|
1494
|
+
}
|
|
1495
|
+
output += value.slice(cursor, ampersand);
|
|
1496
|
+
const next = value[ampersand + 1] ?? "";
|
|
1497
|
+
if (next === "#") {
|
|
1498
|
+
const parsed = decodeNumericCharacterReference(value, ampersand);
|
|
1499
|
+
if (parsed) {
|
|
1500
|
+
output += parsed.value;
|
|
1501
|
+
cursor = parsed.end;
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
const parsed = decodeNamedCharacterReference(value, ampersand);
|
|
1506
|
+
if (parsed) {
|
|
1507
|
+
output += parsed.value;
|
|
1508
|
+
cursor = parsed.end;
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
output += "&";
|
|
1512
|
+
cursor = ampersand + 1;
|
|
1513
|
+
}
|
|
1514
|
+
return output;
|
|
1515
|
+
}
|
|
1516
|
+
function decodeNumericCharacterReference(value, ampersand) {
|
|
1517
|
+
let cursor = ampersand + 2;
|
|
1518
|
+
let radix = 10;
|
|
1519
|
+
if (value[cursor]?.toLowerCase() === "x") {
|
|
1520
|
+
radix = 16;
|
|
1521
|
+
cursor += 1;
|
|
1522
|
+
}
|
|
1523
|
+
const digitsStart = cursor;
|
|
1524
|
+
while (cursor < value.length &&
|
|
1525
|
+
(radix === 16 ? isHexDigit(value[cursor] ?? "") : isAsciiDigit(value[cursor] ?? ""))) {
|
|
1526
|
+
cursor += 1;
|
|
1527
|
+
}
|
|
1528
|
+
if (cursor === digitsStart) {
|
|
1529
|
+
return null;
|
|
1530
|
+
}
|
|
1531
|
+
const body = value.slice(digitsStart, cursor);
|
|
1532
|
+
const end = value[cursor] === ";" ? cursor + 1 : cursor;
|
|
1533
|
+
return {
|
|
1534
|
+
value: codePointToString(Number.parseInt(body, radix), value.slice(ampersand, end)),
|
|
1535
|
+
end,
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
const NAMED_CHARACTER_REFERENCES = new Map([
|
|
1539
|
+
["amp", "&"],
|
|
1540
|
+
["lt", "<"],
|
|
1541
|
+
["gt", ">"],
|
|
1542
|
+
["quot", '"'],
|
|
1543
|
+
["apos", "'"],
|
|
1544
|
+
["colon", ":"],
|
|
1545
|
+
["sol", "/"],
|
|
1546
|
+
["lpar", "("],
|
|
1547
|
+
["rpar", ")"],
|
|
1548
|
+
["newline", "\n"],
|
|
1549
|
+
["tab", "\t"],
|
|
1550
|
+
]);
|
|
1551
|
+
function decodeNamedCharacterReference(value, ampersand) {
|
|
1552
|
+
let cursor = ampersand + 1;
|
|
1553
|
+
while (cursor < value.length && isAsciiAlphanumeric(value[cursor] ?? "")) {
|
|
1554
|
+
cursor += 1;
|
|
1555
|
+
}
|
|
1556
|
+
if (value[cursor] !== ";") {
|
|
1557
|
+
return null;
|
|
1558
|
+
}
|
|
1559
|
+
const replacement = NAMED_CHARACTER_REFERENCES.get(value.slice(ampersand + 1, cursor).toLowerCase());
|
|
1560
|
+
if (replacement === undefined) {
|
|
1561
|
+
return null;
|
|
1562
|
+
}
|
|
1563
|
+
return { value: replacement, end: cursor + 1 };
|
|
1564
|
+
}
|
|
1565
|
+
function containsCharacterReference(value) {
|
|
1566
|
+
return /&(?:#x[0-9a-f]+|#\d+|[a-z][a-z0-9]+);?/i.test(value);
|
|
1567
|
+
}
|
|
1568
|
+
function codePointToString(codePoint, fallback) {
|
|
1569
|
+
if (!Number.isFinite(codePoint)) {
|
|
1570
|
+
return fallback;
|
|
1571
|
+
}
|
|
1572
|
+
try {
|
|
1573
|
+
return String.fromCodePoint(codePoint);
|
|
1574
|
+
}
|
|
1575
|
+
catch {
|
|
1576
|
+
return fallback;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
function isAsciiDigit(value) {
|
|
1580
|
+
return value >= "0" && value <= "9";
|
|
1581
|
+
}
|
|
1582
|
+
function isAsciiAlphanumeric(value) {
|
|
1583
|
+
return ((value >= "0" && value <= "9") ||
|
|
1584
|
+
(value >= "A" && value <= "Z") ||
|
|
1585
|
+
(value >= "a" && value <= "z"));
|
|
1586
|
+
}
|
|
1587
|
+
function removeAsciiWhitespaceAndControl(value) {
|
|
1588
|
+
let output = "";
|
|
1589
|
+
for (const char of value) {
|
|
1590
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
1591
|
+
if (codePoint > 0x20) {
|
|
1592
|
+
output += char;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
return output;
|
|
1596
|
+
}
|
|
1597
|
+
function utf8LengthAtMost(value, limit) {
|
|
1598
|
+
let bytes = 0;
|
|
1599
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1600
|
+
const codeUnit = value.charCodeAt(index);
|
|
1601
|
+
if (codeUnit <= 0x7f) {
|
|
1602
|
+
bytes += 1;
|
|
1603
|
+
}
|
|
1604
|
+
else if (codeUnit <= 0x7ff) {
|
|
1605
|
+
bytes += 2;
|
|
1606
|
+
}
|
|
1607
|
+
else if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
|
|
1608
|
+
const next = value.charCodeAt(index + 1);
|
|
1609
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
1610
|
+
bytes += 4;
|
|
1611
|
+
index += 1;
|
|
1612
|
+
}
|
|
1613
|
+
else {
|
|
1614
|
+
bytes += 3;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
else {
|
|
1618
|
+
bytes += 3;
|
|
1619
|
+
}
|
|
1620
|
+
if (bytes > limit) {
|
|
1621
|
+
return false;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
return true;
|
|
1625
|
+
}
|
|
1626
|
+
function localName(name) {
|
|
1627
|
+
const lower = name.toLowerCase();
|
|
1628
|
+
const separator = lower.lastIndexOf(":");
|
|
1629
|
+
return separator >= 0 ? lower.slice(separator + 1) : lower;
|
|
1630
|
+
}
|
|
1631
|
+
function findClosingStyle(source, start) {
|
|
1632
|
+
let index = source.indexOf("<", start);
|
|
1633
|
+
while (index >= 0) {
|
|
1634
|
+
if (matchesAsciiCaseInsensitive(source, index, "</style")) {
|
|
1635
|
+
return index;
|
|
1636
|
+
}
|
|
1637
|
+
index = source.indexOf("<", index + 1);
|
|
1638
|
+
}
|
|
1639
|
+
return null;
|
|
1640
|
+
}
|
|
1641
|
+
function matchesAsciiCaseInsensitive(source, start, lowerAscii) {
|
|
1642
|
+
if (start + lowerAscii.length > source.length) {
|
|
1643
|
+
return false;
|
|
1644
|
+
}
|
|
1645
|
+
for (let offset = 0; offset < lowerAscii.length; offset += 1) {
|
|
1646
|
+
const code = source.charCodeAt(start + offset);
|
|
1647
|
+
const normalized = code >= 0x41 && code <= 0x5a ? code + 0x20 : code;
|
|
1648
|
+
if (normalized !== lowerAscii.charCodeAt(offset)) {
|
|
1649
|
+
return false;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
return true;
|
|
1653
|
+
}
|
|
1654
|
+
function skipWhitespace(source, cursor) {
|
|
1655
|
+
while (cursor < source.length && isWhitespace(source[cursor] ?? "")) {
|
|
1656
|
+
cursor += 1;
|
|
1657
|
+
}
|
|
1658
|
+
return cursor;
|
|
1659
|
+
}
|
|
1660
|
+
function isWhitespace(char) {
|
|
1661
|
+
return char === " " || char === "\n" || char === "\r" || char === "\t" || char === "\f";
|
|
1662
|
+
}
|
|
1663
|
+
function isOnlyWhitespace(value) {
|
|
1664
|
+
for (const char of value) {
|
|
1665
|
+
if (!isWhitespace(char)) {
|
|
1666
|
+
return false;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
return true;
|
|
1670
|
+
}
|
|
1671
|
+
function isHexDigit(char) {
|
|
1672
|
+
return ((char >= "0" && char <= "9") ||
|
|
1673
|
+
(char >= "a" && char <= "f") ||
|
|
1674
|
+
(char >= "A" && char <= "F"));
|
|
1675
|
+
}
|
|
1676
|
+
function isCssIdentifierChar(char) {
|
|
1677
|
+
return ((char >= "a" && char <= "z") ||
|
|
1678
|
+
(char >= "A" && char <= "Z") ||
|
|
1679
|
+
(char >= "0" && char <= "9") ||
|
|
1680
|
+
char === "_" ||
|
|
1681
|
+
char === "-");
|
|
1682
|
+
}
|
|
1683
|
+
function isNameTerminator(char) {
|
|
1684
|
+
return isWhitespace(char) || char === ">" || char === "/";
|
|
1685
|
+
}
|
|
1686
|
+
function isAttributeNameTerminator(char) {
|
|
1687
|
+
return isWhitespace(char) || char === "=" || char === ">" || char === "/";
|
|
1688
|
+
}
|
|
1689
|
+
function isUnquotedValueTerminator(char) {
|
|
1690
|
+
return isWhitespace(char) || char === ">";
|
|
1691
|
+
}
|
|
1692
|
+
//# sourceMappingURL=svg-safety-policy.js.map
|