@galacean/effects-plugin-dom-content 2.9.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/dist/index.mjs ADDED
@@ -0,0 +1,1704 @@
1
+ /*!
2
+ * Name: @galacean/effects-plugin-dom-content
3
+ * Description: Galacean Effects plugin for rendering HTML/CSS DOM content as texture
4
+ * Author: Ant Group CO., Ltd.
5
+ * Contributors: 陈茉
6
+ * Version: v2.9.0
7
+ */
8
+
9
+ import * as EFFECTS from '@galacean/effects';
10
+ import { Plugin, logger, loadImage, loadBlob, effectsClass, math, MaskableGraphic, Texture, glContext, registerPlugin } from '@galacean/effects';
11
+
12
+ function _set_prototype_of(o, p) {
13
+ _set_prototype_of = Object.setPrototypeOf || function setPrototypeOf(o, p) {
14
+ o.__proto__ = p;
15
+ return o;
16
+ };
17
+ return _set_prototype_of(o, p);
18
+ }
19
+
20
+ function _inherits(subClass, superClass) {
21
+ if (typeof superClass !== "function" && superClass !== null) {
22
+ throw new TypeError("Super expression must either be null or a function");
23
+ }
24
+ subClass.prototype = Object.create(superClass && superClass.prototype, {
25
+ constructor: {
26
+ value: subClass,
27
+ writable: true,
28
+ configurable: true
29
+ }
30
+ });
31
+ if (superClass) _set_prototype_of(subClass, superClass);
32
+ }
33
+
34
+ function _is_native_reflect_construct() {
35
+ // Since Reflect.construct can't be properly polyfilled, some
36
+ // implementations (e.g. core-js@2) don't set the correct internal slots.
37
+ // Those polyfills don't allow us to subclass built-ins, so we need to
38
+ // use our fallback implementation.
39
+ try {
40
+ // If the internal slots aren't set, this throws an error similar to
41
+ // TypeError: this is not a Boolean object.
42
+ var result = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function() {}));
43
+ } catch (_) {}
44
+ return (_is_native_reflect_construct = function _is_native_reflect_construct() {
45
+ return !!result;
46
+ })();
47
+ }
48
+
49
+ function _construct(Parent, args, Class) {
50
+ if (_is_native_reflect_construct()) _construct = Reflect.construct;
51
+ else {
52
+ _construct = function construct(Parent, args, Class) {
53
+ var a = [
54
+ null
55
+ ];
56
+ a.push.apply(a, args);
57
+ var Constructor = Function.bind.apply(Parent, a);
58
+ var instance = new Constructor();
59
+ if (Class) _set_prototype_of(instance, Class.prototype);
60
+ return instance;
61
+ };
62
+ }
63
+ return _construct.apply(null, arguments);
64
+ }
65
+
66
+ function _get_prototype_of(o) {
67
+ _get_prototype_of = Object.setPrototypeOf ? Object.getPrototypeOf : function getPrototypeOf(o) {
68
+ return o.__proto__ || Object.getPrototypeOf(o);
69
+ };
70
+ return _get_prototype_of(o);
71
+ }
72
+
73
+ function _is_native_function(fn) {
74
+ return Function.toString.call(fn).indexOf("[native code]") !== -1;
75
+ }
76
+
77
+ function _wrap_native_super(Class) {
78
+ var _cache = typeof Map === "function" ? new Map() : undefined;
79
+ _wrap_native_super = function _wrap_native_super(Class) {
80
+ if (Class === null || !_is_native_function(Class)) return Class;
81
+ if (typeof Class !== "function") throw new TypeError("Super expression must either be null or a function");
82
+ if (typeof _cache !== "undefined") {
83
+ if (_cache.has(Class)) return _cache.get(Class);
84
+ _cache.set(Class, Wrapper);
85
+ }
86
+ function Wrapper() {
87
+ return _construct(Class, arguments, _get_prototype_of(this).constructor);
88
+ }
89
+ Wrapper.prototype = Object.create(Class.prototype, {
90
+ constructor: {
91
+ value: Wrapper,
92
+ enumerable: false,
93
+ writable: true,
94
+ configurable: true
95
+ }
96
+ });
97
+ return _set_prototype_of(Wrapper, Class);
98
+ };
99
+ return _wrap_native_super(Class);
100
+ }
101
+
102
+ /**
103
+ * DOM Content 插件 Loader
104
+ * 当前为空壳实现,组件在运行时由开发手动挂载,无需 JSON 数据解析
105
+ */ var DomContentLoader = /*#__PURE__*/ function(Plugin) {
106
+ _inherits(DomContentLoader, Plugin);
107
+ function DomContentLoader() {
108
+ var _this;
109
+ _this = Plugin.apply(this, arguments) || this;
110
+ _this.name = "dom-content";
111
+ return _this;
112
+ }
113
+ return DomContentLoader;
114
+ }(_wrap_native_super(Plugin));
115
+
116
+ function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
117
+ try {
118
+ var info = gen[key](arg);
119
+ var value = info.value;
120
+ } catch (error) {
121
+ reject(error);
122
+ return;
123
+ }
124
+ if (info.done) resolve(value);
125
+ else Promise.resolve(value).then(_next, _throw);
126
+ }
127
+ function _async_to_generator(fn) {
128
+ return function() {
129
+ var self = this, args = arguments;
130
+ return new Promise(function(resolve, reject) {
131
+ var gen = fn.apply(self, args);
132
+ function _next(value) {
133
+ asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
134
+ }
135
+ function _throw(err) {
136
+ asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
137
+ }
138
+ _next(undefined);
139
+ });
140
+ };
141
+ }
142
+
143
+ function _array_like_to_array(arr, len) {
144
+ if (len == null || len > arr.length) len = arr.length;
145
+ for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
146
+ return arr2;
147
+ }
148
+
149
+ function _unsupported_iterable_to_array(o, minLen) {
150
+ if (!o) return;
151
+ if (typeof o === "string") return _array_like_to_array(o, minLen);
152
+ var n = Object.prototype.toString.call(o).slice(8, -1);
153
+ if (n === "Object" && o.constructor) n = o.constructor.name;
154
+ if (n === "Map" || n === "Set") return Array.from(n);
155
+ if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array(o, minLen);
156
+ }
157
+
158
+ function _create_for_of_iterator_helper_loose(o, allowArrayLike) {
159
+ var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"];
160
+ if (it) return (it = it.call(o)).next.bind(it);
161
+ // Fallback for engines without symbol support
162
+ if (Array.isArray(o) || (it = _unsupported_iterable_to_array(o)) || allowArrayLike && o && typeof o.length === "number") {
163
+ if (it) o = it;
164
+ var i = 0;
165
+ return function() {
166
+ if (i >= o.length) return {
167
+ done: true
168
+ };
169
+ return {
170
+ done: false,
171
+ value: o[i++]
172
+ };
173
+ };
174
+ }
175
+ throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
176
+ }
177
+
178
+ function __decorate(decorators, target, key, desc) {
179
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
180
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
181
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
182
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
183
+ }
184
+ function __generator(thisArg, body) {
185
+ var _ = {
186
+ label: 0,
187
+ sent: function sent() {
188
+ if (t[0] & 1) throw t[1];
189
+ return t[1];
190
+ },
191
+ trys: [],
192
+ ops: []
193
+ }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
194
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() {
195
+ return this;
196
+ }), g;
197
+ function verb(n) {
198
+ return function(v) {
199
+ return step([
200
+ n,
201
+ v
202
+ ]);
203
+ };
204
+ }
205
+ function step(op) {
206
+ if (f) throw new TypeError("Generator is already executing.");
207
+ while(g && (g = 0, op[0] && (_ = 0)), _)try {
208
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
209
+ if (y = 0, t) op = [
210
+ op[0] & 2,
211
+ t.value
212
+ ];
213
+ switch(op[0]){
214
+ case 0:
215
+ case 1:
216
+ t = op;
217
+ break;
218
+ case 4:
219
+ _.label++;
220
+ return {
221
+ value: op[1],
222
+ done: false
223
+ };
224
+ case 5:
225
+ _.label++;
226
+ y = op[1];
227
+ op = [
228
+ 0
229
+ ];
230
+ continue;
231
+ case 7:
232
+ op = _.ops.pop();
233
+ _.trys.pop();
234
+ continue;
235
+ default:
236
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {
237
+ _ = 0;
238
+ continue;
239
+ }
240
+ if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) {
241
+ _.label = op[1];
242
+ break;
243
+ }
244
+ if (op[0] === 6 && _.label < t[1]) {
245
+ _.label = t[1];
246
+ t = op;
247
+ break;
248
+ }
249
+ if (t && _.label < t[2]) {
250
+ _.label = t[2];
251
+ _.ops.push(op);
252
+ break;
253
+ }
254
+ if (t[2]) _.ops.pop();
255
+ _.trys.pop();
256
+ continue;
257
+ }
258
+ op = body.call(thisArg, _);
259
+ } catch (e) {
260
+ op = [
261
+ 6,
262
+ e
263
+ ];
264
+ y = 0;
265
+ } finally{
266
+ f = t = 0;
267
+ }
268
+ if (op[0] & 5) throw op[1];
269
+ return {
270
+ value: op[0] ? op[1] : void 0,
271
+ done: true
272
+ };
273
+ }
274
+ }
275
+ typeof SuppressedError === "function" ? SuppressedError : function _SuppressedError(error, suppressed, message) {
276
+ var e = new Error(message);
277
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
278
+ };
279
+
280
+ function _extends() {
281
+ _extends = Object.assign || function assign(target) {
282
+ for(var i = 1; i < arguments.length; i++){
283
+ var source = arguments[i];
284
+ for(var key in source)if (Object.prototype.hasOwnProperty.call(source, key)) target[key] = source[key];
285
+ }
286
+ return target;
287
+ };
288
+ return _extends.apply(this, arguments);
289
+ }
290
+
291
+ var DEFAULT_RENDER_OPTIONS = {
292
+ inlineFonts: true,
293
+ inlineImages: true,
294
+ extractDefs: true
295
+ };
296
+ var SVG_DEF_TAGS = new Set([
297
+ "filter",
298
+ "clippath",
299
+ "mask",
300
+ "lineargradient",
301
+ "radialgradient",
302
+ "pattern",
303
+ "marker"
304
+ ]);
305
+ var IMG_TAG_NAMES = new Set([
306
+ "img"
307
+ ]);
308
+ var MEDIA_TAG_NAMES = new Set([
309
+ "video",
310
+ "source",
311
+ "input",
312
+ "audio"
313
+ ]);
314
+ var CSS_URL_REGEX = /url\(\s*(?:"([^"]+)"|'([^']+)'|([^\s)"']+))\s*\)/gi;
315
+ /**
316
+ * 安全提取 HTML 中指定标签名的指定属性值,使用字符级扫描避免 ReDoS。
317
+ * 仅扫描 `<tagName` 后的属性区域,不依赖回溯正则。
318
+ * tagNames 为 null 时匹配任意标签。
319
+ */ function extractTagAttributes(html, tagNames, attrNames, out) {
320
+ var i = 0;
321
+ var len = html.length;
322
+ while(i < len){
323
+ var lt = html.indexOf("<", i);
324
+ if (lt === -1) {
325
+ break;
326
+ }
327
+ var next = html.charCodeAt(lt + 1);
328
+ var isLetter = next >= 65 && next <= 90 || next >= 97 && next <= 122;
329
+ if (!isLetter) {
330
+ i = lt + 1;
331
+ continue;
332
+ }
333
+ var nameEnd = lt + 1;
334
+ while(nameEnd < len){
335
+ var c = html.charCodeAt(nameEnd);
336
+ if (c === 32 || c === 9 || c === 10 || c === 13 || c === 47 || c === 62) {
337
+ break;
338
+ }
339
+ nameEnd++;
340
+ }
341
+ var tagName = html.slice(lt + 1, nameEnd).toLowerCase();
342
+ if (tagNames !== null && !tagNames.has(tagName)) {
343
+ i = nameEnd;
344
+ continue;
345
+ }
346
+ // 找到本标签的结束 '>',在引号内的 '>' 不算,避免 style="background:url(>)" 这类干扰
347
+ var tagLimit = Math.min(len, nameEnd + 16000);
348
+ var tagEnd = -1;
349
+ var inDq = false;
350
+ var inSq = false;
351
+ for(var p = nameEnd; p < tagLimit; p++){
352
+ var c1 = html.charCodeAt(p);
353
+ if (c1 === 34 && !inSq) {
354
+ inDq = !inDq;
355
+ } else if (c1 === 39 && !inDq) {
356
+ inSq = !inSq;
357
+ } else if (c1 === 62 && !inDq && !inSq) {
358
+ tagEnd = p;
359
+ break;
360
+ }
361
+ }
362
+ if (tagEnd === -1) {
363
+ i = nameEnd;
364
+ continue;
365
+ }
366
+ var attrSection = html.slice(nameEnd, tagEnd);
367
+ for(var _iterator = _create_for_of_iterator_helper_loose(attrNames), _step; !(_step = _iterator()).done;){
368
+ var attrName = _step.value;
369
+ extractAttrValueFromSection(attrSection, attrName, out);
370
+ }
371
+ i = tagEnd + 1;
372
+ }
373
+ }
374
+ /**
375
+ * 字符级扫描提取所有 <style>...</style> 块的 textContent。
376
+ * 完全不使用回溯正则,最坏线性时间。
377
+ */ function extractStyleBlocks(html, out) {
378
+ var lower = html.toLowerCase();
379
+ var i = 0;
380
+ var len = html.length;
381
+ while(i < len){
382
+ var start = lower.indexOf("<style", i);
383
+ if (start === -1) {
384
+ break;
385
+ }
386
+ var after = lower.charCodeAt(start + 6);
387
+ var isStyleTag = after === 32 || after === 9 || after === 10 || after === 13 || after === 62 || after === 47;
388
+ if (!isStyleTag) {
389
+ i = start + 6;
390
+ continue;
391
+ }
392
+ // 跳过开标签到 '>' 结束
393
+ var openEnd = lower.indexOf(">", start + 6);
394
+ if (openEnd === -1) {
395
+ break;
396
+ }
397
+ var contentStart = openEnd + 1;
398
+ var closeStart = lower.indexOf("</style", contentStart);
399
+ if (closeStart === -1) {
400
+ break;
401
+ }
402
+ out.push(html.slice(contentStart, closeStart));
403
+ var closeEnd = lower.indexOf(">", closeStart);
404
+ if (closeEnd === -1) {
405
+ break;
406
+ }
407
+ i = closeEnd + 1;
408
+ }
409
+ }
410
+ /** 从单个标签的属性区域提取指定属性值 */ function extractAttrValueFromSection(section, attrName, out) {
411
+ var lower = section.toLowerCase();
412
+ var target = attrName.toLowerCase();
413
+ var pos = 0;
414
+ while(pos < lower.length){
415
+ var idx = lower.indexOf(target, pos);
416
+ if (idx === -1) {
417
+ return;
418
+ }
419
+ var prevChar = idx === 0 ? 32 : lower.charCodeAt(idx - 1);
420
+ var isBoundary = prevChar === 32 || prevChar === 9 || prevChar === 10 || prevChar === 13;
421
+ if (!isBoundary) {
422
+ pos = idx + target.length;
423
+ continue;
424
+ }
425
+ var p = idx + target.length;
426
+ while(p < section.length && /\s/.test(section[p])){
427
+ p++;
428
+ }
429
+ if (section[p] !== "=") {
430
+ pos = idx + target.length;
431
+ continue;
432
+ }
433
+ p++;
434
+ while(p < section.length && /\s/.test(section[p])){
435
+ p++;
436
+ }
437
+ var value = "";
438
+ var quote = section[p];
439
+ if (quote === '"' || quote === "'") {
440
+ var end = section.indexOf(quote, p + 1);
441
+ if (end === -1) {
442
+ return;
443
+ }
444
+ value = section.slice(p + 1, end);
445
+ } else {
446
+ var end1 = p;
447
+ while(end1 < section.length && !/[\s>]/.test(section[end1])){
448
+ end1++;
449
+ }
450
+ value = section.slice(p, end1);
451
+ }
452
+ if (value) {
453
+ out.add(value);
454
+ }
455
+ return;
456
+ }
457
+ }
458
+ /** 安全提取 @font-face 块,避免 ReDoS 正则回溯问题 */ function extractFontFaces(html) {
459
+ var faces = [];
460
+ var regex = /@font-face\s*\{/gi;
461
+ var match;
462
+ while((match = regex.exec(html)) !== null){
463
+ var start = match.index + match[0].length;
464
+ // 从匹配位置往后找匹配的 },最多检查 8000 字符
465
+ var braceCount = 1;
466
+ var end = start;
467
+ for(; end < html.length && end < start + 8000; end++){
468
+ if (html[end] === "{") {
469
+ braceCount++;
470
+ } else if (html[end] === "}") {
471
+ braceCount--;
472
+ if (braceCount === 0) {
473
+ break;
474
+ }
475
+ }
476
+ }
477
+ faces.push(html.slice(match.index, end + 1));
478
+ // 继续搜索时需要将 lastIndex 设置到 end 之后
479
+ regex.lastIndex = end + 1;
480
+ }
481
+ return faces;
482
+ }
483
+ var FONT_URL_REGEX = /url\(\s*(?:"([^"]+)"|'([^']+)'|([^\s)"']+))\s*\)/gi;
484
+ var CSS_SVG_REF_REGEX = /url\(\s*(?:"|')?#([a-zA-Z][\w.-]*)(?:"|')?\s*\)/g;
485
+ var isInlineUrl = function(url) {
486
+ return url.startsWith("data:") || url.startsWith("blob:");
487
+ };
488
+ /** 安全移除 HTML 中所有内联 <svg>...</svg> 块,使用字符级扫描避免 ReDoS */ function stripInlineSVG(html) {
489
+ var result = "";
490
+ var i = 0;
491
+ var len = html.length;
492
+ var lowerHtml = html.toLowerCase();
493
+ while(i < len){
494
+ var start = lowerHtml.indexOf("<svg", i);
495
+ if (start === -1) {
496
+ result += html.slice(i);
497
+ break;
498
+ }
499
+ var after = lowerHtml.charCodeAt(start + 4);
500
+ var isSvgTag = after === 32 || after === 9 || after === 10 || after === 13 || after === 62 || after === 47;
501
+ if (!isSvgTag) {
502
+ result += html.slice(i, start + 4);
503
+ i = start + 4;
504
+ continue;
505
+ }
506
+ result += html.slice(i, start);
507
+ var closeIdx = lowerHtml.indexOf("</svg", start + 4);
508
+ if (closeIdx === -1) {
509
+ break;
510
+ }
511
+ var gt = lowerHtml.indexOf(">", closeIdx);
512
+ if (gt === -1) {
513
+ break;
514
+ }
515
+ i = gt + 1;
516
+ }
517
+ return result;
518
+ }
519
+ /** 将 HTML 渲染为 Image 对象,默认自动执行内联字体/图片/提取 SVG 定义 */ function renderDOMToImage(html, width, height) {
520
+ return _renderDOMToImage.apply(this, arguments);
521
+ }
522
+ function _renderDOMToImage() {
523
+ _renderDOMToImage = _async_to_generator(function(html, width, height, scale, options) {
524
+ var opts, processedHtml, svgDefs, svgWidth, svgHeight, xhtmlSafeHtml, defsBlock, svg;
525
+ return __generator(this, function(_state) {
526
+ switch(_state.label){
527
+ case 0:
528
+ if (scale === void 0) scale = 1;
529
+ if (!Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(scale) || width <= 0 || height <= 0 || scale < 0) {
530
+ throw new Error("renderDOMToImage: width and height must be positive, scale must be non-negative.");
531
+ }
532
+ // scale 为 0 时返回空结果,与 width/height=0 行为保持一致
533
+ if (scale === 0) {
534
+ logger.warn("renderDOMToImage: scale is 0, skipping render.");
535
+ return [
536
+ 2,
537
+ loadImage("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")
538
+ ];
539
+ }
540
+ opts = _extends({}, DEFAULT_RENDER_OPTIONS, options);
541
+ // 先清理危险标记,再内联资源,避免从不安全标记中发现/获取 URL
542
+ processedHtml = sanitizeSVGContent(html);
543
+ if (!opts.inlineFonts) return [
544
+ 3,
545
+ 2
546
+ ];
547
+ return [
548
+ 4,
549
+ inlineFontSources(processedHtml)
550
+ ];
551
+ case 1:
552
+ processedHtml = _state.sent();
553
+ _state.label = 2;
554
+ case 2:
555
+ if (!opts.inlineImages) return [
556
+ 3,
557
+ 4
558
+ ];
559
+ return [
560
+ 4,
561
+ inlineImageSources(processedHtml)
562
+ ];
563
+ case 3:
564
+ processedHtml = _state.sent();
565
+ _state.label = 4;
566
+ case 4:
567
+ svgDefs = opts.extractDefs ? extractSVGDefs(processedHtml) : "";
568
+ svgWidth = width * scale;
569
+ svgHeight = height * scale;
570
+ // 将 HTML 转为合规 XHTML,避免未闭合的 void 标签和未转义实体导致 SVG/XML 解析失败
571
+ xhtmlSafeHtml = convertToXHTML(injectSVGNamespace(processedHtml));
572
+ defsBlock = svgDefs ? "<defs>" + svgDefs + "</defs>" : "";
573
+ svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' + svgWidth + '" height="' + svgHeight + '">' + defsBlock + ('<foreignObject width="' + width + '" height="' + height + '" transform="scale(' + scale + ')">') + xhtmlSafeHtml + "</foreignObject></svg>";
574
+ return [
575
+ 2,
576
+ loadImage("data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg))
577
+ ];
578
+ }
579
+ });
580
+ });
581
+ return _renderDOMToImage.apply(this, arguments);
582
+ }
583
+ /** 危险标签名集合(小写):将这些标签的 < > 转义为 &lt; &gt;,保留内容防止布局错位 */ var DANGEROUS_TAG_NAMES = new Set([
584
+ "foreignobject",
585
+ "script",
586
+ "iframe",
587
+ "object",
588
+ "embed",
589
+ "link",
590
+ "base",
591
+ "meta",
592
+ "template",
593
+ "noscript"
594
+ ]);
595
+ /** 需要进行 URL 协议安全检查的属性名(小写) */ var URL_ATTR_NAMES = new Set([
596
+ "href",
597
+ "src",
598
+ "action",
599
+ "formaction"
600
+ ]);
601
+ /** 不允许的协议列表 */ var DISALLOWED_PROTOCOLS = [
602
+ "javascript",
603
+ "vbscript"
604
+ ];
605
+ /** data: URL 中允许的安全 MIME 类型前缀 */ var SAFE_DATA_PREFIXES = [
606
+ "data:image/",
607
+ "data:font/",
608
+ "data:application/font",
609
+ "data:application/x-font"
610
+ ];
611
+ /**
612
+ * 规范化 URL 属性值:解码 HTML 实体和百分号编码,
613
+ * 移除控制字符和空白,转小写后检查是否包含危险协议
614
+ */ function normalizeUrlAttr(value) {
615
+ var normalized = value;
616
+ // 解码 HTML 实体(数字和命名)
617
+ normalized = normalized.replace(/&#x([0-9a-fA-F]+);?/g, function(_, hex) {
618
+ return String.fromCharCode(parseInt(hex, 16));
619
+ });
620
+ normalized = normalized.replace(/&#(\d+);?/g, function(_, dec) {
621
+ return String.fromCharCode(parseInt(dec, 10));
622
+ });
623
+ normalized = normalized.replace(/&(amp|lt|gt|quot|apos);/gi, function(match, name) {
624
+ var map = {
625
+ amp: "&",
626
+ lt: "<",
627
+ gt: ">",
628
+ quot: '"',
629
+ apos: "'"
630
+ };
631
+ var _map_name_toLowerCase;
632
+ return (_map_name_toLowerCase = map[name.toLowerCase()]) != null ? _map_name_toLowerCase : match;
633
+ });
634
+ // 解码百分号编码
635
+ try {
636
+ normalized = decodeURIComponent(normalized);
637
+ } catch (e) {}
638
+ // 移除控制字符(U+0000-U+001F, U+007F)和空白
639
+ // eslint-disable-next-line no-control-regex
640
+ normalized = normalized.replace(/[\u0000-\u001f\u007f\s]+/g, "");
641
+ // 转小写
642
+ normalized = normalized.toLowerCase();
643
+ // 去除前导非字母字符(如 / 或 \)
644
+ normalized = normalized.replace(/^[^a-z]+/, "");
645
+ return normalized;
646
+ }
647
+ /** 检查规范化后的 URL 是否使用了危险协议 */ function isDangerousUrl(value) {
648
+ var normalized = normalizeUrlAttr(value);
649
+ // 检查 javascript:/vbscript: 等明确危险协议
650
+ if (DISALLOWED_PROTOCOLS.some(function(proto) {
651
+ return normalized.startsWith("" + proto + ":");
652
+ })) {
653
+ return true;
654
+ }
655
+ // 检查 data: URL —— 仅允许安全的 MIME 类型(image/font),其余视为危险
656
+ if (normalized.startsWith("data:")) {
657
+ return !SAFE_DATA_PREFIXES.some(function(prefix) {
658
+ return normalized.startsWith(prefix);
659
+ });
660
+ }
661
+ return false;
662
+ }
663
+ /**
664
+ * 清理 HTML:转义危险标签,移除事件处理器和危险协议 URL 属性。
665
+ * 使用字符级扫描代替回溯正则,避免 ReDoS 漏洞。
666
+ */ function sanitizeSVGContent(html) {
667
+ return scanAndSanitize(html);
668
+ }
669
+ /**
670
+ * 单遍字符级扫描:
671
+ * - 识别危险标签(DANGEROUS_TAG_NAMES),将其 < > 转义为 &lt; &gt;
672
+ * - 在普通标签的属性区域内:移除 on* 事件处理器属性,移除值为危险 URL 的 href/src/action/formaction 属性
673
+ */ function scanAndSanitize(html) {
674
+ var out = "";
675
+ var i = 0;
676
+ var len = html.length;
677
+ while(i < len){
678
+ var ch = html.charCodeAt(i);
679
+ if (ch !== 60 /* < */ ) {
680
+ out += html[i++];
681
+ continue;
682
+ }
683
+ var next = html.charCodeAt(i + 1);
684
+ var isLetter = next >= 65 && next <= 90 || next >= 97 && next <= 122;
685
+ var nextNext = html.charCodeAt(i + 2);
686
+ var isSlashLetter = next === 47 /* / */ && (nextNext >= 65 && nextNext <= 90 || nextNext >= 97 && nextNext <= 122);
687
+ if (!isLetter && !isSlashLetter) {
688
+ out += html[i++];
689
+ continue;
690
+ }
691
+ var isClose = isSlashLetter;
692
+ var nameStart = isClose ? i + 2 : i + 1;
693
+ var nameEnd = nameStart;
694
+ while(nameEnd < len){
695
+ var c = html.charCodeAt(nameEnd);
696
+ if (c === 32 || c === 9 || c === 10 || c === 13 || c === 47 || c === 62) {
697
+ break;
698
+ }
699
+ nameEnd++;
700
+ }
701
+ var tagName = html.slice(nameStart, nameEnd).toLowerCase();
702
+ // 找到标签的结束 '>',识别引号区域避免误判 '>'
703
+ var tagLimit = Math.min(len, nameEnd + 16000);
704
+ var tagEnd = -1;
705
+ var inDoubleQuote = false;
706
+ var inSingleQuote = false;
707
+ for(var p = nameEnd; p < tagLimit; p++){
708
+ var c1 = html.charCodeAt(p);
709
+ if (c1 === 34 /* " */ && !inSingleQuote) {
710
+ inDoubleQuote = !inDoubleQuote;
711
+ } else if (c1 === 39 /* ' */ && !inDoubleQuote) {
712
+ inSingleQuote = !inSingleQuote;
713
+ } else if (c1 === 62 /* > */ && !inDoubleQuote && !inSingleQuote) {
714
+ tagEnd = p;
715
+ break;
716
+ }
717
+ }
718
+ if (tagEnd === -1) {
719
+ out += html[i++];
720
+ continue;
721
+ }
722
+ if (DANGEROUS_TAG_NAMES.has(tagName)) {
723
+ out += "&lt;" + html.slice(i + 1, tagEnd) + "&gt;";
724
+ i = tagEnd + 1;
725
+ continue;
726
+ }
727
+ var tagOpen = html.slice(i, nameEnd);
728
+ var attrSection = html.slice(nameEnd, tagEnd);
729
+ var cleanedAttrs = sanitizeAttrSection(attrSection);
730
+ out += tagOpen + cleanedAttrs + ">";
731
+ i = tagEnd + 1;
732
+ }
733
+ return out;
734
+ }
735
+ /**
736
+ * 清理标签的属性区域:
737
+ * - 移除 on* 事件处理器属性
738
+ * - 移除值为危险 URL 的 href/src/action/formaction 属性
739
+ */ function sanitizeAttrSection(section) {
740
+ var out = "";
741
+ var i = 0;
742
+ var len = section.length;
743
+ while(i < len){
744
+ var c = section.charCodeAt(i);
745
+ if (c === 32 || c === 9 || c === 10 || c === 13 || c === 47) {
746
+ out += section[i++];
747
+ continue;
748
+ }
749
+ var nameStart = i;
750
+ while(i < len){
751
+ var cc = section.charCodeAt(i);
752
+ if (cc === 61 /* = */ || cc === 32 || cc === 9 || cc === 10 || cc === 13 || cc === 47 || cc === 62) {
753
+ break;
754
+ }
755
+ i++;
756
+ }
757
+ if (i === nameStart) {
758
+ out += section[i++];
759
+ continue;
760
+ }
761
+ var attrName = section.slice(nameStart, i);
762
+ var lowerName = attrName.toLowerCase();
763
+ var p = i;
764
+ while(p < len && /\s/.test(section[p])){
765
+ p++;
766
+ }
767
+ var attrEnd = p;
768
+ var value = null;
769
+ var hasValue = false;
770
+ if (section[p] === "=") {
771
+ hasValue = true;
772
+ p++;
773
+ while(p < len && /\s/.test(section[p])){
774
+ p++;
775
+ }
776
+ var q = section[p];
777
+ if (q === '"' || q === "'") {
778
+ var end = section.indexOf(q, p + 1);
779
+ if (end === -1) {
780
+ out += section.slice(nameStart);
781
+ return out;
782
+ }
783
+ value = section.slice(p + 1, end);
784
+ attrEnd = end + 1;
785
+ } else {
786
+ var end1 = p;
787
+ while(end1 < len && !/[\s>]/.test(section[end1])){
788
+ end1++;
789
+ }
790
+ value = section.slice(p, end1);
791
+ attrEnd = end1;
792
+ }
793
+ }
794
+ var isEventHandler = lowerName.length > 2 && lowerName.charCodeAt(0) === 111 /* o */ && lowerName.charCodeAt(1) === 110 /* n */ ;
795
+ var isDangerousUrlAttr = hasValue && value !== null && URL_ATTR_NAMES.has(lowerName) && isDangerousUrl(value);
796
+ if (isEventHandler || isDangerousUrlAttr) {
797
+ i = attrEnd;
798
+ continue;
799
+ }
800
+ out += section.slice(nameStart, attrEnd);
801
+ i = attrEnd;
802
+ }
803
+ return out;
804
+ }
805
+ /** 为缺少 xmlns 的内嵌 <svg> 自动注入命名空间,使用字符级扫描,线性时间且无回溯 */ function injectSVGNamespace(html) {
806
+ var out = "";
807
+ var i = 0;
808
+ var len = html.length;
809
+ var lower = html.toLowerCase();
810
+ while(i < len){
811
+ var start = lower.indexOf("<svg", i);
812
+ if (start === -1) {
813
+ out += html.slice(i);
814
+ break;
815
+ }
816
+ var after = lower.charCodeAt(start + 4);
817
+ var isSvgTag = after === 32 || after === 9 || after === 10 || after === 13 || after === 62 || after === 47;
818
+ if (!isSvgTag) {
819
+ out += html.slice(i, start + 4);
820
+ i = start + 4;
821
+ continue;
822
+ }
823
+ var tagEnd = lower.indexOf(">", start + 4);
824
+ if (tagEnd === -1) {
825
+ out += html.slice(i);
826
+ break;
827
+ }
828
+ var tagSlice = lower.slice(start, tagEnd);
829
+ // 严格判断是否已有 xmlns 属性(前面有空白边界,后接 = 或空白)
830
+ var hasXmlns = /[\s][\s]*xmlns\s*=/i.test(" " + tagSlice);
831
+ out += html.slice(i, start + 4);
832
+ if (!hasXmlns) {
833
+ out += ' xmlns="http://www.w3.org/2000/svg"';
834
+ }
835
+ out += html.slice(start + 4, tagEnd + 1);
836
+ i = tagEnd + 1;
837
+ }
838
+ return out;
839
+ }
840
+ /**
841
+ * 将 HTML 字符串转为合规 XHTML,确保 void 标签被正确闭合、实体被转义,
842
+ * 使其可安全嵌入 SVG foreignObject(image/svg+xml 要求严格 XML 格式)
843
+ */ function convertToXHTML(html) {
844
+ if (typeof document === "undefined") {
845
+ // 非浏览器环境回退:手动闭合常见 void 标签
846
+ return '<div xmlns="http://www.w3.org/1999/xhtml">' + html + "</div>";
847
+ }
848
+ var doc = document.implementation.createHTMLDocument("");
849
+ var container = doc.createElement("div");
850
+ container.innerHTML = html;
851
+ container.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
852
+ return new XMLSerializer().serializeToString(container);
853
+ }
854
+ /**
855
+ * 从 HTML 中提取 CSS 上下文的 URL:仅扫描 <style> 元素的 textContent 和元素的 style 属性,
856
+ * 避免从普通文本/注释中误提取 URL。在无 DOMParser 环境下回退到正则但限制扫描范围。
857
+ */ function collectCSSUrls(html, urlSet) {
858
+ var cssTexts = [];
859
+ if (typeof DOMParser !== "undefined") {
860
+ try {
861
+ // 直接解析输入 HTML(inert document),避免通过模板拼接构造新 HTML 文档字符串
862
+ var doc = new DOMParser().parseFromString(html, "text/html");
863
+ doc.querySelectorAll("style").forEach(function(styleEl) {
864
+ if (styleEl.textContent) {
865
+ cssTexts.push(styleEl.textContent);
866
+ }
867
+ });
868
+ doc.querySelectorAll("[style]").forEach(function(el) {
869
+ var styleAttr = el.getAttribute("style");
870
+ if (styleAttr) {
871
+ cssTexts.push(styleAttr);
872
+ }
873
+ });
874
+ } catch (e) {
875
+ /* 解析失败则回退到下方正则扫描 */ }
876
+ }
877
+ // 回退路径:使用字符级扫描提取 <style>...</style> 块和 style="..." 属性,避免 ReDoS
878
+ if (cssTexts.length === 0) {
879
+ extractStyleBlocks(html, cssTexts);
880
+ var styleAttrSet = new Set();
881
+ extractTagAttributes(html, null, [
882
+ "style"
883
+ ], styleAttrSet);
884
+ for(var _iterator = _create_for_of_iterator_helper_loose(styleAttrSet), _step; !(_step = _iterator()).done;){
885
+ var v = _step.value;
886
+ cssTexts.push(v);
887
+ }
888
+ }
889
+ for(var _iterator1 = _create_for_of_iterator_helper_loose(cssTexts), _step1; !(_step1 = _iterator1()).done;){
890
+ var text = _step1.value;
891
+ CSS_URL_REGEX.lastIndex = 0;
892
+ var match = void 0;
893
+ while((match = CSS_URL_REGEX.exec(text)) !== null){
894
+ var _match_, _ref;
895
+ var url = (_ref = (_match_ = match[1]) != null ? _match_ : match[2]) != null ? _ref : match[3];
896
+ if (url && !isInlineUrl(url) && !url.startsWith("#")) {
897
+ urlSet.add(url);
898
+ }
899
+ }
900
+ }
901
+ }
902
+ /**
903
+ * 将 HTML 中外部资源 URL 转为 base64 内联,失败时保留原 URL
904
+ */ function inlineImageSources(html) {
905
+ return _inlineImageSources.apply(this, arguments);
906
+ }
907
+ function _inlineImageSources() {
908
+ _inlineImageSources = _async_to_generator(function(html) {
909
+ var rawUrlSet, urlSet, _iterator, _step, url, urlToBase64, urls;
910
+ return __generator(this, function(_state) {
911
+ switch(_state.label){
912
+ case 0:
913
+ rawUrlSet = new Set();
914
+ // 使用安全的字符级解析提取 src/poster 属性,避免 ReDoS
915
+ extractTagAttributes(html, IMG_TAG_NAMES, [
916
+ "src"
917
+ ], rawUrlSet);
918
+ extractTagAttributes(html, MEDIA_TAG_NAMES, [
919
+ "src",
920
+ "poster"
921
+ ], rawUrlSet);
922
+ urlSet = new Set();
923
+ for(_iterator = _create_for_of_iterator_helper_loose(rawUrlSet); !(_step = _iterator()).done;){
924
+ url = _step.value;
925
+ if (url && !isInlineUrl(url)) {
926
+ urlSet.add(url);
927
+ }
928
+ }
929
+ // 仅扫描真正的 CSS 上下文(<style> 内容和 style 属性),避免从文本/注释中误提取 URL
930
+ collectCSSUrls(html, urlSet);
931
+ if (urlSet.size === 0) {
932
+ return [
933
+ 2,
934
+ html
935
+ ];
936
+ }
937
+ urlToBase64 = new Map();
938
+ urls = [].concat(urlSet).slice(0, MAX_URLS);
939
+ if (urlSet.size > MAX_URLS) {
940
+ logger.warn("inlineImageSources: URL count (" + urlSet.size + ") exceeds limit (" + MAX_URLS + "), only processing first " + MAX_URLS + ".");
941
+ }
942
+ return [
943
+ 4,
944
+ promisePool(urls.map(function(url) {
945
+ return /*#__PURE__*/ _async_to_generator(function() {
946
+ var _, _tmp, e;
947
+ return __generator(this, function(_state) {
948
+ switch(_state.label){
949
+ case 0:
950
+ _state.trys.push([
951
+ 0,
952
+ 2,
953
+ ,
954
+ 3
955
+ ]);
956
+ _ = urlToBase64.set;
957
+ _tmp = [
958
+ url
959
+ ];
960
+ return [
961
+ 4,
962
+ fetchAsBase64(url)
963
+ ];
964
+ case 1:
965
+ _.apply(urlToBase64, _tmp.concat([
966
+ _state.sent()
967
+ ]));
968
+ return [
969
+ 3,
970
+ 3
971
+ ];
972
+ case 2:
973
+ e = _state.sent();
974
+ logger.warn('inlineImageSources: Failed to fetch "' + url + '".', e);
975
+ return [
976
+ 3,
977
+ 3
978
+ ];
979
+ case 3:
980
+ return [
981
+ 2
982
+ ];
983
+ }
984
+ });
985
+ });
986
+ }), MAX_FETCH_CONCURRENCY)
987
+ ];
988
+ case 1:
989
+ _state.sent();
990
+ return [
991
+ 2,
992
+ replaceUrls(html, urlToBase64)
993
+ ];
994
+ }
995
+ });
996
+ });
997
+ return _inlineImageSources.apply(this, arguments);
998
+ }
999
+ /**
1000
+ * 将 @font-face 中的外部字体 URL 转为 base64 内联
1001
+ */ function inlineFontSources(html) {
1002
+ return _inlineFontSources.apply(this, arguments);
1003
+ }
1004
+ function _inlineFontSources() {
1005
+ _inlineFontSources = _async_to_generator(function(html) {
1006
+ var urlSet, fontFaces, _iterator, _step, face, urlMatch, _urlMatch_, _ref, url, urlToBase64, urls;
1007
+ return __generator(this, function(_state) {
1008
+ switch(_state.label){
1009
+ case 0:
1010
+ urlSet = new Set();
1011
+ // 使用安全的逐字符解析代替正则
1012
+ fontFaces = extractFontFaces(html);
1013
+ for(_iterator = _create_for_of_iterator_helper_loose(fontFaces); !(_step = _iterator()).done;){
1014
+ face = _step.value;
1015
+ FONT_URL_REGEX.lastIndex = 0;
1016
+ urlMatch = void 0;
1017
+ while((urlMatch = FONT_URL_REGEX.exec(face)) !== null){
1018
+ url = (_ref = (_urlMatch_ = urlMatch[1]) != null ? _urlMatch_ : urlMatch[2]) != null ? _ref : urlMatch[3];
1019
+ if (url && !isInlineUrl(url)) {
1020
+ urlSet.add(url);
1021
+ }
1022
+ }
1023
+ }
1024
+ if (urlSet.size === 0) {
1025
+ return [
1026
+ 2,
1027
+ html
1028
+ ];
1029
+ }
1030
+ urlToBase64 = new Map();
1031
+ urls = [].concat(urlSet).slice(0, MAX_URLS);
1032
+ if (urlSet.size > MAX_URLS) {
1033
+ logger.warn("inlineFontSources: URL count (" + urlSet.size + ") exceeds limit (" + MAX_URLS + "), only processing first " + MAX_URLS + ".");
1034
+ }
1035
+ return [
1036
+ 4,
1037
+ promisePool(urls.map(function(url) {
1038
+ return /*#__PURE__*/ _async_to_generator(function() {
1039
+ var _, _tmp, e;
1040
+ return __generator(this, function(_state) {
1041
+ switch(_state.label){
1042
+ case 0:
1043
+ _state.trys.push([
1044
+ 0,
1045
+ 2,
1046
+ ,
1047
+ 3
1048
+ ]);
1049
+ _ = urlToBase64.set;
1050
+ _tmp = [
1051
+ url
1052
+ ];
1053
+ return [
1054
+ 4,
1055
+ fetchAsBase64(url)
1056
+ ];
1057
+ case 1:
1058
+ _.apply(urlToBase64, _tmp.concat([
1059
+ _state.sent()
1060
+ ]));
1061
+ return [
1062
+ 3,
1063
+ 3
1064
+ ];
1065
+ case 2:
1066
+ e = _state.sent();
1067
+ logger.warn('inlineFontSources: Failed to fetch font "' + url + '".', e);
1068
+ return [
1069
+ 3,
1070
+ 3
1071
+ ];
1072
+ case 3:
1073
+ return [
1074
+ 2
1075
+ ];
1076
+ }
1077
+ });
1078
+ });
1079
+ }), MAX_FETCH_CONCURRENCY)
1080
+ ];
1081
+ case 1:
1082
+ _state.sent();
1083
+ return [
1084
+ 2,
1085
+ replaceUrls(html, urlToBase64)
1086
+ ];
1087
+ }
1088
+ });
1089
+ });
1090
+ return _inlineFontSources.apply(this, arguments);
1091
+ }
1092
+ /**
1093
+ * 替换 URL 为 base64:
1094
+ * - 标签属性(src/poster)使用字符级扫描定位后做精确字符串替换,避免回溯正则 ReDoS
1095
+ * - CSS url() 替换使用线性时间的简单正则(无嵌套量词、无回溯陷阱)
1096
+ * - 全程区分大小写匹配 URL,确保通过 Map 精准查找
1097
+ */ function replaceUrls(html, urlToBase64) {
1098
+ // 1) 替换 <img>/<video>/<source>/<input>/<audio> 的 src/poster 属性
1099
+ var html1 = replaceTagAttrUrls(html, new Set([
1100
+ "img",
1101
+ "video",
1102
+ "source",
1103
+ "input",
1104
+ "audio"
1105
+ ]), [
1106
+ "src",
1107
+ "poster"
1108
+ ], urlToBase64);
1109
+ // 2) 替换 CSS url("...") / url('...'),正则不含嵌套量词,线性时间
1110
+ var result = html1.replace(/url\(\s*(["'])([^"'\n\r)]*)\1\s*\)/g, function(match, quote, url) {
1111
+ var base64 = urlToBase64.get(url);
1112
+ return base64 ? "url(" + quote + base64 + quote + ")" : match;
1113
+ });
1114
+ // 3) 替换 CSS url(...) 不带引号的 URL,线性时间
1115
+ result = result.replace(/url\(\s*([^\s)"'\n\r]+)\s*\)/g, function(match, url) {
1116
+ var base64 = urlToBase64.get(url);
1117
+ return base64 ? "url(" + base64 + ")" : match;
1118
+ });
1119
+ return result;
1120
+ }
1121
+ /**
1122
+ * 字符级扫描定位指定标签的指定属性,并将属性值替换为 Map 中的 base64。
1123
+ * 完全不使用回溯正则,最坏情况是线性时间。
1124
+ */ function replaceTagAttrUrls(html, tagNames, attrNames, urlToBase64) {
1125
+ if (urlToBase64.size === 0) {
1126
+ return html;
1127
+ }
1128
+ var targetAttrs = attrNames.map(function(a) {
1129
+ return a.toLowerCase();
1130
+ });
1131
+ var out = "";
1132
+ var i = 0;
1133
+ var len = html.length;
1134
+ while(i < len){
1135
+ var lt = html.indexOf("<", i);
1136
+ if (lt === -1) {
1137
+ out += html.slice(i);
1138
+ break;
1139
+ }
1140
+ var next = html.charCodeAt(lt + 1);
1141
+ var isLetter = next >= 65 && next <= 90 || next >= 97 && next <= 122;
1142
+ if (!isLetter) {
1143
+ out += html.slice(i, lt + 1);
1144
+ i = lt + 1;
1145
+ continue;
1146
+ }
1147
+ var nameEnd = lt + 1;
1148
+ while(nameEnd < len){
1149
+ var c = html.charCodeAt(nameEnd);
1150
+ if (c === 32 || c === 9 || c === 10 || c === 13 || c === 47 || c === 62) {
1151
+ break;
1152
+ }
1153
+ nameEnd++;
1154
+ }
1155
+ var tagName = html.slice(lt + 1, nameEnd).toLowerCase();
1156
+ if (!tagNames.has(tagName)) {
1157
+ out += html.slice(i, nameEnd);
1158
+ i = nameEnd;
1159
+ continue;
1160
+ }
1161
+ // 找到本标签的结束 '>',在引号内的 '>' 不算
1162
+ var tagLimit = Math.min(len, nameEnd + 16000);
1163
+ var tagEnd = -1;
1164
+ var inDq = false;
1165
+ var inSq = false;
1166
+ for(var p = nameEnd; p < tagLimit; p++){
1167
+ var c1 = html.charCodeAt(p);
1168
+ if (c1 === 34 && !inSq) {
1169
+ inDq = !inDq;
1170
+ } else if (c1 === 39 && !inDq) {
1171
+ inSq = !inSq;
1172
+ } else if (c1 === 62 && !inDq && !inSq) {
1173
+ tagEnd = p;
1174
+ break;
1175
+ }
1176
+ }
1177
+ if (tagEnd === -1) {
1178
+ out += html.slice(i, lt + 1);
1179
+ i = lt + 1;
1180
+ continue;
1181
+ }
1182
+ // 在 [nameEnd, tagEnd) 区间内,逐属性替换目标 src/poster
1183
+ out += html.slice(i, nameEnd);
1184
+ out += replaceAttrsInSection(html.slice(nameEnd, tagEnd), targetAttrs, urlToBase64);
1185
+ out += ">";
1186
+ i = tagEnd + 1;
1187
+ }
1188
+ return out;
1189
+ }
1190
+ /** 在单个标签的属性区域内,将匹配 targetAttrs 的属性值替换为 Map 中的 base64 */ function replaceAttrsInSection(section, targetAttrs, urlToBase64) {
1191
+ var out = "";
1192
+ var i = 0;
1193
+ var len = section.length;
1194
+ while(i < len){
1195
+ var c = section.charCodeAt(i);
1196
+ if (c === 32 || c === 9 || c === 10 || c === 13 || c === 47) {
1197
+ out += section[i++];
1198
+ continue;
1199
+ }
1200
+ var nameStart = i;
1201
+ while(i < len){
1202
+ var cc = section.charCodeAt(i);
1203
+ if (cc === 61 /* = */ || cc === 32 || cc === 9 || cc === 10 || cc === 13 || cc === 47 || cc === 62) {
1204
+ break;
1205
+ }
1206
+ i++;
1207
+ }
1208
+ if (i === nameStart) {
1209
+ out += section[i++];
1210
+ continue;
1211
+ }
1212
+ var lowerName = section.slice(nameStart, i).toLowerCase();
1213
+ var p = i;
1214
+ while(p < len && /\s/.test(section[p])){
1215
+ p++;
1216
+ }
1217
+ if (section[p] !== "=") {
1218
+ out += section.slice(nameStart, p);
1219
+ i = p;
1220
+ continue;
1221
+ }
1222
+ p++;
1223
+ while(p < len && /\s/.test(section[p])){
1224
+ p++;
1225
+ }
1226
+ var valueStart = p;
1227
+ var valueEnd = p;
1228
+ var attrEnd = p;
1229
+ var quote = null;
1230
+ if (section[p] === '"' || section[p] === "'") {
1231
+ quote = section[p];
1232
+ var end = section.indexOf(quote, p + 1);
1233
+ if (end === -1) {
1234
+ out += section.slice(nameStart);
1235
+ return out;
1236
+ }
1237
+ valueStart = p + 1;
1238
+ valueEnd = end;
1239
+ attrEnd = end + 1;
1240
+ } else {
1241
+ var end1 = p;
1242
+ while(end1 < len && !/[\s>]/.test(section[end1])){
1243
+ end1++;
1244
+ }
1245
+ valueStart = p;
1246
+ valueEnd = end1;
1247
+ attrEnd = end1;
1248
+ }
1249
+ var value = section.slice(valueStart, valueEnd);
1250
+ if (targetAttrs.includes(lowerName)) {
1251
+ var base64 = urlToBase64.get(value);
1252
+ if (base64) {
1253
+ out += section.slice(nameStart, valueStart) + base64 + (quote != null ? quote : "") + section.slice(attrEnd, attrEnd);
1254
+ // 上一行末尾追加的 '' 是为了清晰;attrEnd 之后的内容由外层循环继续处理
1255
+ // 注意:当 quote 不为 null 时,前缀 section.slice(nameStart, valueStart) 已经包含开引号;
1256
+ // 我们追加 base64 + 闭合引号 即可。
1257
+ i = attrEnd;
1258
+ continue;
1259
+ }
1260
+ }
1261
+ out += section.slice(nameStart, attrEnd);
1262
+ i = attrEnd;
1263
+ }
1264
+ return out;
1265
+ }
1266
+ /**
1267
+ * 从宿主文档提取 CSS url(#id) 引用的 SVG 定义,支持 filter/clipPath/mask/gradient/pattern/marker
1268
+ */ function extractSVGDefs(html) {
1269
+ if (typeof document === "undefined") {
1270
+ return "";
1271
+ }
1272
+ // 使用安全的字符级扫描移除内联 <svg> 块,避免 ReDoS
1273
+ var htmlWithoutInlineSVG = stripInlineSVG(html);
1274
+ var ids = new Set();
1275
+ CSS_SVG_REF_REGEX.lastIndex = 0;
1276
+ var match;
1277
+ while((match = CSS_SVG_REF_REGEX.exec(htmlWithoutInlineSVG)) !== null){
1278
+ ids.add(match[1]);
1279
+ }
1280
+ if (ids.size === 0) {
1281
+ return "";
1282
+ }
1283
+ var serializer = new XMLSerializer();
1284
+ var parts = [];
1285
+ for(var _iterator = _create_for_of_iterator_helper_loose(ids), _step; !(_step = _iterator()).done;){
1286
+ var id = _step.value;
1287
+ var el = document.getElementById(id);
1288
+ if (!el) {
1289
+ logger.warn("extractSVGDefs: #" + id + " not found.");
1290
+ continue;
1291
+ }
1292
+ if (!SVG_DEF_TAGS.has(el.tagName.toLowerCase())) {
1293
+ logger.warn("extractSVGDefs: #" + id + " is not a supported SVG def element.");
1294
+ continue;
1295
+ }
1296
+ parts.push(serializer.serializeToString(el));
1297
+ }
1298
+ return parts.join("");
1299
+ }
1300
+ var MAX_FETCH_CONCURRENCY = 6; // 最大并发获取数
1301
+ var MAX_RESOURCE_BYTES = 10 * 1024 * 1024; // 单个资源最大 10MB
1302
+ var MAX_URLS = 100; // 单次内联处理的最大 URL 数量
1303
+ function promisePool(tasks, concurrency) {
1304
+ return _promisePool.apply(this, arguments);
1305
+ }
1306
+ function _promisePool() {
1307
+ _promisePool = /** 有界并发执行异步任务 */ _async_to_generator(function(tasks, concurrency) {
1308
+ var results, index, workers;
1309
+ function worker() {
1310
+ return _worker.apply(this, arguments);
1311
+ }
1312
+ function _worker() {
1313
+ _worker = _async_to_generator(function() {
1314
+ var i;
1315
+ return __generator(this, function(_state) {
1316
+ switch(_state.label){
1317
+ case 0:
1318
+ if (!(index < tasks.length)) return [
1319
+ 3,
1320
+ 2
1321
+ ];
1322
+ i = index++;
1323
+ return [
1324
+ 4,
1325
+ tasks[i]()
1326
+ ];
1327
+ case 1:
1328
+ results[i] = _state.sent();
1329
+ return [
1330
+ 3,
1331
+ 0
1332
+ ];
1333
+ case 2:
1334
+ return [
1335
+ 2
1336
+ ];
1337
+ }
1338
+ });
1339
+ });
1340
+ return _worker.apply(this, arguments);
1341
+ }
1342
+ return __generator(this, function(_state) {
1343
+ switch(_state.label){
1344
+ case 0:
1345
+ results = new Array(tasks.length);
1346
+ index = 0;
1347
+ workers = Array.from({
1348
+ length: Math.min(concurrency, tasks.length)
1349
+ }, function() {
1350
+ return worker();
1351
+ });
1352
+ return [
1353
+ 4,
1354
+ Promise.all(workers)
1355
+ ];
1356
+ case 1:
1357
+ _state.sent();
1358
+ return [
1359
+ 2,
1360
+ results
1361
+ ];
1362
+ }
1363
+ });
1364
+ });
1365
+ return _promisePool.apply(this, arguments);
1366
+ }
1367
+ function fetchAsBase64(url) {
1368
+ return _fetchAsBase64.apply(this, arguments);
1369
+ }
1370
+ function _fetchAsBase64() {
1371
+ _fetchAsBase64 = _async_to_generator(function(url) {
1372
+ var blob;
1373
+ return __generator(this, function(_state) {
1374
+ switch(_state.label){
1375
+ case 0:
1376
+ return [
1377
+ 4,
1378
+ loadBlob(url)
1379
+ ];
1380
+ case 1:
1381
+ blob = _state.sent();
1382
+ if (blob.size > MAX_RESOURCE_BYTES) {
1383
+ throw new Error('fetchAsBase64: Resource "' + url + '" exceeds max size (' + MAX_RESOURCE_BYTES + " bytes), actual: " + blob.size + ".");
1384
+ }
1385
+ return [
1386
+ 2,
1387
+ new Promise(function(resolve, reject) {
1388
+ var reader = new FileReader();
1389
+ reader.onloadend = function() {
1390
+ resolve(reader.result);
1391
+ };
1392
+ reader.onerror = function() {
1393
+ reject(new Error("FileReader failed."));
1394
+ };
1395
+ reader.readAsDataURL(blob);
1396
+ })
1397
+ ];
1398
+ }
1399
+ });
1400
+ });
1401
+ return _fetchAsBase64.apply(this, arguments);
1402
+ }
1403
+
1404
+ var DATA_TYPE = "DomContentComponent";
1405
+ var MAX_TEXTURE_SIZE = 2048;
1406
+ /** 单次 setContent 接受的 HTML 最大字符数,避免攻击者传入超大字符串造成 OOM/CPU 耗尽 */ var MAX_HTML_LENGTH = 1024 * 1024; // 1M 字符
1407
+ /** alpha 修复用的极小值,避免 canvas 完全透明像素被某些浏览器优化为 0 */ var ALPHA_FIX_VALUE = 1 / 255;
1408
+ var DomContentComponent = /*#__PURE__*/ function(MaskableGraphic1) {
1409
+ _inherits(DomContentComponent, MaskableGraphic1);
1410
+ function DomContentComponent(engine) {
1411
+ var _this;
1412
+ _this = MaskableGraphic1.call(this, engine) || this;
1413
+ _this.htmlContent = "";
1414
+ _this.contentWidth = 300;
1415
+ _this.contentHeight = 200;
1416
+ _this.contentScale = 1;
1417
+ _this.rendering = false;
1418
+ /** 异步任务取消标记:onDestroy 后置为 true,用于让 await 中的 updateTexture 回到同步段时立刻退出,避免泄漏纹理或写已释放的 material。 */ _this.asyncCancelled = false;
1419
+ _this.targetComponent = null;
1420
+ /** 内容版本号:每次 setContent 自增,用于丢弃过期的异步渲染结果,防止慢请求覆盖快请求。 */ _this.renderVersion = 0;
1421
+ /** 由本组件创建的纹理集合,仅这些纹理可被 dispose */ _this.ownedTextures = new Set();
1422
+ /** 保存 target 组件的原始纹理,用于组件销毁时恢复 */ _this.originalTargetTexture = null;
1423
+ _this.isDirty = false;
1424
+ // 自建独占 canvas:本组件的 updateTexture 是异步的,且无短期归还需求,
1425
+ // 不走 canvasPool 以免占用其全局复用槽位影响其他组件(如 TextComponent)。
1426
+ _this.canvas = document.createElement("canvas");
1427
+ _this.context = _this.canvas.getContext("2d", {
1428
+ willReadFrequently: true
1429
+ });
1430
+ return _this;
1431
+ }
1432
+ var _proto = DomContentComponent.prototype;
1433
+ _proto.setContent = function setContent(html, width, height, scale) {
1434
+ if (typeof html !== "string") {
1435
+ logger.warn("DomContentComponent.setContent: html must be a string, ignored.");
1436
+ return;
1437
+ }
1438
+ if (html.length > MAX_HTML_LENGTH) {
1439
+ logger.warn("DomContentComponent.setContent: html length (" + html.length + ") exceeds limit (" + MAX_HTML_LENGTH + "), truncated.");
1440
+ this.htmlContent = html.slice(0, MAX_HTML_LENGTH);
1441
+ } else {
1442
+ this.htmlContent = html;
1443
+ }
1444
+ if (width !== undefined) {
1445
+ this.contentWidth = Math.max(0, width);
1446
+ }
1447
+ if (height !== undefined) {
1448
+ this.contentHeight = Math.max(0, height);
1449
+ }
1450
+ if (scale !== undefined) {
1451
+ this.contentScale = Math.max(0, scale);
1452
+ }
1453
+ this.isDirty = true;
1454
+ this.renderVersion++;
1455
+ };
1456
+ _proto.onAwake = function onAwake() {
1457
+ this.material.setColor("_Color", new math.Color(1, 1, 1, 1));
1458
+ if (this.item) {
1459
+ for(var _iterator = _create_for_of_iterator_helper_loose(this.item.getComponents(MaskableGraphic)), _step; !(_step = _iterator()).done;){
1460
+ var comp = _step.value;
1461
+ if (comp !== this) {
1462
+ this.targetComponent = comp;
1463
+ logger.info("DomContentComponent: Bound to " + comp.constructor.name + ", overriding texture.");
1464
+ break;
1465
+ }
1466
+ }
1467
+ }
1468
+ };
1469
+ _proto.onUpdate = function onUpdate(dt) {
1470
+ MaskableGraphic1.prototype.onUpdate.call(this, dt);
1471
+ if (this.isDirty && !this.rendering) {
1472
+ this.isDirty = false;
1473
+ this.updateTexture().catch(function(e) {
1474
+ logger.error("DomContentComponent: Unhandled error in updateTexture.", e);
1475
+ });
1476
+ }
1477
+ };
1478
+ _proto.render = function render(renderer) {
1479
+ if (this.targetComponent) {
1480
+ return;
1481
+ }
1482
+ MaskableGraphic1.prototype.render.call(this, renderer);
1483
+ };
1484
+ _proto.onDestroy = function onDestroy() {
1485
+ var _this_targetComponent;
1486
+ MaskableGraphic1.prototype.onDestroy.call(this);
1487
+ // 先取消异步任务,再做资源清理,避免 in-flight Promise resolve 后仍写入已释放的资源。
1488
+ this.asyncCancelled = true;
1489
+ this.disposeOwnTexture();
1490
+ // 恢复 target 组件的原始纹理
1491
+ if (((_this_targetComponent = this.targetComponent) == null ? void 0 : _this_targetComponent.renderer) && this.originalTargetTexture) {
1492
+ void this.targetComponent.setTexture(this.originalTargetTexture);
1493
+ }
1494
+ // 仅 dispose 由本组件创建的纹理
1495
+ for(var _iterator = _create_for_of_iterator_helper_loose(this.ownedTextures), _step; !(_step = _iterator()).done;){
1496
+ var tex = _step.value;
1497
+ tex.dispose();
1498
+ }
1499
+ this.ownedTextures.clear();
1500
+ this.originalTargetTexture = null;
1501
+ if (this.canvas) {
1502
+ // 缩到 1x1 立即释放后端 ImageBuffer 显存,无需等待 GC。
1503
+ this.canvas.width = 1;
1504
+ this.canvas.height = 1;
1505
+ }
1506
+ this.targetComponent = null;
1507
+ };
1508
+ /**
1509
+ * 释放当前 renderer 上挂载的纹理(白纹理是引擎共享资源,不应释放)。
1510
+ */ _proto.disposeOwnTexture = function disposeOwnTexture() {
1511
+ var texture = this.renderer.texture;
1512
+ if (texture && texture !== this.engine.whiteTexture) {
1513
+ texture.dispose();
1514
+ }
1515
+ };
1516
+ /**
1517
+ * 将一张 image 绘制到内部 canvas,并据此创建纹理写入到本组件 renderer / material。
1518
+ *
1519
+ * @param width - 纹理宽度(像素)
1520
+ * @param height - 纹理高度(像素)
1521
+ * @param flipY - 是否翻转 Y 轴;为 false 时通过 canvas 变换预先翻转
1522
+ * @param drawCallback - 实际绘制回调,接收已重置变换并清空的 2D 上下文
1523
+ */ _proto.renderImageToTexture = function renderImageToTexture(width, height, flipY, drawCallback) {
1524
+ if (!this.context || !this.canvas) {
1525
+ return;
1526
+ }
1527
+ var context = this.context;
1528
+ context.save();
1529
+ this.canvas.width = width;
1530
+ this.canvas.height = height;
1531
+ context.setTransform(1, 0, 0, 1, 0, 0);
1532
+ if (!flipY) {
1533
+ context.translate(0, height);
1534
+ context.scale(1, -1);
1535
+ }
1536
+ context.clearRect(0, 0, width, height);
1537
+ // 设置 alpha 修复用填充色(不实际输出像素,避免完全透明被某些浏览器优化)
1538
+ context.fillStyle = "rgba(255, 255, 255, " + ALPHA_FIX_VALUE + ")";
1539
+ drawCallback(context);
1540
+ context.restore();
1541
+ var imageData = context.getImageData(0, 0, width, height);
1542
+ var texture = Texture.createWithData(this.engine, {
1543
+ data: new Uint8Array(imageData.data),
1544
+ width: imageData.width,
1545
+ height: imageData.height
1546
+ }, {
1547
+ flipY: flipY,
1548
+ magFilter: glContext.LINEAR,
1549
+ minFilter: glContext.LINEAR,
1550
+ wrapS: glContext.CLAMP_TO_EDGE,
1551
+ wrapT: glContext.CLAMP_TO_EDGE
1552
+ });
1553
+ this.disposeOwnTexture();
1554
+ this.renderer.texture = texture;
1555
+ this.material.setTexture("_MainTex", texture);
1556
+ this.ownedTextures.add(texture);
1557
+ };
1558
+ _proto.updateTexture = function updateTexture() {
1559
+ var _this = this;
1560
+ return _async_to_generator(function() {
1561
+ var startVersion, htmlContent, contentWidth, contentHeight, contentScale, texWidth, texHeight, cappedScaleX, cappedScaleY, cappedScale, image, e, pending;
1562
+ return __generator(this, function(_state) {
1563
+ switch(_state.label){
1564
+ case 0:
1565
+ startVersion = _this.renderVersion;
1566
+ htmlContent = _this.htmlContent, contentWidth = _this.contentWidth, contentHeight = _this.contentHeight, contentScale = _this.contentScale;
1567
+ if (!htmlContent || contentWidth <= 0 || contentHeight <= 0) {
1568
+ return [
1569
+ 2
1570
+ ];
1571
+ }
1572
+ if (contentScale <= 0) {
1573
+ logger.warn("DomContentComponent: contentScale must be positive, skipping render.");
1574
+ return [
1575
+ 2
1576
+ ];
1577
+ }
1578
+ texWidth = Math.min(Math.round(contentWidth * contentScale), MAX_TEXTURE_SIZE);
1579
+ texHeight = Math.min(Math.round(contentHeight * contentScale), MAX_TEXTURE_SIZE);
1580
+ // 计算调整后的 scale,确保 renderDOMToImage 生成的图片不超过 MAX_TEXTURE_SIZE
1581
+ cappedScaleX = texWidth / contentWidth;
1582
+ cappedScaleY = texHeight / contentHeight;
1583
+ cappedScale = Math.min(cappedScaleX, cappedScaleY);
1584
+ _this.rendering = true;
1585
+ _state.label = 1;
1586
+ case 1:
1587
+ _state.trys.push([
1588
+ 1,
1589
+ 3,
1590
+ 4,
1591
+ 5
1592
+ ]);
1593
+ return [
1594
+ 4,
1595
+ renderDOMToImage(htmlContent, contentWidth, contentHeight, cappedScale)
1596
+ ];
1597
+ case 2:
1598
+ image = _state.sent();
1599
+ // 竞态防护:组件已销毁、或已有更新的 setContent 调用,则丢弃本次过期结果。
1600
+ if (_this.asyncCancelled || startVersion !== _this.renderVersion) {
1601
+ return [
1602
+ 2
1603
+ ];
1604
+ }
1605
+ if (_this.targetComponent) {
1606
+ _this.updateTargetTexture(image, texWidth, texHeight);
1607
+ } else {
1608
+ _this.renderImageToTexture(texWidth, texHeight, true, function(ctx) {
1609
+ ctx.drawImage(image, 0, 0, texWidth, texHeight);
1610
+ });
1611
+ }
1612
+ return [
1613
+ 3,
1614
+ 5
1615
+ ];
1616
+ case 3:
1617
+ e = _state.sent();
1618
+ logger.error("DomContentComponent: Failed to render texture.", e);
1619
+ return [
1620
+ 3,
1621
+ 5
1622
+ ];
1623
+ case 4:
1624
+ _this.rendering = false;
1625
+ // 排空脏标记:渲染期间若有新的 setContent,触发再次更新;销毁后则不再启动。
1626
+ if (_this.isDirty && !_this.asyncCancelled) {
1627
+ pending = _this.isDirty;
1628
+ _this.isDirty = false;
1629
+ if (pending) {
1630
+ _this.updateTexture().catch(function(e) {
1631
+ logger.error("DomContentComponent: Unhandled error in updateTexture.", e);
1632
+ });
1633
+ }
1634
+ }
1635
+ return [
1636
+ 7
1637
+ ];
1638
+ case 5:
1639
+ return [
1640
+ 2
1641
+ ];
1642
+ }
1643
+ });
1644
+ })();
1645
+ };
1646
+ _proto.updateTargetTexture = function updateTargetTexture(image, width, height) {
1647
+ if (!this.canvas || !this.context || !this.targetComponent) {
1648
+ return;
1649
+ }
1650
+ var ctx = this.context;
1651
+ var target = this.targetComponent;
1652
+ // 检查 target 的 renderer 是否存在
1653
+ if (!target.renderer) {
1654
+ logger.warn("DomContentComponent: targetComponent.renderer is null, skipping texture update.");
1655
+ return;
1656
+ }
1657
+ // 保存 target 的原始纹理(仅首次)
1658
+ var currentTexture = target.renderer.texture;
1659
+ if (this.originalTargetTexture === null) {
1660
+ this.originalTargetTexture = currentTexture != null ? currentTexture : null;
1661
+ }
1662
+ // 仅 dispose 由本组件创建的纹理,不触碰原始/共享纹理
1663
+ if (currentTexture && this.ownedTextures.has(currentTexture)) {
1664
+ currentTexture.dispose();
1665
+ this.ownedTextures.delete(currentTexture);
1666
+ }
1667
+ ctx.save();
1668
+ this.canvas.width = width;
1669
+ this.canvas.height = height;
1670
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
1671
+ ctx.clearRect(0, 0, width, height);
1672
+ ctx.drawImage(image, 0, 0, width, height);
1673
+ ctx.restore();
1674
+ var imageData = ctx.getImageData(0, 0, width, height);
1675
+ var texture = Texture.createWithData(this.engine, {
1676
+ data: new Uint8Array(imageData.data),
1677
+ width: imageData.width,
1678
+ height: imageData.height
1679
+ }, {
1680
+ flipY: true,
1681
+ magFilter: glContext.LINEAR,
1682
+ minFilter: glContext.LINEAR,
1683
+ wrapS: glContext.CLAMP_TO_EDGE,
1684
+ wrapT: glContext.CLAMP_TO_EDGE
1685
+ });
1686
+ // 将新创建的纹理标记为本组件所有
1687
+ this.ownedTextures.add(texture);
1688
+ void target.setTexture(texture);
1689
+ };
1690
+ return DomContentComponent;
1691
+ }(MaskableGraphic);
1692
+ DomContentComponent = __decorate([
1693
+ effectsClass(DATA_TYPE)
1694
+ ], DomContentComponent);
1695
+
1696
+ var version = "2.9.0";
1697
+ registerPlugin("dom-content", DomContentLoader);
1698
+ logger.info("Plugin dom-content version: " + version + ".");
1699
+ if (version !== EFFECTS.version) {
1700
+ console.error("注意:请统一 DomContent 插件与 Player 版本,不统一的版本混用会有不可预知的后果!", "\nAttention: Please ensure the DomContent plugin is synchronized with the Player version. Mixing and matching incompatible versions may result in unpredictable consequences!");
1701
+ }
1702
+
1703
+ export { DomContentComponent, DomContentLoader, extractSVGDefs, inlineFontSources, inlineImageSources, renderDOMToImage, sanitizeSVGContent, version };
1704
+ //# sourceMappingURL=index.mjs.map