@stll/folio-core 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,8 @@
1
1
  import "../layout-engine/types.js";
2
2
  import { renderPages } from "../layout-painter/renderPage.js";
3
3
  import { templatePreviewValuesKey } from "../prosemirror/plugins/templatePreviewValues.js";
4
+ import { getMargins, getPageSize, twipsToPixels } from "../paged-layout/sectionGeometry.js";
5
+ import { getColumns } from "../layout-bridge/sectionColumns.js";
4
6
  import { toFlowBlocks } from "../layout-bridge/convert/toFlowBlocks.js";
5
7
  import { recordLayoutComplete, recordLayoutError, recordLayoutPhase, recordLayoutStart } from "../layout-engine/layoutInstrumentation.js";
6
8
  import { buildBookmarkPageMap } from "../fields/bookmarkPages.js";
@@ -15,7 +17,6 @@ import { layoutDocument } from "../layout-engine/index.js";
15
17
  import { measureBlocks, measureSingleBlockWithoutFloatingZones } from "../layout-engine/measure/measureBlocks.js";
16
18
  import { tryBuildIncrementalMeasures } from "../paged-layout/incrementalMeasure.js";
17
19
  import { computePerBlockMeasureInputs } from "../paged-layout/sectionBlockWidths.js";
18
- import { getMargins, getPageSize, twipsToPixels } from "../paged-layout/sectionGeometry.js";
19
20
  import { getDocumentWatermark } from "../watermark/index.js";
20
21
  //#region src/controller/layoutPipeline.ts
21
22
  function runLayoutPipeline(deps, state, options = {}) {
@@ -98,10 +99,17 @@ function runLayoutPipeline(deps, state, options = {}) {
98
99
  margins
99
100
  };
100
101
  if (columns !== void 0) bodyLayoutConfig.columns = columns;
102
+ const finalSectionProperties = document?.package.document.sections?.at(-1)?.properties;
103
+ const finalLayoutConfig = finalSectionProperties ? {
104
+ pageSize: getPageSize(finalSectionProperties),
105
+ margins: getMargins(finalSectionProperties)
106
+ } : bodyLayoutConfig;
107
+ const finalColumns = getColumns(finalSectionProperties);
108
+ if (finalColumns !== void 0) finalLayoutConfig.columns = finalColumns;
101
109
  const blockMeasureInputs = computePerBlockMeasureInputs({
102
110
  blocks: newBlocks,
103
111
  bodyConfig: bodyLayoutConfig,
104
- finalConfig: bodyLayoutConfig
112
+ finalConfig: finalLayoutConfig
105
113
  });
106
114
  const blockWidths = blockMeasureInputs.widths;
107
115
  const previousArtifacts = session.artifacts;
@@ -144,10 +152,13 @@ function runLayoutPipeline(deps, state, options = {}) {
144
152
  top: headerBottom
145
153
  };
146
154
  }
147
- const finalSection = document?.package.document.sections?.at(-1);
148
- if (finalSection) {
149
- nextLayoutOpts.finalPageSize = getPageSize(finalSection.properties);
150
- nextLayoutOpts.finalMargins = getMargins(finalSection.properties);
155
+ if (finalSectionProperties) {
156
+ nextLayoutOpts.finalPageSize = finalLayoutConfig.pageSize;
157
+ nextLayoutOpts.finalMargins = finalLayoutConfig.margins;
158
+ nextLayoutOpts.finalColumns = finalLayoutConfig.columns ?? {
159
+ count: 1,
160
+ gap: 0
161
+ };
151
162
  }
152
163
  if (columns !== void 0) nextLayoutOpts.columns = columns;
153
164
  if (bodyBreakType !== void 0) nextLayoutOpts.bodyBreakType = bodyBreakType;
@@ -1,9 +1,9 @@
1
- import { elementToXml, findChild, findDeep, getChildElements, getLocalName, mergeXmlnsDeclarations } from "./xmlParser.js";
1
+ import { elementToXml, findChild, getChildElements, getLocalName, mergeXmlnsDeclarations } from "./xmlParser.js";
2
2
  import { parseBookmarkEnd, parseBookmarkStart } from "./bookmarkParser.js";
3
- import { getTextBoxContentElement, isTextBoxDrawing, parseTextBox, parseTextBoxContent } from "./textBoxParser.js";
4
3
  import { parseSdtProperties } from "./sdtProperties.js";
5
4
  import { parseParagraph } from "./paragraphParser.js";
6
5
  import { appendBookmarkMarkerToLastParagraphInBlocks, prependBookmarkMarkersToFirstParagraphInBlocks } from "./bookmarkPlacement.js";
6
+ import { enrichParagraphTextBoxes } from "./paragraphTextBoxEnrichment.js";
7
7
  import { parseTable } from "./tableParser.js";
8
8
  import { padDecimal } from "./numberingParser.js";
9
9
  import { convertBulletToUnicode } from "./bulletMarkers.js";
@@ -100,104 +100,6 @@ const computeListMarker = (paragraph, numbering, listCounters, abstractCounters)
100
100
  }
101
101
  listRendering.marker = computedMarker;
102
102
  };
103
- const enrichParagraphTextBoxes = (paragraph, paraXml, styles, theme, numbering, rels, media) => {
104
- const xmlChildren = getChildElements(paraXml);
105
- let parsedIndex = 0;
106
- let lastConsumedRun;
107
- for (const xmlChild of xmlChildren) {
108
- if (getLocalName(xmlChild.name ?? "") !== "r") {
109
- if (parsedIndex < paragraph.content.length && paragraph.content[parsedIndex]?.type !== "run") parsedIndex += 1;
110
- continue;
111
- }
112
- const { textBoxDrawings, hasNonTextBoxContent } = scanRunForTextBoxDrawings(xmlChild);
113
- const parsedContent = paragraph.content[parsedIndex];
114
- const parsedRun = parsedContent?.type === "run" ? parsedContent : void 0;
115
- const targetRun = parsedRun ?? (hasNonTextBoxContent ? lastConsumedRun : void 0);
116
- for (const runEl of textBoxDrawings) {
117
- const textBox = parseTextBox(runEl);
118
- if (!textBox) continue;
119
- const wsp = findDeep(runEl, "wps", "wsp");
120
- if (wsp) {
121
- const txbxContentEl = getTextBoxContentElement(wsp);
122
- if (txbxContentEl) textBox.content = parseTextBoxContent(txbxContentEl, parseParagraph, null, styles, theme, numbering, rels ?? void 0, media ?? void 0);
123
- }
124
- const shape = {
125
- type: "shape",
126
- shapeType: "textBox",
127
- size: textBox.size,
128
- ...textBox.position !== void 0 ? { position: textBox.position } : {},
129
- ...textBox.wrap !== void 0 ? { wrap: textBox.wrap } : {},
130
- ...textBox.fill !== void 0 ? { fill: textBox.fill } : {},
131
- ...textBox.outline !== void 0 ? { outline: textBox.outline } : {},
132
- textBody: {
133
- content: textBox.content,
134
- ...textBox.margins !== void 0 ? { margins: textBox.margins } : {}
135
- }
136
- };
137
- if (textBox.id) shape.id = textBox.id;
138
- const shapeContent = {
139
- type: "shape",
140
- shape
141
- };
142
- if (targetRun && hasNonTextBoxContent) targetRun.content.push(shapeContent);
143
- else {
144
- const newRun = {
145
- type: "run",
146
- content: [shapeContent]
147
- };
148
- paragraph.content.splice(parsedIndex, 0, newRun);
149
- lastConsumedRun = newRun;
150
- parsedIndex += 1;
151
- }
152
- }
153
- if (hasNonTextBoxContent && parsedRun) {
154
- lastConsumedRun = parsedRun;
155
- parsedIndex += 1;
156
- }
157
- }
158
- };
159
- const scanRunForTextBoxDrawings = (xmlRun) => {
160
- const textBoxDrawings = [];
161
- let hasNonTextBoxContent = false;
162
- const visitDrawing = (drawingEl) => {
163
- if (isTextBoxDrawing(drawingEl)) {
164
- textBoxDrawings.push(drawingEl);
165
- return;
166
- }
167
- hasNonTextBoxContent = true;
168
- };
169
- for (const el of getChildElements(xmlRun)) {
170
- const name = getLocalName(el.name ?? "");
171
- if (name === "rPr") continue;
172
- if (name === "drawing") {
173
- visitDrawing(el);
174
- continue;
175
- }
176
- if (name === "AlternateContent") {
177
- const branches = getChildElements(el);
178
- const choice = branches.find((branch) => getLocalName(branch.name ?? "") === "Choice");
179
- const fallback = branches.find((branch) => getLocalName(branch.name ?? "") === "Fallback");
180
- const tryBranch = (branch) => {
181
- if (!branch) return false;
182
- let found = false;
183
- for (const innerEl of getChildElements(branch)) if (getLocalName(innerEl.name ?? "") === "drawing") {
184
- visitDrawing(innerEl);
185
- found = true;
186
- }
187
- return found;
188
- };
189
- let foundInBranch = tryBranch(choice);
190
- if (!foundInBranch) foundInBranch = tryBranch(fallback);
191
- if (!foundInBranch) hasNonTextBoxContent = true;
192
- continue;
193
- }
194
- hasNonTextBoxContent = true;
195
- }
196
- return {
197
- textBoxDrawings,
198
- hasNonTextBoxContent
199
- };
200
- };
201
103
  const parseBlockContent = (parent, styles, theme, numbering, rels, media, options) => parseBlockContentWithState(parent, styles, theme, numbering, rels, media, {
202
104
  listCounters: /* @__PURE__ */ new Map(),
203
105
  abstractCounters: /* @__PURE__ */ new Map(),
@@ -0,0 +1,9 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ import { NumberingMap } from "./numberingParser.js";
3
+ import { StyleMap } from "./styleParser.js";
4
+ import { XmlElement } from "./xmlParser.js";
5
+
6
+ //#region src/docx/paragraphTextBoxEnrichment.d.ts
7
+ declare const enrichParagraphTextBoxes: (paragraph: document_d_exports.Paragraph, paraXml: XmlElement, styles: StyleMap | null, theme: document_d_exports.Theme | null, numbering: NumberingMap | null, rels: document_d_exports.RelationshipMap | null, media: Map<string, document_d_exports.MediaFile> | null) => void;
8
+ //#endregion
9
+ export { enrichParagraphTextBoxes };
@@ -0,0 +1,104 @@
1
+ import { findDeep, getChildElements, getLocalName } from "./xmlParser.js";
2
+ import { getTextBoxContentElement, isTextBoxDrawing, parseTextBox, parseTextBoxContent } from "./textBoxParser.js";
3
+ import { parseParagraph } from "./paragraphParser.js";
4
+ //#region src/docx/paragraphTextBoxEnrichment.ts
5
+ const enrichParagraphTextBoxes = (paragraph, paraXml, styles, theme, numbering, rels, media) => {
6
+ const xmlChildren = getChildElements(paraXml);
7
+ let parsedIndex = 0;
8
+ let lastConsumedRun;
9
+ for (const xmlChild of xmlChildren) {
10
+ if (getLocalName(xmlChild.name ?? "") !== "r") {
11
+ if (parsedIndex < paragraph.content.length && paragraph.content[parsedIndex]?.type !== "run") parsedIndex += 1;
12
+ continue;
13
+ }
14
+ const { textBoxDrawings, hasNonTextBoxContent } = scanRunForTextBoxDrawings(xmlChild);
15
+ const parsedContent = paragraph.content[parsedIndex];
16
+ const parsedRun = parsedContent?.type === "run" ? parsedContent : void 0;
17
+ const targetRun = parsedRun ?? (hasNonTextBoxContent ? lastConsumedRun : void 0);
18
+ for (const runEl of textBoxDrawings) {
19
+ const textBox = parseTextBox(runEl);
20
+ if (!textBox) continue;
21
+ const wsp = findDeep(runEl, "wps", "wsp");
22
+ if (wsp) {
23
+ const txbxContentEl = getTextBoxContentElement(wsp);
24
+ if (txbxContentEl) textBox.content = parseTextBoxContent(txbxContentEl, parseParagraph, null, styles, theme, numbering, rels ?? void 0, media ?? void 0);
25
+ }
26
+ const shape = {
27
+ type: "shape",
28
+ shapeType: "textBox",
29
+ size: textBox.size,
30
+ ...textBox.position !== void 0 ? { position: textBox.position } : {},
31
+ ...textBox.wrap !== void 0 ? { wrap: textBox.wrap } : {},
32
+ ...textBox.fill !== void 0 ? { fill: textBox.fill } : {},
33
+ ...textBox.outline !== void 0 ? { outline: textBox.outline } : {},
34
+ textBody: {
35
+ content: textBox.content,
36
+ ...textBox.margins !== void 0 ? { margins: textBox.margins } : {}
37
+ }
38
+ };
39
+ if (textBox.id) shape.id = textBox.id;
40
+ const shapeContent = {
41
+ type: "shape",
42
+ shape
43
+ };
44
+ if (targetRun && hasNonTextBoxContent) targetRun.content.push(shapeContent);
45
+ else {
46
+ const newRun = {
47
+ type: "run",
48
+ content: [shapeContent]
49
+ };
50
+ paragraph.content.splice(parsedIndex, 0, newRun);
51
+ lastConsumedRun = newRun;
52
+ parsedIndex += 1;
53
+ }
54
+ }
55
+ if (hasNonTextBoxContent && parsedRun) {
56
+ lastConsumedRun = parsedRun;
57
+ parsedIndex += 1;
58
+ }
59
+ }
60
+ };
61
+ const scanRunForTextBoxDrawings = (xmlRun) => {
62
+ const textBoxDrawings = [];
63
+ let hasNonTextBoxContent = false;
64
+ const visitDrawing = (drawingEl) => {
65
+ if (isTextBoxDrawing(drawingEl)) {
66
+ textBoxDrawings.push(drawingEl);
67
+ return;
68
+ }
69
+ hasNonTextBoxContent = true;
70
+ };
71
+ for (const el of getChildElements(xmlRun)) {
72
+ const name = getLocalName(el.name ?? "");
73
+ if (name === "rPr") continue;
74
+ if (name === "drawing") {
75
+ visitDrawing(el);
76
+ continue;
77
+ }
78
+ if (name === "AlternateContent") {
79
+ const branches = getChildElements(el);
80
+ const choice = branches.find((branch) => getLocalName(branch.name ?? "") === "Choice");
81
+ const fallback = branches.find((branch) => getLocalName(branch.name ?? "") === "Fallback");
82
+ const tryBranch = (branch) => {
83
+ if (!branch) return false;
84
+ let found = false;
85
+ for (const innerEl of getChildElements(branch)) if (getLocalName(innerEl.name ?? "") === "drawing") {
86
+ visitDrawing(innerEl);
87
+ found = true;
88
+ }
89
+ return found;
90
+ };
91
+ let foundInBranch = tryBranch(choice);
92
+ if (!foundInBranch) foundInBranch = tryBranch(fallback);
93
+ if (!foundInBranch) hasNonTextBoxContent = true;
94
+ continue;
95
+ }
96
+ hasNonTextBoxContent = true;
97
+ }
98
+ return {
99
+ textBoxDrawings,
100
+ hasNonTextBoxContent
101
+ };
102
+ };
103
+ //#endregion
104
+ export { enrichParagraphTextBoxes };
@@ -3,6 +3,7 @@ import { parseBookmarkEnd, parseBookmarkStart } from "./bookmarkParser.js";
3
3
  import { BorderStyleSchema, FloatingTableXSpecSchema, FloatingTableYSpecSchema, ShadingPatternSchema, TableCellTextDirectionSchema, ThemeColorSlotSchema, narrowEnum } from "./parserEnums.js";
4
4
  import { parseParagraph } from "./paragraphParser.js";
5
5
  import { appendBookmarkMarkerToLastParagraphInBlocks, appendBookmarkMarkerToLastParagraphInCells, prependBookmarkMarkersToFirstParagraphInBlocks, prependBookmarkMarkersToFirstParagraphInCell } from "./bookmarkPlacement.js";
6
+ import { enrichParagraphTextBoxes } from "./paragraphTextBoxEnrichment.js";
6
7
  //#region src/docx/tableParser.ts
7
8
  /**
8
9
  * Parse a table measurement (width, height, etc.)
@@ -489,6 +490,7 @@ function parseCellContent(tcElement, styles, theme, numbering, rels, media, opti
489
490
  const localName = getLocalName(child.name);
490
491
  if (localName === "p") {
491
492
  const para = parseParagraph(child, styles, theme, numbering, rels, media, options);
493
+ enrichParagraphTextBoxes(para, child, styles, theme, numbering, rels, media);
492
494
  prependPendingBookmarkMarkers(para, pendingBookmarkMarkers);
493
495
  content.push(para);
494
496
  } else if (localName === "tbl") {
@@ -6,8 +6,9 @@ import { resolveColor, resolveHighlightToCss } from "../../utils/colorResolver.j
6
6
  import { resolveShadingFill } from "../../utils/formatToStyle.js";
7
7
  import { convertBulletToUnicode } from "../../docx/bulletMarkers.js";
8
8
  import { setTextBoxGroupId } from "../../layout-engine/textBoxGroup.js";
9
+ import { getColumns } from "../sectionColumns.js";
9
10
  import { directionIsRtl } from "../../prosemirror/paragraphDirection.js";
10
- import { expectBlockSdtAttrs, expectCharacterSpacingMarkAttrs, expectCommentMarkAttrs, expectEmphasisMarkAttrs, expectFieldAttrs, expectFontFamilyMarkAttrs, expectFontSizeMarkAttrs, expectFootnoteRefMarkAttrs, expectHighlightMarkAttrs, expectHyperlinkMarkAttrs, expectImageAttrs, expectMathAttrs, expectParagraphAttrs, expectRunFormattingOverrideMarkAttrs, expectRunShadingMarkAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs, expectTextColorMarkAttrs, expectTextEffectMarkAttrs, expectTrackedChangeMarkAttrs, expectUnderlineMarkAttrs } from "../../prosemirror/attrs/index.js";
11
+ import { expectBlockSdtAttrs, expectCharacterSpacingMarkAttrs, expectCommentMarkAttrs, expectEmphasisMarkAttrs, expectFieldAttrs, expectFontFamilyMarkAttrs, expectFontSizeMarkAttrs, expectFootnoteRefMarkAttrs, expectHardBreakAttrs, expectHighlightMarkAttrs, expectHyperlinkMarkAttrs, expectImageAttrs, expectMathAttrs, expectParagraphAttrs, expectRunFormattingOverrideMarkAttrs, expectRunShadingMarkAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs, expectTextColorMarkAttrs, expectTextEffectMarkAttrs, expectTrackedChangeMarkAttrs, expectUnderlineMarkAttrs } from "../../prosemirror/attrs/index.js";
11
12
  import { autospacingMatchesBase } from "../../prosemirror/autospacingBase.js";
12
13
  import { runShadingAttrsToShading } from "../../prosemirror/conversion/runShadingMark.js";
13
14
  import { assertValidProseMirrorDocument } from "../../prosemirror/validation.js";
@@ -919,6 +920,7 @@ function convertTableCell(node, startPos, options, tableCellMargins) {
919
920
  const block = convertParagraph(child, offset, options);
920
921
  blocks.push(block);
921
922
  } else if (child.type.name === "table") blocks.push(convertTable(child, offset, options));
923
+ else if (child.type.name === "textBox") blocks.push(convertTextBoxNode(child, offset, options));
922
924
  offset += child.nodeSize;
923
925
  });
924
926
  const trailingBlock = blocks.at(-1);
@@ -1213,9 +1215,17 @@ function toFlowBlocks(doc, options = {}) {
1213
1215
  }
1214
1216
  switch (node.type.name) {
1215
1217
  case "paragraph": {
1216
- const block = convertParagraph(node, pos, opts);
1217
1218
  const pmAttrs = expectParagraphAttrs(node);
1218
- trackedPush(block);
1219
+ const onlyChild = node.childCount === 1 ? node.firstChild : null;
1220
+ if (onlyChild?.type.name === "hardBreak" && expectHardBreakAttrs(onlyChild).breakType === "column") {
1221
+ const columnBreak = {
1222
+ kind: "columnBreak",
1223
+ id: nextBlockId(),
1224
+ pmStart: pos,
1225
+ pmEnd: pos + node.nodeSize
1226
+ };
1227
+ trackedPush(columnBreak);
1228
+ } else trackedPush(convertParagraph(node, pos, opts));
1219
1229
  const secProps = pmAttrs._sectionProperties;
1220
1230
  if (secProps || pmAttrs.sectionBreakType) {
1221
1231
  const sectionBreak = {
@@ -1245,16 +1255,8 @@ function toFlowBlocks(doc, options = {}) {
1245
1255
  if (secProps.headerDistance !== void 0) sectionBreak.margins.header = twipsToPixels(secProps.headerDistance);
1246
1256
  if (secProps.footerDistance !== void 0) sectionBreak.margins.footer = twipsToPixels(secProps.footerDistance);
1247
1257
  }
1248
- const colCount = secProps.columnCount ?? 1;
1249
- if (colCount > 1) {
1250
- const cols = {
1251
- count: colCount,
1252
- gap: twipsToPixels(secProps.columnSpace ?? 720),
1253
- equalWidth: secProps.equalWidth ?? true
1254
- };
1255
- if (secProps.separator !== void 0) cols.separator = secProps.separator;
1256
- sectionBreak.columns = cols;
1257
- }
1258
+ const columns = getColumns(secProps);
1259
+ if (columns) sectionBreak.columns = columns;
1258
1260
  }
1259
1261
  trackedPush(sectionBreak);
1260
1262
  }
@@ -7,13 +7,18 @@ const DEFAULT_COLUMN_SPACE_TWIPS = 720;
7
7
  * Returns undefined for single-column (default) to avoid unnecessary paginator overhead.
8
8
  */
9
9
  function getColumns(sectionProps) {
10
- const count = sectionProps?.columnCount ?? 1;
10
+ const authoredColumns = sectionProps?.columns;
11
+ const count = sectionProps?.columnCount ?? authoredColumns?.length ?? 1;
11
12
  if (count <= 1) return void 0;
12
13
  const columns = {
13
14
  count,
14
15
  gap: twipsToPixels(sectionProps?.columnSpace ?? DEFAULT_COLUMN_SPACE_TWIPS),
15
16
  equalWidth: sectionProps?.equalWidth ?? true
16
17
  };
18
+ if (sectionProps?.equalWidth === false && authoredColumns?.length === count && authoredColumns.every(({ width }) => width !== void 0)) {
19
+ columns.widths = authoredColumns.map(({ width }) => twipsToPixels(width ?? 0));
20
+ columns.gaps = authoredColumns.slice(0, -1).map(({ space }) => twipsToPixels(space ?? sectionProps.columnSpace ?? DEFAULT_COLUMN_SPACE_TWIPS));
21
+ }
17
22
  if (sectionProps?.separator !== void 0) columns.separator = sectionProps.separator;
18
23
  return columns;
19
24
  }
@@ -156,7 +156,8 @@ function layoutDocument(blocks, measures, options) {
156
156
  pageSize: finalPageSize,
157
157
  margins: finalMargins
158
158
  };
159
- if (options.columns !== void 0) finalConfig.columns = options.columns;
159
+ const finalColumns = options.finalColumns ?? options.columns;
160
+ if (finalColumns !== void 0) finalConfig.columns = finalColumns;
160
161
  const { configs: sectionConfigs, breakIndices } = collectSectionConfigs(blocks, bodyConfig, finalConfig);
161
162
  const sectionBreakTypes = [...breakIndices.map((index) => blocks[index].type), options.bodyBreakType];
162
163
  const initialConfig = sectionConfigs.at(0) ?? bodyConfig;
@@ -197,7 +198,7 @@ function layoutDocument(blocks, measures, options) {
197
198
  const pageBeforeBlockLayout = paginator.getCurrentState().page.number;
198
199
  switch (block.kind) {
199
200
  case "paragraph":
200
- layoutParagraph(block, measure, paginator, paginator.getContentWidth(), options.footnoteHeightById);
201
+ layoutParagraph(block, measure, paginator, paginator.columnWidth, options.footnoteHeightById);
201
202
  break;
202
203
  case "table":
203
204
  if (block.floating) layoutFloatingTable(block, measure, paginator, paginator.getContentWidth());
@@ -428,9 +429,9 @@ function layoutTable(block, measure, paginator, footnoteHeightById) {
428
429
  let x = paginator.getColumnX(columnIndex);
429
430
  if (block.justification === "center") x += (paginator.columnWidth - measure.totalWidth) / 2;
430
431
  else if (block.justification === "right") x = x + paginator.columnWidth - measure.totalWidth;
431
- else if (block.indent !== void 0) {
432
- const leadingCellMargin = block.rows[0]?.cells[0]?.padding?.left ?? 0;
433
- x += block.indent - leadingCellMargin;
432
+ else {
433
+ const leadingCellMargin = block.rows.at(0)?.cells.at(0)?.padding?.left ?? 0;
434
+ x += (block.indent ?? 0) - leadingCellMargin;
434
435
  }
435
436
  return x;
436
437
  };
@@ -637,8 +638,9 @@ function layoutFloatingTable(block, measure, paginator, contentWidth) {
637
638
  else if (spec === "center") y = baseY + (contentHeight - tableHeight) / 2;
638
639
  }
639
640
  if (!usedExplicitY) y = paginator.ensureFits(tableHeight).cursorY;
640
- const minX = margins.left;
641
- const maxX = margins.left + contentWidth - tableWidth;
641
+ const pageAnchored = floating?.horzAnchor === "page";
642
+ const minX = pageAnchored ? 0 : margins.left;
643
+ const maxX = pageAnchored ? page.size.w - tableWidth : margins.left + contentWidth - tableWidth;
642
644
  if (Number.isFinite(maxX)) x = Math.max(minX, Math.min(x, maxX));
643
645
  const fragment = {
644
646
  kind: "table",
@@ -1,4 +1,4 @@
1
- import { DEFAULT_TEXTBOX_MARGINS, floatingTextBoxReservesBand, tableColumnsArePinned } from "../types.js";
1
+ import { DEFAULT_TEXTBOX_MARGINS, floatingTextBoxReservesBand, isFloatingTextBoxBlock, tableColumnsArePinned } from "../types.js";
2
2
  import { findClearLineY, measureParagraph } from "./measureParagraph.js";
3
3
  import { getCachedParagraphMeasure, setCachedParagraphMeasure } from "./cache.js";
4
4
  import { getTextBoxGroupId } from "../textBoxGroup.js";
@@ -56,6 +56,7 @@ function isInlineFlowImageRun(run) {
56
56
  return true;
57
57
  }
58
58
  function measureTableCellBlockVisualHeight(block, blockMeasure) {
59
+ if (block.kind === "textBox" && isFloatingTextBoxBlock(block)) return 0;
59
60
  if (block.kind !== "paragraph" || blockMeasure.kind !== "paragraph") {
60
61
  if ("totalHeight" in blockMeasure) return blockMeasure.totalHeight;
61
62
  if ("height" in blockMeasure) return blockMeasure.height;
@@ -95,7 +96,7 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
95
96
  columnWidths = Array.from({ length: colCount }, () => equalWidth);
96
97
  } else if (columnWidths.length > 0 && explicitWidthPx) {
97
98
  const totalWidth = columnWidths.reduce((sum, w) => sum + w, 0);
98
- if (totalWidth > 0 && Math.abs(totalWidth - explicitWidthPx) > 1) {
99
+ if (!(tableBlock.layout !== "fixed" && (tableBlock.widthType === void 0 || tableBlock.widthType === "dxa") && totalWidth - explicitWidthPx > 1) && totalWidth > 0 && Math.abs(totalWidth - explicitWidthPx) > 1) {
99
100
  const scale = explicitWidthPx / totalWidth;
100
101
  columnWidths = columnWidths.map((w) => w * scale);
101
102
  }
@@ -55,6 +55,8 @@ declare function createPaginator(options: PaginatorOptions): {
55
55
  count: number;
56
56
  gap: number;
57
57
  equalWidth?: boolean;
58
+ widths?: number[];
59
+ gaps?: number[];
58
60
  separator?: boolean;
59
61
  }; /** Get current state. */
60
62
  getCurrentState: () => PageState; /** Get available height in current column. */
@@ -7,12 +7,13 @@ import { panic } from "better-result";
7
7
  * Tracks the current page, cursor position, and available space.
8
8
  * Creates new pages when content doesn't fit.
9
9
  */
10
- /**
11
- * Calculate the width of a single column.
12
- */
13
- function calculateColumnWidth(pageWidth, leftMargin, rightMargin, columns) {
14
- return (pageWidth - leftMargin - rightMargin - (columns.count - 1) * columns.gap) / columns.count;
10
+ /** Calculate active column widths, preferring authored unequal widths. */
11
+ function calculateColumnWidths(pageWidth, leftMargin, rightMargin, columns) {
12
+ if (columns.widths?.length === columns.count && columns.widths.every((width) => Number.isFinite(width) && width > 0)) return [...columns.widths];
13
+ const equalWidth = (pageWidth - leftMargin - rightMargin - (columns.count - 1) * columns.gap) / columns.count;
14
+ return Array.from({ length: columns.count }, () => equalWidth);
15
15
  }
16
+ const gapAfterColumn = (columns, columnIndex) => columns.gaps?.[columnIndex] ?? columns.gap;
16
17
  function arePageSizesEqual(left, right) {
17
18
  return left.w === right.w && left.h === right.h;
18
19
  }
@@ -49,9 +50,9 @@ function createPaginator(options) {
49
50
  return arePageSizesEqual(state.page.size, pageSize) && areMarginsEqual(state.page.margins, getPageMargins(state.page.number));
50
51
  }
51
52
  if (getContentHeight() <= 0) panic("Paginator: page size and margins yield no content area");
52
- let columnWidth = calculateColumnWidth(pageSize.w, margins.left, margins.right, columns);
53
- function recalculateColumnWidth() {
54
- columnWidth = calculateColumnWidth(pageSize.w, margins.left, margins.right, columns);
53
+ let columnWidths = calculateColumnWidths(pageSize.w, margins.left, margins.right, columns);
54
+ function recalculateColumnWidths() {
55
+ columnWidths = calculateColumnWidths(pageSize.w, margins.left, margins.right, columns);
55
56
  }
56
57
  function applyPendingLayout() {
57
58
  if (pendingPageSize) pageSize = pendingPageSize;
@@ -59,7 +60,7 @@ function createPaginator(options) {
59
60
  pendingPageSize = void 0;
60
61
  pendingMargins = void 0;
61
62
  if (getContentHeight() <= 0) panic("Paginator: section page size and margins yield no content area");
62
- recalculateColumnWidth();
63
+ recalculateColumnWidths();
63
64
  }
64
65
  function getPageMargins(pageNumber) {
65
66
  const pageMargins = pageNumber === 1 && options.firstPageMargins ? { ...options.firstPageMargins } : { ...margins };
@@ -75,7 +76,9 @@ function createPaginator(options) {
75
76
  * Get X position for a given column index.
76
77
  */
77
78
  function getColumnX(columnIndex) {
78
- return (states.at(-1)?.page.margins.left ?? getPageMargins(1).left) + columnIndex * (columnWidth + columns.gap);
79
+ let x = states.at(-1)?.page.margins.left ?? getPageMargins(1).left;
80
+ for (let index = 0; index < columnIndex; index++) x += (columnWidths[index] ?? columnWidths[0] ?? 0) + gapAfterColumn(columns, index);
81
+ return x;
79
82
  }
80
83
  /**
81
84
  * Create a new page and add it to the list.
@@ -274,7 +277,7 @@ function createPaginator(options) {
274
277
  */
275
278
  function updateColumns(newColumns) {
276
279
  columns = newColumns;
277
- columnWidth = calculateColumnWidth(pageSize.w, margins.left, margins.right, columns);
280
+ recalculateColumnWidths();
278
281
  const state = getCurrentState();
279
282
  if (columns.count > 1) state.page.columns = { ...columns };
280
283
  else delete state.page.columns;
@@ -290,7 +293,7 @@ function createPaginator(options) {
290
293
  if (newPageSize) pageSize = { ...newPageSize };
291
294
  if (newMargins) margins = { ...newMargins };
292
295
  if (getContentHeight() <= 0) panic("Paginator: section page size and margins yield no content area");
293
- recalculateColumnWidth();
296
+ recalculateColumnWidths();
294
297
  pendingPageSize = void 0;
295
298
  pendingMargins = void 0;
296
299
  }
@@ -312,7 +315,8 @@ function createPaginator(options) {
312
315
  states,
313
316
  /** Column width in pixels (use getColumnWidth() for current value after updates). */
314
317
  get columnWidth() {
315
- return columnWidth;
318
+ const columnIndex = states.at(-1)?.columnIndex ?? 0;
319
+ return columnWidths[columnIndex] ?? columnWidths[0] ?? getContentWidth();
316
320
  },
317
321
  /** Get current column layout (returns copy to prevent external mutation). */
318
322
  get columns() {
@@ -1,3 +1,4 @@
1
+ import { isFloatingTextBoxBlock } from "./types.js";
1
2
  import { measureParagraph } from "./measure/measureParagraph.js";
2
3
  import { buildTableCellFloatingZones, getTableCellContentWidth, getTableCellFloatingImages } from "./measure/tableCellFloating.js";
3
4
  //#region src/layout-engine/tableRowBreak.ts
@@ -74,6 +75,8 @@ function cellBreakGeometry(cell, measure) {
74
75
  y = blockTop + blockMeasure.totalHeight;
75
76
  paragraphY += blockMeasure.totalHeight;
76
77
  } else if (blockMeasure) {
78
+ const block = cellBlocks?.[i];
79
+ if (block?.kind === "textBox" && isFloatingTextBoxBlock(block)) continue;
77
80
  const blockHeight = getAtomicBlockHeight(blockMeasure);
78
81
  if (blockHeight > 0) {
79
82
  const top = y;
@@ -846,7 +846,9 @@ type Page = {
846
846
  type ColumnLayout = {
847
847
  count: number;
848
848
  gap: number;
849
- equalWidth?: boolean; /** Draw vertical separator line between columns (w:sep). */
849
+ equalWidth?: boolean; /** Authored widths for unequal-width section columns. */
850
+ widths?: number[]; /** Authored space after each column except the last. */
851
+ gaps?: number[]; /** Draw vertical separator line between columns (w:sep). */
850
852
  separator?: boolean;
851
853
  };
852
854
  /**
@@ -904,7 +906,8 @@ type LayoutOptions = {
904
906
  w: number;
905
907
  h: number;
906
908
  }; /** Body-level final section margins. */
907
- finalMargins?: PageMargins; /** Column configuration. */
909
+ finalMargins?: PageMargins; /** Body-level final section column configuration. */
910
+ finalColumns?: ColumnLayout; /** Column configuration. */
908
911
  columns?: ColumnLayout; /** Gap between rendered pages (for UI). */
909
912
  pageGap?: number; /** Default line height multiplier. */
910
913
  defaultLineHeight?: number; /** Header content heights by variant. */
@@ -1,8 +1,8 @@
1
1
  import { FRAGMENT_CLASS_NAMES, renderFragment } from "./renderFragment.js";
2
2
  import { IMAGE_CLASS_NAMES, renderImageFragment } from "./renderImage.js";
3
3
  import { renderLine, renderParagraphFragment, sliceRunsForLine } from "./renderParagraph.js";
4
- import { TABLE_CLASS_NAMES, renderTableFragment } from "./renderTable.js";
5
4
  import { TEXTBOX_CLASS_NAMES, renderTextBoxFragment } from "./renderTextBox.js";
5
+ import { TABLE_CLASS_NAMES, renderTableFragment } from "./renderTable.js";
6
6
  import { renderPage, renderPages } from "./renderPage.js";
7
7
  import { prefersReducedMotionBehavior } from "../paged-layout/scrollNavigation.js";
8
8
  import { createFeatureRegistry } from "./registry/registry.js";
@@ -8,8 +8,8 @@ import { renderFragment } from "./renderFragment.js";
8
8
  import { applyImageVisualAttrs, hasImageVisualAttrs, renderImageFragment } from "./renderImage.js";
9
9
  import { emuToPixels } from "./renderUtils.js";
10
10
  import { renderParagraphFragment } from "./renderParagraph.js";
11
- import { renderTableFragment } from "./renderTable.js";
12
11
  import { renderTextBoxFragment } from "./renderTextBox.js";
12
+ import { renderTableFragment } from "./renderTable.js";
13
13
  import { renderWatermarkLayer } from "./renderWatermark.js";
14
14
  import { panic } from "better-result";
15
15
  //#region src/layout-painter/renderPage.ts
@@ -560,7 +560,12 @@ function renderHeaderFooterContent(content, context, options, layout) {
560
560
  ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
561
561
  ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
562
562
  }, block, measure, hfContext, { document: doc });
563
- fragEl.style.top = `${cursorY}px`;
563
+ const textBoxTop = block.position ? resolveHeaderFooterFloatTop({
564
+ height: measure.height,
565
+ paragraphY: cursorY,
566
+ position: block.position
567
+ }, layout) : cursorY;
568
+ fragEl.style.top = `${textBoxTop}px`;
564
569
  fragEl.style.left = resolveHeaderFooterFloatLeft(measure.width, block.position?.horizontal, layout);
565
570
  if (block.wrapType === "behind") fragEl.style.zIndex = "-1";
566
571
  containerEl.append(fragEl);
@@ -910,7 +910,9 @@ function renderLine(block, line, alignment, doc, options) {
910
910
  if (!options.isLastLine || options.paragraphEndsWithLineBreak) {
911
911
  const firstLineIndentPx = options.isFirstLine ? options.firstLineIndentPx ?? 0 : 0;
912
912
  const firstLineHangingPx = Math.max(0, -firstLineIndentPx);
913
- const justifyCapacityPx = options.availableWidth + firstLineHangingPx;
913
+ const hasVisibleListMarker = options.isFirstLine && block.attrs?.listMarker && !block.attrs.listMarkerHidden;
914
+ const firstLineHangingExpansionPx = hasVisibleListMarker ? Math.min(firstLineHangingPx, Math.max(0, options.leftIndentPx ?? 0)) : firstLineHangingPx;
915
+ const justifyCapacityPx = options.availableWidth + firstLineHangingExpansionPx;
914
916
  const overfullPx = line.width - justifyCapacityPx;
915
917
  const shrinkableSpaces = countShrinkableSpaces(runsForLine);
916
918
  if (overfullPx > RIGHT_EDGE_EPSILON_PX && shrinkableSpaces > 0) {
@@ -925,7 +927,7 @@ function renderLine(block, line, alignment, doc, options) {
925
927
  lineEl.style.textAlign = "justify";
926
928
  lineEl.style.textAlignLast = "justify";
927
929
  }
928
- const listFirstLineOffset = options.isFirstLine && block.attrs?.listMarker && !block.attrs.listMarkerHidden ? options.firstLineIndentPx ?? 0 : 0;
930
+ const listFirstLineOffset = hasVisibleListMarker ? Math.max(firstLineIndentPx, -firstLineHangingExpansionPx) : 0;
929
931
  lineEl.style.width = `${options.availableWidth - listFirstLineOffset}px`;
930
932
  }
931
933
  }
@@ -1249,7 +1251,6 @@ function renderParagraphFragment(fragment, block, measure, context, options = {}
1249
1251
  } else if (indentLeft > 0) lineEl.style.paddingLeft = `${indentLeft}px`;
1250
1252
  else if (hasFirstLine && !isFlexLine) lineEl.style.textIndent = `${indent.firstLine ?? 0}px`;
1251
1253
  } else if (indentLeft > 0) lineEl.style.paddingLeft = `${indentLeft}px`;
1252
- else if (hasHanging && indentLeft === 0) lineEl.style.paddingLeft = `${indent.hanging ?? 0}px`;
1253
1254
  if (indentRight > 0) lineEl.style.paddingRight = `${indentRight}px`;
1254
1255
  if (isFirstLine && block.attrs?.listMarker && !block.attrs.listMarkerHidden) {
1255
1256
  const hanging = indent?.hanging ?? 0;
@@ -1269,7 +1270,7 @@ function renderParagraphFragment(fragment, block, measure, context, options = {}
1269
1270
  const markerFontSize = block.attrs.listMarkerFontSize ?? firstTextRun?.fontSize ?? block.attrs.defaultFontSize;
1270
1271
  const marker = renderListMarker(block.attrs.listMarker, getListMarkerInlineWidth(block), doc, markerFontFamily, markerFontSize, block.attrs.listMarkerRevision, block.attrs.listMarkerSecondSlotOffsetTwips);
1271
1272
  const markerMarginLeft = markerStart - Math.min(indentLeft, 0);
1272
- if (markerMarginLeft < 0 && indentLeft !== 0) marker.style.marginLeft = `${markerMarginLeft}px`;
1273
+ if (markerMarginLeft < 0) marker.style.marginLeft = `${markerMarginLeft}px`;
1273
1274
  lineEl.prepend(marker);
1274
1275
  }
1275
1276
  fragmentEl.append(lineEl);
@@ -1,8 +1,10 @@
1
- import { tableColumnsArePinned } from "../layout-engine/types.js";
1
+ import { emuToPixels } from "../utils/units.js";
2
+ import { isFloatingImageRun, isFloatingTextBoxBlock, tableColumnsArePinned } from "../layout-engine/types.js";
2
3
  import { measureParagraph } from "../layout-engine/measure/measureParagraph.js";
3
4
  import { getAutomaticTextColorForBackground } from "./documentColors.js";
4
5
  import { renderParagraphFragment } from "./renderParagraph.js";
5
6
  import { buildTableCellFloatingZones, getTableCellContentWidth, getTableCellFloatingImages } from "../layout-engine/measure/tableCellFloating.js";
7
+ import { renderTextBoxFragment } from "./renderTextBox.js";
6
8
  //#region src/layout-painter/renderTable.ts
7
9
  /**
8
10
  * Table Renderer
@@ -26,9 +28,6 @@ const TABLE_CLASS_NAMES = {
26
28
  tableEdgeHandleBottom: "layout-table-edge-handle-bottom",
27
29
  tableEdgeHandleRight: "layout-table-edge-handle-right"
28
30
  };
29
- /**
30
- * Render cell content (paragraphs and nested tables)
31
- */
32
31
  function renderCellContent(cell, cellMeasure, context, doc) {
33
32
  const contentEl = doc.createElement("div");
34
33
  contentEl.className = TABLE_CLASS_NAMES.cellContent;
@@ -37,6 +36,7 @@ function renderCellContent(cell, cellMeasure, context, doc) {
37
36
  contentEl.style.width = `${contentWidth}px`;
38
37
  const cellFloatingImages = getTableCellFloatingImages(cell, cellMeasure, contentWidth);
39
38
  const floatingZones = buildTableCellFloatingZones(cellFloatingImages, contentWidth);
39
+ const floatingLayers = [];
40
40
  if (cellFloatingImages.length > 0) {
41
41
  const floatingLayer = doc.createElement("div");
42
42
  floatingLayer.className = "layout-cell-floating-images-layer";
@@ -47,7 +47,7 @@ function renderCellContent(cell, cellMeasure, context, doc) {
47
47
  floatingLayer.style.height = "100%";
48
48
  floatingLayer.style.pointerEvents = "none";
49
49
  floatingLayer.style.zIndex = "10";
50
- floatingLayer.style.overflow = "hidden";
50
+ floatingLayer.style.overflow = "visible";
51
51
  for (const img of cellFloatingImages) {
52
52
  const imgContainer = doc.createElement("div");
53
53
  imgContainer.className = "layout-cell-floating-image";
@@ -67,9 +67,11 @@ function renderCellContent(cell, cellMeasure, context, doc) {
67
67
  imgContainer.append(imgEl);
68
68
  floatingLayer.append(imgContainer);
69
69
  }
70
- contentEl.append(floatingLayer);
70
+ floatingLayers.push(floatingLayer);
71
71
  }
72
72
  let cumulativeY = 0;
73
+ let anchorParagraphY = 0;
74
+ let floatingTextBoxesLayer;
73
75
  for (let i = 0; i < cell.blocks.length; i++) {
74
76
  const block = cell.blocks[i];
75
77
  const measure = cellMeasure.blocks[i];
@@ -103,15 +105,81 @@ function renderCellContent(cell, cellMeasure, context, doc) {
103
105
  const spaceBefore = paragraphBlock.attrs?.spacing?.before ?? 0;
104
106
  if (spaceBefore > 0) fragEl.style.paddingTop = `${spaceBefore}px`;
105
107
  contentEl.append(fragEl);
108
+ anchorParagraphY = cumulativeY;
106
109
  cumulativeY += paragraphMeasure.totalHeight;
107
110
  } else if (block?.kind === "table" && measure?.kind === "table") {
108
111
  const nestedTableEl = renderNestedTable(block, measure, context, doc);
109
112
  nestedTableEl.style.position = "relative";
110
113
  contentEl.append(nestedTableEl);
111
114
  cumulativeY += measure.totalHeight;
115
+ anchorParagraphY = cumulativeY;
116
+ } else if (block?.kind === "textBox" && measure?.kind === "textBox") {
117
+ const textBoxBlock = block;
118
+ const textBoxMeasure = measure;
119
+ const textBoxEl = renderTextBoxFragment({
120
+ kind: "textBox",
121
+ blockId: textBoxBlock.id,
122
+ x: 0,
123
+ y: 0,
124
+ width: textBoxMeasure.width,
125
+ height: textBoxMeasure.height,
126
+ ...textBoxBlock.pmStart !== void 0 ? { pmStart: textBoxBlock.pmStart } : {},
127
+ ...textBoxBlock.pmEnd !== void 0 ? { pmEnd: textBoxBlock.pmEnd } : {}
128
+ }, textBoxBlock, textBoxMeasure, {
129
+ ...context,
130
+ insideTableCell: true
131
+ }, { document: doc });
132
+ if (isFloatingTextBoxBlock(textBoxBlock)) {
133
+ floatingTextBoxesLayer ??= createCellFloatingTextBoxesLayer(doc);
134
+ textBoxEl.style.left = `${resolveCellTextBoxX(textBoxBlock, contentWidth)}px`;
135
+ textBoxEl.style.top = `${anchorParagraphY + resolveCellTextBoxY(textBoxBlock)}px`;
136
+ textBoxEl.style.pointerEvents = "auto";
137
+ floatingTextBoxesLayer.append(textBoxEl);
138
+ continue;
139
+ }
140
+ textBoxEl.style.position = "relative";
141
+ textBoxEl.style.left = "0";
142
+ textBoxEl.style.top = "0";
143
+ contentEl.append(textBoxEl);
144
+ cumulativeY += textBoxMeasure.height;
145
+ anchorParagraphY = cumulativeY;
112
146
  }
113
147
  }
114
- return contentEl;
148
+ if (floatingTextBoxesLayer) floatingLayers.push(floatingTextBoxesLayer);
149
+ return {
150
+ content: contentEl,
151
+ floatingLayers
152
+ };
153
+ }
154
+ function createCellFloatingTextBoxesLayer(doc) {
155
+ const layer = doc.createElement("div");
156
+ layer.className = "layout-cell-floating-text-boxes-layer";
157
+ layer.style.position = "absolute";
158
+ layer.style.inset = "0";
159
+ layer.style.pointerEvents = "none";
160
+ layer.style.zIndex = "10";
161
+ layer.style.overflow = "visible";
162
+ return layer;
163
+ }
164
+ function resolveCellTextBoxX(block, contentWidth) {
165
+ const horizontal = block.position?.horizontal;
166
+ if (horizontal?.posOffset !== void 0) return emuToPixels(horizontal.posOffset);
167
+ if (horizontal?.align === "center") return (contentWidth - block.width) / 2;
168
+ if (horizontal?.align === "right" || horizontal?.align === "outside") return contentWidth - block.width;
169
+ return 0;
170
+ }
171
+ function resolveCellTextBoxY(block) {
172
+ const vertical = block.position?.vertical;
173
+ if (vertical?.posOffset !== void 0) return emuToPixels(vertical.posOffset);
174
+ return 0;
175
+ }
176
+ function tableHasFloatingCellContent(block) {
177
+ for (const row of block.rows) for (const cell of row.cells) for (const cellBlock of cell.blocks) {
178
+ if (cellBlock.kind === "textBox" && isFloatingTextBoxBlock(cellBlock)) return true;
179
+ if (cellBlock.kind === "paragraph" && cellBlock.runs.some((run) => run.kind === "image" && isFloatingImageRun(run))) return true;
180
+ if (cellBlock.kind === "table" && tableHasFloatingCellContent(cellBlock)) return true;
181
+ }
182
+ return false;
115
183
  }
116
184
  /**
117
185
  * Render a nested table (within a cell)
@@ -214,8 +282,18 @@ function renderTableCell(cell, cellMeasure, x, rowHeight, borderFlags, columnsPi
214
282
  default: break;
215
283
  }
216
284
  }
217
- const contentEl = renderCellContent(cell, cellMeasure, context, doc);
218
- cellEl.append(contentEl);
285
+ const renderedContent = renderCellContent(cell, cellMeasure, context, doc);
286
+ if (renderedContent.floatingLayers.length > 0) {
287
+ renderedContent.content.style.height = "100%";
288
+ renderedContent.content.style.overflow = "hidden";
289
+ cellEl.style.overflow = "visible";
290
+ }
291
+ cellEl.append(renderedContent.content);
292
+ for (const floatingLayer of renderedContent.floatingLayers) {
293
+ floatingLayer.style.left = `${padLeft}px`;
294
+ floatingLayer.style.top = `${padTop}px`;
295
+ cellEl.append(floatingLayer);
296
+ }
219
297
  if (cell.blocks.length > 0) {
220
298
  const firstBlock = cell.blocks.at(0);
221
299
  const lastBlock = cell.blocks.at(-1);
@@ -316,7 +394,7 @@ function renderTableFragment(fragment, block, measure, context, options = {}) {
316
394
  tableEl.style.position = "absolute";
317
395
  tableEl.style.width = `${fragment.width}px`;
318
396
  tableEl.style.height = `${fragment.height}px`;
319
- tableEl.style.overflow = "hidden";
397
+ tableEl.style.overflow = tableHasFloatingCellContent(block) ? "visible" : "hidden";
320
398
  tableEl.dataset["blockId"] = String(fragment.blockId);
321
399
  tableEl.dataset["fromRow"] = String(fragment.fromRow);
322
400
  tableEl.dataset["toRow"] = String(fragment.toRow);
@@ -20,8 +20,10 @@ function computePerBlockWidths({ blocks, bodyConfig, finalConfig }) {
20
20
  }).widths;
21
21
  }
22
22
  function computePerBlockMeasureInputs({ blocks, bodyConfig, finalConfig }) {
23
- function colWidth(cw, cols) {
23
+ function colWidth(cw, cols, columnIndex) {
24
24
  if (cols.count <= 1) return cw;
25
+ const authoredWidth = cols.widths?.[columnIndex];
26
+ if (authoredWidth !== void 0) return authoredWidth;
25
27
  return Math.floor((cw - (cols.count - 1) * cols.gap) / cols.count);
26
28
  }
27
29
  function contentWidth(config) {
@@ -29,17 +31,23 @@ function computePerBlockMeasureInputs({ blocks, bodyConfig, finalConfig }) {
29
31
  }
30
32
  const { configs: sectionConfigs, breakIndices } = collectSectionConfigs(blocks, bodyConfig, finalConfig);
31
33
  let sectionIdx = 0;
34
+ let columnIndex = 0;
32
35
  const widths = [];
33
36
  const marginTops = [];
34
37
  const pageHeights = [];
35
38
  const marginBottoms = [];
36
39
  for (let i = 0; i < blocks.length; i++) {
37
40
  const config = sectionConfigs[sectionIdx] ?? finalConfig;
38
- widths.push(colWidth(contentWidth(config), config.columns ?? SINGLE_COLUMN_LAYOUT));
41
+ const columns = config.columns ?? SINGLE_COLUMN_LAYOUT;
42
+ widths.push(colWidth(contentWidth(config), columns, columnIndex));
39
43
  marginTops.push(config.margins.top);
40
44
  pageHeights.push(config.pageSize.h);
41
45
  marginBottoms.push(config.margins.bottom);
42
- if (sectionIdx < breakIndices.length && i === breakIndices[sectionIdx]) sectionIdx++;
46
+ if (sectionIdx < breakIndices.length && i === breakIndices[sectionIdx]) {
47
+ sectionIdx++;
48
+ columnIndex = 0;
49
+ } else if (blocks[i]?.kind === "pageBreak") columnIndex = 0;
50
+ else if (blocks[i]?.kind === "columnBreak") columnIndex = (columnIndex + 1) % columns.count;
43
51
  }
44
52
  return {
45
53
  widths,
@@ -1430,9 +1430,18 @@ function tableRowAttrsToFormatting(attrs) {
1430
1430
  function convertPMTableCell(node, documentCounts) {
1431
1431
  const attrs = expectTableCellAttrs(node);
1432
1432
  const content = [];
1433
+ let previousStandaloneTextBox = null;
1433
1434
  node.forEach((contentNode) => {
1434
- if (contentNode.type.name === "paragraph") content.push(convertPMParagraph(contentNode, documentCounts));
1435
- else if (contentNode.type.name === "table") content.push(convertPMTable(contentNode, documentCounts));
1435
+ if (contentNode.type.name === "paragraph") {
1436
+ content.push(convertPMParagraph(contentNode, documentCounts));
1437
+ previousStandaloneTextBox = null;
1438
+ } else if (contentNode.type.name === "table") {
1439
+ content.push(convertPMTable(contentNode, documentCounts));
1440
+ previousStandaloneTextBox = null;
1441
+ } else if (contentNode.type.name === "textBox") previousStandaloneTextBox = appendTextBoxBlock(content, contentNode, {
1442
+ pendingPageBreaks: 0,
1443
+ previousStandaloneTextBox
1444
+ });
1436
1445
  });
1437
1446
  const cell = {
1438
1447
  type: "tableCell",
@@ -24,15 +24,18 @@ function toProseDoc(document, options) {
24
24
  const styleResolver = createStyleEngine(options?.styles ?? document.package.styles);
25
25
  const theme = options?.theme ?? document.package.theme ?? null;
26
26
  let textBoxGroupIndex = 0;
27
+ const nextTextBoxGroupId = () => String(textBoxGroupIndex++);
27
28
  const convertBodyBlocks = (blocks) => {
28
29
  const out = [];
29
30
  for (const block of blocks) if (block.type === "paragraph") {
30
31
  const pbPos = paragraphPageBreakPosition(block);
31
32
  if (pbPos === "before") out.push(schema.node("pageBreak"));
32
- out.push(...convertParagraphWithTextBoxes(block, styleResolver, String(textBoxGroupIndex)));
33
- textBoxGroupIndex += 1;
33
+ out.push(...convertParagraphWithTextBoxes(block, styleResolver, { textBoxGroupId: nextTextBoxGroupId() }));
34
34
  if (pbPos === "after") out.push(schema.node("pageBreak"));
35
- } else if (block.type === "table") out.push(convertTable(block, styleResolver, theme));
35
+ } else if (block.type === "table") out.push(convertTable(block, styleResolver, {
36
+ theme,
37
+ nextTextBoxGroupId
38
+ }));
36
39
  else out.push(convertBlockSdt(block, convertBodyBlocks));
37
40
  return out;
38
41
  };
@@ -535,7 +538,7 @@ function paragraphContentHasMeaningfulContent(content) {
535
538
  if (content.type === "insertion" || content.type === "deletion" || content.type === "moveFrom" || content.type === "moveTo") return content.content.some(paragraphContentHasMeaningfulContent);
536
539
  return true;
537
540
  }
538
- function convertTable(table, styleResolver, theme) {
541
+ function convertTable(table, styleResolver, context) {
539
542
  const rowSpanMap = calculateRowSpans(table);
540
543
  const columnWidths = table.columnWidths;
541
544
  const totalWidth = columnWidths?.reduce((sum, w) => sum + w, 0) ?? 0;
@@ -604,7 +607,7 @@ function convertTable(table, styleResolver, theme) {
604
607
  })();
605
608
  })();
606
609
  if (bandingEnabledH && !isFirstRowStyled && !isLastRow) dataRowIndex++;
607
- return convertTableRow(row, styleResolver, isFirstRowStyled, columnWidths, totalWidth, conditionalStyles, rowBandStyle, bandingEnabledV, look, resolvedTableBorders, rowIndex, totalRows, totalColumns, rowSpanMap, cellMarginsAttr, theme);
610
+ return convertTableRow(row, styleResolver, context, isFirstRowStyled, columnWidths, totalWidth, conditionalStyles, rowBandStyle, bandingEnabledV, look, resolvedTableBorders, rowIndex, totalRows, totalColumns, rowSpanMap, cellMarginsAttr);
608
611
  });
609
612
  return schema.node("table", attrs, rows);
610
613
  }
@@ -620,7 +623,7 @@ function countTableColumns(rows) {
620
623
  /**
621
624
  * Convert a TableRow to a ProseMirror table row node
622
625
  */
623
- function convertTableRow(row, styleResolver, isHeaderRow, columnWidths, totalWidth, conditionalStyles, rowBandStyle, bandingEnabledV, tableLook, tableBorders, rowIndex, totalRows, totalColumns, rowSpanMap, defaultCellMargins, theme) {
626
+ function convertTableRow(row, styleResolver, context, isHeaderRow, columnWidths, totalWidth, conditionalStyles, rowBandStyle, bandingEnabledV, tableLook, tableBorders, rowIndex, totalRows, totalColumns, rowSpanMap, defaultCellMargins) {
624
627
  const attrs = { isHeader: !!row.formatting?.header };
625
628
  if (row.formatting?.height?.value !== void 0) attrs.height = row.formatting.height.value;
626
629
  if (row.formatting?.heightRule) attrs.heightRule = row.formatting.heightRule;
@@ -695,7 +698,7 @@ function convertTableRow(row, styleResolver, isHeaderRow, columnWidths, totalWid
695
698
  if (cellIsFirstRow && cellIsLastCol && (tableLook?.firstRow || rowCnf?.firstRow || cellCnf?.firstRow) && (tableLook?.lastColumn || rowCnf?.lastColumn || cellCnf?.lastColumn)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.neCell);
696
699
  if (cellIsLastRow && cellIsFirstCol && (tableLook?.lastRow || rowCnf?.lastRow || cellCnf?.lastRow) && (tableLook?.firstColumn || rowCnf?.firstColumn || cellCnf?.firstColumn)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.swCell);
697
700
  if (cellIsLastRow && cellIsLastCol && (tableLook?.lastRow || rowCnf?.lastRow || cellCnf?.lastRow) && (tableLook?.lastColumn || rowCnf?.lastColumn || cellCnf?.lastColumn)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.seCell);
698
- cells.push(convertTableCell(cell, styleResolver, isHeaderRow, gridWidth, cellConditionalStyle, tableBorders, isFirstRow, isLastRow, isFirstCol, isLastCol, calculatedRowSpan, preserveVMergeRestart, rowSpanInfo?.continuationCells, defaultCellMargins, theme));
701
+ cells.push(convertTableCell(cell, styleResolver, context, isHeaderRow, gridWidth, cellConditionalStyle, tableBorders, isFirstRow, isLastRow, isFirstCol, isLastCol, calculatedRowSpan, preserveVMergeRestart, rowSpanInfo?.continuationCells, defaultCellMargins));
699
702
  }
700
703
  return schema.node("tableRow", attrs, cells);
701
704
  }
@@ -724,7 +727,8 @@ function resolveThemedBorderColors(borders, theme) {
724
727
  /**
725
728
  * Convert a TableCell to a ProseMirror table cell node
726
729
  */
727
- function convertTableCell(cell, styleResolver, isHeader, gridWidthPercent, conditionalStyle, tableBorders, isFirstRow, isLastRow, isFirstCol, isLastCol, calculatedRowSpan, preserveVMergeRestart, vMergeContinuationCells, defaultCellMargins, theme) {
730
+ function convertTableCell(cell, styleResolver, context, isHeader, gridWidthPercent, conditionalStyle, tableBorders, isFirstRow, isLastRow, isFirstCol, isLastCol, calculatedRowSpan, preserveVMergeRestart, vMergeContinuationCells, defaultCellMargins) {
731
+ const { theme } = context;
728
732
  const formatting = cell.formatting;
729
733
  const rowspan = calculatedRowSpan ?? 1;
730
734
  let width = formatting?.width?.value;
@@ -776,8 +780,12 @@ function convertTableCell(cell, styleResolver, isHeader, gridWidthPercent, condi
776
780
  if (preserveVMergeRestart) attrs._preserveVMergeRestart = true;
777
781
  if (vMergeContinuationCells && vMergeContinuationCells.length > 0) attrs._docxVMergeContinuationCells = vMergeContinuationCells;
778
782
  const contentNodes = [];
779
- for (const content of cell.content) if (content.type === "paragraph") contentNodes.push(convertParagraph(content, styleResolver, void 0, conditionalStyle?.rPr, conditionalStyle?.pPr));
780
- else contentNodes.push(convertTable(content, styleResolver, theme));
783
+ for (const content of cell.content) if (content.type === "paragraph") contentNodes.push(...convertParagraphWithTextBoxes(content, styleResolver, {
784
+ textBoxGroupId: context.nextTextBoxGroupId(),
785
+ ...conditionalStyle?.rPr !== void 0 ? { extraRunFormatting: conditionalStyle.rPr } : {},
786
+ ...conditionalStyle?.pPr !== void 0 ? { tableParagraphOverlay: conditionalStyle.pPr } : {}
787
+ }));
788
+ else contentNodes.push(convertTable(content, styleResolver, context));
781
789
  if (contentNodes.length === 0) contentNodes.push(schema.node("paragraph", {}, []));
782
790
  const nodeType = isHeader ? "tableHeader" : "tableCell";
783
791
  return schema.node(nodeType, attrs, contentNodes);
@@ -1214,13 +1222,9 @@ function convertShape(shape) {
1214
1222
  position
1215
1223
  });
1216
1224
  }
1217
- /**
1218
- * Convert a paragraph block to PM nodes, extracting text boxes as sibling nodes.
1219
- * Skips ghost empty paragraphs that only contained text box drawings.
1220
- */
1221
- function convertParagraphWithTextBoxes(block, styleResolver, textBoxGroupId) {
1225
+ function convertParagraphWithTextBoxes(block, styleResolver, { textBoxGroupId, extraRunFormatting, tableParagraphOverlay }) {
1222
1226
  const textBoxes = extractTextBoxesFromParagraph(block);
1223
- const pmParagraph = convertParagraph(block, styleResolver);
1227
+ const pmParagraph = convertParagraph(block, styleResolver, void 0, extraRunFormatting, tableParagraphOverlay);
1224
1228
  const nodes = [];
1225
1229
  const isEmptyAfterExtraction = textBoxes.length > 0 && pmParagraph.content.size === 0;
1226
1230
  const keepWrapperParagraph = isEmptyAfterExtraction && hasParagraphBoundaryPayload(block, pmParagraph);
@@ -1361,12 +1365,14 @@ function headerFooterToProseDoc(content, options) {
1361
1365
  const styleResolver = options?.styles ? createStyleEngine(options.styles) : null;
1362
1366
  const theme = options?.theme ?? null;
1363
1367
  let textBoxGroupIndex = 0;
1368
+ const nextTextBoxGroupId = () => String(textBoxGroupIndex++);
1364
1369
  const convertBlocks = (blocks) => {
1365
1370
  const out = [];
1366
- for (const block of blocks) if (block.type === "paragraph") {
1367
- out.push(...convertParagraphWithTextBoxes(block, styleResolver, String(textBoxGroupIndex)));
1368
- textBoxGroupIndex += 1;
1369
- } else if (block.type === "table") out.push(convertTable(block, styleResolver, theme));
1371
+ for (const block of blocks) if (block.type === "paragraph") out.push(...convertParagraphWithTextBoxes(block, styleResolver, { textBoxGroupId: nextTextBoxGroupId() }));
1372
+ else if (block.type === "table") out.push(convertTable(block, styleResolver, {
1373
+ theme,
1374
+ nextTextBoxGroupId
1375
+ }));
1370
1376
  else out.push(convertBlockSdt(block, convertBlocks));
1371
1377
  return out;
1372
1378
  };
@@ -298,7 +298,7 @@ function buildCellWidthStyles(attrs) {
298
298
  return styles;
299
299
  }
300
300
  const tableCellSpec = {
301
- content: "(paragraph | table)+",
301
+ content: "(paragraph | table | textBox)+",
302
302
  tableRole: "cell",
303
303
  isolating: true,
304
304
  attrs: {
@@ -351,7 +351,7 @@ const tableCellSpec = {
351
351
  }
352
352
  };
353
353
  const tableHeaderSpec = {
354
- content: "(paragraph | table)+",
354
+ content: "(paragraph | table | textBox)+",
355
355
  tableRole: "header_cell",
356
356
  isolating: true,
357
357
  attrs: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/folio-core",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Headless, framework-neutral core of folio: the OOXML (.docx) parser, document model, ProseMirror integration, and page-layout engine. No React.",
5
5
  "keywords": [
6
6
  "document-model",