@stll/folio-core 0.24.0 → 0.25.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 (35) hide show
  1. package/dist/controller/layoutPipeline.js +3 -4
  2. package/dist/docx/fieldParser.d.ts +1 -1
  3. package/dist/docx/fieldParser.js +2 -9
  4. package/dist/docx/paragraphTextBoxEnrichment.js +1 -0
  5. package/dist/docx/sectionParser.js +2 -2
  6. package/dist/docx/serializer/runSerializer.js +1 -0
  7. package/dist/docx/textBoxParser.js +4 -0
  8. package/dist/fields/fieldContext.d.ts +2 -1
  9. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +9 -1
  10. package/dist/layout-bridge/convert/headerFooterLayout.js +26 -1
  11. package/dist/layout-bridge/convert/toFlowBlocks.js +1 -0
  12. package/dist/layout-bridge/engine/selectionRects.js +3 -5
  13. package/dist/layout-engine/index.d.ts +2 -2
  14. package/dist/layout-engine/index.js +3 -5
  15. package/dist/layout-engine/measure/measureBlocks.js +27 -11
  16. package/dist/layout-engine/measure/measureParagraph.js +7 -1
  17. package/dist/layout-engine/measure/tableCellFloating.js +2 -3
  18. package/dist/layout-engine/renderedBreakReconciliation.js +1 -1
  19. package/dist/layout-engine/tableRowBreak.js +2 -2
  20. package/dist/layout-engine/types.d.ts +13 -8
  21. package/dist/layout-engine/types.js +15 -1
  22. package/dist/layout-painter/renderParagraph.d.ts +9 -1
  23. package/dist/layout-painter/renderParagraph.js +57 -35
  24. package/dist/layout-painter/renderTable.js +2 -5
  25. package/dist/layout-painter/renderTextBox.js +1 -1
  26. package/dist/layout-painter/renderUtils.d.ts +2 -1
  27. package/dist/prosemirror/attrs/index.js +2 -0
  28. package/dist/prosemirror/conversion/fromProseDoc.js +1 -0
  29. package/dist/prosemirror/conversion/toProseDoc.js +2 -0
  30. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +2 -0
  31. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +7 -0
  32. package/dist/prosemirror/schema/nodes.d.ts +2 -0
  33. package/dist/utils/paragraphBaseDirection.d.ts +6 -0
  34. package/dist/utils/paragraphBaseDirection.js +12 -0
  35. package/package.json +2 -2
@@ -26,11 +26,12 @@ function bodyMarginsClearHeaderFooter({ authoredMargins, preparedHeader, prepare
26
26
  const headerBottom = preparedHeader ? (authoredMargins.header ?? 0) + (preparedHeader.marginPushBottom ?? preparedHeader.height) : authoredMargins.top;
27
27
  const footerClearance = preparedFooter ? (authoredMargins.footer ?? 0) + (preparedFooter.marginPushBottom ?? preparedFooter.height) : authoredMargins.bottom;
28
28
  const top = Math.max(authoredMargins.top, headerBottom);
29
+ const clearedTop = Math.max(top, preparedHeader?.bodyTopClearance ?? 0);
29
30
  const bottom = Math.max(authoredMargins.bottom, footerClearance);
30
- if (top === authoredMargins.top && bottom === authoredMargins.bottom) return authoredMargins;
31
+ if (clearedTop === authoredMargins.top && bottom === authoredMargins.bottom) return authoredMargins;
31
32
  return {
32
33
  ...authoredMargins,
33
- top,
34
+ top: clearedTop,
34
35
  bottom
35
36
  };
36
37
  }
@@ -255,7 +256,6 @@ function runLayoutPipeline(deps, state, options = {}) {
255
256
  let newLayout;
256
257
  let pageFootnoteMap = /* @__PURE__ */ new Map();
257
258
  let footnoteContentMap = /* @__PURE__ */ new Map();
258
- const bodyBreakType = finalSectionProperties?.sectionStart;
259
259
  const buildLayoutOpts = () => {
260
260
  const nextLayoutOpts = {
261
261
  pageSize,
@@ -279,7 +279,6 @@ function runLayoutPipeline(deps, state, options = {}) {
279
279
  nextLayoutOpts.finalPageNumbering = finalLayoutConfig.pageNumbering;
280
280
  }
281
281
  if (columns !== void 0) nextLayoutOpts.columns = columns;
282
- if (bodyBreakType !== void 0) nextLayoutOpts.bodyBreakType = bodyBreakType;
283
282
  if (sectionHeaderFooterRefs !== void 0) nextLayoutOpts.sectionHeaderFooterRefs = sectionHeaderFooterRefs;
284
283
  if (sectionEvenPageMargins !== void 0) nextLayoutOpts.sectionEvenPageMargins = sectionEvenPageMargins;
285
284
  return nextLayoutOpts;
@@ -188,7 +188,7 @@ declare function isTocField(field: document_d_exports.Field): boolean;
188
188
  * @param instruction - Parsed instruction for format switches
189
189
  * @returns Formatted page number string
190
190
  */
191
- declare function computePageNumber(pageNumber: number, instruction?: ParsedFieldInstruction, sectionFormat?: string): string;
191
+ declare function computePageNumber(pageNumber: number, instruction?: ParsedFieldInstruction, sectionFormat?: document_d_exports.NumberFormat): string;
192
192
  /**
193
193
  * Format a date according to a format string
194
194
  *
@@ -1,3 +1,4 @@
1
+ import { formatOoxmlCounter } from "./ooxmlCounterFormatter.js";
1
2
  import { FieldTypeSchema, narrowEnum } from "./parserEnums.js";
2
3
  import { parseRun } from "./runParser.js";
3
4
  import { findChildren, getAttribute } from "./xmlParser.js";
@@ -354,15 +355,7 @@ function computePageNumber(pageNumber, instruction, sectionFormat) {
354
355
  default: return String(pageNumber);
355
356
  }
356
357
  }
357
- function formatSectionPageNumber(pageNumber, format) {
358
- switch (format) {
359
- case "upperRoman": return toRoman(pageNumber);
360
- case "lowerRoman": return toRoman(pageNumber).toLowerCase();
361
- case "upperLetter": return toLetter(pageNumber);
362
- case "lowerLetter": return toLetter(pageNumber).toLowerCase();
363
- default: return String(pageNumber);
364
- }
365
- }
358
+ const formatSectionPageNumber = (pageNumber, format) => formatOoxmlCounter(pageNumber, format);
366
359
  /**
367
360
  * Convert number to uppercase Roman numerals
368
361
  */
@@ -158,6 +158,7 @@ const enrichTextBoxRuns = ({ content, xmlChildren, styles, theme, numbering, rel
158
158
  textBody: {
159
159
  content: textBox.content,
160
160
  ...textBox.autoFit !== void 0 ? { autoFit: textBox.autoFit } : {},
161
+ ...textBox.textWrap !== void 0 ? { textWrap: textBox.textWrap } : {},
161
162
  ...textBox.margins !== void 0 ? { margins: textBox.margins } : {}
162
163
  }
163
164
  };
@@ -1,6 +1,6 @@
1
1
  import { parseFooterReference, parseHeaderReference } from "./headerFooterRefParser.js";
2
2
  import { parseEndnoteProperties, parseFootnoteProperties } from "./notePropertiesParser.js";
3
- import { BorderStyleSchema, ThemeColorSlotSchema, narrowEnum } from "./parserEnums.js";
3
+ import { BorderStyleSchema, NumberFormatSchema, ThemeColorSlotSchema, narrowEnum } from "./parserEnums.js";
4
4
  import { findChild, findChildren, getAttribute, getChildElements, getLocalName, parseBooleanElement, parseNumericAttribute } from "./xmlParser.js";
5
5
  import { normalizeRevisionId } from "@stll/docx-core/model";
6
6
  //#region src/docx/sectionParser.ts
@@ -272,7 +272,7 @@ function parseSectionProperties(sectPr) {
272
272
  const pgNumType = findChild(sectPr, "w", "pgNumType");
273
273
  if (pgNumType) {
274
274
  const pageNumbering = {};
275
- const format = getAttribute(pgNumType, "w", "fmt");
275
+ const format = narrowEnum(getAttribute(pgNumType, "w", "fmt"), NumberFormatSchema);
276
276
  if (format) pageNumbering.format = format;
277
277
  const start = parseNumericAttribute(pgNumType, "w", "start");
278
278
  if (start !== void 0) pageNumbering.start = start;
@@ -479,6 +479,7 @@ function serializeShapeContent(content) {
479
479
  if (shape.textBody) {
480
480
  const tb = shape.textBody;
481
481
  const bpAttrs = ["rot=\"0\"", "vert=\"horz\""];
482
+ if (tb.textWrap) bpAttrs.push(`wrap="${tb.textWrap}"`);
482
483
  if (tb.anchor) bpAttrs.push(`anchor="${tb.anchor === "middle" ? "ctr" : tb.anchor}"`);
483
484
  if (tb.anchorCenter) bpAttrs.push("anchorCtr=\"1\"");
484
485
  if (tb.margins) {
@@ -11,6 +11,8 @@ const DEFAULT_MARGIN_EMU = 91440;
11
11
  function parseBodyProperties(bodyPr) {
12
12
  if (!bodyPr) return {};
13
13
  const result = {};
14
+ const textWrap = getAttribute(bodyPr, null, "wrap");
15
+ if (textWrap === "none" || textWrap === "square") result.textWrap = textWrap;
14
16
  if (findChildByLocalName(bodyPr, "spAutoFit")) result.autoFit = "shape";
15
17
  else if (findChildByLocalName(bodyPr, "normAutofit")) result.autoFit = "normal";
16
18
  else if (findChildByLocalName(bodyPr, "noAutofit")) result.autoFit = "none";
@@ -128,6 +130,7 @@ function parseTextBox(drawingEl) {
128
130
  if (outline) textBox.outline = outline;
129
131
  if (bodyProps.margins) textBox.margins = bodyProps.margins;
130
132
  if (bodyProps.autoFit) textBox.autoFit = bodyProps.autoFit;
133
+ if (bodyProps.textWrap) textBox.textWrap = bodyProps.textWrap;
131
134
  if (isAnchor) {
132
135
  const position = parseAnchorPosition(container);
133
136
  if (position) textBox.position = position;
@@ -170,6 +173,7 @@ function parseTextBoxFromShape(wsp, size, position, wrap) {
170
173
  if (outline) textBox.outline = outline;
171
174
  if (bodyProps.margins) textBox.margins = bodyProps.margins;
172
175
  if (bodyProps.autoFit) textBox.autoFit = bodyProps.autoFit;
176
+ if (bodyProps.textWrap) textBox.textWrap = bodyProps.textWrap;
173
177
  if (position) textBox.position = position;
174
178
  if (wrap) textBox.wrap = wrap;
175
179
  return textBox;
@@ -1,3 +1,4 @@
1
+ import { document_d_exports } from "../types/document.js";
1
2
  //#region src/fields/fieldContext.d.ts
2
3
  /**
3
4
  * Resolution context for evaluating dynamic DOCX fields against a laid-out
@@ -10,7 +11,7 @@ type FieldContext = {
10
11
  /** 1-indexed page the field is on (PAGE). */
11
12
  pageNumber: number;
12
13
  /** OOXML section-level format used when PAGE has no explicit format switch. */
13
- pageNumberFormat?: string;
14
+ pageNumberFormat?: document_d_exports.NumberFormat;
14
15
  /** Total pages in the document (NUMPAGES). */
15
16
  totalPages: number;
16
17
  /** Pages in the field's current section (SECTIONPAGES); omit until the
@@ -30,6 +30,14 @@ declare function calculateHeaderFooterMarginPushBounds(blocks: FlowBlock[], meas
30
30
  top: number;
31
31
  bottom: number;
32
32
  };
33
+ /**
34
+ * Find the lower page edge of header `wrapTopAndBottom` artwork that overlaps
35
+ * the authored body start. Unlike ordinary anchored artwork, this wrap mode
36
+ * reserves a full-width horizontal band even when the drawing paints behind
37
+ * text. Bands wholly below the body start need page-local flow support and are
38
+ * intentionally not flattened into a top margin here.
39
+ */
40
+ declare function calculateHeaderFooterBodyTopClearance(blocks: FlowBlock[], flowHeight: number, metrics: HeaderFooterMetrics): number | undefined;
33
41
  type ConvertHeaderFooterOptions = {
34
42
  styles?: document_d_exports.StyleDefinitions | null;
35
43
  theme?: document_d_exports.Theme | null;
@@ -74,4 +82,4 @@ declare function convertHeaderFooterToContent(headerFooter: document_d_exports.H
74
82
  */
75
83
  declare function convertHeaderFooterPmDocToContent(pmDoc: Node | null | undefined, contentWidth: number, metrics: HeaderFooterMetrics, options: Omit<ConvertHeaderFooterOptions, "styles">): HeaderFooterContent | undefined;
76
84
  //#endregion
77
- export { ConvertHeaderFooterOptions, HeaderFooterMetrics, calculateHeaderFooterMarginPushBounds, calculateHeaderFooterVisualBounds, convertHeaderFooterPmDocToContent, convertHeaderFooterToContent, normalizeHeaderFooterMeasureBlocks, resolveHeaderFooterPositionedVisualTop, resolveHeaderFooterVisualTop };
85
+ export { ConvertHeaderFooterOptions, HeaderFooterMetrics, calculateHeaderFooterBodyTopClearance, calculateHeaderFooterMarginPushBounds, calculateHeaderFooterVisualBounds, convertHeaderFooterPmDocToContent, convertHeaderFooterToContent, normalizeHeaderFooterMeasureBlocks, resolveHeaderFooterPositionedVisualTop, resolveHeaderFooterVisualTop };
@@ -290,6 +290,29 @@ function calculateHeaderFooterMarginPushBounds(blocks, measures, flowHeight, met
290
290
  };
291
291
  }
292
292
  /**
293
+ * Find the lower page edge of header `wrapTopAndBottom` artwork that overlaps
294
+ * the authored body start. Unlike ordinary anchored artwork, this wrap mode
295
+ * reserves a full-width horizontal band even when the drawing paints behind
296
+ * text. Bands wholly below the body start need page-local flow support and are
297
+ * intentionally not flattened into a top margin here.
298
+ */
299
+ function calculateHeaderFooterBodyTopClearance(blocks, flowHeight, metrics) {
300
+ if (metrics.section !== "header") return;
301
+ const flowTop = metrics.margins.header ?? 48;
302
+ let clearance = 0;
303
+ for (const block of blocks) {
304
+ if (block.kind !== "paragraph") continue;
305
+ for (const run of block.runs) {
306
+ if (run.kind !== "image" || run.wrapType !== "topAndBottom" || run.position?.vertical?.relativeTo !== "page" && run.position?.vertical?.relativeTo !== "margin") continue;
307
+ const pageTop = flowTop + resolveHeaderFooterVisualTop(run, 0, flowHeight, metrics) - (run.distTop ?? 0);
308
+ if (pageTop > metrics.margins.top) continue;
309
+ const pageBottom = Math.min(metrics.pageSize.h, pageTop + (run.distTop ?? 0) + run.height + (run.distBottom ?? 0));
310
+ clearance = Math.max(clearance, pageBottom);
311
+ }
312
+ }
313
+ return clearance > 0 ? clearance : void 0;
314
+ }
315
+ /**
293
316
  * Convert HeaderFooter (document type) to HeaderFooterContent (render type).
294
317
  *
295
318
  * Routes through the same pipeline as the body: HF.content ->
@@ -350,6 +373,7 @@ function finalizeHeaderFooterContent(blocks, contentWidth, metrics, options) {
350
373
  }
351
374
  const { visualTop, visualBottom } = calculateHeaderFooterVisualBounds(blocks, measures, flowHeight, metrics);
352
375
  const { top: marginPushTop, bottom: marginPushBottom } = calculateHeaderFooterMarginPushBounds(blocks, measures, flowHeight, metrics);
376
+ const bodyTopClearance = calculateHeaderFooterBodyTopClearance(blocks, flowHeight, metrics);
353
377
  return {
354
378
  blocks,
355
379
  measures,
@@ -358,6 +382,7 @@ function finalizeHeaderFooterContent(blocks, contentWidth, metrics, options) {
358
382
  visualBottom,
359
383
  marginPushTop,
360
384
  marginPushBottom,
385
+ ...bodyTopClearance !== void 0 ? { bodyTopClearance } : {},
361
386
  textSig: computeHeaderFooterTextSig(blocks),
362
387
  ...options.rId ? { rId: options.rId } : {}
363
388
  };
@@ -489,4 +514,4 @@ function serializeRunFmt(run) {
489
514
  return JSON.stringify(out);
490
515
  }
491
516
  //#endregion
492
- export { calculateHeaderFooterMarginPushBounds, calculateHeaderFooterVisualBounds, convertHeaderFooterPmDocToContent, convertHeaderFooterToContent, normalizeHeaderFooterMeasureBlocks, resolveHeaderFooterPositionedVisualTop, resolveHeaderFooterVisualTop };
517
+ export { calculateHeaderFooterBodyTopClearance, calculateHeaderFooterMarginPushBounds, calculateHeaderFooterVisualBounds, convertHeaderFooterPmDocToContent, convertHeaderFooterToContent, normalizeHeaderFooterMeasureBlocks, resolveHeaderFooterPositionedVisualTop, resolveHeaderFooterVisualTop };
@@ -1221,6 +1221,7 @@ function convertTextBoxNode(node, startPos, opts) {
1221
1221
  };
1222
1222
  if (attrs.height !== void 0) textBox.height = attrs.height;
1223
1223
  if (attrs.autoFit !== void 0) textBox.autoFit = attrs.autoFit;
1224
+ if (attrs.textWrap !== void 0) textBox.textWrap = attrs.textWrap;
1224
1225
  if (attrs.fillColor !== void 0) textBox.fillColor = attrs.fillColor;
1225
1226
  if (attrs.outlineWidth !== void 0) textBox.outlineWidth = attrs.outlineWidth;
1226
1227
  if (attrs.outlineColor !== void 0) textBox.outlineColor = attrs.outlineColor;
@@ -4,7 +4,7 @@ import { buildRunFontStyle } from "../../layout-engine/measure/measureHelpers.js
4
4
  import { measureParagraph } from "../../layout-engine/measure/measureParagraph.js";
5
5
  import { measureRun } from "../../layout-engine/measure/measureProvider.js";
6
6
  import { buildTableCellFloatingZones, getTableCellContentWidth, getTableCellFloatingImages } from "../../layout-engine/measure/tableCellFloating.js";
7
- import { getTableRowLeadingWidth } from "../../layout-engine/types.js";
7
+ import { getTableRowLeadingWidth, resolveTableCellPadding } from "../../layout-engine/types.js";
8
8
  import { inlineImageBoundingBox } from "../../utils/rotationBoundingBox.js";
9
9
  import { getPageTop } from "./hitTest.js";
10
10
  //#region src/layout-bridge/engine/selectionRects.ts
@@ -24,8 +24,6 @@ import { getPageTop } from "./hitTest.js";
24
24
  * across blank lines visible (eigenpal/docx-editor#836).
25
25
  */
26
26
  const EMPTY_PARAGRAPH_SLIVER_WIDTH = 4;
27
- const DEFAULT_TABLE_CELL_PADDING_LEFT = 7;
28
- const DEFAULT_TABLE_CELL_PADDING_TOP = 1;
29
27
  /**
30
28
  * Extract FontStyle from a run for measurement.
31
29
  */
@@ -49,14 +47,14 @@ function findBlockById(blocks, blockId) {
49
47
  return blocks.findIndex((block) => block.id === blockId);
50
48
  }
51
49
  function getCellContentOffsetY(cell, cellMeasure, rowHeight) {
52
- const padTop = cell.padding?.top ?? DEFAULT_TABLE_CELL_PADDING_TOP;
50
+ const { top: padTop } = resolveTableCellPadding(cell);
53
51
  const spareHeight = Math.max(0, rowHeight - cellMeasure.height);
54
52
  if (cell.verticalAlign === "bottom") return padTop + spareHeight;
55
53
  if (cell.verticalAlign === "center") return padTop + spareHeight / 2;
56
54
  return padTop;
57
55
  }
58
56
  function getCellContentOffsetX(cell) {
59
- return cell.padding?.left ?? DEFAULT_TABLE_CELL_PADDING_LEFT;
57
+ return resolveTableCellPadding(cell).left;
60
58
  }
61
59
  function getMeasuredBlockHeight(measure) {
62
60
  if (!measure) return 0;
@@ -1,4 +1,4 @@
1
- import { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListMarkerFormatting, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, RenderedPageBreakRun, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionPageNumbering, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned } from "./types.js";
1
+ import { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListMarkerFormatting, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, RenderedPageBreakRun, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionPageNumbering, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, resolveTableCellPadding, tableColumnsArePinned } from "./types.js";
2
2
  import { PageState, Paginator, PaginatorOptions, createPaginator } from "./paginator.js";
3
3
  import { KeepNextChain, calculateChainHeight, computeKeepNextChains, getMidChainIndices, hasKeepLines, hasPageBreakBefore } from "./keep-together.js";
4
4
  import { resolveSectionHeaderFooterRefs } from "./headerFooterRefs.js";
@@ -44,4 +44,4 @@ declare function layoutDocument(blocks: FlowBlock[], measures: Measure[], option
44
44
  */
45
45
  declare function getHeaderRowsHeight(measure: TableMeasure, headerRowCount: number): number;
46
46
  //#endregion
47
- export { BlockId, BorderStyle, type BreakDecision, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, type KeepNextChain, Layout, LayoutOptions, LineBreakRun, ListMarkerFormatting, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, type PageState, type Paginator, type PaginatorOptions, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, RenderedPageBreakRun, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionLayoutConfig, SectionPageNumbering, type SectionState, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, getTableRowLeadingWidth, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, resolveSectionHeaderFooterRefs, scheduleSectionBreak, tableColumnsArePinned };
47
+ export { BlockId, BorderStyle, type BreakDecision, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, type KeepNextChain, Layout, LayoutOptions, LineBreakRun, ListMarkerFormatting, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, type PageState, type Paginator, type PaginatorOptions, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, RenderedPageBreakRun, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionLayoutConfig, SectionPageNumbering, type SectionState, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, getTableRowLeadingWidth, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, resolveSectionHeaderFooterRefs, resolveTableCellPadding, scheduleSectionBreak, tableColumnsArePinned };
@@ -11,7 +11,7 @@ import { INITIAL_RENDERED_BREAK_STATE, reconcileAfterBlock, reconcileBreakBefore
11
11
  import { applyPendingToActive, createInitialSectionState, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, scheduleSectionBreak } from "./section-breaks.js";
12
12
  import { buildTableRowBreakInfo, getRowContinuationSkip, snapRowBreak } from "./tableRowBreak.js";
13
13
  import { bandFragmentX, bandTopContentY, isPageFrameRelativeAnchor } from "./textBoxFlow.js";
14
- import { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned } from "./types.js";
14
+ import { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, resolveTableCellPadding, tableColumnsArePinned } from "./types.js";
15
15
  import { panic } from "better-result";
16
16
  //#region src/layout-engine/index.ts
17
17
  /**
@@ -257,9 +257,7 @@ function layoutDocument(blocks, measures, options) {
257
257
  break;
258
258
  case "sectionBreak": {
259
259
  const nextSectionConfig = sectionConfigs[sectionIdx + 1] ?? initialConfig;
260
- let nextType = options.bodyBreakType ?? sectionBreakTypes[sectionIdx] ?? DEFAULT_SECTION_BREAK_TYPE;
261
- if (sectionIdx + 1 < sectionBreakTypes.length) nextType = sectionBreakTypes[sectionIdx + 1] ?? DEFAULT_SECTION_BREAK_TYPE;
262
- handleSectionBreak(block, paginator, nextSectionConfig, nextType, sectionIdx + 1);
260
+ handleSectionBreak(block, paginator, nextSectionConfig, sectionBreakTypes[sectionIdx] ?? DEFAULT_SECTION_BREAK_TYPE, sectionIdx + 1);
263
261
  const nextColumns = nextSectionConfig.columns;
264
262
  const nextBreakIndex = breakIndices[sectionIdx + 1] ?? blocks.length;
265
263
  const nextBreak = blocks[nextBreakIndex];
@@ -937,4 +935,4 @@ function handleSectionBreak(_block, paginator, nextSectionConfig, nextSectionTyp
937
935
  paginator.updateColumns(nextSectionConfig.columns ?? DEFAULT_COLUMNS);
938
936
  }
939
937
  //#endregion
940
- export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, getTableRowLeadingWidth, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, resolveSectionHeaderFooterRefs, scheduleSectionBreak, tableColumnsArePinned };
938
+ export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, getTableRowLeadingWidth, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, resolveSectionHeaderFooterRefs, resolveTableCellPadding, scheduleSectionBreak, tableColumnsArePinned };
@@ -2,7 +2,7 @@ import { recordMeasureBlock, recordMeasureBlockError } from "../layoutInstrument
2
2
  import { isParagraphFrameTextBox } from "../paragraphFrame.js";
3
3
  import { bandFragmentX, bandTopContentY, isPageFrameRelativeAnchor } from "../textBoxFlow.js";
4
4
  import { getTextBoxGroupId } from "../textBoxGroup.js";
5
- import { DEFAULT_TEXTBOX_MARGINS, floatingTextBoxReservesBand, floatingTextBoxWrapsText, tableColumnsArePinned } from "../types.js";
5
+ import { DEFAULT_TEXTBOX_MARGINS, floatingTextBoxReservesBand, floatingTextBoxWrapsText, resolveTableCellPadding, tableColumnsArePinned } from "../types.js";
6
6
  import { getCachedParagraphMeasure, setCachedParagraphMeasure } from "./cache.js";
7
7
  import { resolveFloatingTableX } from "./floatingTablePosition.js";
8
8
  import { findClearLineY, measureParagraph } from "./measureParagraph.js";
@@ -63,8 +63,6 @@ function resolveTableWidthPx(width, widthType, contentWidth) {
63
63
  if (widthType === "dxa" || !widthType || widthType === "auto") return Math.round(width / 20 * 1.333);
64
64
  }
65
65
  function measureTableBlock(tableBlock, contentWidth, fieldValues) {
66
- const DEFAULT_CELL_PADDING_X = 7;
67
- const DEFAULT_CELL_PADDING_Y = 0;
68
66
  let columnWidths = tableBlock.columnWidths ?? [];
69
67
  const explicitWidthPx = resolveTableWidthPx(tableBlock.width, tableBlock.widthType, contentWidth);
70
68
  if (columnWidths.length === 0 && tableBlock.rows.length > 0) {
@@ -89,8 +87,7 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
89
87
  for (let c = 0; c < colSpan && columnIndex + c < columnWidths.length; c++) cellWidth += columnWidths[columnIndex + c] ?? 0;
90
88
  if (cellWidth === 0) cellWidth = cell.width ?? 100;
91
89
  columnIndex = getFirstAvailableColumn(cellGrid, rowIdx, columnIndex + colSpan);
92
- const padLeft = cell.padding?.left ?? DEFAULT_CELL_PADDING_X;
93
- const padRight = cell.padding?.right ?? DEFAULT_CELL_PADDING_X;
90
+ const { left: padLeft, right: padRight } = resolveTableCellPadding(cell);
94
91
  const cellContentWidth = Math.max(1, cellWidth - padLeft - padRight);
95
92
  const measureWidth = cell.noWrap === true && !columnsPinned ? NO_WRAP_MEASURE_WIDTH : cellContentWidth;
96
93
  const cellMeasure = {
@@ -132,8 +129,7 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
132
129
  placeTableCellBlock(flowState, sourceBlock, blockMeasure);
133
130
  }
134
131
  cell.height = finishTableCellFlow(flowState);
135
- const padTop = sourceCell?.padding?.top ?? DEFAULT_CELL_PADDING_Y;
136
- const padBottom = sourceCell?.padding?.bottom ?? DEFAULT_CELL_PADDING_Y;
132
+ const { top: padTop, bottom: padBottom } = resolveTableCellPadding(sourceCell);
137
133
  cell.height += padTop + padBottom;
138
134
  if ((sourceCell?.rowSpan ?? 1) > 1) continue;
139
135
  const borderHeight = getTableCellVerticalBorderHeight(cellGrid, sourceCell, rowIdx);
@@ -444,14 +440,34 @@ function measureTextBoxBlock(tb, fieldValues) {
444
440
  const innerWidth = tb.width - margins.left - margins.right;
445
441
  const innerMeasures = tb.content.map((block) => {
446
442
  if (block.kind === "table") return measureTableBlock(block, innerWidth, fieldValues);
447
- return measureParagraph(block, innerWidth, fieldValues ? { fieldValues } : void 0);
443
+ return measureParagraph(block, tb.textWrap === "none" ? NO_WRAP_MEASURE_WIDTH : innerWidth, fieldValues ? { fieldValues } : void 0);
448
444
  });
445
+ let naturalContentWidth = 0;
446
+ for (let index = 0; index < innerMeasures.length; index++) {
447
+ const measure = innerMeasures[index];
448
+ if (measure.kind === "table") {
449
+ naturalContentWidth = Math.max(naturalContentWidth, measure.totalWidth);
450
+ continue;
451
+ }
452
+ const block = tb.content[index];
453
+ if (!block || block.kind !== "paragraph") continue;
454
+ const indent = block.attrs?.indent;
455
+ const left = indent?.left ?? 0;
456
+ const right = indent?.right ?? 0;
457
+ for (let lineIndex = 0; lineIndex < measure.lines.length; lineIndex++) {
458
+ const line = measure.lines[lineIndex];
459
+ const start = left + (lineIndex === 0 ? (indent?.firstLine ?? 0) - (indent?.hanging ?? 0) : 0);
460
+ const end = start + line.width + right;
461
+ naturalContentWidth = Math.max(naturalContentWidth, Math.max(0, end) - Math.min(0, start));
462
+ }
463
+ }
464
+ const fittedWidth = naturalContentWidth + margins.left + margins.right;
465
+ const totalWidth = tb.autoFit === "shape" && tb.textWrap === "none" ? Math.max(tb.width, fittedWidth) : tb.width;
449
466
  const contentBoxHeight = layoutTextBoxContent(tb.content, innerMeasures).totalHeight + margins.top + margins.bottom;
450
- const totalHeight = tb.autoFit === "shape" ? Math.max(tb.height ?? 0, contentBoxHeight) : tb.height ?? contentBoxHeight;
451
467
  return {
452
468
  kind: "textBox",
453
- width: tb.width,
454
- height: totalHeight,
469
+ width: totalWidth,
470
+ height: tb.autoFit === "shape" ? Math.max(tb.height ?? 0, contentBoxHeight) : tb.height ?? contentBoxHeight,
455
471
  innerMeasures
456
472
  };
457
473
  }
@@ -1,5 +1,6 @@
1
1
  import { calculateTabWidth, pixelsToTwips } from "../../prosemirror/utils/tabCalculator.js";
2
2
  import { CJK_FALLBACK_FONT_FAMILY, isCjkFont } from "../../utils/fontResolver.js";
3
+ import { isRtlParagraph } from "../../utils/paragraphBaseDirection.js";
3
4
  import { inlineImageBoundingBox } from "../../utils/rotationBoundingBox.js";
4
5
  import { hasCjk, hasComplexScript } from "../../utils/scriptSegments.js";
5
6
  import { measuredLineAdvance } from "../lineFlow.js";
@@ -668,6 +669,7 @@ function findClearLineY(startY, lineHeight, zones, contentWidth, minWidth) {
668
669
  function measureParagraph(block, maxWidth, options) {
669
670
  const runs = block.runs;
670
671
  const attrs = block.attrs;
672
+ const isRtl = isRtlParagraph(block);
671
673
  const spacing = attrs?.spacing;
672
674
  const isJustifiedParagraph = attrs?.alignment === "justify";
673
675
  const justificationProfile = {
@@ -936,9 +938,13 @@ function measureParagraph(block, maxWidth, options) {
936
938
  decimalPrefixWidth
937
939
  });
938
940
  let tabWidth = tabResult.width;
941
+ const authoredEndpoint = contentX + tabWidth + followingWidth;
942
+ const activeContentRightEdge = maxWidth - currentLine.rightOffset;
943
+ const preservesLogicalRtlEndStop = isRtl && tabResult.alignment === "end" && authoredEndpoint <= activeContentRightEdge + WIDTH_TOLERANCE;
939
944
  const landsOnLeftIndent = tabResult.alignment === "start" && indentLeft > 0 && Math.abs(contentX + tabWidth - indentLeft) <= WIDTH_TOLERANCE;
940
945
  const lineRightEdgeX = indentLeft + (isFirstLine ? firstLineOffset + markerInlineWidth : 0) + currentLine.availableWidth + currentLine.leftOffset;
941
- if (!landsOnLeftIndent && !hasFollowingTabOnLine(runs, runIndex) && canClampTabToRightEdge(tabResult.alignment, currentLine.width, hasPriorTabOnLine(runs, runIndex), followingWidth, currentLine.availableWidth) && (tabWidth > 0 || followingWidth > 0) && contentX + tabWidth + followingWidth > lineRightEdgeX + WIDTH_TOLERANCE) tabWidth = Math.max(1, lineRightEdgeX - contentX - followingWidth);
946
+ if (!preservesLogicalRtlEndStop && !landsOnLeftIndent && !hasFollowingTabOnLine(runs, runIndex) && canClampTabToRightEdge(tabResult.alignment, currentLine.width, hasPriorTabOnLine(runs, runIndex), followingWidth, currentLine.availableWidth) && (tabWidth > 0 || followingWidth > 0) && contentX + tabWidth + followingWidth > lineRightEdgeX + WIDTH_TOLERANCE) tabWidth = Math.max(1, lineRightEdgeX - contentX - followingWidth);
947
+ if (preservesLogicalRtlEndStop) currentLine.availableWidth = Math.max(currentLine.availableWidth, currentLine.width + tabWidth + followingWidth);
942
948
  if (currentLine.width + tabWidth > currentLine.availableWidth + WIDTH_TOLERANCE) {
943
949
  startNewLine(runIndex, 0);
944
950
  updateMaxFont(style);
@@ -1,5 +1,5 @@
1
1
  import { emuToPixels } from "../../utils/units.js";
2
- import { isFloatingImageRun } from "../types.js";
2
+ import { isFloatingImageRun, resolveTableCellPadding } from "../types.js";
3
3
  import { clampFloatingWrapMargins } from "./clampFloatingWrapMargins.js";
4
4
  import { createTableCellFlowState, placeTableCellBlock } from "./tableCellFlow.js";
5
5
  //#region src/layout-engine/measure/tableCellFloating.ts
@@ -34,8 +34,7 @@ const resolveCellScopedPosition = ({ run, paragraphY, contentWidth }) => {
34
34
  };
35
35
  };
36
36
  function getTableCellContentWidth(cell, cellMeasure) {
37
- const padLeft = cell?.padding?.left ?? 7;
38
- const padRight = cell?.padding?.right ?? 7;
37
+ const { left: padLeft, right: padRight } = resolveTableCellPadding(cell);
39
38
  return Math.max(0, cellMeasure.width - padLeft - padRight);
40
39
  }
41
40
  function getTableCellFloatingImages(cell, cellMeasure, contentWidth, resolvePosition) {
@@ -11,7 +11,7 @@ const INITIAL_RENDERED_BREAK_STATE = { type: "noPageAdvance" };
11
11
  */
12
12
  const reconcileBreakBeforeBlock = ({ state, block, previousBlock, page, blocksById, hasExplicitPageBreak, renderedBreakNeedsSnap }) => {
13
13
  if (hasExplicitPageBreak) return {
14
- forcePageBreak: true,
14
+ forcePageBreak: !(state.type === "pageAdvance" && state.reason === "authoredBoundary" && state.boundary === "sectionBreak" && previousBlock?.kind === "sectionBreak" && previousBlock.type !== "continuous"),
15
15
  suppressSpaceBefore: false,
16
16
  state: INITIAL_RENDERED_BREAK_STATE
17
17
  };
@@ -2,6 +2,7 @@ import { measureParagraph } from "./measure/measureParagraph.js";
2
2
  import { buildTableCellFloatingZones, getTableCellContentWidth, getTableCellFloatingImages } from "./measure/tableCellFloating.js";
3
3
  import { createTableCellFlowState, placeTableCellBlock } from "./measure/tableCellFlow.js";
4
4
  import { isEmptyParagraph } from "./paragraphSpacing.js";
5
+ import { resolveTableCellPadding } from "./types.js";
5
6
  //#region src/layout-engine/tableRowBreak.ts
6
7
  /**
7
8
  * Table row-break geometry. Ported from eigenpal/docx-editor#698 (folio subset).
@@ -18,7 +19,6 @@ import { isEmptyParagraph } from "./paragraphSpacing.js";
18
19
  * here yet (a separate follow-up); each row uses its own cells' content.
19
20
  */
20
21
  const BREAK_OFFSET_EPSILON = .01;
21
- const DEFAULT_TABLE_CELL_PADDING_TOP = 1;
22
22
  function isInsideRange(offset, range) {
23
23
  return offset > range.top && offset < range.bottom;
24
24
  }
@@ -49,7 +49,7 @@ function cellBreakGeometry(cell, measure) {
49
49
  const suppressibleLeadingRanges = [];
50
50
  const cellBlocks = cell?.blocks;
51
51
  const blockMeasures = measure.blocks;
52
- const padTop = cell?.padding?.top ?? DEFAULT_TABLE_CELL_PADDING_TOP;
52
+ const { top: padTop } = resolveTableCellPadding(cell);
53
53
  const contentWidth = getTableCellContentWidth(cell, measure);
54
54
  const floatingZones = buildTableCellFloatingZones(cell !== void 0 ? getTableCellFloatingImages(cell, measure, contentWidth) : [], contentWidth);
55
55
  const flowState = createTableCellFlowState();
@@ -1,7 +1,7 @@
1
1
  import { OutlineStyleAttr } from "../types/documentEnumValues.js";
2
- import { ImagePosition, ImageWrap, SdtProperties, SdtType, ShapeTextBody, TableCellFormatting, TableWidthType } from "@stll/docx-core/model";
2
+ import { ImagePosition, ImageWrap, NumberFormat, SdtProperties, SdtType, ShapeTextBody, TableCellFormatting, TableWidthType } from "@stll/docx-core/model";
3
3
  declare namespace types_d_exports {
4
- export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListMarkerFormatting, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, RenderedPageBreakRun, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionPageNumbering, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned };
4
+ export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListMarkerFormatting, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, RenderedPageBreakRun, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionPageNumbering, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, resolveTableCellPadding, tableColumnsArePinned };
5
5
  }
6
6
  /**
7
7
  * Unique identifier for a block in the document.
@@ -578,6 +578,9 @@ type TableCell = {
578
578
  */
579
579
  noWrap?: boolean;
580
580
  };
581
+ type TableCellPadding = NonNullable<TableCell["padding"]>;
582
+ /** Resolve authored cell margins against the TableNormal defaults. */
583
+ declare const resolveTableCellPadding: (cell: Pick<TableCell, "padding"> | undefined) => TableCellPadding;
581
584
  /**
582
585
  * A table row containing cells.
583
586
  */
@@ -705,11 +708,11 @@ type SectionBreakBlock = {
705
708
  /** Normalized section page-number policy. An omitted OOXML start continues numbering. */
706
709
  type SectionPageNumbering = {
707
710
  type: "continue";
708
- format?: string;
711
+ format?: NumberFormat;
709
712
  } | {
710
713
  type: "restart";
711
714
  start: number;
712
- format?: string;
715
+ format?: NumberFormat;
713
716
  };
714
717
  type PageHeaderFooterRefs = {
715
718
  titlePg?: boolean;
@@ -760,6 +763,8 @@ type TextBoxBlock = {
760
763
  height?: number;
761
764
  /** Text fitting behavior */
762
765
  autoFit?: ShapeTextBody["autoFit"];
766
+ /** Horizontal text wrapping inside the box */
767
+ textWrap?: ShapeTextBody["textWrap"];
763
768
  /** Fill/background color */
764
769
  fillColor?: string;
765
770
  /** Border width in pixels */
@@ -1081,7 +1086,7 @@ type Page = {
1081
1086
  /** Authored page number shown by PAGE fields. */
1082
1087
  logicalNumber: number;
1083
1088
  /** OOXML number format for this page's section. */
1084
- logicalNumberFormat?: string;
1089
+ logicalNumberFormat?: NumberFormat;
1085
1090
  /** Fragments positioned on this page. */
1086
1091
  fragments: Fragment[];
1087
1092
  /** Page margins. */
@@ -1235,8 +1240,6 @@ type LayoutOptions = {
1235
1240
  * page.
1236
1241
  */
1237
1242
  footnoteHeightById?: Map<number, number>;
1238
- /** Section break type for the body-level (final) section (for section transition logic). */
1239
- bodyBreakType?: "continuous" | "nextPage" | "evenPage" | "oddPage";
1240
1243
  /** Header/footer references for each document section, by section index. */
1241
1244
  sectionHeaderFooterRefs?: PageHeaderFooterRefs[];
1242
1245
  };
@@ -1308,6 +1311,8 @@ type HeaderFooterContent = {
1308
1311
  */
1309
1312
  marginPushTop?: number;
1310
1313
  marginPushBottom?: number;
1314
+ /** Page-coordinate lower edge of a header wrap band overlapping the body top. */
1315
+ bodyTopClearance?: number;
1311
1316
  /**
1312
1317
  * Relationship id (`rId`) of the source HF part. Emitted as `data-rid`
1313
1318
  * on the painted `.layout-page-header` / `.layout-page-footer` so the
@@ -1380,4 +1385,4 @@ declare function tableColumnsArePinned(table: TableBlock): boolean;
1380
1385
  /** Return the leading visual offset for a row with omitted grid columns. */
1381
1386
  declare const getTableRowLeadingWidth: (row: TableRow, columnWidths: readonly number[]) => number;
1382
1387
  //#endregion
1383
- export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListMarkerFormatting, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, RenderedPageBreakRun, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionPageNumbering, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned, types_d_exports };
1388
+ export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListMarkerFormatting, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, RenderedPageBreakRun, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionPageNumbering, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, resolveTableCellPadding, tableColumnsArePinned, types_d_exports };
@@ -13,8 +13,22 @@ var types_exports = /* @__PURE__ */ __exportAll({
13
13
  isFloatingImageRun: () => isFloatingImageRun,
14
14
  isFloatingTextBoxBlock: () => isFloatingTextBoxBlock,
15
15
  isTextWrappingFloatingImageRun: () => isTextWrappingFloatingImageRun,
16
+ resolveTableCellPadding: () => resolveTableCellPadding,
16
17
  tableColumnsArePinned: () => tableColumnsArePinned
17
18
  });
19
+ const DEFAULT_TABLE_CELL_PADDING = {
20
+ top: 0,
21
+ right: 7,
22
+ bottom: 0,
23
+ left: 7
24
+ };
25
+ /** Resolve authored cell margins against the TableNormal defaults. */
26
+ const resolveTableCellPadding = (cell) => ({
27
+ top: cell?.padding?.top ?? DEFAULT_TABLE_CELL_PADDING.top,
28
+ right: cell?.padding?.right ?? DEFAULT_TABLE_CELL_PADDING.right,
29
+ bottom: cell?.padding?.bottom ?? DEFAULT_TABLE_CELL_PADDING.bottom,
30
+ left: cell?.padding?.left ?? DEFAULT_TABLE_CELL_PADDING.left
31
+ });
18
32
  /** Default internal margins for text boxes (OOXML defaults in pixels) */
19
33
  const DEFAULT_TEXTBOX_MARGINS = {
20
34
  top: 4,
@@ -128,4 +142,4 @@ const getTableRowLeadingWidth = (row, columnWidths) => {
128
142
  return columnWidths.slice(0, row.gridBefore ?? 0).reduce((sum, columnWidth) => sum + columnWidth, 0);
129
143
  };
130
144
  //#endregion
131
- export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned, types_exports };
145
+ export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, resolveTableCellPadding, tableColumnsArePinned, types_exports };
@@ -1,4 +1,4 @@
1
- import { MeasuredLine, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphMeasure, Run, TabStop } from "../layout-engine/types.js";
1
+ import { HyperlinkInfo, MeasuredLine, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphMeasure, Run, TabStop } from "../layout-engine/types.js";
2
2
  import { RenderContext } from "./renderUtils.js";
3
3
  //#region src/layout-painter/renderParagraph.d.ts
4
4
  /**
@@ -64,8 +64,14 @@ type RenderLineOptions = {
64
64
  context?: RenderContext;
65
65
  /** Left indent in pixels */
66
66
  leftIndentPx?: number;
67
+ /** Authored OOXML left indent used by logical tab-stop calculations. */
68
+ tabLeftIndentPx?: number;
67
69
  /** First line indent in pixels (positive) or hanging indent (negative) */
68
70
  firstLineIndentPx?: number;
71
+ /** Paragraph base direction for logical first-line indentation. */
72
+ isRtl?: boolean;
73
+ /** Full paragraph content-box width before physical indents. */
74
+ contentWidthPx?: number;
69
75
  /** Line-specific floating image margins (calculated per-line based on Y overlap) */
70
76
  floatingMargins?: {
71
77
  leftMargin: number;
@@ -73,6 +79,8 @@ type RenderLineOptions = {
73
79
  };
74
80
  /** Track inline image runs already rendered in this paragraph fragment to prevent duplicates */
75
81
  renderedInlineImageKeys?: Set<string>;
82
+ /** Hyperlink fragments whose complete displayed text is a URL. */
83
+ leftToRightDisplayedUrlHyperlinks?: ReadonlySet<HyperlinkInfo>;
76
84
  /**
77
85
  * Rightmost x where inline content may render, in content-area coords. Used
78
86
  * by the right-tab anchor (TOC pattern); passed in directly rather than
@@ -8,9 +8,9 @@ import { FONT_KERNING_MODE, countCompressibleSpaces, getFontKerningMode, getRunF
8
8
  import { isFloatingImageRun } from "../layout-engine/types.js";
9
9
  import { calculateTabWidth } from "../prosemirror/utils/tabCalculator.js";
10
10
  import { AUTHOR_COLORS, getAuthorColorIdx } from "../utils/authorColors.js";
11
- import { detectBaseDirection } from "../utils/baseDirection.js";
12
11
  import { resolveFontFamily } from "../utils/fontResolver.js";
13
12
  import "../utils/fontWeights.js";
13
+ import { isRtlParagraph } from "../utils/paragraphBaseDirection.js";
14
14
  import { inlineImageBoundingBox, parseRotationDegrees, rotatedBoundingBox } from "../utils/rotationBoundingBox.js";
15
15
  import { applySanitizedImageSrc } from "../utils/sanitizeImageSrc.js";
16
16
  import { SCRIPT_CLASS, hasCjk, hasComplexScript, segmentByScript } from "../utils/scriptSegments.js";
@@ -39,12 +39,42 @@ const PARAGRAPH_CLASS_NAMES = {
39
39
  image: "layout-run-image",
40
40
  lineBreak: "layout-run-linebreak"
41
41
  };
42
+ const LEFT_TO_RIGHT_DIRECTION = "ltr";
43
+ const RIGHT_TO_LEFT_DIRECTION = "rtl";
44
+ const DISPLAYED_URL_PATTERN = /^(?:https?:\/\/|www\.)\S+$/iu;
42
45
  /**
43
46
  * Check if run is a text run
44
47
  */
45
48
  function isTextRun(run) {
46
49
  return run.kind === "text";
47
50
  }
51
+ function hyperlinksMatch(left, right) {
52
+ return left.href === right.href && left.tooltip === right.tooltip && left.noDefaultStyle === right.noDefaultStyle;
53
+ }
54
+ function collectLeftToRightDisplayedUrlHyperlinks(runs) {
55
+ const result = /* @__PURE__ */ new Set();
56
+ let index = 0;
57
+ while (index < runs.length) {
58
+ const first = runs[index];
59
+ if (!first || !isTextRun(first) || !first.hyperlink) {
60
+ index += 1;
61
+ continue;
62
+ }
63
+ const hyperlinks = [];
64
+ let displayedText = "";
65
+ let groupEnd = index;
66
+ while (groupEnd < runs.length) {
67
+ const run = runs[groupEnd];
68
+ if (!run || !isTextRun(run) || !run.hyperlink || !hyperlinksMatch(first.hyperlink, run.hyperlink)) break;
69
+ displayedText += toPaintedText(run.text);
70
+ hyperlinks.push(run.hyperlink);
71
+ groupEnd += 1;
72
+ }
73
+ if (DISPLAYED_URL_PATTERN.test(displayedText.trim())) for (const hyperlink of hyperlinks) result.add(hyperlink);
74
+ index = groupEnd;
75
+ }
76
+ return result;
77
+ }
48
78
  /**
49
79
  * Check if run is a tab run
50
80
  */
@@ -144,8 +174,8 @@ function applyRunStyles(element, run) {
144
174
  element.style.webkitTextStroke = "1px currentColor";
145
175
  element.style.webkitTextFillColor = "transparent";
146
176
  }
147
- if (run.rtl === true) element.dir = "rtl";
148
- else if (run.rtl === false) element.dir = "ltr";
177
+ if (run.rtl === true) element.dir = RIGHT_TO_LEFT_DIRECTION;
178
+ else if (run.rtl === false) element.dir = LEFT_TO_RIGHT_DIRECTION;
149
179
  if (run.textEffect) {
150
180
  element.classList.add("docx-text-effect", `docx-text-effect-${run.textEffect}`);
151
181
  element.dataset["effect"] = run.textEffect;
@@ -264,7 +294,7 @@ function applyPmPositions(element, pmStart, pmEnd) {
264
294
  /**
265
295
  * Render a text run
266
296
  */
267
- function renderTextRun(run, doc) {
297
+ function renderTextRun(run, doc, hyperlinkDirection) {
268
298
  const span = doc.createElement("span");
269
299
  span.className = `${PARAGRAPH_CLASS_NAMES.run} ${PARAGRAPH_CLASS_NAMES.text}`;
270
300
  if (run.footnoteRefId !== void 0) {
@@ -284,6 +314,7 @@ function renderTextRun(run, doc) {
284
314
  if (run.hyperlink) {
285
315
  const anchor = doc.createElement("a");
286
316
  anchor.href = run.hyperlink.href;
317
+ if (hyperlinkDirection || DISPLAYED_URL_PATTERN.test(paintedText.trim())) anchor.dir = LEFT_TO_RIGHT_DIRECTION;
287
318
  if (!run.hyperlink.href.startsWith("#")) {
288
319
  anchor.target = "_blank";
289
320
  anchor.rel = "noopener noreferrer";
@@ -1053,7 +1084,7 @@ function renderLine(block, line, alignment, doc, options) {
1053
1084
  return withCursiveJoiners(runEl, run, cursiveJoinerPlan, applyJoinerRunStyles, doc);
1054
1085
  };
1055
1086
  const renderLineTextRun = (run) => {
1056
- const runEl = renderTextRun(run, doc);
1087
+ const runEl = renderTextRun(run, doc, run.hyperlink && options?.leftToRightDisplayedUrlHyperlinks?.has(run.hyperlink) ? LEFT_TO_RIGHT_DIRECTION : void 0);
1057
1088
  if (collapsedLeadingSpaceRuns.has(run)) runEl.dataset["collapsedLeadingSpaces"] = "true";
1058
1089
  if (collapsedTrailingSpaceRuns.has(run)) runEl.dataset["collapsedTrailingSpaces"] = "true";
1059
1090
  if (isCollapsedLineEdgeSpaceRun(run)) {
@@ -1125,7 +1156,8 @@ function renderLine(block, line, alignment, doc, options) {
1125
1156
  const measureText = collapsedSpaceMeasureText ?? (hasTabRuns || hasScaledTextRun ? createTextMeasurer(doc) : void 0);
1126
1157
  if (hasTabRuns) {
1127
1158
  const explicitStops = options?.tabStops?.map(convertTabStopToCalc);
1128
- const leftIndentTwips = options?.leftIndentPx ? Math.round(options.leftIndentPx * 15) : 0;
1159
+ const tabLeftIndentPx = options?.tabLeftIndentPx ?? options?.leftIndentPx ?? 0;
1160
+ const leftIndentTwips = Math.round(tabLeftIndentPx * 15);
1129
1161
  tabContext = {
1130
1162
  ...explicitStops !== void 0 ? { explicitStops } : {},
1131
1163
  ...block.attrs?.defaultTabStopTwips !== void 0 ? { defaultTabInterval: block.attrs.defaultTabStopTwips } : {},
@@ -1134,11 +1166,13 @@ function renderLine(block, line, alignment, doc, options) {
1134
1166
  }
1135
1167
  let currentX;
1136
1168
  const leftIndentPx = options?.leftIndentPx ?? 0;
1169
+ const tabLeftIndentPx = options?.tabLeftIndentPx ?? leftIndentPx;
1170
+ const floatingLeftOffsetPx = options?.floatingMargins?.leftMargin ?? 0;
1137
1171
  if (options?.isFirstLine) {
1138
1172
  const firstLineIndentPx = options.firstLineIndentPx ?? 0;
1139
1173
  const markerInlineWidth = getListMarkerInlineWidth(block);
1140
- currentX = leftIndentPx + firstLineIndentPx + markerInlineWidth;
1141
- } else currentX = leftIndentPx;
1174
+ currentX = tabLeftIndentPx + firstLineIndentPx + markerInlineWidth + floatingLeftOffsetPx;
1175
+ } else currentX = tabLeftIndentPx + floatingLeftOffsetPx;
1142
1176
  for (let i = 0; i < runsForLine.length; i++) {
1143
1177
  const run = runsForLine[i];
1144
1178
  if (isTabRun(run) && tabContext && measureText) {
@@ -1160,7 +1194,7 @@ function renderLine(block, line, alignment, doc, options) {
1160
1194
  break;
1161
1195
  }
1162
1196
  }
1163
- if (lineRightEdgeX !== void 0 && tabResult.alignment === "end" && !hasFollowingTab && currentX + tabResult.width + followingWidthForCheck >= lineRightEdgeX - RIGHT_EDGE_EPSILON_PX) {
1197
+ if (lineRightEdgeX !== void 0 && options?.isRtl !== true && tabResult.alignment === "end" && !hasFollowingTab && currentX + tabResult.width + followingWidthForCheck >= lineRightEdgeX - RIGHT_EDGE_EPSILON_PX) {
1164
1198
  lineEl.style.display = "flex";
1165
1199
  lineEl.style.alignItems = "baseline";
1166
1200
  lineEl.style.whiteSpace = "pre";
@@ -1173,7 +1207,8 @@ function renderLine(block, line, alignment, doc, options) {
1173
1207
  tabEl.style.minWidth = "0";
1174
1208
  tabEl.style.width = "auto";
1175
1209
  lineEl.append(tabEl);
1176
- if (options?.isFirstLine && options.firstLineIndentPx !== void 0 && options.firstLineIndentPx !== 0 && lineEl.firstElementChild instanceof HTMLElement) lineEl.firstElementChild.style.marginLeft = `${options.firstLineIndentPx}px`;
1210
+ const firstFlexChild = lineEl.firstElementChild ?? void 0;
1211
+ if (options?.isFirstLine && options.firstLineIndentPx !== void 0 && options.firstLineIndentPx !== 0 && isStyleableHTMLElement(firstFlexChild)) firstFlexChild.style.marginLeft = `${options.firstLineIndentPx}px`;
1177
1212
  for (let j = i + 1; j < runsForLine.length; j++) {
1178
1213
  const next = runsForLine[j];
1179
1214
  if (isTabRun(next) || isLineBreakRun(next)) break;
@@ -1191,7 +1226,10 @@ function renderLine(block, line, alignment, doc, options) {
1191
1226
  break;
1192
1227
  }
1193
1228
  let tabWidth = tabResult.width;
1194
- if (!(tabResult.alignment === "start" && leftIndentPx > 0 && Math.abs(currentX + tabWidth - leftIndentPx) <= RIGHT_EDGE_EPSILON_PX) && lineRightEdgeX !== void 0 && canClampTabToRightEdge(tabResult.alignment, i > 0, runsForLine.slice(0, i).some(isTabRun), options?.isLastLine === true) && currentX + tabWidth + followingWidthForCheck > lineRightEdgeX) tabWidth = Math.max(1, lineRightEdgeX - currentX - followingWidthForCheck);
1229
+ const activeContentRightEdge = options?.contentWidthPx === void 0 ? void 0 : options.contentWidthPx - (options.floatingMargins?.rightMargin ?? 0);
1230
+ const preservesLogicalRtlEndStop = options?.isRtl === true && tabResult.alignment === "end" && activeContentRightEdge !== void 0 && currentX + tabWidth + followingWidthForCheck <= activeContentRightEdge + RIGHT_EDGE_EPSILON_PX;
1231
+ const landsOnLeftIndent = tabResult.alignment === "start" && tabLeftIndentPx > 0 && Math.abs(currentX + tabWidth - tabLeftIndentPx) <= RIGHT_EDGE_EPSILON_PX;
1232
+ if (!preservesLogicalRtlEndStop && !landsOnLeftIndent && lineRightEdgeX !== void 0 && canClampTabToRightEdge(tabResult.alignment, i > 0, runsForLine.slice(0, i).some(isTabRun), options?.isLastLine === true) && currentX + tabWidth + followingWidthForCheck > lineRightEdgeX) tabWidth = Math.max(1, lineRightEdgeX - currentX - followingWidthForCheck);
1195
1233
  const tabEl = renderTabRun(run, doc, tabWidth, tabResult.leader);
1196
1234
  lineEl.append(tabEl);
1197
1235
  currentX += tabWidth;
@@ -1281,22 +1319,6 @@ function bordersFormGroup(a, b) {
1281
1319
  return bordersEqual(a.top, b.top) && bordersEqual(a.bottom, b.bottom) && bordersEqual(a.left, b.left) && bordersEqual(a.right, b.right) && bordersEqual(a.between, b.between);
1282
1320
  }
1283
1321
  /**
1284
- * Decide whether a paragraph without an explicit `w:bidi` flag should still be
1285
- * laid out right-to-left. Only paragraphs that carry at least one `w:rtl` run
1286
- * are candidates; among those the base direction follows the first strong
1287
- * directional character (the `dir="auto"` rule), so Hebrew/Arabic-led lines
1288
- * order RTL while an English- (or CJK-/Devanagari-/…) led line stays LTR.
1289
- * eigenpal #723 (#719).
1290
- */
1291
- function paragraphBaseIsRtl(block) {
1292
- const runs = block.runs.filter((r) => isTextRun(r) || isFieldRun(r));
1293
- if (!runs.some((r) => r.rtl)) return false;
1294
- return detectBaseDirection(runs.map((r) => {
1295
- if (isTextRun(r)) return r.text;
1296
- return isFieldRun(r) ? r.fallback ?? "" : "";
1297
- }).join("")) !== "ltr";
1298
- }
1299
- /**
1300
1322
  * Render a paragraph fragment
1301
1323
  *
1302
1324
  * @param fragment - The fragment to render
@@ -1322,7 +1344,7 @@ function renderParagraphFragment(fragment, block, measure, context, options = {}
1322
1344
  const lines = measure.lines.slice(fragment.fromLine, fragment.toLine);
1323
1345
  const alignment = block.attrs?.alignment;
1324
1346
  if (block.attrs?.styleId) fragmentEl.dataset["styleId"] = block.attrs.styleId;
1325
- const isRtl = block.attrs?.bidi ?? paragraphBaseIsRtl(block);
1347
+ const isRtl = isRtlParagraph(block);
1326
1348
  if (isRtl) fragmentEl.dir = "rtl";
1327
1349
  if (alignment) if (alignment === "center") fragmentEl.style.textAlign = "center";
1328
1350
  else if (alignment === "right") fragmentEl.style.textAlign = "right";
@@ -1407,6 +1429,7 @@ function renderParagraphFragment(fragment, block, measure, context, options = {}
1407
1429
  else if (indent?.firstLine && indent.firstLine > 0) firstLineIndentPx = indent.firstLine;
1408
1430
  let _cumulativeLineY = 0;
1409
1431
  const renderedInlineImageKeys = options.renderedInlineImageKeys ?? /* @__PURE__ */ new Set();
1432
+ const leftToRightDisplayedUrlHyperlinks = isRtl ? collectLeftToRightDisplayedUrlHyperlinks(block.runs) : void 0;
1410
1433
  for (let i = 0; i < lines.length; i++) {
1411
1434
  const line = lines[i];
1412
1435
  const lineIndex = fragment.fromLine + i;
@@ -1422,13 +1445,17 @@ function renderParagraphFragment(fragment, block, measure, context, options = {}
1422
1445
  paragraphEndsWithLineBreak,
1423
1446
  ...block.attrs?.tabs !== void 0 ? { tabStops: block.attrs.tabs } : {},
1424
1447
  leftIndentPx: indentLeft,
1448
+ tabLeftIndentPx: indent?.left ?? 0,
1425
1449
  firstLineIndentPx: isFirstLine ? firstLineIndentPx : 0,
1450
+ isRtl,
1451
+ contentWidthPx: fragment.width,
1426
1452
  context,
1427
1453
  floatingMargins: {
1428
1454
  leftMargin: lineLeftOffset,
1429
1455
  rightMargin: lineRightOffset
1430
1456
  },
1431
1457
  renderedInlineImageKeys,
1458
+ ...leftToRightDisplayedUrlHyperlinks === void 0 ? {} : { leftToRightDisplayedUrlHyperlinks },
1432
1459
  lineRightEdgePx: fragment.width - indentRight - lineRightOffset
1433
1460
  });
1434
1461
  const isFlexLine = lineEl.dataset["flexLine"] === "true";
@@ -1447,13 +1474,8 @@ function renderParagraphFragment(fragment, block, measure, context, options = {}
1447
1474
  const hasHanging = indent?.hanging && indent.hanging > 0;
1448
1475
  const hasFirstLine = indent?.firstLine && indent.firstLine > 0;
1449
1476
  if (isFirstLine) {
1450
- if (indentLeft !== 0 && hasHanging) {
1451
- lineEl.style.paddingLeft = `${Math.max(indentLeft, 0)}px`;
1452
- if (!isFlexLine) lineEl.style.textIndent = `-${indent.hanging ?? 0}px`;
1453
- } else if (indentLeft !== 0 && hasFirstLine) {
1454
- lineEl.style.paddingLeft = `${Math.max(indentLeft, 0)}px`;
1455
- if (!isFlexLine) lineEl.style.textIndent = `${indent.firstLine ?? 0}px`;
1456
- } else if (indentLeft > 0) lineEl.style.paddingLeft = `${indentLeft}px`;
1477
+ if (indentLeft > 0) lineEl.style.paddingLeft = `${indentLeft}px`;
1478
+ if (hasHanging && !isFlexLine) lineEl.style.textIndent = `-${indent.hanging ?? 0}px`;
1457
1479
  else if (hasFirstLine && !isFlexLine) lineEl.style.textIndent = `${indent.firstLine ?? 0}px`;
1458
1480
  } else if (indentLeft > 0) lineEl.style.paddingLeft = `${indentLeft}px`;
1459
1481
  if (indentRight > 0) lineEl.style.paddingRight = `${indentRight}px`;
@@ -1,7 +1,7 @@
1
1
  import { measureParagraph } from "../layout-engine/measure/measureParagraph.js";
2
2
  import { buildTableCellFloatingZones, getTableCellContentWidth, getTableCellFloatingImages } from "../layout-engine/measure/tableCellFloating.js";
3
3
  import { createTableCellFlowState, finishTableCellFlow, placeTableCellBlock } from "../layout-engine/measure/tableCellFlow.js";
4
- import { getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, tableColumnsArePinned } from "../layout-engine/types.js";
4
+ import { getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, resolveTableCellPadding, tableColumnsArePinned } from "../layout-engine/types.js";
5
5
  import { applySanitizedImageSrc } from "../utils/sanitizeImageSrc.js";
6
6
  import { emuToPixels } from "../utils/units.js";
7
7
  import { resolveAnchoredImagePosition } from "./anchoredImagePosition.js";
@@ -330,10 +330,7 @@ function renderTableCell({ cell, cellMeasure, x, rowHeight, borderFlags, columns
330
330
  cellEl.style.height = `${rowHeight}px`;
331
331
  cellEl.style.overflow = "hidden";
332
332
  cellEl.style.boxSizing = "border-box";
333
- const padTop = cell.padding?.top ?? 1;
334
- const padRight = cell.padding?.right ?? 7;
335
- const padBottom = cell.padding?.bottom ?? 1;
336
- const padLeft = cell.padding?.left ?? 7;
333
+ const { top: padTop, right: padRight, bottom: padBottom, left: padLeft } = resolveTableCellPadding(cell);
337
334
  cellEl.style.padding = `${padTop}px ${padRight}px ${padBottom}px ${padLeft}px`;
338
335
  if (cell.borders) {
339
336
  if (borderFlags.drawTop) applyBorder(cellEl, "top", cell.borders.top);
@@ -26,7 +26,7 @@ function renderTextBoxFragment(fragment, block, measure, context, options = {})
26
26
  containerEl.style.position = "absolute";
27
27
  containerEl.style.width = `${fragment.width}px`;
28
28
  containerEl.style.height = `${fragment.height}px`;
29
- containerEl.style.overflow = "hidden";
29
+ containerEl.style.overflow = block.textWrap === "none" ? "visible" : "hidden";
30
30
  containerEl.style.boxSizing = "border-box";
31
31
  if (block.fillColor) containerEl.style.backgroundColor = block.fillColor;
32
32
  if (block.outlineWidth && block.outlineWidth > 0) {
@@ -1,3 +1,4 @@
1
+ import { document_d_exports } from "../types/document.js";
1
2
  import { ImageRun, isFloatingImageRun, isTextWrappingFloatingImageRun } from "../layout-engine/types.js";
2
3
  //#region src/layout-painter/renderUtils.d.ts
3
4
  /**
@@ -17,7 +18,7 @@ type RenderContext = {
17
18
  /** Current page number (1-indexed) */
18
19
  pageNumber: number;
19
20
  /** OOXML section-level format for the current logical page number. */
20
- pageNumberFormat?: string;
21
+ pageNumberFormat?: document_d_exports.NumberFormat;
21
22
  /** Total number of pages */
22
23
  totalPages: number;
23
24
  /** Which section is being rendered */
@@ -25,6 +25,7 @@ const TEXT_BOX_AUTO_FIT_VALUES = [
25
25
  "normal",
26
26
  "shape"
27
27
  ];
28
+ const TEXT_BOX_TEXT_WRAP_VALUES = ["square", "none"];
28
29
  const FIELD_KINDS = ["simple", "complex"];
29
30
  const MATH_DISPLAYS = ["inline", "block"];
30
31
  const SHAPE_FILL_TYPES = [
@@ -463,6 +464,7 @@ const readTextBoxAttrs = (node) => {
463
464
  optionalNumber(attrs, "width", "textBox.attrs.width", issues);
464
465
  optionalNumber(attrs, "height", "textBox.attrs.height", issues);
465
466
  optionalOneOf(attrs, "autoFit", "textBox.attrs.autoFit", issues, TEXT_BOX_AUTO_FIT_VALUES);
467
+ optionalOneOf(attrs, "textWrap", "textBox.attrs.textWrap", issues, TEXT_BOX_TEXT_WRAP_VALUES);
466
468
  optionalString(attrs, "textBoxId", "textBox.attrs.textBoxId", issues);
467
469
  optionalString(attrs, "fillColor", "textBox.attrs.fillColor", issues);
468
470
  optionalNumber(attrs, "outlineWidth", "textBox.attrs.outlineWidth", issues);
@@ -1878,6 +1878,7 @@ function convertPMTextBox(node) {
1878
1878
  content: []
1879
1879
  }],
1880
1880
  ...attrs.autoFit !== void 0 ? { autoFit: attrs.autoFit } : {},
1881
+ ...attrs.textWrap !== void 0 ? { textWrap: attrs.textWrap } : {},
1881
1882
  margins: (() => {
1882
1883
  const m = {};
1883
1884
  if (typeof attrs.marginTop === "number") m.top = pixelsToEmu(attrs.marginTop);
@@ -1523,6 +1523,7 @@ function textBoxFromShape(shape, textBody) {
1523
1523
  if (shape.outline) textBox.outline = shape.outline;
1524
1524
  if (textBody.margins) textBox.margins = textBody.margins;
1525
1525
  if (textBody.autoFit) textBox.autoFit = textBody.autoFit;
1526
+ if (textBody.textWrap) textBox.textWrap = textBody.textWrap;
1526
1527
  return textBox;
1527
1528
  }
1528
1529
  /**
@@ -1597,6 +1598,7 @@ function convertTextBox(textBox, styleResolver, options) {
1597
1598
  width: widthPx,
1598
1599
  height: heightPx,
1599
1600
  autoFit: textBox.autoFit,
1601
+ textWrap: textBox.textWrap,
1600
1602
  textBoxId: textBox.id,
1601
1603
  fillColor,
1602
1604
  outlineWidth,
@@ -10,6 +10,8 @@ type TextBoxAttrs = {
10
10
  height?: number;
11
11
  /** Text fitting behavior */
12
12
  autoFit?: document_d_exports.ShapeTextBody["autoFit"];
13
+ /** Horizontal text wrapping inside the box */
14
+ textWrap?: document_d_exports.ShapeTextBody["textWrap"];
13
15
  /** Unique identifier */
14
16
  textBoxId?: string;
15
17
  /** Fill color as CSS color */
@@ -18,6 +18,9 @@ function parseTextBoxWrapType(raw) {
18
18
  function parseTextBoxAutoFit(raw) {
19
19
  if (raw === "none" || raw === "normal" || raw === "shape") return raw;
20
20
  }
21
+ function parseTextBoxTextWrap(raw) {
22
+ if (raw === "none" || raw === "square") return raw;
23
+ }
21
24
  const TextBoxExtension = createNodeExtension({
22
25
  name: "textBox",
23
26
  schemaNodeName: "textBox",
@@ -30,6 +33,7 @@ const TextBoxExtension = createNodeExtension({
30
33
  width: { default: 200 },
31
34
  height: { default: null },
32
35
  autoFit: { default: null },
36
+ textWrap: { default: null },
33
37
  textBoxId: { default: null },
34
38
  fillColor: { default: null },
35
39
  outlineWidth: { default: null },
@@ -63,10 +67,12 @@ const TextBoxExtension = createNodeExtension({
63
67
  const position = parseTextBoxPosition(d["position"]);
64
68
  const wrapType = parseTextBoxWrapType(d["wrapType"]);
65
69
  const autoFit = parseTextBoxAutoFit(d["autoFit"]);
70
+ const textWrap = parseTextBoxTextWrap(d["textWrap"]);
66
71
  return {
67
72
  ...d["width"] ? { width: Number(d["width"]) } : {},
68
73
  ...d["height"] ? { height: Number(d["height"]) } : {},
69
74
  ...autoFit ? { autoFit } : {},
75
+ ...textWrap ? { textWrap } : {},
70
76
  ...d["textboxId"] ? { textBoxId: d["textboxId"] } : {},
71
77
  ...d["fillColor"] ? { fillColor: d["fillColor"] } : {},
72
78
  ...d["outlineWidth"] ? { outlineWidth: Number(d["outlineWidth"]) } : {},
@@ -95,6 +101,7 @@ const TextBoxExtension = createNodeExtension({
95
101
  if (attrs.width) domAttrs["data-width"] = String(attrs.width);
96
102
  if (attrs.height) domAttrs["data-height"] = String(attrs.height);
97
103
  if (attrs.autoFit) domAttrs["data-auto-fit"] = attrs.autoFit;
104
+ if (attrs.textWrap) domAttrs["data-text-wrap"] = attrs.textWrap;
98
105
  if (attrs.textBoxId) domAttrs["data-textbox-id"] = attrs.textBoxId;
99
106
  if (attrs.fillColor) domAttrs["data-fill-color"] = attrs.fillColor;
100
107
  if (attrs.outlineWidth) domAttrs["data-outline-width"] = String(attrs.outlineWidth);
@@ -457,6 +457,8 @@ type TextBoxAttrs = {
457
457
  height?: number;
458
458
  /** Text fitting behavior */
459
459
  autoFit?: document_d_exports.ShapeTextBody["autoFit"];
460
+ /** Horizontal text wrapping inside the box */
461
+ textWrap?: document_d_exports.ShapeTextBody["textWrap"];
460
462
  /** Unique identifier */
461
463
  textBoxId?: string;
462
464
  /** Fill color as CSS color */
@@ -0,0 +1,6 @@
1
+ import { ParagraphBlock } from "../layout-engine/types.js";
2
+ //#region src/utils/paragraphBaseDirection.d.ts
3
+ /** Resolve the paragraph direction used by both measurement and painting. */
4
+ declare const isRtlParagraph: (block: ParagraphBlock) => boolean;
5
+ //#endregion
6
+ export { isRtlParagraph };
@@ -0,0 +1,12 @@
1
+ import { detectBaseDirection } from "./baseDirection.js";
2
+ //#region src/utils/paragraphBaseDirection.ts
3
+ const isDirectionalTextRun = (run) => run.kind === "text" || run.kind === "field";
4
+ /** Resolve the paragraph direction used by both measurement and painting. */
5
+ const isRtlParagraph = (block) => {
6
+ if (block.attrs?.bidi !== void 0) return block.attrs.bidi;
7
+ const runs = block.runs.filter(isDirectionalTextRun);
8
+ if (!runs.some(({ rtl }) => rtl === true)) return false;
9
+ return detectBaseDirection(runs.map((run) => run.kind === "text" ? run.text : run.fallback ?? "").join("")) !== "ltr";
10
+ };
11
+ //#endregion
12
+ export { isRtlParagraph };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/folio-core",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
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",
@@ -113,7 +113,7 @@
113
113
  "perf": "bun scripts/profile-editor.ts"
114
114
  },
115
115
  "dependencies": {
116
- "@stll/docx-core": "^0.16.0",
116
+ "@stll/docx-core": "^0.17.0",
117
117
  "@stll/docx-utils": "^0.1.0",
118
118
  "@stll/template-conditions": "^0.1.0",
119
119
  "better-result": "3.0.1",