html2canvas-pro 2.4.2 → 2.4.4
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/html2canvas-pro.cjs +463 -72
- package/dist/html2canvas-pro.cjs.map +1 -1
- package/dist/html2canvas-pro.esm.js +463 -72
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +463 -72
- package/dist/html2canvas-pro.js.map +1 -1
- package/dist/html2canvas-pro.min.js +5 -5
- package/dist/lib/core/render-element.js +14 -15
- package/dist/lib/css/property-descriptors/filter.js +14 -4
- package/dist/lib/css/property-descriptors/font-family.js +16 -1
- package/dist/lib/dom/node-parser.js +1 -0
- package/dist/lib/render/canvas/canvas-renderer.js +190 -19
- package/dist/lib/render/canvas/filter-surface.js +270 -0
- package/dist/lib/render/canvas/surface-bounds.js +64 -0
- package/dist/lib/render/canvas/text-renderer.js +13 -19
- package/dist/lib/render/stacking-context.js +6 -5
- package/dist/types/render/canvas/canvas-renderer.d.ts +8 -1
- package/dist/types/render/canvas/filter-surface.d.ts +17 -0
- package/dist/types/render/canvas/surface-bounds.d.ts +10 -0
- package/dist/types/render/canvas/text-renderer.d.ts +10 -8
- package/dist/types/render/stacking-context.d.ts +1 -1
- package/package.json +4 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* html2canvas-pro 2.4.
|
|
2
|
+
* html2canvas-pro 2.4.4 <https://yorickshan.github.io/html2canvas-pro/>
|
|
3
3
|
* Copyright (c) 2024-present yorickshan and html2canvas-pro contributors
|
|
4
4
|
* Released under MIT License
|
|
5
5
|
*/
|
|
@@ -5324,10 +5324,15 @@ const fontFamily = {
|
|
|
5324
5324
|
parse: (_context, tokens) => {
|
|
5325
5325
|
const accumulator = [];
|
|
5326
5326
|
const results = [];
|
|
5327
|
+
const quoted = [];
|
|
5328
|
+
let isQuoted = false;
|
|
5327
5329
|
tokens.forEach((token) => {
|
|
5328
5330
|
switch (token.type) {
|
|
5329
5331
|
case 20:
|
|
5332
|
+
accumulator.push(token.value);
|
|
5333
|
+
break;
|
|
5330
5334
|
case 0:
|
|
5335
|
+
isQuoted = true;
|
|
5331
5336
|
accumulator.push(token.value);
|
|
5332
5337
|
break;
|
|
5333
5338
|
case 17:
|
|
@@ -5335,12 +5340,17 @@ const fontFamily = {
|
|
|
5335
5340
|
break;
|
|
5336
5341
|
case 4:
|
|
5337
5342
|
results.push(accumulator.join(" "));
|
|
5343
|
+
quoted.push(isQuoted);
|
|
5344
|
+
isQuoted = false;
|
|
5338
5345
|
accumulator.length = 0;
|
|
5339
5346
|
break;
|
|
5340
5347
|
}
|
|
5341
5348
|
});
|
|
5342
|
-
if (accumulator.length)
|
|
5343
|
-
|
|
5349
|
+
if (accumulator.length) {
|
|
5350
|
+
results.push(accumulator.join(" "));
|
|
5351
|
+
quoted.push(isQuoted);
|
|
5352
|
+
}
|
|
5353
|
+
return results.map((result, index) => quoted[index] || result.indexOf(" ") !== -1 ? `'${result.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\a ").replace(/\r/g, "\\d ").replace(/\f/g, "\\c ")}'` : result);
|
|
5344
5354
|
}
|
|
5345
5355
|
};
|
|
5346
5356
|
//#endregion
|
|
@@ -5703,7 +5713,7 @@ const filter = {
|
|
|
5703
5713
|
const renderedArgs = renderFilterArgs(fn.values);
|
|
5704
5714
|
switch (fn.name) {
|
|
5705
5715
|
case "blur":
|
|
5706
|
-
parts.push(`blur(${renderedArgs}
|
|
5716
|
+
parts.push(`blur(${renderedArgs})`);
|
|
5707
5717
|
break;
|
|
5708
5718
|
case "brightness":
|
|
5709
5719
|
case "contrast":
|
|
@@ -5717,7 +5727,7 @@ const filter = {
|
|
|
5717
5727
|
parts.push(`grayscale(${renderedArgs})`);
|
|
5718
5728
|
break;
|
|
5719
5729
|
case "hue-rotate":
|
|
5720
|
-
parts.push(`hue-rotate(${renderedArgs}
|
|
5730
|
+
parts.push(`hue-rotate(${renderedArgs})`);
|
|
5721
5731
|
break;
|
|
5722
5732
|
case "drop-shadow":
|
|
5723
5733
|
parts.push(`drop-shadow(${renderedArgs})`);
|
|
@@ -5735,14 +5745,20 @@ const filter = {
|
|
|
5735
5745
|
const renderFilterArgs = (values) => {
|
|
5736
5746
|
const parts = [];
|
|
5737
5747
|
for (const v of values) {
|
|
5738
|
-
if (v.type === 31)
|
|
5739
|
-
|
|
5748
|
+
if (v.type === 31) {
|
|
5749
|
+
parts.push(" ");
|
|
5750
|
+
continue;
|
|
5751
|
+
}
|
|
5752
|
+
if (v.type === 18) parts.push(`${v.name}(${renderFilterArgs(v.values)})`);
|
|
5753
|
+
else if (v.type === 4) parts.push(",");
|
|
5754
|
+
else if (v.type === 6) parts.push(v.value);
|
|
5755
|
+
else if (v.type === 15) parts.push(`${v.number}${v.unit}`);
|
|
5740
5756
|
else if (v.type === 17) parts.push(`${v.number}`);
|
|
5741
5757
|
else if (v.type === 16) parts.push(`${v.number}%`);
|
|
5742
5758
|
else if (v.type === 20) parts.push(v.value);
|
|
5743
5759
|
else if (v.type === 5) parts.push(`#${v.value}`);
|
|
5744
5760
|
}
|
|
5745
|
-
return parts.join("
|
|
5761
|
+
return parts.join("").trim();
|
|
5746
5762
|
};
|
|
5747
5763
|
//#endregion
|
|
5748
5764
|
//#region src/css/property-descriptors/font-variant-ligatures.ts
|
|
@@ -8292,7 +8308,7 @@ const parseTree = (context, element) => {
|
|
|
8292
8308
|
return container;
|
|
8293
8309
|
};
|
|
8294
8310
|
const createsRealStackingContext = (node, container, root) => {
|
|
8295
|
-
return container.styles.isPositionedWithZIndex() || container.styles.opacity < 1 || container.styles.isTransformed() || isBodyElement(node) && root.styles.isTransparent();
|
|
8311
|
+
return container.styles.isPositionedWithZIndex() || container.styles.opacity < 1 || Boolean(container.styles.filter) || container.styles.isTransformed() || isBodyElement(node) && root.styles.isTransparent();
|
|
8296
8312
|
};
|
|
8297
8313
|
const createsStackingContext = (styles) => {
|
|
8298
8314
|
if (styles.isPositioned() || styles.isFloating()) return true;
|
|
@@ -8729,12 +8745,13 @@ var ElementPaint = class {
|
|
|
8729
8745
|
this.effects.push(new TransformEffect(offsetX, offsetY, zoomMatrix));
|
|
8730
8746
|
}
|
|
8731
8747
|
}
|
|
8732
|
-
getEffects(target) {
|
|
8748
|
+
getEffects(target, surfaceRoot) {
|
|
8733
8749
|
let inFlow = [2, 3].indexOf(this.container.styles.position) === -1;
|
|
8734
|
-
let parent = this.parent;
|
|
8735
|
-
const
|
|
8750
|
+
let parent = this === surfaceRoot ? null : this.parent;
|
|
8751
|
+
const sourceEffects = (paint) => paint.effects.filter((effect) => paint !== surfaceRoot || !isFilterEffect(effect) && !isOpacityEffect(effect));
|
|
8752
|
+
const effects = sourceEffects(this);
|
|
8736
8753
|
while (parent) {
|
|
8737
|
-
const croplessEffects = parent.
|
|
8754
|
+
const croplessEffects = sourceEffects(parent).filter((effect) => !isClipEffect(effect));
|
|
8738
8755
|
if (inFlow || parent.container.styles.position !== 0 || !parent.parent) {
|
|
8739
8756
|
inFlow = [2, 3].indexOf(parent.container.styles.position) === -1;
|
|
8740
8757
|
if (hasOverflowClip(parent.container.styles)) {
|
|
@@ -8744,7 +8761,7 @@ var ElementPaint = class {
|
|
|
8744
8761
|
}
|
|
8745
8762
|
effects.unshift(...croplessEffects);
|
|
8746
8763
|
} else effects.unshift(...croplessEffects);
|
|
8747
|
-
parent = parent.parent;
|
|
8764
|
+
parent = parent === surfaceRoot ? null : parent.parent;
|
|
8748
8765
|
}
|
|
8749
8766
|
return effects.filter((effect) => contains(effect.target, target));
|
|
8750
8767
|
}
|
|
@@ -9025,6 +9042,268 @@ const calculateBackgroundRepeatPath = (repeat, [x, y], [width, height], backgrou
|
|
|
9025
9042
|
}
|
|
9026
9043
|
};
|
|
9027
9044
|
//#endregion
|
|
9045
|
+
//#region src/core/abort-helper.ts
|
|
9046
|
+
/**
|
|
9047
|
+
* AbortSignal helper utilities.
|
|
9048
|
+
*/
|
|
9049
|
+
/**
|
|
9050
|
+
* Throw a DOMException if the given AbortSignal is already aborted.
|
|
9051
|
+
* Used at checkpoints throughout the rendering pipeline to support
|
|
9052
|
+
* cancellable renders.
|
|
9053
|
+
*
|
|
9054
|
+
* @param signal - The AbortSignal to check. If undefined or not aborted, this is a no-op.
|
|
9055
|
+
* @throws {DOMException} With name 'AbortError' if the signal has been aborted.
|
|
9056
|
+
*/
|
|
9057
|
+
const throwIfAborted = (signal) => {
|
|
9058
|
+
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
9059
|
+
};
|
|
9060
|
+
//#endregion
|
|
9061
|
+
//#region src/render/canvas/filter-surface.ts
|
|
9062
|
+
var FilterSurfaceError = class extends Error {};
|
|
9063
|
+
const releaseSurface = (canvas) => {
|
|
9064
|
+
canvas.width = canvas.height = 0;
|
|
9065
|
+
};
|
|
9066
|
+
function parseSimpleFilter(value) {
|
|
9067
|
+
if (!value || value === "none") return { blur: 0 };
|
|
9068
|
+
const effect = new FilterEffect(value);
|
|
9069
|
+
const shadows = value.match(/drop-shadow\(/g) || [];
|
|
9070
|
+
if (shadows.length > 1 || shadows.length === 1 && !effect.shadow) return null;
|
|
9071
|
+
const blur = effect.safeFilterString.match(/^blur\((\d+(?:\.\d+)?)px\)$/);
|
|
9072
|
+
if (effect.safeFilterString && !blur) return null;
|
|
9073
|
+
if (blur && shadows.length && value.indexOf("blur(") > value.indexOf("drop-shadow(")) return null;
|
|
9074
|
+
return {
|
|
9075
|
+
blur: blur ? Number(blur[1]) : 0,
|
|
9076
|
+
shadow: effect.shadow ? {
|
|
9077
|
+
x: effect.shadow.offsetX,
|
|
9078
|
+
y: effect.shadow.offsetY,
|
|
9079
|
+
blur: effect.shadow.blur,
|
|
9080
|
+
color: effect.shadow.color
|
|
9081
|
+
} : void 0
|
|
9082
|
+
};
|
|
9083
|
+
}
|
|
9084
|
+
function filterOutset(filter) {
|
|
9085
|
+
return Math.ceil(3 * (filter.blur + (filter.shadow?.blur ?? 0)) + Math.max(Math.abs(filter.shadow?.x ?? 0), Math.abs(filter.shadow?.y ?? 0)));
|
|
9086
|
+
}
|
|
9087
|
+
function canvasLike(source) {
|
|
9088
|
+
const canvas = document.createElement("canvas");
|
|
9089
|
+
canvas.width = source.width;
|
|
9090
|
+
canvas.height = source.height;
|
|
9091
|
+
return canvas;
|
|
9092
|
+
}
|
|
9093
|
+
const decodeSurface = (image, signal) => new Promise((resolve, reject) => {
|
|
9094
|
+
const finish = (error) => {
|
|
9095
|
+
clearTimeout(timer);
|
|
9096
|
+
signal?.removeEventListener("abort", abort);
|
|
9097
|
+
if (error) reject(error);
|
|
9098
|
+
else resolve();
|
|
9099
|
+
};
|
|
9100
|
+
const abort = () => finish(new DOMException("The operation was aborted.", "AbortError"));
|
|
9101
|
+
const timer = setTimeout(() => finish(new FilterSurfaceError("Filter surface image decode timed out")), 1e4);
|
|
9102
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
9103
|
+
if (signal?.aborted) abort();
|
|
9104
|
+
else image.decode().then(() => finish(), () => finish(new FilterSurfaceError("Filter surface image decode failed")));
|
|
9105
|
+
});
|
|
9106
|
+
const canvasFilterSupport = /* @__PURE__ */ new WeakMap();
|
|
9107
|
+
function supportsNativeFilters(owner = document) {
|
|
9108
|
+
const cached = canvasFilterSupport.get(owner);
|
|
9109
|
+
if (cached !== void 0) return cached;
|
|
9110
|
+
const source = owner.createElement("canvas");
|
|
9111
|
+
const target = owner.createElement("canvas");
|
|
9112
|
+
let supported = false;
|
|
9113
|
+
try {
|
|
9114
|
+
source.width = source.height = target.width = target.height = 32;
|
|
9115
|
+
const input = source.getContext("2d");
|
|
9116
|
+
const context = target.getContext("2d");
|
|
9117
|
+
if (input && context && "filter" in context) {
|
|
9118
|
+
input.fillStyle = "#fff";
|
|
9119
|
+
input.fillRect(8, 8, 8, 8);
|
|
9120
|
+
context.filter = "blur(2px)";
|
|
9121
|
+
context.drawImage(source, 0, 0);
|
|
9122
|
+
const blurred = context.getImageData(6, 12, 1, 1).data[3] > 0;
|
|
9123
|
+
context.clearRect(0, 0, 32, 32);
|
|
9124
|
+
context.filter = "drop-shadow(12px 0px 0px rgba(255, 0, 0, 0.5))";
|
|
9125
|
+
context.drawImage(source, 0, 0);
|
|
9126
|
+
const shadow = context.getImageData(24, 12, 1, 1).data;
|
|
9127
|
+
supported = blurred && shadow[0] > 240 && shadow[1] < 10 && shadow[3] >= 126 && shadow[3] <= 129;
|
|
9128
|
+
}
|
|
9129
|
+
} catch {} finally {
|
|
9130
|
+
releaseSurface(source);
|
|
9131
|
+
releaseSurface(target);
|
|
9132
|
+
}
|
|
9133
|
+
canvasFilterSupport.set(owner, supported);
|
|
9134
|
+
return supported;
|
|
9135
|
+
}
|
|
9136
|
+
function tryNativeFilterSurface(source, { blur, shadow }, opacity, scale, signal) {
|
|
9137
|
+
let filtered;
|
|
9138
|
+
let output;
|
|
9139
|
+
let complete = false;
|
|
9140
|
+
try {
|
|
9141
|
+
throwIfAborted(signal);
|
|
9142
|
+
filtered = canvasLike(source);
|
|
9143
|
+
const context = filtered.getContext("2d");
|
|
9144
|
+
if (!context || !("filter" in context)) return null;
|
|
9145
|
+
const functions = [];
|
|
9146
|
+
if (blur) functions.push(`blur(${blur * scale}px)`);
|
|
9147
|
+
if (shadow) functions.push(`drop-shadow(${shadow.x * scale}px ${shadow.y * scale}px ${shadow.blur * scale}px ${shadow.color})`);
|
|
9148
|
+
context.filter = functions.join(" ");
|
|
9149
|
+
if (context.filter === "none") return null;
|
|
9150
|
+
context.drawImage(source, 0, 0);
|
|
9151
|
+
throwIfAborted(signal);
|
|
9152
|
+
output = canvasLike(source);
|
|
9153
|
+
const composite = output.getContext("2d");
|
|
9154
|
+
if (!composite) return null;
|
|
9155
|
+
composite.globalAlpha = opacity;
|
|
9156
|
+
composite.drawImage(filtered, 0, 0);
|
|
9157
|
+
throwIfAborted(signal);
|
|
9158
|
+
complete = true;
|
|
9159
|
+
return output;
|
|
9160
|
+
} catch {
|
|
9161
|
+
throwIfAborted(signal);
|
|
9162
|
+
return null;
|
|
9163
|
+
} finally {
|
|
9164
|
+
if (filtered) releaseSurface(filtered);
|
|
9165
|
+
if (output && !complete) releaseSurface(output);
|
|
9166
|
+
}
|
|
9167
|
+
}
|
|
9168
|
+
async function renderFilterSurface(source, filter, opacity, scale, signal) {
|
|
9169
|
+
throwIfAborted(signal);
|
|
9170
|
+
if ((filter.blur || filter.shadow) && supportsNativeFilters()) {
|
|
9171
|
+
const output = tryNativeFilterSurface(source, filter, opacity, scale, signal);
|
|
9172
|
+
if (output) return output;
|
|
9173
|
+
}
|
|
9174
|
+
return renderSvgFilterSurface(source, filter, opacity, scale, signal);
|
|
9175
|
+
}
|
|
9176
|
+
async function renderSvgFilterSurface(source, { blur, shadow }, opacity, scale, signal) {
|
|
9177
|
+
throwIfAborted(signal);
|
|
9178
|
+
if (!blur && !shadow) {
|
|
9179
|
+
const output = canvasLike(source);
|
|
9180
|
+
const context = output.getContext("2d");
|
|
9181
|
+
if (!context) {
|
|
9182
|
+
releaseSurface(output);
|
|
9183
|
+
throw new FilterSurfaceError("Filter surface canvas is unavailable");
|
|
9184
|
+
}
|
|
9185
|
+
context.globalAlpha = opacity;
|
|
9186
|
+
try {
|
|
9187
|
+
context.drawImage(source, 0, 0);
|
|
9188
|
+
return output;
|
|
9189
|
+
} catch {
|
|
9190
|
+
releaseSurface(output);
|
|
9191
|
+
throw new FilterSurfaceError("Filter surface image cannot be drawn");
|
|
9192
|
+
}
|
|
9193
|
+
}
|
|
9194
|
+
const namespace = "http://www.w3.org/2000/svg";
|
|
9195
|
+
const element = (tag, attributes = {}) => {
|
|
9196
|
+
const node = document.createElementNS(namespace, tag);
|
|
9197
|
+
Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, String(value)));
|
|
9198
|
+
return node;
|
|
9199
|
+
};
|
|
9200
|
+
const svg = element("svg", {
|
|
9201
|
+
width: source.width,
|
|
9202
|
+
height: source.height,
|
|
9203
|
+
viewBox: `0 0 ${source.width} ${source.height}`
|
|
9204
|
+
});
|
|
9205
|
+
const defs = element("defs");
|
|
9206
|
+
const filter = element("filter", {
|
|
9207
|
+
id: "composited-filter",
|
|
9208
|
+
filterUnits: "userSpaceOnUse",
|
|
9209
|
+
x: 0,
|
|
9210
|
+
y: 0,
|
|
9211
|
+
width: source.width,
|
|
9212
|
+
height: source.height,
|
|
9213
|
+
"color-interpolation-filters": "sRGB"
|
|
9214
|
+
});
|
|
9215
|
+
if (blur) filter.appendChild(element("feGaussianBlur", {
|
|
9216
|
+
stdDeviation: blur * scale,
|
|
9217
|
+
result: "blurred-source"
|
|
9218
|
+
}));
|
|
9219
|
+
if (shadow) {
|
|
9220
|
+
filter.appendChild(element("feGaussianBlur", {
|
|
9221
|
+
in: blur ? "blurred-source" : "SourceAlpha",
|
|
9222
|
+
stdDeviation: shadow.blur * scale
|
|
9223
|
+
}));
|
|
9224
|
+
filter.appendChild(element("feOffset", {
|
|
9225
|
+
dx: shadow.x * scale,
|
|
9226
|
+
dy: shadow.y * scale,
|
|
9227
|
+
result: "offset-shadow"
|
|
9228
|
+
}));
|
|
9229
|
+
filter.appendChild(element("feFlood", { "flood-color": shadow.color }));
|
|
9230
|
+
filter.appendChild(element("feComposite", {
|
|
9231
|
+
in2: "offset-shadow",
|
|
9232
|
+
operator: "in"
|
|
9233
|
+
}));
|
|
9234
|
+
const merge = element("feMerge");
|
|
9235
|
+
merge.appendChild(element("feMergeNode"));
|
|
9236
|
+
merge.appendChild(element("feMergeNode", { in: blur ? "blurred-source" : "SourceGraphic" }));
|
|
9237
|
+
filter.appendChild(merge);
|
|
9238
|
+
}
|
|
9239
|
+
defs.appendChild(filter);
|
|
9240
|
+
svg.appendChild(defs);
|
|
9241
|
+
let sourceUrl;
|
|
9242
|
+
try {
|
|
9243
|
+
sourceUrl = source.toDataURL("image/png");
|
|
9244
|
+
if (sourceUrl === "data:,") throw new Error("Canvas size is unsupported");
|
|
9245
|
+
} catch {
|
|
9246
|
+
throw new FilterSurfaceError("Filter surface pixels cannot be serialized");
|
|
9247
|
+
}
|
|
9248
|
+
svg.appendChild(element("image", {
|
|
9249
|
+
width: source.width,
|
|
9250
|
+
height: source.height,
|
|
9251
|
+
href: sourceUrl,
|
|
9252
|
+
filter: "url(#composited-filter)"
|
|
9253
|
+
}));
|
|
9254
|
+
const image = new Image();
|
|
9255
|
+
let output;
|
|
9256
|
+
try {
|
|
9257
|
+
image.src = `data:image/svg+xml,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
|
|
9258
|
+
await decodeSurface(image, signal);
|
|
9259
|
+
throwIfAborted(signal);
|
|
9260
|
+
output = canvasLike(source);
|
|
9261
|
+
const context = output.getContext("2d");
|
|
9262
|
+
if (!context) throw new FilterSurfaceError("Filter surface canvas is unavailable");
|
|
9263
|
+
context.globalAlpha = opacity;
|
|
9264
|
+
context.drawImage(image, 0, 0);
|
|
9265
|
+
return output;
|
|
9266
|
+
} catch (error) {
|
|
9267
|
+
if (output) releaseSurface(output);
|
|
9268
|
+
if (signal?.aborted) throwIfAborted(signal);
|
|
9269
|
+
throw error instanceof FilterSurfaceError ? error : new FilterSurfaceError("Filter surface image cannot be drawn");
|
|
9270
|
+
} finally {
|
|
9271
|
+
image.removeAttribute("src");
|
|
9272
|
+
}
|
|
9273
|
+
}
|
|
9274
|
+
function reserveSurface(budget, width, height) {
|
|
9275
|
+
const pixels = width * height * 4;
|
|
9276
|
+
if (!Number.isFinite(pixels) || width <= 0 || height <= 0 || width > 8192 || height > 8192 || budget.pixels + pixels > 16777216) return 0;
|
|
9277
|
+
budget.pixels += pixels;
|
|
9278
|
+
return pixels;
|
|
9279
|
+
}
|
|
9280
|
+
const expand = (b, margin) => b.add(-margin, -margin, margin * 2, margin * 2);
|
|
9281
|
+
const union = (a, b) => new Bounds(Math.min(a.left, b.left), Math.min(a.top, b.top), Math.max(a.left + a.width, b.left + b.width) - Math.min(a.left, b.left), Math.max(a.top + a.height, b.top + b.height) - Math.min(a.top, b.top));
|
|
9282
|
+
function surfaceBounds(container) {
|
|
9283
|
+
const styles = container.styles;
|
|
9284
|
+
let bounds = container.bounds;
|
|
9285
|
+
for (const shadow of styles.boxShadow) if (!shadow.inset) {
|
|
9286
|
+
const spread = Math.max(0, shadow.spread.number) + 3 * shadow.blur.number;
|
|
9287
|
+
bounds = union(bounds, expand(container.bounds.add(shadow.offsetX.number, shadow.offsetY.number, 0, 0), spread));
|
|
9288
|
+
}
|
|
9289
|
+
for (const text of container.textNodes) for (const part of text.textBounds) {
|
|
9290
|
+
const ink = expand(part.bounds, 2 * getAbsoluteValue(styles.fontSize, container.bounds.height) + styles.webkitTextStrokeWidth);
|
|
9291
|
+
bounds = union(bounds, ink);
|
|
9292
|
+
for (const shadow of styles.textShadow) bounds = union(bounds, expand(ink.add(shadow.offsetX.number, shadow.offsetY.number, 0, 0), 3 * shadow.blur.number));
|
|
9293
|
+
}
|
|
9294
|
+
for (const child of container.elements) if (child.styles.isVisible()) bounds = union(bounds, surfaceBounds(child));
|
|
9295
|
+
const filter = parseSimpleFilter(styles.filter);
|
|
9296
|
+
return expand(bounds, filter ? filterOutset(filter) : 0);
|
|
9297
|
+
}
|
|
9298
|
+
function cropSurface(bounds, viewport, margin, scale) {
|
|
9299
|
+
const area = expand(viewport, margin);
|
|
9300
|
+
const left = Math.floor(Math.max(bounds.left, area.left) * scale) / scale;
|
|
9301
|
+
const top = Math.floor(Math.max(bounds.top, area.top) * scale) / scale;
|
|
9302
|
+
const right = Math.ceil(Math.min(bounds.left + bounds.width, area.left + area.width) * scale) / scale;
|
|
9303
|
+
const bottom = Math.ceil(Math.min(bounds.top + bounds.height, area.top + area.height) * scale) / scale;
|
|
9304
|
+
return new Bounds(left, top, Math.max(0, right - left), Math.max(0, bottom - top));
|
|
9305
|
+
}
|
|
9306
|
+
//#endregion
|
|
9028
9307
|
//#region src/render/canvas/text/text-decoration-renderer.ts
|
|
9029
9308
|
var TextDecorationRenderer = class {
|
|
9030
9309
|
constructor(ctx) {
|
|
@@ -9154,8 +9433,8 @@ const iOSBrokenFonts = ["-apple-system", "system-ui"];
|
|
|
9154
9433
|
const TEXT_OVERFLOW_EPSILON = 1;
|
|
9155
9434
|
/**
|
|
9156
9435
|
* Detect CJK (Chinese, Japanese, Korean) characters in a string.
|
|
9157
|
-
*
|
|
9158
|
-
*
|
|
9436
|
+
* Vertical writing modes use it to keep CJK glyphs upright and on the
|
|
9437
|
+
* ideographic baseline while other scripts are rotated.
|
|
9159
9438
|
*
|
|
9160
9439
|
* Covers:
|
|
9161
9440
|
* U+2E80–U+2FFF CJK Radicals Supplement, Kangxi Radicals
|
|
@@ -9225,15 +9504,17 @@ var TextRenderer = class {
|
|
|
9225
9504
|
return w;
|
|
9226
9505
|
}
|
|
9227
9506
|
/**
|
|
9228
|
-
* Iterate grapheme clusters one-by-one, applying correct letter-spacing
|
|
9229
|
-
* per-script baseline for each character.
|
|
9507
|
+
* Iterate grapheme clusters one-by-one, applying correct letter-spacing.
|
|
9230
9508
|
*
|
|
9231
9509
|
* Issue #73: When letter-spacing is non-zero, text must be rendered character by
|
|
9232
|
-
* character
|
|
9233
|
-
*
|
|
9234
|
-
*
|
|
9235
|
-
*
|
|
9236
|
-
*
|
|
9510
|
+
* character, adding `letterSpacing` to each character's advance width.
|
|
9511
|
+
*
|
|
9512
|
+
* In horizontal text every glyph keeps the alphabetic baseline, CJK included:
|
|
9513
|
+
* inline layout aligns a whole line on its alphabetic baseline regardless of
|
|
9514
|
+
* script, and `baseline` was measured for that. Drawing CJK graphemes at the
|
|
9515
|
+
* same y with `textBaseline = 'ideographic'` lifts them above the Latin text
|
|
9516
|
+
* next to them, and away from where the `letterSpacing === 0` whole-string
|
|
9517
|
+
* path draws them.
|
|
9237
9518
|
*
|
|
9238
9519
|
* The `renderFn` callback receives (letter, x, y) and performs the actual draw
|
|
9239
9520
|
* call (fillText or strokeText), allowing fill and stroke paths to share one
|
|
@@ -9249,12 +9530,7 @@ var TextRenderer = class {
|
|
|
9249
9530
|
const fontString = this.ctx.font;
|
|
9250
9531
|
let left = text.bounds.left;
|
|
9251
9532
|
for (const letter of letters) {
|
|
9252
|
-
|
|
9253
|
-
const savedBaseline = this.ctx.textBaseline;
|
|
9254
|
-
this.ctx.textBaseline = "ideographic";
|
|
9255
|
-
renderFn(letter, left, y);
|
|
9256
|
-
this.ctx.textBaseline = savedBaseline;
|
|
9257
|
-
} else renderFn(letter, left, y);
|
|
9533
|
+
renderFn(letter, left, y);
|
|
9258
9534
|
left += this.cachedMeasureText(letter, fontString) + letterSpacing;
|
|
9259
9535
|
}
|
|
9260
9536
|
}
|
|
@@ -9296,7 +9572,7 @@ var TextRenderer = class {
|
|
|
9296
9572
|
/**
|
|
9297
9573
|
* Render text with letter-spacing applied (fill pass).
|
|
9298
9574
|
* When letterSpacing is 0 the whole string is drawn in one call; otherwise each
|
|
9299
|
-
* grapheme is drawn individually so spacing
|
|
9575
|
+
* grapheme is drawn individually so spacing is applied correctly.
|
|
9300
9576
|
*/
|
|
9301
9577
|
renderTextWithLetterSpacing(text, letterSpacing, baseline, writingMode = 0) {
|
|
9302
9578
|
this.renderFillText(text, letterSpacing, baseline, writingMode);
|
|
@@ -10531,12 +10807,19 @@ const canvasTextAlign = (textAlign) => {
|
|
|
10531
10807
|
//#endregion
|
|
10532
10808
|
//#region src/render/canvas/canvas-renderer.ts
|
|
10533
10809
|
var CanvasRenderer = class CanvasRenderer {
|
|
10534
|
-
constructor(context, options) {
|
|
10810
|
+
constructor(context, options, surfaceRoot, surfaceBudget = { pixels: 0 }) {
|
|
10811
|
+
this.surfaceRoot = surfaceRoot;
|
|
10812
|
+
this.surfaceBudget = surfaceBudget;
|
|
10813
|
+
this.legacySubtree = false;
|
|
10535
10814
|
this.context = context;
|
|
10536
10815
|
this.options = options;
|
|
10537
10816
|
this.canvas = options.canvas ? options.canvas : document.createElement("canvas");
|
|
10538
10817
|
const ctx = this.canvas.getContext("2d");
|
|
10539
|
-
if (!ctx)
|
|
10818
|
+
if (!ctx) {
|
|
10819
|
+
if (!options.canvas) releaseSurface(this.canvas);
|
|
10820
|
+
if (surfaceRoot) throw new FilterSurfaceError("Filter surface canvas is unavailable");
|
|
10821
|
+
throw new Error("Failed to get 2D rendering context from canvas");
|
|
10822
|
+
}
|
|
10540
10823
|
this.ctx = ctx;
|
|
10541
10824
|
if (!options.canvas) {
|
|
10542
10825
|
this.canvas.width = Math.floor(options.width * options.scale);
|
|
@@ -10572,7 +10855,77 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10572
10855
|
this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
|
|
10573
10856
|
}
|
|
10574
10857
|
async renderStack(stack) {
|
|
10575
|
-
|
|
10858
|
+
const styles = stack.element.container.styles;
|
|
10859
|
+
if (styles.isVisible()) {
|
|
10860
|
+
const filter = parseSimpleFilter(styles.filter);
|
|
10861
|
+
if (stack.element !== this.surfaceRoot && !this.legacySubtree && filter && (styles.filter || styles.opacity < 1) && this.canComposite(stack.element)) {
|
|
10862
|
+
if (!await this.renderCompositedStack(stack, filter)) {
|
|
10863
|
+
this.legacySubtree = true;
|
|
10864
|
+
try {
|
|
10865
|
+
await this.renderStackContent(stack);
|
|
10866
|
+
} finally {
|
|
10867
|
+
this.legacySubtree = false;
|
|
10868
|
+
}
|
|
10869
|
+
}
|
|
10870
|
+
return;
|
|
10871
|
+
}
|
|
10872
|
+
await this.renderStackContent(stack);
|
|
10873
|
+
}
|
|
10874
|
+
}
|
|
10875
|
+
canComposite(paint) {
|
|
10876
|
+
const supported = (container) => {
|
|
10877
|
+
const styles = container.styles;
|
|
10878
|
+
return !styles.isTransformed() && styles.zoom === 1 && styles.mixBlendMode === MIX_BLEND_MODE$1.NORMAL && styles.clipPath.type === 0 && !contains(styles.display, 2048) && parseSimpleFilter(styles.filter) !== null;
|
|
10879
|
+
};
|
|
10880
|
+
const subtree = (container) => supported(container) && container.elements.every(subtree);
|
|
10881
|
+
let ancestor = paint.parent;
|
|
10882
|
+
while (ancestor && ancestor !== this.surfaceRoot) {
|
|
10883
|
+
if (!supported(ancestor.container)) return false;
|
|
10884
|
+
ancestor = ancestor.parent;
|
|
10885
|
+
}
|
|
10886
|
+
return subtree(paint.container);
|
|
10887
|
+
}
|
|
10888
|
+
async renderCompositedStack(stack, filter) {
|
|
10889
|
+
const signal = this.options.signal;
|
|
10890
|
+
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
10891
|
+
const bounds = cropSurface(surfaceBounds(stack.element.container), new Bounds(this.options.x, this.options.y, this.options.width, this.options.height), filterOutset(filter), this.options.scale);
|
|
10892
|
+
if (!bounds.width || !bounds.height) return true;
|
|
10893
|
+
const reserved = reserveSurface(this.surfaceBudget, Math.ceil(bounds.width * this.options.scale), Math.ceil(bounds.height * this.options.scale));
|
|
10894
|
+
if (!reserved) {
|
|
10895
|
+
this.context.logger.info("Filter surface budget exceeded; using the existing renderer");
|
|
10896
|
+
return false;
|
|
10897
|
+
}
|
|
10898
|
+
const options = {
|
|
10899
|
+
...this.options,
|
|
10900
|
+
canvas: void 0,
|
|
10901
|
+
backgroundColor: null,
|
|
10902
|
+
x: bounds.left,
|
|
10903
|
+
y: bounds.top,
|
|
10904
|
+
width: bounds.width,
|
|
10905
|
+
height: bounds.height
|
|
10906
|
+
};
|
|
10907
|
+
let source;
|
|
10908
|
+
let filtered;
|
|
10909
|
+
try {
|
|
10910
|
+
source = new CanvasRenderer(this.context, options, stack.element, this.surfaceBudget);
|
|
10911
|
+
await source.renderStackContent(stack);
|
|
10912
|
+
source.effectsRenderer.applyEffects([]);
|
|
10913
|
+
filtered = await renderFilterSurface(source.canvas, filter, stack.element.container.styles.opacity, options.scale, signal);
|
|
10914
|
+
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
10915
|
+
const own = stack.element.effects;
|
|
10916
|
+
const effects = stack.element.getEffects(4, this.surfaceRoot).filter((effect) => !own.includes(effect) || !isFilterEffect(effect) && !isOpacityEffect(effect) && !isClipEffect(effect));
|
|
10917
|
+
this.effectsRenderer.applyEffects(effects);
|
|
10918
|
+
this.ctx.drawImage(filtered, options.x, options.y, filtered.width / options.scale, filtered.height / options.scale);
|
|
10919
|
+
return true;
|
|
10920
|
+
} catch (error) {
|
|
10921
|
+
if (!(error instanceof FilterSurfaceError)) throw error;
|
|
10922
|
+
this.context.logger.info(`${error.message}; using the existing renderer`);
|
|
10923
|
+
return false;
|
|
10924
|
+
} finally {
|
|
10925
|
+
if (source) releaseSurface(source.canvas);
|
|
10926
|
+
if (filtered) releaseSurface(filtered);
|
|
10927
|
+
this.surfaceBudget.pixels -= reserved;
|
|
10928
|
+
}
|
|
10576
10929
|
}
|
|
10577
10930
|
async renderNode(paint) {
|
|
10578
10931
|
if (paint.container.debugRender) debugger;
|
|
@@ -10600,7 +10953,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10600
10953
|
}
|
|
10601
10954
|
}
|
|
10602
10955
|
async renderNodeContent(paint) {
|
|
10603
|
-
this.effectsRenderer.applyEffects(paint.getEffects(4));
|
|
10956
|
+
this.effectsRenderer.applyEffects(paint.getEffects(4, this.surfaceRoot));
|
|
10604
10957
|
const container = paint.container;
|
|
10605
10958
|
const curves = paint.curves;
|
|
10606
10959
|
const styles = container.styles;
|
|
@@ -10664,8 +11017,51 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10664
11017
|
formatPath(paths) {
|
|
10665
11018
|
formatCanvasPath(this.ctx, paths);
|
|
10666
11019
|
}
|
|
11020
|
+
async renderSurfaceBoxShadow(paint, shadow) {
|
|
11021
|
+
const spread = shadow.spread.number;
|
|
11022
|
+
const blur = shadow.blur.number / 2;
|
|
11023
|
+
const margin = Math.ceil(3 * blur) + 1;
|
|
11024
|
+
const box = paint.container.bounds.add(-spread, -spread, 2 * spread, 2 * spread);
|
|
11025
|
+
if (box.width <= 0 || box.height <= 0) return;
|
|
11026
|
+
const scale = this.options.scale;
|
|
11027
|
+
const bounds = cropSurface(box.add(-margin, -margin, 2 * margin, 2 * margin), new Bounds(this.options.x - shadow.offsetX.number, this.options.y - shadow.offsetY.number, this.options.width, this.options.height), margin, scale);
|
|
11028
|
+
if (!bounds.width || !bounds.height) return;
|
|
11029
|
+
const width = Math.ceil(bounds.width * scale), height = Math.ceil(bounds.height * scale);
|
|
11030
|
+
const reserved = reserveSurface(this.surfaceBudget, width, height);
|
|
11031
|
+
if (!reserved) throw new FilterSurfaceError("Box shadow surface budget exceeded");
|
|
11032
|
+
let source;
|
|
11033
|
+
let filtered;
|
|
11034
|
+
try {
|
|
11035
|
+
source = document.createElement("canvas");
|
|
11036
|
+
source.width = width;
|
|
11037
|
+
source.height = height;
|
|
11038
|
+
const ctx = source.getContext("2d");
|
|
11039
|
+
if (!ctx) throw new FilterSurfaceError("Box shadow surface canvas is unavailable");
|
|
11040
|
+
ctx.scale(scale, scale);
|
|
11041
|
+
ctx.translate(-bounds.left, -bounds.top);
|
|
11042
|
+
createCanvasPath(ctx, transformPath(calculateBorderBoxPath(paint.curves), -spread, -spread, 2 * spread, 2 * spread));
|
|
11043
|
+
ctx.fillStyle = asString(shadow.color);
|
|
11044
|
+
ctx.fill();
|
|
11045
|
+
filtered = await renderFilterSurface(source, { blur }, 1, scale, this.options.signal);
|
|
11046
|
+
this.ctx.save();
|
|
11047
|
+
try {
|
|
11048
|
+
this.ctx.beginPath();
|
|
11049
|
+
this.ctx.rect(this.options.x, this.options.y, this.options.width, this.options.height);
|
|
11050
|
+
formatCanvasPath(this.ctx, calculateBorderBoxPath(paint.curves));
|
|
11051
|
+
this.ctx.closePath();
|
|
11052
|
+
this.ctx.clip("evenodd");
|
|
11053
|
+
this.ctx.drawImage(filtered, bounds.left + shadow.offsetX.number, bounds.top + shadow.offsetY.number, filtered.width / scale, filtered.height / scale);
|
|
11054
|
+
} finally {
|
|
11055
|
+
this.ctx.restore();
|
|
11056
|
+
}
|
|
11057
|
+
} finally {
|
|
11058
|
+
if (source) releaseSurface(source);
|
|
11059
|
+
if (filtered) releaseSurface(filtered);
|
|
11060
|
+
this.surfaceBudget.pixels -= reserved;
|
|
11061
|
+
}
|
|
11062
|
+
}
|
|
10667
11063
|
async renderNodeBackgroundAndBorders(paint) {
|
|
10668
|
-
this.effectsRenderer.applyEffects(paint.getEffects(2));
|
|
11064
|
+
this.effectsRenderer.applyEffects(paint.getEffects(2, this.surfaceRoot));
|
|
10669
11065
|
const styles = paint.container.styles;
|
|
10670
11066
|
const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
|
|
10671
11067
|
const hasTextClip = hasTextBackgroundClip(styles);
|
|
@@ -10706,7 +11102,11 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10706
11102
|
await this.backgroundRenderer.renderBackgroundImage(paint.container);
|
|
10707
11103
|
this.ctx.restore();
|
|
10708
11104
|
}
|
|
10709
|
-
styles.boxShadow.slice(0).reverse()
|
|
11105
|
+
for (const shadow of styles.boxShadow.slice(0).reverse()) {
|
|
11106
|
+
if (this.surfaceRoot && !shadow.inset) {
|
|
11107
|
+
await this.renderSurfaceBoxShadow(paint, shadow);
|
|
11108
|
+
continue;
|
|
11109
|
+
}
|
|
10710
11110
|
this.ctx.save();
|
|
10711
11111
|
const borderBoxArea = calculateBorderBoxPath(paint.curves);
|
|
10712
11112
|
const maskOffset = shadow.inset ? 0 : SHADOW_MASK_OFFSET;
|
|
@@ -10720,14 +11120,15 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10720
11120
|
this.ctx.clip();
|
|
10721
11121
|
this.path(shadowPaintingArea);
|
|
10722
11122
|
}
|
|
10723
|
-
this.
|
|
10724
|
-
this.ctx.
|
|
11123
|
+
const shadowScale = this.surfaceRoot ? this.options.scale : 1;
|
|
11124
|
+
this.ctx.shadowOffsetX = (shadow.offsetX.number + maskOffset) * shadowScale;
|
|
11125
|
+
this.ctx.shadowOffsetY = shadow.offsetY.number * shadowScale;
|
|
10725
11126
|
this.ctx.shadowColor = asString(shadow.color);
|
|
10726
|
-
this.ctx.shadowBlur = shadow.blur.number;
|
|
11127
|
+
this.ctx.shadowBlur = shadow.blur.number * shadowScale;
|
|
10727
11128
|
this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : "rgba(0,0,0,1)";
|
|
10728
11129
|
this.ctx.fill();
|
|
10729
11130
|
this.ctx.restore();
|
|
10730
|
-
}
|
|
11131
|
+
}
|
|
10731
11132
|
}
|
|
10732
11133
|
if (styles.borderImageSource) {
|
|
10733
11134
|
const source = styles.borderImageSource;
|
|
@@ -10783,14 +11184,19 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10783
11184
|
}
|
|
10784
11185
|
}
|
|
10785
11186
|
async render(element) {
|
|
10786
|
-
|
|
10787
|
-
|
|
10788
|
-
|
|
11187
|
+
try {
|
|
11188
|
+
if (this.options.backgroundColor) {
|
|
11189
|
+
this.ctx.fillStyle = asString(this.options.backgroundColor);
|
|
11190
|
+
this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
|
|
11191
|
+
}
|
|
11192
|
+
const stack = parseStackingContexts(element);
|
|
11193
|
+
await this.renderStack(stack);
|
|
11194
|
+
this.effectsRenderer.applyEffects([]);
|
|
11195
|
+
return this.canvas;
|
|
11196
|
+
} catch (error) {
|
|
11197
|
+
if (!this.options.canvas) releaseSurface(this.canvas);
|
|
11198
|
+
throw error;
|
|
10789
11199
|
}
|
|
10790
|
-
const stack = parseStackingContexts(element);
|
|
10791
|
-
await this.renderStack(stack);
|
|
10792
|
-
this.effectsRenderer.applyEffects([]);
|
|
10793
|
-
return this.canvas;
|
|
10794
11200
|
}
|
|
10795
11201
|
/**
|
|
10796
11202
|
* Render background clipped to text shape using offscreen canvas with destination-in compositing.
|
|
@@ -11206,22 +11612,6 @@ var Context = class Context {
|
|
|
11206
11612
|
}
|
|
11207
11613
|
};
|
|
11208
11614
|
//#endregion
|
|
11209
|
-
//#region src/core/abort-helper.ts
|
|
11210
|
-
/**
|
|
11211
|
-
* AbortSignal helper utilities.
|
|
11212
|
-
*/
|
|
11213
|
-
/**
|
|
11214
|
-
* Throw a DOMException if the given AbortSignal is already aborted.
|
|
11215
|
-
* Used at checkpoints throughout the rendering pipeline to support
|
|
11216
|
-
* cancellable renders.
|
|
11217
|
-
*
|
|
11218
|
-
* @param signal - The AbortSignal to check. If undefined or not aborted, this is a no-op.
|
|
11219
|
-
* @throws {DOMException} With name 'AbortError' if the signal has been aborted.
|
|
11220
|
-
*/
|
|
11221
|
-
const throwIfAborted = (signal) => {
|
|
11222
|
-
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
11223
|
-
};
|
|
11224
|
-
//#endregion
|
|
11225
11615
|
//#region src/core/config-assembler.ts
|
|
11226
11616
|
/**
|
|
11227
11617
|
* Coerce known numeric options from string (or other) values to actual numbers.
|
|
@@ -11390,18 +11780,19 @@ const renderElement = async (element, opts, config) => {
|
|
|
11390
11780
|
canvas = await new CanvasRenderer(context, renderOptions).render(root);
|
|
11391
11781
|
perfMonitor.end("render");
|
|
11392
11782
|
}
|
|
11783
|
+
} finally {
|
|
11393
11784
|
perfMonitor.start("cleanup");
|
|
11394
|
-
|
|
11395
|
-
if (
|
|
11785
|
+
try {
|
|
11786
|
+
if (root) root.restoreTree();
|
|
11787
|
+
} finally {
|
|
11788
|
+
if (opts.removeContainer ?? true) DocumentCloner.destroy(container);
|
|
11789
|
+
perfMonitor.end("cleanup");
|
|
11396
11790
|
}
|
|
11397
|
-
perfMonitor.end("cleanup");
|
|
11398
|
-
perfMonitor.end("total");
|
|
11399
|
-
context.logger.debug(`Finished rendering`);
|
|
11400
|
-
if (performanceMonitoring) perfMonitor.logSummary();
|
|
11401
|
-
return canvas;
|
|
11402
|
-
} finally {
|
|
11403
|
-
if (root) root.restoreTree();
|
|
11404
11791
|
}
|
|
11792
|
+
perfMonitor.end("total");
|
|
11793
|
+
context.logger.debug(`Finished rendering`);
|
|
11794
|
+
if (performanceMonitoring) perfMonitor.logSummary();
|
|
11795
|
+
return canvas;
|
|
11405
11796
|
};
|
|
11406
11797
|
//#endregion
|
|
11407
11798
|
//#region src/index.ts
|