reamkit 1.30.0 → 1.31.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 (78) hide show
  1. package/README.md +60 -24
  2. package/dist/esm/core/converter/project.js +3 -1
  3. package/dist/esm/core/crypto/offcrypto.js +1 -1
  4. package/dist/esm/core/document-model/index.d.ts +1 -1
  5. package/dist/esm/core/document-model/types.d.ts +63 -0
  6. package/dist/esm/core/font/index.d.ts +2 -0
  7. package/dist/esm/core/font/ttf-build.d.ts +113 -0
  8. package/dist/esm/core/font/ttf-build.js +1224 -0
  9. package/dist/esm/core/font/ttf-subset.d.ts +19 -0
  10. package/dist/esm/core/font/ttf-subset.js +14 -1
  11. package/dist/esm/core/fonts/provider.d.ts +17 -0
  12. package/dist/esm/core/fonts/provider.js +27 -2
  13. package/dist/esm/core/fonts/remote-fonts.js +8 -1
  14. package/dist/esm/core/ir/flow.d.ts +71 -1
  15. package/dist/esm/core/numbering/index.d.ts +1 -1
  16. package/dist/esm/core/numbering/state.d.ts +11 -1
  17. package/dist/esm/core/numbering/state.js +10 -1
  18. package/dist/esm/core/style-cascade/resolver.js +35 -4
  19. package/dist/esm/core/style-cascade/types.d.ts +19 -1
  20. package/dist/esm/core/style-cascade/types.js +3 -0
  21. package/dist/esm/index.d.ts +2 -2
  22. package/dist/esm/layout/page-doc.d.ts +10 -3
  23. package/dist/esm/layout/styled-layout.d.ts +48 -7
  24. package/dist/esm/layout/styled-layout.js +881 -119
  25. package/dist/esm/layout/turned-section.d.ts +38 -0
  26. package/dist/esm/layout/turned-section.js +193 -0
  27. package/dist/esm/pdf/styled-page-emitter.js +78 -2
  28. package/dist/esm/pdf-reader/cff-outline.d.ts +27 -0
  29. package/dist/esm/pdf-reader/cff-outline.js +169 -21
  30. package/dist/esm/pdf-reader/content.d.ts +53 -0
  31. package/dist/esm/pdf-reader/content.js +9 -1
  32. package/dist/esm/pdf-reader/display.d.ts +36 -0
  33. package/dist/esm/pdf-reader/display.js +66 -1
  34. package/dist/esm/pdf-reader/document.js +5 -1
  35. package/dist/esm/pdf-reader/embedded-fonts.d.ts +2 -1
  36. package/dist/esm/pdf-reader/embedded-fonts.js +32 -2
  37. package/dist/esm/pdf-reader/encodings.d.ts +8 -0
  38. package/dist/esm/pdf-reader/encodings.js +25 -3
  39. package/dist/esm/pdf-reader/face-outlines.d.ts +78 -0
  40. package/dist/esm/pdf-reader/face-outlines.js +362 -0
  41. package/dist/esm/pdf-reader/figures.d.ts +52 -0
  42. package/dist/esm/pdf-reader/figures.js +433 -0
  43. package/dist/esm/pdf-reader/flow-build.d.ts +131 -5
  44. package/dist/esm/pdf-reader/flow-build.js +345 -26
  45. package/dist/esm/pdf-reader/font.js +321 -36
  46. package/dist/esm/pdf-reader/glyf-outline.d.ts +33 -0
  47. package/dist/esm/pdf-reader/glyf-outline.js +135 -1
  48. package/dist/esm/pdf-reader/glyph-names.js +154 -1
  49. package/dist/esm/pdf-reader/layout.d.ts +93 -0
  50. package/dist/esm/pdf-reader/layout.js +1703 -214
  51. package/dist/esm/pdf-reader/page-numbers.d.ts +53 -0
  52. package/dist/esm/pdf-reader/page-numbers.js +167 -0
  53. package/dist/esm/pdf-reader/tagged.js +182 -23
  54. package/dist/esm/pdf-reader/text.d.ts +6 -3
  55. package/dist/esm/pdf-reader/text.js +98 -9
  56. package/dist/esm/pdf-reader/type1-outline.d.ts +11 -0
  57. package/dist/esm/pdf-reader/type1-outline.js +63 -8
  58. package/dist/esm/pdf-reader/vector.js +71 -1
  59. package/dist/esm/word/doc/doc-reader.js +6 -2
  60. package/dist/esm/word/doc/doc-text.d.ts +6 -0
  61. package/dist/esm/word/doc/doc-text.js +19 -1
  62. package/dist/esm/word/document-parser.d.ts +2 -2
  63. package/dist/esm/word/document-parser.js +11 -1
  64. package/dist/esm/word/docx-reader.js +5 -3
  65. package/dist/esm/word/docx-writer.js +207 -21
  66. package/dist/esm/word/drawing-parser.d.ts +5 -3
  67. package/dist/esm/word/drawing-parser.js +49 -8
  68. package/dist/esm/word/font-embed.d.ts +30 -0
  69. package/dist/esm/word/font-embed.js +173 -0
  70. package/dist/esm/word/font-table.d.ts +10 -0
  71. package/dist/esm/word/font-table.js +13 -1
  72. package/dist/esm/word/index.js +1 -1
  73. package/dist/esm/word/numbering-parser.d.ts +3 -1
  74. package/dist/esm/word/numbering-parser.js +2 -1
  75. package/dist/esm/word/paragraph-properties.d.ts +7 -6
  76. package/dist/esm/word/paragraph-properties.js +14 -2
  77. package/dist/esm/word/run-properties.js +26 -0
  78. package/package.json +8 -3
@@ -16,6 +16,7 @@ import { FORCED_BREAK, breakLines } from "../core/line-breaker/knuth-plass.js";
16
16
  import { greedyBreakLines } from "../core/line-breaker/greedy.js";
17
17
  import { cjkBreakBetween, splitCjkSegment } from "../core/line-breaker/cjk.js";
18
18
  import "../core/line-breaker/index.js";
19
+ import { formatCounter } from "../core/numbering/state.js";
19
20
  import { DEFAULT_RESOLVED_PARAGRAPH, DEFAULT_RESOLVED_RUN } from "../core/style-cascade/types.js";
20
21
  import { resolveParagraphProperties, resolveRunProperties } from "../core/style-cascade/resolver.js";
21
22
  import "../core/style-cascade/index.js";
@@ -28,6 +29,7 @@ import { layoutMath, mathGlyphSegments, variantStyle } from "./math-layout.js";
28
29
  import { rectPath } from "../core/drawingml/preset-geometry.js";
29
30
  import { buildShapePaths, buildShapeTransform, buildStroke, gradientToSolid, lineEndPaths } from "../core/drawingml/shape-render.js";
30
31
  import { StructTreeBuilder } from "../pdf/struct-tree.js";
32
+ import { floatIntoFrame, turnOntoSheet } from "./turned-section.js";
31
33
  //#region src/layout/styled-layout.ts
32
34
  function parsePdfAProfile(pdfA) {
33
35
  const part = Number(pdfA.charAt(6));
@@ -39,6 +41,260 @@ function parsePdfAProfile(pdfA) {
39
41
  version: part === 1 ? "1.4" : "1.7"
40
42
  };
41
43
  }
44
+ function leadingOf(options) {
45
+ const profile = options.layoutProfile ?? "ream";
46
+ if (profile !== "ream") return profile;
47
+ return options.typesetBy === "word" ? "word" : "flat";
48
+ }
49
+ var FLAT_LEADING = {
50
+ model: "flat",
51
+ markLinePt: 0
52
+ };
53
+ /**
54
+ * The single line Word sets a family at, in 2048ths of the size: the ascent,
55
+ * the gap it adds above it, and the descent. It is the face's `hhea` line (the
56
+ * `OS/2` typo line where the face sets USE_TYPO_METRICS), not the `usWin` cell
57
+ * — Book Antiqua stands 1.206 of its size in Word, its cell is 1.243. Measured
58
+ * in Word for Mac on the families marked `*`; the rest read by the same rule
59
+ * from the faces Office ships. The layout draws most of them with a twin or a
60
+ * fallback whose own line differs (Caladea stands 1.15 of its size, Cambria
61
+ * 1.172), so a run's height comes from here, by the family it names, and the
62
+ * face in hand answers only for a family not listed.
63
+ */
64
+ var WORD_LINE_METRICS = (() => {
65
+ const arial = [
66
+ 1854,
67
+ 67,
68
+ 434
69
+ ];
70
+ const times = [
71
+ 1825,
72
+ 87,
73
+ 443
74
+ ];
75
+ const courier = [
76
+ 1705,
77
+ 0,
78
+ 615
79
+ ];
80
+ const aptos = [
81
+ 1923,
82
+ 0,
83
+ 577
84
+ ];
85
+ const franklin = [
86
+ 1877,
87
+ 0,
88
+ 445
89
+ ];
90
+ const century = [
91
+ 2019,
92
+ 0,
93
+ 443
94
+ ];
95
+ const lucida = [
96
+ 1980,
97
+ 0,
98
+ 432
99
+ ];
100
+ return new Map([
101
+ ["calibri", [
102
+ 1950,
103
+ 0,
104
+ 550
105
+ ]],
106
+ ["calibri light", [
107
+ 1950,
108
+ 0,
109
+ 550
110
+ ]],
111
+ ["carlito", [
112
+ 1950,
113
+ 0,
114
+ 550
115
+ ]],
116
+ ["cambria", [
117
+ 1946,
118
+ 0,
119
+ 455
120
+ ]],
121
+ ["arial", arial],
122
+ ["helvetica", arial],
123
+ ["liberation sans", arial],
124
+ ["arimo", arial],
125
+ ["arial narrow", [
126
+ 1916,
127
+ 0,
128
+ 434
129
+ ]],
130
+ ["arial black", [
131
+ 2254,
132
+ 0,
133
+ 634
134
+ ]],
135
+ ["times new roman", times],
136
+ ["times", times],
137
+ ["liberation serif", times],
138
+ ["tinos", times],
139
+ ["courier new", courier],
140
+ ["courier", courier],
141
+ ["liberation mono", courier],
142
+ ["cousine", courier],
143
+ ["aptos", aptos],
144
+ ["aptos narrow", aptos],
145
+ ["aptos light", aptos],
146
+ ["aptos semibold", aptos],
147
+ ["aptos extrabold", aptos],
148
+ ["aptos black", aptos],
149
+ ["georgia", [
150
+ 1878,
151
+ 0,
152
+ 449
153
+ ]],
154
+ ["verdana", [
155
+ 2059,
156
+ 0,
157
+ 430
158
+ ]],
159
+ ["tahoma", [
160
+ 2049,
161
+ 0,
162
+ 423
163
+ ]],
164
+ ["trebuchet ms", [
165
+ 1923,
166
+ 0,
167
+ 455
168
+ ]],
169
+ ["candara", [
170
+ 1484,
171
+ 452,
172
+ 564
173
+ ]],
174
+ ["consolas", [
175
+ 1521,
176
+ 350,
177
+ 527
178
+ ]],
179
+ ["constantia", [
180
+ 1538,
181
+ 452,
182
+ 510
183
+ ]],
184
+ ["corbel", [
185
+ 1523,
186
+ 425,
187
+ 525
188
+ ]],
189
+ ["century gothic", [
190
+ 2060,
191
+ 0,
192
+ 451
193
+ ]],
194
+ ["century", century],
195
+ ["century schoolbook", century],
196
+ ["garamond", [
197
+ 1765,
198
+ 0,
199
+ 539
200
+ ]],
201
+ ["book antiqua", [
202
+ 1891,
203
+ 0,
204
+ 578
205
+ ]],
206
+ ["palatino linotype", [
207
+ 2150,
208
+ 0,
209
+ 613
210
+ ]],
211
+ ["bookman old style", [
212
+ 1929,
213
+ 0,
214
+ 475
215
+ ]],
216
+ ["rockwell", [
217
+ 1937,
218
+ 0,
219
+ 468
220
+ ]],
221
+ ["franklin gothic book", franklin],
222
+ ["franklin gothic medium", franklin],
223
+ ["franklin gothic demi", franklin],
224
+ ["gill sans mt", [
225
+ 1903,
226
+ 0,
227
+ 472
228
+ ]],
229
+ ["tw cen mt", [
230
+ 1753,
231
+ 0,
232
+ 477
233
+ ]],
234
+ ["lucida sans unicode", [
235
+ 2246,
236
+ 0,
237
+ 901
238
+ ]],
239
+ ["lucida sans", lucida],
240
+ ["lucida bright", lucida],
241
+ ["lucida console", [
242
+ 1616,
243
+ 0,
244
+ 432
245
+ ]],
246
+ ["comic sans ms", [
247
+ 2257,
248
+ 0,
249
+ 597
250
+ ]],
251
+ ["impact", [
252
+ 2066,
253
+ 0,
254
+ 432
255
+ ]],
256
+ ["goudy old style", [
257
+ 1832,
258
+ 140,
259
+ 486
260
+ ]],
261
+ ["calisto mt", [
262
+ 1894,
263
+ 0,
264
+ 470
265
+ ]],
266
+ ["perpetua", [
267
+ 1679,
268
+ 0,
269
+ 668
270
+ ]],
271
+ ["symbol", [
272
+ 2059,
273
+ 0,
274
+ 450
275
+ ]],
276
+ ["wingdings", [
277
+ 1841,
278
+ 0,
279
+ 432
280
+ ]],
281
+ ["wingdings 2", [
282
+ 1727,
283
+ 0,
284
+ 432
285
+ ]],
286
+ ["wingdings 3", [
287
+ 1900,
288
+ 0,
289
+ 432
290
+ ]],
291
+ ["webdings", [
292
+ 1638,
293
+ 0,
294
+ 410
295
+ ]]
296
+ ]);
297
+ })();
42
298
  /** §17.6.4 — how heavy Word draws the rule between columns. */
43
299
  var COLUMN_SEPARATOR_PT = .5;
44
300
  /**
@@ -55,7 +311,6 @@ var FOOTNOTE_RULE_PT = .75;
55
311
  var FOOTNOTE_RULE_GAP_ABOVE = 4;
56
312
  var FOOTNOTE_SEPARATOR_HEIGHT = 10;
57
313
  var LINE_NUMBER_GAP_PT = 18;
58
- var FLOAT_TEXT_GAP = 6;
59
314
  var MIN_WRAP_WIDTH = 36;
60
315
  function isOutOfFlowFloat(f) {
61
316
  return f !== void 0 && f.wrap !== "topAndBottom";
@@ -315,6 +570,7 @@ function layoutStyledDocument(body, options) {
315
570
  const blocks = numberedBody.map((el, idx) => {
316
571
  while (sectionIdx < sectionCtxs.length - 1 && idx >= sectionCtxs[sectionIdx].endIndex) sectionIdx++;
317
572
  const ctx = sectionCtxs[sectionIdx];
573
+ const own = ctx.sheet ? floatIntoFrame(el, ctx.sheet) : el;
318
574
  const width = ctx.columns ? ctx.columns[0].widthPt : ctx.contentWidth;
319
575
  const box = {
320
576
  pageWidthPt: ctx.pageWidth,
@@ -327,7 +583,7 @@ function layoutStyledDocument(body, options) {
327
583
  const group = bodyFrames.starts.get(idx);
328
584
  if (group) return layoutFrameBlock(group, ctx.options ?? options, fontResources, imageResources, width);
329
585
  if (bodyFrames.continued.has(idx)) return emptyBlock(options);
330
- return layoutBodyElement(el, ctx.options ?? options, fontResources, imageResources, width, ctx.pageContentHeight, box);
586
+ return layoutBodyElement(own, ctx.options ?? options, fontResources, imageResources, width, ctx.pageContentHeight, box);
331
587
  });
332
588
  for (let i = 0; i < blocks.length; i++) {
333
589
  const here = blocks[i];
@@ -359,9 +615,15 @@ function layoutStyledDocument(body, options) {
359
615
  let laid = void 0;
360
616
  if (content && n !== void 0 && !noteAssigned.deferredFootnotes.includes(id)) {
361
617
  const noteBlocks = substituteNoteNumber(content, n).map((el) => layoutBodyElement(el, sectionCtx.options ?? options, fontResources, imageResources, sectionCtx.contentWidth, sectionCtx.pageContentHeight));
618
+ const spacing = new SpacingStack(collapsesSpacing(sectionCtx.options ?? options));
362
619
  laid = {
363
620
  blocks: noteBlocks,
364
- heightPt: noteBlocks.reduce((sum, b) => sum + (b.kind === "paragraph" ? b.spacingBeforePt + b.heightPt + b.spacingAfterPt : 0), 0)
621
+ heightPt: noteBlocks.reduce((sum, b) => {
622
+ if (b.kind !== "paragraph") return sum;
623
+ const before = spacing.open(b.spacingBeforePt);
624
+ spacing.close(b.spacingAfterPt);
625
+ return sum + before + b.heightPt + b.spacingAfterPt;
626
+ }, 0)
365
627
  };
366
628
  }
367
629
  byId.set(id, laid);
@@ -435,7 +697,7 @@ function layoutStyledDocument(body, options) {
435
697
  const commentNotes = options.commentAnnotations && options.comments ? buildCommentNotes(noteAssigned.comments, options.comments) : void 0;
436
698
  const reflowParagraph = (paragraph, width, widths) => layoutParagraphBlock(paragraph, options, fontResources, imageResources, width, widths);
437
699
  return {
438
- pages: paginateSections(blocks, sectionCtxs, structBuilder, options.language ?? "en-US", notePlan, bookmarks, reflowParagraph, backgroundImageOf(options, imageResources)),
700
+ pages: paginateSections(blocks, sectionCtxs, structBuilder, options.language ?? "en-US", notePlan, bookmarks, reflowParagraph, backgroundImageOf(options, imageResources), collapsesSpacing(options), options.compatibilityMode),
439
701
  resources: options.resources ?? new ResourceStore(),
440
702
  fontResources,
441
703
  imageResources,
@@ -509,9 +771,52 @@ function buildSectionContext(section, options, headersFooters, fontResources, im
509
771
  bottom: dims.marginBottom
510
772
  }, relativeBox);
511
773
  const columns = buildColumnGeometry(section.properties.columns, contentWidth);
512
- const headerHeight = Math.max(headerSet.default.heightPt ?? 0, section.properties.titlePg === true ? headerSet.first.heightPt ?? 0 : 0, section.properties.evenAndOddHeaders === true ? headerSet.even.heightPt ?? 0 : 0);
513
- const headerBottom = headerHeight > 0 ? dims.headerOffsetPt + headerHeight : 0;
514
- const marginTop = Math.max(dims.marginTop, headerBottom);
774
+ const bodyMarginsFor = (band) => {
775
+ const header = pickBand(headerSet, band).heightPt ?? 0;
776
+ const footer = pickBand(footerSet, band).heightPt ?? 0;
777
+ return {
778
+ top: Math.max(dims.marginTop, header > 0 ? dims.headerOffsetPt + header : 0),
779
+ bottom: Math.max(dims.marginBottom, footer > 0 ? dims.footerOffsetPt + footer : 0)
780
+ };
781
+ };
782
+ const bandMargins = {
783
+ default: bodyMarginsFor("default"),
784
+ first: bodyMarginsFor("first"),
785
+ even: bodyMarginsFor("even")
786
+ };
787
+ const marginTop = bandMargins.default.top;
788
+ const marginBottom = bandMargins.default.bottom;
789
+ if (section.properties.textDirection !== void 0) {
790
+ const sheet = {
791
+ width: dims.pageWidth,
792
+ height: dims.pageHeight,
793
+ marginLeft: dims.marginLeft,
794
+ marginRight: dims.marginRight,
795
+ marginTop,
796
+ marginBottom
797
+ };
798
+ const across = sheet.height - sheet.marginTop - sheet.marginBottom;
799
+ const turnedColumns = buildColumnGeometry(section.properties.columns, across);
800
+ return {
801
+ endIndex: section.endIndex,
802
+ properties: section.properties,
803
+ pageWidth: sheet.height,
804
+ pageHeight: sheet.width,
805
+ marginLeft: sheet.marginTop,
806
+ marginTop: sheet.marginRight,
807
+ marginBottom: sheet.marginLeft,
808
+ contentWidth: across,
809
+ pageContentHeight: sheet.width - sheet.marginRight - sheet.marginLeft,
810
+ ...turnedColumns ? { columns: turnedColumns } : {},
811
+ headerSet,
812
+ footerSet,
813
+ titlePg: section.properties.titlePg === true,
814
+ evenAndOddHeaders: section.properties.evenAndOddHeaders === true,
815
+ continuous: section.properties.sectionStart === "continuous",
816
+ options: secOptions,
817
+ sheet
818
+ };
819
+ }
515
820
  return {
516
821
  endIndex: section.endIndex,
517
822
  properties: section.properties,
@@ -519,12 +824,13 @@ function buildSectionContext(section, options, headersFooters, fontResources, im
519
824
  pageHeight: dims.pageHeight,
520
825
  marginLeft: dims.marginLeft,
521
826
  marginTop,
522
- marginBottom: dims.marginBottom,
827
+ marginBottom,
523
828
  contentWidth,
524
- pageContentHeight: dims.pageHeight - marginTop - dims.marginBottom,
829
+ pageContentHeight: dims.pageHeight - marginTop - marginBottom,
525
830
  ...columns ? { columns } : {},
526
831
  headerSet,
527
832
  footerSet,
833
+ bandMargins,
528
834
  titlePg: section.properties.titlePg === true,
529
835
  evenAndOddHeaders: section.properties.evenAndOddHeaders === true,
530
836
  continuous: section.properties.sectionStart === "continuous",
@@ -572,15 +878,15 @@ function layoutHeaderSet(section, headersFooters, options, fontResources, imageR
572
878
  let measured = 0;
573
879
  const render = (c) => {
574
880
  const blocks = laidOutBlocksFor(c, options, fontResources, contentWidth, imageResources, relativeBox);
575
- measured = blocksHeight(blocks);
576
- return markPagination(drawBlocksSequentially(blocks, marginLeft, pageHeight - headerOffsetPt, pageHeight, contentWidth, void 0, bandMargins));
881
+ measured = blocksHeight(blocks, collapsesSpacing(options));
882
+ return markPagination(drawBlocksSequentially(blocks, marginLeft, pageHeight - headerOffsetPt, pageHeight, contentWidth, void 0, bandMargins, collapsesSpacing(options)));
577
883
  };
578
884
  if (contentHasPageFields(content)) {
579
- render(substitutePageFields(content, 1, 1));
885
+ render(substitutePageFields(content, "1", 1));
580
886
  return {
581
887
  commands: [],
582
888
  heightPt: measured,
583
- renderDynamic: (n, total) => render(substitutePageFields(content, n, total))
889
+ renderDynamic: (page, total) => render(substitutePageFields(content, page, total))
584
890
  };
585
891
  }
586
892
  return {
@@ -609,11 +915,61 @@ function balancedColumnHeight(bandHeightPt, colCount, roomPt) {
609
915
  if (bandHeightPt > roomPt * colCount) return 0;
610
916
  return Math.min(roomPt, bandHeightPt / colCount);
611
917
  }
612
- /** Total laid-out height of a run of blocks, paragraph spacing included. */
613
- function blocksHeight(blocks) {
918
+ /**
919
+ * §17.3.1.33 — the space between paragraphs down one story (a page, a cell, a
920
+ * band, a box). Where the document is set as Word sets it, two paragraphs
921
+ * stand the LARGER of the first's space after and the second's space before
922
+ * apart, not the two together. Measured in Word for Mac, in modes 14 and 15
923
+ * alike: 10pt after and 10pt before stand 10pt apart; 10 and 24 stand 24,
924
+ * whichever comes first; 6 and 12 stand 12 either way. An empty paragraph
925
+ * between takes its share of both sides. Elsewhere the two add up.
926
+ */
927
+ var SpacingStack = class {
928
+ /** The space after the last block, which a paragraph right below may share. */
929
+ shared = 0;
930
+ constructor(collapse) {
931
+ this.collapse = collapse;
932
+ }
933
+ /**
934
+ * @param beforePt A block's own space before.
935
+ * @returns The room to open above it.
936
+ */
937
+ open(beforePt) {
938
+ const room = this.collapse ? Math.max(0, beforePt - this.shared) : beforePt;
939
+ this.shared = 0;
940
+ return room;
941
+ }
942
+ /** @param spacingPt The space a block just left below itself, as far as a paragraph may share it. */
943
+ close(spacingPt) {
944
+ this.shared = spacingPt;
945
+ }
946
+ };
947
+ /** Whether a layout stacks paragraph spacing as Word does (see {@link SpacingStack}). */
948
+ function collapsesSpacing(options) {
949
+ return options.typesetBy === "word";
950
+ }
951
+ /** A block's space after, less the line a picture's spacing adds under it: the part a paragraph below shares. */
952
+ function sharedAfter(block) {
953
+ return block.spacingAfterPt - (block.lineExtraPt ?? 0);
954
+ }
955
+ /**
956
+ * Total laid-out height of a run of blocks, paragraph spacing included.
957
+ *
958
+ * @param blocks The blocks, in order.
959
+ * @param collapse Whether their spacing stacks as Word stacks it (see {@link SpacingStack}).
960
+ * @returns The height in points.
961
+ */
962
+ function blocksHeight(blocks, collapse = false) {
963
+ const spacing = new SpacingStack(collapse);
614
964
  return blocks.reduce((sum, b) => {
615
965
  if (b.kind !== "paragraph" && isOutOfFlowFloat(b.float)) return sum;
616
- return sum + (b.kind === "paragraph" ? b.spacingBeforePt + b.heightPt + b.spacingAfterPt : b.heightPt);
966
+ if (!("spacingBeforePt" in b)) {
967
+ spacing.close(0);
968
+ return sum + b.heightPt;
969
+ }
970
+ const before = spacing.open(b.spacingBeforePt);
971
+ spacing.close(sharedAfter(b));
972
+ return sum + before + b.heightPt + b.spacingAfterPt;
617
973
  }, 0);
618
974
  }
619
975
  function layoutFooterSet(section, headersFooters, options, fontResources, imageResources, contentWidth, marginLeft, pageHeight, footerOffsetPt, bandMargins, relativeBox) {
@@ -622,15 +978,25 @@ function layoutFooterSet(section, headersFooters, options, fontResources, imageR
622
978
  if (!ref) return { commands: [] };
623
979
  const content = headersFooters.get(ref.relationshipId);
624
980
  if (!content) return { commands: [] };
981
+ let measured = 0;
625
982
  const render = (c) => {
626
983
  const blocks = laidOutBlocksFor(c, options, fontResources, contentWidth, imageResources, relativeBox);
627
- return markPagination(drawBlocksSequentially(blocks, marginLeft, footerOffsetPt + blocksHeight(blocks), pageHeight, contentWidth, void 0, bandMargins));
984
+ const totalHeight = blocksHeight(blocks, collapsesSpacing(options));
985
+ measured = totalHeight;
986
+ return markPagination(drawBlocksSequentially(blocks, marginLeft, footerOffsetPt + totalHeight, pageHeight, contentWidth, void 0, bandMargins, collapsesSpacing(options)));
628
987
  };
629
- if (contentHasPageFields(content)) return {
630
- commands: [],
631
- renderDynamic: (n, total) => render(substitutePageFields(content, n, total))
988
+ if (contentHasPageFields(content)) {
989
+ render(substitutePageFields(content, "1", 1));
990
+ return {
991
+ commands: [],
992
+ heightPt: measured,
993
+ renderDynamic: (page, total) => render(substitutePageFields(content, page, total))
994
+ };
995
+ }
996
+ return {
997
+ commands: render(content),
998
+ heightPt: measured
632
999
  };
633
- return { commands: render(content) };
634
1000
  };
635
1001
  return {
636
1002
  default: band("default"),
@@ -642,7 +1008,20 @@ function contentHasPageFields(content) {
642
1008
  for (const el of content) if (el.kind === "paragraph" && el.paragraph.runs.some((r) => r.field !== void 0)) return true;
643
1009
  return false;
644
1010
  }
645
- function substitutePageFields(content, pageNumber, totalPages) {
1011
+ /**
1012
+ * §17.6.12 — a page's number as its section prints it: in the numerals
1013
+ * `w:pgNumType w:fmt` names (§17.18.59), decimal where it names none. A format
1014
+ * that has no numeral for the number — a roman zero — prints it in digits.
1015
+ *
1016
+ * @param pageNumber The page's number in its section's count.
1017
+ * @param format The section's page-number format.
1018
+ * @returns The text a PAGE field shows on that page.
1019
+ */
1020
+ function pageNumberText(pageNumber, format) {
1021
+ const text = format === void 0 ? "" : formatCounter(format, pageNumber);
1022
+ return text !== "" ? text : String(pageNumber);
1023
+ }
1024
+ function substitutePageFields(content, pageText, totalPages) {
646
1025
  return content.map((el) => {
647
1026
  if (el.kind !== "paragraph") return el;
648
1027
  if (!el.paragraph.runs.some((r) => r.field !== void 0)) return el;
@@ -652,7 +1031,7 @@ function substitutePageFields(content, pageNumber, totalPages) {
652
1031
  ...el.paragraph,
653
1032
  runs: el.paragraph.runs.map((r) => r.field === void 0 ? r : {
654
1033
  ...r,
655
- text: String(r.field === "PAGE" ? pageNumber : totalPages)
1034
+ text: r.field === "PAGE" ? pageText : String(totalPages)
656
1035
  })
657
1036
  }
658
1037
  };
@@ -953,12 +1332,33 @@ function layoutBodyElement(el, options, fontResources, imageResources, contentWi
953
1332
  if (el.kind === "image") {
954
1333
  const res = el.image.resource ? imageResources?.get(el.image.resource) : void 0;
955
1334
  const meta = res?.metafile;
956
- return meta ? layoutMetafileBlock(el.image, meta, res.metafileImages, options, fontResources, contentWidth, maxHeight, box) : layoutImageBlock(el.image, imageResources, contentWidth, maxHeight, box);
1335
+ return meta ? layoutMetafileBlock(el.image, meta, res.metafileImages, options, fontResources, contentWidth, maxHeight, box) : layoutImageBlock(el.image, imageResources, contentWidth, maxHeight, box, options);
957
1336
  }
958
1337
  if (el.kind === "chart") return layoutChartBlock(el.chart, options, fontResources, contentWidth, maxHeight, imageResources);
959
1338
  return layoutShapeBlock(el.shape, options, fontResources, imageResources, contentWidth, maxHeight, box);
960
1339
  }
961
- function layoutImageBlock(image, imageResources, contentWidth, maxHeight, box) {
1340
+ /**
1341
+ * §17.3.1.33 — what a paragraph's line spacing adds under a picture that
1342
+ * stands on a line of its own. Word sets that line as tall as the picture,
1343
+ * and a spacing of more than one line adds what the extra lines of the
1344
+ * paragraph mark's font would, not a share of the picture: in Word for Mac a
1345
+ * 100pt picture's next paragraph stands 100, 102.5 and 113 points down at
1346
+ * single, 1.15 and double spacing of an 11pt mark.
1347
+ *
1348
+ * @param pp The paragraph the picture stands on, resolved.
1349
+ * @param options The render options its mark's font resolves against; absent
1350
+ * for a picture out of the flow.
1351
+ * @returns The extra points, 0 where the spacing is not a multiple above one.
1352
+ */
1353
+ function pictureLineExtra(pp, options) {
1354
+ if (!options) return 0;
1355
+ if (pp.spacingLine === void 0 || (pp.spacingLineRule ?? "auto") !== "auto") return 0;
1356
+ const multiple = Math.round(pp.spacingLine * 20) / 240;
1357
+ if (multiple <= 1) return 0;
1358
+ if (leadingOf(options) !== "flat") return (multiple - 1) * paragraphLeading(pp, options).markLinePt;
1359
+ return (multiple - 1) * resolveRunProperties({}, pp, options.styles).fontSizePt * 1.2;
1360
+ }
1361
+ function layoutImageBlock(image, imageResources, contentWidth, maxHeight, box, options) {
962
1362
  let widthPt = relativeWidth(image.relativeSize, contentWidth, box) ?? image.width;
963
1363
  let heightPt = relativeHeight(image.relativeSize, maxHeight, box) ?? (image.relativeSize?.widthPct && image.width > 0 ? image.height * (widthPt / image.width) : image.height);
964
1364
  if (widthPt > contentWidth && !image.float) {
@@ -993,6 +1393,7 @@ function layoutImageBlock(image, imageResources, contentWidth, maxHeight, box) {
993
1393
  heightPt = reservedH;
994
1394
  drawInset = box;
995
1395
  }
1396
+ const lineExtraPt = image.float ? 0 : pictureLineExtra(image.paragraphProperties, options);
996
1397
  const res = image.resource ? imageResources?.get(image.resource) : void 0;
997
1398
  const resolvedAlignment = image.paragraphProperties.alignment ?? "left";
998
1399
  return {
@@ -1010,7 +1411,8 @@ function layoutImageBlock(image, imageResources, contentWidth, maxHeight, box) {
1010
1411
  ...image.flipV ? { flipV: true } : {},
1011
1412
  ...drawInset ? { drawInset } : {},
1012
1413
  spacingBeforePt: image.paragraphProperties.spacingBefore ?? 0,
1013
- spacingAfterPt: image.paragraphProperties.spacingAfter ?? 0,
1414
+ spacingAfterPt: (image.paragraphProperties.spacingAfter ?? 0) + lineExtraPt,
1415
+ ...lineExtraPt > 0 ? { lineExtraPt } : {},
1014
1416
  ...image.altText ? { altText: image.altText } : {},
1015
1417
  ...image.float ? { float: image.float } : {}
1016
1418
  };
@@ -1054,8 +1456,10 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
1054
1456
  const upright = text?.upright;
1055
1457
  const warp = text?.warp;
1056
1458
  if (text && text.content.length > 0) {
1057
- const innerWidth = warp ? Number.MAX_SAFE_INTEGER : vertical ? Math.max(1, heightPt - insetTopPt - insetBottomPt) : Math.max(1, widthPt - insetLeftPt - insetRightPt);
1459
+ const innerWidth = warp || text.noWrap === true ? Number.MAX_SAFE_INTEGER : vertical ? Math.max(1, heightPt - insetTopPt - insetBottomPt) : Math.max(1, widthPt - insetLeftPt - insetRightPt);
1460
+ const spacing = new SpacingStack(collapsesSpacing(options));
1058
1461
  for (const el of text.content) {
1462
+ if (el.kind !== "paragraph") spacing.close(0);
1059
1463
  if (el.kind === "image") {
1060
1464
  const res = el.image.resource ? imageResources?.get(el.image.resource) : void 0;
1061
1465
  const scaled = Math.min(1, innerWidth / Math.max(1, el.image.width));
@@ -1129,7 +1533,8 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
1129
1533
  continue;
1130
1534
  }
1131
1535
  const blk = layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, innerWidth);
1132
- const before = textLines.length > 0 ? blk.spacingBeforePt : 0;
1536
+ const opened = spacing.open(blk.spacingBeforePt);
1537
+ const before = textLines.length > 0 ? opened : 0;
1133
1538
  if (before > 0) {
1134
1539
  const last = textLines.length - 1;
1135
1540
  textLineGaps.set(last, (textLineGaps.get(last) ?? 0) + before);
@@ -1144,6 +1549,7 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
1144
1549
  textLineGaps.set(last, (textLineGaps.get(last) ?? 0) + blk.spacingAfterPt);
1145
1550
  }
1146
1551
  textHeightPt += blk.spacingAfterPt;
1552
+ spacing.close(blk.spacingAfterPt);
1147
1553
  }
1148
1554
  }
1149
1555
  if (text?.autoFit && !vertical && textLines.length > 0) heightPt = textHeightPt + insetTopPt + insetBottomPt;
@@ -1259,6 +1665,7 @@ function layoutMetafileBlock(image, pic, blitted, options, fontResources, conten
1259
1665
  const heightPt = placed.heightPt;
1260
1666
  const layout = rotateDrawing(metafileDrawing(pic, widthPt, heightPt, options, fontResources, image.colorChange, blitted), (image.rotation60k ?? 0) / 6e4, widthPt / 2, heightPt / 2);
1261
1667
  const pp = image.paragraphProperties;
1668
+ const lineExtraPt = image.float ? 0 : pictureLineExtra(pp, options);
1262
1669
  return {
1263
1670
  kind: "chart",
1264
1671
  widthPt,
@@ -1266,7 +1673,8 @@ function layoutMetafileBlock(image, pic, blitted, options, fontResources, conten
1266
1673
  layout,
1267
1674
  resolvedAlignment: pp.alignment ?? "left",
1268
1675
  spacingBeforePt: pp.spacingBefore ?? 0,
1269
- spacingAfterPt: pp.spacingAfter ?? 0,
1676
+ spacingAfterPt: (pp.spacingAfter ?? 0) + lineExtraPt,
1677
+ ...lineExtraPt > 0 ? { lineExtraPt } : {},
1270
1678
  figureRole: "Image",
1271
1679
  ...image.altText ? { altText: image.altText } : {},
1272
1680
  ...image.float ? { float: image.float } : {}
@@ -2231,9 +2639,10 @@ function bandAnchorTop(float, cursorY, pageHeight, heightPt = 0, margins) {
2231
2639
  const offset = float.posV?.offsetPt ?? 0;
2232
2640
  return float.posV?.relativeFrom === "page" ? pageHeight - offset : cursorY - offset;
2233
2641
  }
2234
- function drawBlocksSequentially(blocks, startX, startY, pageHeight, contentWidth, structId, bandMargins) {
2642
+ function drawBlocksSequentially(blocks, startX, startY, pageHeight, contentWidth, structId, bandMargins, collapse = false) {
2235
2643
  const out = [];
2236
2644
  let cursorY = startY;
2645
+ const spacing = new SpacingStack(collapse);
2237
2646
  const isBehind = (b) => "float" in b && b.float?.behind === true;
2238
2647
  const ordered = blocks.some(isBehind) ? [...blocks.filter(isBehind), ...blocks.filter((b) => !isBehind(b))] : blocks;
2239
2648
  for (const block of ordered) {
@@ -2252,18 +2661,20 @@ function drawBlocksSequentially(blocks, startX, startY, pageHeight, contentWidth
2252
2661
  emitRowChunk(out, row, tableX, cursorY, pageHeight, block.colCount);
2253
2662
  cursorY -= row.heightPt;
2254
2663
  }
2664
+ spacing.close(0);
2255
2665
  continue;
2256
2666
  }
2257
2667
  if (block.kind === "chart") {
2258
- cursorY -= block.spacingBeforePt + block.heightPt;
2668
+ cursorY -= spacing.open(block.spacingBeforePt) + block.heightPt;
2259
2669
  const offset = alignmentOffset(block.resolvedAlignment, block.widthPt, contentWidth);
2260
2670
  out.push(...chartPageItems(block, startX + offset, cursorY, pageHeight, structId));
2261
2671
  cursorY -= block.spacingAfterPt;
2672
+ spacing.close(sharedAfter(block));
2262
2673
  continue;
2263
2674
  }
2264
2675
  if (block.kind === "image") {
2265
- cursorY -= block.spacingBeforePt;
2266
2676
  const anchored = bandAnchor(block.float, block.widthPt, contentWidth, startX);
2677
+ cursorY -= anchored === void 0 ? spacing.open(block.spacingBeforePt) : block.spacingBeforePt;
2267
2678
  const offset = anchored ?? alignmentOffset(block.resolvedAlignment, block.widthPt, contentWidth);
2268
2679
  const top = anchored === void 0 || !block.float ? cursorY : bandAnchorTop(block.float, cursorY, pageHeight, block.heightPt, bandMargins);
2269
2680
  out.push({
@@ -2283,19 +2694,21 @@ function drawBlocksSequentially(blocks, startX, startY, pageHeight, contentWidth
2283
2694
  if (block.outline) out.push(...pictureOutlineItems(block.outline, startX + offset, pageHeight - top, block.widthPt, block.heightPt, structId));
2284
2695
  if (anchored === void 0) cursorY -= block.heightPt;
2285
2696
  cursorY -= block.spacingAfterPt;
2697
+ if (anchored === void 0) spacing.close(sharedAfter(block));
2286
2698
  continue;
2287
2699
  }
2288
2700
  if (block.kind === "shape") {
2289
- cursorY -= block.spacingBeforePt;
2290
2701
  const anchored = bandAnchor(block.float, block.widthPt, contentWidth, startX);
2702
+ cursorY -= anchored === void 0 ? spacing.open(block.spacingBeforePt) : block.spacingBeforePt;
2291
2703
  const offset = anchored ?? alignmentOffset(block.resolvedAlignment, block.widthPt, contentWidth);
2292
2704
  const top = anchored === void 0 || !block.float ? cursorY : bandAnchorTop(block.float, cursorY, pageHeight, block.heightPt, bandMargins);
2293
2705
  emitShapeItems(block, startX + offset, top - block.heightPt, out, pageHeight, structId);
2294
2706
  if (anchored === void 0) cursorY -= block.heightPt;
2295
2707
  cursorY -= block.spacingAfterPt;
2708
+ if (anchored === void 0) spacing.close(block.spacingAfterPt);
2296
2709
  continue;
2297
2710
  }
2298
- cursorY -= block.spacingBeforePt;
2711
+ cursorY -= spacing.open(block.spacingBeforePt);
2299
2712
  const bandTopY = cursorY;
2300
2713
  for (const line of block.lines) {
2301
2714
  const h = computeLineHeight(line, block.resolved);
@@ -2312,6 +2725,7 @@ function drawBlocksSequentially(blocks, startX, startY, pageHeight, contentWidth
2312
2725
  }
2313
2726
  out.push(...paragraphDecoration(block.resolved, startX + block.resolved.indentLeft, startX + contentWidth - block.resolved.indentRight, bandTopY, cursorY, pageHeight));
2314
2727
  cursorY -= block.spacingAfterPt;
2728
+ spacing.close(block.spacingAfterPt);
2315
2729
  }
2316
2730
  return out;
2317
2731
  }
@@ -2604,6 +3018,7 @@ function lookupFont(resources, fontKey) {
2604
3018
  }
2605
3019
  function collectFontResources(body, headersFooters, options, imageResources) {
2606
3020
  const used = /* @__PURE__ */ new Map();
3021
+ const numerals = [...new Set([options.section, ...(options.sections ?? []).map((sec) => sec.properties)].flatMap((p) => p?.pageNumberFormat !== void 0 ? [p.pageNumberFormat] : []))].map((fmt) => Array.from({ length: 200 }, (_, i) => formatCounter(fmt, i + 1)).join("")).join("");
2607
3022
  const addRun = (run, para) => {
2608
3023
  const resolved = resolveRunProperties(run.properties, para.properties, options.styles);
2609
3024
  const { fontKey, parsed } = runFontKeyAndParsed(options, resolved.fontFamily.ascii, resolved.bold, resolved.italic);
@@ -2678,7 +3093,7 @@ function collectFontResources(body, headersFooters, options, imageResources) {
2678
3093
  if (run.inlineImage && !run.text) continue;
2679
3094
  if (run.field !== void 0) {
2680
3095
  addRun({
2681
- text: `${run.text}0123456789`,
3096
+ text: `${run.text}0123456789${numerals}`,
2682
3097
  properties: run.properties
2683
3098
  }, el.paragraph);
2684
3099
  continue;
@@ -2793,13 +3208,10 @@ function collectFontResources(body, headersFooters, options, imageResources) {
2793
3208
  * the paragraph mark's own font (§17.3.1.31 `w:rPr` on the `w:pPr`, which is
2794
3209
  * exactly the run properties Word keeps for a paragraph that holds no runs).
2795
3210
  */
2796
- function emptyLine(paragraph, options, resolved, availableWidthPt, fontResources) {
2797
- const mark = resolveRunProperties(paragraph.properties.runProperties ?? {}, paragraph.properties, options.styles);
2798
- const fontSizePt = mark.fontSizePt;
2799
- const profile = options.layoutProfile ?? "ream";
2800
- const { variant } = options.registry.resolveByStyle(mark.bold, mark.italic);
2801
- const parsed = fontResources.get(variant)?.parsed;
2802
- const metric = profile !== "ream" && parsed ? fontLeadingPt(parsed, fontSizePt, profile) : void 0;
3211
+ function emptyLine(paragraph, options, resolved, availableWidthPt) {
3212
+ const fontSizePt = resolveRunProperties(paragraph.properties.runProperties ?? {}, paragraph.properties, options.styles).fontSizePt;
3213
+ const model = leadingOf(options);
3214
+ const metric = model === "flat" ? void 0 : markLine(paragraph.properties, options, model);
2803
3215
  return {
2804
3216
  tokens: [],
2805
3217
  contentWidthPt: 0,
@@ -2810,10 +3222,37 @@ function emptyLine(paragraph, options, resolved, availableWidthPt, fontResources
2810
3222
  isLastInParagraph: true,
2811
3223
  ...metric && metric.ascentPt + metric.descentPt > 0 ? {
2812
3224
  metricHeightPt: metric.ascentPt + metric.descentPt + metric.lineGapPt,
2813
- metricDescentPt: metric.descentPt
3225
+ metricDescentPt: metric.descentPt,
3226
+ metricTextHeightPt: metric.ascentPt + metric.descentPt + metric.lineGapPt
2814
3227
  } : {}
2815
3228
  };
2816
3229
  }
3230
+ /**
3231
+ * The single line of a paragraph mark's font (§17.3.1.29 `w:rPr` on the
3232
+ * `w:pPr`) under a model that reads faces: what an empty paragraph stands, and
3233
+ * what a spacing of more lines adds to a line with no text on it.
3234
+ *
3235
+ * @param pp The paragraph's properties.
3236
+ * @param options Render options (styles, fonts).
3237
+ * @param model The line model.
3238
+ * @returns Its ascent, descent and gap, in points.
3239
+ */
3240
+ function markLine(pp, options, model) {
3241
+ const mark = resolveRunProperties(pp.runProperties ?? {}, pp, options.styles);
3242
+ const family = mark.fontFamily.ascii;
3243
+ const { parsed } = runFontKeyAndParsed(options, family, mark.bold, mark.italic);
3244
+ return fontLeadingPt(parsed, mark.fontSizePt, model, family);
3245
+ }
3246
+ /** What a paragraph's lines are reckoned from, under the layout's model. */
3247
+ function paragraphLeading(pp, options) {
3248
+ const model = leadingOf(options);
3249
+ if (model === "flat") return FLAT_LEADING;
3250
+ const m = markLine(pp, options, model);
3251
+ return {
3252
+ model,
3253
+ markLinePt: m.ascentPt + m.descentPt + m.lineGapPt
3254
+ };
3255
+ }
2817
3256
  function layoutParagraphBlock(paragraph, options, fontResources, imageResources, contentWidth, lineWidths) {
2818
3257
  const cascaded = resolveParagraphProperties(paragraph.properties, options.styles);
2819
3258
  const baseResolved = options.gridLinePitchPt !== void 0 && cascaded.snapToGrid !== false ? {
@@ -2842,7 +3281,8 @@ function layoutParagraphBlock(paragraph, options, fontResources, imageResources,
2842
3281
  const tokens = tokenizeParagraph(paragraph, options, fontResources, imageResources, contentWidth, baseDir);
2843
3282
  const firstLineWidth = paragraphMaxWidth(resolved, contentWidth, true);
2844
3283
  const otherWidth = paragraphMaxWidth(resolved, contentWidth, false);
2845
- const runWrap = (part, firstWidth, widths) => wrap(part, firstWidth, otherWidth, resolved, options.hyphenator, options.layoutProfile ?? "ream", options.doNotExpandShiftReturn === true, widths);
3284
+ const leading = paragraphLeading(paragraph.properties, options);
3285
+ const runWrap = (part, firstWidth, widths) => wrap(part, firstWidth, otherWidth, resolved, options.hyphenator, options.layoutProfile ?? "ream", leading, options.doNotExpandShiftReturn === true, widths);
2846
3286
  const parts = splitAtColumnBreaks(tokens);
2847
3287
  const columnBreakLines = /* @__PURE__ */ new Set();
2848
3288
  let wrapped;
@@ -2858,12 +3298,13 @@ function layoutParagraphBlock(paragraph, options, fontResources, imageResources,
2858
3298
  })));
2859
3299
  }
2860
3300
  }
2861
- const lines = wrapped.length > 0 ? wrapped : [emptyLine(paragraph, options, resolved, Math.max(firstLineWidth, 0), fontResources)];
3301
+ const lines = wrapped.length > 0 ? wrapped : [emptyLine(paragraph, options, resolved, Math.max(firstLineWidth, 0))];
2862
3302
  let heightPt = 0;
2863
3303
  for (const line of lines) heightPt += computeLineHeight(line, resolved);
2864
3304
  const numbering = paragraph.properties.numbering;
2865
3305
  const breakIdx = paragraph.runs.findIndex((r) => r.pageBreak === true);
2866
- const leadingPageBreak = breakIdx >= 0 && paragraph.runs.slice(0, breakIdx + 1).every((r) => r.text === "" && r.inlineImage === void 0);
3306
+ const holds = (r) => r.text !== "" || r.inlineImage !== void 0 || r.math !== void 0;
3307
+ const leadingPageBreak = breakIdx >= 0 && paragraph.runs.slice(0, breakIdx + 1).every((r) => r.text === "" && r.inlineImage === void 0) && paragraph.runs.slice(breakIdx + 1).some(holds);
2867
3308
  const trailingPageBreak = paragraph.runs.some((r, i) => r.pageBreak === true && (!leadingPageBreak || i > breakIdx));
2868
3309
  return {
2869
3310
  kind: "paragraph",
@@ -3186,7 +3627,7 @@ function measureRunText(plan, text, face) {
3186
3627
  font: plan.font,
3187
3628
  ...plan.synthetic ? { synthetic: plan.synthetic } : {}
3188
3629
  };
3189
- const base = drawn.font.measure.textWidthPt(text, plan.fontSizePt) * (drawn.synthetic?.widthScale ?? 1);
3630
+ const base = drawn.font.measure.textWidthPt(text, plan.fontSizePt) * (drawn.synthetic?.widthScale ?? 1) * (plan.resolvedRun.widthScale ?? 1);
3190
3631
  const extra = plan.resolvedRun.letterSpacingPt;
3191
3632
  return extra === void 0 || extra === 0 ? base : base + extra * [...text].length;
3192
3633
  }
@@ -3634,7 +4075,7 @@ function splitToWidth(tok, widthPt) {
3634
4075
  }
3635
4076
  /** The fewest characters a mid-word break may leave on a line. */
3636
4077
  var MIN_SPLIT_CHARS = 6;
3637
- function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, resolved, profile, allowEmpty = false) {
4078
+ function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, resolved, leading, allowEmpty = false) {
3638
4079
  let st = start;
3639
4080
  let et = breakIdx;
3640
4081
  while (st < et && (entries[st].token === null || !isFirst && entries[st].token?.isSpace === true)) st++;
@@ -3662,10 +4103,12 @@ function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, reso
3662
4103
  let maxSize = 0;
3663
4104
  let mathAscent = 0;
3664
4105
  let mathDescent = 0;
3665
- const useMetric = profile !== "ream";
3666
- let metricAscent = 0;
3667
- let metricDescent = 0;
3668
- let metricLineGap = 0;
4106
+ const useMetric = leading.model !== "flat";
4107
+ let textAscent = 0;
4108
+ let textDescent = 0;
4109
+ let markerAscent = 0;
4110
+ let markerDescent = 0;
4111
+ let pictureHeight = 0;
3669
4112
  const prints = lineTokens.some((t) => !t.isSpace);
3670
4113
  for (const t of lineTokens) {
3671
4114
  contentWidth += t.widthPt;
@@ -3676,13 +4119,22 @@ function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, reso
3676
4119
  mathAscent = Math.max(mathAscent, t.ascentPt);
3677
4120
  mathDescent = Math.max(mathDescent, t.descentPt);
3678
4121
  } else if (useMetric && t.kind === "text") {
3679
- const m = fontLeadingPt(t.font.parsed, t.fontSizePt, profile);
3680
- if (m.ascentPt > metricAscent) metricAscent = m.ascentPt;
3681
- if (m.descentPt > metricDescent) metricDescent = m.descentPt;
3682
- if (m.lineGapPt > metricLineGap) metricLineGap = m.lineGapPt;
3683
- }
4122
+ const m = fontLeadingPt(t.font.parsed, t.fontSizePt, leading.model, wordFamilyOf(t));
4123
+ const above = m.ascentPt + m.lineGapPt;
4124
+ if (t.listMarker === true) {
4125
+ markerAscent = Math.max(markerAscent, above);
4126
+ markerDescent = Math.max(markerDescent, m.descentPt);
4127
+ } else {
4128
+ textAscent = Math.max(textAscent, above);
4129
+ textDescent = Math.max(textDescent, m.descentPt);
4130
+ }
4131
+ } else if (useMetric && t.kind === "image") pictureHeight = Math.max(pictureHeight, t.heightPt);
3684
4132
  }
3685
- const hasMetric = useMetric && metricAscent + metricDescent > 0;
4133
+ const hasText = textAscent + textDescent > 0;
4134
+ const ascent = Math.max(textAscent, markerAscent);
4135
+ const descent = hasText ? textDescent : markerDescent;
4136
+ const hasMetric = useMetric && ascent + descent + pictureHeight > 0;
4137
+ const textLine = ascent + descent > 0 ? ascent + descent : leading.markLinePt;
3686
4138
  return {
3687
4139
  tokens: lineTokens,
3688
4140
  contentWidthPt: contentWidth,
@@ -3694,8 +4146,9 @@ function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, reso
3694
4146
  mathAscentPt: mathAscent,
3695
4147
  mathDescentPt: mathDescent,
3696
4148
  ...hasMetric ? {
3697
- metricHeightPt: metricAscent + metricDescent + metricLineGap,
3698
- metricDescentPt: metricDescent
4149
+ metricHeightPt: Math.max(ascent, pictureHeight) + descent,
4150
+ metricDescentPt: descent,
4151
+ metricTextHeightPt: textLine
3699
4152
  } : {}
3700
4153
  };
3701
4154
  }
@@ -3715,7 +4168,7 @@ function withLeadingTabWidths(tokens, resolved, firstLineWidth) {
3715
4168
  }
3716
4169
  return out;
3717
4170
  }
3718
- function wrap(tokens, firstLineWidth, otherWidth, resolved, hyphenator, profile, noExpandShiftReturn, lineWidths) {
4171
+ function wrap(tokens, firstLineWidth, otherWidth, resolved, hyphenator, profile, leading, noExpandShiftReturn, lineWidths) {
3719
4172
  if (tokens.length === 0) return [];
3720
4173
  const widths = lineWidths ?? [firstLineWidth, otherWidth];
3721
4174
  const entries = paragraphItemStream(withLeadingTabWidths(tokens, resolved, widths[0] ?? firstLineWidth), hyphenator, Math.max(0, ...widths));
@@ -3728,7 +4181,7 @@ function wrap(tokens, firstLineWidth, otherWidth, resolved, hyphenator, profile,
3728
4181
  let lineIdx = 0;
3729
4182
  for (const [i, breakIdx] of at.entries()) {
3730
4183
  const width = lineIdx < widths.length ? widths[lineIdx] : widths[widths.length - 1];
3731
- const line = lineFromRange(entries, start, breakIdx, width, isFirst, resolved, profile, i < at.length - 1);
4184
+ const line = lineFromRange(entries, start, breakIdx, width, isFirst, resolved, leading, i < at.length - 1);
3732
4185
  if (line) out.push(noExpandShiftReturn && entries[breakIdx]?.softBreak ? {
3733
4186
  ...line,
3734
4187
  noJustify: true
@@ -3748,14 +4201,55 @@ function wrap(tokens, firstLineWidth, otherWidth, resolved, hyphenator, profile,
3748
4201
  if (lines.length > 0) lines[lines.length - 1].isLastInParagraph = true;
3749
4202
  return lines;
3750
4203
  }
3751
- function fontLeadingPt(parsed, sizePt, profile) {
4204
+ /**
4205
+ * §17.3.2.26 — the family Word sets a token's characters in: East Asian text
4206
+ * in the run's `eastAsia` font, complex-script text (and a `w:rtl` run) in its
4207
+ * `cs` one, the rest in `ascii` — `hAnsi` above ASCII. The layout draws a run
4208
+ * with its ascii face and hands what that face lacks to another, so the name
4209
+ * the run gives for THESE characters is the one Word's line is looked up by:
4210
+ * a Chinese sentence in a Calibri run does not stand on Calibri's line.
4211
+ *
4212
+ * @param t The text token.
4213
+ * @returns The family's name, when the run gives one for that slot.
4214
+ */
4215
+ function wordFamilyOf(t) {
4216
+ const fonts = t.resolvedRun.fontFamily;
4217
+ if (t.resolvedRun.rtl) return fonts.cs;
4218
+ let high = false;
4219
+ for (const ch of t.text) {
4220
+ const cp = ch.codePointAt(0) ?? 0;
4221
+ if (cp <= 127) continue;
4222
+ high = true;
4223
+ const script = scriptForCodepoint(cp);
4224
+ if (script === "hebrew" || script === "arabic" || script === "thai") return fonts.cs;
4225
+ if (script === "kr" || script === "jp" || script === "sc") return fonts.eastAsia;
4226
+ }
4227
+ return high ? fonts.hAnsi ?? fonts.ascii : fonts.ascii;
4228
+ }
4229
+ /**
4230
+ * A face's single line at a size: its ascent, its descent, and the gap set
4231
+ * above the text. Under `'word'` a family Word's own line is known for (see
4232
+ * {@link WORD_LINE_METRICS}) takes that; otherwise the face in hand gives its
4233
+ * hhea line, or the OS/2 typo one when it sets USE_TYPO_METRICS.
4234
+ *
4235
+ * @param parsed The face the run is drawn with.
4236
+ * @param sizePt The run's size.
4237
+ * @param model The line model (never `'flat'`, which asks no face).
4238
+ * @param family The family the run names, when it names one.
4239
+ * @returns The three, in points; the descent as a magnitude.
4240
+ */
4241
+ function fontLeadingPt(parsed, sizePt, model, family) {
4242
+ const known = model === "word" && family !== void 0 ? WORD_LINE_METRICS.get(family.trim().toLowerCase()) : void 0;
4243
+ if (known) {
4244
+ const k = sizePt / 2048;
4245
+ return {
4246
+ ascentPt: known[0] * k,
4247
+ lineGapPt: known[1] * k,
4248
+ descentPt: known[2] * k
4249
+ };
4250
+ }
3752
4251
  const s = sizePt / parsed.unitsPerEm;
3753
4252
  const vm = parsed.vmetrics;
3754
- if (profile === "word") return {
3755
- ascentPt: vm.winAscent * s,
3756
- descentPt: vm.winDescent * s,
3757
- lineGapPt: 0
3758
- };
3759
4253
  const asc = vm.useTypoMetrics ? vm.typoAscent : parsed.ascender;
3760
4254
  const desc = vm.useTypoMetrics ? vm.typoDescent : parsed.descender;
3761
4255
  const gap = vm.useTypoMetrics ? vm.typoLineGap : parsed.lineGap;
@@ -3780,7 +4274,8 @@ function naturalLineHeight(line, p) {
3780
4274
  if (p.spacingLineRule === "atLeast" && p.spacingLine > 0) return Math.max(natural, p.spacingLine, mathNeed);
3781
4275
  const lineTwips = Math.round(p.spacingLine * 20);
3782
4276
  const multiple = lineTwips > 0 ? lineTwips / 240 : 1;
3783
- return Math.max(natural * multiple, mathNeed);
4277
+ const scaled = line.metricTextHeightPt === void 0 ? natural * multiple : natural + (multiple - 1) * line.metricTextHeightPt;
4278
+ return Math.max(scaled, mathNeed);
3784
4279
  }
3785
4280
  function lineDescent(line) {
3786
4281
  const fs = line.maxFontSizePt || 12;
@@ -3931,7 +4426,7 @@ function layoutTableBlock(table, options, fontResources, imageResources, content
3931
4426
  growRowsForMerges(rows);
3932
4427
  const heightPt = rows.reduce((s, r) => s + r.heightPt, 0);
3933
4428
  const totalWidthPt = columnWidthsPt.reduce((s, w) => s + w, 0);
3934
- const xOffsetPt = (table.properties.indentPt ?? 0) + tableXOffset(table.properties.alignment, contentWidth, totalWidthPt);
4429
+ const xOffsetPt = (table.properties.indentPt ?? 0) + tableXOffset(table.properties.alignment, contentWidth, totalWidthPt) - legacyTableOutdent(table, rows, options);
3935
4430
  return {
3936
4431
  kind: "table",
3937
4432
  ...table.properties.float ? { float: table.properties.float } : {},
@@ -3968,6 +4463,29 @@ function growRowsForMerges(rows) {
3968
4463
  };
3969
4464
  }
3970
4465
  }
4466
+ /**
4467
+ * [MS-DOCX] `compatibilityMode` — how far a table stands out past its indent
4468
+ * in a document laid out as Word 2010 and earlier lay it out. There the
4469
+ * indent places the first cell's TEXT, not the table's edge, so the table
4470
+ * stands out by that cell's left margin. Measured in Word for Mac: with
4471
+ * first-cell margins of 5.4, 15 and 20pt, the text stands at the margin in a
4472
+ * document of mode 14 and in one that states no mode, and 5.4, 15 and 20pt in
4473
+ * at mode 15. A 36pt `w:tblInd` puts the text 36pt in, not 41.4. A centred
4474
+ * table does not move. Only a Word document's table moves; a spreadsheet has
4475
+ * no such history.
4476
+ *
4477
+ * @param table The table.
4478
+ * @param rows Its laid-out rows, the first cell's margin resolved.
4479
+ * @param options Render options: whose document, and which mode.
4480
+ * @returns The points to move the table left by, 0 where it stays.
4481
+ */
4482
+ function legacyTableOutdent(table, rows, options) {
4483
+ if (options.typesetBy !== "word" || (options.compatibilityMode ?? 0) >= 15) return 0;
4484
+ if ((table.properties.alignment ?? "left") !== "left" || table.properties.float) return 0;
4485
+ const first = rows[0];
4486
+ if (!first || first.columnXOffsets[0] !== 0) return 0;
4487
+ return first.cells[0]?.padLeftPt ?? 0;
4488
+ }
3971
4489
  function tableXOffset(alignment, contentWidth, totalWidthPt) {
3972
4490
  const slack = contentWidth - totalWidthPt;
3973
4491
  if (slack <= 0 || !alignment || alignment === "left") return 0;
@@ -4126,7 +4644,7 @@ function measureSingleLine(paragraph, options, fontResources) {
4126
4644
  const { fontKey, synthetic } = runFontKeyAndParsed(options, resolved.fontFamily.ascii, resolved.bold, resolved.italic);
4127
4645
  const font = lookupFont(fontResources, fontKey);
4128
4646
  const fontSizePt = resolved.fontSizePt;
4129
- total += font.measure.textWidthPt(run.text, fontSizePt) * (synthetic?.widthScale ?? 1);
4647
+ total += font.measure.textWidthPt(run.text, fontSizePt) * (synthetic?.widthScale ?? 1) * (resolved.widthScale ?? 1);
4130
4648
  }
4131
4649
  return total;
4132
4650
  }
@@ -4179,6 +4697,13 @@ function layoutTableRow(row, rowIdx, rowCount, colCount, tableProps, columnWidth
4179
4697
  cursorX += widthPt;
4180
4698
  colIdx += span;
4181
4699
  }
4700
+ const bands = options.typesetBy === "word" ? withBorderBands(cells, rowIdx === rowCount - 1) : void 0;
4701
+ const bandSpread = bands && (bands.topPt > 0 || bands.bottomPt > 0) ? bands : void 0;
4702
+ if (bandSpread) cells.splice(0, cells.length, ...bandSpread.cells);
4703
+ const borderBands = bandSpread ? { borderBands: {
4704
+ topPt: bandSpread.topPt,
4705
+ bottomPt: bandSpread.bottomPt
4706
+ } } : {};
4182
4707
  let heightPt = 0;
4183
4708
  for (const c of cells) {
4184
4709
  if (c.mergeRole === "start") continue;
@@ -4192,7 +4717,8 @@ function layoutTableRow(row, rowIdx, rowCount, colCount, tableProps, columnWidth
4192
4717
  columnXOffsets,
4193
4718
  rowIdx,
4194
4719
  rowCount,
4195
- ...row.properties.cantSplit ? { cantSplit: true } : {}
4720
+ ...row.properties.cantSplit ? { cantSplit: true } : {},
4721
+ ...borderBands
4196
4722
  };
4197
4723
  } else if (row.properties.height && row.properties.heightRule === "atLeast") heightPt = Math.max(heightPt, row.properties.height);
4198
4724
  return {
@@ -4201,7 +4727,42 @@ function layoutTableRow(row, rowIdx, rowCount, colCount, tableProps, columnWidth
4201
4727
  columnXOffsets,
4202
4728
  rowIdx,
4203
4729
  rowCount,
4204
- ...row.properties.cantSplit ? { cantSplit: true } : {}
4730
+ ...row.properties.cantSplit ? { cantSplit: true } : {},
4731
+ ...borderBands
4732
+ };
4733
+ }
4734
+ /**
4735
+ * §17.4.38 — a table row as Word sets it: every horizontal border takes the
4736
+ * room it is wide, the edge above a row in that row and the table's bottom
4737
+ * edge in its last one, and the text stands clear of it. Measured in Word for
4738
+ * Mac, a row of 11pt Calibri with no cell margins stands 13.44pt apart from
4739
+ * the next with no borders, 13.92 under 0.5pt ones, 14.94 under 1.5pt and
4740
+ * 16.43 under 3pt; a one-row table between two paragraphs adds its top and
4741
+ * its bottom border whole, 3pt each at 3pt. LibreOffice does the same. The
4742
+ * room is the widest border along the edge, and it goes into the cells' top
4743
+ * and bottom padding, which is what places their text and sizes the row.
4744
+ *
4745
+ * @param cells The row's laid-out cells, their borders resolved.
4746
+ * @param lastRow Whether the row is the table's last, whose bottom border is
4747
+ * the table's own.
4748
+ * @returns The cells with the room in their padding, and the room taken.
4749
+ */
4750
+ function withBorderBands(cells, lastRow) {
4751
+ let topPt = 0;
4752
+ let bottomPt = 0;
4753
+ for (const c of cells) {
4754
+ topPt = Math.max(topPt, borderWeight(c.borders.top));
4755
+ if (lastRow) bottomPt = Math.max(bottomPt, borderWeight(c.borders.bottom));
4756
+ }
4757
+ return {
4758
+ cells: cells.map((c) => ({
4759
+ ...c,
4760
+ padTopPt: c.padTopPt + topPt,
4761
+ padBottomPt: c.padBottomPt + bottomPt,
4762
+ totalHeightPt: c.totalHeightPt + topPt + bottomPt
4763
+ })),
4764
+ topPt,
4765
+ bottomPt
4205
4766
  };
4206
4767
  }
4207
4768
  /**
@@ -4278,8 +4839,9 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
4278
4839
  let contentHeightPt = 0;
4279
4840
  let clipped = false;
4280
4841
  let pendingGapPt = 0;
4842
+ const collapse = collapsesSpacing(options);
4281
4843
  const openParagraph = (spacingBeforePt) => {
4282
- const gap = pendingGapPt + spacingBeforePt;
4844
+ const gap = collapse ? Math.max(pendingGapPt, spacingBeforePt) : pendingGapPt + spacingBeforePt;
4283
4845
  pendingGapPt = 0;
4284
4846
  if (gap <= 0) return;
4285
4847
  lineGaps.set(lines.length, (lineGaps.get(lines.length) ?? 0) + gap);
@@ -4316,7 +4878,6 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
4316
4878
  lines.push(line);
4317
4879
  contentHeightPt += computeLineHeight(line, block.resolved);
4318
4880
  }
4319
- contentHeightPt += block.spacingAfterPt;
4320
4881
  pendingGapPt = block.spacingAfterPt;
4321
4882
  } else if (el.kind === "shape" && !isOutOfFlowFloat(el.shape.float)) {
4322
4883
  const block = layoutShapeBlock(el.shape, options, fontResources, imageResources, innerWidth);
@@ -4492,7 +5053,7 @@ var PageAssembler = class PageAssembler {
4492
5053
  this.ctx = sectionCtxs[0];
4493
5054
  this.pageStartCtx = this.ctx;
4494
5055
  this.pageNumber = this.ctx.properties.pageNumberStart ?? 1;
4495
- this.cursorY = this.ctx.pageHeight - this.ctx.marginTop;
5056
+ this.cursorY = this.pageTopY();
4496
5057
  this.colStartY = this.cursorY;
4497
5058
  this.bandTopY = this.cursorY;
4498
5059
  }
@@ -4509,6 +5070,56 @@ var PageAssembler = class PageAssembler {
4509
5070
  current = [];
4510
5071
  pendingPageBreak = false;
4511
5072
  cursorY;
5073
+ /** Whether paragraph spacing stacks as Word stacks it (see {@link SpacingStack}). */
5074
+ collapseSpacing = false;
5075
+ /**
5076
+ * Where the last block's space after ended, on which page, and how much of
5077
+ * it a paragraph right below may share — nothing once the cursor has moved
5078
+ * on, to a new column or page or past a table.
5079
+ */
5080
+ spacingMark = void 0;
5081
+ /**
5082
+ * Whether the next block opens a section on a page of its own (the
5083
+ * document's first included), whose space before Word keeps at the top.
5084
+ */
5085
+ sectionStart = true;
5086
+ /**
5087
+ * Whether a paragraph that breaks to a new page by its own
5088
+ * `w:pageBreakBefore` keeps its space before there — Word 2010 and earlier
5089
+ * (compatibilityMode below 15, or none) keep it; Word 2013 drops it.
5090
+ */
5091
+ keepsBeforeAtOwnBreak = false;
5092
+ /**
5093
+ * §17.3.1.33 — the room to open above a block, where the spacing stacks as
5094
+ * Word stacks it (see {@link SpacingStack}): the space after the block right
5095
+ * above it shared, and nothing at all at the top of a page or column. Word
5096
+ * for Mac drops a 24pt space before there whether the paragraph ran over
5097
+ * onto the page, broke to it after a `w:br w:type="page"` or ran into the
5098
+ * next column, in every mode; it keeps it for the first paragraph of the
5099
+ * document and of a section, and — below mode 15 — for a paragraph that
5100
+ * breaks to the page by its own `w:pageBreakBefore`.
5101
+ *
5102
+ * @param beforePt A block's own space before.
5103
+ * @param opensSection Whether the block is the first of a section on a page of its own.
5104
+ * @param ownBreak Whether the block broke to this page by its own `w:pageBreakBefore`.
5105
+ * @returns The room to open above it.
5106
+ */
5107
+ openBefore(beforePt, opensSection = false, ownBreak = false) {
5108
+ const mark = this.spacingMark;
5109
+ this.spacingMark = void 0;
5110
+ if (!this.collapseSpacing) return beforePt;
5111
+ if (!this.colHasContent()) return opensSection || ownBreak && this.keepsBeforeAtOwnBreak ? beforePt : 0;
5112
+ if (!mark || mark.page !== this.current || mark.y !== this.cursorY) return beforePt;
5113
+ return Math.max(0, beforePt - mark.shared);
5114
+ }
5115
+ /** @param sharedPt The space after the block just placed, as far as a paragraph below may share it. */
5116
+ closeAfter(sharedPt) {
5117
+ this.spacingMark = {
5118
+ y: this.cursorY,
5119
+ page: this.current,
5120
+ shared: sharedPt
5121
+ };
5122
+ }
4512
5123
  /**
4513
5124
  * §17.6.4 multi-column flow: content fills column after column before the page
4514
5125
  * flushes. `colStartLen` marks where the current column's items begin in
@@ -4557,6 +5168,16 @@ var PageAssembler = class PageAssembler {
4557
5168
  * all and eleven controls wider than its page.
4558
5169
  */
4559
5170
  pageHasContent = () => this.current.length > 0 || this.floatsBehind.length > 0 || this.floatsFront.length > 0;
5171
+ /**
5172
+ * §17.10 — the body's margins on the page being laid out: below the header
5173
+ * band that page shows and above its footer band (see `bandMargins`).
5174
+ */
5175
+ pageMargins = () => this.ctx.bandMargins?.[bandForPage(this.pageInSection, this.globalPageIdx, this.ctx.titlePg, this.ctx.evenAndOddHeaders)] ?? {
5176
+ top: this.ctx.marginTop,
5177
+ bottom: this.ctx.marginBottom
5178
+ };
5179
+ /** Where the body of the page being laid out begins, y-up. */
5180
+ pageTopY = () => this.ctx.pageHeight - this.pageMargins().top;
4560
5181
  /** Overflow step: next column on this page, or a fresh page after the last. */
4561
5182
  advanceColumn = () => {
4562
5183
  if (this.ctx.columns && this.colIdx + 1 < this.ctx.columns.length) {
@@ -4652,7 +5273,7 @@ var PageAssembler = class PageAssembler {
4652
5273
  */
4653
5274
  beginBalancedBand = (bandHeightPt) => {
4654
5275
  const colCount = this.ctx.columns?.length ?? 1;
4655
- const room = this.cursorY - (this.ctx.marginBottom + this.noteReserve);
5276
+ const room = this.cursorY - (this.pageMargins().bottom + this.noteReserve);
4656
5277
  const share = colCount > 1 ? balancedColumnHeight(bandHeightPt, colCount, room) : 0;
4657
5278
  this.balanceHeightPt = share;
4658
5279
  this.balanceBottomY = share > 0 ? this.cursorY - share : void 0;
@@ -4737,7 +5358,7 @@ var PageAssembler = class PageAssembler {
4737
5358
  * margin (plus any footnote reserve); inside a BALANCED band it is the line
4738
5359
  * the band's share of the height reaches, so the columns come out even.
4739
5360
  */
4740
- bottomLimit = () => Math.max(this.ctx.marginBottom + this.noteReserve, this.balanceBottomY ?? Number.NEGATIVE_INFINITY);
5361
+ bottomLimit = () => Math.max(this.pageMargins().bottom + this.noteReserve, this.balanceBottomY ?? Number.NEGATIVE_INFINITY);
4741
5362
  /**
4742
5363
  * §17.6.4 — a `continuous` section of several columns whose content fits the
4743
5364
  * page is BALANCED: Word evens the columns out rather than filling the first
@@ -4772,8 +5393,8 @@ var PageAssembler = class PageAssembler {
4772
5393
  xOffset: 0
4773
5394
  };
4774
5395
  const cl = this.colLeft();
4775
- const leftRoom = e.x0 - FLOAT_TEXT_GAP - cl;
4776
- const rightRoom = cl + full - (e.x1 + FLOAT_TEXT_GAP);
5396
+ const leftRoom = e.x0 - cl;
5397
+ const rightRoom = cl + full - e.x1;
4777
5398
  if (Math.max(leftRoom, rightRoom) < MIN_WRAP_WIDTH) return {
4778
5399
  width: full,
4779
5400
  xOffset: 0,
@@ -4806,19 +5427,22 @@ var PageAssembler = class PageAssembler {
4806
5427
  if (this.exclusions.length === 0 || block.lines.length === 0) return void 0;
4807
5428
  const h0 = computeLineHeight(block.lines[0], block.resolved);
4808
5429
  if (h0 <= 0) return void 0;
5430
+ const p = block.resolved;
5431
+ const full = this.colWidth();
5432
+ const hold = (width, xOffset, first) => Math.max(1, (xOffset > 0 ? width - p.indentRight : Math.min(width, full - p.indentRight)) - p.indentLeft - (first ? p.indentFirstLine : 0));
4809
5433
  const widths = [];
4810
5434
  let narrowed = false;
4811
5435
  let y = startY;
4812
5436
  for (let i = 0; i < 200; i++) {
4813
5437
  const g = this.lineGeometryAt(y, h0);
4814
5438
  if (g.blockedUntilYUp !== void 0) {
4815
- widths.push(this.colWidth());
5439
+ widths.push(hold(full, 0, widths.length === 0));
4816
5440
  y = g.blockedUntilYUp - h0;
4817
5441
  if (y < this.bottomLimit()) break;
4818
5442
  continue;
4819
5443
  }
4820
- widths.push(g.width);
4821
- if (g.pair) widths.push(g.pair.width);
5444
+ widths.push(hold(g.width, g.xOffset, widths.length === 0));
5445
+ if (g.pair) widths.push(hold(g.pair.width, g.pair.xOffset, false));
4822
5446
  if (g.width < this.colWidth()) narrowed = true;
4823
5447
  else if (narrowed) break;
4824
5448
  y -= h0;
@@ -4850,13 +5474,43 @@ var PageAssembler = class PageAssembler {
4850
5474
  return out;
4851
5475
  };
4852
5476
  /**
5477
+ * The first of a paragraph's lines, from `from` on, that the current column
5478
+ * no longer holds: each line is counted with the notes it brings onto the
5479
+ * page, the way the line loop places it, and an explicit column break ends
5480
+ * the column where it stands.
5481
+ *
5482
+ * @param pb The paragraph being placed.
5483
+ * @param from The first line not yet placed.
5484
+ * @returns The index of the first line that does not stay in this column;
5485
+ * `pb.lines.length` when every one does.
5486
+ */
5487
+ firstLineOverflowing = (pb, from) => {
5488
+ let y = this.cursorY;
5489
+ let reserve = 0;
5490
+ let notesOnPage = this.pageNotes.length > 0;
5491
+ const counted = /* @__PURE__ */ new Set();
5492
+ for (let i = from; i < pb.lines.length; i++) {
5493
+ if (i > from && pb.columnBreakLines?.has(i)) return i;
5494
+ const line = pb.lines[i];
5495
+ const h = computeLineHeight(line, pb.resolved);
5496
+ const notes = this.lineFootnotes(line).filter((x) => !counted.has(x.id));
5497
+ const added = notes.reduce((sum, x) => sum + x.heightPt, 0) + (!notesOnPage && notes.length > 0 ? FOOTNOTE_SEPARATOR_HEIGHT : 0);
5498
+ if (y - h < this.bottomLimit() + reserve + added) return i;
5499
+ y -= h;
5500
+ reserve += added;
5501
+ if (notes.length > 0) notesOnPage = true;
5502
+ for (const x of notes) counted.add(x.id);
5503
+ }
5504
+ return pb.lines.length;
5505
+ };
5506
+ /**
4853
5507
  * The notes band for the flushing page: separator rule + each note's blocks
4854
5508
  * stacked inside the reserved area. Tagged: each note is a Note→P element.
4855
5509
  */
4856
5510
  renderNotesBand = () => {
4857
5511
  if (this.pageNotes.length === 0) return [];
4858
5512
  const out = [];
4859
- const top = this.ctx.marginBottom + this.noteReserve;
5513
+ const top = this.pageMargins().bottom + this.noteReserve;
4860
5514
  out.push({
4861
5515
  type: "fill",
4862
5516
  x: pt(this.ctx.marginLeft),
@@ -4872,7 +5526,7 @@ var PageAssembler = class PageAssembler {
4872
5526
  const noteNode = this.builder.create("Note", this.builder.root);
4873
5527
  structId = this.builder.create("P", noteNode).id;
4874
5528
  }
4875
- out.push(...drawBlocksSequentially(note.blocks, this.ctx.marginLeft, cursor, this.ctx.pageHeight, this.ctx.contentWidth, structId));
5529
+ out.push(...drawBlocksSequentially(note.blocks, this.ctx.marginLeft, cursor, this.ctx.pageHeight, this.ctx.contentWidth, structId, void 0, this.collapseSpacing));
4876
5530
  cursor -= note.heightPt;
4877
5531
  }
4878
5532
  return out;
@@ -4895,35 +5549,38 @@ var PageAssembler = class PageAssembler {
4895
5549
  const band = bandForPage(this.pageInSection, this.globalPageIdx, this.ctx.titlePg, this.ctx.evenAndOddHeaders);
4896
5550
  const header = pickBand(this.ctx.headerSet, band);
4897
5551
  const footer = pickBand(this.ctx.footerSet, band);
5552
+ const pageText = pageNumberText(this.pageNumber, this.ctx.properties.pageNumberFormat);
4898
5553
  if (header.renderDynamic) this.dynBands.push({
4899
5554
  pageIdx: this.pages.length,
4900
- pageNumber: this.pageNumber,
5555
+ pageText,
4901
5556
  position: "header",
4902
5557
  render: header.renderDynamic
4903
5558
  });
4904
5559
  if (footer.renderDynamic) this.dynBands.push({
4905
5560
  pageIdx: this.pages.length,
4906
- pageNumber: this.pageNumber,
5561
+ pageText,
4907
5562
  position: "footer",
4908
5563
  render: footer.renderDynamic
4909
5564
  });
5565
+ const sheet = this.ctx.sheet;
5566
+ const onSheet = (items) => sheet ? items.map((item) => turnOntoSheet(item, sheet.width)) : items;
4910
5567
  this.pages.push({
4911
5568
  commands: [
4912
- ...this.pageBackgroundItems(),
4913
- ...this.pageBorderItems(),
4914
- ...this.columnSeparatorItems(),
5569
+ ...onSheet(this.pageBackgroundItems()),
5570
+ ...onSheet(this.pageBorderItems()),
5571
+ ...onSheet(this.columnSeparatorItems()),
4915
5572
  ...header.commands,
4916
- ...PageAssembler.byZ(this.floatsBehind).map((item) => ({
5573
+ ...onSheet(PageAssembler.byZ(this.floatsBehind).map((item) => ({
4917
5574
  ...item,
4918
5575
  behind: true
4919
- })),
4920
- ...this.current,
4921
- ...PageAssembler.byZ(this.floatsFront),
4922
- ...this.renderNotesBand(),
5576
+ }))),
5577
+ ...onSheet(this.current),
5578
+ ...onSheet(PageAssembler.byZ(this.floatsFront)),
5579
+ ...onSheet(this.renderNotesBand()),
4923
5580
  ...footer.commands
4924
5581
  ],
4925
- width: pt(this.ctx.pageWidth),
4926
- height: pt(this.ctx.pageHeight)
5582
+ width: pt(sheet?.width ?? this.ctx.pageWidth),
5583
+ height: pt(sheet?.height ?? this.ctx.pageHeight)
4927
5584
  });
4928
5585
  this.current = [];
4929
5586
  this.floatsBehind = [];
@@ -4937,7 +5594,7 @@ var PageAssembler = class PageAssembler {
4937
5594
  this.globalPageIdx++;
4938
5595
  this.pageNumber++;
4939
5596
  if ((this.ctx.properties.lineNumbering?.restart ?? "newPage") === "newPage") this.lineNumber = 0;
4940
- this.cursorY = this.ctx.pageHeight - this.ctx.marginTop;
5597
+ this.cursorY = this.pageTopY();
4941
5598
  this.colStartY = this.cursorY;
4942
5599
  this.bandTopY = this.cursorY;
4943
5600
  this.balanceBottomY = void 0;
@@ -5126,32 +5783,80 @@ var PageAssembler = class PageAssembler {
5126
5783
  this.noteReserve = 0;
5127
5784
  this.colIdx = 0;
5128
5785
  this.colStartLen = 0;
5129
- this.cursorY = this.ctx.pageHeight - this.ctx.marginTop;
5786
+ this.cursorY = this.pageTopY();
5130
5787
  this.colStartY = this.cursorY;
5131
5788
  this.bandTopY = this.cursorY;
5132
5789
  this.balanceBottomY = void 0;
5133
5790
  this.balanceHeightPt = 0;
5134
5791
  };
5135
5792
  };
5136
- function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", notes, bookmarkPositions, reflowParagraph, backgroundImage) {
5793
+ /**
5794
+ * §17.3.1.14/15 — the height a paragraph needs in one column: all of its lines
5795
+ * where it keeps them together, and where it keeps with the next, the whole of
5796
+ * every paragraph in the chain it heads and as much of the block after as has
5797
+ * to stand beside it — a paragraph's first line, or its first two (all of a
5798
+ * short one) where its own widow control would not leave one alone; a table's
5799
+ * first row; a picture. The space between them counts, the chain's own space
5800
+ * before does not. A chain is decided at its head: one that follows a
5801
+ * paragraph kept with it moved with that paragraph or not at all, and asks
5802
+ * only whether its own lines stay together.
5803
+ *
5804
+ * @param blocks The section's blocks.
5805
+ * @param from The paragraph asked about.
5806
+ * @param end The section's end: nothing is kept with what lies past it.
5807
+ * @returns The height, or undefined where the paragraph asks for neither.
5808
+ */
5809
+ function keptTogetherHeight(blocks, from, end, collapse = false) {
5810
+ const first = blocks[from];
5811
+ if (first?.kind !== "paragraph") return void 0;
5812
+ const before = blocks[from - 1];
5813
+ const inChain = before?.kind === "paragraph" && before.resolved.keepNext && !before.pageBreakAfter && !first.resolved.pageBreakBefore && !first.pageBreakBefore;
5814
+ const keepsNext = first.resolved.keepNext && !inChain;
5815
+ if (!keepsNext && !first.resolved.keepLines) return void 0;
5816
+ let height = first.heightPt;
5817
+ let current = first;
5818
+ for (let i = from + 1; keepsNext && current.resolved.keepNext && !current.pageBreakAfter; i++) {
5819
+ const next = i < end ? blocks[i] : void 0;
5820
+ if (next?.kind === "paragraph") {
5821
+ if (next.resolved.pageBreakBefore || next.pageBreakBefore) break;
5822
+ height += collapse ? Math.max(current.spacingAfterPt, next.spacingBeforePt) : current.spacingAfterPt + next.spacingBeforePt;
5823
+ if (next.resolved.keepNext) {
5824
+ height += next.heightPt;
5825
+ current = next;
5826
+ continue;
5827
+ }
5828
+ const count = next.lines.length;
5829
+ const stays = next.resolved.keepLines ? count : next.resolved.widowControl ? count <= 3 ? count : 2 : 1;
5830
+ for (const line of next.lines.slice(0, stays)) height += computeLineHeight(line, next.resolved);
5831
+ } else if (next?.kind === "table" && !next.float) height += current.spacingAfterPt + (next.rows[0]?.heightPt ?? 0);
5832
+ else if (next?.kind === "image" && !next.float) height += current.spacingAfterPt + next.heightPt;
5833
+ break;
5834
+ }
5835
+ return height;
5836
+ }
5837
+ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", notes, bookmarkPositions, reflowParagraph, backgroundImage, typesetByWord = false, compatibilityMode) {
5137
5838
  if (sectionCtxs.length === 0) return [];
5138
5839
  const asm = new PageAssembler(sectionCtxs, builder, notes, bookmarkPositions, backgroundImage);
5840
+ asm.collapseSpacing = typesetByWord;
5841
+ asm.keepsBeforeAtOwnBreak = (compatibilityMode ?? 0) < 15;
5139
5842
  const balanceIfEndsContinuous = (fromBlock) => {
5140
5843
  const here = asm.ctx;
5141
5844
  const next = sectionCtxs[asm.secIdx + 1];
5142
5845
  if ((here.columns?.length ?? 1) < 2) return;
5143
5846
  if (!next?.continuous) return;
5144
5847
  if (next.pageWidth !== here.pageWidth || next.pageHeight !== here.pageHeight) return;
5848
+ if (next.sheet === void 0 !== (here.sheet === void 0)) return;
5145
5849
  const end = Math.min(here.endIndex, blocks.length);
5146
- asm.beginBalancedBand(blocksHeight(blocks.slice(fromBlock, end)));
5850
+ asm.beginBalancedBand(blocksHeight(blocks.slice(fromBlock, end), typesetByWord));
5147
5851
  };
5148
5852
  balanceIfEndsContinuous(0);
5853
+ let brokenFor;
5149
5854
  for (let blockIdx = 0; blockIdx < blocks.length; blockIdx++) {
5150
5855
  while (asm.secIdx < sectionCtxs.length - 1 && blockIdx >= asm.ctx.endIndex) {
5151
5856
  const next = sectionCtxs[asm.secIdx + 1];
5152
- if (next.continuous && next.pageWidth === asm.ctx.pageWidth && next.pageHeight === asm.ctx.pageHeight) {
5857
+ if (next.continuous && next.pageWidth === asm.ctx.pageWidth && next.pageHeight === asm.ctx.pageHeight && next.sheet === void 0 === (asm.ctx.sheet === void 0)) {
5153
5858
  const end = Math.min(next.endIndex, blocks.length);
5154
- asm.startBandSection(next, blocksHeight(blocks.slice(blockIdx, end)));
5859
+ asm.startBandSection(next, blocksHeight(blocks.slice(blockIdx, end), typesetByWord));
5155
5860
  continue;
5156
5861
  }
5157
5862
  if (asm.pageHasContent() || asm.pageInSection === 0) asm.flushPage(true);
@@ -5160,7 +5865,7 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
5160
5865
  const wants = opening?.sectionStart;
5161
5866
  if (wants === "oddPage" || wants === "evenPage") {
5162
5867
  if ((opening?.pageNumberStart ?? asm.pageNumber) % 2 === 1 !== (wants === "oddPage")) {
5163
- asm.cursorY = asm.ctx.pageHeight - asm.ctx.marginTop;
5868
+ asm.cursorY = asm.pageTopY();
5164
5869
  asm.flushPage(true);
5165
5870
  }
5166
5871
  }
@@ -5168,18 +5873,43 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
5168
5873
  asm.ctx = sectionCtxs[asm.secIdx];
5169
5874
  asm.restartPageNumbers(asm.ctx);
5170
5875
  asm.pageInSection = 0;
5171
- asm.cursorY = asm.ctx.pageHeight - asm.ctx.marginTop;
5876
+ asm.cursorY = asm.pageTopY();
5877
+ asm.sectionStart = true;
5172
5878
  balanceIfEndsContinuous(blockIdx);
5173
5879
  }
5174
5880
  const block = blocks[blockIdx];
5881
+ const opensSection = asm.sectionStart;
5882
+ const anchored = "float" in block && isOutOfFlowFloat(block.float);
5883
+ if (!anchored) asm.sectionStart = false;
5175
5884
  if (asm.pendingPageBreak) {
5176
5885
  asm.pendingPageBreak = false;
5177
5886
  asm.flushPage(true);
5178
5887
  }
5888
+ if (typesetByWord && anchored && asm.pageHasContent() && brokenFor === void 0) {
5889
+ let owner = blockIdx + 1;
5890
+ while (owner < blocks.length) {
5891
+ const b = blocks[owner];
5892
+ if (!("float" in b && isOutOfFlowFloat(b.float))) break;
5893
+ owner++;
5894
+ }
5895
+ const para = blocks[owner];
5896
+ if (para?.kind === "paragraph" && para.resolved.pageBreakBefore) {
5897
+ asm.flushPage();
5898
+ brokenFor = owner;
5899
+ }
5900
+ }
5179
5901
  if (builder && !(block.kind === "paragraph" && block.list)) asm.listStack.length = 0;
5180
5902
  if (block.kind === "paragraph") {
5181
- if ((block.resolved.pageBreakBefore || block.pageBreakBefore) && asm.pageHasContent()) asm.flushPage();
5182
- asm.cursorY -= block.spacingBeforePt;
5903
+ const brokeAhead = brokenFor === blockIdx;
5904
+ brokenFor = void 0;
5905
+ const ownBreak = block.resolved.pageBreakBefore && (brokeAhead || asm.pageHasContent());
5906
+ if (!brokeAhead && (block.resolved.pageBreakBefore || block.pageBreakBefore) && asm.pageHasContent()) asm.flushPage();
5907
+ const columnInUse = asm.colHasContent();
5908
+ const opensDocument = opensSection && asm.pages.length === 0 && asm.secIdx === 0;
5909
+ const beforePt = asm.collapseSpacing && opensDocument && block.resolved.spacingBeforeAuto !== void 0 ? 0 : block.spacingBeforePt;
5910
+ asm.cursorY -= asm.openBefore(beforePt, opensSection, ownBreak);
5911
+ const kept = keptTogetherHeight(blocks, blockIdx, asm.ctx.endIndex, typesetByWord);
5912
+ if (kept !== void 0 && columnInUse && asm.exclusions.length === 0 && kept > asm.cursorY - asm.bottomLimit()) asm.advanceColumn();
5183
5913
  let pb = block;
5184
5914
  const colW = asm.colWidth();
5185
5915
  if (reflowParagraph && pb.source && Math.abs(colW - asm.builtWidth()) > .5) pb = reflowParagraph(pb.source, colW);
@@ -5187,8 +5917,26 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
5187
5917
  const widths = asm.lineWidthsFor(pb, asm.cursorY);
5188
5918
  if (widths) pb = reflowParagraph(pb.source, colW, widths);
5189
5919
  }
5190
- const borderTopY = asm.cursorY;
5191
- const borderPage = asm.pages.length;
5920
+ let borderTopY = asm.cursorY;
5921
+ let borderPage = asm.pages.length;
5922
+ const keptColumnEnd = (from) => {
5923
+ const n = pb.lines.length;
5924
+ if (!pb.resolved.widowControl || n < 2 || asm.exclusions.length > 0) return void 0;
5925
+ let end = asm.firstLineOverflowing(pb, from);
5926
+ if (end >= n) return void 0;
5927
+ if (n - end === 1) end--;
5928
+ if (from === 0 && end === 1) end = 0;
5929
+ if (end <= from) return asm.colHasContent() ? from : from + 1;
5930
+ return end;
5931
+ };
5932
+ const advanceBefore = (lineIdx) => {
5933
+ asm.advanceColumn();
5934
+ if (lineIdx === 0) {
5935
+ borderTopY = asm.cursorY;
5936
+ borderPage = asm.pages.length;
5937
+ }
5938
+ };
5939
+ let columnEnd = keptColumnEnd(0);
5192
5940
  let structId;
5193
5941
  let markerLblId;
5194
5942
  const firstLine = pb.lines[0];
@@ -5205,13 +5953,21 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
5205
5953
  let firstLineOfBlock = true;
5206
5954
  let pairPending;
5207
5955
  for (const [lineIdx, line] of pb.lines.entries()) {
5208
- if (pb.columnBreakLines?.has(lineIdx) && asm.colHasContent()) asm.advanceColumn();
5956
+ if (pb.columnBreakLines?.has(lineIdx) && asm.colHasContent()) {
5957
+ advanceBefore(lineIdx);
5958
+ columnEnd = keptColumnEnd(lineIdx);
5959
+ }
5960
+ if (lineIdx === columnEnd && asm.colHasContent()) {
5961
+ advanceBefore(lineIdx);
5962
+ columnEnd = keptColumnEnd(lineIdx);
5963
+ }
5209
5964
  const h = computeLineHeight(line, pb.resolved);
5210
5965
  let newNotes = asm.lineFootnotes(line);
5211
5966
  const addedReserve = (sub) => sub.reduce((sum, x) => sum + x.heightPt, 0) + (asm.pageNotes.length === 0 && sub.length > 0 ? FOOTNOTE_SEPARATOR_HEIGHT : 0);
5212
5967
  if (asm.cursorY - h < asm.bottomLimit() + addedReserve(newNotes) && asm.colHasContent()) {
5213
- asm.advanceColumn();
5968
+ advanceBefore(lineIdx);
5214
5969
  newNotes = asm.lineFootnotes(line);
5970
+ columnEnd = keptColumnEnd(lineIdx);
5215
5971
  }
5216
5972
  if (newNotes.length > 0) {
5217
5973
  asm.noteReserve += addedReserve(newNotes);
@@ -5291,6 +6047,7 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
5291
6047
  }
5292
6048
  if (asm.pages.length === borderPage) asm.current.push(...paragraphDecoration(pb.resolved, asm.colLeft() + pb.resolved.indentLeft, asm.colLeft() + asm.colWidth() - pb.resolved.indentRight, borderTopY, asm.cursorY, asm.ctx.pageHeight));
5293
6049
  asm.cursorY -= pb.spacingAfterPt;
6050
+ asm.closeAfter(pb.spacingAfterPt);
5294
6051
  if (pb.pageBreakAfter) asm.pendingPageBreak = true;
5295
6052
  } else if (block.kind === "image") {
5296
6053
  const figId = builder ? createFigure(builder, block.altText, "Image") : void 0;
@@ -5321,12 +6078,13 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
5321
6078
  emitImageAt(fx, fy, PageAssembler.zSink(block.float.behind ? asm.floatsBehind : asm.floatsFront, block.float.zOrder));
5322
6079
  if (block.float.wrap !== "none") asm.excludeFloat(block.float, fx, fy, block.widthPt, block.heightPt);
5323
6080
  } else {
5324
- asm.cursorY -= block.spacingBeforePt;
6081
+ asm.cursorY -= asm.openBefore(block.spacingBeforePt, opensSection);
5325
6082
  if (asm.cursorY - block.heightPt < asm.bottomLimit() && asm.colHasContent()) asm.advanceColumn();
5326
6083
  asm.cursorY -= block.heightPt;
5327
6084
  const offset = alignmentOffset(block.resolvedAlignment, block.widthPt, asm.colWidth());
5328
6085
  emitImageAt(asm.colLeft() + offset, asm.cursorY + block.heightPt, asm.current);
5329
6086
  asm.cursorY -= block.spacingAfterPt;
6087
+ asm.closeAfter(sharedAfter(block));
5330
6088
  }
5331
6089
  } else if (block.kind === "shape") {
5332
6090
  const figId = builder ? createFigure(builder, block.altText, "Shape") : void 0;
@@ -5337,12 +6095,13 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
5337
6095
  emitShapeAt(fx, fy - block.heightPt, PageAssembler.zSink(block.float.behind ? asm.floatsBehind : asm.floatsFront, block.float.zOrder));
5338
6096
  if (block.float.wrap !== "none") asm.excludeFloat(block.float, fx, fy, block.widthPt, block.heightPt);
5339
6097
  } else {
5340
- asm.cursorY -= block.spacingBeforePt;
6098
+ asm.cursorY -= asm.openBefore(block.spacingBeforePt, opensSection);
5341
6099
  if (asm.cursorY - block.heightPt < asm.bottomLimit() && asm.colHasContent()) asm.advanceColumn();
5342
6100
  asm.cursorY -= block.heightPt;
5343
6101
  const offset = alignmentOffset(block.resolvedAlignment, block.widthPt, asm.colWidth());
5344
6102
  emitShapeAt(asm.colLeft() + offset, asm.cursorY, asm.current);
5345
6103
  asm.cursorY -= block.spacingAfterPt;
6104
+ asm.closeAfter(block.spacingAfterPt);
5346
6105
  }
5347
6106
  } else if (block.kind === "chart") {
5348
6107
  const figId = builder ? createFigure(builder, block.altText, block.figureRole ?? "Chart") : void 0;
@@ -5355,12 +6114,13 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
5355
6114
  emitChartAt(fx, fy - block.heightPt, PageAssembler.zSink(block.float.behind ? asm.floatsBehind : asm.floatsFront, block.float.zOrder));
5356
6115
  if (block.float.wrap !== "none") asm.excludeFloat(block.float, fx, fy, block.widthPt, block.heightPt);
5357
6116
  } else {
5358
- asm.cursorY -= block.spacingBeforePt;
6117
+ asm.cursorY -= asm.openBefore(block.spacingBeforePt, opensSection);
5359
6118
  if (asm.cursorY - block.heightPt < asm.bottomLimit() && asm.colHasContent()) asm.advanceColumn();
5360
6119
  asm.cursorY -= block.heightPt;
5361
6120
  const offset = alignmentOffset(block.resolvedAlignment, block.widthPt, asm.colWidth());
5362
6121
  emitChartAt(asm.colLeft() + offset, asm.cursorY, asm.current);
5363
6122
  asm.cursorY -= block.spacingAfterPt;
6123
+ asm.closeAfter(sharedAfter(block));
5364
6124
  }
5365
6125
  } else {
5366
6126
  const colCount = block.rows.reduce((max, r) => Math.max(max, r.cells.reduce((s, c) => Math.max(s, c.colStart + c.colSpan), 0)), 0);
@@ -5432,7 +6192,7 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
5432
6192
  asm.flushPage();
5433
6193
  if (asm.pages.length === 0) asm.flushPage(true);
5434
6194
  for (const d of asm.dynBands) {
5435
- const cmds = d.render(d.pageNumber, asm.pages.length);
6195
+ const cmds = d.render(d.pageText, asm.pages.length);
5436
6196
  const page = asm.pages[d.pageIdx];
5437
6197
  if (d.position === "header") page.commands.unshift(...cmds);
5438
6198
  else page.commands.push(...cmds);
@@ -5623,7 +6383,7 @@ function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellS
5623
6383
  });
5624
6384
  }
5625
6385
  }
5626
- emitCellBorders(out, cell, cellX, rowBottom, row.heightPt, pageHeight, row.rowIdx, row.rowCount, colCount);
6386
+ emitCellBorders(out, cell, cellX, rowBottom, row.heightPt, pageHeight, row.rowIdx, row.rowCount, colCount, row.borderBands);
5627
6387
  const diagDown = cell.borders.diagonalDown;
5628
6388
  const diagUp = cell.borders.diagonalUp;
5629
6389
  if ((diagDown || diagUp) && cell.mergeRole !== "middle" && cell.mergeRole !== "end") {
@@ -5840,17 +6600,19 @@ function splitRowIntoChunks(row, capacity, restCapacity = capacity) {
5840
6600
  return out.length > 0 ? out : [row];
5841
6601
  }
5842
6602
  var FILL_SEAM_PT = .07;
5843
- function emitCellBorders(out, cell, cellX, cellY, rowHeight, pageHeight, rowIdx, rowCount, colCount) {
6603
+ function emitCellBorders(out, cell, cellX, cellY, rowHeight, pageHeight, rowIdx, rowCount, colCount, bands) {
5844
6604
  const pushSide = (side, border) => {
5845
6605
  if (!border || border.style === "none") return;
5846
6606
  const sz = border.width ?? DEFAULT_BORDER_SIZE_EIGHTH * EIGHTH_PT;
6607
+ const lowered = side === "top" ? (bands?.topPt ?? 0) / 2 : 0;
6608
+ const raised = side === "bottom" ? (bands?.bottomPt ?? 0) / 2 : 0;
5847
6609
  out.push({
5848
6610
  type: "border",
5849
6611
  side,
5850
6612
  x: pt(cellX),
5851
- y: pt(pageHeight - cellY - rowHeight),
6613
+ y: pt(pageHeight - cellY - rowHeight + lowered),
5852
6614
  width: pt(cell.paintWidthPt ?? cell.widthPt),
5853
- height: pt(rowHeight),
6615
+ height: pt(rowHeight - lowered - raised),
5854
6616
  borderSizePt: sz,
5855
6617
  borderColorHex: border.colorHex ?? "000000",
5856
6618
  ...border.style !== "single" ? { borderStyle: border.style } : {}