reamkit 1.15.3 → 1.16.0

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 (95) hide show
  1. package/dist/esm/core/converter/facade.d.ts +5 -0
  2. package/dist/esm/core/converter/ream.d.ts +6 -0
  3. package/dist/esm/core/converter/ream.js +34 -8
  4. package/dist/esm/core/document-model/index.d.ts +1 -1
  5. package/dist/esm/core/document-model/types.d.ts +187 -1
  6. package/dist/esm/core/drawingml/chart-geometry.d.ts +28 -0
  7. package/dist/esm/core/drawingml/chart-geometry.js +371 -88
  8. package/dist/esm/core/drawingml/chart-parser.js +262 -11
  9. package/dist/esm/core/drawingml/colors.d.ts +6 -6
  10. package/dist/esm/core/drawingml/colors.js +58 -18
  11. package/dist/esm/core/drawingml/preset-geometry.js +7 -6
  12. package/dist/esm/core/drawingml/sparkline-geometry.js +6 -4
  13. package/dist/esm/core/drawingml/theme-parser.d.ts +41 -0
  14. package/dist/esm/core/drawingml/theme-parser.js +74 -1
  15. package/dist/esm/core/font/ttf-subset.js +26 -1
  16. package/dist/esm/core/images.js +172 -43
  17. package/dist/esm/core/indexed-colors.d.ts +11 -0
  18. package/dist/esm/core/indexed-colors.js +81 -0
  19. package/dist/esm/core/ir/sheet.d.ts +67 -3
  20. package/dist/esm/{excel → core}/number-format.d.ts +37 -0
  21. package/dist/esm/core/number-format.js +800 -0
  22. package/dist/esm/core/opc/alternate-content.d.ts +13 -0
  23. package/dist/esm/core/opc/alternate-content.js +72 -0
  24. package/dist/esm/core/opc/core-properties.js +1 -0
  25. package/dist/esm/core/opc/package.d.ts +14 -1
  26. package/dist/esm/core/opc/package.js +43 -4
  27. package/dist/esm/core/opc/relationships.js +1 -0
  28. package/dist/esm/core/opc/xml-entities.d.ts +12 -0
  29. package/dist/esm/core/opc/xml-entities.js +91 -0
  30. package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
  31. package/dist/esm/core/spreadsheet-model/types.d.ts +137 -0
  32. package/dist/esm/core/vector.d.ts +13 -0
  33. package/dist/esm/excel/activex-parser.d.ts +9 -0
  34. package/dist/esm/excel/activex-parser.js +16 -1
  35. package/dist/esm/excel/column-bands.d.ts +26 -1
  36. package/dist/esm/excel/column-bands.js +156 -20
  37. package/dist/esm/excel/comments-parser.js +5 -3
  38. package/dist/esm/excel/conditional-format.d.ts +25 -2
  39. package/dist/esm/excel/conditional-format.js +63 -19
  40. package/dist/esm/excel/escaped-text.d.ts +9 -0
  41. package/dist/esm/excel/escaped-text.js +20 -0
  42. package/dist/esm/excel/form-control-parser.js +1 -0
  43. package/dist/esm/excel/formula/dates.js +1 -1
  44. package/dist/esm/excel/header-footer.d.ts +9 -1
  45. package/dist/esm/excel/header-footer.js +162 -14
  46. package/dist/esm/excel/index.d.ts +1 -1
  47. package/dist/esm/excel/index.js +2 -2
  48. package/dist/esm/excel/pivot-table-parser.js +1 -0
  49. package/dist/esm/excel/print-model.d.ts +75 -0
  50. package/dist/esm/excel/print-model.js +902 -77
  51. package/dist/esm/excel/printer-settings.d.ts +16 -0
  52. package/dist/esm/excel/printer-settings.js +36 -0
  53. package/dist/esm/excel/rich-value.d.ts +13 -0
  54. package/dist/esm/excel/rich-value.js +120 -0
  55. package/dist/esm/excel/shared-strings-parser.js +11 -7
  56. package/dist/esm/excel/sheet-drawing.d.ts +11 -0
  57. package/dist/esm/excel/sheet-drawing.js +26 -3
  58. package/dist/esm/excel/sheet-shape-parser.d.ts +53 -1
  59. package/dist/esm/excel/sheet-shape-parser.js +461 -32
  60. package/dist/esm/excel/sheet-to-flow.d.ts +23 -2
  61. package/dist/esm/excel/sheet-to-flow.js +551 -36
  62. package/dist/esm/excel/slicer-parser.js +1 -0
  63. package/dist/esm/excel/styles-parser.d.ts +30 -2
  64. package/dist/esm/excel/styles-parser.js +177 -40
  65. package/dist/esm/excel/table-parser.js +1 -0
  66. package/dist/esm/excel/vml-drawing.d.ts +68 -0
  67. package/dist/esm/excel/vml-drawing.js +158 -0
  68. package/dist/esm/excel/workbook-parser.d.ts +5 -0
  69. package/dist/esm/excel/workbook-parser.js +5 -1
  70. package/dist/esm/excel/worksheet-parser.d.ts +2 -1
  71. package/dist/esm/excel/worksheet-parser.js +149 -28
  72. package/dist/esm/excel/xlsx-reader.js +271 -33
  73. package/dist/esm/excel/xlsx-to-pdf.js +12 -1
  74. package/dist/esm/html/html-writer.js +11 -2
  75. package/dist/esm/layout/page-doc.d.ts +26 -0
  76. package/dist/esm/layout/styled-layout.d.ts +2 -0
  77. package/dist/esm/layout/styled-layout.js +493 -64
  78. package/dist/esm/pdf/cid-font.js +20 -9
  79. package/dist/esm/pdf/objects.d.ts +13 -0
  80. package/dist/esm/pdf/objects.js +18 -1
  81. package/dist/esm/pdf/styled-page-emitter.js +164 -32
  82. package/dist/esm/pdf/text-page-renderer.js +1 -0
  83. package/dist/esm/pdf/vector-graphics.d.ts +3 -1
  84. package/dist/esm/pdf/vector-graphics.js +19 -2
  85. package/dist/esm/pptx/pptx-reader.js +1 -0
  86. package/dist/esm/pptx/slide-parser.js +12 -1
  87. package/dist/esm/svg/svg-writer.js +23 -4
  88. package/dist/esm/word/document-parser.js +1 -0
  89. package/dist/esm/word/drawing-parser.d.ts +34 -1
  90. package/dist/esm/word/drawing-parser.js +52 -2
  91. package/dist/esm/word/numbering-parser.js +1 -0
  92. package/dist/esm/word/settings-parser.js +1 -0
  93. package/dist/esm/word/styles-parser.js +1 -0
  94. package/package.json +7 -3
  95. package/dist/esm/excel/number-format.js +0 -476
@@ -17,7 +17,7 @@ import "../core/style-cascade/index.js";
17
17
  import { PathBuilder, flipTransform } from "../core/vector.js";
18
18
  import { buildSparkline } from "../core/drawingml/sparkline-geometry.js";
19
19
  import { arcPoint, arcToBeziers } from "../core/arc-to-bezier.js";
20
- import { buildChartScene } from "../core/drawingml/chart-geometry.js";
20
+ import { buildChartScene, legendSeriesName } from "../core/drawingml/chart-geometry.js";
21
21
  import { layoutMath, mathGlyphSegments, variantStyle } from "./math-layout.js";
22
22
  import { rectPath } from "../core/drawingml/preset-geometry.js";
23
23
  import { buildShapePaths, buildShapeTransform, buildStroke, gradientToSolid } from "../core/drawingml/shape-render.js";
@@ -400,16 +400,18 @@ function buildSectionContext(section, options, headersFooters, fontResources) {
400
400
  const headerSet = layoutHeaderSet(section.properties, headersFooters, options, fontResources, contentWidth, dims.marginLeft, dims.pageHeight, dims.headerOffsetPt);
401
401
  const footerSet = layoutFooterSet(section.properties, headersFooters, options, fontResources, contentWidth, dims.marginLeft, dims.pageHeight, dims.footerOffsetPt);
402
402
  const columns = buildColumnGeometry(section.properties.columns, contentWidth);
403
+ const headerBottom = dims.headerOffsetPt + Math.max(headerSet.default.heightPt ?? 0, headerSet.first.heightPt ?? 0, headerSet.even.heightPt ?? 0);
404
+ const marginTop = Math.max(dims.marginTop, headerBottom);
403
405
  return {
404
406
  endIndex: section.endIndex,
405
407
  properties: section.properties,
406
408
  pageWidth: dims.pageWidth,
407
409
  pageHeight: dims.pageHeight,
408
410
  marginLeft: dims.marginLeft,
409
- marginTop: dims.marginTop,
411
+ marginTop,
410
412
  marginBottom: dims.marginBottom,
411
413
  contentWidth,
412
- pageContentHeight: dims.pageHeight - dims.marginTop - dims.marginBottom,
414
+ pageContentHeight: dims.pageHeight - marginTop - dims.marginBottom,
413
415
  ...columns ? { columns } : {},
414
416
  headerSet,
415
417
  footerSet,
@@ -455,14 +457,24 @@ function layoutHeaderSet(section, headersFooters, options, fontResources, conten
455
457
  if (!ref) return { commands: [] };
456
458
  const content = headersFooters.get(ref.relationshipId);
457
459
  if (!content) return { commands: [] };
460
+ let measured = 0;
458
461
  const render = (c) => {
459
- return markPagination(drawBlocksSequentially(laidOutBlocksFor(c, options, fontResources, contentWidth), marginLeft, pageHeight - headerOffsetPt, pageHeight));
462
+ const blocks = laidOutBlocksFor(c, options, fontResources, contentWidth);
463
+ measured = blocksHeight(blocks);
464
+ return markPagination(drawBlocksSequentially(blocks, marginLeft, pageHeight - headerOffsetPt, pageHeight));
460
465
  };
461
- if (contentHasPageFields(content)) return {
462
- commands: [],
463
- renderDynamic: (n, total) => render(substitutePageFields(content, n, total))
466
+ if (contentHasPageFields(content)) {
467
+ render(substitutePageFields(content, 1, 1));
468
+ return {
469
+ commands: [],
470
+ heightPt: measured,
471
+ renderDynamic: (n, total) => render(substitutePageFields(content, n, total))
472
+ };
473
+ }
474
+ return {
475
+ commands: render(content),
476
+ heightPt: measured
464
477
  };
465
- return { commands: render(content) };
466
478
  };
467
479
  return {
468
480
  default: band("default"),
@@ -470,6 +482,10 @@ function layoutHeaderSet(section, headersFooters, options, fontResources, conten
470
482
  even: band("even")
471
483
  };
472
484
  }
485
+ /** Total laid-out height of a run of blocks, paragraph spacing included. */
486
+ function blocksHeight(blocks) {
487
+ return blocks.reduce((sum, b) => sum + (b.kind === "paragraph" ? b.spacingBeforePt + b.heightPt + b.spacingAfterPt : b.heightPt), 0);
488
+ }
473
489
  function layoutFooterSet(section, headersFooters, options, fontResources, contentWidth, marginLeft, pageHeight, footerOffsetPt) {
474
490
  const band = (type) => {
475
491
  const ref = refByType(section.footers, type);
@@ -586,7 +602,16 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
586
602
  for (const el of text.content) {
587
603
  if (el.kind !== "paragraph") continue;
588
604
  const blk = layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, innerWidth);
589
- for (const line of blk.lines) {
605
+ const lines = blk.lines.length > 0 ? blk.lines : [{
606
+ tokens: [],
607
+ contentWidthPt: 0,
608
+ maxFontSizePt: el.paragraph.runs[0]?.properties.fontSizePt ?? options.styles.defaultRunProperties.fontSizePt ?? pt(11),
609
+ availableWidthPt: innerWidth,
610
+ firstLine: true,
611
+ resolved: blk.resolved,
612
+ isLastInParagraph: true
613
+ }];
614
+ for (const line of lines) {
590
615
  textLines.push(line);
591
616
  textHeightPt += computeLineHeight(line, blk.resolved);
592
617
  }
@@ -601,6 +626,7 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
601
626
  ...fillColorHex ? { fillColorHex } : {},
602
627
  ...fillGradient ? { fillGradient } : {},
603
628
  ...stroke ? { stroke } : {},
629
+ ...shape.shadow ? { shadow: shape.shadow } : {},
604
630
  rotation60k: t?.rotation60k ?? 0,
605
631
  flipH: t?.flipH ?? false,
606
632
  flipV: t?.flipV ?? false,
@@ -666,6 +692,8 @@ function buildChartLayout(chart, widthPt, heightPt, font) {
666
692
  texts: []
667
693
  };
668
694
  const shapes = [];
695
+ if (scene.background) shapes.push(rectPrim(scene.background));
696
+ for (const g of scene.gridlines ?? []) shapes.push(polylinePrim(g));
669
697
  for (const pg of scene.polygons ?? []) shapes.push(polygonPrim(pg));
670
698
  for (const r of scene.rects) shapes.push(rectPrim(r));
671
699
  for (const p of scene.polylines) shapes.push(polylinePrim(p));
@@ -682,7 +710,6 @@ var CF_ICON_SIZE_PT = 9;
682
710
  var CF_ICON_GUTTER_PT = 12;
683
711
  var CF_ICON_EMPTY_HEX = "BFBFBF";
684
712
  var DROPDOWN_BUTTON_PT = 11;
685
- var DROPDOWN_GUTTER_PT = 13;
686
713
  var DROPDOWN_FILL_HEX = "F0F0F0";
687
714
  var DROPDOWN_BORDER_HEX = "B0B0B0";
688
715
  var DROPDOWN_ARROW_HEX = "595959";
@@ -839,9 +866,16 @@ function wedgePrim(w) {
839
866
  function labelPrim(l, font) {
840
867
  const line = makeChartLabelLine(l.text, font, l.sizePt, l.colorHex);
841
868
  const w = line.contentWidthPt;
869
+ const shift = l.align === "center" ? -w / 2 : l.align === "right" ? -w : 0;
870
+ if (l.rotationDeg) return {
871
+ line,
872
+ x: l.x,
873
+ y: l.y + shift,
874
+ rotationDeg: l.rotationDeg
875
+ };
842
876
  return {
843
877
  line,
844
- x: l.align === "center" ? l.x - w / 2 : l.align === "right" ? l.x - w : l.x,
878
+ x: l.x + shift,
845
879
  y: l.y
846
880
  };
847
881
  }
@@ -1020,7 +1054,16 @@ function collectFontResources(body, headersFooters, options) {
1020
1054
  }
1021
1055
  addRun(run, el.paragraph);
1022
1056
  }
1023
- else if (el.kind === "table") for (const row of el.table.rows) for (const cell of row.cells) visit(cell.content);
1057
+ else if (el.kind === "table") for (const row of el.table.rows) for (const cell of row.cells) {
1058
+ if (cell.properties.hashOnOverflow === true) for (const inner of cell.content) {
1059
+ if (inner.kind !== "paragraph") continue;
1060
+ for (const run of inner.paragraph.runs) addRun({
1061
+ text: "#",
1062
+ properties: run.properties
1063
+ }, inner.paragraph);
1064
+ }
1065
+ visit(cell.content);
1066
+ }
1024
1067
  else if (el.kind === "shape") {
1025
1068
  if (el.shape.text) visit(el.shape.text.content);
1026
1069
  } else if (el.kind === "chart") {
@@ -1040,8 +1083,12 @@ function collectFontResources(body, headersFooters, options) {
1040
1083
  };
1041
1084
  if (chart.title) add(chart.title);
1042
1085
  for (const c of chart.categories) add(c);
1043
- for (const sr of chart.series) if (sr.name) add(sr.name);
1086
+ chart.series.forEach((sr, i) => add(legendSeriesName(sr, i)));
1087
+ if (chart.catAxisTitle) add(chart.catAxisTitle);
1088
+ if (chart.valAxisTitle) add(chart.valAxisTitle);
1089
+ for (const sr of chart.series) for (const label of sr.pointLabels ?? []) add(label.text);
1044
1090
  add("0123456789.,-%() ");
1091
+ if (chart.numberFormat) add(chart.numberFormat);
1045
1092
  }
1046
1093
  }
1047
1094
  };
@@ -1049,12 +1096,29 @@ function collectFontResources(body, headersFooters, options) {
1049
1096
  for (const hf of headersFooters.values()) visit(hf);
1050
1097
  for (const note of options.footnotes?.values() ?? []) visit(note);
1051
1098
  for (const note of options.endnotes?.values() ?? []) visit(note);
1052
- for (const comment of options.comments?.values() ?? []) visit(comment.content);
1099
+ for (const comment of options.comments?.values() ?? []) {
1100
+ visit(comment.content);
1101
+ visit(commentTailBlocks(comment, 1, {
1102
+ parentN: 1,
1103
+ depth: 1,
1104
+ done: true
1105
+ }));
1106
+ visit([{
1107
+ kind: "paragraph",
1108
+ paragraph: {
1109
+ properties: {},
1110
+ runs: [{
1111
+ text: "0123456789",
1112
+ properties: {}
1113
+ }]
1114
+ }
1115
+ }]);
1116
+ }
1053
1117
  if (used.size === 0) {
1054
1118
  const regular = options.registry.resolveByStyle(false, false);
1055
1119
  used.set(regular.variant, {
1056
1120
  parsed: regular.parsed,
1057
- gids: new Set([0])
1121
+ gids: /* @__PURE__ */ new Set()
1058
1122
  });
1059
1123
  }
1060
1124
  const out = /* @__PURE__ */ new Map();
@@ -1098,6 +1162,16 @@ function layoutParagraphBlock(paragraph, options, fontResources, imageResources,
1098
1162
  source: paragraph
1099
1163
  };
1100
1164
  }
1165
+ /**
1166
+ * Word and Excel both draw a super/subscript at about two thirds of the base
1167
+ * size, raised a third of it or dropped a sixth. The base line height does not
1168
+ * change: a superscript inside a line of body text must not open the leading.
1169
+ */
1170
+ var SCRIPT_SCALE = .66;
1171
+ var SCRIPT_OFFSET = {
1172
+ superscript: 1 / 3,
1173
+ subscript: -1 / 6
1174
+ };
1101
1175
  function resolveMathItems(items, fontFor) {
1102
1176
  return items.map((it) => {
1103
1177
  if (it.kind === "glyph") return {
@@ -1174,11 +1248,13 @@ function tokenizeParagraph(paragraph, options, fontResources, imageResources, co
1174
1248
  }
1175
1249
  const resolvedRun = resolveRunProperties(run.properties, paragraph.properties, options.styles);
1176
1250
  const { fontKey } = runFontKeyAndParsed(options, resolvedRun.fontFamily.ascii, resolvedRun.bold, resolvedRun.italic);
1251
+ const script = SCRIPT_OFFSET[resolvedRun.verticalAlign] ?? 0;
1177
1252
  return {
1178
1253
  run,
1179
1254
  resolvedRun,
1180
1255
  font: lookupFont(fontResources, fontKey),
1181
- fontSizePt: resolvedRun.fontSizePt,
1256
+ fontSizePt: script === 0 ? resolvedRun.fontSizePt : resolvedRun.fontSizePt * SCRIPT_SCALE,
1257
+ ...script === 0 ? {} : { risePt: resolvedRun.fontSizePt * script },
1182
1258
  isImage: false,
1183
1259
  imageWidthPt: 0,
1184
1260
  imageHeightPt: 0,
@@ -1233,6 +1309,7 @@ function tokenizePlansLtr(plans) {
1233
1309
  resolvedRun: plan.resolvedRun,
1234
1310
  font: plan.font,
1235
1311
  fontSizePt: plan.fontSizePt,
1312
+ ...plan.risePt !== void 0 ? { risePt: plan.risePt } : {},
1236
1313
  widthPt: plan.font.measure.textWidthPt(t.text, plan.fontSizePt),
1237
1314
  bidiLevel: 0
1238
1315
  });
@@ -1288,6 +1365,7 @@ function tokenizePlansBidi(plans, realLevels) {
1288
1365
  resolvedRun: plan.resolvedRun,
1289
1366
  font: plan.font,
1290
1367
  fontSizePt: plan.fontSizePt,
1368
+ ...plan.risePt !== void 0 ? { risePt: plan.risePt } : {},
1291
1369
  widthPt: plan.font.measure.textWidthPt(text, plan.fontSizePt),
1292
1370
  bidiLevel: curLevel
1293
1371
  });
@@ -1347,7 +1425,7 @@ function lastCodePoint(s) {
1347
1425
  const chars = [...s];
1348
1426
  return chars.length > 0 ? chars[chars.length - 1].codePointAt(0) : 0;
1349
1427
  }
1350
- function paragraphItemStream(tokens, hyphenator) {
1428
+ function paragraphItemStream(tokens, hyphenator, maxLineWidthPt) {
1351
1429
  const entries = [];
1352
1430
  let prevBoxEndCp = null;
1353
1431
  for (const tok of tokens) {
@@ -1380,6 +1458,28 @@ function paragraphItemStream(tokens, hyphenator) {
1380
1458
  prevBoxEndCp = lastCodePoint(tok.text);
1381
1459
  const positions = hyphenator ? hyphenator.hyphenate(tok.text) : [];
1382
1460
  if (positions.length === 0) {
1461
+ const chunks = maxLineWidthPt !== void 0 && maxLineWidthPt > 0 && tok.widthPt > maxLineWidthPt * 1.1 ? splitToWidth(tok, maxLineWidthPt) : void 0;
1462
+ if (chunks) {
1463
+ chunks.forEach((chunk, ci) => {
1464
+ if (ci > 0) entries.push({
1465
+ item: {
1466
+ type: "penalty",
1467
+ width: 0,
1468
+ penalty: 0,
1469
+ flagged: false
1470
+ },
1471
+ token: null
1472
+ });
1473
+ entries.push({
1474
+ item: {
1475
+ type: "box",
1476
+ width: chunk.widthPt
1477
+ },
1478
+ token: chunk
1479
+ });
1480
+ });
1481
+ continue;
1482
+ }
1383
1483
  entries.push({
1384
1484
  item: {
1385
1485
  type: "box",
@@ -1445,11 +1545,52 @@ function paragraphItemStream(tokens, hyphenator) {
1445
1545
  });
1446
1546
  return entries;
1447
1547
  }
1548
+ /**
1549
+ * Cut an unbreakable word into the widest pieces that fit `widthPt`, measured
1550
+ * with the token's own font. Each piece keeps the run's formatting, so the line
1551
+ * builder reassembles them exactly as it does hyphenation fragments.
1552
+ *
1553
+ * @param tok The text token.
1554
+ * @param widthPt The line width to fit each piece into.
1555
+ * @returns The pieces in order, or undefined when even one character overruns
1556
+ * (nothing can be gained by splitting then).
1557
+ */
1558
+ function splitToWidth(tok, widthPt) {
1559
+ if (tok.kind !== "text") return void 0;
1560
+ const chars = [...tok.text];
1561
+ if (tok.font.measure.textWidthPt(chars.slice(0, MIN_SPLIT_CHARS).join(""), tok.fontSizePt) > widthPt) return;
1562
+ const out = [];
1563
+ let start = 0;
1564
+ while (start < chars.length) {
1565
+ let end = start + 1;
1566
+ let text = chars[start];
1567
+ let width = tok.font.measure.textWidthPt(text, tok.fontSizePt);
1568
+ if (width > widthPt && start === 0 && chars.length === 1) return void 0;
1569
+ while (end < chars.length) {
1570
+ const next = text + chars[end];
1571
+ const nextWidth = tok.font.measure.textWidthPt(next, tok.fontSizePt);
1572
+ if (nextWidth > widthPt) break;
1573
+ text = next;
1574
+ width = nextWidth;
1575
+ end++;
1576
+ }
1577
+ out.push({
1578
+ ...tok,
1579
+ text,
1580
+ widthPt: width
1581
+ });
1582
+ start = end;
1583
+ }
1584
+ return out.length > 1 ? out : void 0;
1585
+ }
1586
+ /** The fewest characters a mid-word break may leave on a line. */
1587
+ var MIN_SPLIT_CHARS = 6;
1448
1588
  function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, resolved, profile) {
1449
1589
  let st = start;
1450
1590
  let et = breakIdx;
1451
- while (st < et && (entries[st].token === null || entries[st].token?.isSpace)) st++;
1452
- while (et > st && (entries[et - 1].token === null || entries[et - 1].token?.isSpace)) et--;
1591
+ while (st < et && (entries[st].token === null || !isFirst && entries[st].token?.isSpace === true)) st++;
1592
+ const paints = (t) => t !== null && t !== void 0 && t.kind === "text" && (t.resolvedRun.underline !== "none" || t.resolvedRun.strike);
1593
+ while (et > st && (entries[et - 1].token === null || entries[et - 1].token?.isSpace === true && !paints(entries[et - 1].token))) et--;
1453
1594
  if (st >= et) return null;
1454
1595
  const lineTokens = [];
1455
1596
  for (let i = st; i < et; i++) {
@@ -1509,20 +1650,29 @@ function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, reso
1509
1650
  function wrap(tokens, firstLineWidth, otherWidth, resolved, hyphenator, profile, lineWidths) {
1510
1651
  if (tokens.length === 0) return [];
1511
1652
  const widths = lineWidths ?? [firstLineWidth, otherWidth];
1512
- const entries = paragraphItemStream(tokens, hyphenator);
1653
+ const entries = paragraphItemStream(tokens, hyphenator, Math.max(0, ...widths));
1513
1654
  const items = entries.map((e) => e.item);
1514
1655
  const breaks = profile === "ream" ? breakLines(items, widths).breaks : greedyBreakLines(items, widths);
1515
- const lines = [];
1516
- let start = 0;
1517
- let isFirst = true;
1518
- let lineIdx = 0;
1519
- for (const breakIdx of breaks) {
1520
- const width = lineIdx < widths.length ? widths[lineIdx] : widths[widths.length - 1];
1521
- const line = lineFromRange(entries, start, breakIdx, width, isFirst, resolved, profile);
1522
- if (line) lines.push(line);
1523
- start = breakIdx + 1;
1524
- isFirst = false;
1525
- lineIdx++;
1656
+ const buildLines = (at) => {
1657
+ const out = [];
1658
+ let start = 0;
1659
+ let isFirst = true;
1660
+ let lineIdx = 0;
1661
+ for (const breakIdx of at) {
1662
+ const width = lineIdx < widths.length ? widths[lineIdx] : widths[widths.length - 1];
1663
+ const line = lineFromRange(entries, start, breakIdx, width, isFirst, resolved, profile);
1664
+ if (line) out.push(line);
1665
+ start = breakIdx + 1;
1666
+ isFirst = false;
1667
+ lineIdx++;
1668
+ }
1669
+ return out;
1670
+ };
1671
+ let lines = buildLines(breaks);
1672
+ const spill = (ls) => Math.max(0, ...ls.map((l) => l.contentWidthPt - l.availableWidthPt));
1673
+ if (profile === "ream" && lines.length === 1 && spill(lines) > .01) {
1674
+ const greedy = buildLines(greedyBreakLines(items, widths));
1675
+ if (greedy.length > 1 && spill(greedy) < spill(lines) - .01) lines = greedy;
1526
1676
  }
1527
1677
  if (lines.length > 0) lines[lines.length - 1].isLastInParagraph = true;
1528
1678
  return lines;
@@ -1683,6 +1833,30 @@ function measureSingleLine(paragraph, options, fontResources) {
1683
1833
  }
1684
1834
  return total;
1685
1835
  }
1836
+ /**
1837
+ * The above-neighbour a cell resolves its top edge against.
1838
+ *
1839
+ * A spanning cell covers several columns and paints ONE top across all of them,
1840
+ * so taking the first column's neighbour throws away any rule the others
1841
+ * declare. tdf100034.xlsx rules the bottom of its two header cells, and a label
1842
+ * overflowing from two columns to their left spanned over both and wiped that
1843
+ * rule off the page. Take the heaviest bottom under the span instead — it is
1844
+ * the one edge the span can draw, so it should be the strongest claim on it.
1845
+ */
1846
+ function aboveOfSpan(aboveBordersByCol, colIdx, span) {
1847
+ const first = aboveBordersByCol[colIdx];
1848
+ if (span <= 1) return first;
1849
+ let bottom = first?.bottom;
1850
+ for (let k = 1; k < span; k++) {
1851
+ const b = aboveBordersByCol[colIdx + k]?.bottom;
1852
+ if (heavierBorder(bottom, b) === b) bottom = b;
1853
+ }
1854
+ if (bottom === first?.bottom) return first;
1855
+ return {
1856
+ ...first,
1857
+ ...bottom ? { bottom } : {}
1858
+ };
1859
+ }
1686
1860
  function layoutTableRow(row, rowIdx, rowCount, colCount, tableProps, columnWidthsPt, options, fontResources, imageResources, rowMergeRoles, aboveBordersByCol) {
1687
1861
  const cells = [];
1688
1862
  const columnXOffsets = [];
@@ -1693,18 +1867,32 @@ function layoutTableRow(row, rowIdx, rowCount, colCount, tableProps, columnWidth
1693
1867
  const span = Math.max(1, cell.properties.colSpan ?? 1);
1694
1868
  let widthPt = 0;
1695
1869
  for (let k = 0; k < span && colIdx + k < columnWidthsPt.length; k++) widthPt += columnWidthsPt[colIdx + k];
1870
+ const paintSpan = Math.min(span, Math.max(1, cell.properties.paintColumns ?? span));
1871
+ let paintWidthPt = 0;
1872
+ for (let k = 0; k < paintSpan && colIdx + k < columnWidthsPt.length; k++) paintWidthPt += columnWidthsPt[colIdx + k];
1696
1873
  columnXOffsets.push(cursorX);
1697
1874
  const mergeRole = rowMergeRoles[cellIdx] ?? "standalone";
1698
1875
  const leftNeighbor = cellIdx > 0 ? row.cells[cellIdx - 1].properties.borders : void 0;
1699
- const cellLayout = layoutTableCell(cell, tableProps, widthPt, options, fontResources, imageResources, rowIdx, colIdx, span, rowCount, colCount, mergeRole, leftNeighbor, aboveBordersByCol[colIdx]);
1700
- cells.push(cellLayout);
1876
+ const cellLayout = layoutTableCell(cell, tableProps, widthPt, options, fontResources, imageResources, rowIdx, colIdx, span, rowCount, colCount, mergeRole, leftNeighbor, aboveOfSpan(aboveBordersByCol, colIdx, span));
1877
+ cells.push(paintWidthPt < widthPt ? {
1878
+ ...cellLayout,
1879
+ paintWidthPt
1880
+ } : cellLayout);
1701
1881
  cursorX += widthPt;
1702
1882
  colIdx += span;
1703
1883
  }
1704
1884
  let heightPt = 0;
1705
1885
  for (const c of cells) if (c.totalHeightPt > heightPt) heightPt = c.totalHeightPt;
1706
- if (row.properties.height && row.properties.heightRule === "exact") heightPt = row.properties.height;
1707
- else if (row.properties.height && row.properties.heightRule === "atLeast") heightPt = Math.max(heightPt, row.properties.height);
1886
+ if (row.properties.height && row.properties.heightRule === "exact") {
1887
+ heightPt = row.properties.height;
1888
+ return {
1889
+ heightPt,
1890
+ cells: cells.map((c) => clipCellToHeight(c, heightPt)),
1891
+ columnXOffsets,
1892
+ rowIdx,
1893
+ rowCount
1894
+ };
1895
+ } else if (row.properties.height && row.properties.heightRule === "atLeast") heightPt = Math.max(heightPt, row.properties.height);
1708
1896
  return {
1709
1897
  heightPt,
1710
1898
  cells,
@@ -1713,6 +1901,35 @@ function layoutTableRow(row, rowIdx, rowCount, colCount, tableProps, columnWidth
1713
1901
  rowCount
1714
1902
  };
1715
1903
  }
1904
+ /**
1905
+ * A cell with only the lines that fit inside `heightPt`.
1906
+ *
1907
+ * Whole lines only: half a line of text reads as a rendering fault, where a
1908
+ * missing one reads as the clipping it is. A cell that already fits, or whose
1909
+ * first line does not, is returned untouched — the first line always draws, as
1910
+ * it does in Excel.
1911
+ *
1912
+ * @param cell The laid-out cell.
1913
+ * @param heightPt The row's pinned height.
1914
+ * @returns The cell, clipped.
1915
+ */
1916
+ function clipCellToHeight(cell, heightPt) {
1917
+ const room = heightPt - cell.padTopPt - cell.padBottomPt;
1918
+ if (cell.lines.length <= 1 || cell.contentHeightPt <= room) return cell;
1919
+ const kept = [];
1920
+ let used = 0;
1921
+ for (const line of cell.lines) {
1922
+ const h = computeLineHeight(line, line.resolved);
1923
+ if (kept.length > 0 && used + h > room) break;
1924
+ kept.push(line);
1925
+ used += h;
1926
+ }
1927
+ return {
1928
+ ...cell,
1929
+ lines: kept,
1930
+ contentHeightPt: used
1931
+ };
1932
+ }
1716
1933
  function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imageResources, rowIdx, colStart, colSpan, rowCount, colCount, mergeRole, leftNeighborBorders, aboveNeighborBorders) {
1717
1934
  const cellMar = cell.properties.margins;
1718
1935
  const tableMar = tableProps.defaultCellMargins;
@@ -1721,15 +1938,20 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
1721
1938
  const padLeftBase = cellMar?.left ?? tableMar?.left ?? DEFAULT_CELL_PADDING_TWIPS * TWIP_TO_PT;
1722
1939
  const padRightBase = cellMar?.right ?? tableMar?.right ?? DEFAULT_CELL_PADDING_TWIPS * TWIP_TO_PT;
1723
1940
  const padLeftPt = padLeftBase + (cell.properties.icon ? CF_ICON_GUTTER_PT : 0);
1724
- const padRightPt = padRightBase + (cell.properties.dropdown ? DROPDOWN_GUTTER_PT : 0);
1941
+ const padRightPt = padRightBase;
1725
1942
  const innerWidth = Math.max(1, widthPt - padLeftPt - padRightPt);
1726
1943
  const lines = [];
1727
1944
  const nestedTables = [];
1728
1945
  let contentHeightPt = 0;
1946
+ let clipped = false;
1729
1947
  if (mergeRole !== "middle" && mergeRole !== "end") {
1730
1948
  for (const el of cell.content) if (el.kind === "paragraph") {
1731
- const block = layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, innerWidth);
1732
- for (const line of block.lines) {
1949
+ const noWrap = cell.properties.noWrap === true;
1950
+ const block = layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, noWrap ? NO_WRAP_MEASURE_WIDTH : innerWidth);
1951
+ const cutAt = widthPt - padLeftPt;
1952
+ if (noWrap && (block.lines[0]?.contentWidthPt ?? 0) > cutAt) clipped = true;
1953
+ const keep = noWrap ? clipLineToWidth(block.lines, cutAt, cell.properties.hashOnOverflow === true) : block.lines;
1954
+ for (const line of keep) {
1733
1955
  lines.push(line);
1734
1956
  contentHeightPt += computeLineHeight(line, block.resolved);
1735
1957
  }
@@ -1754,14 +1976,15 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
1754
1976
  ...cell.properties.dataBar ? { dataBar: cell.properties.dataBar } : {},
1755
1977
  ...cell.properties.icon ? { icon: cell.properties.icon } : {},
1756
1978
  ...cell.properties.sparkline ? { sparkline: cell.properties.sparkline } : {},
1757
- ...cell.properties.dropdown ? { dropdown: true } : {},
1758
1979
  lines,
1759
1980
  ...nestedTables.length > 0 ? { nestedTables } : {},
1760
1981
  contentHeightPt,
1761
1982
  totalHeightPt,
1983
+ ...cell.properties.verticalAlign ? { verticalAlign: cell.properties.verticalAlign } : {},
1762
1984
  colStart,
1763
1985
  colSpan,
1764
- mergeRole
1986
+ mergeRole,
1987
+ ...clipped ? { clipped: true } : {}
1765
1988
  };
1766
1989
  }
1767
1990
  var BORDER_STYLE_RANK = {
@@ -1769,6 +1992,8 @@ var BORDER_STYLE_RANK = {
1769
1992
  thick: 4,
1770
1993
  single: 3,
1771
1994
  dashed: 2,
1995
+ dashDot: 2,
1996
+ dashDotDot: 2,
1772
1997
  dotted: 1,
1773
1998
  none: 0
1774
1999
  };
@@ -1929,6 +2154,14 @@ var PageAssembler = class {
1929
2154
  * Consumed space counts as use, whether or not any ink went with it.
1930
2155
  */
1931
2156
  colHasContent = () => this.current.length > this.colStartLen || this.cursorY < this.colStartY;
2157
+ /**
2158
+ * Whether this page has ANYTHING on it — including drawings, which is the
2159
+ * whole point: a page carrying only floats has an empty `current`, so a page
2160
+ * break asked for after it was silently dropped and the next band's drawings
2161
+ * landed on top of the first's. tdf111980_radioButtons.xlsx has no cells at
2162
+ * all and eleven controls wider than its page.
2163
+ */
2164
+ pageHasContent = () => this.current.length > 0 || this.floatsBehind.length > 0 || this.floatsFront.length > 0;
1932
2165
  /** Overflow step: next column on this page, or a fresh page after the last. */
1933
2166
  advanceColumn = () => {
1934
2167
  if (this.ctx.columns && this.colIdx + 1 < this.ctx.columns.length) {
@@ -2114,7 +2347,7 @@ var PageAssembler = class {
2114
2347
  * guarantee one page for a header/footer-only document).
2115
2348
  */
2116
2349
  flushPage = (force = false) => {
2117
- if (this.current.length === 0 && this.cursorY >= this.colStartY && !force) return;
2350
+ if (!this.pageHasContent() && this.cursorY >= this.colStartY && !force) return;
2118
2351
  const band = bandForPage(this.pageInSection, this.globalPageIdx, this.ctx.titlePg, this.ctx.evenAndOddHeaders);
2119
2352
  const header = pickBand(this.ctx.headerSet, band);
2120
2353
  const footer = pickBand(this.ctx.footerSet, band);
@@ -2174,7 +2407,7 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2174
2407
  }
2175
2408
  if (builder && !(block.kind === "paragraph" && block.list)) asm.listStack.length = 0;
2176
2409
  if (block.kind === "paragraph") {
2177
- if (block.resolved.pageBreakBefore && asm.current.length > 0) asm.flushPage();
2410
+ if (block.resolved.pageBreakBefore && asm.pageHasContent()) asm.flushPage();
2178
2411
  asm.cursorY -= block.spacingBeforePt;
2179
2412
  let pb = block;
2180
2413
  if (reflowParagraph && pb.source && asm.exclusions.length > 0) {
@@ -2310,6 +2543,7 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2310
2543
  ...block.fillColorHex ? { fillColorHex: block.fillColorHex } : {},
2311
2544
  ...block.fillGradient ? { fillGradient: block.fillGradient } : {},
2312
2545
  ...block.stroke ? { stroke: block.stroke } : {},
2546
+ ...block.shadow ? { shadow: block.shadow } : {},
2313
2547
  transform
2314
2548
  },
2315
2549
  ...figId !== void 0 ? { structId: figId } : {}
@@ -2380,6 +2614,7 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2380
2614
  line: t.line,
2381
2615
  originX: pt(x + t.x),
2382
2616
  baselineY: pt(asm.ctx.pageHeight - (bottomYUp + t.y)),
2617
+ ...t.rotationDeg ? { rotationDeg: t.rotationDeg } : {},
2383
2618
  ...fig
2384
2619
  });
2385
2620
  };
@@ -2439,8 +2674,16 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2439
2674
  asm.cursorY -= hr.heightPt;
2440
2675
  }
2441
2676
  }
2442
- emitRowChunk(asm.current, chunk, tableX, asm.cursorY, asm.ctx.pageHeight, colCount, cellStructIds);
2677
+ const mergedHeights = chunks.length === 1 && row.cells.some((c) => c.mergeRole === "start") ? row.cells.map((c) => c.mergeRole === "start" ? mergedBoxHeightPt(block.rows, ri, c.colStart, asm.cursorY - asm.bottomLimit()) : void 0) : void 0;
2678
+ emitRowChunk(asm.current, chunk, tableX, asm.cursorY, asm.ctx.pageHeight, colCount, cellStructIds, mergedHeights);
2443
2679
  asm.cursorY -= chunk.heightPt;
2680
+ if (ci === chunks.length - 1 && ri < block.rows.length - 1) {
2681
+ const next = block.rows[ri + 1];
2682
+ const nextHeight = Math.min(next.heightPt, asm.ctx.pageContentHeight);
2683
+ const nextForced = next.breakBefore && ri + 1 >= headerRows.length && asm.current.length > 0;
2684
+ const nextOverflow = asm.cursorY - nextHeight < asm.bottomLimit() && asm.colHasContent();
2685
+ if (nextForced || nextOverflow) emitRowBottomEdge(asm.current, chunk, tableX, asm.cursorY, asm.ctx.pageHeight);
2686
+ }
2444
2687
  }
2445
2688
  }
2446
2689
  }
@@ -2453,8 +2696,44 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2453
2696
  if (d.position === "header") page.commands.unshift(...cmds);
2454
2697
  else page.commands.push(...cmds);
2455
2698
  }
2699
+ for (const page of asm.pages) coalesceVerticalFills(page.commands);
2456
2700
  return asm.pages;
2457
2701
  }
2702
+ function coalesceVerticalFills(commands) {
2703
+ const touching = (a, b) => Math.abs(a.x - b.x) < .01 && Math.abs(a.width - b.width) < .01 && a.fillColorHex === b.fillColorHex && Math.min(a.y + a.height, b.y + b.height) >= Math.max(a.y, b.y) - .01;
2704
+ const open = /* @__PURE__ */ new Map();
2705
+ const dead = /* @__PURE__ */ new Set();
2706
+ for (const item of commands) {
2707
+ if (item.type !== "fill") continue;
2708
+ const fill = item;
2709
+ const k = `${fill.x.toFixed(2)}|${fill.width.toFixed(2)}|${fill.fillColorHex}`;
2710
+ const prev = open.get(k);
2711
+ if (prev && touching(prev, fill)) {
2712
+ const top = Math.max(prev.y + prev.height, fill.y + fill.height);
2713
+ const bottom = Math.min(prev.y, fill.y);
2714
+ prev.y = pt(bottom);
2715
+ prev.height = pt(top - bottom);
2716
+ dead.add(item);
2717
+ continue;
2718
+ }
2719
+ open.set(k, fill);
2720
+ }
2721
+ if (dead.size === 0) return;
2722
+ const kept = commands.filter((c) => !dead.has(c));
2723
+ commands.length = 0;
2724
+ commands.push(...kept);
2725
+ }
2726
+ function mergedBoxHeightPt(rows, startRowIdx, colStart, availablePt) {
2727
+ let height = rows[startRowIdx].heightPt;
2728
+ for (let r = startRowIdx + 1; r < rows.length; r++) {
2729
+ const cont = rows[r].cells.find((c) => c.colStart === colStart && (c.mergeRole === "middle" || c.mergeRole === "end"));
2730
+ if (!cont) break;
2731
+ if (height + rows[r].heightPt > availablePt) break;
2732
+ height += rows[r].heightPt;
2733
+ if (cont.mergeRole === "end") break;
2734
+ }
2735
+ return height;
2736
+ }
2458
2737
  function tableCellRowSpan(rows, startRowIdx, colStart) {
2459
2738
  let span = 1;
2460
2739
  for (let r = startRowIdx + 1; r < rows.length; r++) {
@@ -2465,32 +2744,74 @@ function tableCellRowSpan(rows, startRowIdx, colStart) {
2465
2744
  }
2466
2745
  return span;
2467
2746
  }
2468
- function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellStructIds) {
2747
+ function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellStructIds, mergedHeights) {
2469
2748
  const rowTop = cursorY;
2470
2749
  const rowBottom = cursorY - row.heightPt;
2750
+ {
2751
+ let i = 0;
2752
+ while (i < row.cells.length) {
2753
+ const cell = row.cells[i];
2754
+ if (!cell.shadingColorHex) {
2755
+ i++;
2756
+ continue;
2757
+ }
2758
+ let width = cell.paintWidthPt ?? cell.widthPt;
2759
+ let j = i + 1;
2760
+ while (j < row.cells.length && cell.paintWidthPt === void 0) {
2761
+ const next = row.cells[j];
2762
+ if (next.mergeRole === "middle" || next.mergeRole === "end") break;
2763
+ if (next.shadingColorHex !== cell.shadingColorHex) break;
2764
+ width += next.paintWidthPt ?? next.widthPt;
2765
+ j++;
2766
+ }
2767
+ out.push({
2768
+ type: "fill",
2769
+ x: pt(marginLeft + (row.columnXOffsets[i] ?? 0)),
2770
+ y: pt(pageHeight - rowBottom - row.heightPt - FILL_SEAM_PT),
2771
+ width: pt(width),
2772
+ height: pt(row.heightPt + FILL_SEAM_PT),
2773
+ fillColorHex: cell.shadingColorHex
2774
+ });
2775
+ i = j;
2776
+ }
2777
+ }
2471
2778
  for (let i = 0; i < row.cells.length; i++) {
2472
2779
  const cell = row.cells[i];
2473
2780
  const cellX = marginLeft + (row.columnXOffsets[i] ?? 0);
2474
2781
  const structId = cellStructIds?.[i];
2475
- if (cell.shadingColorHex && cell.mergeRole !== "middle" && cell.mergeRole !== "end") out.push({
2476
- type: "fill",
2477
- x: pt(cellX),
2478
- y: pt(pageHeight - rowBottom - row.heightPt),
2479
- width: pt(cell.widthPt),
2480
- height: pt(row.heightPt),
2481
- fillColorHex: cell.shadingColorHex
2482
- });
2483
2782
  if (cell.dataBar && cell.mergeRole !== "middle" && cell.mergeRole !== "end") {
2484
2783
  const start = Math.max(0, Math.min(1, cell.dataBar.startFraction ?? 0));
2485
2784
  const barWidth = cell.widthPt * Math.max(0, Math.min(1, cell.dataBar.fraction));
2486
- if (barWidth > 0) out.push({
2487
- type: "fill",
2488
- x: pt(cellX + cell.widthPt * start),
2489
- y: pt(pageHeight - rowBottom - row.heightPt),
2490
- width: pt(barWidth),
2491
- height: pt(row.heightPt),
2492
- fillColorHex: cell.dataBar.colorHex
2493
- });
2785
+ if (barWidth > 0) {
2786
+ const barX = cellX + cell.widthPt * start;
2787
+ const barBottomYUp = rowBottom;
2788
+ out.push({
2789
+ type: "shape",
2790
+ shape: {
2791
+ paths: [rectAtPath(0, 0, barWidth, row.heightPt)],
2792
+ fillColorHex: cell.dataBar.colorHex,
2793
+ fillGradient: {
2794
+ kind: "linear",
2795
+ angle: cell.dataBar.negative ? 180 : 0,
2796
+ stops: [{
2797
+ offset: 0,
2798
+ colorHex: cell.dataBar.colorHex
2799
+ }, {
2800
+ offset: 1,
2801
+ colorHex: "FFFFFF"
2802
+ }]
2803
+ },
2804
+ transform: flipTransform([
2805
+ 1,
2806
+ 0,
2807
+ 0,
2808
+ 1,
2809
+ barX,
2810
+ barBottomYUp
2811
+ ], pageHeight)
2812
+ }
2813
+ });
2814
+ }
2494
2815
  }
2495
2816
  if (cell.icon && cell.mergeRole !== "middle" && cell.mergeRole !== "end") {
2496
2817
  const iconSize = Math.min(CF_ICON_SIZE_PT, Math.max(0, row.heightPt - 1));
@@ -2592,16 +2913,26 @@ function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellS
2592
2913
  if (diagUp) pushDiagonal(diagUp, 0, h);
2593
2914
  }
2594
2915
  if (cell.mergeRole === "middle" || cell.mergeRole === "end") continue;
2595
- let textY = rowTop - cell.padTopPt;
2916
+ const boxHeightPt = mergedHeights?.[i] ?? row.heightPt;
2917
+ const slack = cell.verticalAlign !== void 0 ? boxHeightPt - cell.padTopPt - cell.contentHeightPt - cell.padBottomPt : 0;
2918
+ const vOffset = slack <= 0 ? 0 : cell.verticalAlign === "bottom" ? slack : cell.verticalAlign === "center" ? slack / 2 : 0;
2919
+ let textY = rowTop - cell.padTopPt - vOffset;
2596
2920
  for (const line of cell.lines) {
2597
2921
  const h = computeLineHeight(line, line.resolved);
2598
2922
  textY -= h;
2599
2923
  const offset = alignmentOffset(line.resolved.alignment, line.contentWidthPt, cell.widthPt - cell.padLeftPt - cell.padRightPt);
2924
+ const indentLeft = line.resolved.indentLeft + (line.firstLine ? line.resolved.indentFirstLine : 0);
2600
2925
  out.push({
2601
2926
  type: "line",
2602
2927
  line,
2603
- originX: pt(cellX + cell.padLeftPt + offset),
2928
+ originX: pt(cellX + cell.padLeftPt + indentLeft + offset),
2604
2929
  baselineY: pt(pageHeight - (textY + lineDescent(line))),
2930
+ ...cell.clipped ? { clip: {
2931
+ x: pt(cellX),
2932
+ y: pt(pageHeight - rowTop),
2933
+ width: pt(cell.widthPt),
2934
+ height: pt(boxHeightPt)
2935
+ } } : {},
2605
2936
  ...structId !== void 0 ? { structId } : {}
2606
2937
  });
2607
2938
  }
@@ -2697,6 +3028,7 @@ function splitRowIntoChunks(row, capacity) {
2697
3028
  }
2698
3029
  return out.length > 0 ? out : [row];
2699
3030
  }
3031
+ var FILL_SEAM_PT = .07;
2700
3032
  function emitCellBorders(out, cell, cellX, cellY, rowHeight, pageHeight, rowIdx, rowCount, colCount) {
2701
3033
  const pushSide = (side, border) => {
2702
3034
  if (!border || border.style === "none") return;
@@ -2706,10 +3038,11 @@ function emitCellBorders(out, cell, cellX, cellY, rowHeight, pageHeight, rowIdx,
2706
3038
  side,
2707
3039
  x: pt(cellX),
2708
3040
  y: pt(pageHeight - cellY - rowHeight),
2709
- width: pt(cell.widthPt),
3041
+ width: pt(cell.paintWidthPt ?? cell.widthPt),
2710
3042
  height: pt(rowHeight),
2711
3043
  borderSizePt: sz,
2712
- borderColorHex: border.colorHex ?? "000000"
3044
+ borderColorHex: border.colorHex ?? "000000",
3045
+ ...border.style !== "single" ? { borderStyle: border.style } : {}
2713
3046
  });
2714
3047
  };
2715
3048
  pushSide("top", cell.borders.top);
@@ -2717,5 +3050,101 @@ function emitCellBorders(out, cell, cellX, cellY, rowHeight, pageHeight, rowIdx,
2717
3050
  if (rowIdx === rowCount - 1) pushSide("bottom", cell.borders.bottom);
2718
3051
  if (cell.colStart + cell.colSpan - 1 === colCount - 1) pushSide("right", cell.borders.right);
2719
3052
  }
3053
+ var NO_WRAP_MEASURE_WIDTH = 1e6;
3054
+ /**
3055
+ * One line, trimmed to the glyph that STRADDLES `widthPt` — what a non-wrapping
3056
+ * spreadsheet cell shows. Excel and LibreOffice paint that glyph and cut it
3057
+ * through the middle, and so do we now: the caller marks the cell and the
3058
+ * emitters clip the line to its box (TextLineItem.clip). Stopping at the last
3059
+ * glyph that wholly fits is what dropped a character off every cut cell.
3060
+ */
3061
+ function clipLineToWidth(lines, widthPt, hashOnOverflow = false) {
3062
+ const line = lines[0];
3063
+ if (!line) return [];
3064
+ if (line.contentWidthPt <= widthPt) return [line];
3065
+ if (hashOnOverflow) return [hashFill(line, widthPt)];
3066
+ const tokens = [];
3067
+ let used = 0;
3068
+ for (const token of line.tokens) {
3069
+ const advance = token.widthPt;
3070
+ if (used + advance <= widthPt) {
3071
+ tokens.push(token);
3072
+ used += advance;
3073
+ continue;
3074
+ }
3075
+ if (token.kind === "text") {
3076
+ const room = widthPt - used;
3077
+ let text = "";
3078
+ let textWidth = 0;
3079
+ for (const ch of token.text) {
3080
+ if (textWidth >= room) break;
3081
+ text += ch;
3082
+ textWidth = token.font.measure.textWidthPt(text, token.fontSizePt);
3083
+ }
3084
+ if (text.length > 0) {
3085
+ tokens.push({
3086
+ ...token,
3087
+ text,
3088
+ widthPt: textWidth
3089
+ });
3090
+ used += textWidth;
3091
+ }
3092
+ }
3093
+ break;
3094
+ }
3095
+ return [{
3096
+ ...line,
3097
+ tokens,
3098
+ contentWidthPt: used
3099
+ }];
3100
+ }
3101
+ /**
3102
+ * The line as a row of `#` filling `widthPt` — what Excel and LibreOffice put in
3103
+ * a cell whose number does not fit. At least one, so the cell is never blank:
3104
+ * a column too narrow even for a single `#` still has to say something.
3105
+ */
3106
+ function hashFill(line, widthPt) {
3107
+ const first = line.tokens.find((t) => t.kind === "text");
3108
+ if (!first) return line;
3109
+ const one = first.font.measure.textWidthPt("#", first.fontSizePt);
3110
+ const count = one > 0 ? Math.max(1, Math.floor(widthPt / one)) : 1;
3111
+ const text = "#".repeat(count);
3112
+ const token = {
3113
+ ...first,
3114
+ text,
3115
+ widthPt: first.font.measure.textWidthPt(text, first.fontSizePt)
3116
+ };
3117
+ return {
3118
+ ...line,
3119
+ tokens: [token],
3120
+ contentWidthPt: token.widthPt
3121
+ };
3122
+ }
3123
+ /**
3124
+ * The bottom edge of a row that ends a page — the rule the row below it owed it
3125
+ * and drew on the next page instead. See the call site: a table that spills over
3126
+ * a break is closed on both sides of it, the way Excel and Word print one.
3127
+ *
3128
+ * A cell whose bottom is suppressed (a vertical merge continuing past the break)
3129
+ * stays open; inventing an edge there would cut the merge in half.
3130
+ */
3131
+ function emitRowBottomEdge(out, row, marginLeft, rowBottom, pageHeight) {
3132
+ for (let i = 0; i < row.cells.length; i++) {
3133
+ const cell = row.cells[i];
3134
+ const border = cell.borders.bottom;
3135
+ if (!border || border.style === "none") continue;
3136
+ out.push({
3137
+ type: "border",
3138
+ side: "bottom",
3139
+ x: pt(marginLeft + (row.columnXOffsets[i] ?? 0)),
3140
+ y: pt(pageHeight - rowBottom - row.heightPt),
3141
+ width: pt(cell.widthPt),
3142
+ height: pt(row.heightPt),
3143
+ borderSizePt: border.width ?? DEFAULT_BORDER_SIZE_EIGHTH * EIGHTH_PT,
3144
+ borderColorHex: border.colorHex ?? "000000",
3145
+ ...border.style !== "single" ? { borderStyle: border.style } : {}
3146
+ });
3147
+ }
3148
+ }
2720
3149
  //#endregion
2721
3150
  export { layoutStyledDocument };