pagyra-js 0.0.21 → 0.0.23

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 (92) hide show
  1. package/README.md +283 -264
  2. package/dist/browser/pagyra.min.js +30 -30
  3. package/dist/browser/pagyra.min.js.map +4 -4
  4. package/dist/src/css/apply-declarations.js +2 -1
  5. package/dist/src/css/clip-path-types.d.ts +9 -1
  6. package/dist/src/css/compute-style/overrides.js +10 -1
  7. package/dist/src/css/parsers/clip-path-parser.js +51 -0
  8. package/dist/src/css/parsers/register-parsers.js +21 -0
  9. package/dist/src/css/properties/visual.d.ts +2 -0
  10. package/dist/src/css/style.d.ts +5 -0
  11. package/dist/src/css/style.js +3 -0
  12. package/dist/src/css/ua-defaults/element-defaults.js +13 -0
  13. package/dist/src/dom/node.d.ts +2 -0
  14. package/dist/src/dom/node.js +1 -0
  15. package/dist/src/fonts/woff2/decoder.d.ts +1 -9
  16. package/dist/src/fonts/woff2/decoder.js +6 -565
  17. package/dist/src/fonts/woff2/glyf-reconstructor.d.ts +54 -0
  18. package/dist/src/fonts/woff2/glyf-reconstructor.js +357 -0
  19. package/dist/src/fonts/woff2/hmtx-reconstructor.d.ts +5 -0
  20. package/dist/src/fonts/woff2/hmtx-reconstructor.js +42 -0
  21. package/dist/src/fonts/woff2/sfnt-builder.d.ts +7 -0
  22. package/dist/src/fonts/woff2/sfnt-builder.js +55 -0
  23. package/dist/src/fonts/woff2/utils.d.ts +12 -0
  24. package/dist/src/fonts/woff2/utils.js +111 -0
  25. package/dist/src/html-to-pdf/render-finalize.js +5 -1
  26. package/dist/src/layout/inline/run-placer.js +1 -1
  27. package/dist/src/layout/strategies/flex/alignment.d.ts +10 -0
  28. package/dist/src/layout/strategies/flex/alignment.js +91 -0
  29. package/dist/src/layout/strategies/flex/distributor.d.ts +5 -0
  30. package/dist/src/layout/strategies/flex/distributor.js +56 -0
  31. package/dist/src/layout/strategies/flex/line-builder.d.ts +5 -0
  32. package/dist/src/layout/strategies/flex/line-builder.js +55 -0
  33. package/dist/src/layout/strategies/flex/types.d.ts +27 -0
  34. package/dist/src/layout/strategies/flex/types.js +2 -0
  35. package/dist/src/layout/strategies/flex/utils.d.ts +12 -0
  36. package/dist/src/layout/strategies/flex/utils.js +113 -0
  37. package/dist/src/layout/strategies/flex.js +4 -308
  38. package/dist/src/layout/strategies/grid.js +0 -3
  39. package/dist/src/layout/strategies/table.js +85 -58
  40. package/dist/src/layout/utils/text-metrics.js +16 -8
  41. package/dist/src/pdf/font/embedder.js +3 -3
  42. package/dist/src/pdf/font/font-subset.js +1 -3
  43. package/dist/src/pdf/font/to-unicode.js +16 -16
  44. package/dist/src/pdf/layout-tree-builder.js +15 -9
  45. package/dist/src/pdf/renderer/box-painter.js +74 -9
  46. package/dist/src/pdf/renderers/text-renderer.d.ts +4 -2
  47. package/dist/src/pdf/renderers/text-renderer.js +52 -2
  48. package/dist/src/pdf/types.d.ts +16 -1
  49. package/dist/src/pdf/utils/clip-path-resolver.js +28 -12
  50. package/dist/src/pdf/utils/mask-resolver.d.ts +7 -0
  51. package/dist/src/pdf/utils/mask-resolver.js +25 -0
  52. package/dist/src/pdf/utils/node-text-run-factory.d.ts +2 -1
  53. package/dist/src/pdf/utils/node-text-run-factory.js +5 -26
  54. package/dist/src/pdf/utils/rounded-rect-to-path.d.ts +7 -0
  55. package/dist/src/pdf/utils/rounded-rect-to-path.js +86 -0
  56. package/dist/src/render/offset.d.ts +5 -0
  57. package/dist/src/render/offset.js +93 -9
  58. package/dist/src/text/line-breaker.js +31 -0
  59. package/dist/tests/css/clip-path-parser.spec.js +15 -8
  60. package/dist/tests/environment/path-resolution.spec.js +2 -1
  61. package/dist/tests/helpers/ai-layout-diagnostics.js +6 -6
  62. package/dist/tests/layout/container-query-units.spec.js +0 -7
  63. package/dist/tests/layout/inline-background-alignment.spec.js +6 -6
  64. package/dist/tests/layout/table-image-cell.spec.js +95 -0
  65. package/dist/tests/pdf/alignments.spec.js +12 -12
  66. package/dist/tests/pdf/clip-path.spec.js +3 -1
  67. package/dist/tests/pdf/form-text-encoding.spec.js +1 -1
  68. package/dist/tests/pdf/svg-stroke-dash.spec.js +8 -8
  69. package/dist/tests/pdf/text-transform-matrix.spec.js +1 -1
  70. package/dist/tests/pdf/xref-integrity.spec.js +1 -1
  71. package/dist/tests/verify-subset-multi.spec.js +14 -14
  72. package/dist/tests/verify-subset.spec.js +12 -12
  73. package/package.json +89 -71
  74. package/dist/src/image/js-png-backend.d.ts +0 -7
  75. package/dist/src/image/js-png-backend.js +0 -9
  76. package/dist/src/image/png-backend.d.ts +0 -5
  77. package/dist/src/image/png-wasm-loader.d.ts +0 -5
  78. package/dist/src/image/png-wasm-loader.js +0 -59
  79. package/dist/src/image/wasm/png_decoder_wasm.d.ts +0 -8
  80. package/dist/src/image/wasm/png_decoder_wasm.js +0 -24
  81. package/dist/src/image/wasm/png_decoder_wasm_bg.js +0 -16
  82. package/dist/src/image/wasm-png-backend.d.ts +0 -6
  83. package/dist/src/image/wasm-png-backend.js +0 -17
  84. package/dist/src/layout/table/cell_layout.d.ts +0 -2
  85. package/dist/src/layout/table/cell_layout.js +0 -26
  86. package/dist/tests/image/png-backend.spec.d.ts +0 -1
  87. package/dist/tests/image/png-backend.spec.js +0 -34
  88. package/dist/tests/pdf/font-subset-registry-key.spec.d.ts +0 -1
  89. package/dist/tests/pdf/font-subset-registry-key.spec.js +0 -66
  90. package/dist/tests/pdf/header-footer.spec.d.ts +0 -1
  91. package/dist/tests/pdf/header-footer.spec.js +0 -46
  92. /package/dist/{src/image/png-backend.js → tests/layout/table-image-cell.spec.d.ts} +0 -0
@@ -3,183 +3,10 @@ import { AlignItems, Display, JustifyContent } from "../../css/enums.js";
3
3
  import { adjustForBoxSizing, containingBlock, resolveBoxMetrics, resolveWidthBlock } from "../utils/node-math.js";
4
4
  import { resolveLength, isAutoLength } from "../../css/length.js";
5
5
  import { GapCalculator, calculateTotalGap } from "../utils/gap-calculator.js";
6
- function blockifyFlexItemDisplay(display) {
7
- switch (display) {
8
- case Display.Inline:
9
- case Display.InlineBlock:
10
- return Display.Block;
11
- case Display.InlineFlex:
12
- return Display.Flex;
13
- case Display.InlineGrid:
14
- return Display.Grid;
15
- case Display.InlineTable:
16
- return Display.Table;
17
- default:
18
- return display;
19
- }
20
- }
21
- function allowsPreferredShrink(originalDisplay, effectiveDisplay) {
22
- switch (effectiveDisplay) {
23
- case Display.Flex:
24
- case Display.InlineFlex:
25
- case Display.Grid:
26
- case Display.InlineGrid:
27
- return false;
28
- default:
29
- break;
30
- }
31
- if (effectiveDisplay === originalDisplay) {
32
- return true;
33
- }
34
- return originalDisplay === Display.Inline;
35
- }
36
- function buildFlexLines(items, containerMainSize, mainAxisGap) {
37
- if (items.length === 0) {
38
- return [];
39
- }
40
- const lines = [];
41
- let current = { items: [], mainSizeWithGaps: 0, crossSize: 0 };
42
- for (const item of items) {
43
- const addition = current.items.length === 0 ? item.mainContribution : mainAxisGap + item.mainContribution;
44
- const shouldWrap = current.items.length > 0 &&
45
- containerMainSize > 0 &&
46
- current.mainSizeWithGaps + addition > containerMainSize + 0.01;
47
- if (shouldWrap) {
48
- lines.push(current);
49
- current = { items: [], mainSizeWithGaps: 0, crossSize: 0 };
50
- }
51
- if (current.items.length > 0) {
52
- current.mainSizeWithGaps += mainAxisGap;
53
- }
54
- current.items.push(item);
55
- current.mainSizeWithGaps += item.mainContribution;
56
- current.crossSize = Math.max(current.crossSize, item.crossContribution);
57
- }
58
- if (current.items.length > 0) {
59
- lines.push(current);
60
- }
61
- return lines;
62
- }
63
- function calculateLinesCrossSize(lines, crossAxisGap) {
64
- if (lines.length === 0) {
65
- return 0;
66
- }
67
- const totalLines = lines.reduce((sum, line) => sum + line.crossSize, 0);
68
- return totalLines + calculateTotalGap(crossAxisGap, lines.length);
69
- }
70
- function refreshFlexItemSizes(item, isRow) {
71
- item.mainSize = isRow ? item.node.box.borderBoxWidth : item.node.box.borderBoxHeight;
72
- item.crossSize = isRow ? item.node.box.borderBoxHeight : item.node.box.borderBoxWidth;
73
- item.mainContribution = item.mainSize + item.mainMarginStart + item.mainMarginEnd;
74
- item.crossContribution = item.crossSize + item.crossMarginStart + item.crossMarginEnd;
75
- }
76
- function recomputeLineMetrics(line, mainAxisGap) {
77
- let mainSizeWithGaps = 0;
78
- let crossSize = 0;
79
- for (let i = 0; i < line.items.length; i++) {
80
- const item = line.items[i];
81
- if (i > 0) {
82
- mainSizeWithGaps += mainAxisGap;
83
- }
84
- mainSizeWithGaps += item.mainContribution;
85
- crossSize = Math.max(crossSize, item.crossContribution);
86
- }
87
- line.mainSizeWithGaps = mainSizeWithGaps;
88
- line.crossSize = crossSize;
89
- }
90
- function relayoutFlexItemForMainSize(container, item, context, targetMainSize, isRow) {
91
- if (!Number.isFinite(targetMainSize) || targetMainSize < 0) {
92
- return;
93
- }
94
- const prevContainerWidth = container.box.contentWidth;
95
- const prevContainerHeight = container.box.contentHeight;
96
- const targetContribution = targetMainSize + item.mainMarginStart + item.mainMarginEnd;
97
- let displayMutated = false;
98
- if (item.node.style.display !== item.effectiveDisplay) {
99
- item.node.style.display = item.effectiveDisplay;
100
- displayMutated = true;
101
- }
102
- try {
103
- if (isRow) {
104
- container.box.contentWidth = Math.max(0, targetContribution);
105
- }
106
- else {
107
- container.box.contentHeight = Math.max(0, targetContribution);
108
- }
109
- context.layoutChild(item.node);
110
- }
111
- finally {
112
- container.box.contentWidth = prevContainerWidth;
113
- container.box.contentHeight = prevContainerHeight;
114
- if (displayMutated) {
115
- item.node.style.display = item.originalDisplay;
116
- }
117
- }
118
- }
119
- function distributeFlexGrowAcrossLines(lines, container, context, containerMainSize, mainAxisGap, isRow) {
120
- if (lines.length === 0 || !(containerMainSize > 0)) {
121
- return;
122
- }
123
- for (const line of lines) {
124
- const freeSpace = containerMainSize - line.mainSizeWithGaps;
125
- if (!(freeSpace > 0)) {
126
- continue;
127
- }
128
- const growItems = line.items.filter((item) => item.flexGrow > 0);
129
- const totalGrow = growItems.reduce((sum, item) => sum + item.flexGrow, 0);
130
- if (!(totalGrow > 0)) {
131
- continue;
132
- }
133
- for (const item of growItems) {
134
- const delta = (freeSpace * item.flexGrow) / totalGrow;
135
- const targetMainSize = Math.max(0, item.mainSize + delta);
136
- if (Math.abs(targetMainSize - item.mainSize) < 0.01) {
137
- continue;
138
- }
139
- relayoutFlexItemForMainSize(container, item, context, targetMainSize, isRow);
140
- refreshFlexItemSizes(item, isRow);
141
- }
142
- recomputeLineMetrics(line, mainAxisGap);
143
- }
144
- }
145
- function resolveAlignContentLayout(lines, alignContent, containerCrossSize, crossAxisGap) {
146
- const lineCrossSizes = lines.map((line) => line.crossSize);
147
- if (lines.length === 0) {
148
- return { lineCrossSizes, initialOffset: 0, additionalGap: 0 };
149
- }
150
- const naturalCross = calculateLinesCrossSize(lines, crossAxisGap);
151
- const freeSpace = Math.max(0, containerCrossSize - naturalCross);
152
- switch (alignContent) {
153
- case "stretch":
154
- if (freeSpace > 0) {
155
- const extraPerLine = freeSpace / lines.length;
156
- for (let i = 0; i < lineCrossSizes.length; i++) {
157
- lineCrossSizes[i] += extraPerLine;
158
- }
159
- }
160
- return { lineCrossSizes, initialOffset: 0, additionalGap: 0 };
161
- case "flex-end":
162
- return { lineCrossSizes, initialOffset: freeSpace, additionalGap: 0 };
163
- case "center":
164
- return { lineCrossSizes, initialOffset: freeSpace / 2, additionalGap: 0 };
165
- case "space-between":
166
- if (lines.length <= 1) {
167
- return { lineCrossSizes, initialOffset: 0, additionalGap: 0 };
168
- }
169
- return { lineCrossSizes, initialOffset: 0, additionalGap: freeSpace / (lines.length - 1) };
170
- case "space-around": {
171
- const gap = freeSpace / lines.length;
172
- return { lineCrossSizes, initialOffset: gap / 2, additionalGap: gap };
173
- }
174
- case "space-evenly": {
175
- const gap = freeSpace / (lines.length + 1);
176
- return { lineCrossSizes, initialOffset: gap, additionalGap: gap };
177
- }
178
- case "flex-start":
179
- default:
180
- return { lineCrossSizes, initialOffset: 0, additionalGap: 0 };
181
- }
182
- }
6
+ import { blockifyFlexItemDisplay, allowsPreferredShrink, isRowDirection, isAutoMainSize, computePreferredInlineWidth, offsetLayoutSubtree, resolveInitialDimension, resolveFlexSize } from "./flex/utils.js";
7
+ import { buildFlexLines, calculateLinesCrossSize } from "./flex/line-builder.js";
8
+ import { distributeFlexGrowAcrossLines } from "./flex/distributor.js";
9
+ import { resolveAlignContentLayout, resolveJustifySpacing, computeCrossOffset, resolveItemAlignment } from "./flex/alignment.js";
183
10
  export class FlexLayoutStrategy {
184
11
  constructor() {
185
12
  this.supportedDisplays = new Set([Display.Flex, Display.InlineFlex]);
@@ -521,134 +348,3 @@ export class FlexLayoutStrategy {
521
348
  node.box.scrollHeight = node.box.contentHeight;
522
349
  }
523
350
  }
524
- function resolveInitialDimension(specified, fallback) {
525
- if (specified !== undefined && Number.isFinite(specified)) {
526
- return Math.max(specified, 0);
527
- }
528
- if (Number.isFinite(fallback)) {
529
- return Math.max(fallback, 0);
530
- }
531
- return 0;
532
- }
533
- function resolveFlexSize(value, reference, containerWidth = reference, containerHeight = reference) {
534
- if (value === undefined) {
535
- return undefined;
536
- }
537
- if (typeof value === "number") {
538
- return value;
539
- }
540
- if (value === "auto" || isAutoLength(value)) {
541
- return undefined;
542
- }
543
- return resolveLength(value, reference, { auto: "reference", containerWidth, containerHeight });
544
- }
545
- function resolveItemAlignment(alignSelf, containerAlign) {
546
- if (alignSelf && alignSelf !== "auto") {
547
- return alignSelf;
548
- }
549
- return containerAlign;
550
- }
551
- function computeCrossOffset(alignment, containerSize, itemSize, marginStart, marginEnd) {
552
- const total = itemSize + marginStart + marginEnd;
553
- const reference = Number.isFinite(containerSize) ? containerSize : total;
554
- const freeSpace = reference - total;
555
- if (freeSpace <= 0) {
556
- return 0;
557
- }
558
- switch (alignment) {
559
- case AlignItems.Center:
560
- return freeSpace / 2;
561
- case AlignItems.FlexEnd:
562
- return freeSpace;
563
- default:
564
- return 0;
565
- }
566
- }
567
- function resolveJustifySpacing(justify, freeSpace, itemCount) {
568
- if (itemCount <= 0) {
569
- return { offset: 0, gap: 0 };
570
- }
571
- const clamped = Number.isFinite(freeSpace) ? Math.max(freeSpace, 0) : 0;
572
- switch (justify) {
573
- case JustifyContent.Center:
574
- return { offset: clamped / 2, gap: 0 };
575
- case JustifyContent.FlexEnd:
576
- case JustifyContent.End:
577
- case JustifyContent.Right:
578
- return { offset: clamped, gap: 0 };
579
- case JustifyContent.SpaceBetween:
580
- if (itemCount === 1) {
581
- return { offset: 0, gap: 0 };
582
- }
583
- return { offset: 0, gap: clamped / (itemCount - 1) };
584
- case JustifyContent.SpaceAround: {
585
- const gap = clamped / itemCount;
586
- return { offset: gap / 2, gap };
587
- }
588
- case JustifyContent.SpaceEvenly: {
589
- const gap = clamped / (itemCount + 1);
590
- return { offset: gap, gap };
591
- }
592
- default:
593
- return { offset: 0, gap: 0 };
594
- }
595
- }
596
- function isRowDirection(direction) {
597
- return direction === "row" || direction === "row-reverse";
598
- }
599
- function isAutoMainSize(value) {
600
- if (value === undefined) {
601
- return true;
602
- }
603
- if (typeof value === "number") {
604
- return false;
605
- }
606
- if (value === "auto") {
607
- return true;
608
- }
609
- return isAutoLength(value);
610
- }
611
- function computePreferredInlineWidth(node) {
612
- let maxWidth = 0;
613
- node.walk((desc) => {
614
- if (desc.inlineRuns && desc.inlineRuns.length > 0) {
615
- const localMax = desc.inlineRuns.reduce((max, run) => Math.max(max, run.lineWidth ?? run.width), 0);
616
- if (localMax > maxWidth) {
617
- maxWidth = localMax;
618
- }
619
- return;
620
- }
621
- if (!desc.lineBoxes || desc.lineBoxes.length === 0) {
622
- return;
623
- }
624
- for (const line of desc.lineBoxes) {
625
- if (!line) {
626
- continue;
627
- }
628
- const width = typeof line.width === "number" ? line.width : 0;
629
- if (width > maxWidth) {
630
- maxWidth = width;
631
- }
632
- }
633
- });
634
- return maxWidth > 0 ? maxWidth : undefined;
635
- }
636
- function offsetLayoutSubtree(node, deltaX, deltaY) {
637
- if (deltaX === 0 && deltaY === 0) {
638
- return;
639
- }
640
- node.walk((desc) => {
641
- if (desc !== node) {
642
- desc.box.x += deltaX;
643
- desc.box.y += deltaY;
644
- }
645
- desc.box.baseline += deltaY;
646
- // Update inline runs if they exist
647
- if (desc.inlineRuns && desc.inlineRuns.length > 0) {
648
- for (const run of desc.inlineRuns) {
649
- run.startX += deltaX;
650
- run.baseline += deltaY;
651
- }
652
- }
653
- });
654
- }
@@ -161,7 +161,6 @@ export class GridLayoutStrategy {
161
161
  node.box.marginBoxWidth = node.box.borderBoxWidth + horizontalMargin(node, resolvedContentWidth, cb.height);
162
162
  const columnOffsets = calculateTrackOffsets(columnWidths, columnGap);
163
163
  const rows = [];
164
- let currentRowTop = contentOriginY;
165
164
  let currentRowHeight = 0;
166
165
  let columnIndex = 0;
167
166
  let currentRowItems = [];
@@ -174,7 +173,6 @@ export class GridLayoutStrategy {
174
173
  if (columnIndex + span > columnWidths.length) {
175
174
  if (currentRowItems.length > 0) {
176
175
  rows.push({ items: currentRowItems, height: currentRowHeight });
177
- currentRowTop += currentRowHeight + rowGap;
178
176
  currentRowHeight = 0;
179
177
  currentRowItems = [];
180
178
  }
@@ -203,7 +201,6 @@ export class GridLayoutStrategy {
203
201
  columnIndex += span;
204
202
  if (columnIndex >= columnWidths.length) {
205
203
  rows.push({ items: currentRowItems, height: currentRowHeight });
206
- currentRowTop += currentRowHeight + rowGap;
207
204
  currentRowHeight = 0;
208
205
  columnIndex = 0;
209
206
  currentRowItems = [];
@@ -3,7 +3,6 @@ import { LayoutNode } from "../../dom/node.js";
3
3
  import { log } from "../../logging/debug.js";
4
4
  import { resolveLength } from "../../css/length.js";
5
5
  import { adjustForBoxSizing, containingBlock, horizontalNonContent, resolveWidthBlock, verticalNonContent } from "../utils/node-math.js";
6
- import { layoutTableCell } from "../table/cell_layout.js";
7
6
  import { auditTableCell, debugTableCell } from "../table/diagnostics.js";
8
7
  export class TableLayoutStrategy {
9
8
  constructor() {
@@ -42,22 +41,8 @@ export class TableLayoutStrategy {
42
41
  if (!cell || !this.isOriginCell(cell, r, c))
43
42
  continue;
44
43
  const row = cell.parent;
45
- // Set default border width and color for table cells if not set
46
- const isTableCell = cell.tagName === 'td' || cell.tagName === 'th';
47
- if (isTableCell) {
48
- if (cell.style.borderTop === undefined)
49
- cell.style.borderTop = collapsedBorders ? 0 : 1;
50
- if (cell.style.borderRight === undefined)
51
- cell.style.borderRight = collapsedBorders ? 0 : 1;
52
- if (cell.style.borderBottom === undefined)
53
- cell.style.borderBottom = collapsedBorders ? 0 : 1;
54
- if (cell.style.borderLeft === undefined)
55
- cell.style.borderLeft = collapsedBorders ? 0 : 1;
56
- if (cell.style.borderColor === undefined)
57
- cell.style.borderColor = '#000';
58
- }
59
44
  // Inherit from row/table if still not set
60
- if (cell.style.borderTop === undefined) {
45
+ if (cell.style.borderTop === undefined || cell.style.borderTop === 0) {
61
46
  if (row && row.style.borderTop !== undefined && row.style.borderTop !== 0) {
62
47
  cell.style.borderTop = row.style.borderTop;
63
48
  }
@@ -65,7 +50,7 @@ export class TableLayoutStrategy {
65
50
  cell.style.borderTop = node.style.borderTop;
66
51
  }
67
52
  }
68
- if (cell.style.borderRight === undefined) {
53
+ if (cell.style.borderRight === undefined || cell.style.borderRight === 0) {
69
54
  if (row && row.style.borderRight !== undefined && row.style.borderRight !== 0) {
70
55
  cell.style.borderRight = row.style.borderRight;
71
56
  }
@@ -73,7 +58,7 @@ export class TableLayoutStrategy {
73
58
  cell.style.borderRight = node.style.borderRight;
74
59
  }
75
60
  }
76
- if (cell.style.borderBottom === undefined) {
61
+ if (cell.style.borderBottom === undefined || cell.style.borderBottom === 0) {
77
62
  if (row && row.style.borderBottom !== undefined && row.style.borderBottom !== 0) {
78
63
  cell.style.borderBottom = row.style.borderBottom;
79
64
  }
@@ -81,7 +66,7 @@ export class TableLayoutStrategy {
81
66
  cell.style.borderBottom = node.style.borderBottom;
82
67
  }
83
68
  }
84
- if (cell.style.borderLeft === undefined) {
69
+ if (cell.style.borderLeft === undefined || cell.style.borderLeft === 0) {
85
70
  if (row && row.style.borderLeft !== undefined && row.style.borderLeft !== 0) {
86
71
  cell.style.borderLeft = row.style.borderLeft;
87
72
  }
@@ -243,13 +228,20 @@ export class TableLayoutStrategy {
243
228
  cell.box.y = 0;
244
229
  cell.box.contentWidth = cellAvailableWidth;
245
230
  debugTableCell(cell);
246
- layoutTableCell(cell);
231
+ // Lay the cell out through the normal engine (BlockLayoutStrategy handles table-cell):
232
+ // this runs a real inline formatting context so mixed inline content (text + <b> + <a>)
233
+ // wraps and is positioned, and replaced content (<img>) gets sized by ImageLayoutStrategy.
234
+ // The cell resolves its content width from its containing block (the row), so temporarily
235
+ // set the row's content width to the cell's spanned column width while laying it out.
236
+ const row = cell.parent;
237
+ const savedRowWidth = row?.box.contentWidth;
238
+ if (row)
239
+ row.box.contentWidth = spannedWidth;
240
+ context.layoutChild(cell);
241
+ if (row && savedRowWidth !== undefined)
242
+ row.box.contentWidth = savedRowWidth;
247
243
  if (cell.textContent?.includes("Row 3, Cell 3"))
248
244
  auditTableCell(cell);
249
- for (const child of cell.children) {
250
- child.box.x = (child.box.x ?? 0) + boxMetrics.paddingLeft;
251
- child.box.y = (child.box.y ?? 0) + boxMetrics.paddingTop;
252
- }
253
245
  if (cell.style.textAlign || cell.style.verticalAlign) {
254
246
  for (const child of cell.children) {
255
247
  if (cell.style.textAlign)
@@ -302,21 +294,31 @@ export class TableLayoutStrategy {
302
294
  // The cell's border box starts at colOffsets[c], NOT at the content position
303
295
  const borderBoxX = node.box.x + colOffsets[c];
304
296
  const borderBoxY = node.box.y + rowOffsets[r];
305
- // Content position is inside border and padding
306
- const contentX = borderBoxX + boxMetrics.borderLeft + boxMetrics.paddingLeft;
307
- const contentY = borderBoxY + boxMetrics.borderTop + boxMetrics.paddingTop;
308
- // Calculate the offset from the cell's position during layout (which was 0,0)
309
- const deltaX = contentX - cell.box.x;
310
- const deltaY = contentY - cell.box.y;
297
+ // The cell was laid out by BlockLayoutStrategy with its border-box origin at (0,0),
298
+ // which already offsets its content by the cell's own border + padding. So the shift
299
+ // applied to descendants is the border-box delta (NOT the content delta), otherwise
300
+ // the cell's padding/border would be counted twice.
301
+ const deltaX = borderBoxX - cell.box.x;
302
+ const deltaY = borderBoxY - cell.box.y;
311
303
  // Set cell position to border box position (not content position)
312
304
  cell.box.x = borderBoxX;
313
305
  cell.box.y = borderBoxY;
314
- // Apply the content offset to all of the cell's descendants, plus any vertical-align offset
306
+ // Apply the border-box offset to all of the cell's descendants, plus any vertical-align
307
+ // offset. Inline runs produced by the inline formatter carry their own baked-in
308
+ // coordinates (startX/baseline), so they must be shifted too — otherwise wrapped cell
309
+ // text stays at its (0,0) layout origin while images (positioned via box) move correctly.
310
+ const shiftY = deltaY + alignOffsetY;
315
311
  cell.walk((descendant) => {
316
312
  descendant.box.x += deltaX;
317
- descendant.box.y += deltaY + alignOffsetY;
313
+ descendant.box.y += shiftY;
318
314
  if (descendant.box.baseline !== undefined) {
319
- descendant.box.baseline += deltaY + alignOffsetY;
315
+ descendant.box.baseline += shiftY;
316
+ }
317
+ if (descendant.inlineRuns) {
318
+ for (const run of descendant.inlineRuns) {
319
+ run.startX += deltaX;
320
+ run.baseline += shiftY;
321
+ }
320
322
  }
321
323
  }, false);
322
324
  // Set border box dimensions
@@ -434,55 +436,80 @@ export class TableLayoutStrategy {
434
436
  const numCols = grid[0]?.length || 0;
435
437
  if (numCols === 0)
436
438
  return [];
437
- const minContentWidths = new Array(numCols).fill(0);
439
+ // Per-column min-content (longest unbreakable word) and max-content (preferred,
440
+ // whole text on one line) widths, following the CSS auto table layout model.
441
+ const minWidths = new Array(numCols).fill(0);
442
+ const maxWidths = new Array(numCols).fill(0);
438
443
  for (let r = 0; r < grid.length; r++) {
439
444
  for (let c = 0; c < numCols; c++) {
440
445
  const cell = grid[r][c];
441
446
  if (!cell || !this.isOriginCell(cell, r, c))
442
447
  continue;
443
- let maxIntrinsicWidth = 0;
444
- if (cell.intrinsicInlineSize) {
445
- maxIntrinsicWidth = cell.intrinsicInlineSize;
446
- }
448
+ let cellMinContent = cell.minIntrinsicInlineSize ?? cell.intrinsicInlineSize ?? 0;
449
+ let cellMaxContent = cell.intrinsicInlineSize ?? 0;
447
450
  cell.walk((node) => {
448
- if (node.intrinsicInlineSize !== undefined) {
449
- maxIntrinsicWidth = Math.max(maxIntrinsicWidth, node.intrinsicInlineSize);
451
+ const nodeMax = node.intrinsicInlineSize;
452
+ if (nodeMax !== undefined) {
453
+ cellMaxContent = Math.max(cellMaxContent, nodeMax);
454
+ // Images and other replaced content do not record a min-content width;
455
+ // they cannot wrap, so their max-content doubles as the min-content.
456
+ const nodeMin = node.minIntrinsicInlineSize ?? nodeMax;
457
+ cellMinContent = Math.max(cellMinContent, nodeMin);
450
458
  }
451
459
  });
452
460
  const horizontalExtras = horizontalNonContent(cell, tableWidth);
453
- const cellMinWidth = maxIntrinsicWidth + horizontalExtras;
461
+ const cellMin = cellMinContent + horizontalExtras;
462
+ const cellMax = cellMaxContent + horizontalExtras;
454
463
  const colSpan = Math.min(this.cellColSpan(cell), numCols - c);
455
464
  if (colSpan === 1) {
456
- minContentWidths[c] = Math.max(minContentWidths[c], cellMinWidth);
465
+ minWidths[c] = Math.max(minWidths[c], cellMin);
466
+ maxWidths[c] = Math.max(maxWidths[c], cellMax);
457
467
  }
458
468
  else {
459
- const share = cellMinWidth / colSpan;
469
+ const minShare = cellMin / colSpan;
470
+ const maxShare = cellMax / colSpan;
460
471
  for (let offset = 0; offset < colSpan; offset++) {
461
- minContentWidths[c + offset] = Math.max(minContentWidths[c + offset], share);
472
+ minWidths[c + offset] = Math.max(minWidths[c + offset], minShare);
473
+ maxWidths[c + offset] = Math.max(maxWidths[c + offset], maxShare);
462
474
  }
463
475
  }
464
476
  }
465
477
  }
466
- const totalMinWidth = minContentWidths.reduce((a, b) => a + b, 0);
467
- if (totalMinWidth < tableWidth) {
468
- const remainingWidth = tableWidth - totalMinWidth;
469
- const weights = minContentWidths.map((w) => (w > 0 ? w : totalMinWidth > 0 ? 0 : 1));
478
+ const totalMin = minWidths.reduce((a, b) => a + b, 0);
479
+ const totalMax = maxWidths.reduce((a, b) => a + b, 0);
480
+ // Preferred widths fit: lay out at max-content and distribute any slack.
481
+ if (totalMax <= tableWidth) {
482
+ if (totalMax <= 0) {
483
+ return new Array(numCols).fill(tableWidth / numCols);
484
+ }
485
+ const remaining = tableWidth - totalMax;
486
+ const weights = maxWidths.map((w) => (w > 0 ? w : 0));
470
487
  const totalWeight = weights.reduce((a, b) => a + b, 0);
471
488
  if (totalWeight > 0) {
472
- return minContentWidths.map((minWidth, i) => {
473
- return minWidth + remainingWidth * (weights[i] / totalWeight);
474
- });
475
- }
476
- else {
477
- return new Array(numCols).fill(tableWidth / numCols);
489
+ return maxWidths.map((w, i) => w + remaining * (weights[i] / totalWeight));
478
490
  }
491
+ return new Array(numCols).fill(tableWidth / numCols);
479
492
  }
480
- else if (totalMinWidth > 0) {
481
- return minContentWidths;
493
+ // Preferred widths overflow but min-content fits: start at min-content and grow each
494
+ // column toward its preferred width by a shared fraction of the leftover space. This lets
495
+ // wide text columns shrink and wrap while fixed/replaced columns keep their natural width.
496
+ if (totalMin <= tableWidth) {
497
+ const growthRoom = totalMax - totalMin;
498
+ const available = tableWidth - totalMin;
499
+ if (growthRoom <= 0) {
500
+ return minWidths;
501
+ }
502
+ const fraction = available / growthRoom;
503
+ return minWidths.map((min, i) => min + (maxWidths[i] - min) * fraction);
482
504
  }
483
- else {
484
- return new Array(numCols).fill(tableWidth / numCols);
505
+ // Even min-content overflows (e.g. an unbreakable URL wider than the page). Scale the columns
506
+ // down proportionally so the table still fits the page; the line-breaker's emergency break then
507
+ // wraps the over-long token inside its column instead of letting it run off the page.
508
+ if (totalMin > 0) {
509
+ const scale = tableWidth / totalMin;
510
+ return minWidths.map((w) => w * scale);
485
511
  }
512
+ return new Array(numCols).fill(tableWidth / numCols);
486
513
  }
487
514
  isOriginCell(cell, row, col) {
488
515
  const origin = cell.tableCellOrigin;
@@ -47,11 +47,13 @@ export function assignIntrinsicTextMetrics(root, fontEmbedder) {
47
47
  const trimmed = node.textContent;
48
48
  if (trimmed.length === 0) {
49
49
  node.intrinsicInlineSize = 0;
50
+ node.minIntrinsicInlineSize = 0;
50
51
  node.intrinsicBlockSize = resolvedLineHeight(node.style);
51
52
  return;
52
53
  }
53
- const { inlineSize, blockSize } = measureText(trimmed, node.style, fontEmbedder);
54
+ const { inlineSize, minInlineSize, blockSize } = measureText(trimmed, node.style, fontEmbedder);
54
55
  node.intrinsicInlineSize = inlineSize;
56
+ node.minIntrinsicInlineSize = minInlineSize;
55
57
  node.intrinsicBlockSize = blockSize;
56
58
  });
57
59
  }
@@ -59,21 +61,27 @@ function measureText(text, style, fontEmbedder) {
59
61
  const effectiveText = applyTextTransform(text, style.textTransform);
60
62
  const lines = effectiveText.split(/\r?\n/);
61
63
  let maxLineWidth = 0;
64
+ // min-content width = widest unbreakable word (longest run with no whitespace).
65
+ // Columns may shrink down to this without forcing a single word to overflow.
66
+ let maxWordWidth = 0;
62
67
  const fontWeight = typeof style.fontWeight === "number" ? style.fontWeight : 400;
63
68
  const fontStyle = style.fontStyle ?? "normal";
64
69
  const fontMetrics = fontEmbedder?.getMetrics(style.fontFamily ?? "", fontWeight, fontStyle);
70
+ const measureSegment = (segment) => {
71
+ const glyphWidth = measureTextWithGlyphs(segment, style, fontMetrics ?? null);
72
+ return glyphWidth !== null ? glyphWidth : estimateLineWidth(segment, style);
73
+ };
65
74
  for (const line of lines) {
66
- const glyphWidth = measureTextWithGlyphs(line, style, fontMetrics ?? null);
67
- if (glyphWidth !== null) {
68
- maxLineWidth = Math.max(maxLineWidth, glyphWidth);
69
- }
70
- else {
71
- maxLineWidth = Math.max(maxLineWidth, estimateLineWidth(line, style));
75
+ maxLineWidth = Math.max(maxLineWidth, measureSegment(line));
76
+ for (const word of line.split(/\s+/)) {
77
+ if (word.length === 0)
78
+ continue;
79
+ maxWordWidth = Math.max(maxWordWidth, measureSegment(word));
72
80
  }
73
81
  }
74
82
  const lineHeight = resolvedLineHeight(style);
75
83
  const blockSize = Math.max(lineHeight, lines.length * lineHeight);
76
- return { inlineSize: maxLineWidth, blockSize };
84
+ return { inlineSize: maxLineWidth, minInlineSize: maxWordWidth, blockSize };
77
85
  }
78
86
  export function estimateLineWidth(line, style) {
79
87
  if (!line) {
@@ -90,7 +90,7 @@ export class FontEmbedder {
90
90
  const fullFontData = new Uint8Array(face.data);
91
91
  const resourceName = `F${this.embeddedFonts.size + 1}`;
92
92
  let realizedRef;
93
- const self = this;
93
+ const realizeFont = this.realizeFont.bind(this);
94
94
  const embedded = {
95
95
  resourceName,
96
96
  baseFont: face.name,
@@ -98,14 +98,14 @@ export class FontEmbedder {
98
98
  subset: fullFontData,
99
99
  get ref() {
100
100
  if (!realizedRef) {
101
- realizedRef = self.realizeFont(face, metrics, resourceName);
101
+ realizedRef = realizeFont(face, metrics);
102
102
  }
103
103
  return realizedRef;
104
104
  }
105
105
  };
106
106
  return embedded;
107
107
  }
108
- realizeFont(face, metrics, resourceName) {
108
+ realizeFont(face, metrics) {
109
109
  const unitsPerEm = metrics.metrics.unitsPerEm;
110
110
  const scaleTo1000 = (v) => Math.round((v / unitsPerEm) * 1000);
111
111
  let fontBBox;