html2canvas-pro 1.6.5 → 1.6.7

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 1.6.5 <https://yorickshan.github.io/html2canvas-pro/>
2
+ * html2canvas-pro 1.6.7 <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
  */
@@ -4494,6 +4494,30 @@
4494
4494
  }
4495
4495
  };
4496
4496
 
4497
+ /**
4498
+ * -webkit-line-clamp property descriptor
4499
+ * Used with display: -webkit-box and -webkit-box-orient: vertical
4500
+ * to limit text to a specific number of lines
4501
+ */
4502
+ const webkitLineClamp = {
4503
+ name: '-webkit-line-clamp',
4504
+ initialValue: 'none',
4505
+ prefix: true,
4506
+ type: 0 /* PropertyDescriptorParsingType.VALUE */,
4507
+ parse: (_context, token) => {
4508
+ // 'none' means no line clamping
4509
+ if (token.type === 20 /* TokenType.IDENT_TOKEN */ && token.value === 'none') {
4510
+ return 0;
4511
+ }
4512
+ // Number value specifies the number of lines
4513
+ if (token.type === 17 /* TokenType.NUMBER_TOKEN */) {
4514
+ return Math.max(0, Math.floor(token.number));
4515
+ }
4516
+ // Default to 0 (no clamping)
4517
+ return 0;
4518
+ }
4519
+ };
4520
+
4497
4521
  const objectFit = {
4498
4522
  name: 'objectFit',
4499
4523
  initialValue: 'fill',
@@ -4607,6 +4631,7 @@
4607
4631
  this.visibility = parse(context, visibility, declaration.visibility);
4608
4632
  this.webkitTextStrokeColor = parse(context, webkitTextStrokeColor, declaration.webkitTextStrokeColor);
4609
4633
  this.webkitTextStrokeWidth = parse(context, webkitTextStrokeWidth, declaration.webkitTextStrokeWidth);
4634
+ this.webkitLineClamp = parse(context, webkitLineClamp, declaration.webkitLineClamp);
4610
4635
  this.wordBreak = parse(context, wordBreak, declaration.wordBreak);
4611
4636
  this.zIndex = parse(context, zIndex, declaration.zIndex);
4612
4637
  this.objectFit = parse(context, objectFit, declaration.objectFit);
@@ -6363,20 +6388,161 @@
6363
6388
  }
6364
6389
  }
6365
6390
  }
6366
- cloneChildNodes(node, clone, copyStyles) {
6367
- // Clone shadow DOM content if it exists
6368
- if (node.shadowRoot && clone.shadowRoot) {
6369
- for (let child = node.shadowRoot.firstChild; child; child = child.nextSibling) {
6370
- // Clone all shadow DOM children including <slot> elements
6371
- // The browser will automatically handle slot assignment
6372
- clone.shadowRoot.appendChild(this.cloneNode(child, copyStyles));
6391
+ /**
6392
+ * Check if a child node should be cloned based on filtering rules
6393
+ * Filters out: scripts, ignored elements, and optionally styles
6394
+ */
6395
+ shouldCloneChild(child) {
6396
+ return (!isElementNode(child) ||
6397
+ (!isScriptElement(child) &&
6398
+ !child.hasAttribute(IGNORE_ATTRIBUTE) &&
6399
+ (typeof this.options.ignoreElements !== 'function' || !this.options.ignoreElements(child))));
6400
+ }
6401
+ /**
6402
+ * Check if a style element should be cloned based on copyStyles option
6403
+ */
6404
+ shouldCloneStyleElement(child) {
6405
+ return !this.options.copyStyles || !isElementNode(child) || !isStyleElement(child);
6406
+ }
6407
+ /**
6408
+ * Safely append a cloned child to a target, applying all filtering rules
6409
+ */
6410
+ safeAppendClonedChild(target, child, copyStyles) {
6411
+ if (this.shouldCloneChild(child) && this.shouldCloneStyleElement(child)) {
6412
+ target.appendChild(this.cloneNode(child, copyStyles));
6413
+ }
6414
+ }
6415
+ /**
6416
+ * Clone assigned nodes from a slot element to the target
6417
+ */
6418
+ cloneAssignedNodes(assignedNodes, target, copyStyles) {
6419
+ assignedNodes.forEach((node) => {
6420
+ this.safeAppendClonedChild(target, node, copyStyles);
6421
+ });
6422
+ }
6423
+ /**
6424
+ * Clone fallback content from a slot element when no nodes are assigned
6425
+ */
6426
+ cloneSlotFallbackContent(slot, target, copyStyles) {
6427
+ for (let child = slot.firstChild; child; child = child.nextSibling) {
6428
+ this.safeAppendClonedChild(target, child, copyStyles);
6429
+ }
6430
+ }
6431
+ /**
6432
+ * Handle cloning of a slot element, including assigned nodes or fallback content
6433
+ */
6434
+ cloneSlotElement(slot, targetShadowRoot, copyStyles) {
6435
+ if (!isSlotElement(slot)) {
6436
+ return;
6437
+ }
6438
+ const slotElement = slot;
6439
+ // Defensive check: ensure assignedNodes method exists
6440
+ if (typeof slotElement.assignedNodes !== 'function') {
6441
+ this.context.logger.warn('HTMLSlotElement.assignedNodes is not available', slot);
6442
+ this.cloneSlotFallbackContent(slot, targetShadowRoot, copyStyles);
6443
+ return;
6444
+ }
6445
+ const assignedNodes = slotElement.assignedNodes();
6446
+ // Defensive check: ensure assignedNodes returns an array
6447
+ if (!assignedNodes || !Array.isArray(assignedNodes)) {
6448
+ this.context.logger.warn('assignedNodes() did not return a valid array', slot);
6449
+ this.cloneSlotFallbackContent(slot, targetShadowRoot, copyStyles);
6450
+ return;
6451
+ }
6452
+ if (assignedNodes.length > 0) {
6453
+ // Clone assigned nodes
6454
+ this.cloneAssignedNodes(assignedNodes, targetShadowRoot, copyStyles);
6455
+ }
6456
+ else {
6457
+ // Clone fallback content
6458
+ this.cloneSlotFallbackContent(slot, targetShadowRoot, copyStyles);
6459
+ }
6460
+ }
6461
+ /**
6462
+ * Clone shadow DOM children to the target shadow root
6463
+ */
6464
+ cloneShadowDOMChildren(shadowRoot, targetShadowRoot, copyStyles) {
6465
+ for (let child = shadowRoot.firstChild; child; child = child.nextSibling) {
6466
+ if (isElementNode(child) && isSlotElement(child)) {
6467
+ // Handle slot elements specially
6468
+ this.cloneSlotElement(child, targetShadowRoot, copyStyles);
6469
+ }
6470
+ else {
6471
+ // Clone regular elements
6472
+ this.safeAppendClonedChild(targetShadowRoot, child, copyStyles);
6373
6473
  }
6374
6474
  }
6375
- // Clone light DOM content (always, even if shadow DOM exists)
6475
+ }
6476
+ /**
6477
+ * Clone light DOM children to the target element
6478
+ */
6479
+ cloneLightDOMChildren(node, clone, copyStyles) {
6376
6480
  for (let child = node.firstChild; child; child = child.nextSibling) {
6377
6481
  this.appendChildNode(clone, child, copyStyles);
6378
6482
  }
6379
6483
  }
6484
+ /**
6485
+ * Clone slot element as light DOM when shadow root creation failed
6486
+ */
6487
+ cloneSlotElementAsLightDOM(slot, clone, copyStyles) {
6488
+ if (!isSlotElement(slot)) {
6489
+ return;
6490
+ }
6491
+ const slotElement = slot;
6492
+ if (typeof slotElement.assignedNodes !== 'function') {
6493
+ // Fallback: clone slot's children
6494
+ for (let child = slot.firstChild; child; child = child.nextSibling) {
6495
+ this.appendChildNode(clone, child, copyStyles);
6496
+ }
6497
+ return;
6498
+ }
6499
+ const assignedNodes = slotElement.assignedNodes();
6500
+ if (assignedNodes && Array.isArray(assignedNodes) && assignedNodes.length > 0) {
6501
+ // Clone assigned nodes as light DOM
6502
+ assignedNodes.forEach((node) => this.appendChildNode(clone, node, copyStyles));
6503
+ }
6504
+ else {
6505
+ // Clone fallback content as light DOM
6506
+ for (let child = slot.firstChild; child; child = child.nextSibling) {
6507
+ this.appendChildNode(clone, child, copyStyles);
6508
+ }
6509
+ }
6510
+ }
6511
+ /**
6512
+ * Clone shadow DOM content as light DOM when shadow root creation failed
6513
+ * This is a fallback mechanism to ensure content is not lost
6514
+ */
6515
+ cloneShadowDOMAsLightDOM(shadowRoot, clone, copyStyles) {
6516
+ for (let child = shadowRoot.firstChild; child; child = child.nextSibling) {
6517
+ if (isElementNode(child) && isSlotElement(child)) {
6518
+ this.cloneSlotElementAsLightDOM(child, clone, copyStyles);
6519
+ }
6520
+ else {
6521
+ this.appendChildNode(clone, child, copyStyles);
6522
+ }
6523
+ }
6524
+ }
6525
+ /**
6526
+ * Clone child nodes from source element to clone element
6527
+ * Handles shadow DOM, slots, and light DOM appropriately
6528
+ */
6529
+ cloneChildNodes(node, clone, copyStyles) {
6530
+ if (node.shadowRoot && clone.shadowRoot) {
6531
+ // Both original and clone have shadow roots - clone shadow DOM content
6532
+ this.cloneShadowDOMChildren(node.shadowRoot, clone.shadowRoot, copyStyles);
6533
+ // Also clone light DOM (slot content sources)
6534
+ this.cloneLightDOMChildren(node, clone, copyStyles);
6535
+ }
6536
+ else if (node.shadowRoot && !clone.shadowRoot) {
6537
+ // Original has shadow root but clone doesn't (creation failed)
6538
+ // Fallback: clone shadow DOM content as light DOM to preserve content
6539
+ this.cloneShadowDOMAsLightDOM(node.shadowRoot, clone, copyStyles);
6540
+ }
6541
+ else {
6542
+ // No shadow DOM - just clone light DOM children
6543
+ this.cloneLightDOMChildren(node, clone, copyStyles);
6544
+ }
6545
+ }
6380
6546
  cloneNode(node, copyStyles) {
6381
6547
  if (isTextNode(node)) {
6382
6548
  return document.createTextNode(node.data);
@@ -7644,6 +7810,28 @@
7644
7810
  }, text.bounds.left);
7645
7811
  }
7646
7812
  }
7813
+ /**
7814
+ * Helper method to render text with paint order support
7815
+ * Reduces code duplication in line-clamp and normal rendering
7816
+ */
7817
+ renderTextBoundWithPaintOrder(textBound, styles, paintOrderLayers) {
7818
+ paintOrderLayers.forEach((paintOrderLayer) => {
7819
+ switch (paintOrderLayer) {
7820
+ case 0 /* PAINT_ORDER_LAYER.FILL */:
7821
+ this.ctx.fillStyle = asString(styles.color);
7822
+ this.renderTextWithLetterSpacing(textBound, styles.letterSpacing, styles.fontSize.number);
7823
+ break;
7824
+ case 1 /* PAINT_ORDER_LAYER.STROKE */:
7825
+ if (styles.webkitTextStrokeWidth && textBound.text.trim().length) {
7826
+ this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
7827
+ this.ctx.lineWidth = styles.webkitTextStrokeWidth;
7828
+ this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
7829
+ this.renderTextWithLetterSpacing(textBound, styles.letterSpacing, styles.fontSize.number);
7830
+ }
7831
+ break;
7832
+ }
7833
+ });
7834
+ }
7647
7835
  renderTextDecoration(bounds, styles) {
7648
7836
  this.ctx.fillStyle = asString(styles.textDecorationColor || styles.color);
7649
7837
  // Calculate decoration line thickness
@@ -7793,24 +7981,120 @@
7793
7981
  this.ctx.textAlign = 'left';
7794
7982
  this.ctx.textBaseline = 'alphabetic';
7795
7983
  const paintOrder = styles.paintOrder;
7984
+ // Calculate line height for text layout detection (used by both line-clamp and ellipsis)
7985
+ const lineHeight = styles.fontSize.number * 1.5;
7986
+ // Check if we need to apply -webkit-line-clamp
7987
+ // This limits text to a specific number of lines with ellipsis
7988
+ const shouldApplyLineClamp = styles.webkitLineClamp > 0 &&
7989
+ (styles.display & 2 /* DISPLAY.BLOCK */) !== 0 &&
7990
+ styles.overflowY === 1 /* OVERFLOW.HIDDEN */ &&
7991
+ text.textBounds.length > 0;
7992
+ if (shouldApplyLineClamp) {
7993
+ // Group text bounds by lines based on their Y position
7994
+ const lines = [];
7995
+ let currentLine = [];
7996
+ let currentLineTop = text.textBounds[0].bounds.top;
7997
+ text.textBounds.forEach((tb) => {
7998
+ // If this text bound is on a different line, start a new line
7999
+ if (Math.abs(tb.bounds.top - currentLineTop) >= lineHeight * 0.5) {
8000
+ if (currentLine.length > 0) {
8001
+ lines.push(currentLine);
8002
+ }
8003
+ currentLine = [tb];
8004
+ currentLineTop = tb.bounds.top;
8005
+ }
8006
+ else {
8007
+ currentLine.push(tb);
8008
+ }
8009
+ });
8010
+ // Don't forget the last line
8011
+ if (currentLine.length > 0) {
8012
+ lines.push(currentLine);
8013
+ }
8014
+ // Only render up to webkitLineClamp lines
8015
+ const maxLines = styles.webkitLineClamp;
8016
+ if (lines.length > maxLines) {
8017
+ // Render only the first (maxLines - 1) complete lines
8018
+ for (let i = 0; i < maxLines - 1; i++) {
8019
+ lines[i].forEach((textBound) => {
8020
+ this.renderTextBoundWithPaintOrder(textBound, styles, paintOrder);
8021
+ });
8022
+ }
8023
+ // For the last line, truncate with ellipsis
8024
+ const lastLine = lines[maxLines - 1];
8025
+ if (lastLine && lastLine.length > 0 && containerBounds) {
8026
+ const lastLineText = lastLine.map((tb) => tb.text).join('');
8027
+ const firstBound = lastLine[0];
8028
+ const availableWidth = containerBounds.width - (firstBound.bounds.left - containerBounds.left);
8029
+ const truncatedText = this.truncateTextWithEllipsis(lastLineText, availableWidth, styles.letterSpacing);
8030
+ paintOrder.forEach((paintOrderLayer) => {
8031
+ switch (paintOrderLayer) {
8032
+ case 0 /* PAINT_ORDER_LAYER.FILL */:
8033
+ this.ctx.fillStyle = asString(styles.color);
8034
+ if (styles.letterSpacing === 0) {
8035
+ this.ctx.fillText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
8036
+ }
8037
+ else {
8038
+ const letters = segmentGraphemes(truncatedText);
8039
+ letters.reduce((left, letter) => {
8040
+ this.ctx.fillText(letter, left, firstBound.bounds.top + styles.fontSize.number);
8041
+ return left + this.ctx.measureText(letter).width + styles.letterSpacing;
8042
+ }, firstBound.bounds.left);
8043
+ }
8044
+ break;
8045
+ case 1 /* PAINT_ORDER_LAYER.STROKE */:
8046
+ if (styles.webkitTextStrokeWidth && truncatedText.trim().length) {
8047
+ this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
8048
+ this.ctx.lineWidth = styles.webkitTextStrokeWidth;
8049
+ this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
8050
+ if (styles.letterSpacing === 0) {
8051
+ this.ctx.strokeText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
8052
+ }
8053
+ else {
8054
+ const letters = segmentGraphemes(truncatedText);
8055
+ letters.reduce((left, letter) => {
8056
+ this.ctx.strokeText(letter, left, firstBound.bounds.top + styles.fontSize.number);
8057
+ return left + this.ctx.measureText(letter).width + styles.letterSpacing;
8058
+ }, firstBound.bounds.left);
8059
+ }
8060
+ }
8061
+ break;
8062
+ }
8063
+ });
8064
+ }
8065
+ return; // Don't render anything else
8066
+ }
8067
+ // If lines.length <= maxLines, fall through to normal rendering
8068
+ }
7796
8069
  // Check if we need to apply text-overflow: ellipsis
7797
- const shouldApplyEllipsis = styles.textOverflow === 1 /* TEXT_OVERFLOW.ELLIPSIS */ && containerBounds && styles.overflowX === 1 /* OVERFLOW.HIDDEN */;
8070
+ // Issue #203: Only apply ellipsis for single-line text overflow
8071
+ // Multi-line text truncation (like -webkit-line-clamp) should not be affected
8072
+ const shouldApplyEllipsis = styles.textOverflow === 1 /* TEXT_OVERFLOW.ELLIPSIS */ &&
8073
+ containerBounds &&
8074
+ styles.overflowX === 1 /* OVERFLOW.HIDDEN */ &&
8075
+ text.textBounds.length > 0;
7798
8076
  // Calculate total text width if ellipsis might be needed
7799
8077
  let needsEllipsis = false;
7800
8078
  let truncatedText = '';
7801
- if (shouldApplyEllipsis && text.textBounds.length > 0) {
7802
- // Measure the full text content
7803
- // Note: text.textBounds may contain whitespace characters from HTML formatting
7804
- // We need to collapse them like the browser does for white-space: nowrap
7805
- let fullText = text.textBounds.map((tb) => tb.text).join('');
7806
- // Collapse whitespace: replace sequences of whitespace (including newlines) with single spaces
7807
- // and trim leading/trailing whitespace
7808
- fullText = fullText.replace(/\s+/g, ' ').trim();
7809
- const fullTextWidth = this.ctx.measureText(fullText).width;
7810
- const availableWidth = containerBounds.width;
7811
- if (fullTextWidth > availableWidth) {
7812
- needsEllipsis = true;
7813
- truncatedText = this.truncateTextWithEllipsis(fullText, availableWidth, styles.letterSpacing);
8079
+ if (shouldApplyEllipsis) {
8080
+ // Check if all text bounds are on approximately the same line (single-line scenario)
8081
+ // For multi-line text (like -webkit-line-clamp), textBounds will have different Y positions
8082
+ const firstTop = text.textBounds[0].bounds.top;
8083
+ const isSingleLine = text.textBounds.every((tb) => Math.abs(tb.bounds.top - firstTop) < lineHeight * 0.5);
8084
+ if (isSingleLine) {
8085
+ // Measure the full text content
8086
+ // Note: text.textBounds may contain whitespace characters from HTML formatting
8087
+ // We need to collapse them like the browser does for white-space: nowrap
8088
+ let fullText = text.textBounds.map((tb) => tb.text).join('');
8089
+ // Collapse whitespace: replace sequences of whitespace (including newlines) with single spaces
8090
+ // and trim leading/trailing whitespace
8091
+ fullText = fullText.replace(/\s+/g, ' ').trim();
8092
+ const fullTextWidth = this.ctx.measureText(fullText).width;
8093
+ const availableWidth = containerBounds.width;
8094
+ if (fullTextWidth > availableWidth) {
8095
+ needsEllipsis = true;
8096
+ truncatedText = this.truncateTextWithEllipsis(fullText, availableWidth, styles.letterSpacing);
8097
+ }
7814
8098
  }
7815
8099
  }
7816
8100
  // If ellipsis is needed, render the truncated text once