html2canvas-pro 1.6.4 → 1.6.6

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.
Files changed (33) hide show
  1. package/dist/html2canvas-pro.esm.js +508 -43
  2. package/dist/html2canvas-pro.esm.js.map +1 -1
  3. package/dist/html2canvas-pro.js +508 -42
  4. package/dist/html2canvas-pro.js.map +1 -1
  5. package/dist/html2canvas-pro.min.js +4 -4
  6. package/dist/lib/css/index.js +10 -0
  7. package/dist/lib/css/index.js.map +1 -1
  8. package/dist/lib/css/property-descriptors/text-decoration-style.js +25 -0
  9. package/dist/lib/css/property-descriptors/text-decoration-style.js.map +1 -0
  10. package/dist/lib/css/property-descriptors/text-decoration-thickness.js +27 -0
  11. package/dist/lib/css/property-descriptors/text-decoration-thickness.js.map +1 -0
  12. package/dist/lib/css/property-descriptors/text-overflow.js +19 -0
  13. package/dist/lib/css/property-descriptors/text-overflow.js.map +1 -0
  14. package/dist/lib/css/property-descriptors/text-underline-offset.js +24 -0
  15. package/dist/lib/css/property-descriptors/text-underline-offset.js.map +1 -0
  16. package/dist/lib/css/property-descriptors/webkit-line-clamp.js +27 -0
  17. package/dist/lib/css/property-descriptors/webkit-line-clamp.js.map +1 -0
  18. package/dist/lib/dom/document-cloner.js +50 -22
  19. package/dist/lib/dom/document-cloner.js.map +1 -1
  20. package/dist/lib/index.js +10 -2
  21. package/dist/lib/index.js.map +1 -1
  22. package/dist/lib/render/canvas/canvas-renderer.js +338 -18
  23. package/dist/lib/render/canvas/canvas-renderer.js.map +1 -1
  24. package/dist/types/css/index.d.ts +10 -0
  25. package/dist/types/css/property-descriptors/text-decoration-style.d.ts +9 -0
  26. package/dist/types/css/property-descriptors/text-decoration-thickness.d.ts +3 -0
  27. package/dist/types/css/property-descriptors/text-overflow.d.ts +6 -0
  28. package/dist/types/css/property-descriptors/text-underline-offset.d.ts +3 -0
  29. package/dist/types/css/property-descriptors/webkit-line-clamp.d.ts +7 -0
  30. package/dist/types/dom/document-cloner.d.ts +2 -0
  31. package/dist/types/index.d.ts +6 -2
  32. package/dist/types/render/canvas/canvas-renderer.d.ts +10 -1
  33. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * html2canvas-pro 1.6.4 <https://yorickshan.github.io/html2canvas-pro/>
2
+ * html2canvas-pro 1.6.6 <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
  */
@@ -4120,6 +4120,71 @@ const textDecorationLine = {
4120
4120
  }
4121
4121
  };
4122
4122
 
4123
+ const textDecorationStyle = {
4124
+ name: 'text-decoration-style',
4125
+ initialValue: 'solid',
4126
+ prefix: false,
4127
+ type: 2 /* PropertyDescriptorParsingType.IDENT_VALUE */,
4128
+ parse: (_context, style) => {
4129
+ switch (style) {
4130
+ case 'double':
4131
+ return 1 /* TEXT_DECORATION_STYLE.DOUBLE */;
4132
+ case 'dotted':
4133
+ return 2 /* TEXT_DECORATION_STYLE.DOTTED */;
4134
+ case 'dashed':
4135
+ return 3 /* TEXT_DECORATION_STYLE.DASHED */;
4136
+ case 'wavy':
4137
+ return 4 /* TEXT_DECORATION_STYLE.WAVY */;
4138
+ case 'solid':
4139
+ default:
4140
+ return 0 /* TEXT_DECORATION_STYLE.SOLID */;
4141
+ }
4142
+ }
4143
+ };
4144
+
4145
+ const textDecorationThickness = {
4146
+ name: 'text-decoration-thickness',
4147
+ initialValue: 'auto',
4148
+ prefix: false,
4149
+ type: 0 /* PropertyDescriptorParsingType.VALUE */,
4150
+ parse: (_context, token) => {
4151
+ if (isIdentToken(token)) {
4152
+ switch (token.value) {
4153
+ case 'auto':
4154
+ return 'auto';
4155
+ case 'from-font':
4156
+ return 'from-font';
4157
+ }
4158
+ }
4159
+ if (isDimensionToken(token)) {
4160
+ // Convert to pixels
4161
+ return token.number;
4162
+ }
4163
+ // Default to auto
4164
+ return 'auto';
4165
+ }
4166
+ };
4167
+
4168
+ const textUnderlineOffset = {
4169
+ name: 'text-underline-offset',
4170
+ initialValue: 'auto',
4171
+ prefix: false,
4172
+ type: 0 /* PropertyDescriptorParsingType.VALUE */,
4173
+ parse: (_context, token) => {
4174
+ if (isIdentToken(token)) {
4175
+ if (token.value === 'auto') {
4176
+ return 'auto';
4177
+ }
4178
+ }
4179
+ if (isDimensionToken(token)) {
4180
+ // Return pixel value
4181
+ return token.number;
4182
+ }
4183
+ // Default to auto
4184
+ return 'auto';
4185
+ }
4186
+ };
4187
+
4123
4188
  const fontFamily = {
4124
4189
  name: `font-family`,
4125
4190
  initialValue: '',
@@ -4423,6 +4488,30 @@ const webkitTextStrokeWidth = {
4423
4488
  }
4424
4489
  };
4425
4490
 
4491
+ /**
4492
+ * -webkit-line-clamp property descriptor
4493
+ * Used with display: -webkit-box and -webkit-box-orient: vertical
4494
+ * to limit text to a specific number of lines
4495
+ */
4496
+ const webkitLineClamp = {
4497
+ name: '-webkit-line-clamp',
4498
+ initialValue: 'none',
4499
+ prefix: true,
4500
+ type: 0 /* PropertyDescriptorParsingType.VALUE */,
4501
+ parse: (_context, token) => {
4502
+ // 'none' means no line clamping
4503
+ if (token.type === 20 /* TokenType.IDENT_TOKEN */ && token.value === 'none') {
4504
+ return 0;
4505
+ }
4506
+ // Number value specifies the number of lines
4507
+ if (token.type === 17 /* TokenType.NUMBER_TOKEN */) {
4508
+ return Math.max(0, Math.floor(token.number));
4509
+ }
4510
+ // Default to 0 (no clamping)
4511
+ return 0;
4512
+ }
4513
+ };
4514
+
4426
4515
  const objectFit = {
4427
4516
  name: 'objectFit',
4428
4517
  initialValue: 'fill',
@@ -4448,6 +4537,22 @@ const parseDisplayValue = (display) => {
4448
4537
  return 0 /* OBJECT_FIT.FILL */;
4449
4538
  };
4450
4539
 
4540
+ const textOverflow = {
4541
+ name: 'text-overflow',
4542
+ initialValue: 'clip',
4543
+ prefix: false,
4544
+ type: 2 /* PropertyDescriptorParsingType.IDENT_VALUE */,
4545
+ parse: (_context, textOverflow) => {
4546
+ switch (textOverflow) {
4547
+ case 'ellipsis':
4548
+ return 1 /* TEXT_OVERFLOW.ELLIPSIS */;
4549
+ case 'clip':
4550
+ default:
4551
+ return 0 /* TEXT_OVERFLOW.CLIP */;
4552
+ }
4553
+ }
4554
+ };
4555
+
4451
4556
  class CSSParsedDeclaration {
4452
4557
  constructor(context, declaration) {
4453
4558
  this.animationDuration = parse(context, duration, declaration.animationDuration);
@@ -4508,14 +4613,19 @@ class CSSParsedDeclaration {
4508
4613
  this.textAlign = parse(context, textAlign, declaration.textAlign);
4509
4614
  this.textDecorationColor = parse(context, textDecorationColor, declaration.textDecorationColor ?? declaration.color);
4510
4615
  this.textDecorationLine = parse(context, textDecorationLine, declaration.textDecorationLine ?? declaration.textDecoration);
4616
+ this.textDecorationStyle = parse(context, textDecorationStyle, declaration.textDecorationStyle);
4617
+ this.textDecorationThickness = parse(context, textDecorationThickness, declaration.textDecorationThickness);
4618
+ this.textUnderlineOffset = parse(context, textUnderlineOffset, declaration.textUnderlineOffset);
4511
4619
  this.textShadow = parse(context, textShadow, declaration.textShadow);
4512
4620
  this.textTransform = parse(context, textTransform, declaration.textTransform);
4621
+ this.textOverflow = parse(context, textOverflow, declaration.textOverflow);
4513
4622
  this.transform = parse(context, transform$1, declaration.transform);
4514
4623
  this.transformOrigin = parse(context, transformOrigin, declaration.transformOrigin);
4515
4624
  this.rotate = parse(context, rotate, declaration.rotate);
4516
4625
  this.visibility = parse(context, visibility, declaration.visibility);
4517
4626
  this.webkitTextStrokeColor = parse(context, webkitTextStrokeColor, declaration.webkitTextStrokeColor);
4518
4627
  this.webkitTextStrokeWidth = parse(context, webkitTextStrokeWidth, declaration.webkitTextStrokeWidth);
4628
+ this.webkitLineClamp = parse(context, webkitLineClamp, declaration.webkitLineClamp);
4519
4629
  this.wordBreak = parse(context, wordBreak, declaration.wordBreak);
4520
4630
  this.zIndex = parse(context, zIndex, declaration.zIndex);
4521
4631
  this.objectFit = parse(context, objectFit, declaration.objectFit);
@@ -6004,6 +6114,27 @@ const createCounterText = (value, type, appendSuffix) => {
6004
6114
  };
6005
6115
 
6006
6116
  const IGNORE_ATTRIBUTE = 'data-html2canvas-ignore';
6117
+ /**
6118
+ * Find the parent ShadowRoot of an element, if any
6119
+ * @param element - The element to check
6120
+ * @returns The parent ShadowRoot or null
6121
+ */
6122
+ const findParentShadowRoot = (element) => {
6123
+ let current = element;
6124
+ while (current) {
6125
+ // Check if we've reached a shadow root boundary
6126
+ if (current.parentNode && current.parentNode.host) {
6127
+ return current.parentNode;
6128
+ }
6129
+ // Use getRootNode to check if we're in a shadow root
6130
+ const root = current.getRootNode();
6131
+ if (root && root !== current.ownerDocument && root.host) {
6132
+ return root;
6133
+ }
6134
+ current = current.parentNode;
6135
+ }
6136
+ return null;
6137
+ };
6007
6138
  class DocumentCloner {
6008
6139
  constructor(context, element, options) {
6009
6140
  this.context = context;
@@ -6015,10 +6146,17 @@ class DocumentCloner {
6015
6146
  if (!element.ownerDocument) {
6016
6147
  throw new Error('Cloned element does not have an owner document');
6017
6148
  }
6149
+ // Auto-detect Shadow Root if not explicitly provided
6150
+ if (!this.options.iframeContainer) {
6151
+ const shadowRoot = findParentShadowRoot(element);
6152
+ if (shadowRoot) {
6153
+ this.options.iframeContainer = shadowRoot;
6154
+ }
6155
+ }
6018
6156
  this.documentElement = this.cloneNode(element.ownerDocument.documentElement, false);
6019
6157
  }
6020
6158
  toIFrame(ownerDocument, windowSize) {
6021
- const iframe = createIFrameContainer(ownerDocument, windowSize);
6159
+ const iframe = createIFrameContainer(ownerDocument, windowSize, this.options.iframeContainer);
6022
6160
  if (!iframe.contentWindow) {
6023
6161
  return Promise.reject(`Unable to find iframe window`);
6024
6162
  }
@@ -6124,20 +6262,8 @@ class DocumentCloner {
6124
6262
  return clone;
6125
6263
  }
6126
6264
  createCustomElementClone(node) {
6127
- // Ensure html2canvascustomelement is defined
6128
- if (typeof window !== 'undefined' && !customElements.get('html2canvascustomelement')) {
6129
- try {
6130
- customElements.define('html2canvascustomelement', class extends HTMLElement {
6131
- constructor() {
6132
- super();
6133
- }
6134
- });
6135
- }
6136
- catch (e) {
6137
- // Already defined or cannot define
6138
- }
6139
- }
6140
- const clone = document.createElement('html2canvascustomelement');
6265
+ const clone = document.createElement('div');
6266
+ clone.className = node.className;
6141
6267
  copyCSSStyles(node.style, clone);
6142
6268
  // Clone shadow DOM if it exists
6143
6269
  // Fix for Issue #108: This is critical for Web Components with slots to work correctly
@@ -6166,6 +6292,9 @@ class DocumentCloner {
6166
6292
  }, '');
6167
6293
  const style = node.cloneNode(false);
6168
6294
  style.textContent = css;
6295
+ if (this.options.cspNonce) {
6296
+ style.nonce = this.options.cspNonce;
6297
+ }
6169
6298
  return style;
6170
6299
  }
6171
6300
  }
@@ -6176,7 +6305,11 @@ class DocumentCloner {
6176
6305
  throw e;
6177
6306
  }
6178
6307
  }
6179
- return node.cloneNode(false);
6308
+ const cloned = node.cloneNode(false);
6309
+ if (this.options.cspNonce) {
6310
+ cloned.nonce = this.options.cspNonce;
6311
+ }
6312
+ return cloned;
6180
6313
  }
6181
6314
  createCanvasClone(canvas) {
6182
6315
  if (this.options.inlineImages && canvas.ownerDocument) {
@@ -6281,7 +6414,7 @@ class DocumentCloner {
6281
6414
  this.clonedReferenceElement = clone;
6282
6415
  }
6283
6416
  if (isBodyElement(clone)) {
6284
- createPseudoHideStyles(clone);
6417
+ createPseudoHideStyles(clone, this.options.cspNonce);
6285
6418
  }
6286
6419
  const counters = this.counters.parse(new CSSParsedCounterDeclaration(this.context, style));
6287
6420
  const before = this.resolvePseudoContent(node, clone, styleBefore, PseudoElementType.BEFORE);
@@ -6409,7 +6542,7 @@ var PseudoElementType;
6409
6542
  PseudoElementType[PseudoElementType["BEFORE"] = 0] = "BEFORE";
6410
6543
  PseudoElementType[PseudoElementType["AFTER"] = 1] = "AFTER";
6411
6544
  })(PseudoElementType || (PseudoElementType = {}));
6412
- const createIFrameContainer = (ownerDocument, bounds) => {
6545
+ const createIFrameContainer = (ownerDocument, bounds, customContainer) => {
6413
6546
  const cloneIframeContainer = ownerDocument.createElement('iframe');
6414
6547
  cloneIframeContainer.className = 'html2canvas-container';
6415
6548
  cloneIframeContainer.style.visibility = 'hidden';
@@ -6421,7 +6554,9 @@ const createIFrameContainer = (ownerDocument, bounds) => {
6421
6554
  cloneIframeContainer.height = bounds.height.toString();
6422
6555
  cloneIframeContainer.scrolling = 'no'; // ios won't scroll without it
6423
6556
  cloneIframeContainer.setAttribute(IGNORE_ATTRIBUTE, 'true');
6424
- ownerDocument.body.appendChild(cloneIframeContainer);
6557
+ // Use custom container if provided, otherwise use body
6558
+ const container = customContainer || ownerDocument.body;
6559
+ container.appendChild(cloneIframeContainer);
6425
6560
  return cloneIframeContainer;
6426
6561
  };
6427
6562
  const imageReady = (img) => {
@@ -6514,15 +6649,18 @@ const PSEUDO_HIDE_ELEMENT_STYLE = `{
6514
6649
  content: "" !important;
6515
6650
  display: none !important;
6516
6651
  }`;
6517
- const createPseudoHideStyles = (body) => {
6652
+ const createPseudoHideStyles = (body, cspNonce) => {
6518
6653
  createStyles(body, `.${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}${PSEUDO_BEFORE}${PSEUDO_HIDE_ELEMENT_STYLE}
6519
- .${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}${PSEUDO_AFTER}${PSEUDO_HIDE_ELEMENT_STYLE}`);
6654
+ .${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}${PSEUDO_AFTER}${PSEUDO_HIDE_ELEMENT_STYLE}`, cspNonce);
6520
6655
  };
6521
- const createStyles = (body, styles) => {
6656
+ const createStyles = (body, styles, cspNonce) => {
6522
6657
  const document = body.ownerDocument;
6523
6658
  if (document) {
6524
6659
  const style = document.createElement('style');
6525
6660
  style.textContent = styles;
6661
+ if (cspNonce) {
6662
+ style.nonce = cspNonce;
6663
+ }
6526
6664
  body.appendChild(style);
6527
6665
  }
6528
6666
  };
@@ -7525,6 +7663,156 @@ class CanvasRenderer extends Renderer {
7525
7663
  }, text.bounds.left);
7526
7664
  }
7527
7665
  }
7666
+ /**
7667
+ * Helper method to render text with paint order support
7668
+ * Reduces code duplication in line-clamp and normal rendering
7669
+ */
7670
+ renderTextBoundWithPaintOrder(textBound, styles, paintOrderLayers) {
7671
+ paintOrderLayers.forEach((paintOrderLayer) => {
7672
+ switch (paintOrderLayer) {
7673
+ case 0 /* PAINT_ORDER_LAYER.FILL */:
7674
+ this.ctx.fillStyle = asString(styles.color);
7675
+ this.renderTextWithLetterSpacing(textBound, styles.letterSpacing, styles.fontSize.number);
7676
+ break;
7677
+ case 1 /* PAINT_ORDER_LAYER.STROKE */:
7678
+ if (styles.webkitTextStrokeWidth && textBound.text.trim().length) {
7679
+ this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
7680
+ this.ctx.lineWidth = styles.webkitTextStrokeWidth;
7681
+ this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
7682
+ this.renderTextWithLetterSpacing(textBound, styles.letterSpacing, styles.fontSize.number);
7683
+ }
7684
+ break;
7685
+ }
7686
+ });
7687
+ }
7688
+ renderTextDecoration(bounds, styles) {
7689
+ this.ctx.fillStyle = asString(styles.textDecorationColor || styles.color);
7690
+ // Calculate decoration line thickness
7691
+ let thickness = 1; // default
7692
+ if (typeof styles.textDecorationThickness === 'number') {
7693
+ thickness = styles.textDecorationThickness;
7694
+ }
7695
+ else if (styles.textDecorationThickness === 'from-font') {
7696
+ // Use a reasonable default based on font size
7697
+ thickness = Math.max(1, Math.floor(styles.fontSize.number * 0.05));
7698
+ }
7699
+ // 'auto' uses default thickness of 1
7700
+ // Calculate underline offset
7701
+ let underlineOffset = 0;
7702
+ if (typeof styles.textUnderlineOffset === 'number') {
7703
+ // It's a pixel value
7704
+ underlineOffset = styles.textUnderlineOffset;
7705
+ }
7706
+ // 'auto' uses default offset of 0
7707
+ const decorationStyle = styles.textDecorationStyle;
7708
+ styles.textDecorationLine.forEach((textDecorationLine) => {
7709
+ let y = 0;
7710
+ switch (textDecorationLine) {
7711
+ case 1 /* TEXT_DECORATION_LINE.UNDERLINE */:
7712
+ y = bounds.top + bounds.height - thickness + underlineOffset;
7713
+ break;
7714
+ case 2 /* TEXT_DECORATION_LINE.OVERLINE */:
7715
+ y = bounds.top;
7716
+ break;
7717
+ case 3 /* TEXT_DECORATION_LINE.LINE_THROUGH */:
7718
+ y = bounds.top + (bounds.height / 2 - thickness / 2);
7719
+ break;
7720
+ default:
7721
+ return;
7722
+ }
7723
+ this.drawDecorationLine(bounds.left, y, bounds.width, thickness, decorationStyle);
7724
+ });
7725
+ }
7726
+ drawDecorationLine(x, y, width, thickness, style) {
7727
+ switch (style) {
7728
+ case 0 /* TEXT_DECORATION_STYLE.SOLID */:
7729
+ // Solid line (default)
7730
+ this.ctx.fillRect(x, y, width, thickness);
7731
+ break;
7732
+ case 1 /* TEXT_DECORATION_STYLE.DOUBLE */:
7733
+ // Double line
7734
+ const gap = Math.max(1, thickness);
7735
+ this.ctx.fillRect(x, y, width, thickness);
7736
+ this.ctx.fillRect(x, y + thickness + gap, width, thickness);
7737
+ break;
7738
+ case 2 /* TEXT_DECORATION_STYLE.DOTTED */:
7739
+ // Dotted line
7740
+ this.ctx.save();
7741
+ this.ctx.beginPath();
7742
+ this.ctx.setLineDash([thickness, thickness * 2]);
7743
+ this.ctx.lineWidth = thickness;
7744
+ this.ctx.strokeStyle = this.ctx.fillStyle;
7745
+ this.ctx.moveTo(x, y + thickness / 2);
7746
+ this.ctx.lineTo(x + width, y + thickness / 2);
7747
+ this.ctx.stroke();
7748
+ this.ctx.restore();
7749
+ break;
7750
+ case 3 /* TEXT_DECORATION_STYLE.DASHED */:
7751
+ // Dashed line
7752
+ this.ctx.save();
7753
+ this.ctx.beginPath();
7754
+ this.ctx.setLineDash([thickness * 3, thickness * 2]);
7755
+ this.ctx.lineWidth = thickness;
7756
+ this.ctx.strokeStyle = this.ctx.fillStyle;
7757
+ this.ctx.moveTo(x, y + thickness / 2);
7758
+ this.ctx.lineTo(x + width, y + thickness / 2);
7759
+ this.ctx.stroke();
7760
+ this.ctx.restore();
7761
+ break;
7762
+ case 4 /* TEXT_DECORATION_STYLE.WAVY */:
7763
+ // Wavy line (approximation using quadratic curves)
7764
+ this.ctx.save();
7765
+ this.ctx.beginPath();
7766
+ this.ctx.lineWidth = thickness;
7767
+ this.ctx.strokeStyle = this.ctx.fillStyle;
7768
+ const amplitude = thickness * 2;
7769
+ const wavelength = thickness * 4;
7770
+ let currentX = x;
7771
+ this.ctx.moveTo(currentX, y + thickness / 2);
7772
+ while (currentX < x + width) {
7773
+ const nextX = Math.min(currentX + wavelength / 2, x + width);
7774
+ this.ctx.quadraticCurveTo(currentX + wavelength / 4, y + thickness / 2 - amplitude, nextX, y + thickness / 2);
7775
+ currentX = nextX;
7776
+ if (currentX < x + width) {
7777
+ const nextX2 = Math.min(currentX + wavelength / 2, x + width);
7778
+ this.ctx.quadraticCurveTo(currentX + wavelength / 4, y + thickness / 2 + amplitude, nextX2, y + thickness / 2);
7779
+ currentX = nextX2;
7780
+ }
7781
+ }
7782
+ this.ctx.stroke();
7783
+ this.ctx.restore();
7784
+ break;
7785
+ default:
7786
+ // Fallback to solid
7787
+ this.ctx.fillRect(x, y, width, thickness);
7788
+ }
7789
+ }
7790
+ // Helper method to truncate text and add ellipsis if needed
7791
+ truncateTextWithEllipsis(text, maxWidth, letterSpacing) {
7792
+ const ellipsis = '...';
7793
+ const ellipsisWidth = this.ctx.measureText(ellipsis).width;
7794
+ if (letterSpacing === 0) {
7795
+ let truncated = text;
7796
+ while (this.ctx.measureText(truncated).width + ellipsisWidth > maxWidth && truncated.length > 0) {
7797
+ truncated = truncated.slice(0, -1);
7798
+ }
7799
+ return truncated + ellipsis;
7800
+ }
7801
+ else {
7802
+ const letters = segmentGraphemes(text);
7803
+ let width = ellipsisWidth;
7804
+ let result = [];
7805
+ for (const letter of letters) {
7806
+ const letterWidth = this.ctx.measureText(letter).width + letterSpacing;
7807
+ if (width + letterWidth > maxWidth) {
7808
+ break;
7809
+ }
7810
+ result.push(letter);
7811
+ width += letterWidth;
7812
+ }
7813
+ return result.join('') + ellipsis;
7814
+ }
7815
+ }
7528
7816
  createFontStyle(styles) {
7529
7817
  const fontVariant = styles.fontVariant
7530
7818
  .filter((variant) => variant === 'normal' || variant === 'small-caps')
@@ -7539,13 +7827,195 @@ class CanvasRenderer extends Renderer {
7539
7827
  fontSize
7540
7828
  ];
7541
7829
  }
7542
- async renderTextNode(text, styles) {
7830
+ async renderTextNode(text, styles, containerBounds) {
7543
7831
  const [font] = this.createFontStyle(styles);
7544
7832
  this.ctx.font = font;
7545
7833
  this.ctx.direction = styles.direction === 1 /* DIRECTION.RTL */ ? 'rtl' : 'ltr';
7546
7834
  this.ctx.textAlign = 'left';
7547
7835
  this.ctx.textBaseline = 'alphabetic';
7548
7836
  const paintOrder = styles.paintOrder;
7837
+ // Calculate line height for text layout detection (used by both line-clamp and ellipsis)
7838
+ const lineHeight = styles.fontSize.number * 1.5;
7839
+ // Check if we need to apply -webkit-line-clamp
7840
+ // This limits text to a specific number of lines with ellipsis
7841
+ const shouldApplyLineClamp = styles.webkitLineClamp > 0 &&
7842
+ (styles.display & 2 /* DISPLAY.BLOCK */) !== 0 &&
7843
+ styles.overflowY === 1 /* OVERFLOW.HIDDEN */ &&
7844
+ text.textBounds.length > 0;
7845
+ if (shouldApplyLineClamp) {
7846
+ // Group text bounds by lines based on their Y position
7847
+ const lines = [];
7848
+ let currentLine = [];
7849
+ let currentLineTop = text.textBounds[0].bounds.top;
7850
+ text.textBounds.forEach((tb) => {
7851
+ // If this text bound is on a different line, start a new line
7852
+ if (Math.abs(tb.bounds.top - currentLineTop) >= lineHeight * 0.5) {
7853
+ if (currentLine.length > 0) {
7854
+ lines.push(currentLine);
7855
+ }
7856
+ currentLine = [tb];
7857
+ currentLineTop = tb.bounds.top;
7858
+ }
7859
+ else {
7860
+ currentLine.push(tb);
7861
+ }
7862
+ });
7863
+ // Don't forget the last line
7864
+ if (currentLine.length > 0) {
7865
+ lines.push(currentLine);
7866
+ }
7867
+ // Only render up to webkitLineClamp lines
7868
+ const maxLines = styles.webkitLineClamp;
7869
+ if (lines.length > maxLines) {
7870
+ // Render only the first (maxLines - 1) complete lines
7871
+ for (let i = 0; i < maxLines - 1; i++) {
7872
+ lines[i].forEach((textBound) => {
7873
+ this.renderTextBoundWithPaintOrder(textBound, styles, paintOrder);
7874
+ });
7875
+ }
7876
+ // For the last line, truncate with ellipsis
7877
+ const lastLine = lines[maxLines - 1];
7878
+ if (lastLine && lastLine.length > 0 && containerBounds) {
7879
+ const lastLineText = lastLine.map((tb) => tb.text).join('');
7880
+ const firstBound = lastLine[0];
7881
+ const availableWidth = containerBounds.width - (firstBound.bounds.left - containerBounds.left);
7882
+ const truncatedText = this.truncateTextWithEllipsis(lastLineText, availableWidth, styles.letterSpacing);
7883
+ paintOrder.forEach((paintOrderLayer) => {
7884
+ switch (paintOrderLayer) {
7885
+ case 0 /* PAINT_ORDER_LAYER.FILL */:
7886
+ this.ctx.fillStyle = asString(styles.color);
7887
+ if (styles.letterSpacing === 0) {
7888
+ this.ctx.fillText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
7889
+ }
7890
+ else {
7891
+ const letters = segmentGraphemes(truncatedText);
7892
+ letters.reduce((left, letter) => {
7893
+ this.ctx.fillText(letter, left, firstBound.bounds.top + styles.fontSize.number);
7894
+ return left + this.ctx.measureText(letter).width + styles.letterSpacing;
7895
+ }, firstBound.bounds.left);
7896
+ }
7897
+ break;
7898
+ case 1 /* PAINT_ORDER_LAYER.STROKE */:
7899
+ if (styles.webkitTextStrokeWidth && truncatedText.trim().length) {
7900
+ this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
7901
+ this.ctx.lineWidth = styles.webkitTextStrokeWidth;
7902
+ this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
7903
+ if (styles.letterSpacing === 0) {
7904
+ this.ctx.strokeText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
7905
+ }
7906
+ else {
7907
+ const letters = segmentGraphemes(truncatedText);
7908
+ letters.reduce((left, letter) => {
7909
+ this.ctx.strokeText(letter, left, firstBound.bounds.top + styles.fontSize.number);
7910
+ return left + this.ctx.measureText(letter).width + styles.letterSpacing;
7911
+ }, firstBound.bounds.left);
7912
+ }
7913
+ }
7914
+ break;
7915
+ }
7916
+ });
7917
+ }
7918
+ return; // Don't render anything else
7919
+ }
7920
+ // If lines.length <= maxLines, fall through to normal rendering
7921
+ }
7922
+ // Check if we need to apply text-overflow: ellipsis
7923
+ // Issue #203: Only apply ellipsis for single-line text overflow
7924
+ // Multi-line text truncation (like -webkit-line-clamp) should not be affected
7925
+ const shouldApplyEllipsis = styles.textOverflow === 1 /* TEXT_OVERFLOW.ELLIPSIS */ &&
7926
+ containerBounds &&
7927
+ styles.overflowX === 1 /* OVERFLOW.HIDDEN */ &&
7928
+ text.textBounds.length > 0;
7929
+ // Calculate total text width if ellipsis might be needed
7930
+ let needsEllipsis = false;
7931
+ let truncatedText = '';
7932
+ if (shouldApplyEllipsis) {
7933
+ // Check if all text bounds are on approximately the same line (single-line scenario)
7934
+ // For multi-line text (like -webkit-line-clamp), textBounds will have different Y positions
7935
+ const firstTop = text.textBounds[0].bounds.top;
7936
+ const isSingleLine = text.textBounds.every((tb) => Math.abs(tb.bounds.top - firstTop) < lineHeight * 0.5);
7937
+ if (isSingleLine) {
7938
+ // Measure the full text content
7939
+ // Note: text.textBounds may contain whitespace characters from HTML formatting
7940
+ // We need to collapse them like the browser does for white-space: nowrap
7941
+ let fullText = text.textBounds.map((tb) => tb.text).join('');
7942
+ // Collapse whitespace: replace sequences of whitespace (including newlines) with single spaces
7943
+ // and trim leading/trailing whitespace
7944
+ fullText = fullText.replace(/\s+/g, ' ').trim();
7945
+ const fullTextWidth = this.ctx.measureText(fullText).width;
7946
+ const availableWidth = containerBounds.width;
7947
+ if (fullTextWidth > availableWidth) {
7948
+ needsEllipsis = true;
7949
+ truncatedText = this.truncateTextWithEllipsis(fullText, availableWidth, styles.letterSpacing);
7950
+ }
7951
+ }
7952
+ }
7953
+ // If ellipsis is needed, render the truncated text once
7954
+ if (needsEllipsis) {
7955
+ const firstBound = text.textBounds[0];
7956
+ paintOrder.forEach((paintOrderLayer) => {
7957
+ switch (paintOrderLayer) {
7958
+ case 0 /* PAINT_ORDER_LAYER.FILL */:
7959
+ this.ctx.fillStyle = asString(styles.color);
7960
+ if (styles.letterSpacing === 0) {
7961
+ this.ctx.fillText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
7962
+ }
7963
+ else {
7964
+ const letters = segmentGraphemes(truncatedText);
7965
+ letters.reduce((left, letter) => {
7966
+ this.ctx.fillText(letter, left, firstBound.bounds.top + styles.fontSize.number);
7967
+ return left + this.ctx.measureText(letter).width + styles.letterSpacing;
7968
+ }, firstBound.bounds.left);
7969
+ }
7970
+ const textShadows = styles.textShadow;
7971
+ if (textShadows.length && truncatedText.trim().length) {
7972
+ textShadows
7973
+ .slice(0)
7974
+ .reverse()
7975
+ .forEach((textShadow) => {
7976
+ this.ctx.shadowColor = asString(textShadow.color);
7977
+ this.ctx.shadowOffsetX = textShadow.offsetX.number * this.options.scale;
7978
+ this.ctx.shadowOffsetY = textShadow.offsetY.number * this.options.scale;
7979
+ this.ctx.shadowBlur = textShadow.blur.number;
7980
+ if (styles.letterSpacing === 0) {
7981
+ this.ctx.fillText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
7982
+ }
7983
+ else {
7984
+ const letters = segmentGraphemes(truncatedText);
7985
+ letters.reduce((left, letter) => {
7986
+ this.ctx.fillText(letter, left, firstBound.bounds.top + styles.fontSize.number);
7987
+ return left + this.ctx.measureText(letter).width + styles.letterSpacing;
7988
+ }, firstBound.bounds.left);
7989
+ }
7990
+ });
7991
+ this.ctx.shadowColor = '';
7992
+ this.ctx.shadowOffsetX = 0;
7993
+ this.ctx.shadowOffsetY = 0;
7994
+ this.ctx.shadowBlur = 0;
7995
+ }
7996
+ break;
7997
+ case 1 /* PAINT_ORDER_LAYER.STROKE */:
7998
+ if (styles.webkitTextStrokeWidth && truncatedText.trim().length) {
7999
+ this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
8000
+ this.ctx.lineWidth = styles.webkitTextStrokeWidth;
8001
+ this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
8002
+ if (styles.letterSpacing === 0) {
8003
+ this.ctx.strokeText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
8004
+ }
8005
+ else {
8006
+ const letters = segmentGraphemes(truncatedText);
8007
+ letters.reduce((left, letter) => {
8008
+ this.ctx.strokeText(letter, left, firstBound.bounds.top + styles.fontSize.number);
8009
+ return left + this.ctx.measureText(letter).width + styles.letterSpacing;
8010
+ }, firstBound.bounds.left);
8011
+ }
8012
+ }
8013
+ break;
8014
+ }
8015
+ });
8016
+ return;
8017
+ }
8018
+ // Normal rendering (no ellipsis needed)
7549
8019
  text.textBounds.forEach((text) => {
7550
8020
  paintOrder.forEach((paintOrderLayer) => {
7551
8021
  switch (paintOrderLayer) {
@@ -7570,22 +8040,7 @@ class CanvasRenderer extends Renderer {
7570
8040
  this.ctx.shadowBlur = 0;
7571
8041
  }
7572
8042
  if (styles.textDecorationLine.length) {
7573
- this.ctx.fillStyle = asString(styles.textDecorationColor || styles.color);
7574
- const decorationLineHeight = 1;
7575
- styles.textDecorationLine.forEach((textDecorationLine) => {
7576
- // Fix the issue where textDecorationLine exhibits x-axis positioning errors on high-resolution devices due to varying devicePixelRatio, corrected by using relative values of element heights.
7577
- switch (textDecorationLine) {
7578
- case 1 /* TEXT_DECORATION_LINE.UNDERLINE */:
7579
- this.ctx.fillRect(text.bounds.left, text.bounds.top + text.bounds.height - decorationLineHeight, text.bounds.width, decorationLineHeight);
7580
- break;
7581
- case 2 /* TEXT_DECORATION_LINE.OVERLINE */:
7582
- this.ctx.fillRect(text.bounds.left, text.bounds.top, text.bounds.width, decorationLineHeight);
7583
- break;
7584
- case 3 /* TEXT_DECORATION_LINE.LINE_THROUGH */:
7585
- this.ctx.fillRect(text.bounds.left, text.bounds.top + (text.bounds.height / 2 - decorationLineHeight / 2), text.bounds.width, decorationLineHeight);
7586
- break;
7587
- }
7588
- });
8043
+ this.renderTextDecoration(text.bounds, styles);
7589
8044
  }
7590
8045
  break;
7591
8046
  case 1 /* PAINT_ORDER_LAYER.STROKE */:
@@ -7707,8 +8162,11 @@ class CanvasRenderer extends Renderer {
7707
8162
  const container = paint.container;
7708
8163
  const curves = paint.curves;
7709
8164
  const styles = container.styles;
8165
+ // Use content box for text overflow calculation (excludes padding and border)
8166
+ // This matches browser behavior where text-overflow uses the content width
8167
+ const textBounds = contentBox(container);
7710
8168
  for (const child of container.textNodes) {
7711
- await this.renderTextNode(child, styles);
8169
+ await this.renderTextNode(child, styles, textBounds);
7712
8170
  }
7713
8171
  if (container instanceof ImageElementContainer) {
7714
8172
  try {
@@ -8363,9 +8821,14 @@ class Context {
8363
8821
  }
8364
8822
  Context.instanceCount = 1;
8365
8823
 
8824
+ let cspNonce;
8825
+ const setCspNonce = (nonce) => {
8826
+ cspNonce = nonce;
8827
+ };
8366
8828
  const html2canvas = (element, options = {}) => {
8367
8829
  return renderElement(element, options);
8368
8830
  };
8831
+ html2canvas.setCspNonce = setCspNonce;
8369
8832
  if (typeof window !== 'undefined') {
8370
8833
  CacheStorage.setContext(window);
8371
8834
  }
@@ -8406,8 +8869,10 @@ const renderElement = async (element, opts) => {
8406
8869
  allowTaint: opts.allowTaint ?? false,
8407
8870
  onclone: opts.onclone,
8408
8871
  ignoreElements: opts.ignoreElements,
8872
+ iframeContainer: opts.iframeContainer,
8409
8873
  inlineImages: foreignObjectRendering,
8410
- copyStyles: foreignObjectRendering
8874
+ copyStyles: foreignObjectRendering,
8875
+ cspNonce
8411
8876
  };
8412
8877
  context.logger.debug(`Starting document clone with size ${windowBounds.width}x${windowBounds.height} scrolled to ${-windowBounds.left},${-windowBounds.top}`);
8413
8878
  const documentCloner = new DocumentCloner(context, element, cloneOptions);
@@ -8477,5 +8942,5 @@ const parseBackgroundColor = (context, element, backgroundColorOverride) => {
8477
8942
  : defaultBackgroundColor;
8478
8943
  };
8479
8944
 
8480
- export { html2canvas as default, html2canvas };
8945
+ export { html2canvas as default, html2canvas, setCspNonce };
8481
8946
  //# sourceMappingURL=html2canvas-pro.esm.js.map