reamkit 1.16.0 → 1.17.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 (65) hide show
  1. package/dist/esm/core/bidi/segments.js +1 -2
  2. package/dist/esm/core/converter/project.js +5 -1
  3. package/dist/esm/core/document-model/index.d.ts +1 -1
  4. package/dist/esm/core/document-model/types.d.ts +473 -7
  5. package/dist/esm/core/drawingml/chart-geometry.d.ts +8 -12
  6. package/dist/esm/core/drawingml/chart-geometry.js +58 -9
  7. package/dist/esm/core/drawingml/chart-parser.js +44 -5
  8. package/dist/esm/core/drawingml/colors.d.ts +1 -1
  9. package/dist/esm/core/drawingml/colors.js +27 -1
  10. package/dist/esm/core/drawingml/preset-geometry.js +233 -0
  11. package/dist/esm/core/drawingml/shape-render.d.ts +12 -0
  12. package/dist/esm/core/drawingml/shape-render.js +116 -3
  13. package/dist/esm/core/drawingml/theme-parser.d.ts +10 -0
  14. package/dist/esm/core/drawingml/theme-parser.js +19 -1
  15. package/dist/esm/core/font/measure.d.ts +5 -0
  16. package/dist/esm/core/font/measure.js +21 -1
  17. package/dist/esm/core/images.d.ts +2 -2
  18. package/dist/esm/core/images.js +181 -0
  19. package/dist/esm/core/ir/flow.d.ts +23 -1
  20. package/dist/esm/core/numbering/apply.d.ts +5 -3
  21. package/dist/esm/core/numbering/apply.js +42 -11
  22. package/dist/esm/core/numbering/index.js +2 -0
  23. package/dist/esm/core/numbering/state.d.ts +13 -1
  24. package/dist/esm/core/numbering/state.js +256 -15
  25. package/dist/esm/core/po-helpers.js +16 -1
  26. package/dist/esm/core/style-cascade/resolver.js +66 -11
  27. package/dist/esm/core/style-cascade/table.js +35 -11
  28. package/dist/esm/core/style-cascade/types.d.ts +42 -1
  29. package/dist/esm/core/style-cascade/types.js +4 -0
  30. package/dist/esm/core/vector.d.ts +17 -0
  31. package/dist/esm/excel/print-model.js +1 -1
  32. package/dist/esm/excel/sheet-to-flow.js +10 -8
  33. package/dist/esm/excel/worksheet-parser.js +1 -1
  34. package/dist/esm/html/html-writer.js +18 -4
  35. package/dist/esm/layout/page-doc.d.ts +62 -2
  36. package/dist/esm/layout/styled-layout.d.ts +44 -2
  37. package/dist/esm/layout/styled-layout.js +1870 -228
  38. package/dist/esm/pdf/shading.js +76 -6
  39. package/dist/esm/pdf/styled-page-emitter.js +217 -43
  40. package/dist/esm/pdf/vector-graphics.d.ts +28 -1
  41. package/dist/esm/pdf/vector-graphics.js +94 -11
  42. package/dist/esm/pptx/pptx-reader.js +1 -0
  43. package/dist/esm/pptx/slide-parser.d.ts +21 -4
  44. package/dist/esm/pptx/slide-parser.js +86 -23
  45. package/dist/esm/svg/svg-writer.js +1 -1
  46. package/dist/esm/word/document-parser.d.ts +96 -7
  47. package/dist/esm/word/document-parser.js +537 -116
  48. package/dist/esm/word/docx-reader.js +135 -43
  49. package/dist/esm/word/docx-writer.js +6 -1
  50. package/dist/esm/word/drawing-parser.d.ts +53 -5
  51. package/dist/esm/word/drawing-parser.js +1540 -54
  52. package/dist/esm/word/index.d.ts +4 -4
  53. package/dist/esm/word/numbering-parser.d.ts +2 -1
  54. package/dist/esm/word/numbering-parser.js +107 -21
  55. package/dist/esm/word/paragraph-properties.d.ts +11 -1
  56. package/dist/esm/word/paragraph-properties.js +219 -10
  57. package/dist/esm/word/run-properties.js +45 -1
  58. package/dist/esm/word/settings-parser.d.ts +24 -0
  59. package/dist/esm/word/settings-parser.js +31 -5
  60. package/dist/esm/word/shading.d.ts +11 -0
  61. package/dist/esm/word/shading.js +64 -0
  62. package/dist/esm/word/styles-parser.js +8 -2
  63. package/dist/esm/word/table-parser.d.ts +9 -1
  64. package/dist/esm/word/table-parser.js +182 -30
  65. package/package.json +1 -1
@@ -10,17 +10,18 @@ import { FORCED_BREAK, breakLines } from "../core/line-breaker/knuth-plass.js";
10
10
  import { greedyBreakLines } from "../core/line-breaker/greedy.js";
11
11
  import { cjkBreakBetween, splitCjkSegment } from "../core/line-breaker/cjk.js";
12
12
  import "../core/line-breaker/index.js";
13
- import { applyNumbering, applyNumberingToHeadersFooters } from "../core/numbering/apply.js";
14
13
  import { DEFAULT_RESOLVED_PARAGRAPH, DEFAULT_RESOLVED_RUN } from "../core/style-cascade/types.js";
15
14
  import { resolveParagraphProperties, resolveRunProperties } from "../core/style-cascade/resolver.js";
16
15
  import "../core/style-cascade/index.js";
16
+ import { applyNumbering, applyNumberingToHeadersFooters } from "../core/numbering/apply.js";
17
+ import "../core/numbering/index.js";
17
18
  import { PathBuilder, flipTransform } from "../core/vector.js";
18
19
  import { buildSparkline } from "../core/drawingml/sparkline-geometry.js";
19
20
  import { arcPoint, arcToBeziers } from "../core/arc-to-bezier.js";
20
21
  import { buildChartScene, legendSeriesName } from "../core/drawingml/chart-geometry.js";
21
22
  import { layoutMath, mathGlyphSegments, variantStyle } from "./math-layout.js";
22
23
  import { rectPath } from "../core/drawingml/preset-geometry.js";
23
- import { buildShapePaths, buildShapeTransform, buildStroke, gradientToSolid } from "../core/drawingml/shape-render.js";
24
+ import { buildShapePaths, buildShapeTransform, buildStroke, gradientToSolid, lineEndPaths } from "../core/drawingml/shape-render.js";
24
25
  import { StructTreeBuilder } from "../pdf/struct-tree.js";
25
26
  //#region src/layout/styled-layout.ts
26
27
  function parsePdfAProfile(pdfA) {
@@ -33,6 +34,14 @@ function parsePdfAProfile(pdfA) {
33
34
  version: part === 1 ? "1.4" : "1.7"
34
35
  };
35
36
  }
37
+ /** §17.6.4 — how heavy Word draws the rule between columns. */
38
+ var COLUMN_SEPARATOR_PT = .5;
39
+ /**
40
+ * How far a space may be squeezed below its natural width — the shrink the
41
+ * line breaker offers when it weighs a line, and therefore the shrink the
42
+ * emitter owes it back when it draws one.
43
+ */
44
+ var GLUE_SHRINK_RATIO = .3;
36
45
  var TWIP_TO_PT = 1 / 20;
37
46
  var EIGHTH_PT = 1 / 8;
38
47
  var DEFAULT_CELL_PADDING_TWIPS = 108;
@@ -40,6 +49,7 @@ var DEFAULT_BORDER_SIZE_EIGHTH = 4;
40
49
  var FOOTNOTE_RULE_PT = .75;
41
50
  var FOOTNOTE_RULE_GAP_ABOVE = 4;
42
51
  var FOOTNOTE_SEPARATOR_HEIGHT = 10;
52
+ var LINE_NUMBER_GAP_PT = 18;
43
53
  var FLOAT_TEXT_GAP = 6;
44
54
  var MIN_WRAP_WIDTH = 36;
45
55
  function isOutOfFlowFloat(f) {
@@ -257,21 +267,48 @@ function commentTailBlocks(comment, n, opts) {
257
267
  */
258
268
  function layoutStyledDocument(body, options) {
259
269
  const sectionList = resolveSectionList(body, options);
260
- const noteAssigned = assignNoteNumbers(applyNumbering(body, options.numbering), options.comments ? new Set(options.comments.keys()) : void 0);
270
+ const noteAssigned = assignNoteNumbers(applyNumbering(body, options.numbering, options.styles), options.comments ? new Set(options.comments.keys()) : void 0);
261
271
  const numberedBody = noteAssigned.body;
262
- const numberedHeadersFooters = applyNumberingToHeadersFooters(options.headersFooters, options.numbering);
272
+ const numberedHeadersFooters = applyNumberingToHeadersFooters(options.headersFooters, options.numbering, options.styles);
263
273
  const pdfaProfile = options.pdfA ? parsePdfAProfile(options.pdfA) : void 0;
264
274
  const tagged = options.tagged === true || options.pdfUA === true || (pdfaProfile?.tagged ?? false);
265
275
  const structBuilder = tagged ? new StructTreeBuilder() : void 0;
266
276
  const fontResources = collectFontResources(numberedBody, numberedHeadersFooters, options);
267
277
  const imageResources = collectImageResources(numberedBody, numberedHeadersFooters, options);
268
- const sectionCtxs = sectionList.map((s) => buildSectionContext(s, options, numberedHeadersFooters, fontResources));
278
+ const sectionCtxs = sectionList.map((s) => buildSectionContext(s, options, numberedHeadersFooters, fontResources, imageResources));
269
279
  let sectionIdx = 0;
280
+ const bodyFrames = frameGroups(numberedBody);
270
281
  const blocks = numberedBody.map((el, idx) => {
271
282
  while (sectionIdx < sectionCtxs.length - 1 && idx >= sectionCtxs[sectionIdx].endIndex) sectionIdx++;
272
283
  const ctx = sectionCtxs[sectionIdx];
273
- return layoutBodyElement(el, options, fontResources, imageResources, ctx.columns ? ctx.columns[0].widthPt : ctx.contentWidth, ctx.pageContentHeight);
284
+ const width = ctx.columns ? ctx.columns[0].widthPt : ctx.contentWidth;
285
+ const box = {
286
+ pageWidthPt: ctx.pageWidth,
287
+ pageHeightPt: ctx.pageHeight,
288
+ marginLeftPt: ctx.marginLeft,
289
+ marginRightPt: ctx.pageWidth - ctx.marginLeft - ctx.contentWidth,
290
+ marginTopPt: ctx.marginTop,
291
+ marginBottomPt: ctx.marginBottom
292
+ };
293
+ const group = bodyFrames.starts.get(idx);
294
+ if (group) return layoutFrameBlock(group, ctx.options ?? options, fontResources, imageResources, width);
295
+ if (bodyFrames.continued.has(idx)) return emptyBlock(options);
296
+ return layoutBodyElement(el, ctx.options ?? options, fontResources, imageResources, width, ctx.pageContentHeight, box);
274
297
  });
298
+ for (let i = 0; i < blocks.length; i++) {
299
+ const here = blocks[i];
300
+ const next = blocks[i + 1];
301
+ if (here?.kind !== "paragraph" || next?.kind !== "paragraph") continue;
302
+ if (here.source?.properties.styleId !== next.source?.properties.styleId) continue;
303
+ if (here.resolved.contextualSpacing) blocks[i] = {
304
+ ...here,
305
+ spacingAfterPt: 0
306
+ };
307
+ if (next.resolved.contextualSpacing) blocks[i + 1] = {
308
+ ...next,
309
+ spacingBeforePt: 0
310
+ };
311
+ }
275
312
  const noteBlockCache = /* @__PURE__ */ new Map();
276
313
  const notePlan = options.footnotes && noteAssigned.footnotes.size > 0 ? {
277
314
  numbers: noteAssigned.footnotes,
@@ -286,7 +323,7 @@ function layoutStyledDocument(body, options) {
286
323
  const n = noteAssigned.footnotes.get(id);
287
324
  let laid = void 0;
288
325
  if (content && n !== void 0 && !noteAssigned.deferredFootnotes.includes(id)) {
289
- const noteBlocks = substituteNoteNumber(content, n).map((el) => layoutBodyElement(el, options, fontResources, imageResources, sectionCtx.contentWidth, sectionCtx.pageContentHeight));
326
+ const noteBlocks = substituteNoteNumber(content, n).map((el) => layoutBodyElement(el, sectionCtx.options ?? options, fontResources, imageResources, sectionCtx.contentWidth, sectionCtx.pageContentHeight));
290
327
  laid = {
291
328
  blocks: noteBlocks,
292
329
  heightPt: noteBlocks.reduce((sum, b) => sum + (b.kind === "paragraph" ? b.spacingBeforePt + b.heightPt + b.spacingAfterPt : 0), 0)
@@ -314,7 +351,24 @@ function layoutStyledDocument(body, options) {
314
351
  n
315
352
  });
316
353
  }
317
- for (const note of tailNotes.sort((a, b) => a.n - b.n)) for (const el of substituteNoteNumber(note.content, note.n)) blocks.push(layoutBodyElement(el, options, fontResources, imageResources, lastCtx.contentWidth, lastCtx.pageContentHeight));
354
+ if (tailNotes.length > 0) blocks.push(layoutShapeBlock({
355
+ width: pt(FOOTNOTE_RULE_WIDTH),
356
+ height: pt(FOOTNOTE_RULE_PT),
357
+ geometry: {
358
+ kind: "preset",
359
+ preset: "rect",
360
+ adjust: /* @__PURE__ */ new Map()
361
+ },
362
+ fill: {
363
+ kind: "solid",
364
+ colorHex: "000000"
365
+ },
366
+ paragraphProperties: {
367
+ spacingBefore: pt(FOOTNOTE_SEPARATOR_HEIGHT),
368
+ spacingAfter: pt(FOOTNOTE_RULE_GAP_ABOVE)
369
+ }
370
+ }, options, fontResources, imageResources, lastCtx.contentWidth));
371
+ for (const note of tailNotes.sort((a, b) => a.n - b.n)) for (const el of substituteNoteNumber(note.content, note.n)) blocks.push(layoutBodyElement(el, lastCtx.options ?? options, fontResources, imageResources, lastCtx.contentWidth, lastCtx.pageContentHeight));
318
372
  const commentNums = noteAssigned.comments;
319
373
  const threadDepth = (id) => {
320
374
  let depth = 0;
@@ -346,7 +400,7 @@ function layoutStyledDocument(body, options) {
346
400
  const commentNotes = options.commentAnnotations && options.comments ? buildCommentNotes(noteAssigned.comments, options.comments) : void 0;
347
401
  const reflowParagraph = (paragraph, width, widths) => layoutParagraphBlock(paragraph, options, fontResources, imageResources, width, widths);
348
402
  return {
349
- pages: paginateSections(blocks, sectionCtxs, structBuilder, options.language ?? "en-US", notePlan, bookmarks, reflowParagraph),
403
+ pages: paginateSections(blocks, sectionCtxs, structBuilder, options.language ?? "en-US", notePlan, bookmarks, reflowParagraph, options.pageBackgroundFill?.imageResource !== void 0 ? imageResources.get(options.pageBackgroundFill.imageResource)?.resourceName : void 0),
350
404
  resources: options.resources ?? new ResourceStore(),
351
405
  fontResources,
352
406
  imageResources,
@@ -367,14 +421,16 @@ function resolvePageDimensions(options, section) {
367
421
  const sectionRight = section?.margins?.right !== void 0 ? section.margins.right : void 0;
368
422
  const sectionTop = section?.margins?.top !== void 0 ? section.margins.top : void 0;
369
423
  const sectionBottom = section?.margins?.bottom !== void 0 ? section.margins.bottom : void 0;
424
+ const gutter = section?.margins?.gutter ?? 0;
425
+ const atTop = options.gutterAtTop === true;
370
426
  const headerOffsetPt = section?.margins?.header ?? 720 * TWIP_TO_PT;
371
427
  const footerOffsetPt = section?.margins?.footer ?? 720 * TWIP_TO_PT;
372
428
  return {
373
429
  pageWidth: options.pageWidth ?? sectionPageWidth ?? 595,
374
430
  pageHeight: options.pageHeight ?? sectionPageHeight ?? 842,
375
- marginLeft: options.marginLeft ?? sectionLeft ?? 72,
431
+ marginLeft: (options.marginLeft ?? sectionLeft ?? 72) + (atTop ? 0 : gutter),
376
432
  marginRight: options.marginRight ?? sectionRight ?? 72,
377
- marginTop: options.marginTop ?? sectionTop ?? 72,
433
+ marginTop: (options.marginTop ?? sectionTop ?? 72) + (atTop ? gutter : 0),
378
434
  marginBottom: options.marginBottom ?? sectionBottom ?? 72,
379
435
  headerOffsetPt,
380
436
  footerOffsetPt
@@ -394,13 +450,32 @@ function resolveSectionList(body, options) {
394
450
  endIndex: body.length
395
451
  }];
396
452
  }
397
- function buildSectionContext(section, options, headersFooters, fontResources) {
453
+ function buildSectionContext(section, options, headersFooters, fontResources, imageResources) {
398
454
  const dims = resolvePageDimensions(options, section.properties);
399
455
  const contentWidth = dims.pageWidth - dims.marginLeft - dims.marginRight;
400
- const headerSet = layoutHeaderSet(section.properties, headersFooters, options, fontResources, contentWidth, dims.marginLeft, dims.pageHeight, dims.headerOffsetPt);
401
- const footerSet = layoutFooterSet(section.properties, headersFooters, options, fontResources, contentWidth, dims.marginLeft, dims.pageHeight, dims.footerOffsetPt);
456
+ const secOptions = section.properties.gridLinePitchPt !== void 0 ? {
457
+ ...options,
458
+ gridLinePitchPt: section.properties.gridLinePitchPt
459
+ } : options;
460
+ const relativeBox = {
461
+ pageWidthPt: dims.pageWidth,
462
+ pageHeightPt: dims.pageHeight,
463
+ marginLeftPt: dims.marginLeft,
464
+ marginRightPt: dims.marginRight,
465
+ marginTopPt: dims.marginTop,
466
+ marginBottomPt: dims.marginBottom
467
+ };
468
+ const headerSet = layoutHeaderSet(section.properties, headersFooters, secOptions, fontResources, imageResources, contentWidth, dims.marginLeft, dims.pageHeight, dims.headerOffsetPt, {
469
+ top: dims.marginTop,
470
+ bottom: dims.marginBottom
471
+ }, relativeBox);
472
+ const footerSet = layoutFooterSet(section.properties, headersFooters, secOptions, fontResources, imageResources, contentWidth, dims.marginLeft, dims.pageHeight, dims.footerOffsetPt, {
473
+ top: dims.marginTop,
474
+ bottom: dims.marginBottom
475
+ }, relativeBox);
402
476
  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);
477
+ 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);
478
+ const headerBottom = headerHeight > 0 ? dims.headerOffsetPt + headerHeight : 0;
404
479
  const marginTop = Math.max(dims.marginTop, headerBottom);
405
480
  return {
406
481
  endIndex: section.endIndex,
@@ -416,7 +491,9 @@ function buildSectionContext(section, options, headersFooters, fontResources) {
416
491
  headerSet,
417
492
  footerSet,
418
493
  titlePg: section.properties.titlePg === true,
419
- evenAndOddHeaders: section.properties.evenAndOddHeaders === true
494
+ evenAndOddHeaders: section.properties.evenAndOddHeaders === true,
495
+ continuous: section.properties.sectionStart === "continuous",
496
+ options: secOptions
420
497
  };
421
498
  }
422
499
  function buildColumnGeometry(cols, contentWidth) {
@@ -451,7 +528,7 @@ function markPagination(cmds) {
451
528
  artifact: "pagination"
452
529
  }));
453
530
  }
454
- function layoutHeaderSet(section, headersFooters, options, fontResources, contentWidth, marginLeft, pageHeight, headerOffsetPt) {
531
+ function layoutHeaderSet(section, headersFooters, options, fontResources, imageResources, contentWidth, marginLeft, pageHeight, headerOffsetPt, bandMargins, relativeBox) {
455
532
  const band = (type) => {
456
533
  const ref = refByType(section.headers, type);
457
534
  if (!ref) return { commands: [] };
@@ -459,9 +536,9 @@ function layoutHeaderSet(section, headersFooters, options, fontResources, conten
459
536
  if (!content) return { commands: [] };
460
537
  let measured = 0;
461
538
  const render = (c) => {
462
- const blocks = laidOutBlocksFor(c, options, fontResources, contentWidth);
539
+ const blocks = laidOutBlocksFor(c, options, fontResources, contentWidth, imageResources, relativeBox);
463
540
  measured = blocksHeight(blocks);
464
- return markPagination(drawBlocksSequentially(blocks, marginLeft, pageHeight - headerOffsetPt, pageHeight));
541
+ return markPagination(drawBlocksSequentially(blocks, marginLeft, pageHeight - headerOffsetPt, pageHeight, contentWidth, void 0, bandMargins));
465
542
  };
466
543
  if (contentHasPageFields(content)) {
467
544
  render(substitutePageFields(content, 1, 1));
@@ -482,19 +559,37 @@ function layoutHeaderSet(section, headersFooters, options, fontResources, conten
482
559
  even: band("even")
483
560
  };
484
561
  }
562
+ /**
563
+ * §17.6.4 — the height each column of a BALANCED band is given: the band's
564
+ * share, but never more than the page has room for, and never so little that
565
+ * the band could not fit the columns at all.
566
+ *
567
+ * @param bandHeightPt The band's whole laid-out height.
568
+ * @param colCount How many columns it has to share.
569
+ * @param roomPt What the page has left below the band's top.
570
+ * @returns The per-column height, or 0 when the band should just fill.
571
+ */
572
+ function balancedColumnHeight(bandHeightPt, colCount, roomPt) {
573
+ if (bandHeightPt <= 0 || colCount < 2 || roomPt <= 0) return 0;
574
+ if (bandHeightPt > roomPt * colCount) return 0;
575
+ return Math.min(roomPt, bandHeightPt / colCount);
576
+ }
485
577
  /** Total laid-out height of a run of blocks, paragraph spacing included. */
486
578
  function blocksHeight(blocks) {
487
- return blocks.reduce((sum, b) => sum + (b.kind === "paragraph" ? b.spacingBeforePt + b.heightPt + b.spacingAfterPt : b.heightPt), 0);
579
+ return blocks.reduce((sum, b) => {
580
+ if (b.kind !== "paragraph" && b.kind !== "table" && isOutOfFlowFloat(b.float)) return sum;
581
+ return sum + (b.kind === "paragraph" ? b.spacingBeforePt + b.heightPt + b.spacingAfterPt : b.heightPt);
582
+ }, 0);
488
583
  }
489
- function layoutFooterSet(section, headersFooters, options, fontResources, contentWidth, marginLeft, pageHeight, footerOffsetPt) {
584
+ function layoutFooterSet(section, headersFooters, options, fontResources, imageResources, contentWidth, marginLeft, pageHeight, footerOffsetPt, bandMargins, relativeBox) {
490
585
  const band = (type) => {
491
586
  const ref = refByType(section.footers, type);
492
587
  if (!ref) return { commands: [] };
493
588
  const content = headersFooters.get(ref.relationshipId);
494
589
  if (!content) return { commands: [] };
495
590
  const render = (c) => {
496
- const blocks = laidOutBlocksFor(c, options, fontResources, contentWidth);
497
- return markPagination(drawBlocksSequentially(blocks, marginLeft, footerOffsetPt + blocks.reduce((sum, b) => sum + (b.kind === "paragraph" ? b.spacingBeforePt + b.heightPt + b.spacingAfterPt : b.heightPt), 0), pageHeight));
591
+ const blocks = laidOutBlocksFor(c, options, fontResources, contentWidth, imageResources, relativeBox);
592
+ return markPagination(drawBlocksSequentially(blocks, marginLeft, footerOffsetPt + blocksHeight(blocks), pageHeight, contentWidth, void 0, bandMargins));
498
593
  };
499
594
  if (contentHasPageFields(content)) return {
500
595
  commands: [],
@@ -530,7 +625,7 @@ function substitutePageFields(content, pageNumber, totalPages) {
530
625
  }
531
626
  function pickBand(set, band) {
532
627
  const has = (e) => e.commands.length > 0 || e.renderDynamic !== void 0;
533
- if (band === "first") return has(set.first) ? set.first : set.default;
628
+ if (band === "first") return set.first;
534
629
  if (band === "even") return has(set.even) ? set.even : set.default;
535
630
  return set.default;
536
631
  }
@@ -539,24 +634,236 @@ function bandForPage(pageInSection, globalPageIdx, titlePg, evenAndOddHeaders) {
539
634
  if (evenAndOddHeaders && (globalPageIdx + 1) % 2 === 0) return "even";
540
635
  return "default";
541
636
  }
542
- function laidOutBlocksFor(elements, options, fontResources, contentWidth) {
543
- return elements.map((el) => layoutBodyElement(el, options, fontResources, void 0, contentWidth));
637
+ /**
638
+ * §17.3.1.11 — the runs of consecutive paragraphs that carry the SAME frame.
639
+ * Word treats them as one floating text frame, so they lay out together in one
640
+ * box; the entries after the first become empty blocks, which keeps every
641
+ * caller's element indexing (and its section bounds) intact.
642
+ *
643
+ * @param elements The body elements, in order.
644
+ * @returns `starts`: the index each frame begins at → its paragraphs;
645
+ * `continued`: the indices swallowed by a frame that began earlier.
646
+ */
647
+ function frameGroups(elements) {
648
+ const starts = /* @__PURE__ */ new Map();
649
+ const continued = /* @__PURE__ */ new Set();
650
+ const key = (p) => p.properties.frame ? JSON.stringify(p.properties.frame) : void 0;
651
+ let i = 0;
652
+ while (i < elements.length) {
653
+ const el = elements[i];
654
+ const k = el.kind === "paragraph" ? key(el.paragraph) : void 0;
655
+ if (k === void 0) {
656
+ i++;
657
+ continue;
658
+ }
659
+ const members = [el.paragraph];
660
+ let j = i + 1;
661
+ while (j < elements.length) {
662
+ const next = elements[j];
663
+ if (next.kind !== "paragraph" || key(next.paragraph) !== k) break;
664
+ members.push(next.paragraph);
665
+ continued.add(j);
666
+ j++;
667
+ }
668
+ starts.set(i, members);
669
+ i = j;
670
+ }
671
+ return {
672
+ starts,
673
+ continued
674
+ };
675
+ }
676
+ /** A block that occupies no space at all — the tail of a frame group. */
677
+ function emptyBlock(options) {
678
+ return {
679
+ kind: "paragraph",
680
+ resolved: resolveParagraphProperties({}, options.styles),
681
+ lines: [],
682
+ heightPt: 0,
683
+ spacingBeforePt: 0,
684
+ spacingAfterPt: 0
685
+ };
686
+ }
687
+ /**
688
+ * A text frame as the floating box it is: the paragraphs inside it lay out at
689
+ * the frame's width, the box takes the height the frame states or the text
690
+ * needs, and the body flows past it as its `w:wrap` says.
691
+ *
692
+ * @param paragraphs The frame's paragraphs, in order.
693
+ * @param options Render options.
694
+ * @param fontResources Fonts by variant.
695
+ * @param imageResources Images by resource id.
696
+ * @param contentWidth The width available where the frame is anchored.
697
+ * @returns The laid-out frame.
698
+ */
699
+ function layoutFrameBlock(paragraphs, options, fontResources, imageResources, contentWidth) {
700
+ const frame = paragraphs[0].properties.frame;
701
+ const exact = frame.heightRule === "exact" && frame.heightPt !== void 0;
702
+ const raw = (widthPt, pin) => layoutShapeBlock(frameShape(frame, paragraphs, widthPt, pin), options, fontResources, imageResources, contentWidth);
703
+ const atLeast = !exact && frame.heightPt !== void 0 && frame.heightRule !== "auto" ? frame.heightPt : 0;
704
+ const build = (widthPt) => {
705
+ const laid = raw(widthPt, exact);
706
+ return laid.kind === "shape" && atLeast > laid.heightPt ? raw(widthPt, true) : laid;
707
+ };
708
+ if (frame.widthPt !== void 0) return build(frame.widthPt);
709
+ const measured = build(contentWidth);
710
+ const natural = measured.kind === "shape" ? Math.max(1, ...measured.textLines.map((l) => l.contentWidthPt)) : contentWidth;
711
+ return build(Math.min(contentWidth, natural));
712
+ }
713
+ var MAX_FIT_PASSES = 4;
714
+ function scaleTextBody(text, k) {
715
+ return {
716
+ ...text,
717
+ content: text.content.map((el) => el.kind === "paragraph" ? {
718
+ ...el,
719
+ paragraph: {
720
+ ...el.paragraph,
721
+ runs: el.paragraph.runs.map((run) => ({
722
+ ...run,
723
+ properties: {
724
+ ...run.properties,
725
+ fontSizePt: pt((run.properties.fontSizePt ?? 11) * k)
726
+ }
727
+ }))
728
+ }
729
+ } : el)
730
+ };
731
+ }
732
+ function frameShape(frame, paragraphs, widthPt, exact) {
733
+ const lead = paragraphs[0]?.properties;
734
+ const edge = lead?.borders?.top ?? lead?.borders?.left;
735
+ const vertical = lead?.textDirection === "btLr" ? "vert270" : lead?.textDirection === "tbRl" ? "vert" : void 0;
736
+ const painted = lead?.shading !== void 0 || edge !== void 0 && edge.style !== "none";
737
+ return {
738
+ width: pt(widthPt),
739
+ height: pt(exact ? frame.heightPt ?? 0 : 0),
740
+ geometry: painted ? {
741
+ kind: "preset",
742
+ preset: "rect",
743
+ adjust: /* @__PURE__ */ new Map()
744
+ } : {
745
+ kind: "custom",
746
+ custom: {
747
+ pathWidth: 0,
748
+ pathHeight: 0,
749
+ commands: []
750
+ }
751
+ },
752
+ fill: lead?.shading ? {
753
+ kind: "solid",
754
+ colorHex: lead.shading.colorHex
755
+ } : { kind: "none" },
756
+ ...edge && edge.style !== "none" ? { line: {
757
+ fill: "solid",
758
+ colorHex: edge.colorHex ?? "000000",
759
+ width: edge.width ?? pt(.75)
760
+ } } : {},
761
+ float: frameFloat(frame),
762
+ paragraphProperties: {},
763
+ text: {
764
+ content: paragraphs.map((paragraph) => ({
765
+ kind: "paragraph",
766
+ paragraph
767
+ })),
768
+ insetLeft: pt(0),
769
+ insetRight: pt(0),
770
+ insetTop: pt(0),
771
+ insetBottom: pt(0),
772
+ ...vertical ? { vertical } : {},
773
+ ...exact ? {} : { autoFit: true }
774
+ }
775
+ };
544
776
  }
545
- function layoutBodyElement(el, options, fontResources, imageResources, contentWidth, maxHeight) {
777
+ /** §17.3.1.11 → §20.4.2.3: the frame's anchor, in the terms floats are placed in. */
778
+ function frameFloat(frame) {
779
+ const wrap = frame.wrap === "none" ? "none" : frame.wrap === "notBeside" ? "topAndBottom" : "square";
780
+ const hRel = frame.hAnchor === "page" ? "page" : frame.hAnchor === "margin" ? "margin" : "column";
781
+ const vRel = frame.vAnchor === "page" ? "page" : frame.vAnchor === "margin" ? "margin" : "paragraph";
782
+ const align = frame.xAlign === "left" || frame.xAlign === "center" || frame.xAlign === "right" ? frame.xAlign : void 0;
783
+ const vAlign = frame.yAlign === "top" || frame.yAlign === "center" || frame.yAlign === "bottom" ? frame.yAlign : void 0;
784
+ return {
785
+ wrap,
786
+ posH: {
787
+ relativeFrom: hRel,
788
+ ...align ? { align } : { offsetPt: frame.xPt ?? pt(0) }
789
+ },
790
+ posV: {
791
+ relativeFrom: vRel,
792
+ ...vAlign ? { align: vAlign } : { offsetPt: frame.yPt ?? pt(0) }
793
+ }
794
+ };
795
+ }
796
+ /**
797
+ * A drawing's width when it states one as a share of the page or the margins.
798
+ *
799
+ * @param rel The relative size, if any.
800
+ * @param content The margin box's width.
801
+ * @param box The page box, when the caller knows it.
802
+ * @returns The width in points, or `undefined` when none is stated.
803
+ */
804
+ function relativeWidth(rel, content, box) {
805
+ if (!rel?.widthPct) return void 0;
806
+ const from = rel.widthFrom;
807
+ return (box === void 0 || from === void 0 || from === "margin" ? content : from === "page" ? box.pageWidthPt : from === "leftMargin" ? box.marginLeftPt : box.marginRightPt) * rel.widthPct;
808
+ }
809
+ /** The height twin of {@link relativeWidth}; the margin box's height is the page's content height. */
810
+ function relativeHeight(rel, contentHeight, box) {
811
+ if (!rel?.heightPct) return void 0;
812
+ const from = rel.heightFrom;
813
+ const base = box === void 0 || from === void 0 || from === "margin" ? contentHeight : from === "page" ? box.pageHeightPt : from === "topMargin" ? box.marginTopPt : box.marginBottomPt;
814
+ return base === void 0 ? void 0 : base * rel.heightPct;
815
+ }
816
+ function laidOutBlocksFor(elements, options, fontResources, contentWidth, imageResources, box) {
817
+ const frames = frameGroups(elements);
818
+ return elements.map((el, idx) => {
819
+ const group = frames.starts.get(idx);
820
+ if (group) return layoutFrameBlock(group, options, fontResources, imageResources, contentWidth);
821
+ if (frames.continued.has(idx)) return emptyBlock(options);
822
+ return layoutBodyElement(el, options, fontResources, imageResources, contentWidth, void 0, box);
823
+ });
824
+ }
825
+ function layoutBodyElement(el, options, fontResources, imageResources, contentWidth, maxHeight, box) {
546
826
  if (el.kind === "paragraph") return layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, contentWidth);
547
827
  if (el.kind === "table") return layoutTableBlock(el.table, options, fontResources, imageResources, contentWidth);
548
- if (el.kind === "image") return layoutImageBlock(el.image, imageResources, contentWidth);
828
+ if (el.kind === "image") return layoutImageBlock(el.image, imageResources, contentWidth, maxHeight, box);
549
829
  if (el.kind === "chart") return layoutChartBlock(el.chart, options, fontResources, contentWidth, maxHeight);
550
- return layoutShapeBlock(el.shape, options, fontResources, imageResources, contentWidth, maxHeight);
830
+ return layoutShapeBlock(el.shape, options, fontResources, imageResources, contentWidth, maxHeight, box);
551
831
  }
552
- function layoutImageBlock(image, imageResources, contentWidth) {
553
- let widthPt = image.width;
554
- let heightPt = image.height;
555
- if (widthPt > contentWidth) {
832
+ function layoutImageBlock(image, imageResources, contentWidth, maxHeight, box) {
833
+ let widthPt = relativeWidth(image.relativeSize, contentWidth, box) ?? image.width;
834
+ let heightPt = relativeHeight(image.relativeSize, maxHeight, box) ?? (image.relativeSize?.widthPct && image.width > 0 ? image.height * (widthPt / image.width) : image.height);
835
+ if (widthPt > contentWidth && !image.float) {
556
836
  const scale = contentWidth / widthPt;
557
837
  widthPt = contentWidth;
558
838
  heightPt = heightPt * scale;
559
839
  }
840
+ let drawInset;
841
+ const eff = image.effectExtent;
842
+ if (eff) {
843
+ const s = image.width > 0 ? widthPt / image.width : 1;
844
+ let box = {
845
+ dxPt: eff.leftPt * s,
846
+ dyTopPt: eff.topPt * s,
847
+ widthPt,
848
+ heightPt
849
+ };
850
+ let reservedW = widthPt + (eff.leftPt + eff.rightPt) * s;
851
+ let reservedH = heightPt + (eff.topPt + eff.bottomPt) * s;
852
+ if (reservedW > contentWidth) {
853
+ const k = contentWidth / reservedW;
854
+ reservedW = contentWidth;
855
+ reservedH *= k;
856
+ box = {
857
+ dxPt: box.dxPt * k,
858
+ dyTopPt: box.dyTopPt * k,
859
+ widthPt: box.widthPt * k,
860
+ heightPt: box.heightPt * k
861
+ };
862
+ }
863
+ widthPt = reservedW;
864
+ heightPt = reservedH;
865
+ drawInset = box;
866
+ }
560
867
  const res = image.resource ? imageResources?.get(image.resource) : void 0;
561
868
  const resolvedAlignment = image.paragraphProperties.alignment ?? "left";
562
869
  return {
@@ -565,27 +872,33 @@ function layoutImageBlock(image, imageResources, contentWidth) {
565
872
  heightPt,
566
873
  resolvedAlignment,
567
874
  resourceName: res?.resourceName ?? "",
875
+ ...image.outline ? { outline: image.outline } : {},
876
+ ...image.crop ? { crop: image.crop } : {},
877
+ ...image.rotation60k ? { rotationDeg: image.rotation60k / 6e4 } : {},
878
+ ...image.flipH ? { flipH: true } : {},
879
+ ...image.flipV ? { flipV: true } : {},
880
+ ...drawInset ? { drawInset } : {},
568
881
  spacingBeforePt: image.paragraphProperties.spacingBefore ?? 0,
569
882
  spacingAfterPt: image.paragraphProperties.spacingAfter ?? 0,
570
883
  ...image.altText ? { altText: image.altText } : {},
571
884
  ...image.float ? { float: image.float } : {}
572
885
  };
573
886
  }
574
- function layoutShapeBlock(shape, options, fontResources, imageResources, contentWidth, maxHeight) {
575
- let widthPt = shape.width;
576
- let heightPt = shape.height;
577
- if (widthPt > contentWidth && widthPt > 0) {
887
+ function layoutShapeBlock(shape, options, fontResources, imageResources, contentWidth, maxHeight, box, fitPass = 0) {
888
+ let widthPt = relativeWidth(shape.relativeSize, contentWidth, box) ?? shape.width;
889
+ let heightPt = relativeHeight(shape.relativeSize, maxHeight, box) ?? (shape.relativeSize?.widthPct && shape.width > 0 ? shape.height * (widthPt / shape.width) : shape.height);
890
+ if (!shape.float && widthPt > contentWidth && widthPt > 0) {
578
891
  const scale = contentWidth / widthPt;
579
892
  widthPt = contentWidth;
580
893
  heightPt *= scale;
581
894
  }
582
- if (maxHeight !== void 0 && heightPt > maxHeight && heightPt > 0) {
895
+ if (!shape.float && maxHeight !== void 0 && heightPt > maxHeight && heightPt > 0) {
583
896
  const scale = maxHeight / heightPt;
584
897
  widthPt *= scale;
585
898
  heightPt = maxHeight;
586
899
  }
587
- const paths = buildShapePaths(shape.geometry, widthPt, heightPt);
588
900
  const fillGradient = shape.fill.kind === "gradient" ? shape.fill.gradient : void 0;
901
+ const fillImageResourceName = shape.fill.kind === "picture" && shape.fill.imageResource !== void 0 ? imageResources?.get(shape.fill.imageResource)?.resourceName : void 0;
589
902
  const fillColorHex = shape.fill.kind === "solid" ? shape.fill.colorHex : fillGradient ? gradientToSolid(fillGradient) : void 0;
590
903
  const stroke = buildStroke(shape.line);
591
904
  const t = shape.transform;
@@ -596,33 +909,150 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
596
909
  const insetTopPt = text?.insetTop ?? 3.6;
597
910
  const insetBottomPt = text?.insetBottom ?? 3.6;
598
911
  const textLines = [];
912
+ const textCharts = /* @__PURE__ */ new Map();
913
+ const textShapes = /* @__PURE__ */ new Map();
914
+ const textTables = /* @__PURE__ */ new Map();
915
+ const textLineGaps = /* @__PURE__ */ new Map();
599
916
  let textHeightPt = 0;
917
+ const vertical = text?.vertical;
600
918
  if (text && text.content.length > 0) {
601
- const innerWidth = Math.max(1, widthPt - insetLeftPt - insetRightPt);
919
+ const innerWidth = vertical ? Math.max(1, heightPt - insetTopPt - insetBottomPt) : Math.max(1, widthPt - insetLeftPt - insetRightPt);
602
920
  for (const el of text.content) {
603
- if (el.kind !== "paragraph") continue;
921
+ if (el.kind === "image") {
922
+ const res = el.image.resource ? imageResources?.get(el.image.resource) : void 0;
923
+ const scaled = Math.min(1, innerWidth / Math.max(1, el.image.width));
924
+ const line = {
925
+ tokens: [{
926
+ kind: "image",
927
+ imageResourceName: res?.resourceName ?? "",
928
+ widthPt: el.image.width * scaled,
929
+ heightPt: el.image.height * scaled,
930
+ isSpace: false,
931
+ bidiLevel: 0
932
+ }],
933
+ contentWidthPt: el.image.width * scaled,
934
+ maxFontSizePt: el.image.height * scaled,
935
+ availableWidthPt: innerWidth,
936
+ firstLine: true,
937
+ resolved: resolveParagraphProperties(el.image.paragraphProperties, options.styles),
938
+ isLastInParagraph: true
939
+ };
940
+ textLines.push(line);
941
+ textHeightPt += computeLineHeight(line, line.resolved);
942
+ continue;
943
+ }
944
+ if (el.kind === "chart") {
945
+ const laid = layoutChartBlock(el.chart, options, fontResources, innerWidth);
946
+ const line = {
947
+ tokens: [],
948
+ contentWidthPt: laid.widthPt,
949
+ maxFontSizePt: laid.heightPt,
950
+ availableWidthPt: innerWidth,
951
+ firstLine: true,
952
+ resolved: resolveParagraphProperties(el.chart.paragraphProperties, options.styles),
953
+ isLastInParagraph: true
954
+ };
955
+ textCharts.set(textLines.length, laid);
956
+ textLines.push(line);
957
+ textHeightPt += computeLineHeight(line, line.resolved);
958
+ continue;
959
+ }
960
+ if (el.kind === "shape") {
961
+ const laid = layoutShapeBlock(el.shape, options, fontResources, imageResources, innerWidth);
962
+ const line = {
963
+ tokens: [],
964
+ contentWidthPt: laid.widthPt,
965
+ maxFontSizePt: laid.heightPt,
966
+ availableWidthPt: innerWidth,
967
+ firstLine: true,
968
+ resolved: resolveParagraphProperties(el.shape.paragraphProperties, options.styles),
969
+ isLastInParagraph: true
970
+ };
971
+ textShapes.set(textLines.length, laid);
972
+ textLines.push(line);
973
+ textHeightPt += computeLineHeight(line, line.resolved);
974
+ continue;
975
+ }
976
+ if (el.kind === "table") {
977
+ const laid = layoutTableBlock(el.table, options, fontResources, imageResources, innerWidth);
978
+ const line = {
979
+ tokens: [],
980
+ contentWidthPt: laid.totalWidthPt,
981
+ maxFontSizePt: laid.heightPt,
982
+ metricHeightPt: laid.heightPt,
983
+ availableWidthPt: innerWidth,
984
+ firstLine: true,
985
+ resolved: resolveParagraphProperties({}, options.styles),
986
+ isLastInParagraph: true
987
+ };
988
+ textTables.set(textLines.length, laid);
989
+ textLines.push(line);
990
+ textHeightPt += computeLineHeight(line, line.resolved);
991
+ continue;
992
+ }
604
993
  const blk = layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, innerWidth);
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) {
994
+ const before = textLines.length > 0 ? blk.spacingBeforePt : 0;
995
+ if (before > 0) {
996
+ const last = textLines.length - 1;
997
+ textLineGaps.set(last, (textLineGaps.get(last) ?? 0) + before);
998
+ textHeightPt += before;
999
+ }
1000
+ for (const line of blk.lines) {
615
1001
  textLines.push(line);
616
1002
  textHeightPt += computeLineHeight(line, blk.resolved);
617
1003
  }
1004
+ if (blk.spacingAfterPt > 0) {
1005
+ const last = textLines.length - 1;
1006
+ textLineGaps.set(last, (textLineGaps.get(last) ?? 0) + blk.spacingAfterPt);
1007
+ }
618
1008
  textHeightPt += blk.spacingAfterPt;
619
1009
  }
620
1010
  }
1011
+ if (text?.autoFit && !vertical && textLines.length > 0) heightPt = textHeightPt + insetTopPt + insetBottomPt;
1012
+ if (text?.fitToBox === true && textLines.length > 0 && fitPass < MAX_FIT_PASSES) {
1013
+ const innerW = Math.max(1, widthPt - insetLeftPt - insetRightPt);
1014
+ const innerH = Math.max(1, heightPt - insetTopPt - insetBottomPt);
1015
+ let widest = 0;
1016
+ let running = 0;
1017
+ let paragraphs = 0;
1018
+ for (const l of textLines) {
1019
+ running += l.contentWidthPt;
1020
+ if (l.isLastInParagraph) {
1021
+ widest = Math.max(widest, running);
1022
+ running = 0;
1023
+ paragraphs++;
1024
+ }
1025
+ }
1026
+ widest = Math.max(1, widest, running);
1027
+ const perLine = textHeightPt / Math.max(1, textLines.length);
1028
+ const naturalHeight = Math.max(1, perLine * Math.max(1, paragraphs || textLines.length));
1029
+ const k = Math.min(innerW / widest, innerH / naturalHeight);
1030
+ if (Number.isFinite(k) && k > .05 && k < 20 && Math.abs(k - 1) > .02) return layoutShapeBlock({
1031
+ ...shape,
1032
+ text: scaleTextBody(text, k)
1033
+ }, options, fontResources, imageResources, contentWidth, maxHeight, box, fitPass + 1);
1034
+ }
1035
+ const paths = buildShapePaths(shape.geometry, widthPt, heightPt);
1036
+ const markerPaths = lineEndPaths(paths, shape.line, stroke?.widthPt ?? 0);
1037
+ const scale = shape.width > 0 ? widthPt / shape.width : 1;
1038
+ const children = (shape.children ?? []).map((child) => ({
1039
+ laid: layoutShapeBlock({
1040
+ ...child.shape,
1041
+ width: pt(child.shape.width * scale),
1042
+ height: pt(child.shape.height * scale)
1043
+ }, options, fontResources, imageResources, Number.POSITIVE_INFINITY),
1044
+ xPt: child.xPt * scale,
1045
+ yPt: child.yPt * scale
1046
+ }));
621
1047
  return {
622
1048
  kind: "shape",
623
1049
  widthPt,
624
1050
  heightPt,
1051
+ ...children.length > 0 ? { children } : {},
625
1052
  paths,
1053
+ ...markerPaths.length > 0 ? { markerPaths } : {},
1054
+ ...fillImageResourceName ? { fillImageResourceName } : {},
1055
+ ...fillImageResourceName && shape.fill.imageCrop ? { fillImageCrop: shape.fill.imageCrop } : {},
626
1056
  ...fillColorHex ? { fillColorHex } : {},
627
1057
  ...fillGradient ? { fillGradient } : {},
628
1058
  ...stroke ? { stroke } : {},
@@ -634,12 +1064,17 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
634
1064
  spacingBeforePt: pp.spacingBefore ?? 0,
635
1065
  spacingAfterPt: pp.spacingAfter ?? 0,
636
1066
  textLines,
1067
+ ...textCharts.size > 0 ? { textCharts } : {},
1068
+ ...textShapes.size > 0 ? { textShapes } : {},
1069
+ ...textTables.size > 0 ? { textTables } : {},
1070
+ ...textLineGaps.size > 0 ? { textLineGaps } : {},
637
1071
  textHeightPt,
638
1072
  insetLeftPt,
639
1073
  insetRightPt,
640
1074
  insetTopPt,
641
1075
  insetBottomPt,
642
1076
  anchor: text?.anchor ?? "t",
1077
+ ...vertical ? { vertical } : {},
643
1078
  ...shape.altText ? { altText: shape.altText } : {},
644
1079
  ...shape.float ? { float: shape.float } : {}
645
1080
  };
@@ -693,6 +1128,7 @@ function buildChartLayout(chart, widthPt, heightPt, font) {
693
1128
  };
694
1129
  const shapes = [];
695
1130
  if (scene.background) shapes.push(rectPrim(scene.background));
1131
+ if (scene.plotBackground) shapes.push(rectPrim(scene.plotBackground));
696
1132
  for (const g of scene.gridlines ?? []) shapes.push(polylinePrim(g));
697
1133
  for (const pg of scene.polygons ?? []) shapes.push(polygonPrim(pg));
698
1134
  for (const r of scene.rects) shapes.push(rectPrim(r));
@@ -818,14 +1254,20 @@ function circlePath(cx, cy, r) {
818
1254
  const k = .5522847498307936 * r;
819
1255
  return new PathBuilder().moveTo(cx + r, cy).cubicTo(cx + r, cy + k, cx + k, cy + r, cx, cy + r).cubicTo(cx - k, cy + r, cx - r, cy + k, cx - r, cy).cubicTo(cx - r, cy - k, cx - k, cy - r, cx, cy - r).cubicTo(cx + k, cy - r, cx + r, cy - k, cx + r, cy).close().build();
820
1256
  }
1257
+ function strokeOfRect(r) {
1258
+ if (!r.strokeHex) return {};
1259
+ const stroke = buildStroke({
1260
+ colorHex: r.strokeHex,
1261
+ width: pt(r.strokeWidthPt ?? 1),
1262
+ ...r.strokeDash ? { dash: r.strokeDash } : {}
1263
+ });
1264
+ return stroke ? { stroke } : {};
1265
+ }
821
1266
  function rectPrim(r) {
822
1267
  return {
823
1268
  paths: [rectAtPath(r.x, r.y, r.w, r.h)],
824
1269
  ...r.fillHex ? { fillColorHex: r.fillHex } : {},
825
- ...r.strokeHex ? { stroke: {
826
- colorHex: r.strokeHex,
827
- widthPt: r.strokeWidthPt ?? 1
828
- } } : {}
1270
+ ...strokeOfRect(r)
829
1271
  };
830
1272
  }
831
1273
  function polylinePrim(p) {
@@ -915,10 +1357,16 @@ function collectImageResources(body, headersFooters, options) {
915
1357
  for (const run of el.paragraph.runs) if (run.inlineImage?.resource) seen.add(run.inlineImage.resource);
916
1358
  } else if (el.kind === "table") for (const row of el.table.rows) for (const cell of row.cells) visit(cell.content);
917
1359
  else if (el.kind === "shape") {
918
- if (el.shape.text) visit(el.shape.text.content);
1360
+ const shapeImages = (sh) => {
1361
+ if (sh.fill.imageResource) seen.add(sh.fill.imageResource);
1362
+ if (sh.text) visit(sh.text.content);
1363
+ for (const child of sh.children ?? []) shapeImages(child.shape);
1364
+ };
1365
+ shapeImages(el.shape);
919
1366
  }
920
1367
  };
921
1368
  visit(body);
1369
+ if (options.pageBackgroundFill?.imageResource !== void 0) seen.add(options.pageBackgroundFill.imageResource);
922
1370
  for (const hf of headersFooters.values()) visit(hf);
923
1371
  for (const note of options.footnotes?.values() ?? []) visit(note);
924
1372
  for (const note of options.endnotes?.values() ?? []) visit(note);
@@ -942,12 +1390,257 @@ function collectImageResources(body, headersFooters, options) {
942
1390
  }
943
1391
  return out;
944
1392
  }
945
- function drawBlocksSequentially(blocks, startX, startY, pageHeight, structId) {
1393
+ function chartPageItems(laid, x, bottomYUp, pageHeight, structId) {
1394
+ const fig = structId !== void 0 ? { structId } : {};
1395
+ const out = [];
1396
+ for (const sh of laid.layout.shapes) out.push({
1397
+ type: "shape",
1398
+ shape: {
1399
+ paths: sh.paths,
1400
+ ...sh.fillColorHex ? { fillColorHex: sh.fillColorHex } : {},
1401
+ ...sh.stroke ? { stroke: sh.stroke } : {},
1402
+ transform: flipTransform([
1403
+ 1,
1404
+ 0,
1405
+ 0,
1406
+ 1,
1407
+ x,
1408
+ bottomYUp
1409
+ ], pageHeight)
1410
+ },
1411
+ ...fig
1412
+ });
1413
+ for (const t of laid.layout.texts) out.push({
1414
+ type: "line",
1415
+ line: t.line,
1416
+ originX: pt(x + t.x),
1417
+ baselineY: pt(pageHeight - (bottomYUp + t.y)),
1418
+ ...t.rotationDeg ? { rotationDeg: t.rotationDeg } : {},
1419
+ ...fig
1420
+ });
1421
+ return out;
1422
+ }
1423
+ function emitShapeText(sh, x, bottomYUp, sink, pageHeight, figId) {
1424
+ if (sh.textLines.length === 0) return;
1425
+ if (sh.vertical) {
1426
+ const up = sh.vertical === "vert270";
1427
+ const innerHeight = Math.max(1, sh.heightPt - sh.insetTopPt - sh.insetBottomPt);
1428
+ let across = sh.anchor === "b" ? sh.widthPt - sh.insetRightPt - sh.textHeightPt : sh.anchor === "ctr" ? (sh.widthPt - sh.textHeightPt) / 2 : sh.insetLeftPt;
1429
+ for (const line of sh.textLines) {
1430
+ const h = computeLineHeight(line, line.resolved);
1431
+ const lineOffset = alignmentOffset(line.resolved.alignment, line.contentWidthPt, innerHeight);
1432
+ const off = lineBaselineOffset(line, line.resolved);
1433
+ const baselineAcross = up ? across + h - off : across + off;
1434
+ sink.push({
1435
+ type: "line",
1436
+ line,
1437
+ originX: pt(up ? x + baselineAcross : x + sh.widthPt - baselineAcross),
1438
+ baselineY: pt(pageHeight - (up ? bottomYUp + sh.insetBottomPt + lineOffset : bottomYUp + sh.heightPt - sh.insetTopPt - lineOffset)),
1439
+ rotationDeg: up ? 90 : -90,
1440
+ ...figId !== void 0 ? { structId: figId } : {}
1441
+ });
1442
+ across += h;
1443
+ }
1444
+ return;
1445
+ }
1446
+ const shapeTop = bottomYUp + sh.heightPt;
1447
+ const innerWidth = Math.max(1, sh.widthPt - sh.insetLeftPt - sh.insetRightPt);
1448
+ let textY;
1449
+ if (sh.anchor === "b") textY = bottomYUp + sh.insetBottomPt + sh.textHeightPt;
1450
+ else if (sh.anchor === "ctr") textY = bottomYUp + (sh.heightPt + sh.textHeightPt) / 2;
1451
+ else textY = shapeTop - sh.insetTopPt;
1452
+ sh.textLines.forEach((line, i) => {
1453
+ textY -= computeLineHeight(line, line.resolved);
1454
+ const lineOffset = alignmentOffset(line.resolved.alignment, line.contentWidthPt, innerWidth);
1455
+ const nested = sh.textCharts?.get(i);
1456
+ if (nested) {
1457
+ sink.push(...chartPageItems(nested, x + sh.insetLeftPt + lineOffset, textY, pageHeight, figId));
1458
+ return;
1459
+ }
1460
+ const drawing = sh.textShapes?.get(i);
1461
+ if (drawing) {
1462
+ emitShapeItems(drawing, x + sh.insetLeftPt + lineOffset, textY, sink, pageHeight, figId);
1463
+ return;
1464
+ }
1465
+ const table = sh.textTables?.get(i);
1466
+ if (table) {
1467
+ let rowY = textY + table.heightPt;
1468
+ for (const row of table.rows) {
1469
+ emitRowChunk(sink, row, x + sh.insetLeftPt + lineOffset + table.xOffsetPt, rowY, pageHeight, table.colCount);
1470
+ rowY -= row.heightPt;
1471
+ }
1472
+ return;
1473
+ }
1474
+ sink.push({
1475
+ type: "line",
1476
+ line,
1477
+ originX: pt(x + sh.insetLeftPt + lineOffset),
1478
+ baselineY: pt(pageHeight - (textY + lineBaselineOffset(line, line.resolved))),
1479
+ ...figId !== void 0 ? { structId: figId } : {}
1480
+ });
1481
+ textY -= sh.textLineGaps?.get(i) ?? 0;
1482
+ });
1483
+ }
1484
+ function emitShapeItems(sh, x, bottomYUp, sink, pageHeight, figId) {
1485
+ if (sh.fillImageResourceName) sink.push({
1486
+ type: "image",
1487
+ x: pt(x),
1488
+ y: pt(pageHeight - (bottomYUp + sh.heightPt)),
1489
+ width: pt(sh.widthPt),
1490
+ height: pt(sh.heightPt),
1491
+ imageResourceName: sh.fillImageResourceName,
1492
+ ...sh.fillImageCrop ? { crop: sh.fillImageCrop } : {},
1493
+ clip: {
1494
+ paths: sh.paths,
1495
+ transform: flipTransform(buildShapeTransform(x, bottomYUp, sh.widthPt, sh.heightPt, sh.rotation60k, sh.flipH, sh.flipV), pageHeight)
1496
+ },
1497
+ ...figId !== void 0 ? { structId: figId } : {}
1498
+ });
1499
+ if (sh.markerPaths && sh.stroke) sink.push({
1500
+ type: "shape",
1501
+ shape: {
1502
+ paths: sh.markerPaths,
1503
+ fillColorHex: sh.stroke.colorHex,
1504
+ transform: flipTransform(buildShapeTransform(x, bottomYUp, sh.widthPt, sh.heightPt, sh.rotation60k, sh.flipH, sh.flipV), pageHeight)
1505
+ },
1506
+ ...figId !== void 0 ? { structId: figId } : {}
1507
+ });
1508
+ if (sh.paths.some((path) => path.segments.length > 0)) sink.push({
1509
+ type: "shape",
1510
+ shape: {
1511
+ paths: sh.paths,
1512
+ ...sh.fillColorHex ? { fillColorHex: sh.fillColorHex } : {},
1513
+ ...sh.fillGradient ? { fillGradient: sh.fillGradient } : {},
1514
+ ...sh.stroke ? { stroke: sh.stroke } : {},
1515
+ ...sh.shadow ? { shadow: sh.shadow } : {},
1516
+ transform: flipTransform(buildShapeTransform(x, bottomYUp, sh.widthPt, sh.heightPt, sh.rotation60k, sh.flipH, sh.flipV), pageHeight)
1517
+ },
1518
+ ...figId !== void 0 ? { structId: figId } : {}
1519
+ });
1520
+ for (const child of sh.children ?? []) emitShapeItems(child.laid, x + child.xPt, bottomYUp + sh.heightPt - child.yPt - child.laid.heightPt, sink, pageHeight, figId);
1521
+ emitShapeText(sh, x, bottomYUp, sink, pageHeight, figId);
1522
+ }
1523
+ /**
1524
+ * §20.1.2.2.24 — the four rules of a picture's own frame, around the box the
1525
+ * picture is drawn in. Borders paint after images and before shapes, so the
1526
+ * frame lands on top of the picture the way Word draws it.
1527
+ *
1528
+ * @param outline The picture's frame.
1529
+ * @param x Left edge of the picture box.
1530
+ * @param topY Distance from the page TOP down to the box's top edge.
1531
+ * @param widthPt Box width.
1532
+ * @param heightPt Box height.
1533
+ * @param structId Tagged-PDF structure node the picture belongs to.
1534
+ * @returns One {@link BorderItem} per side.
1535
+ */
1536
+ function pictureOutlineItems(outline, x, topY, widthPt, heightPt, structId) {
1537
+ return [
1538
+ "top",
1539
+ "bottom",
1540
+ "left",
1541
+ "right"
1542
+ ].map((side) => ({
1543
+ type: "border",
1544
+ side,
1545
+ x: pt(x),
1546
+ y: pt(topY),
1547
+ width: pt(widthPt),
1548
+ height: pt(heightPt),
1549
+ borderSizePt: outline.widthPt,
1550
+ borderColorHex: outline.colorHex,
1551
+ ...structId !== void 0 ? { structId } : {}
1552
+ }));
1553
+ }
1554
+ function bandAnchor(float, widthPt, contentWidth, bandLeftPt = 0) {
1555
+ if (!float) return void 0;
1556
+ const align = float.posH?.align;
1557
+ if (align === "right") return contentWidth - widthPt;
1558
+ if (align === "center") return (contentWidth - widthPt) / 2;
1559
+ if (align === "left") return 0;
1560
+ const offset = float.posH?.offsetPt ?? 0;
1561
+ return float.posH?.relativeFrom === "page" ? offset - bandLeftPt : offset;
1562
+ }
1563
+ /**
1564
+ * §20.4.2.3 — how far DOWN the page a band's anchored drawing starts, in the
1565
+ * band emitter's y-UP frame. A `page`-relative anchor is measured from the
1566
+ * paper's own top edge, whatever the band's cursor has reached: n773061.docx
1567
+ * hangs its header text box 92pt down the page and, measured from the header's
1568
+ * first paragraph, it landed 60pt lower than Word and LibreOffice put it.
1569
+ *
1570
+ * @param float The anchor.
1571
+ * @param cursorY The band's cursor (y-up), where a paragraph-relative
1572
+ * anchor is measured from.
1573
+ * @param pageHeight The page height.
1574
+ * @returns The drawing's top edge, y-up.
1575
+ */
1576
+ function bandAnchorTop(float, cursorY, pageHeight, heightPt = 0, margins) {
1577
+ const align = float.posV?.align;
1578
+ if (align !== void 0) {
1579
+ const page = float.posV?.relativeFrom === "page" || !margins;
1580
+ const topYUp = page ? pageHeight : pageHeight - margins.top;
1581
+ const bottomYUp = page ? 0 : margins.bottom;
1582
+ if (align === "top") return topYUp;
1583
+ if (align === "bottom") return bottomYUp + heightPt;
1584
+ return bottomYUp + (topYUp - bottomYUp + heightPt) / 2;
1585
+ }
1586
+ const offset = float.posV?.offsetPt ?? 0;
1587
+ return float.posV?.relativeFrom === "page" ? pageHeight - offset : cursorY - offset;
1588
+ }
1589
+ function drawBlocksSequentially(blocks, startX, startY, pageHeight, contentWidth, structId, bandMargins) {
946
1590
  const out = [];
947
1591
  let cursorY = startY;
948
- for (const block of blocks) {
949
- if (block.kind !== "paragraph") continue;
1592
+ const isBehind = (b) => "float" in b && b.float?.behind === true;
1593
+ const ordered = blocks.some(isBehind) ? [...blocks.filter(isBehind), ...blocks.filter((b) => !isBehind(b))] : blocks;
1594
+ for (const block of ordered) {
1595
+ if (block.kind === "table") {
1596
+ const tableX = startX + block.xOffsetPt;
1597
+ for (const row of block.rows) {
1598
+ emitRowChunk(out, row, tableX, cursorY, pageHeight, block.colCount);
1599
+ cursorY -= row.heightPt;
1600
+ }
1601
+ continue;
1602
+ }
1603
+ if (block.kind === "chart") {
1604
+ cursorY -= block.spacingBeforePt + block.heightPt;
1605
+ const offset = alignmentOffset(block.resolvedAlignment, block.widthPt, contentWidth);
1606
+ out.push(...chartPageItems(block, startX + offset, cursorY, pageHeight, structId));
1607
+ cursorY -= block.spacingAfterPt;
1608
+ continue;
1609
+ }
1610
+ if (block.kind === "image") {
1611
+ cursorY -= block.spacingBeforePt;
1612
+ const anchored = bandAnchor(block.float, block.widthPt, contentWidth, startX);
1613
+ const offset = anchored ?? alignmentOffset(block.resolvedAlignment, block.widthPt, contentWidth);
1614
+ const top = anchored === void 0 || !block.float ? cursorY : bandAnchorTop(block.float, cursorY, pageHeight, block.heightPt, bandMargins);
1615
+ out.push({
1616
+ type: "image",
1617
+ x: pt(startX + offset),
1618
+ y: pt(pageHeight - top),
1619
+ width: pt(block.widthPt),
1620
+ height: pt(block.heightPt),
1621
+ imageResourceName: block.resourceName,
1622
+ ...block.crop ? { crop: block.crop } : {},
1623
+ ...block.rotationDeg ? { rotationDeg: block.rotationDeg } : {},
1624
+ ...block.flipH ? { flipH: true } : {},
1625
+ ...block.flipV ? { flipV: true } : {}
1626
+ });
1627
+ if (block.outline) out.push(...pictureOutlineItems(block.outline, startX + offset, pageHeight - top, block.widthPt, block.heightPt, structId));
1628
+ if (anchored === void 0) cursorY -= block.heightPt;
1629
+ cursorY -= block.spacingAfterPt;
1630
+ continue;
1631
+ }
1632
+ if (block.kind === "shape") {
1633
+ cursorY -= block.spacingBeforePt;
1634
+ const anchored = bandAnchor(block.float, block.widthPt, contentWidth, startX);
1635
+ const offset = anchored ?? alignmentOffset(block.resolvedAlignment, block.widthPt, contentWidth);
1636
+ const top = anchored === void 0 || !block.float ? cursorY : bandAnchorTop(block.float, cursorY, pageHeight, block.heightPt, bandMargins);
1637
+ emitShapeItems(block, startX + offset, top - block.heightPt, out, pageHeight, structId);
1638
+ if (anchored === void 0) cursorY -= block.heightPt;
1639
+ cursorY -= block.spacingAfterPt;
1640
+ continue;
1641
+ }
950
1642
  cursorY -= block.spacingBeforePt;
1643
+ const bandTopY = cursorY;
951
1644
  for (const line of block.lines) {
952
1645
  const h = computeLineHeight(line, block.resolved);
953
1646
  cursorY -= h;
@@ -957,14 +1650,60 @@ function drawBlocksSequentially(blocks, startX, startY, pageHeight, structId) {
957
1650
  type: "line",
958
1651
  line,
959
1652
  originX: pt(startX + indentLeft + offset),
960
- baselineY: pt(pageHeight - (cursorY + lineDescent(line))),
1653
+ baselineY: pt(pageHeight - (cursorY + lineBaselineOffset(line, block.resolved))),
961
1654
  ...structId !== void 0 ? { structId } : {}
962
1655
  });
963
1656
  }
1657
+ out.push(...paragraphDecoration(block.resolved, startX + block.resolved.indentLeft, startX + contentWidth - block.resolved.indentRight, bandTopY, cursorY, pageHeight));
964
1658
  cursorY -= block.spacingAfterPt;
965
1659
  }
966
1660
  return out;
967
1661
  }
1662
+ /**
1663
+ * §17.3.1.31 `w:shd` and §17.3.1.24 `w:pBdr` — the band behind a paragraph and
1664
+ * the rules around it, for a paragraph occupying `topY`…`bottomY` (both in the
1665
+ * y-up page frame) between `x0` and `x1`.
1666
+ *
1667
+ * @returns The fill and border items, in paint order.
1668
+ */
1669
+ function paragraphDecoration(resolved, x0, x1, topY, bottomY, pageHeight) {
1670
+ const out = [];
1671
+ const width = Math.max(0, x1 - x0);
1672
+ if (resolved.shading) out.push({
1673
+ type: "fill",
1674
+ x: pt(x0),
1675
+ y: pt(pageHeight - topY),
1676
+ width: pt(width),
1677
+ height: pt(Math.max(0, topY - bottomY)),
1678
+ fillColorHex: resolved.shading.colorHex
1679
+ });
1680
+ const borders = resolved.borders;
1681
+ if (!borders) return out;
1682
+ for (const side of [
1683
+ "top",
1684
+ "bottom",
1685
+ "left",
1686
+ "right"
1687
+ ]) {
1688
+ const edge = borders[side];
1689
+ if (!edge || edge.style === "none") continue;
1690
+ const gap = edge.spacePt ?? 0;
1691
+ const top = topY + gap;
1692
+ const bottom = bottomY - gap;
1693
+ out.push({
1694
+ type: "border",
1695
+ side,
1696
+ x: pt(x0),
1697
+ y: pt(pageHeight - top),
1698
+ width: pt(width),
1699
+ height: pt(Math.max(0, top - bottom)),
1700
+ borderSizePt: edge.width ?? DEFAULT_BORDER_SIZE_EIGHTH * EIGHTH_PT,
1701
+ borderColorHex: edge.colorHex ?? "000000",
1702
+ ...edge.style !== "single" ? { borderStyle: edge.style } : {}
1703
+ });
1704
+ }
1705
+ return out;
1706
+ }
968
1707
  function runFontKeyAndParsed(options, ascii, bold, italic) {
969
1708
  if (ascii && options.embeddedFonts) {
970
1709
  const name = ascii.trim().toLowerCase();
@@ -1015,6 +1754,10 @@ function collectFontResources(body, headersFooters, options) {
1015
1754
  }
1016
1755
  const shaped = shapeText(run.text, parsed.glyphForCodepoint, parsed.advanceWidths, parsed.ligatures, parsed.kerning, parsed.joiningForms);
1017
1756
  for (const g of shaped.gids) bucket.gids.add(g);
1757
+ if (run.text.includes(" ")) for (const stop of resolveParagraphProperties(para.properties, options.styles).tabs) {
1758
+ const ch = stop.leader === void 0 ? void 0 : TAB_LEADER_CHARS.get(stop.leader);
1759
+ if (ch !== void 0) bucket.gids.add(parsed.glyphForCodepoint(ch.codePointAt(0)));
1760
+ }
1018
1761
  };
1019
1762
  const addMath = (node) => {
1020
1763
  for (const seg of mathGlyphSegments(node)) {
@@ -1032,7 +1775,7 @@ function collectFontResources(body, headersFooters, options) {
1032
1775
  }
1033
1776
  };
1034
1777
  const visit = (elements) => {
1035
- for (const el of elements) if (el.kind === "paragraph") for (const run of el.paragraph.runs) {
1778
+ for (const el of elements) if (el.kind === "paragraph") for (const run of displayCapsRuns(el.paragraph, options)) {
1036
1779
  if (run.math) {
1037
1780
  addMath(run.math);
1038
1781
  continue;
@@ -1065,7 +1808,11 @@ function collectFontResources(body, headersFooters, options) {
1065
1808
  visit(cell.content);
1066
1809
  }
1067
1810
  else if (el.kind === "shape") {
1068
- if (el.shape.text) visit(el.shape.text.content);
1811
+ const shapeText = (sh) => {
1812
+ if (sh.text) visit(sh.text.content);
1813
+ for (const child of sh.children ?? []) shapeText(child.shape);
1814
+ };
1815
+ shapeText(el.shape);
1069
1816
  } else if (el.kind === "chart") {
1070
1817
  const chart = options.charts?.get(el.chart.chartRelId);
1071
1818
  if (chart) {
@@ -1093,6 +1840,16 @@ function collectFontResources(body, headersFooters, options) {
1093
1840
  }
1094
1841
  };
1095
1842
  visit(body);
1843
+ if ((options.sections ?? []).some((sec) => sec.properties.lineNumbering)) visit([{
1844
+ kind: "paragraph",
1845
+ paragraph: {
1846
+ properties: {},
1847
+ runs: [{
1848
+ text: "0123456789",
1849
+ properties: {}
1850
+ }]
1851
+ }
1852
+ }]);
1096
1853
  for (const hf of headersFooters.values()) visit(hf);
1097
1854
  for (const note of options.footnotes?.values() ?? []) visit(note);
1098
1855
  for (const note of options.endnotes?.values() ?? []) visit(note);
@@ -1135,17 +1892,83 @@ function collectFontResources(body, headersFooters, options) {
1135
1892
  }
1136
1893
  return out;
1137
1894
  }
1895
+ /**
1896
+ * The one line an empty paragraph still occupies — no tokens, but as tall as
1897
+ * the paragraph mark's own font (§17.3.1.31 `w:rPr` on the `w:pPr`, which is
1898
+ * exactly the run properties Word keeps for a paragraph that holds no runs).
1899
+ */
1900
+ function emptyLine(paragraph, options, resolved, availableWidthPt, fontResources) {
1901
+ const mark = resolveRunProperties(paragraph.properties.runProperties ?? {}, paragraph.properties, options.styles);
1902
+ const fontSizePt = mark.fontSizePt;
1903
+ const profile = options.layoutProfile ?? "ream";
1904
+ const { variant } = options.registry.resolveByStyle(mark.bold, mark.italic);
1905
+ const parsed = fontResources.get(variant)?.parsed;
1906
+ const metric = profile !== "ream" && parsed ? fontLeadingPt(parsed, fontSizePt, profile) : void 0;
1907
+ return {
1908
+ tokens: [],
1909
+ contentWidthPt: 0,
1910
+ maxFontSizePt: fontSizePt,
1911
+ availableWidthPt,
1912
+ firstLine: true,
1913
+ resolved,
1914
+ isLastInParagraph: true,
1915
+ ...metric && metric.ascentPt + metric.descentPt > 0 ? {
1916
+ metricHeightPt: metric.ascentPt + metric.descentPt + metric.lineGapPt,
1917
+ metricDescentPt: metric.descentPt
1918
+ } : {}
1919
+ };
1920
+ }
1138
1921
  function layoutParagraphBlock(paragraph, options, fontResources, imageResources, contentWidth, lineWidths) {
1139
- const baseResolved = resolveParagraphProperties(paragraph.properties, options.styles);
1922
+ const cascaded = resolveParagraphProperties(paragraph.properties, options.styles);
1923
+ const baseResolved = options.gridLinePitchPt !== void 0 && cascaded.snapToGrid !== false ? {
1924
+ ...cascaded,
1925
+ gridLinePitchPt: options.gridLinePitchPt
1926
+ } : cascaded;
1927
+ if (paragraph.properties.sectionBreak === true && paragraph.runs.length === 0) return {
1928
+ kind: "paragraph",
1929
+ resolved: baseResolved,
1930
+ lines: [],
1931
+ heightPt: 0,
1932
+ spacingBeforePt: 0,
1933
+ spacingAfterPt: 0
1934
+ };
1140
1935
  const baseDir = paragraphBaseDirection(paragraph, baseResolved);
1141
- const resolved = baseDir === "rtl" && baseResolved.alignment === "left" ? {
1936
+ const swapAlign = (a) => a === "left" ? "right" : a === "right" ? "left" : a;
1937
+ const resolved = baseResolved.bidi === true ? {
1938
+ ...baseResolved,
1939
+ alignment: swapAlign(baseResolved.alignment),
1940
+ indentLeft: baseResolved.indentRight,
1941
+ indentRight: baseResolved.indentLeft
1942
+ } : baseDir === "rtl" && baseResolved.alignment === "left" ? {
1142
1943
  ...baseResolved,
1143
1944
  alignment: "right"
1144
1945
  } : baseResolved;
1145
- const lines = wrap(tokenizeParagraph(paragraph, options, fontResources, imageResources, contentWidth, baseDir), paragraphMaxWidth(resolved, contentWidth, true), paragraphMaxWidth(resolved, contentWidth, false), resolved, options.hyphenator, options.layoutProfile ?? "ream", lineWidths);
1946
+ const tokens = tokenizeParagraph(paragraph, options, fontResources, imageResources, contentWidth, baseDir);
1947
+ const firstLineWidth = paragraphMaxWidth(resolved, contentWidth, true);
1948
+ const otherWidth = paragraphMaxWidth(resolved, contentWidth, false);
1949
+ const runWrap = (part, firstWidth, widths) => wrap(part, firstWidth, otherWidth, resolved, options.hyphenator, options.layoutProfile ?? "ream", options.doNotExpandShiftReturn === true, widths);
1950
+ const parts = splitAtColumnBreaks(tokens);
1951
+ const columnBreakLines = /* @__PURE__ */ new Set();
1952
+ let wrapped;
1953
+ if (parts.length === 1) wrapped = runWrap(tokens, firstLineWidth, lineWidths);
1954
+ else {
1955
+ wrapped = [];
1956
+ for (const [i, part] of parts.entries()) {
1957
+ const lines = runWrap(part, i === 0 ? firstLineWidth : otherWidth, lineWidths?.slice(wrapped.length));
1958
+ if (i > 0 && lines.length > 0) columnBreakLines.add(wrapped.length);
1959
+ wrapped.push(...i === 0 ? lines : lines.map((l) => ({
1960
+ ...l,
1961
+ firstLine: false
1962
+ })));
1963
+ }
1964
+ }
1965
+ const lines = wrapped.length > 0 ? wrapped : [emptyLine(paragraph, options, resolved, Math.max(firstLineWidth, 0), fontResources)];
1146
1966
  let heightPt = 0;
1147
1967
  for (const line of lines) heightPt += computeLineHeight(line, resolved);
1148
1968
  const numbering = paragraph.properties.numbering;
1969
+ const breakIdx = paragraph.runs.findIndex((r) => r.pageBreak === true);
1970
+ const leadingPageBreak = breakIdx >= 0 && paragraph.runs.slice(0, breakIdx + 1).every((r) => r.text === "" && r.inlineImage === void 0);
1971
+ const trailingPageBreak = paragraph.runs.some((r, i) => r.pageBreak === true && (!leadingPageBreak || i > breakIdx));
1149
1972
  return {
1150
1973
  kind: "paragraph",
1151
1974
  resolved,
@@ -1157,7 +1980,9 @@ function layoutParagraphBlock(paragraph, options, fontResources, imageResources,
1157
1980
  numId: numbering.numId,
1158
1981
  level: numbering.ilvl
1159
1982
  } } : {},
1160
- ...paragraph.runs.some((r) => r.pageBreak) ? { pageBreakAfter: true } : {},
1983
+ ...leadingPageBreak ? { pageBreakBefore: true } : {},
1984
+ ...trailingPageBreak ? { pageBreakAfter: true } : {},
1985
+ ...columnBreakLines.size > 0 ? { columnBreakLines } : {},
1161
1986
  ...paragraph.bookmarks && paragraph.bookmarks.length > 0 ? { bookmarks: paragraph.bookmarks } : {},
1162
1987
  source: paragraph
1163
1988
  };
@@ -1197,13 +2022,76 @@ function resolveMathItems(items, fontFor) {
1197
2022
  };
1198
2023
  });
1199
2024
  }
2025
+ /**
2026
+ * §17.3.2.5 `w:caps` / §17.3.2.33 `w:smallCaps` — the paragraph's runs as they
2027
+ * are DISPLAYED. A capitals run is upper-cased; a small-capitals run is too,
2028
+ * and the letters that were lower case are split out to be set smaller.
2029
+ *
2030
+ * @param paragraph The paragraph.
2031
+ * @param options The render options (for the style cascade).
2032
+ * @returns The runs to lay out — the same array when none asks for capitals.
2033
+ */
2034
+ function displayCapsRuns(paragraph, options) {
2035
+ let any = false;
2036
+ const out = [];
2037
+ for (const raw of paragraph.runs) {
2038
+ const run = raw.text.includes(SOFT_HYPHEN) ? (any = true, {
2039
+ ...raw,
2040
+ text: raw.text.replaceAll(SOFT_HYPHEN, "")
2041
+ }) : raw;
2042
+ const resolved = run.text.length > 0 ? resolveRunProperties(run.properties, paragraph.properties, options.styles) : void 0;
2043
+ if (!resolved || !resolved.caps && !resolved.smallCaps) {
2044
+ out.push(run);
2045
+ continue;
2046
+ }
2047
+ any = true;
2048
+ if (resolved.caps) {
2049
+ out.push({
2050
+ ...run,
2051
+ text: run.text.toUpperCase()
2052
+ });
2053
+ continue;
2054
+ }
2055
+ let start = 0;
2056
+ const wasLower = (i) => {
2057
+ const ch = run.text[i];
2058
+ return ch.toLowerCase() === ch && ch.toUpperCase() !== ch;
2059
+ };
2060
+ for (let i = 1; i <= run.text.length; i++) {
2061
+ if (i < run.text.length && wasLower(i) === wasLower(start)) continue;
2062
+ const segment = run.text.slice(start, i).toUpperCase();
2063
+ out.push(wasLower(start) ? {
2064
+ ...run,
2065
+ text: segment,
2066
+ properties: {
2067
+ ...run.properties,
2068
+ fontSizePt: pt(resolved.fontSizePt * .8)
2069
+ }
2070
+ } : {
2071
+ ...run,
2072
+ text: segment
2073
+ });
2074
+ start = i;
2075
+ }
2076
+ }
2077
+ return any ? out : paragraph.runs;
2078
+ }
1200
2079
  function tokenizeParagraph(paragraph, options, fontResources, imageResources, contentWidth, baseDir) {
1201
- const plans = paragraph.runs.map((run) => {
2080
+ const plans = displayCapsRuns(paragraph, options).map((run) => {
1202
2081
  if (run.inlineImage) {
1203
- const naturalW = run.inlineImage.width;
2082
+ const eff = run.inlineImage.effectExtent;
2083
+ const padW = (eff?.leftPt ?? 0) + (eff?.rightPt ?? 0);
2084
+ const padH = (eff?.topPt ?? 0) + (eff?.bottomPt ?? 0);
2085
+ const naturalW = run.inlineImage.width + padW;
1204
2086
  const widthPt = Math.min(naturalW, contentWidth);
1205
2087
  const scale = naturalW > 0 ? widthPt / naturalW : 1;
1206
- const heightPt = run.inlineImage.height * scale;
2088
+ const heightPt = (run.inlineImage.height + padH) * scale;
2089
+ const drawBox = eff ? {
2090
+ dxPt: eff.leftPt * scale,
2091
+ dyPt: eff.bottomPt * scale,
2092
+ widthPt: run.inlineImage.width * scale,
2093
+ heightPt: run.inlineImage.height * scale
2094
+ } : void 0;
1207
2095
  const res = run.inlineImage.resource ? imageResources?.get(run.inlineImage.resource) : void 0;
1208
2096
  const resolvedRun = resolveRunProperties(run.properties, paragraph.properties, options.styles);
1209
2097
  const { fontKey } = runFontKeyAndParsed(options, resolvedRun.fontFamily.ascii, resolvedRun.bold, resolvedRun.italic);
@@ -1215,7 +2103,14 @@ function tokenizeParagraph(paragraph, options, fontResources, imageResources, co
1215
2103
  isImage: true,
1216
2104
  imageWidthPt: widthPt,
1217
2105
  imageHeightPt: heightPt,
1218
- imageResourceName: res?.resourceName ?? ""
2106
+ imageResourceName: res?.resourceName ?? "",
2107
+ ...run.inlineImage.outline ? { imageOutline: run.inlineImage.outline } : {},
2108
+ ...run.inlineImage.shadow ? { imageShadow: run.inlineImage.shadow } : {},
2109
+ ...run.inlineImage.crop ? { imageCrop: run.inlineImage.crop } : {},
2110
+ ...run.inlineImage.rotation60k ? { imageRotationDeg: run.inlineImage.rotation60k / 6e4 } : {},
2111
+ ...run.inlineImage.flipH ? { imageFlipH: true } : {},
2112
+ ...run.inlineImage.flipV ? { imageFlipV: true } : {},
2113
+ ...drawBox ? { imageDrawBox: drawBox } : {}
1219
2114
  };
1220
2115
  }
1221
2116
  if (run.math) {
@@ -1270,6 +2165,19 @@ function tokenizeParagraph(paragraph, options, fontResources, imageResources, co
1270
2165
  ...plan.resolvedRun.rtl ? { rtl: true } : {}
1271
2166
  })), baseDir));
1272
2167
  }
2168
+ /**
2169
+ * Split a paragraph's tokens at every §17.3.3.1 column break, dropping the
2170
+ * break token itself (its line ends there anyway). One part ⇒ no break.
2171
+ *
2172
+ * @param tokens The paragraph's tokens in order.
2173
+ * @returns The parts, in order; always at least one.
2174
+ */
2175
+ function splitAtColumnBreaks(tokens) {
2176
+ const parts = [[]];
2177
+ for (const t of tokens) if (t.kind === "text" && t.columnBreak) parts.push([]);
2178
+ else parts[parts.length - 1].push(t);
2179
+ return parts.filter((p, i) => i === 0 || p.length > 0);
2180
+ }
1273
2181
  function tokenizePlansLtr(plans) {
1274
2182
  const tokens = [];
1275
2183
  for (const plan of plans) {
@@ -1279,6 +2187,13 @@ function tokenizePlansLtr(plans) {
1279
2187
  imageResourceName: plan.imageResourceName,
1280
2188
  widthPt: plan.imageWidthPt,
1281
2189
  heightPt: plan.imageHeightPt,
2190
+ ...plan.imageOutline ? { outline: plan.imageOutline } : {},
2191
+ ...plan.imageShadow ? { shadow: plan.imageShadow } : {},
2192
+ ...plan.imageCrop ? { crop: plan.imageCrop } : {},
2193
+ ...plan.imageRotationDeg ? { rotationDeg: plan.imageRotationDeg } : {},
2194
+ ...plan.imageFlipH ? { flipH: true } : {},
2195
+ ...plan.imageFlipV ? { flipV: true } : {},
2196
+ ...plan.imageDrawBox ? { drawBox: plan.imageDrawBox } : {},
1282
2197
  isSpace: false,
1283
2198
  bidiLevel: 0
1284
2199
  });
@@ -1301,21 +2216,37 @@ function tokenizePlansLtr(plans) {
1301
2216
  kind: "text",
1302
2217
  text: t.text,
1303
2218
  isSpace: t.isSpace,
2219
+ ...t.tab ? { tab: true } : {},
1304
2220
  ...plan.run.href !== void 0 ? { href: plan.run.href } : {},
1305
2221
  ...plan.run.footnoteRef !== void 0 ? { footnoteRef: plan.run.footnoteRef } : {},
1306
2222
  ...plan.run.anchor !== void 0 ? { anchor: plan.run.anchor } : {},
1307
2223
  ...plan.run.listMarker ? { listMarker: true } : {},
2224
+ ...plan.run.columnBreak ? { columnBreak: true } : {},
1308
2225
  ...highlight ? { highlight: true } : {},
1309
2226
  resolvedRun: plan.resolvedRun,
1310
2227
  font: plan.font,
1311
2228
  fontSizePt: plan.fontSizePt,
1312
2229
  ...plan.risePt !== void 0 ? { risePt: plan.risePt } : {},
1313
- widthPt: plan.font.measure.textWidthPt(t.text, plan.fontSizePt),
2230
+ widthPt: measureRunText(plan, t.text),
1314
2231
  bidiLevel: 0
1315
2232
  });
1316
2233
  }
1317
2234
  return tokens;
1318
2235
  }
2236
+ /**
2237
+ * §17.3.2.35 — a run's text measured with the space it puts between its
2238
+ * characters. PDF adds the same amount per glyph through `Tc`, so the width the
2239
+ * line is broken at and the width the page draws agree.
2240
+ *
2241
+ * @param plan The run being tokenized.
2242
+ * @param text The token's text.
2243
+ * @returns The advance width in points.
2244
+ */
2245
+ function measureRunText(plan, text) {
2246
+ const base = plan.font.measure.textWidthPt(text, plan.fontSizePt);
2247
+ const extra = plan.resolvedRun.letterSpacingPt;
2248
+ return extra === void 0 || extra === 0 ? base : base + extra * [...text].length;
2249
+ }
1319
2250
  function tokenizePlansBidi(plans, realLevels) {
1320
2251
  const tokens = [];
1321
2252
  let realIdx = 0;
@@ -1327,6 +2258,13 @@ function tokenizePlansBidi(plans, realLevels) {
1327
2258
  imageResourceName: plan.imageResourceName,
1328
2259
  widthPt: plan.imageWidthPt,
1329
2260
  heightPt: plan.imageHeightPt,
2261
+ ...plan.imageOutline ? { outline: plan.imageOutline } : {},
2262
+ ...plan.imageShadow ? { shadow: plan.imageShadow } : {},
2263
+ ...plan.imageCrop ? { crop: plan.imageCrop } : {},
2264
+ ...plan.imageRotationDeg ? { rotationDeg: plan.imageRotationDeg } : {},
2265
+ ...plan.imageFlipH ? { flipH: true } : {},
2266
+ ...plan.imageFlipV ? { flipV: true } : {},
2267
+ ...plan.imageDrawBox ? { drawBox: plan.imageDrawBox } : {},
1330
2268
  isSpace: false,
1331
2269
  bidiLevel: realLevels[realIdx] ?? 0
1332
2270
  });
@@ -1361,12 +2299,13 @@ function tokenizePlansBidi(plans, realLevels) {
1361
2299
  ...plan.run.footnoteRef !== void 0 ? { footnoteRef: plan.run.footnoteRef } : {},
1362
2300
  ...plan.run.anchor !== void 0 ? { anchor: plan.run.anchor } : {},
1363
2301
  ...plan.run.listMarker ? { listMarker: true } : {},
2302
+ ...plan.run.columnBreak ? { columnBreak: true } : {},
1364
2303
  ...(plan.run.commentRangeRefs?.length ?? 0) > 0 ? { highlight: true } : {},
1365
2304
  resolvedRun: plan.resolvedRun,
1366
2305
  font: plan.font,
1367
2306
  fontSizePt: plan.fontSizePt,
1368
2307
  ...plan.risePt !== void 0 ? { risePt: plan.risePt } : {},
1369
- widthPt: plan.font.measure.textWidthPt(text, plan.fontSizePt),
2308
+ widthPt: measureRunText(plan, text),
1370
2309
  bidiLevel: curLevel
1371
2310
  });
1372
2311
  };
@@ -1393,6 +2332,7 @@ function paragraphBaseDirection(paragraph, resolved) {
1393
2332
  if (resolved.bidi) return "rtl";
1394
2333
  let text = "";
1395
2334
  for (const run of paragraph.runs) {
2335
+ if (run.listMarker === true) continue;
1396
2336
  if (!run.inlineImage) text += run.text;
1397
2337
  if (text.length > 64) break;
1398
2338
  }
@@ -1405,16 +2345,29 @@ function paragraphMaxWidth(p, contentWidth, firstLine) {
1405
2345
  const firstLineExtra = firstLine ? p.indentFirstLine : 0;
1406
2346
  return Math.max(1, contentWidth - indentLeft - indentRight - firstLineExtra);
1407
2347
  }
2348
+ var HYPHENS = new Set([
2349
+ 45,
2350
+ 8208,
2351
+ 8211,
2352
+ 8212
2353
+ ]);
2354
+ /** §17.3.3.29 — the optional hyphen, printed only where a line breaks on it. */
2355
+ var SOFT_HYPHEN = "­";
1408
2356
  function tokenizeText(text) {
1409
2357
  const out = [];
1410
2358
  if (text.length === 0) return out;
1411
- const re = /(\s+)|(\S+)/g;
2359
+ const re = /(\t)|(\s+)|(\S+)/g;
1412
2360
  let m;
1413
2361
  while ((m = re.exec(text)) !== null) if (m[1] !== void 0) out.push({
1414
- text: m[1],
2362
+ text: "",
2363
+ isSpace: true,
2364
+ tab: true
2365
+ });
2366
+ else if (m[2] !== void 0) out.push({
2367
+ text: m[2],
1415
2368
  isSpace: true
1416
2369
  });
1417
- else if (m[2] !== void 0) for (const piece of splitCjkSegment(m[2])) out.push({
2370
+ else if (m[3] !== void 0) for (const piece of splitCjkSegment(m[3])) out.push({
1418
2371
  text: piece,
1419
2372
  isSpace: false
1420
2373
  });
@@ -1428,14 +2381,51 @@ function lastCodePoint(s) {
1428
2381
  function paragraphItemStream(tokens, hyphenator, maxLineWidthPt) {
1429
2382
  const entries = [];
1430
2383
  let prevBoxEndCp = null;
2384
+ let prevWasImage = false;
1431
2385
  for (const tok of tokens) {
2386
+ const breaks = tok.isSpace ? tok.text.match(/\r\n|[\r\n]/gu)?.length ?? 0 : 0;
2387
+ if (breaks > 0) {
2388
+ for (let i = 0; i < breaks; i++) {
2389
+ entries.push({
2390
+ item: {
2391
+ type: "glue",
2392
+ width: 0,
2393
+ stretch: 1e6,
2394
+ shrink: 0
2395
+ },
2396
+ token: null
2397
+ });
2398
+ entries.push({
2399
+ item: {
2400
+ type: "penalty",
2401
+ width: 0,
2402
+ penalty: FORCED_BREAK,
2403
+ flagged: false
2404
+ },
2405
+ token: null,
2406
+ softBreak: true
2407
+ });
2408
+ }
2409
+ prevBoxEndCp = null;
2410
+ continue;
2411
+ }
1432
2412
  if (tok.isSpace || tok.kind === "image" || tok.kind === "math") {
2413
+ if (tok.kind === "image" && (prevWasImage || prevBoxEndCp !== null)) entries.push({
2414
+ item: {
2415
+ type: "penalty",
2416
+ width: 0,
2417
+ penalty: 0,
2418
+ flagged: false
2419
+ },
2420
+ token: null
2421
+ });
2422
+ prevWasImage = tok.kind === "image";
1433
2423
  entries.push({
1434
2424
  item: tok.isSpace ? {
1435
2425
  type: "glue",
1436
2426
  width: tok.widthPt,
1437
2427
  stretch: tok.widthPt * .6,
1438
- shrink: tok.widthPt * .3
2428
+ shrink: tok.widthPt * GLUE_SHRINK_RATIO
1439
2429
  } : {
1440
2430
  type: "box",
1441
2431
  width: tok.widthPt
@@ -1446,7 +2436,7 @@ function paragraphItemStream(tokens, hyphenator, maxLineWidthPt) {
1446
2436
  continue;
1447
2437
  }
1448
2438
  const startCp = tok.text.codePointAt(0) ?? 0;
1449
- if (prevBoxEndCp !== null && cjkBreakBetween(prevBoxEndCp, startCp)) entries.push({
2439
+ if (prevWasImage || prevBoxEndCp !== null && cjkBreakBetween(prevBoxEndCp, startCp)) entries.push({
1450
2440
  item: {
1451
2441
  type: "penalty",
1452
2442
  width: 0,
@@ -1455,10 +2445,12 @@ function paragraphItemStream(tokens, hyphenator, maxLineWidthPt) {
1455
2445
  },
1456
2446
  token: null
1457
2447
  });
2448
+ prevWasImage = false;
1458
2449
  prevBoxEndCp = lastCodePoint(tok.text);
1459
2450
  const positions = hyphenator ? hyphenator.hyphenate(tok.text) : [];
1460
2451
  if (positions.length === 0) {
1461
- const chunks = maxLineWidthPt !== void 0 && maxLineWidthPt > 0 && tok.widthPt > maxLineWidthPt * 1.1 ? splitToWidth(tok, maxLineWidthPt) : void 0;
2452
+ const tooWide = maxLineWidthPt !== void 0 && maxLineWidthPt > 0;
2453
+ const chunks = (tooWide && tok.widthPt > maxLineWidthPt ? splitAtHyphens(tok) : void 0) ?? (tooWide && tok.widthPt > maxLineWidthPt * 1.1 ? splitToWidth(tok, maxLineWidthPt) : void 0);
1462
2454
  if (chunks) {
1463
2455
  chunks.forEach((chunk, ci) => {
1464
2456
  if (ci > 0) entries.push({
@@ -1545,6 +2537,94 @@ function paragraphItemStream(tokens, hyphenator, maxLineWidthPt) {
1545
2537
  });
1546
2538
  return entries;
1547
2539
  }
2540
+ /** Word's own default when a document names none (§17.15.1.25): half an inch. */
2541
+ var DEFAULT_TAB_STOP_PT = 36;
2542
+ var TAB_LEADER_CHARS = new Map([
2543
+ ["dot", "."],
2544
+ ["hyphen", "-"],
2545
+ ["underscore", "_"],
2546
+ ["middleDot", "·"]
2547
+ ]);
2548
+ /**
2549
+ * Give each tab on a line the width that carries it to its stop, and the
2550
+ * leader that fills the way there.
2551
+ *
2552
+ * §17.3.1.37 — a tab advances to a POSITION, not by a fixed amount, and where
2553
+ * that position is depends on how far the line has already run. Measured as
2554
+ * ordinary whitespace it collapsed to a space: the page numbers of
2555
+ * FDO77715.docx's index sat against their titles with no dot leader between,
2556
+ * and its header ran its left and right halves together on one line.
2557
+ *
2558
+ * A `right`, `center` or `decimal` stop positions the text AFTER the tab, so
2559
+ * each is resolved against the segment that follows it.
2560
+ *
2561
+ * @param tokens The line's tokens, replaced in place.
2562
+ * @param resolved The paragraph's resolved properties: the stops and indents.
2563
+ * @param isFirst Whether this is the paragraph's first line.
2564
+ */
2565
+ function resolveTabs(tokens, resolved, isFirst, availableWidthPt) {
2566
+ const stops = resolved.tabs;
2567
+ let x = resolved.indentLeft + (isFirst ? resolved.indentFirstLine : 0);
2568
+ for (let i = 0; i < tokens.length; i++) {
2569
+ const tok = tokens[i];
2570
+ if (!tok) continue;
2571
+ if (!(tok.kind === "text" && tok.tab === true)) {
2572
+ x += tok.widthPt;
2573
+ continue;
2574
+ }
2575
+ let segment = 0;
2576
+ let decimalAt = 0;
2577
+ let seenDecimal = false;
2578
+ for (let j = i + 1; j < tokens.length; j++) {
2579
+ const next = tokens[j];
2580
+ if (next.kind === "text" && next.tab === true) break;
2581
+ if (!seenDecimal && next.kind === "text") {
2582
+ const dot = next.text.indexOf(".");
2583
+ if (dot >= 0) {
2584
+ decimalAt = segment + next.font.measure.textWidthPt(next.text.slice(0, dot), next.fontSizePt);
2585
+ seenDecimal = true;
2586
+ }
2587
+ }
2588
+ segment += next.widthPt;
2589
+ }
2590
+ const stopAt = (sp) => sp.relativeTo === "center" ? availableWidthPt / 2 : sp.relativeTo === "right" ? availableWidthPt : sp.positionPt;
2591
+ const stop = stops.find((sp) => stopAt(sp) > x + .01);
2592
+ const hangingStop = isFirst && resolved.indentFirstLine < 0 && resolved.indentLeft > x + .01 ? resolved.indentLeft : void 0;
2593
+ const explicitAt = stop ? stopAt(stop) : void 0;
2594
+ const position = explicitAt !== void 0 && (hangingStop === void 0 || explicitAt <= hangingStop) ? explicitAt : hangingStop ?? (Math.floor(x / DEFAULT_TAB_STOP_PT) + 1) * DEFAULT_TAB_STOP_PT;
2595
+ const alignment = stop?.alignment ?? "left";
2596
+ if (segment === 0 && alignment !== "left") {
2597
+ const after = tokens[i + 1];
2598
+ if (after?.kind === "text" && after.tab === true) {
2599
+ tokens[i] = {
2600
+ ...tok,
2601
+ widthPt: 0,
2602
+ text: ""
2603
+ };
2604
+ continue;
2605
+ }
2606
+ }
2607
+ let target = position;
2608
+ if (alignment === "right") target = position - segment;
2609
+ else if (alignment === "center") target = position - segment / 2;
2610
+ else if (alignment === "decimal") target = position - (seenDecimal ? decimalAt : segment);
2611
+ const width = Math.max(0, target - x);
2612
+ const leaderChar = stop?.leader === void 0 ? void 0 : TAB_LEADER_CHARS.get(stop.leader);
2613
+ const text = leaderChar === void 0 ? "" : fillLeader(tok, leaderChar, width);
2614
+ tokens[i] = {
2615
+ ...tok,
2616
+ widthPt: width,
2617
+ text
2618
+ };
2619
+ x += width;
2620
+ }
2621
+ }
2622
+ /** As many leader characters as fit the gap, measured in the tab's own font. */
2623
+ function fillLeader(tab, leader, widthPt) {
2624
+ const unit = tab.font.measure.textWidthPt(leader, tab.fontSizePt);
2625
+ if (!(unit > 0)) return "";
2626
+ return leader.repeat(Math.max(0, Math.floor(widthPt / unit)));
2627
+ }
1548
2628
  /**
1549
2629
  * Cut an unbreakable word into the widest pieces that fit `widthPt`, measured
1550
2630
  * with the token's own font. Each piece keeps the run's formatting, so the line
@@ -1555,6 +2635,24 @@ function paragraphItemStream(tokens, hyphenator, maxLineWidthPt) {
1555
2635
  * @returns The pieces in order, or undefined when even one character overruns
1556
2636
  * (nothing can be gained by splitting then).
1557
2637
  */
2638
+ function splitAtHyphens(tok) {
2639
+ if (tok.kind !== "text") return void 0;
2640
+ const parts = [];
2641
+ let start = 0;
2642
+ for (let i = 0; i < tok.text.length; i++) {
2643
+ if (!HYPHENS.has(tok.text.codePointAt(i) ?? 0)) continue;
2644
+ if (i === start || i === tok.text.length - 1) continue;
2645
+ parts.push(tok.text.slice(start, i + 1));
2646
+ start = i + 1;
2647
+ }
2648
+ if (parts.length === 0) return void 0;
2649
+ parts.push(tok.text.slice(start));
2650
+ return parts.map((text) => ({
2651
+ ...tok,
2652
+ text,
2653
+ widthPt: tok.font.measure.textWidthPt(text, tok.fontSizePt)
2654
+ }));
2655
+ }
1558
2656
  function splitToWidth(tok, widthPt) {
1559
2657
  if (tok.kind !== "text") return void 0;
1560
2658
  const chars = [...tok.text];
@@ -1585,18 +2683,19 @@ function splitToWidth(tok, widthPt) {
1585
2683
  }
1586
2684
  /** The fewest characters a mid-word break may leave on a line. */
1587
2685
  var MIN_SPLIT_CHARS = 6;
1588
- function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, resolved, profile) {
2686
+ function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, resolved, profile, allowEmpty = false) {
1589
2687
  let st = start;
1590
2688
  let et = breakIdx;
1591
2689
  while (st < et && (entries[st].token === null || !isFirst && entries[st].token?.isSpace === true)) st++;
1592
2690
  const paints = (t) => t !== null && t !== void 0 && t.kind === "text" && (t.resolvedRun.underline !== "none" || t.resolvedRun.strike);
1593
2691
  while (et > st && (entries[et - 1].token === null || entries[et - 1].token?.isSpace === true && !paints(entries[et - 1].token))) et--;
1594
- if (st >= et) return null;
2692
+ if (st >= et && !allowEmpty) return null;
1595
2693
  const lineTokens = [];
1596
2694
  for (let i = st; i < et; i++) {
1597
2695
  const ft = entries[i].token;
1598
2696
  if (ft) lineTokens.push(ft);
1599
2697
  }
2698
+ resolveTabs(lineTokens, resolved, isFirst, availableWidthPt);
1600
2699
  const hyphenWidth = entries[breakIdx]?.hyphenWidthPt;
1601
2700
  if (hyphenWidth !== void 0) {
1602
2701
  const lastIdx = lineTokens.length - 1;
@@ -1616,8 +2715,10 @@ function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, reso
1616
2715
  let metricAscent = 0;
1617
2716
  let metricDescent = 0;
1618
2717
  let metricLineGap = 0;
2718
+ const prints = lineTokens.some((t) => !t.isSpace);
1619
2719
  for (const t of lineTokens) {
1620
2720
  contentWidth += t.widthPt;
2721
+ if (prints && t.isSpace) continue;
1621
2722
  const sz = tokenLineSize(t);
1622
2723
  if (sz > maxSize) maxSize = sz;
1623
2724
  if (t.kind === "math") {
@@ -1647,10 +2748,26 @@ function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, reso
1647
2748
  } : {}
1648
2749
  };
1649
2750
  }
1650
- function wrap(tokens, firstLineWidth, otherWidth, resolved, hyphenator, profile, lineWidths) {
2751
+ function withLeadingTabWidths(tokens, resolved, firstLineWidth) {
2752
+ const first = tokens[0];
2753
+ if (!(first?.kind === "text" && first.tab === true)) return tokens;
2754
+ const probe = tokens.map((t) => ({ ...t }));
2755
+ resolveTabs(probe, resolved, true, firstLineWidth);
2756
+ const out = tokens.slice();
2757
+ for (let i = 0; i < out.length; i++) {
2758
+ const tok = out[i];
2759
+ if (!(tok.kind === "text" && tok.tab === true)) break;
2760
+ out[i] = {
2761
+ ...tok,
2762
+ widthPt: probe[i].widthPt
2763
+ };
2764
+ }
2765
+ return out;
2766
+ }
2767
+ function wrap(tokens, firstLineWidth, otherWidth, resolved, hyphenator, profile, noExpandShiftReturn, lineWidths) {
1651
2768
  if (tokens.length === 0) return [];
1652
2769
  const widths = lineWidths ?? [firstLineWidth, otherWidth];
1653
- const entries = paragraphItemStream(tokens, hyphenator, Math.max(0, ...widths));
2770
+ const entries = paragraphItemStream(withLeadingTabWidths(tokens, resolved, widths[0] ?? firstLineWidth), hyphenator, Math.max(0, ...widths));
1654
2771
  const items = entries.map((e) => e.item);
1655
2772
  const breaks = profile === "ream" ? breakLines(items, widths).breaks : greedyBreakLines(items, widths);
1656
2773
  const buildLines = (at) => {
@@ -1658,10 +2775,13 @@ function wrap(tokens, firstLineWidth, otherWidth, resolved, hyphenator, profile,
1658
2775
  let start = 0;
1659
2776
  let isFirst = true;
1660
2777
  let lineIdx = 0;
1661
- for (const breakIdx of at) {
2778
+ for (const [i, breakIdx] of at.entries()) {
1662
2779
  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);
2780
+ const line = lineFromRange(entries, start, breakIdx, width, isFirst, resolved, profile, i < at.length - 1);
2781
+ if (line) out.push(noExpandShiftReturn && entries[breakIdx]?.softBreak ? {
2782
+ ...line,
2783
+ noJustify: true
2784
+ } : line);
1665
2785
  start = breakIdx + 1;
1666
2786
  isFirst = false;
1667
2787
  lineIdx++;
@@ -1695,11 +2815,17 @@ function fontLeadingPt(parsed, sizePt, profile) {
1695
2815
  };
1696
2816
  }
1697
2817
  function computeLineHeight(line, p) {
2818
+ const natural = naturalLineHeight(line, p);
2819
+ const pitch = p.gridLinePitchPt;
2820
+ if (pitch !== void 0 && pitch > 0) return Math.ceil(natural / pitch - 1e-6) * pitch;
2821
+ return natural;
2822
+ }
2823
+ function naturalLineHeight(line, p) {
1698
2824
  const fontSize = line.maxFontSizePt || 12;
1699
2825
  const natural = line.metricHeightPt ?? fontSize * 1.2;
1700
2826
  const mathH = (line.mathAscentPt ?? 0) + (line.mathDescentPt ?? 0);
1701
2827
  const mathNeed = mathH > 0 ? mathH * 1.05 : 0;
1702
- if (p.spacingLineRule === "exact" && p.spacingLine > 0) return Math.max(p.spacingLine, mathNeed);
2828
+ if (p.spacingLineRule === "exact" && p.spacingLine >= 0) return Math.max(p.spacingLine, mathNeed);
1703
2829
  if (p.spacingLineRule === "atLeast" && p.spacingLine > 0) return Math.max(natural, p.spacingLine, mathNeed);
1704
2830
  const lineTwips = Math.round(p.spacingLine * 20);
1705
2831
  const multiple = lineTwips > 0 ? lineTwips / 240 : 1;
@@ -1710,6 +2836,37 @@ function lineDescent(line) {
1710
2836
  const base = line.metricDescentPt ?? fs * .2;
1711
2837
  return Math.max(base, line.mathDescentPt ?? 0);
1712
2838
  }
2839
+ /**
2840
+ * §17.3.1.33 — how far the baseline stands above the BOTTOM of the line box,
2841
+ * which is what says where the extra height of a spaced-out line goes.
2842
+ *
2843
+ * A line given a fixed height (`exact`, `atLeast`) hangs from the bottom of its
2844
+ * box: the space a taller `w:line` asks for opens ABOVE the text, which is what
2845
+ * makes "at least 24pt" push a heading down its own line.
2846
+ *
2847
+ * `auto` — Word's "Multiple", the 1.15 every modern template sets — scales the
2848
+ * line instead, and the space it gains falls BELOW: the first line of a spaced
2849
+ * paragraph starts exactly where a single-spaced one does. Read the other way
2850
+ * round, fdo80800.docx's 1.5-line cells sat 6.6pt under the plain cell beside
2851
+ * them, where Word and LibreOffice write all three on one baseline.
2852
+ *
2853
+ * §17.6.5 — the extra a document GRID adds is not the paragraph's at all, and
2854
+ * is split evenly: the line is centred on its grid row.
2855
+ *
2856
+ * @param line The laid-out line.
2857
+ * @param p The owning paragraph's resolved properties.
2858
+ * @returns The distance from the line box's bottom edge up to the baseline.
2859
+ */
2860
+ function lineBaselineOffset(line, p) {
2861
+ const descent = lineDescent(line);
2862
+ const natural = naturalLineHeight(line, p);
2863
+ const singled = naturalLineHeight(line, {
2864
+ ...p,
2865
+ spacingLineRule: "auto",
2866
+ spacingLine: pt(0)
2867
+ });
2868
+ return descent + (p.spacingLineRule === "auto" ? Math.max(0, natural - singled) : 0) + (computeLineHeight(line, p) - natural) / 2;
2869
+ }
1713
2870
  function alignmentOffset(alignment, lineWidth, available) {
1714
2871
  if (alignment === "right") return Math.max(0, available - lineWidth);
1715
2872
  if (alignment === "center") return Math.max(0, (available - lineWidth) / 2);
@@ -1724,7 +2881,7 @@ function layoutTableBlock(table, options, fontResources, imageResources, content
1724
2881
  for (let r = 0; r < table.rows.length; r++) {
1725
2882
  const rl = layoutTableRow(table.rows[r], r, table.rows.length, colCount, table.properties, columnWidthsPt, options, fontResources, imageResources, mergeRoles[r] ?? [], aboveBordersByCol);
1726
2883
  const nextAbove = [];
1727
- let ci = 0;
2884
+ let ci = table.rows[r].properties.gridBefore ?? 0;
1728
2885
  for (const cell of table.rows[r].cells) {
1729
2886
  const span = Math.max(1, cell.properties.colSpan ?? 1);
1730
2887
  for (let k = 0; k < span; k++) nextAbove[ci + k] = cell.properties.borders;
@@ -1738,17 +2895,46 @@ function layoutTableBlock(table, options, fontResources, imageResources, content
1738
2895
  ...rp.pageBreakBefore ? { breakBefore: true } : {}
1739
2896
  } : rl);
1740
2897
  }
2898
+ growRowsForMerges(rows);
1741
2899
  const heightPt = rows.reduce((s, r) => s + r.heightPt, 0);
1742
2900
  const totalWidthPt = columnWidthsPt.reduce((s, w) => s + w, 0);
2901
+ const xOffsetPt = (table.properties.indentPt ?? 0) + tableXOffset(table.properties.alignment, contentWidth, totalWidthPt);
1743
2902
  return {
1744
2903
  kind: "table",
2904
+ ...table.properties.float ? { float: table.properties.float } : {},
1745
2905
  rows,
1746
2906
  heightPt,
1747
2907
  totalWidthPt,
1748
2908
  colCount,
1749
- xOffsetPt: tableXOffset(table.properties.alignment, contentWidth, totalWidthPt)
2909
+ xOffsetPt
1750
2910
  };
1751
2911
  }
2912
+ /**
2913
+ * §17.4.85 `w:vMerge` — make every vertical merge fit. Each row is already as
2914
+ * tall as its own (unmerged) cells; a merge that needs more than its rows add
2915
+ * up to grows the LAST row of the run, which is where Word puts the slack.
2916
+ *
2917
+ * @param rows The table's laid-out rows, adjusted in place.
2918
+ */
2919
+ function growRowsForMerges(rows) {
2920
+ for (let r = 0; r < rows.length; r++) for (const cell of rows[r].cells) {
2921
+ if (cell.mergeRole !== "start") continue;
2922
+ let last = r;
2923
+ for (let k = r + 1; k < rows.length; k++) {
2924
+ const below = rows[k].cells.find((c) => c.colStart === cell.colStart);
2925
+ if (!below || below.mergeRole !== "middle" && below.mergeRole !== "end") break;
2926
+ last = k;
2927
+ if (below.mergeRole === "end") break;
2928
+ }
2929
+ let have = 0;
2930
+ for (let k = r; k <= last; k++) have += rows[k].heightPt;
2931
+ const deficit = cell.totalHeightPt - have;
2932
+ if (deficit > .01) rows[last] = {
2933
+ ...rows[last],
2934
+ heightPt: rows[last].heightPt + deficit
2935
+ };
2936
+ }
2937
+ }
1752
2938
  function tableXOffset(alignment, contentWidth, totalWidthPt) {
1753
2939
  const slack = contentWidth - totalWidthPt;
1754
2940
  if (slack <= 0 || !alignment || alignment === "left") return 0;
@@ -1757,15 +2943,18 @@ function tableXOffset(alignment, contentWidth, totalWidthPt) {
1757
2943
  function computeColumnWidths(table, options, fontResources, contentWidth) {
1758
2944
  let colCount = table.grid.length;
1759
2945
  for (const row of table.rows) {
1760
- let rowCols = 0;
2946
+ let rowCols = row.properties.gridBefore ?? 0;
1761
2947
  for (const cell of row.cells) rowCols += Math.max(1, cell.properties.colSpan ?? 1);
1762
2948
  if (rowCols > colCount) colCount = rowCols;
1763
2949
  }
1764
2950
  if (colCount === 0) return [];
1765
- if (table.properties.layout === "fixed") return gridWidthsScaled(table, contentWidth, colCount);
2951
+ const gridShort = gridColTwips(table).length < colCount;
2952
+ const declared = declaredColumnWidths(table, colCount, contentWidth, gridShort);
2953
+ if (declared) return declared;
2954
+ if (gridColTwips(table).some((w) => w > 0)) return gridWidthsScaled(table, contentWidth, colCount);
1766
2955
  const colNaturalWidths = new Array(colCount).fill(0);
1767
2956
  for (const row of table.rows) {
1768
- let colIdx = 0;
2957
+ let colIdx = row.properties.gridBefore ?? 0;
1769
2958
  for (let i = 0; i < row.cells.length; i++) {
1770
2959
  const cell = row.cells[i];
1771
2960
  const span = Math.max(1, cell.properties.colSpan ?? 1);
@@ -1798,6 +2987,81 @@ function computeColumnWidths(table, options, fontResources, contentWidth) {
1798
2987
  function gridColTwips(table) {
1799
2988
  return table.grid.map((w) => Math.round(w * 20));
1800
2989
  }
2990
+ /**
2991
+ * §17.4.53 — column widths taken from the cells' own `w:tcW` for a table laid
2992
+ * out `fixed`. Each cell shares its preferred width over the columns it spans
2993
+ * and the widest claim wins; a column nobody declares keeps its grid share.
2994
+ * Returns `undefined` (⇒ the grid decides) unless the table says `fixed` and
2995
+ * enough of it is declared to trust.
2996
+ */
2997
+ function declaredColumnWidths(table, colCount, contentWidth, gridShort = false) {
2998
+ if (table.properties.layout !== "fixed" && !gridShort) return void 0;
2999
+ const target = totalTableTarget(table, contentWidth);
3000
+ const declaredWidth = (cell) => {
3001
+ const p = cell.properties;
3002
+ if (p.widthFraction !== void 0) return p.widthFraction * target;
3003
+ if (p.width !== void 0 && p.widthType !== "auto" && p.widthType !== "nil") return p.width;
3004
+ };
3005
+ const claimed = new Array(colCount).fill(0);
3006
+ const spanning = [];
3007
+ let anyDeclared = false;
3008
+ let reach = 0;
3009
+ for (const row of table.rows) {
3010
+ if (row.cells.length < 2) continue;
3011
+ let span = row.properties.gridBefore ?? 0;
3012
+ for (const cell of row.cells) span += Math.max(1, cell.properties.colSpan ?? 1);
3013
+ if (span > reach) reach = span;
3014
+ }
3015
+ const phantomFrom = reach > 0 && reach < colCount ? reach : colCount;
3016
+ for (const row of table.rows) {
3017
+ let colIdx = row.properties.gridBefore ?? 0;
3018
+ for (const cell of row.cells) {
3019
+ const span = Math.max(1, cell.properties.colSpan ?? 1);
3020
+ const widthPt = declaredWidth(cell);
3021
+ if (widthPt !== void 0 && widthPt > 0) {
3022
+ anyDeclared = true;
3023
+ if (span === 1) {
3024
+ if (widthPt > claimed[colIdx]) claimed[colIdx] = widthPt;
3025
+ } else spanning.push({
3026
+ start: colIdx,
3027
+ span,
3028
+ widthPt
3029
+ });
3030
+ }
3031
+ colIdx += span;
3032
+ }
3033
+ }
3034
+ if (!anyDeclared) return void 0;
3035
+ for (const s of spanning) {
3036
+ const end = Math.min(s.start + s.span, phantomFrom);
3037
+ let have = 0;
3038
+ const blank = [];
3039
+ for (let i = s.start; i < end; i++) {
3040
+ have += claimed[i];
3041
+ if (claimed[i] === 0) blank.push(i);
3042
+ }
3043
+ const deficit = s.widthPt - have;
3044
+ if (deficit <= 0) continue;
3045
+ if (blank.length > 0) {
3046
+ const each = deficit / blank.length;
3047
+ for (const i of blank) claimed[i] = each;
3048
+ } else if (have > 0) for (let i = s.start; i < end; i++) claimed[i] = claimed[i] * (s.widthPt / have);
3049
+ }
3050
+ const gridTwips = gridColTwips(table);
3051
+ const rest = [];
3052
+ let claimedTotal = 0;
3053
+ for (let i = 0; i < phantomFrom; i++) if (claimed[i] > 0) claimedTotal += claimed[i];
3054
+ else rest.push(i);
3055
+ if (rest.length > 0) {
3056
+ const leftover = Math.max(0, target - claimedTotal);
3057
+ const restGrid = rest.reduce((s, i) => s + (gridTwips[i] ?? 0), 0);
3058
+ for (const i of rest) claimed[i] = restGrid > 0 ? (gridTwips[i] ?? 0) / restGrid * leftover : leftover / rest.length;
3059
+ }
3060
+ const total = claimed.reduce((s, w) => s + w, 0);
3061
+ if (total <= 0) return void 0;
3062
+ const scale = total > target ? target / total : 1;
3063
+ return claimed.map((w) => w * scale);
3064
+ }
1801
3065
  function gridWidthsScaled(table, contentWidth, colCount) {
1802
3066
  const gridTwips = gridColTwips(table);
1803
3067
  const totalGridTwips = gridTwips.reduce((s, w) => s + w, 0);
@@ -1860,8 +3124,9 @@ function aboveOfSpan(aboveBordersByCol, colIdx, span) {
1860
3124
  function layoutTableRow(row, rowIdx, rowCount, colCount, tableProps, columnWidthsPt, options, fontResources, imageResources, rowMergeRoles, aboveBordersByCol) {
1861
3125
  const cells = [];
1862
3126
  const columnXOffsets = [];
1863
- let cursorX = 0;
1864
- let colIdx = 0;
3127
+ const skip = Math.min(Math.max(0, row.properties.gridBefore ?? 0), columnWidthsPt.length);
3128
+ let cursorX = columnWidthsPt.slice(0, skip).reduce((sum, w) => sum + w, 0);
3129
+ let colIdx = skip;
1865
3130
  for (let cellIdx = 0; cellIdx < row.cells.length; cellIdx++) {
1866
3131
  const cell = row.cells[cellIdx];
1867
3132
  const span = Math.max(1, cell.properties.colSpan ?? 1);
@@ -1882,7 +3147,10 @@ function layoutTableRow(row, rowIdx, rowCount, colCount, tableProps, columnWidth
1882
3147
  colIdx += span;
1883
3148
  }
1884
3149
  let heightPt = 0;
1885
- for (const c of cells) if (c.totalHeightPt > heightPt) heightPt = c.totalHeightPt;
3150
+ for (const c of cells) {
3151
+ if (c.mergeRole === "start") continue;
3152
+ if (c.totalHeightPt > heightPt) heightPt = c.totalHeightPt;
3153
+ }
1886
3154
  if (row.properties.height && row.properties.heightRule === "exact") {
1887
3155
  heightPt = row.properties.height;
1888
3156
  return {
@@ -1930,6 +3198,30 @@ function clipCellToHeight(cell, heightPt) {
1930
3198
  contentHeightPt: used
1931
3199
  };
1932
3200
  }
3201
+ /**
3202
+ * The one run a picture BLOCK is made of, so a context that lays out paragraphs
3203
+ * (a table cell) can draw it without a block path of its own.
3204
+ *
3205
+ * @param image The image block.
3206
+ * @returns A run carrying it as an inline picture.
3207
+ */
3208
+ function imageRun(image) {
3209
+ return {
3210
+ text: "",
3211
+ properties: {},
3212
+ inlineImage: {
3213
+ ...image.resource ? { resource: image.resource } : {},
3214
+ ...image.outline ? { outline: image.outline } : {},
3215
+ width: image.width,
3216
+ height: image.height,
3217
+ ...image.crop ? { crop: image.crop } : {},
3218
+ ...image.rotation60k ? { rotation60k: image.rotation60k } : {},
3219
+ ...image.flipH ? { flipH: true } : {},
3220
+ ...image.flipV ? { flipV: true } : {},
3221
+ ...image.effectExtent ? { effectExtent: image.effectExtent } : {}
3222
+ }
3223
+ };
3224
+ }
1933
3225
  function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imageResources, rowIdx, colStart, colSpan, rowCount, colCount, mergeRole, leftNeighborBorders, aboveNeighborBorders) {
1934
3226
  const cellMar = cell.properties.margins;
1935
3227
  const tableMar = tableProps.defaultCellMargins;
@@ -1942,8 +3234,18 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
1942
3234
  const innerWidth = Math.max(1, widthPt - padLeftPt - padRightPt);
1943
3235
  const lines = [];
1944
3236
  const nestedTables = [];
3237
+ const cellShapes = [];
3238
+ const lineGaps = /* @__PURE__ */ new Map();
1945
3239
  let contentHeightPt = 0;
1946
3240
  let clipped = false;
3241
+ let pendingGapPt = 0;
3242
+ const openParagraph = (spacingBeforePt) => {
3243
+ const gap = pendingGapPt + spacingBeforePt;
3244
+ pendingGapPt = 0;
3245
+ if (gap <= 0) return;
3246
+ lineGaps.set(lines.length, (lineGaps.get(lines.length) ?? 0) + gap);
3247
+ contentHeightPt += gap;
3248
+ };
1947
3249
  if (mergeRole !== "middle" && mergeRole !== "end") {
1948
3250
  for (const el of cell.content) if (el.kind === "paragraph") {
1949
3251
  const noWrap = cell.properties.noWrap === true;
@@ -1951,16 +3253,43 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
1951
3253
  const cutAt = widthPt - padLeftPt;
1952
3254
  if (noWrap && (block.lines[0]?.contentWidthPt ?? 0) > cutAt) clipped = true;
1953
3255
  const keep = noWrap ? clipLineToWidth(block.lines, cutAt, cell.properties.hashOnOverflow === true) : block.lines;
3256
+ const markOnly = cell.properties.hideMark === true && el.paragraph.runs.every((r) => r.text === "");
3257
+ if (!markOnly) openParagraph(block.spacingBeforePt);
1954
3258
  for (const line of keep) {
1955
3259
  lines.push(line);
1956
- contentHeightPt += computeLineHeight(line, block.resolved);
3260
+ if (!markOnly) contentHeightPt += computeLineHeight(line, block.resolved);
1957
3261
  }
1958
- contentHeightPt += block.spacingAfterPt;
3262
+ if (!markOnly) pendingGapPt = block.spacingAfterPt;
1959
3263
  } else if (el.kind === "table") {
1960
3264
  const nested = layoutTableBlock(el.table, options, fontResources, imageResources, innerWidth);
1961
- nestedTables.push(nested);
3265
+ nestedTables.push({
3266
+ block: nested,
3267
+ afterLine: lines.length
3268
+ });
1962
3269
  contentHeightPt += nested.heightPt;
3270
+ } else if (el.kind === "image" && !isOutOfFlowFloat(el.image.float)) {
3271
+ const block = layoutParagraphBlock({
3272
+ properties: el.image.paragraphProperties,
3273
+ runs: [imageRun(el.image)]
3274
+ }, options, fontResources, imageResources, innerWidth);
3275
+ openParagraph(block.spacingBeforePt);
3276
+ for (const line of block.lines) {
3277
+ lines.push(line);
3278
+ contentHeightPt += computeLineHeight(line, block.resolved);
3279
+ }
3280
+ contentHeightPt += block.spacingAfterPt;
3281
+ pendingGapPt = block.spacingAfterPt;
3282
+ } else if (el.kind === "shape" && !isOutOfFlowFloat(el.shape.float)) {
3283
+ const block = layoutShapeBlock(el.shape, options, fontResources, imageResources, innerWidth);
3284
+ openParagraph(block.spacingBeforePt);
3285
+ cellShapes.push({
3286
+ block,
3287
+ afterLine: lines.length
3288
+ });
3289
+ contentHeightPt += block.heightPt;
3290
+ pendingGapPt = block.spacingAfterPt;
1963
3291
  }
3292
+ contentHeightPt += pendingGapPt;
1964
3293
  }
1965
3294
  const colEnd = colStart + colSpan - 1;
1966
3295
  const borders = resolveCellBorders(cell.properties.borders, tableProps.borders, rowIdx, colStart, colEnd, rowCount, colCount, mergeRole, leftNeighborBorders, aboveNeighborBorders);
@@ -1977,7 +3306,9 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
1977
3306
  ...cell.properties.icon ? { icon: cell.properties.icon } : {},
1978
3307
  ...cell.properties.sparkline ? { sparkline: cell.properties.sparkline } : {},
1979
3308
  lines,
3309
+ ...lineGaps.size > 0 ? { lineGaps } : {},
1980
3310
  ...nestedTables.length > 0 ? { nestedTables } : {},
3311
+ ...cellShapes.length > 0 ? { shapes: cellShapes } : {},
1981
3312
  contentHeightPt,
1982
3313
  totalHeightPt,
1983
3314
  ...cell.properties.verticalAlign ? { verticalAlign: cell.properties.verticalAlign } : {},
@@ -1992,6 +3323,7 @@ var BORDER_STYLE_RANK = {
1992
3323
  thick: 4,
1993
3324
  single: 3,
1994
3325
  dashed: 2,
3326
+ dashSmallGap: 2,
1995
3327
  dashDot: 2,
1996
3328
  dashDotDot: 2,
1997
3329
  dotted: 1,
@@ -2099,7 +3431,7 @@ function listItemParagraphNode(builder, stack, list, wantLbl) {
2099
3431
  * Push order into `current`/`pages` IS the byte order of the emitted PDF, so the
2100
3432
  * bodies move unchanged.
2101
3433
  */
2102
- var PageAssembler = class {
3434
+ var PageAssembler = class PageAssembler {
2103
3435
  /**
2104
3436
  * @param sectionCtxs The per-section render contexts, in order; assembly
2105
3437
  * starts on the first section's first page.
@@ -2109,20 +3441,29 @@ var PageAssembler = class {
2109
3441
  * `undefined` when there are none.
2110
3442
  * @param bookmarkPositions Out-param: bookmark name → its GoTo destination.
2111
3443
  */
2112
- constructor(sectionCtxs, builder, notes, bookmarkPositions) {
3444
+ constructor(sectionCtxs, builder, notes, bookmarkPositions, backgroundImageName) {
2113
3445
  this.sectionCtxs = sectionCtxs;
2114
3446
  this.builder = builder;
2115
3447
  this.notes = notes;
2116
3448
  this.bookmarkPositions = bookmarkPositions;
3449
+ this.backgroundImageName = backgroundImageName;
2117
3450
  this.ctx = sectionCtxs[0];
3451
+ this.pageStartCtx = this.ctx;
3452
+ this.pageNumber = this.ctx.properties.pageNumberStart ?? 1;
2118
3453
  this.cursorY = this.ctx.pageHeight - this.ctx.marginTop;
2119
3454
  this.colStartY = this.cursorY;
3455
+ this.bandTopY = this.cursorY;
2120
3456
  }
2121
3457
  pages = [];
2122
3458
  ctx;
2123
3459
  secIdx = 0;
2124
3460
  pageInSection = 0;
2125
3461
  globalPageIdx = 0;
3462
+ /**
3463
+ * §17.6.12 — the number the NEXT page prints for a PAGE field. Counts up with
3464
+ * the pages and restarts wherever a section names a `w:pgNumType w:start`.
3465
+ */
3466
+ pageNumber = 1;
2126
3467
  current = [];
2127
3468
  pendingPageBreak = false;
2128
3469
  cursorY;
@@ -2136,6 +3477,12 @@ var PageAssembler = class {
2136
3477
  colStartLen = 0;
2137
3478
  /** The cursor's y at the top of the current column — see {@link colHasContent}. */
2138
3479
  colStartY;
3480
+ /**
3481
+ * Where this page's column band begins. The top margin on an ordinary page;
3482
+ * further down when a §17.6.22 `continuous` section started mid-page, whose
3483
+ * columns run beside each other from there, not from the paper's top.
3484
+ */
3485
+ bandTopY;
2139
3486
  /** The current column's left edge (`marginLeft` plus the column x-offset). */
2140
3487
  colLeft = () => this.ctx.marginLeft + (this.ctx.columns?.[this.colIdx]?.xOffsetPt ?? 0);
2141
3488
  /** The current column's width (the section content width when single-column). */
@@ -2167,15 +3514,118 @@ var PageAssembler = class {
2167
3514
  if (this.ctx.columns && this.colIdx + 1 < this.ctx.columns.length) {
2168
3515
  this.colIdx++;
2169
3516
  this.colStartLen = this.current.length;
2170
- this.cursorY = this.ctx.pageHeight - this.ctx.marginTop;
3517
+ this.cursorY = this.bandTopY;
2171
3518
  this.colStartY = this.cursorY;
3519
+ const isLast = this.colIdx + 1 >= this.ctx.columns.length;
3520
+ this.balanceBottomY = this.balanceHeightPt > 0 && !isLast ? this.cursorY - this.balanceHeightPt : void 0;
2172
3521
  } else this.flushPage();
2173
3522
  };
2174
3523
  /**
3524
+ * §17.6.8 — the running line count for the margin numbers. Reset by the
3525
+ * section's own `w:restart` rule.
3526
+ */
3527
+ lineNumber = 0;
3528
+ /**
3529
+ * §17.6.8 — the margin number for the body line just placed, if the section
3530
+ * asks for one at this count. Pushed at the line's own baseline, right-
3531
+ * aligned a quarter inch off the text.
3532
+ *
3533
+ * @param baselineY The line's baseline, in page (y-down) coordinates.
3534
+ * @param line The line being numbered — its font draws the digits.
3535
+ */
3536
+ emitLineNumber = (baselineY, line) => {
3537
+ const ln = this.ctx.properties.lineNumbering;
3538
+ if (!ln) return;
3539
+ this.lineNumber++;
3540
+ const n = ln.start + this.lineNumber - 1;
3541
+ if ((n - ln.start + 1) % ln.countBy !== 0) return;
3542
+ const token = line.tokens.find((t) => t.kind === "text");
3543
+ if (!token) return;
3544
+ const text = String(n);
3545
+ const sizePt = token.fontSizePt;
3546
+ const widthPt = token.font.measure.textWidthPt(text, sizePt);
3547
+ const digits = {
3548
+ kind: "text",
3549
+ text,
3550
+ isSpace: false,
3551
+ resolvedRun: token.resolvedRun,
3552
+ font: token.font,
3553
+ fontSizePt: sizePt,
3554
+ widthPt,
3555
+ bidiLevel: 0
3556
+ };
3557
+ const numbered = {
3558
+ ...line,
3559
+ tokens: [digits],
3560
+ contentWidthPt: widthPt
3561
+ };
3562
+ this.current.push({
3563
+ type: "line",
3564
+ line: numbered,
3565
+ originX: pt(this.colLeft() - (ln.distancePt ?? LINE_NUMBER_GAP_PT) - widthPt),
3566
+ baselineY: pt(baselineY)
3567
+ });
3568
+ };
3569
+ /**
3570
+ * §17.6.12 — a section that names its own first page number restarts the
3571
+ * count there; one that names none carries on from the section before.
3572
+ *
3573
+ * @param next The section being entered.
3574
+ */
3575
+ restartPageNumbers = (next) => {
3576
+ const start = next.properties.pageNumberStart;
3577
+ if (start !== void 0) this.pageNumber = start;
3578
+ if ((next.properties.lineNumbering?.restart ?? "newPage") !== "continuous") this.lineNumber = 0;
3579
+ };
3580
+ /**
3581
+ * §17.6.22 — start a `continuous` section on the page in hand: its columns
3582
+ * begin where the section does, not at the top of the paper.
3583
+ *
3584
+ * @param next The section to continue into.
3585
+ */
3586
+ startBandSection = (next, bandHeightPt) => {
3587
+ this.secIdx++;
3588
+ this.ctx = next;
3589
+ this.restartPageNumbers(next);
3590
+ this.colIdx = 0;
3591
+ this.colStartLen = this.current.length;
3592
+ this.cursorY = Math.min(this.cursorY, next.pageHeight - next.marginTop);
3593
+ this.bandTopY = this.cursorY;
3594
+ this.colStartY = this.cursorY;
3595
+ this.beginBalancedBand(bandHeightPt);
3596
+ };
3597
+ /**
3598
+ * §17.6.4 — even the columns of the band that starts at the cursor: each gets
3599
+ * the same share of `bandHeightPt`. A band too tall for the page fills column
3600
+ * after column as before; there is nothing to even out when it will spill
3601
+ * anyway.
3602
+ *
3603
+ * @param bandHeightPt The height of everything the band holds.
3604
+ */
3605
+ beginBalancedBand = (bandHeightPt) => {
3606
+ const colCount = this.ctx.columns?.length ?? 1;
3607
+ const room = this.cursorY - (this.ctx.marginBottom + this.noteReserve);
3608
+ const share = colCount > 1 ? balancedColumnHeight(bandHeightPt, colCount, room) : 0;
3609
+ this.balanceHeightPt = share;
3610
+ this.balanceBottomY = share > 0 ? this.cursorY - share : void 0;
3611
+ };
3612
+ /**
2175
3613
  * §20.4.2.3 out-of-flow drawings (wrap `'none'`): they render at their anchored
2176
3614
  * position without moving the cursor. `behindDoc` sinks below the body text,
2177
3615
  * everything else above it; both flush with the page.
2178
3616
  */
3617
+ /**
3618
+ * §20.4.2.3 — a float layer, drawn in `relativeHeight` order. A stable sort
3619
+ * by z leaves the ones that state none where the document put them:
3620
+ * dml-rectangle-relsize.docx writes its blue bar first and gives it the
3621
+ * higher z, and drawn in document order the white rectangle covered it.
3622
+ */
3623
+ /** A float layer that stamps each item it takes with the float's z-order. */
3624
+ static zSink = (target, z) => z === void 0 ? target : { push: (...items) => target.push(...items.map((i) => ({
3625
+ ...i,
3626
+ z
3627
+ }))) };
3628
+ static byZ = (items) => items.every((i) => i.z === void 0) ? [...items] : [...items].sort((a, b) => (a.z ?? 0) - (b.z ?? 0));
2179
3629
  floatsBehind = [];
2180
3630
  floatsFront = [];
2181
3631
  /**
@@ -2183,12 +3633,26 @@ var PageAssembler = class {
2183
3633
  * must flow around. Page-scoped, like the float graphics.
2184
3634
  */
2185
3635
  exclusions = [];
3636
+ /**
3637
+ * Claim the area a side-wrapping float keeps to itself: its own box grown by
3638
+ * the §20.4.2.3 `distT/B/L/R` stand-off the anchor asks for.
3639
+ */
3640
+ excludeFloat = (f, x, topYUp, w, h) => {
3641
+ const d = f.wrapDist;
3642
+ this.exclusions.push({
3643
+ x0: x - (d?.leftPt ?? 0),
3644
+ x1: x + w + (d?.rightPt ?? 0),
3645
+ topYUp: topYUp + (d?.topPt ?? 0),
3646
+ bottomYUp: topYUp - h - (d?.bottomPt ?? 0)
3647
+ });
3648
+ };
2186
3649
  /** The float's left edge on the page, resolved from its horizontal anchor. */
2187
3650
  floatX = (f, widthPt) => {
2188
3651
  const h = f.posH;
2189
3652
  if (!h) return this.colLeft();
2190
- const base = h.relativeFrom === "page" ? 0 : h.relativeFrom === "column" ? this.colLeft() : this.ctx.marginLeft;
2191
- const span = h.relativeFrom === "page" ? this.ctx.pageWidth : h.relativeFrom === "column" ? this.colWidth() : this.ctx.contentWidth;
3653
+ const textRight = this.ctx.marginLeft + this.ctx.contentWidth;
3654
+ const base = h.relativeFrom === "page" || h.relativeFrom === "leftMargin" ? 0 : h.relativeFrom === "column" ? this.colLeft() : h.relativeFrom === "rightMargin" ? textRight : this.ctx.marginLeft;
3655
+ const span = h.relativeFrom === "page" ? this.ctx.pageWidth : h.relativeFrom === "column" ? this.colWidth() : h.relativeFrom === "leftMargin" ? this.ctx.marginLeft : h.relativeFrom === "rightMargin" ? this.ctx.pageWidth - textRight : this.ctx.contentWidth;
2192
3656
  if (h.align === "center") return base + (span - widthPt) / 2;
2193
3657
  if (h.align === "right") return base + span - widthPt;
2194
3658
  return base + (h.offsetPt ?? 0);
@@ -2197,10 +3661,19 @@ var PageAssembler = class {
2197
3661
  * The drawing's TOP in the y-up cursor frame. Paragraph/line-relative offsets
2198
3662
  * hang off the anchoring paragraph's current position.
2199
3663
  */
2200
- floatTopYUp = (f) => {
3664
+ floatTopYUp = (f, heightPt = 0) => {
2201
3665
  const v = f.posV;
2202
3666
  if (!v) return this.cursorY;
2203
- if (v.relativeFrom === "page") return this.ctx.pageHeight - (v.offsetPt ?? 0);
3667
+ if (v.align !== void 0) {
3668
+ const page = v.relativeFrom === "page";
3669
+ const topYUp = page ? this.ctx.pageHeight : this.ctx.pageHeight - this.ctx.marginTop;
3670
+ const bottomYUp = page ? 0 : this.ctx.marginBottom;
3671
+ if (v.align === "top") return topYUp;
3672
+ if (v.align === "bottom") return bottomYUp + heightPt;
3673
+ return bottomYUp + (topYUp - bottomYUp + heightPt) / 2;
3674
+ }
3675
+ if (v.relativeFrom === "page" || v.relativeFrom === "topMargin") return this.ctx.pageHeight - (v.offsetPt ?? 0);
3676
+ if (v.relativeFrom === "bottomMargin") return this.ctx.marginBottom - (v.offsetPt ?? 0);
2204
3677
  if (v.relativeFrom === "margin") return this.ctx.pageHeight - this.ctx.marginTop - (v.offsetPt ?? 0);
2205
3678
  return this.cursorY - (v.offsetPt ?? 0);
2206
3679
  };
@@ -2219,7 +3692,21 @@ var PageAssembler = class {
2219
3692
  noteReserve = 0;
2220
3693
  placedNotes = /* @__PURE__ */ new Set();
2221
3694
  /** The page's usable bottom: the margin plus whatever the notes band has claimed so far. */
2222
- bottomLimit = () => this.ctx.marginBottom + this.noteReserve;
3695
+ /**
3696
+ * §17.6.4 — where the current column must stop. Normally the page's bottom
3697
+ * margin (plus any footnote reserve); inside a BALANCED band it is the line
3698
+ * the band's share of the height reaches, so the columns come out even.
3699
+ */
3700
+ bottomLimit = () => Math.max(this.ctx.marginBottom + this.noteReserve, this.balanceBottomY ?? Number.NEGATIVE_INFINITY);
3701
+ /**
3702
+ * §17.6.4 — a `continuous` section of several columns whose content fits the
3703
+ * page is BALANCED: Word evens the columns out rather than filling the first
3704
+ * to the bottom. Set while such a band is being laid out; the height each
3705
+ * column is given is `balanceHeightPt`. IndexFieldFlagF.docx sets its index
3706
+ * that way and we filled column one and left the rest empty.
3707
+ */
3708
+ balanceBottomY;
3709
+ balanceHeightPt = 0;
2223
3710
  /**
2224
3711
  * The first exclusion a line spanning `[yTop-h, yTop]` collides with in the
2225
3712
  * current column (x-overlap with the column required).
@@ -2247,15 +3734,17 @@ var PageAssembler = class {
2247
3734
  const cl = this.colLeft();
2248
3735
  const leftRoom = e.x0 - FLOAT_TEXT_GAP - cl;
2249
3736
  const rightRoom = cl + full - (e.x1 + FLOAT_TEXT_GAP);
2250
- if (rightRoom >= leftRoom) {
2251
- const w = Math.max(MIN_WRAP_WIDTH, rightRoom);
2252
- return {
2253
- width: w,
2254
- xOffset: full - w
2255
- };
2256
- }
3737
+ if (Math.max(leftRoom, rightRoom) < MIN_WRAP_WIDTH) return {
3738
+ width: full,
3739
+ xOffset: 0,
3740
+ blockedUntilYUp: e.bottomYUp
3741
+ };
3742
+ if (rightRoom >= leftRoom) return {
3743
+ width: rightRoom,
3744
+ xOffset: full - rightRoom
3745
+ };
2257
3746
  return {
2258
- width: Math.max(MIN_WRAP_WIDTH, leftRoom),
3747
+ width: leftRoom,
2259
3748
  xOffset: 0
2260
3749
  };
2261
3750
  };
@@ -2274,9 +3763,15 @@ var PageAssembler = class {
2274
3763
  let y = startY;
2275
3764
  for (let i = 0; i < 200; i++) {
2276
3765
  const g = this.lineGeometryAt(y, h0);
3766
+ if (g.blockedUntilYUp !== void 0) {
3767
+ widths.push(this.colWidth());
3768
+ y = g.blockedUntilYUp - h0;
3769
+ if (y < this.bottomLimit()) break;
3770
+ continue;
3771
+ }
2277
3772
  widths.push(g.width);
2278
3773
  if (g.width < this.colWidth()) narrowed = true;
2279
- else if (i > 0) break;
3774
+ else if (narrowed) break;
2280
3775
  y -= h0;
2281
3776
  if (y < this.bottomLimit()) break;
2282
3777
  }
@@ -2328,7 +3823,7 @@ var PageAssembler = class {
2328
3823
  const noteNode = this.builder.create("Note", this.builder.root);
2329
3824
  structId = this.builder.create("P", noteNode).id;
2330
3825
  }
2331
- out.push(...drawBlocksSequentially(note.blocks, this.ctx.marginLeft, cursor, this.ctx.pageHeight, structId));
3826
+ out.push(...drawBlocksSequentially(note.blocks, this.ctx.marginLeft, cursor, this.ctx.pageHeight, this.ctx.contentWidth, structId));
2332
3827
  cursor -= note.heightPt;
2333
3828
  }
2334
3829
  return out;
@@ -2353,22 +3848,25 @@ var PageAssembler = class {
2353
3848
  const footer = pickBand(this.ctx.footerSet, band);
2354
3849
  if (header.renderDynamic) this.dynBands.push({
2355
3850
  pageIdx: this.pages.length,
2356
- pageNumber: this.globalPageIdx + 1,
3851
+ pageNumber: this.pageNumber,
2357
3852
  position: "header",
2358
3853
  render: header.renderDynamic
2359
3854
  });
2360
3855
  if (footer.renderDynamic) this.dynBands.push({
2361
3856
  pageIdx: this.pages.length,
2362
- pageNumber: this.globalPageIdx + 1,
3857
+ pageNumber: this.pageNumber,
2363
3858
  position: "footer",
2364
3859
  render: footer.renderDynamic
2365
3860
  });
2366
3861
  this.pages.push({
2367
3862
  commands: [
3863
+ ...this.pageBackgroundItems(),
3864
+ ...this.pageBorderItems(),
3865
+ ...this.columnSeparatorItems(),
2368
3866
  ...header.commands,
2369
- ...this.floatsBehind,
3867
+ ...PageAssembler.byZ(this.floatsBehind),
2370
3868
  ...this.current,
2371
- ...this.floatsFront,
3869
+ ...PageAssembler.byZ(this.floatsFront),
2372
3870
  ...this.renderNotesBand(),
2373
3871
  ...footer.commands
2374
3872
  ],
@@ -2385,35 +3883,212 @@ var PageAssembler = class {
2385
3883
  this.colStartLen = 0;
2386
3884
  this.pageInSection++;
2387
3885
  this.globalPageIdx++;
3886
+ this.pageNumber++;
3887
+ if ((this.ctx.properties.lineNumbering?.restart ?? "newPage") === "newPage") this.lineNumber = 0;
3888
+ this.cursorY = this.ctx.pageHeight - this.ctx.marginTop;
3889
+ this.colStartY = this.cursorY;
3890
+ this.bandTopY = this.cursorY;
3891
+ this.balanceBottomY = void 0;
3892
+ this.balanceHeightPt = 0;
3893
+ this.pageStartCtx = this.ctx;
3894
+ };
3895
+ /**
3896
+ * §17.6.4 `w:cols w:sep` — the rule down the middle of every gutter, run the
3897
+ * height of the text area. multi-column-separator-with-line.docx asks for one
3898
+ * and we drew its two columns with nothing between them.
3899
+ *
3900
+ * @returns One border item per gutter, or none when the section asks for no rule.
3901
+ */
3902
+ columnSeparatorItems = () => {
3903
+ const ctx = this.pageStartCtx;
3904
+ const cols = ctx.columns;
3905
+ if (!cols || cols.length < 2 || ctx.properties.columns?.separator !== true) return [];
3906
+ let deepest = ctx.marginTop;
3907
+ for (const item of this.current) {
3908
+ const bottom = item.type === "line" ? item.baselineY : item.type === "shape" ? 0 : item.y + item.height;
3909
+ if (bottom > deepest) deepest = bottom;
3910
+ }
3911
+ const height = Math.min(deepest, ctx.pageHeight - ctx.marginBottom) - ctx.marginTop;
3912
+ if (height <= 0) return [];
3913
+ const out = [];
3914
+ for (let i = 1; i < cols.length; i++) {
3915
+ const left = cols[i - 1];
3916
+ const right = cols[i];
3917
+ const x = ctx.marginLeft + (left.xOffsetPt + left.widthPt + right.xOffsetPt) / 2;
3918
+ out.push({
3919
+ type: "border",
3920
+ side: "left",
3921
+ x: pt(x),
3922
+ y: pt(ctx.marginTop),
3923
+ width: pt(0),
3924
+ height: pt(height),
3925
+ borderSizePt: COLUMN_SEPARATOR_PT,
3926
+ borderColorHex: "000000"
3927
+ });
3928
+ }
3929
+ return out;
3930
+ };
3931
+ /** The section context the CURRENT page began in (see {@link columnSeparatorItems}). */
3932
+ pageStartCtx;
3933
+ /**
3934
+ * §17.6.10 `w:pgBorders` — the rules around the page. `@w:offsetFrom` says
3935
+ * what each edge's space is measured from: the paper's own edge, or the text
3936
+ * margin the rule stands outside of. a4andborders.docx frames every page and
3937
+ * we drew nothing.
3938
+ *
3939
+ * @returns The border items for this page, in paint order.
3940
+ */
3941
+ /**
3942
+ * §17.2.1 — what the page is painted before anything is drawn on it: the
3943
+ * `v:background`'s PICTURE or GRADIENT when it has one, else the flat
3944
+ * `w:background @w:color`. Read as the colour alone, fill.docx's five-stop
3945
+ * sweep came out the navy of its fallback and tdf126533_pageBitmap.docx's
3946
+ * paper came out plain.
3947
+ *
3948
+ * @returns The background items for this page (at most one).
3949
+ */
3950
+ pageBackgroundItems = () => {
3951
+ const options = this.ctx.options;
3952
+ const w = this.ctx.pageWidth;
3953
+ const h = this.ctx.pageHeight;
3954
+ const fill = options?.pageBackgroundFill;
3955
+ if (fill?.kind === "picture" && this.backgroundImageName !== void 0) return [{
3956
+ type: "image",
3957
+ x: pt(0),
3958
+ y: pt(0),
3959
+ width: pt(w),
3960
+ height: pt(h),
3961
+ imageResourceName: this.backgroundImageName
3962
+ }];
3963
+ if (fill?.kind === "gradient" && fill.gradient) return [{
3964
+ type: "shape",
3965
+ shape: {
3966
+ paths: [rectAtPath(0, 0, w, h)],
3967
+ fillColorHex: gradientToSolid(fill.gradient),
3968
+ fillGradient: fill.gradient,
3969
+ transform: flipTransform([
3970
+ 1,
3971
+ 0,
3972
+ 0,
3973
+ 1,
3974
+ 0,
3975
+ 0
3976
+ ], h)
3977
+ }
3978
+ }];
3979
+ if (options?.pageBackgroundColorHex !== void 0) return [{
3980
+ type: "fill",
3981
+ x: pt(0),
3982
+ y: pt(0),
3983
+ width: pt(w),
3984
+ height: pt(h),
3985
+ fillColorHex: options.pageBackgroundColorHex
3986
+ }];
3987
+ return [];
3988
+ };
3989
+ pageBorderItems = () => {
3990
+ const pg = this.ctx.properties.pageBorders;
3991
+ if (!pg) return [];
3992
+ const H = this.ctx.pageHeight;
3993
+ const W = this.ctx.pageWidth;
3994
+ const out = [];
3995
+ for (const side of [
3996
+ "top",
3997
+ "bottom",
3998
+ "left",
3999
+ "right"
4000
+ ]) {
4001
+ const edge = pg.borders[side];
4002
+ if (!edge || edge.style === "none") continue;
4003
+ const gap = edge.spacePt ?? 0;
4004
+ const x0 = pg.offsetFrom === "page" ? gap : this.ctx.marginLeft - gap;
4005
+ const x1 = pg.offsetFrom === "page" ? W - gap : W - this.ctx.marginLeft + gap;
4006
+ const yTop = pg.offsetFrom === "page" ? gap : this.ctx.marginTop - gap;
4007
+ const yBottom = pg.offsetFrom === "page" ? H - gap : H - this.ctx.marginBottom + gap;
4008
+ out.push({
4009
+ type: "border",
4010
+ side,
4011
+ x: pt(Math.max(0, x0)),
4012
+ y: pt(Math.max(0, yTop)),
4013
+ width: pt(Math.max(0, x1 - x0)),
4014
+ height: pt(Math.max(0, yBottom - yTop)),
4015
+ borderSizePt: edge.width ?? DEFAULT_BORDER_SIZE_EIGHTH * EIGHTH_PT,
4016
+ borderColorHex: edge.colorHex ?? "000000",
4017
+ ...edge.style !== "single" ? { borderStyle: edge.style } : {}
4018
+ });
4019
+ }
4020
+ return out;
4021
+ };
4022
+ /**
4023
+ * Throw the in-progress page away and reset for the next one.
4024
+ *
4025
+ * A section boundary reached with a page that draws nothing is the one place
4026
+ * this is wanted: {@link flushPage} keeps such a page whenever the cursor
4027
+ * moved at all, which is right in the middle of a flow and wrong at a
4028
+ * boundary, where the space consumed was the section break itself.
4029
+ */
4030
+ dropPage = () => {
4031
+ this.current = [];
4032
+ this.floatsBehind = [];
4033
+ this.floatsFront = [];
4034
+ this.exclusions = [];
4035
+ this.pageNotes = [];
4036
+ this.noteReserve = 0;
4037
+ this.colIdx = 0;
4038
+ this.colStartLen = 0;
2388
4039
  this.cursorY = this.ctx.pageHeight - this.ctx.marginTop;
2389
4040
  this.colStartY = this.cursorY;
4041
+ this.bandTopY = this.cursorY;
4042
+ this.balanceBottomY = void 0;
4043
+ this.balanceHeightPt = 0;
2390
4044
  };
2391
4045
  };
2392
- function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", notes, bookmarkPositions, reflowParagraph) {
4046
+ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", notes, bookmarkPositions, reflowParagraph, backgroundImageName) {
2393
4047
  if (sectionCtxs.length === 0) return [];
2394
- const asm = new PageAssembler(sectionCtxs, builder, notes, bookmarkPositions);
4048
+ const asm = new PageAssembler(sectionCtxs, builder, notes, bookmarkPositions, backgroundImageName);
4049
+ const balanceIfEndsContinuous = (fromBlock) => {
4050
+ const here = asm.ctx;
4051
+ const next = sectionCtxs[asm.secIdx + 1];
4052
+ if ((here.columns?.length ?? 1) < 2) return;
4053
+ if (!next?.continuous) return;
4054
+ if (next.pageWidth !== here.pageWidth || next.pageHeight !== here.pageHeight) return;
4055
+ const end = Math.min(here.endIndex, blocks.length);
4056
+ asm.beginBalancedBand(blocksHeight(blocks.slice(fromBlock, end)));
4057
+ };
4058
+ balanceIfEndsContinuous(0);
2395
4059
  for (let blockIdx = 0; blockIdx < blocks.length; blockIdx++) {
2396
4060
  while (asm.secIdx < sectionCtxs.length - 1 && blockIdx >= asm.ctx.endIndex) {
2397
- asm.flushPage();
4061
+ const next = sectionCtxs[asm.secIdx + 1];
4062
+ if (next.continuous && next.pageWidth === asm.ctx.pageWidth && next.pageHeight === asm.ctx.pageHeight) {
4063
+ const end = Math.min(next.endIndex, blocks.length);
4064
+ asm.startBandSection(next, blocksHeight(blocks.slice(blockIdx, end)));
4065
+ continue;
4066
+ }
4067
+ if (asm.pageHasContent() || asm.pageInSection === 0) asm.flushPage(true);
4068
+ else asm.dropPage();
2398
4069
  asm.secIdx++;
2399
4070
  asm.ctx = sectionCtxs[asm.secIdx];
4071
+ asm.restartPageNumbers(asm.ctx);
2400
4072
  asm.pageInSection = 0;
2401
4073
  asm.cursorY = asm.ctx.pageHeight - asm.ctx.marginTop;
4074
+ balanceIfEndsContinuous(blockIdx);
2402
4075
  }
2403
4076
  const block = blocks[blockIdx];
2404
4077
  if (asm.pendingPageBreak) {
2405
4078
  asm.pendingPageBreak = false;
2406
- if (asm.current.length > 0) asm.flushPage();
4079
+ asm.flushPage(true);
2407
4080
  }
2408
4081
  if (builder && !(block.kind === "paragraph" && block.list)) asm.listStack.length = 0;
2409
4082
  if (block.kind === "paragraph") {
2410
- if (block.resolved.pageBreakBefore && asm.pageHasContent()) asm.flushPage();
4083
+ if ((block.resolved.pageBreakBefore || block.pageBreakBefore) && asm.pageHasContent()) asm.flushPage();
2411
4084
  asm.cursorY -= block.spacingBeforePt;
2412
4085
  let pb = block;
2413
4086
  if (reflowParagraph && pb.source && asm.exclusions.length > 0) {
2414
4087
  const widths = asm.lineWidthsFor(block, asm.cursorY);
2415
4088
  if (widths) pb = reflowParagraph(pb.source, asm.colWidth(), widths);
2416
4089
  }
4090
+ const borderTopY = asm.cursorY;
4091
+ const borderPage = asm.pages.length;
2417
4092
  let structId;
2418
4093
  let markerLblId;
2419
4094
  const firstLine = pb.lines[0];
@@ -2428,7 +4103,8 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2428
4103
  if (lang && lang !== defaultLang) builder.node(structId).lang = lang;
2429
4104
  }
2430
4105
  let firstLineOfBlock = true;
2431
- for (const line of pb.lines) {
4106
+ for (const [lineIdx, line] of pb.lines.entries()) {
4107
+ if (pb.columnBreakLines?.has(lineIdx) && asm.colHasContent()) asm.advanceColumn();
2432
4108
  const h = computeLineHeight(line, pb.resolved);
2433
4109
  let newNotes = asm.lineFootnotes(line);
2434
4110
  const addedReserve = (sub) => sub.reduce((sum, x) => sum + x.heightPt, 0) + (asm.pageNotes.length === 0 && sub.length > 0 ? FOOTNOTE_SEPARATOR_HEIGHT : 0);
@@ -2456,10 +4132,15 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2456
4132
  });
2457
4133
  }
2458
4134
  }
4135
+ for (let guard = 0; guard < 8 && asm.exclusions.length > 0; guard++) {
4136
+ const blocked = asm.lineGeometryAt(asm.cursorY, h).blockedUntilYUp;
4137
+ if (blocked === void 0 || blocked >= asm.cursorY) break;
4138
+ asm.cursorY = blocked;
4139
+ }
2459
4140
  asm.cursorY -= h;
2460
4141
  const indentLeft = pb.resolved.indentLeft + (line.firstLine ? pb.resolved.indentFirstLine : 0);
2461
4142
  const offset = alignmentOffset(pb.resolved.alignment, line.contentWidthPt, line.availableWidthPt);
2462
- const baselineY = pt(asm.ctx.pageHeight - (asm.cursorY + lineDescent(line)));
4143
+ const baselineY = pt(asm.ctx.pageHeight - (asm.cursorY + lineBaselineOffset(line, pb.resolved)));
2463
4144
  const exclusionShift = asm.exclusions.length > 0 ? asm.lineGeometryAt(asm.cursorY + h, h).xOffset : 0;
2464
4145
  const originX = asm.colLeft() + indentLeft + offset + exclusionShift;
2465
4146
  if (markerLblId !== void 0 && line === firstLine) {
@@ -2499,87 +4180,53 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2499
4180
  baselineY,
2500
4181
  ...structId !== void 0 ? { structId } : {}
2501
4182
  });
4183
+ asm.emitLineNumber(baselineY, line);
2502
4184
  }
4185
+ 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));
2503
4186
  asm.cursorY -= pb.spacingAfterPt;
2504
4187
  if (pb.pageBreakAfter) asm.pendingPageBreak = true;
2505
4188
  } else if (block.kind === "image") {
2506
4189
  const figId = builder ? createFigure(builder, block.altText, "Image") : void 0;
2507
4190
  const emitImageAt = (x, topYUp, sink) => {
4191
+ const ins = block.drawInset;
2508
4192
  sink.push({
2509
4193
  type: "image",
2510
- x: pt(x),
2511
- y: pt(asm.ctx.pageHeight - topYUp),
2512
- width: pt(block.widthPt),
2513
- height: pt(block.heightPt),
4194
+ x: pt(x + (ins?.dxPt ?? 0)),
4195
+ y: pt(asm.ctx.pageHeight - (topYUp - (ins?.dyTopPt ?? 0))),
4196
+ width: pt(ins?.widthPt ?? block.widthPt),
4197
+ height: pt(ins?.heightPt ?? block.heightPt),
2514
4198
  imageResourceName: block.resourceName,
4199
+ ...block.crop ? { crop: block.crop } : {},
4200
+ ...block.rotationDeg ? { rotationDeg: block.rotationDeg } : {},
4201
+ ...block.flipH ? { flipH: true } : {},
4202
+ ...block.flipV ? { flipV: true } : {},
4203
+ ...block.flipH ? { flipH: true } : {},
4204
+ ...block.flipV ? { flipV: true } : {},
2515
4205
  ...figId !== void 0 ? { structId: figId } : {}
2516
4206
  });
4207
+ if (block.outline) sink.push(...pictureOutlineItems(block.outline, x + (ins?.dxPt ?? 0), asm.ctx.pageHeight - (topYUp - (ins?.dyTopPt ?? 0)), ins?.widthPt ?? block.widthPt, ins?.heightPt ?? block.heightPt, figId));
2517
4208
  };
2518
4209
  if (isOutOfFlowFloat(block.float)) {
2519
4210
  const fx = asm.floatX(block.float, block.widthPt);
2520
- const fy = asm.floatTopYUp(block.float);
2521
- emitImageAt(fx, fy, block.float.behind ? asm.floatsBehind : asm.floatsFront);
2522
- if (block.float.wrap !== "none") asm.exclusions.push({
2523
- x0: fx,
2524
- x1: fx + block.widthPt,
2525
- topYUp: fy,
2526
- bottomYUp: fy - block.heightPt
2527
- });
4211
+ const fy = asm.floatTopYUp(block.float, block.heightPt);
4212
+ emitImageAt(fx, fy, PageAssembler.zSink(block.float.behind ? asm.floatsBehind : asm.floatsFront, block.float.zOrder));
4213
+ if (block.float.wrap !== "none") asm.excludeFloat(block.float, fx, fy, block.widthPt, block.heightPt);
2528
4214
  } else {
2529
4215
  asm.cursorY -= block.spacingBeforePt;
2530
4216
  if (asm.cursorY - block.heightPt < asm.bottomLimit() && asm.colHasContent()) asm.advanceColumn();
2531
4217
  asm.cursorY -= block.heightPt;
2532
- emitImageAt(asm.colLeft(), asm.cursorY + block.heightPt, asm.current);
4218
+ const offset = alignmentOffset(block.resolvedAlignment, block.widthPt, asm.colWidth());
4219
+ emitImageAt(asm.colLeft() + offset, asm.cursorY + block.heightPt, asm.current);
2533
4220
  asm.cursorY -= block.spacingAfterPt;
2534
4221
  }
2535
4222
  } else if (block.kind === "shape") {
2536
4223
  const figId = builder ? createFigure(builder, block.altText, "Shape") : void 0;
2537
- const emitShapeAt = (x, bottomYUp, sink) => {
2538
- const transform = flipTransform(buildShapeTransform(x, bottomYUp, block.widthPt, block.heightPt, block.rotation60k, block.flipH, block.flipV), asm.ctx.pageHeight);
2539
- sink.push({
2540
- type: "shape",
2541
- shape: {
2542
- paths: block.paths,
2543
- ...block.fillColorHex ? { fillColorHex: block.fillColorHex } : {},
2544
- ...block.fillGradient ? { fillGradient: block.fillGradient } : {},
2545
- ...block.stroke ? { stroke: block.stroke } : {},
2546
- ...block.shadow ? { shadow: block.shadow } : {},
2547
- transform
2548
- },
2549
- ...figId !== void 0 ? { structId: figId } : {}
2550
- });
2551
- if (block.textLines.length > 0) {
2552
- const shapeBottom = bottomYUp;
2553
- const shapeTop = bottomYUp + block.heightPt;
2554
- const innerWidth = Math.max(1, block.widthPt - block.insetLeftPt - block.insetRightPt);
2555
- let textY;
2556
- if (block.anchor === "b") textY = shapeBottom + block.insetBottomPt + block.textHeightPt;
2557
- else if (block.anchor === "ctr") textY = shapeBottom + (block.heightPt + block.textHeightPt) / 2;
2558
- else textY = shapeTop - block.insetTopPt;
2559
- for (const line of block.textLines) {
2560
- const h = computeLineHeight(line, line.resolved);
2561
- textY -= h;
2562
- const lineOffset = alignmentOffset(line.resolved.alignment, line.contentWidthPt, innerWidth);
2563
- sink.push({
2564
- type: "line",
2565
- line,
2566
- originX: pt(x + block.insetLeftPt + lineOffset),
2567
- baselineY: pt(asm.ctx.pageHeight - (textY + lineDescent(line))),
2568
- ...figId !== void 0 ? { structId: figId } : {}
2569
- });
2570
- }
2571
- }
2572
- };
4224
+ const emitShapeAt = (x, bottomYUp, sink) => emitShapeItems(block, x, bottomYUp, sink, asm.ctx.pageHeight, figId);
2573
4225
  if (isOutOfFlowFloat(block.float)) {
2574
4226
  const fx = asm.floatX(block.float, block.widthPt);
2575
- const fy = asm.floatTopYUp(block.float);
2576
- emitShapeAt(fx, fy - block.heightPt, block.float.behind ? asm.floatsBehind : asm.floatsFront);
2577
- if (block.float.wrap !== "none") asm.exclusions.push({
2578
- x0: fx,
2579
- x1: fx + block.widthPt,
2580
- topYUp: fy,
2581
- bottomYUp: fy - block.heightPt
2582
- });
4227
+ const fy = asm.floatTopYUp(block.float, block.heightPt);
4228
+ emitShapeAt(fx, fy - block.heightPt, PageAssembler.zSink(block.float.behind ? asm.floatsBehind : asm.floatsFront, block.float.zOrder));
4229
+ if (block.float.wrap !== "none") asm.excludeFloat(block.float, fx, fy, block.widthPt, block.heightPt);
2583
4230
  } else {
2584
4231
  asm.cursorY -= block.spacingBeforePt;
2585
4232
  if (asm.cursorY - block.heightPt < asm.bottomLimit() && asm.colHasContent()) asm.advanceColumn();
@@ -2590,44 +4237,14 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2590
4237
  }
2591
4238
  } else if (block.kind === "chart") {
2592
4239
  const figId = builder ? createFigure(builder, block.altText, "Chart") : void 0;
2593
- const fig = figId !== void 0 ? { structId: figId } : {};
2594
4240
  const emitChartAt = (x, bottomYUp, sink) => {
2595
- for (const s of block.layout.shapes) sink.push({
2596
- type: "shape",
2597
- shape: {
2598
- paths: s.paths,
2599
- ...s.fillColorHex ? { fillColorHex: s.fillColorHex } : {},
2600
- ...s.stroke ? { stroke: s.stroke } : {},
2601
- transform: flipTransform([
2602
- 1,
2603
- 0,
2604
- 0,
2605
- 1,
2606
- x,
2607
- bottomYUp
2608
- ], asm.ctx.pageHeight)
2609
- },
2610
- ...fig
2611
- });
2612
- for (const t of block.layout.texts) sink.push({
2613
- type: "line",
2614
- line: t.line,
2615
- originX: pt(x + t.x),
2616
- baselineY: pt(asm.ctx.pageHeight - (bottomYUp + t.y)),
2617
- ...t.rotationDeg ? { rotationDeg: t.rotationDeg } : {},
2618
- ...fig
2619
- });
4241
+ sink.push(...chartPageItems(block, x, bottomYUp, asm.ctx.pageHeight, figId));
2620
4242
  };
2621
4243
  if (isOutOfFlowFloat(block.float)) {
2622
4244
  const fx = asm.floatX(block.float, block.widthPt);
2623
- const fy = asm.floatTopYUp(block.float);
2624
- emitChartAt(fx, fy - block.heightPt, block.float.behind ? asm.floatsBehind : asm.floatsFront);
2625
- if (block.float.wrap !== "none") asm.exclusions.push({
2626
- x0: fx,
2627
- x1: fx + block.widthPt,
2628
- topYUp: fy,
2629
- bottomYUp: fy - block.heightPt
2630
- });
4245
+ const fy = asm.floatTopYUp(block.float, block.heightPt);
4246
+ emitChartAt(fx, fy - block.heightPt, PageAssembler.zSink(block.float.behind ? asm.floatsBehind : asm.floatsFront, block.float.zOrder));
4247
+ if (block.float.wrap !== "none") asm.excludeFloat(block.float, fx, fy, block.widthPt, block.heightPt);
2631
4248
  } else {
2632
4249
  asm.cursorY -= block.spacingBeforePt;
2633
4250
  if (asm.cursorY - block.heightPt < asm.bottomLimit() && asm.colHasContent()) asm.advanceColumn();
@@ -2638,6 +4255,19 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
2638
4255
  }
2639
4256
  } else {
2640
4257
  const colCount = block.rows.reduce((max, r) => Math.max(max, r.cells.reduce((s, c) => Math.max(s, c.colStart + c.colSpan), 0)), 0);
4258
+ const anchorY = isOutOfFlowFloat(block.float) ? asm.floatTopYUp(block.float, block.heightPt) : 0;
4259
+ if (isOutOfFlowFloat(block.float) && anchorY - block.heightPt >= asm.ctx.marginBottom) {
4260
+ const fx = asm.floatX(block.float, block.totalWidthPt);
4261
+ const fy = anchorY;
4262
+ const sink = PageAssembler.zSink(block.float.behind ? asm.floatsBehind : asm.floatsFront, block.float.zOrder);
4263
+ let rowY = fy;
4264
+ for (const row of block.rows) {
4265
+ emitRowChunk(sink, row, fx, rowY, asm.ctx.pageHeight, colCount);
4266
+ rowY -= row.heightPt;
4267
+ }
4268
+ if (block.float.wrap !== "none") asm.excludeFloat(block.float, fx, fy, block.totalWidthPt, block.heightPt);
4269
+ continue;
4270
+ }
2641
4271
  const headerRows = [];
2642
4272
  for (const r of block.rows) if (r.isHeader) headerRows.push(r);
2643
4273
  else break;
@@ -2917,7 +4547,26 @@ function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellS
2917
4547
  const slack = cell.verticalAlign !== void 0 ? boxHeightPt - cell.padTopPt - cell.contentHeightPt - cell.padBottomPt : 0;
2918
4548
  const vOffset = slack <= 0 ? 0 : cell.verticalAlign === "bottom" ? slack : cell.verticalAlign === "center" ? slack / 2 : 0;
2919
4549
  let textY = rowTop - cell.padTopPt - vOffset;
2920
- for (const line of cell.lines) {
4550
+ const nestedX = cellX + cell.padLeftPt;
4551
+ const drawNestedBefore = (n) => {
4552
+ for (const nt of cell.nestedTables ?? []) {
4553
+ if (nt.afterLine !== n) continue;
4554
+ for (const nrow of nt.block.rows) {
4555
+ const nestedIds = structId !== void 0 ? nrow.cells.map(() => structId) : void 0;
4556
+ emitRowChunk(out, nrow, nestedX, textY, pageHeight, nt.block.colCount, nestedIds);
4557
+ textY -= nrow.heightPt;
4558
+ }
4559
+ }
4560
+ for (const cs of cell.shapes ?? []) {
4561
+ if (cs.afterLine !== n) continue;
4562
+ const offset = alignmentOffset(cs.block.resolvedAlignment, cs.block.widthPt, cell.widthPt - cell.padLeftPt - cell.padRightPt);
4563
+ textY -= cs.block.heightPt;
4564
+ emitShapeItems(cs.block, nestedX + offset, textY, out, pageHeight, structId);
4565
+ }
4566
+ };
4567
+ for (const [lineIdx, line] of cell.lines.entries()) {
4568
+ drawNestedBefore(lineIdx);
4569
+ textY -= cell.lineGaps?.get(lineIdx) ?? 0;
2921
4570
  const h = computeLineHeight(line, line.resolved);
2922
4571
  textY -= h;
2923
4572
  const offset = alignmentOffset(line.resolved.alignment, line.contentWidthPt, cell.widthPt - cell.padLeftPt - cell.padRightPt);
@@ -2926,7 +4575,7 @@ function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellS
2926
4575
  type: "line",
2927
4576
  line,
2928
4577
  originX: pt(cellX + cell.padLeftPt + indentLeft + offset),
2929
- baselineY: pt(pageHeight - (textY + lineDescent(line))),
4578
+ baselineY: pt(pageHeight - (textY + lineBaselineOffset(line, line.resolved))),
2930
4579
  ...cell.clipped ? { clip: {
2931
4580
  x: pt(cellX),
2932
4581
  y: pt(pageHeight - rowTop),
@@ -2936,14 +4585,7 @@ function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellS
2936
4585
  ...structId !== void 0 ? { structId } : {}
2937
4586
  });
2938
4587
  }
2939
- if (cell.nestedTables) {
2940
- const nestedX = cellX + cell.padLeftPt;
2941
- for (const nt of cell.nestedTables) for (const nrow of nt.rows) {
2942
- const nestedIds = structId !== void 0 ? nrow.cells.map(() => structId) : void 0;
2943
- emitRowChunk(out, nrow, nestedX, textY, pageHeight, nt.colCount, nestedIds);
2944
- textY -= nrow.heightPt;
2945
- }
2946
- }
4588
+ drawNestedBefore(cell.lines.length);
2947
4589
  }
2948
4590
  }
2949
4591
  function splitRowIntoChunks(row, capacity) {
@@ -3147,4 +4789,4 @@ function emitRowBottomEdge(out, row, marginLeft, rowBottom, pageHeight) {
3147
4789
  }
3148
4790
  }
3149
4791
  //#endregion
3150
- export { layoutStyledDocument };
4792
+ export { GLUE_SHRINK_RATIO, layoutStyledDocument };