html2canvas-pro 2.5.2 → 2.5.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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * html2canvas-pro 2.5.2 <https://yorickshan.github.io/html2canvas-pro/>
2
+ * html2canvas-pro 2.5.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
  */
@@ -6521,6 +6521,15 @@ const SHADOW_MASK_OFFSET = 1e4;
6521
6521
  */
6522
6522
  const CLONE_READY_TIMEOUT_MS = 3e4;
6523
6523
  /**
6524
+ * Total time (ms) to wait for the clone's external stylesheets (link and
6525
+ * @import chains) to apply. Shorter than the document readiness window: a
6526
+ * stylesheet that needs longer usually means a hung CDN, and rendering with
6527
+ * a 10s-old layout beats blocking the capture for 30s. A missed deadline
6528
+ * resolves with a warning (the capture continues with a possibly unstyled
6529
+ * clone).
6530
+ */
6531
+ const STYLESHEET_READY_TIMEOUT_MS = 1e4;
6532
+ /**
6524
6533
  * Hard ceiling (ms) for the cloned iframe's load event itself. If the event
6525
6534
  * never fires, the mount is rejected instead of hanging forever.
6526
6535
  */
@@ -8033,6 +8042,129 @@ const imageReady = (img) => {
8033
8042
  const imagesReady = (document) => {
8034
8043
  return Promise.all([].slice.call(document.images, 0).map(imageReady));
8035
8044
  };
8045
+ /**
8046
+ * Whether every CSS rule of the sheet (and of the @import chains it pulls
8047
+ * in) has been applied. A sheet is "attached" as soon as its rules are
8048
+ * readable; an external @import inside it may still be loading, which shows
8049
+ * up as an inaccessible CSSImportRule.sheet. `cssRules` can throw for
8050
+ * cross-origin sheets without a CORS-approved response — those rules cannot
8051
+ * be inspected, so the sheet counts as ready rather than blocking the render
8052
+ * forever.
8053
+ */
8054
+ const isSheetFullyApplied = (sheet, depth = 0) => {
8055
+ if (!sheet || depth > MAX_IMPORT_DEPTH) return true;
8056
+ let rules;
8057
+ try {
8058
+ rules = sheet.cssRules;
8059
+ } catch (_e) {
8060
+ return true;
8061
+ }
8062
+ if (!rules) return true;
8063
+ for (let i = 0; i < rules.length; i++) {
8064
+ const rule = rules[i];
8065
+ if (rule && typeof CSSImportRule !== "undefined" && rule instanceof CSSImportRule) {
8066
+ if (!rule.styleSheet || !isSheetFullyApplied(rule.styleSheet, depth + 1)) return false;
8067
+ }
8068
+ }
8069
+ return true;
8070
+ };
8071
+ const MAX_IMPORT_DEPTH = 32;
8072
+ /** Collect every <link> in the document, including links inside shadow roots. */
8073
+ const collectLinks = (root, out) => {
8074
+ const links = root.querySelectorAll("link");
8075
+ for (let i = 0; i < links.length; i++) {
8076
+ const link = links[i];
8077
+ if (link) out.push(link);
8078
+ }
8079
+ const walker = (root.ownerDocument ?? root).createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
8080
+ let current = walker.nextNode();
8081
+ while (current) {
8082
+ const shadowRoot = current.shadowRoot;
8083
+ if (shadowRoot) collectLinks(shadowRoot, out);
8084
+ current = walker.nextNode();
8085
+ }
8086
+ };
8087
+ /**
8088
+ * Wait until every external stylesheet of the clone has been applied or
8089
+ * definitively failed, or the readiness budget expires.
8090
+ *
8091
+ * The clone is mounted by adoptNode() into an already-closed document
8092
+ * (close() must precede adoptNode for Chrome uppercase class-name matching,
8093
+ * see the mount sequence below), so the cloned <link rel="stylesheet">
8094
+ * elements are inserted after the document finished loading: they neither
8095
+ * delay the iframe load event nor change readyState, and the iframeLoader
8096
+ * readiness poll cannot observe them. On a cold cache the first capture
8097
+ * could then measure the clone before its external CSS arrived and render
8098
+ * an unstyled layout (issue #245). Inline <style> clones apply
8099
+ * synchronously and document-level adopted stylesheets are serialised at
8100
+ * clone time, so neither needs this wait.
8101
+ *
8102
+ * Readiness is polled rather than purely event-driven because a <style>
8103
+ * clone serialised from `createStyleClone` re-fetches its external @import
8104
+ * rules without firing link load/error events, and shadow-root links are
8105
+ * equally event-hostile in some engines. The poll covers both; link events
8106
+ * remain as the settle path for plain <link> fetches.
8107
+ * @internal – exported for testing only.
8108
+ */
8109
+ const stylesheetsReady = (documentClone, logger, signal) => {
8110
+ if (signal?.aborted) return Promise.resolve();
8111
+ const pending = [];
8112
+ const links = [];
8113
+ collectLinks(documentClone, links);
8114
+ for (const link of links) {
8115
+ if (link.sheet) continue;
8116
+ const rel = (link.getAttribute("rel") ?? "").trim().toLowerCase().split(/\s+/);
8117
+ if (!rel.includes("stylesheet") || rel.includes("alternate") || link.disabled || !link.getAttribute("href")) continue;
8118
+ pending.push(new Promise((resolve) => {
8119
+ const href = link.href;
8120
+ const settle = () => {
8121
+ link.removeEventListener("load", settle);
8122
+ link.removeEventListener("error", settle);
8123
+ signal?.removeEventListener("abort", onAbort);
8124
+ clearInterval(timer);
8125
+ resolve();
8126
+ };
8127
+ const onAbort = () => settle();
8128
+ if (link.sheet) {
8129
+ resolve();
8130
+ return;
8131
+ }
8132
+ link.addEventListener("load", settle);
8133
+ link.addEventListener("error", settle);
8134
+ signal?.addEventListener("abort", onAbort, { once: true });
8135
+ let elapsed = 0;
8136
+ const timer = setInterval(() => {
8137
+ elapsed += 50;
8138
+ if (link.sheet) settle();
8139
+ else if (elapsed >= 1e4) {
8140
+ logger?.warn(`Stylesheet ${href} did not finish loading within ${STYLESHEET_READY_TIMEOUT_MS / 1e3}s; continuing with a possibly unstyled clone`);
8141
+ settle();
8142
+ }
8143
+ }, 50);
8144
+ }));
8145
+ }
8146
+ const styleElements = documentClone.getElementsByTagName("style");
8147
+ for (let i = 0; i < styleElements.length; i++) {
8148
+ const style = styleElements[i];
8149
+ const sheet = style?.sheet;
8150
+ if (!sheet || !style?.textContent?.includes("@import")) continue;
8151
+ if (isSheetFullyApplied(sheet)) continue;
8152
+ pending.push(new Promise((resolve) => {
8153
+ let elapsed = 0;
8154
+ const timer = setInterval(() => {
8155
+ elapsed += 50;
8156
+ const currentSheet = style.sheet;
8157
+ if (currentSheet && isSheetFullyApplied(currentSheet) || elapsed >= 1e4) {
8158
+ if (elapsed >= 1e4 && !isSheetFullyApplied(currentSheet ?? null)) logger?.warn(`@import chain in an inline <style> did not finish loading within ${STYLESHEET_READY_TIMEOUT_MS / 1e3}s; continuing with a possibly unstyled clone`);
8159
+ clearInterval(timer);
8160
+ resolve();
8161
+ }
8162
+ }, 50);
8163
+ }));
8164
+ }
8165
+ if (pending.length === 0) return Promise.resolve();
8166
+ return Promise.all(pending).then(() => void 0);
8167
+ };
8036
8168
  const iframeLoader = (iframe, logger, signal) => {
8037
8169
  return new Promise((resolve, reject) => {
8038
8170
  const cloneWindow = iframe.contentWindow;
@@ -8075,6 +8207,15 @@ const iframeLoader = (iframe, logger, signal) => {
8075
8207
  });
8076
8208
  };
8077
8209
  /**
8210
+ * Reduce a doctype component to characters that are inert inside a
8211
+ * <!DOCTYPE ...> declaration. Doctype content is parsed as a declaration
8212
+ * token, never as HTML markup, but the fields originate from the captured
8213
+ * document — stripping markup delimiters and control characters here makes
8214
+ * that property locally verifiable (and keeps the sanitizer guarantee even
8215
+ * if a future serializer ever embeds the result elsewhere).
8216
+ */
8217
+ const sanitizeDoctypePart = (value) => value.replace(/[<>&"'`\\]|[\u0000-\u001f\u007f]/g, "");
8218
+ /**
8078
8219
  * Serialise a document type declaration to an HTML string.
8079
8220
  * @internal – exported for testing only, not part of the public API.
8080
8221
  */
@@ -8082,12 +8223,12 @@ const serializeDoctype = (doctype) => {
8082
8223
  let str = "";
8083
8224
  if (doctype) {
8084
8225
  str += "<!DOCTYPE ";
8085
- if (doctype.name) str += doctype.name;
8086
- if (doctype.internalSubset) str += " " + doctype.internalSubset.replace(/"/g, "&quot;").replace(/>/g, "&gt;");
8226
+ if (doctype.name) str += sanitizeDoctypePart(doctype.name);
8227
+ if (doctype.internalSubset) str += " " + sanitizeDoctypePart(doctype.internalSubset);
8087
8228
  if (doctype.publicId) {
8088
- str += " PUBLIC \"" + doctype.publicId.replace(/"/g, "&quot;") + "\"";
8089
- if (doctype.systemId) str += " \"" + doctype.systemId.replace(/"/g, "&quot;") + "\"";
8090
- } else if (doctype.systemId) str += " SYSTEM \"" + doctype.systemId.replace(/"/g, "&quot;") + "\"";
8229
+ str += " PUBLIC \"" + sanitizeDoctypePart(doctype.publicId) + "\"";
8230
+ if (doctype.systemId) str += " \"" + sanitizeDoctypePart(doctype.systemId) + "\"";
8231
+ } else if (doctype.systemId) str += " SYSTEM \"" + sanitizeDoctypePart(doctype.systemId) + "\"";
8091
8232
  str += ">";
8092
8233
  }
8093
8234
  return str;
@@ -8123,6 +8264,8 @@ const mountCloneInIFrame = (context, ownerDocument, windowSize, mount) => {
8123
8264
  const referenceElement = mount.clonedReferenceElement;
8124
8265
  if (typeof referenceElement === "undefined") throw new Error(`Error finding the ${mount.referenceElementName} in the cloned document`);
8125
8266
  throwIfAborted(mount.signal);
8267
+ await stylesheetWait;
8268
+ throwIfAborted(mount.signal);
8126
8269
  if (documentClone.fonts && documentClone.fonts.ready) await documentClone.fonts.ready;
8127
8270
  throwIfAborted(mount.signal);
8128
8271
  if (/(AppleWebKit)/g.test(navigator.userAgent)) {
@@ -8173,7 +8316,11 @@ const mountCloneInIFrame = (context, ownerDocument, windowSize, mount) => {
8173
8316
  const adoptedNode = documentClone.adoptNode(mount.documentElement);
8174
8317
  addBase(adoptedNode, baseUri);
8175
8318
  documentClone.replaceChild(adoptedNode, documentClone.documentElement);
8176
- return iframeLoad;
8319
+ const stylesheetWait = stylesheetsReady(documentClone, context.logger, mount.signal);
8320
+ return iframeLoad.catch((error) => {
8321
+ iframe.parentNode?.removeChild(iframe);
8322
+ throw error;
8323
+ });
8177
8324
  };
8178
8325
  //#endregion
8179
8326
  //#region src/dom/document-cloner.ts
@@ -13481,14 +13628,19 @@ var ForeignObjectRenderer = class {
13481
13628
  const svg = createForeignObjectSVG(this.options.width, this.options.height, 0, 0, element);
13482
13629
  const imagePromise = loadSerializedSVG(svg);
13483
13630
  restoreRootOffsets();
13484
- const img = await imagePromise;
13485
- if (this.options.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
13486
- if (this.options.backgroundColor) {
13487
- this.ctx.fillStyle = asString(this.options.backgroundColor);
13488
- this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
13631
+ try {
13632
+ const img = await imagePromise;
13633
+ if (this.options.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
13634
+ if (this.options.backgroundColor) {
13635
+ this.ctx.fillStyle = asString(this.options.backgroundColor);
13636
+ this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
13637
+ }
13638
+ this.ctx.drawImage(img, this.options.x, this.options.y, this.options.width, this.options.height);
13639
+ return this.canvas;
13640
+ } catch (error) {
13641
+ if (!this.options.canvas) releaseSurface(this.canvas);
13642
+ throw error;
13489
13643
  }
13490
- this.ctx.drawImage(img, this.options.x, this.options.y, this.options.width, this.options.height);
13491
- return this.canvas;
13492
13644
  }
13493
13645
  };
13494
13646
  /**
@@ -14044,19 +14196,17 @@ const renderElement = async (element, opts, config) => {
14044
14196
  const documentCloner = new DocumentCloner(context, element, cloneOptions);
14045
14197
  const clonedElement = documentCloner.clonedReferenceElement;
14046
14198
  if (!clonedElement) throw new Error("Unable to find element in cloned iframe");
14047
- const container = await documentCloner.toIFrame(ownerDocument, windowBounds);
14048
- perfMonitor.end("clone");
14049
- emitProgress(opts.onProgress, context, "clone", 30);
14050
- if (signal?.aborted) {
14051
- if (opts.removeContainer ?? true) DocumentCloner.destroy(container);
14052
- throwIfAborted(signal);
14053
- }
14054
- const { width, height, left, top } = isBodyElement(clonedElement) || isHTMLElement(clonedElement) ? parseDocumentSize(clonedElement.ownerDocument) : parseBounds(context, clonedElement);
14055
- const backgroundColor = parseBackgroundColor(context, clonedElement, opts.backgroundColor);
14056
- const renderOptions = assembleRenderOptions(opts, backgroundColor, left, top, width, height, defaultView.devicePixelRatio ?? 1);
14199
+ let container;
14057
14200
  let canvas;
14058
14201
  let root;
14059
14202
  try {
14203
+ container = await documentCloner.toIFrame(ownerDocument, windowBounds);
14204
+ perfMonitor.end("clone");
14205
+ emitProgress(opts.onProgress, context, "clone", 30);
14206
+ throwIfAborted(signal);
14207
+ const { width, height, left, top } = isBodyElement(clonedElement) || isHTMLElement(clonedElement) ? parseDocumentSize(clonedElement.ownerDocument) : parseBounds(context, clonedElement);
14208
+ const backgroundColor = parseBackgroundColor(context, clonedElement, opts.backgroundColor);
14209
+ const renderOptions = assembleRenderOptions(opts, backgroundColor, left, top, width, height, defaultView.devicePixelRatio ?? 1);
14060
14210
  if (foreignObjectRendering) {
14061
14211
  context.logger.debug(`Document cloned, using foreign object rendering`);
14062
14212
  perfMonitor.start("render-foreignobject");
@@ -14090,7 +14240,7 @@ const renderElement = async (element, opts, config) => {
14090
14240
  try {
14091
14241
  if (root) root.restoreTree();
14092
14242
  } finally {
14093
- if (opts.removeContainer ?? true) DocumentCloner.destroy(container);
14243
+ if (container && (opts.removeContainer ?? true)) DocumentCloner.destroy(container);
14094
14244
  perfMonitor.end("cleanup");
14095
14245
  }
14096
14246
  }