@stll/folio-core 0.8.0 → 0.10.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 (168) hide show
  1. package/README.md +39 -0
  2. package/dist/ai-edits/apply.js +18 -4
  3. package/dist/ai-edits/headless.d.ts +73 -6
  4. package/dist/ai-edits/headless.js +299 -60
  5. package/dist/ai-edits/index.d.ts +3 -3
  6. package/dist/ai-edits/index.js +2 -1
  7. package/dist/ai-edits/snapshot.js +40 -3
  8. package/dist/ai-edits/types.d.ts +2 -1
  9. package/dist/compat/eigenpal.d.ts +6 -4
  10. package/dist/compat/eigenpal.js +5 -3
  11. package/dist/controller/latestRequestGate.d.ts +13 -0
  12. package/dist/controller/latestRequestGate.js +16 -0
  13. package/dist/controller/layoutPipeline.js +13 -1
  14. package/dist/document-operations.d.ts +20 -4
  15. package/dist/document-operations.js +36 -15
  16. package/dist/document-stories.d.ts +10 -0
  17. package/dist/document-stories.js +27 -0
  18. package/dist/docx/blockContentParser.js +2 -2
  19. package/dist/docx/commentParser.js +1 -1
  20. package/dist/docx/compatibility.d.ts +3 -7
  21. package/dist/docx/compatibility.js +0 -11
  22. package/dist/docx/corePropertiesParser.d.ts +8 -0
  23. package/dist/docx/corePropertiesParser.js +53 -0
  24. package/dist/docx/encryption/agileDecryption.js +1 -1
  25. package/dist/docx/encryption/encryptionInfo.js +1 -1
  26. package/dist/docx/encryption/openEncryptedDocx.js +1 -1
  27. package/dist/docx/fontTableParser.d.ts +6 -0
  28. package/dist/docx/fontTableParser.js +72 -0
  29. package/dist/docx/groupDrawingParser.js +7 -2
  30. package/dist/docx/headerFooterParser.js +1 -1
  31. package/dist/docx/index.d.ts +2 -1
  32. package/dist/docx/index.js +2 -1
  33. package/dist/docx/metadataPrivacy.d.ts +40 -0
  34. package/dist/docx/metadataPrivacy.js +131 -0
  35. package/dist/docx/normalizeBaseDirection.js +1 -1
  36. package/dist/docx/numberingParser.js +1 -0
  37. package/dist/docx/paragraphParser.js +12 -1
  38. package/dist/docx/parser.d.ts +1 -1
  39. package/dist/docx/parser.js +16 -10
  40. package/dist/docx/rezip.d.ts +5 -4
  41. package/dist/docx/rezip.js +76 -13
  42. package/dist/docx/runConsolidator.js +4 -0
  43. package/dist/docx/runParser.js +19 -1
  44. package/dist/docx/selectiveSave.js +3 -3
  45. package/dist/docx/serializer/fontTableSerializer.d.ts +6 -0
  46. package/dist/docx/serializer/fontTableSerializer.js +26 -0
  47. package/dist/docx/serializer/headerFooterSerializer.js +1 -1
  48. package/dist/docx/serializer/paragraphSerializer.js +4 -0
  49. package/dist/docx/serializer/runSerializer.js +7 -0
  50. package/dist/docx/serializer/settingsSerializer.d.ts +6 -0
  51. package/dist/docx/serializer/settingsSerializer.js +16 -0
  52. package/dist/docx/serializer/stylesSerializer.d.ts +6 -0
  53. package/dist/docx/serializer/stylesSerializer.js +56 -0
  54. package/dist/docx/serializer/tableSerializer.js +25 -26
  55. package/dist/docx/serializer/themeSerializer.d.ts +6 -0
  56. package/dist/docx/serializer/themeSerializer.js +36 -0
  57. package/dist/docx/settingsParser.js +46 -0
  58. package/dist/docx/styleParser.js +26 -1
  59. package/dist/docx/tableParser.js +11 -1
  60. package/dist/docx/vmlImageParser.js +1 -0
  61. package/dist/i18n/messages/catalogs.gen.d.ts +5916 -5916
  62. package/dist/i18n/messages/catalogs.gen.js +5910 -5910
  63. package/dist/index.d.ts +6 -4
  64. package/dist/index.js +5 -3
  65. package/dist/layout-bridge/convert/footnoteLayout.d.ts +3 -0
  66. package/dist/layout-bridge/convert/footnoteLayout.js +3 -1
  67. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +3 -0
  68. package/dist/layout-bridge/convert/headerFooterLayout.js +6 -1
  69. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +14 -2
  70. package/dist/layout-bridge/convert/toFlowBlocks.js +52 -14
  71. package/dist/layout-bridge/engine/measuring/index.d.ts +2 -1
  72. package/dist/layout-bridge/engine/measuring/index.js +2 -1
  73. package/dist/layout-engine/index.js +36 -28
  74. package/dist/layout-engine/measure/cache.js +6 -3
  75. package/dist/layout-engine/measure/effectiveLineBreakPolicy.d.ts +29 -0
  76. package/dist/layout-engine/measure/effectiveLineBreakPolicy.js +60 -0
  77. package/dist/layout-engine/measure/font-metrics.worker.js +1 -0
  78. package/dist/layout-engine/measure/index.d.ts +2 -1
  79. package/dist/layout-engine/measure/index.js +2 -1
  80. package/dist/layout-engine/measure/lineBreakProvider.d.ts +33 -0
  81. package/dist/layout-engine/measure/lineBreakProvider.js +252 -0
  82. package/dist/layout-engine/measure/lineBreaks.d.ts +7 -2
  83. package/dist/layout-engine/measure/lineBreaks.js +11 -18
  84. package/dist/layout-engine/measure/listMarkerWidth.d.ts +1 -0
  85. package/dist/layout-engine/measure/listMarkerWidth.js +12 -6
  86. package/dist/layout-engine/measure/measureBlocks.d.ts +1 -2
  87. package/dist/layout-engine/measure/measureBlocks.js +6 -36
  88. package/dist/layout-engine/measure/measureContainer.js +32 -9
  89. package/dist/layout-engine/measure/measureHelpers.js +5 -2
  90. package/dist/layout-engine/measure/measureParagraph.js +332 -73
  91. package/dist/layout-engine/measure/measureTypes.d.ts +2 -1
  92. package/dist/layout-engine/measure/measureWorker.d.ts +2 -2
  93. package/dist/layout-engine/measure/measureWorker.js +8 -19
  94. package/dist/layout-engine/measure/measureWorkerProtocol.d.ts +3 -14
  95. package/dist/layout-engine/measure/tableCellFloating.d.ts +10 -3
  96. package/dist/layout-engine/measure/tableCellFloating.js +51 -37
  97. package/dist/layout-engine/measure/tableCellFlow.d.ts +27 -0
  98. package/dist/layout-engine/measure/tableCellFlow.js +73 -0
  99. package/dist/layout-engine/measure/tableCellGrid.js +1 -1
  100. package/dist/layout-engine/measure/textMeasurementPolicy.d.ts +18 -0
  101. package/dist/layout-engine/measure/textMeasurementPolicy.js +24 -0
  102. package/dist/layout-engine/renderedBreakReconciliation.d.ts +6 -1
  103. package/dist/layout-engine/renderedBreakReconciliation.js +34 -11
  104. package/dist/layout-engine/tableRowBreak.d.ts +5 -2
  105. package/dist/layout-engine/tableRowBreak.js +62 -29
  106. package/dist/layout-engine/types.d.ts +43 -8
  107. package/dist/layout-painter/anchoredImagePosition.d.ts +23 -0
  108. package/dist/layout-painter/anchoredImagePosition.js +95 -0
  109. package/dist/layout-painter/borderStroke.d.ts +23 -0
  110. package/dist/layout-painter/borderStroke.js +39 -0
  111. package/dist/layout-painter/renderImage.d.ts +5 -4
  112. package/dist/layout-painter/renderImage.js +18 -4
  113. package/dist/layout-painter/renderPage.d.ts +2 -27
  114. package/dist/layout-painter/renderPage.js +13 -99
  115. package/dist/layout-painter/renderParagraph.js +41 -23
  116. package/dist/layout-painter/renderTable.d.ts +2 -0
  117. package/dist/layout-painter/renderTable.js +223 -43
  118. package/dist/managers/DocumentLoaderManager.d.ts +1 -1
  119. package/dist/managers/DocumentLoaderManager.js +2 -2
  120. package/dist/prosemirror/attrs/index.d.ts +4 -2
  121. package/dist/prosemirror/attrs/index.js +22 -2
  122. package/dist/prosemirror/commands/propertyChangeScope.js +1 -1
  123. package/dist/prosemirror/conversion/effectiveTableCellFormatting.d.ts +62 -0
  124. package/dist/prosemirror/conversion/effectiveTableCellFormatting.js +131 -0
  125. package/dist/prosemirror/conversion/fromProseDoc.js +34 -18
  126. package/dist/prosemirror/conversion/toProseDoc.js +81 -77
  127. package/dist/prosemirror/extensions/StarterKit.js +2 -0
  128. package/dist/prosemirror/extensions/core/ParagraphExtension.js +7 -2
  129. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +1 -1
  130. package/dist/prosemirror/extensions/features/ListExtension.js +1 -0
  131. package/dist/prosemirror/extensions/marks/FootnoteRefExtension.js +1 -0
  132. package/dist/prosemirror/extensions/marks/HighlightExtension.js +1 -1
  133. package/dist/prosemirror/extensions/marks/LanguageExtension.d.ts +7 -0
  134. package/dist/prosemirror/extensions/marks/LanguageExtension.js +30 -0
  135. package/dist/prosemirror/extensions/marks/RunShadingExtension.js +1 -1
  136. package/dist/prosemirror/extensions/marks/TextColorExtension.js +1 -1
  137. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +1 -1
  138. package/dist/prosemirror/extensions/marks/markUtils.js +12 -0
  139. package/dist/prosemirror/extensions/nodes/ImageExtension.js +3 -1
  140. package/dist/prosemirror/extensions/nodes/TableExtension.js +4 -2
  141. package/dist/prosemirror/schema/index.d.ts +2 -2
  142. package/dist/prosemirror/schema/marks.d.ts +6 -1
  143. package/dist/prosemirror/schema/nodes.d.ts +15 -12
  144. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  145. package/dist/prosemirror/utils/tabCalculator.d.ts +3 -1
  146. package/dist/prosemirror/utils/tabCalculator.js +12 -10
  147. package/dist/prosemirror/validation.js +4 -1
  148. package/dist/redline.d.ts +26 -11
  149. package/dist/redline.js +107 -64
  150. package/dist/server.d.ts +9 -5
  151. package/dist/server.js +8 -4
  152. package/dist/style-sets/extract.d.ts +35 -0
  153. package/dist/style-sets/extract.js +123 -0
  154. package/dist/style-sets/stellaStyle.d.ts +13 -0
  155. package/dist/style-sets/stellaStyle.js +516 -0
  156. package/dist/style-sets/types.d.ts +31 -0
  157. package/dist/style-sets/types.js +5 -0
  158. package/dist/utils/createDocument.d.ts +12 -2
  159. package/dist/utils/createDocument.js +42 -31
  160. package/dist/utils/fontResolver.js +6 -6
  161. package/dist/utils/formatToStyle.js +1 -1
  162. package/dist/utils/paragraphFormattingMerge.d.ts +14 -3
  163. package/dist/utils/paragraphFormattingMerge.js +14 -4
  164. package/dist/version-comparison.d.ts +65 -11
  165. package/dist/version-comparison.js +187 -29
  166. package/package.json +5 -1
  167. package/dist/docx/capabilities.d.ts +0 -41
  168. package/dist/docx/capabilities.js +0 -322
@@ -1,28 +1,24 @@
1
- import { isFloatingTextBoxBlock } from "./types.js";
2
1
  import { measureParagraph } from "./measure/measureParagraph.js";
2
+ import { isEmptyParagraph } from "./paragraphSpacing.js";
3
+ import { createTableCellFlowState, placeTableCellBlock } from "./measure/tableCellFlow.js";
3
4
  import { buildTableCellFloatingZones, getTableCellContentWidth, getTableCellFloatingImages } from "./measure/tableCellFloating.js";
4
5
  //#region src/layout-engine/tableRowBreak.ts
5
6
  /**
6
7
  * Table row-break geometry. Ported from eigenpal/docx-editor#698 (folio subset).
7
8
  *
8
- * Word lets a table row break across a page boundary ("allow row to break
9
- * across pages", on by default). When a row is taller than a whole page it
10
- * cannot fit anywhere, so the portion that fits stays and the rest continues on
11
- * the next page broken between whole text lines, never through a glyph.
12
- * Without this folio forced the oversized row onto one page and the overflow
13
- * was clipped (content lost; upstream #570).
9
+ * Table rows may break across flow regions unless `cantSplit` is set. When a
10
+ * permitted row exceeds the current region, the portion that fits stays and
11
+ * the rest continues in the next region, broken between whole text lines and
12
+ * never through a glyph. This also prevents oversized rows from overflowing
13
+ * one page and clipping content (upstream #570).
14
14
  *
15
15
  * This computes, per row, the safe break offsets (the y of every line bottom in
16
16
  * the row's cells) so the paginator can snap a break to the deepest whole line
17
17
  * that still fits. Vertically-merged cells that span into a row are not modeled
18
18
  * here yet (a separate follow-up); each row uses its own cells' content.
19
19
  */
20
+ const BREAK_OFFSET_EPSILON = .01;
20
21
  const DEFAULT_TABLE_CELL_PADDING_TOP = 1;
21
- function getAtomicBlockHeight(measure) {
22
- if ("totalHeight" in measure) return measure.totalHeight;
23
- if ("height" in measure) return measure.height;
24
- return 0;
25
- }
26
22
  function isInsideRange(offset, range) {
27
23
  return offset > range.top && offset < range.bottom;
28
24
  }
@@ -39,6 +35,10 @@ function shiftCellGeometry(geometry, offset) {
39
35
  unsafeRanges: geometry.unsafeRanges.map((range) => ({
40
36
  top: range.top + offset,
41
37
  bottom: range.bottom + offset
38
+ })),
39
+ suppressibleLeadingRanges: geometry.suppressibleLeadingRanges.map((range) => ({
40
+ top: range.top + offset,
41
+ bottom: range.bottom + offset
42
42
  }))
43
43
  };
44
44
  }
@@ -46,22 +46,28 @@ function shiftCellGeometry(geometry, offset) {
46
46
  function cellBreakGeometry(cell, measure) {
47
47
  const bottoms = [];
48
48
  const unsafeRanges = [];
49
+ const suppressibleLeadingRanges = [];
49
50
  const cellBlocks = cell?.blocks;
50
51
  const blockMeasures = measure.blocks;
51
52
  const padTop = cell?.padding?.top ?? DEFAULT_TABLE_CELL_PADDING_TOP;
52
53
  const contentWidth = getTableCellContentWidth(cell, measure);
53
54
  const floatingZones = buildTableCellFloatingZones(cell !== void 0 ? getTableCellFloatingImages(cell, measure, contentWidth) : [], contentWidth);
54
- let y = padTop;
55
- let paragraphY = 0;
55
+ const flowState = createTableCellFlowState();
56
56
  for (let i = 0; i < blockMeasures.length; i++) {
57
57
  let blockMeasure = blockMeasures[i];
58
58
  if (blockMeasure?.kind === "paragraph") {
59
59
  const block = cellBlocks?.[i];
60
- if (block?.kind === "paragraph" && floatingZones.length > 0) blockMeasure = measureParagraph(block, contentWidth, {
60
+ if (block?.kind !== "paragraph") continue;
61
+ if (floatingZones.length > 0) blockMeasure = measureParagraph(block, contentWidth, {
61
62
  floatingZones,
62
- paragraphYOffset: paragraphY
63
+ paragraphYOffset: placeTableCellBlock({ ...flowState }, block, blockMeasure).contentTop
64
+ });
65
+ const placement = placeTableCellBlock(flowState, block, blockMeasure);
66
+ if (placement.leadingSpacing > 0 && isEmptyParagraph(block)) suppressibleLeadingRanges.push({
67
+ top: padTop + placement.top,
68
+ bottom: padTop + placement.contentTop
63
69
  });
64
- const blockTop = y;
70
+ let y = padTop + placement.contentTop;
65
71
  for (const line of blockMeasure.lines) {
66
72
  y += line.floatSkipBefore ?? 0;
67
73
  const top = y;
@@ -72,16 +78,15 @@ function cellBreakGeometry(cell, measure) {
72
78
  });
73
79
  bottoms.push(y);
74
80
  }
75
- y = blockTop + blockMeasure.totalHeight;
76
- paragraphY += blockMeasure.totalHeight;
81
+ bottoms.push(padTop + flowState.height);
77
82
  } else if (blockMeasure) {
78
83
  const block = cellBlocks?.[i];
79
- if (block?.kind === "textBox" && isFloatingTextBoxBlock(block)) continue;
80
- const blockHeight = getAtomicBlockHeight(blockMeasure);
84
+ if (!block) continue;
85
+ const placement = placeTableCellBlock(flowState, block, blockMeasure);
86
+ const blockHeight = placement.contentHeight;
81
87
  if (blockHeight > 0) {
82
- const top = y;
83
- y += blockHeight;
84
- paragraphY += blockHeight;
88
+ const top = padTop + placement.contentTop;
89
+ const y = top + blockHeight;
85
90
  unsafeRanges.push({
86
91
  top,
87
92
  bottom: y
@@ -92,9 +97,26 @@ function cellBreakGeometry(cell, measure) {
92
97
  }
93
98
  return {
94
99
  bottoms,
95
- unsafeRanges
100
+ unsafeRanges,
101
+ suppressibleLeadingRanges
96
102
  };
97
103
  }
104
+ const continuationSkipForCell = (geometry, offset) => {
105
+ if (!geometry.unsafeRanges.some(({ bottom }) => bottom > offset + BREAK_OFFSET_EPSILON)) return;
106
+ const leadingRange = geometry.suppressibleLeadingRanges.find(({ top, bottom }) => top <= offset + BREAK_OFFSET_EPSILON && bottom > offset + BREAK_OFFSET_EPSILON);
107
+ return leadingRange ? leadingRange.bottom - offset : 0;
108
+ };
109
+ /**
110
+ * Leading spacing before an empty paragraph is consumed when a split row
111
+ * resumes at the top of a fresh flow region. Only a whitespace band shared by
112
+ * every cell with remaining content is suppressible; non-empty paragraphs,
113
+ * line boxes, cell padding, and explicit row height retain authored space.
114
+ */
115
+ const continuationSkipAfter = (geometries, offset) => {
116
+ const skips = geometries.map((geometry) => continuationSkipForCell(geometry, offset)).filter((skip) => skip !== void 0);
117
+ if (skips.length === 0 || skips.some((skip) => skip <= BREAK_OFFSET_EPSILON)) return 0;
118
+ return Math.min(...skips);
119
+ };
98
120
  /** Build break geometry for a table from its block + measure. */
99
121
  function buildTableRowBreakInfo(block, measure) {
100
122
  const rowCount = measure.rows.length;
@@ -106,6 +128,7 @@ function buildTableRowBreakInfo(block, measure) {
106
128
  }
107
129
  rowTops.push(acc);
108
130
  const breakOffsets = [];
131
+ const continuationSkips = [];
109
132
  for (let r = 0; r < rowCount; r++) {
110
133
  const rowHeight = measure.rows[r]?.height ?? 0;
111
134
  const offsets = /* @__PURE__ */ new Set();
@@ -121,14 +144,24 @@ function buildTableRowBreakInfo(block, measure) {
121
144
  cellGeometries.push(geometry);
122
145
  for (const b of geometry.bottoms) if (b > 0 && b < rowHeight) offsets.add(b);
123
146
  }
124
- const safeOffsets = [...offsets].filter((offset) => offset === rowHeight || cellGeometries.every((geometry) => geometry.unsafeRanges.every((range) => !isInsideRange(offset, range))));
125
- breakOffsets.push(safeOffsets.sort((a, b) => a - b));
147
+ const sortedOffsets = [...offsets].filter((offset) => offset === rowHeight || cellGeometries.every((geometry) => geometry.unsafeRanges.every((range) => !isInsideRange(offset, range)))).sort((a, b) => a - b);
148
+ breakOffsets.push(sortedOffsets);
149
+ continuationSkips.push(sortedOffsets.map((offset) => continuationSkipAfter(cellGeometries, offset)));
126
150
  }
127
151
  return {
128
152
  rowTops,
129
- breakOffsets
153
+ breakOffsets,
154
+ continuationSkips
130
155
  };
131
156
  }
157
+ /** Leading empty-paragraph whitespace to consume when a row resumes after `offset`. */
158
+ function getRowContinuationSkip(info, rowIndex, offset) {
159
+ const offsets = info.breakOffsets[rowIndex];
160
+ const skips = info.continuationSkips[rowIndex];
161
+ const index = offsets?.findIndex((candidate) => Math.abs(candidate - offset) <= BREAK_OFFSET_EPSILON);
162
+ if (index === void 0 || index < 0) return 0;
163
+ return skips?.[index] ?? 0;
164
+ }
132
165
  /**
133
166
  * Given a row and how much of it has already been placed (`fromOffset`), return
134
167
  * how many more px can be placed ending on a whole line, without exceeding
@@ -147,4 +180,4 @@ function snapRowBreak(info, rowIndex, fromOffset, maxSlice) {
147
180
  return best;
148
181
  }
149
182
  //#endregion
150
- export { buildTableRowBreakInfo, snapRowBreak };
183
+ export { buildTableRowBreakInfo, getRowContinuationSkip, snapRowBreak };
@@ -1,5 +1,5 @@
1
1
  import { OutlineStyleAttr } from "../types/documentEnumValues.js";
2
- import { ImagePosition, ImageWrap, SdtProperties, SdtType, ShapeTextBody, TableWidthType } from "@stll/docx-core/model";
2
+ import { ImagePosition, ImageWrap, SdtProperties, SdtType, ShapeTextBody, TableCellFormatting, TableWidthType } from "@stll/docx-core/model";
3
3
 
4
4
  //#region src/layout-engine/types.d.ts
5
5
  declare namespace types_d_exports {
@@ -38,7 +38,12 @@ type RunFormatting = {
38
38
  * `segmentByScript`) so wrapping stays correct. Absent means CJK falls back
39
39
  * to `fontFamily`.
40
40
  */
41
- eastAsiaFontFamily?: string;
41
+ eastAsiaFontFamily?: string; /** Run language metadata resolved from `w:lang`. */
42
+ language?: {
43
+ val?: string;
44
+ eastAsia?: string;
45
+ bidi?: string;
46
+ };
42
47
  fontSize?: number;
43
48
  letterSpacing?: number;
44
49
  superscript?: boolean;
@@ -157,14 +162,16 @@ type ImageRun = {
157
162
  * eigenpal #424 (opacity render pipeline).
158
163
  */
159
164
  opacity?: number; /** Position for floating/anchored images */
160
- position?: ImageRunPosition; /** Wrap type from DOCX (inline, square, tight, through, topAndBottom, etc.) */
165
+ position?: ImageRunPosition; /** Whether a table-cell anchor uses the cell as its positioning scope. Undefined defaults true. */
166
+ layoutInCell?: boolean; /** Wrap type from DOCX (inline, square, tight, through, topAndBottom, etc.) */
161
167
  wrapType?: ImageWrap["type"]; /** Display mode for CSS rendering */
162
168
  displayMode?: "inline" | "block" | "float"; /** CSS float direction */
163
169
  cssFloat?: "left" | "right" | "none"; /** Wrap distances in pixels */
164
170
  distTop?: number;
165
171
  distBottom?: number;
166
172
  distLeft?: number;
167
- distRight?: number;
173
+ distRight?: number; /** Use the image box as the exact line height for an embedded-object preview. */
174
+ exactLineHeight?: boolean;
168
175
  /**
169
176
  * wp:srcRect crop fractions in [0, 1]; emit as CSS `clip-path: inset(...)`
170
177
  * to match Word's visible region. eigenpal #424 (image-crop subset).
@@ -286,7 +293,27 @@ type ListNumPr = {
286
293
  * Paragraph block attributes.
287
294
  */
288
295
  type ParagraphAttrs = {
289
- alignment?: "left" | "center" | "right" | "justify"; /** OOXML outline level (`w:outlineLvl`), where zero is the top level. */
296
+ alignment?: "left" | "center" | "right" | "justify"; /** East Asian first/last-character restrictions (`w:kinsoku`). */
297
+ kinsoku?: boolean; /** Hanging punctuation (`w:overflowPunct`); defaults to enabled when omitted. */
298
+ overflowPunctuation?: boolean; /** Exempt this paragraph from document automatic hyphenation. */
299
+ suppressAutoHyphens?: boolean; /** Document-wide automatic hyphenation controls retained for line layout. */
300
+ automaticHyphenation?: {
301
+ enabled: true;
302
+ doNotHyphenateCaps?: boolean;
303
+ consecutiveLineLimit?: number; /** Maximum tolerated line-end whitespace before hyphenation, in twips. */
304
+ hyphenationZoneTwips?: number;
305
+ }; /** Document-wide custom line-edge characters and compatibility behavior. */
306
+ lineBreakRules?: {
307
+ noLineBreaksBefore?: {
308
+ language?: string;
309
+ characters: string;
310
+ };
311
+ noLineBreaksAfter?: {
312
+ language?: string;
313
+ characters: string;
314
+ };
315
+ useLegacyEthiopicAmharicRules?: boolean;
316
+ }; /** OOXML outline level (`w:outlineLvl`), where zero is the top level. */
290
317
  outlineLevel?: number;
291
318
  spacing?: ParagraphSpacing; /** Spacing sides resolved from OOXML automatic paragraph spacing. */
292
319
  automaticSpacing?: {
@@ -338,7 +365,8 @@ type ParagraphAttrs = {
338
365
  listIsBullet?: boolean;
339
366
  listMarkerHidden?: boolean;
340
367
  listMarkerFontFamily?: string;
341
- listMarkerFontSize?: number; /** Horizontal alignment of the marker around the paragraph's list anchor. */
368
+ listMarkerFontSize?: number;
369
+ listMarkerBold?: boolean; /** Horizontal alignment of the marker around the paragraph's list anchor. */
342
370
  listMarkerAlignment?: "left" | "center" | "right";
343
371
  /**
344
372
  * `w:suff` (§17.9.25) — what follows the marker before body text.
@@ -413,6 +441,8 @@ type CellBorders = {
413
441
  bottom?: CellBorderSpec;
414
442
  left?: CellBorderSpec;
415
443
  right?: CellBorderSpec;
444
+ topLeftToBottomRight?: CellBorderSpec;
445
+ topRightToBottomLeft?: CellBorderSpec;
416
446
  };
417
447
  /**
418
448
  * A table cell with content.
@@ -424,6 +454,7 @@ type TableCell = {
424
454
  rowSpan?: number;
425
455
  width?: number;
426
456
  verticalAlign?: "top" | "center" | "bottom";
457
+ textDirection?: NonNullable<TableCellFormatting["textDirection"]>;
427
458
  background?: string;
428
459
  borders?: CellBorders; /** Per-cell padding in pixels (from w:tcMar or table-level w:tblCellMar) */
429
460
  padding?: {
@@ -449,7 +480,8 @@ type TableRow = {
449
480
  gridAfter?: number;
450
481
  height?: number;
451
482
  heightRule?: "auto" | "atLeast" | "exact";
452
- isHeader?: boolean;
483
+ isHeader?: boolean; /** `w:cantSplit`: keep this row in one flow region. */
484
+ cantSplit?: boolean;
453
485
  hidden?: boolean;
454
486
  };
455
487
  /**
@@ -669,7 +701,10 @@ type MeasuredLine = {
669
701
  * no usable horizontal room at the natural line Y. Painters render this
670
702
  * as marginTop on the line element; measurement adds it to totalHeight.
671
703
  */
672
- floatSkipBefore?: number;
704
+ floatSkipBefore?: number; /** Decorative hyphen inserted at a dictionary break without changing source text. */
705
+ discretionaryHyphen?: {
706
+ runIndex: number;
707
+ };
673
708
  };
674
709
  /**
675
710
  * Measurement result for a paragraph block.
@@ -0,0 +1,23 @@
1
+ import { ImageRun } from "../layout-engine/types.js";
2
+
3
+ //#region src/layout-painter/anchoredImagePosition.d.ts
4
+ type PageGeometry = {
5
+ pageWidth: number;
6
+ pageHeight: number;
7
+ marginLeft: number;
8
+ marginTop: number;
9
+ marginRight: number;
10
+ marginBottom: number;
11
+ contentWidth: number;
12
+ contentHeight: number;
13
+ };
14
+ /** Coordinates relative to the page content area's top-left corner. */
15
+ type AnchoredImagePosition = {
16
+ x: number;
17
+ y: number;
18
+ side: "left" | "right";
19
+ };
20
+ /** Resolve an anchored image into page-content-relative coordinates. */
21
+ declare function resolveAnchoredImagePosition(imgRun: ImageRun, fragmentY: number, geometry: PageGeometry): AnchoredImagePosition;
22
+ //#endregion
23
+ export { AnchoredImagePosition, PageGeometry, resolveAnchoredImagePosition };
@@ -0,0 +1,95 @@
1
+ import { emuToPixels } from "./renderUtils.js";
2
+ //#region src/layout-painter/anchoredImagePosition.ts
3
+ function resolveHorizontalBand(relativeTo, geometry) {
4
+ switch (relativeTo) {
5
+ case "page": return {
6
+ baseX: -geometry.marginLeft,
7
+ bandWidth: geometry.pageWidth
8
+ };
9
+ case "leftMargin":
10
+ case "insideMargin": return {
11
+ baseX: -geometry.marginLeft,
12
+ bandWidth: geometry.marginLeft
13
+ };
14
+ case "rightMargin":
15
+ case "outsideMargin": return {
16
+ baseX: geometry.contentWidth,
17
+ bandWidth: geometry.marginRight
18
+ };
19
+ case "character": return {
20
+ baseX: 0,
21
+ bandWidth: 0
22
+ };
23
+ default: return {
24
+ baseX: 0,
25
+ bandWidth: geometry.contentWidth
26
+ };
27
+ }
28
+ }
29
+ function resolveVerticalBand(relativeTo, fragmentY, geometry) {
30
+ switch (relativeTo) {
31
+ case "page": return {
32
+ baseY: -geometry.marginTop,
33
+ bandHeight: geometry.pageHeight
34
+ };
35
+ case "topMargin": return {
36
+ baseY: -geometry.marginTop,
37
+ bandHeight: geometry.marginTop
38
+ };
39
+ case "bottomMargin": return {
40
+ baseY: geometry.contentHeight,
41
+ bandHeight: geometry.marginBottom
42
+ };
43
+ case "paragraph":
44
+ case "line": return {
45
+ baseY: fragmentY,
46
+ bandHeight: 0
47
+ };
48
+ default: return {
49
+ baseY: 0,
50
+ bandHeight: geometry.contentHeight
51
+ };
52
+ }
53
+ }
54
+ /** Resolve an anchored image into page-content-relative coordinates. */
55
+ function resolveAnchoredImagePosition(imgRun, fragmentY, geometry) {
56
+ const position = imgRun.position;
57
+ const contentWidth = geometry.contentWidth;
58
+ let side = "left";
59
+ let x = 0;
60
+ if (position?.horizontal) {
61
+ const h = position.horizontal;
62
+ const { baseX, bandWidth } = resolveHorizontalBand(h.relativeTo, geometry);
63
+ const horizontalHasBand = h.relativeTo !== "character";
64
+ if (h.align === "right" || h.align === "outside") {
65
+ side = "right";
66
+ x = horizontalHasBand ? baseX + bandWidth - imgRun.width : 0;
67
+ } else if (h.align === "left" || h.align === "inside") x = baseX;
68
+ else if (h.align === "center") x = horizontalHasBand ? baseX + (bandWidth - imgRun.width) / 2 : 0;
69
+ else if (h.posOffset !== void 0) {
70
+ x = baseX + emuToPixels(h.posOffset);
71
+ side = x > contentWidth / 2 ? "right" : "left";
72
+ } else x = baseX;
73
+ } else if (imgRun.cssFloat === "right") {
74
+ side = "right";
75
+ x = contentWidth - imgRun.width;
76
+ }
77
+ let y;
78
+ if (position?.vertical) {
79
+ const v = position.vertical;
80
+ const { baseY, bandHeight } = resolveVerticalBand(v.relativeTo, fragmentY, geometry);
81
+ const verticalHasBand = v.relativeTo !== "paragraph" && v.relativeTo !== "line";
82
+ if (v.align === "top" || v.align === "inside") y = baseY;
83
+ else if (v.align === "center") y = verticalHasBand ? baseY + (bandHeight - imgRun.height) / 2 : fragmentY;
84
+ else if (v.align === "bottom" || v.align === "outside") y = verticalHasBand ? baseY + bandHeight - imgRun.height : fragmentY;
85
+ else if (v.posOffset !== void 0) y = baseY + emuToPixels(v.posOffset);
86
+ else y = verticalHasBand ? baseY : fragmentY;
87
+ } else y = fragmentY;
88
+ return {
89
+ x,
90
+ y,
91
+ side
92
+ };
93
+ }
94
+ //#endregion
95
+ export { resolveAnchoredImagePosition };
@@ -0,0 +1,23 @@
1
+ import { BorderStyle, ParagraphBorders } from "../layout-engine/types.js";
2
+
3
+ //#region src/layout-painter/borderStroke.d.ts
4
+ type CssBorderStroke = {
5
+ color: string;
6
+ style: string;
7
+ width: number;
8
+ };
9
+ /**
10
+ * Preserve the optical weight of authored subpixel borders. CSS quantizes a
11
+ * fractional border width to one device pixel, so its color carries the
12
+ * fractional coverage while layout keeps the authored width.
13
+ */
14
+ declare const resolveCssBorderStroke: (border: Pick<BorderStyle, "color" | "style" | "width">) => CssBorderStroke;
15
+ declare const borderStrokeToCss: (border: Pick<BorderStyle, "color" | "style" | "width">) => string;
16
+ type ParagraphBorderHorizontalOutsets = {
17
+ left: number;
18
+ right: number;
19
+ };
20
+ /** Resolve the outer horizontal edges of a paragraph border box. */
21
+ declare const resolveParagraphBorderHorizontalOutsets: (borders: ParagraphBorders, hasHorizontalRule: boolean) => ParagraphBorderHorizontalOutsets;
22
+ //#endregion
23
+ export { borderStrokeToCss, resolveCssBorderStroke, resolveParagraphBorderHorizontalOutsets };
@@ -0,0 +1,39 @@
1
+ import { pointsToPixels } from "../utils/units.js";
2
+ //#region src/layout-painter/borderStroke.ts
3
+ const DEFAULT_BORDER_COLOR = "#000000";
4
+ const DEFAULT_BORDER_STYLE = "solid";
5
+ const CSS_HAIRLINE_WIDTH = 1;
6
+ const PARAGRAPH_RULE_ENDPOINT_OUTSET = pointsToPixels(1.5);
7
+ /**
8
+ * Preserve the optical weight of authored subpixel borders. CSS quantizes a
9
+ * fractional border width to one device pixel, so its color carries the
10
+ * fractional coverage while layout keeps the authored width.
11
+ */
12
+ const resolveCssBorderStroke = (border) => {
13
+ const authoredWidth = border.width ?? CSS_HAIRLINE_WIDTH;
14
+ if (authoredWidth <= 0 || authoredWidth >= CSS_HAIRLINE_WIDTH) return {
15
+ color: border.color ?? DEFAULT_BORDER_COLOR,
16
+ style: border.style ?? DEFAULT_BORDER_STYLE,
17
+ width: Math.max(CSS_HAIRLINE_WIDTH, authoredWidth)
18
+ };
19
+ const coveragePercent = Number((authoredWidth * 100).toFixed(4));
20
+ return {
21
+ color: `color-mix(in srgb, ${border.color ?? DEFAULT_BORDER_COLOR} ${coveragePercent}%, transparent)`,
22
+ style: border.style ?? DEFAULT_BORDER_STYLE,
23
+ width: CSS_HAIRLINE_WIDTH
24
+ };
25
+ };
26
+ const borderStrokeToCss = (border) => {
27
+ const stroke = resolveCssBorderStroke(border);
28
+ return `${stroke.width}px ${stroke.style} ${stroke.color}`;
29
+ };
30
+ /** Resolve the outer horizontal edges of a paragraph border box. */
31
+ const resolveParagraphBorderHorizontalOutsets = (borders, hasHorizontalRule) => {
32
+ const ruleOutset = hasHorizontalRule ? PARAGRAPH_RULE_ENDPOINT_OUTSET : 0;
33
+ return {
34
+ left: borders.left ? (borders.left.space ?? 0) + (borders.left.width ?? 0) : ruleOutset,
35
+ right: borders.right ? (borders.right.space ?? 0) + (borders.right.width ?? 0) : ruleOutset
36
+ };
37
+ };
38
+ //#endregion
39
+ export { borderStrokeToCss, resolveCssBorderStroke, resolveParagraphBorderHorizontalOutsets };
@@ -10,7 +10,7 @@ declare const IMAGE_CLASS_NAMES: {
10
10
  imageAnchored: string;
11
11
  };
12
12
  /**
13
- * Structural shape required to apply Word's per-image visual attributes:
13
+ * Structural shape required to apply OOXML per-image visual attributes:
14
14
  * `wp:srcRect` crop fractions and `a:alphaModFix` opacity. `ImageRun` and
15
15
  * `ImageBlock` both satisfy this, so callers don't need an adapter.
16
16
  */
@@ -32,10 +32,11 @@ type ImageVisualAttrs = {
32
32
  * `0`) — that bug otherwise hides every plain image behind `opacity: 0`.
33
33
  */
34
34
  declare function hasImageVisualAttrs(v: ImageVisualAttrs): boolean;
35
+ declare function hasImageCrop(v: ImageVisualAttrs): boolean;
35
36
  /**
36
- * Apply Word's `<a:srcRect>` crop to an `<img>` whose parent already has
37
+ * Apply an OOXML `<a:srcRect>` crop to an `<img>` whose parent already has
37
38
  * `overflow: hidden` and is sized to the *visible* (cropped) dimensions
38
- * (this matches `wp:extent` semantics: in Word the extent is the cropped
39
+ * (this matches `wp:extent` semantics: the extent is the cropped
39
40
  * frame, with `<a:stretch><a:fillRect/>` stretching the cropped source to
40
41
  * fill it).
41
42
  *
@@ -85,4 +86,4 @@ type RenderImageFragmentOptions = {
85
86
  */
86
87
  declare function renderImageFragment(fragment: ImageFragment, block: ImageBlock, _measure: ImageMeasure, _context: RenderContext, options?: RenderImageFragmentOptions): HTMLElement;
87
88
  //#endregion
88
- export { IMAGE_CLASS_NAMES, ImageVisualAttrs, RenderImageFragmentOptions, applyImageVisualAttrs, hasImageVisualAttrs, renderImageFragment, wrapImageWithCrop };
89
+ export { IMAGE_CLASS_NAMES, ImageVisualAttrs, RenderImageFragmentOptions, applyImageVisualAttrs, hasImageCrop, hasImageVisualAttrs, renderImageFragment, wrapImageWithCrop };
@@ -18,12 +18,15 @@ const IMAGE_CLASS_NAMES = {
18
18
  */
19
19
  function hasImageVisualAttrs(v) {
20
20
  if (v.opacity != null && v.opacity < 1) return true;
21
+ return hasImageCrop(v);
22
+ }
23
+ function hasImageCrop(v) {
21
24
  return Boolean(v.cropTop || v.cropRight || v.cropBottom || v.cropLeft);
22
25
  }
23
26
  /**
24
- * Apply Word's `<a:srcRect>` crop to an `<img>` whose parent already has
27
+ * Apply an OOXML `<a:srcRect>` crop to an `<img>` whose parent already has
25
28
  * `overflow: hidden` and is sized to the *visible* (cropped) dimensions
26
- * (this matches `wp:extent` semantics: in Word the extent is the cropped
29
+ * (this matches `wp:extent` semantics: the extent is the cropped
27
30
  * frame, with `<a:stretch><a:fillRect/>` stretching the cropped source to
28
31
  * fill it).
29
32
  *
@@ -54,8 +57,19 @@ function applyImageVisualAttrs(img, v) {
54
57
  const fh = 1 / remainingH;
55
58
  img.style.width = `${fw * 100}%`;
56
59
  img.style.height = `${fh * 100}%`;
60
+ img.style.maxWidth = "none";
61
+ img.style.maxHeight = "none";
57
62
  img.style.marginLeft = `${-left * fw * 100}%`;
58
- img.style.marginTop = `${-top * fh * 100}%`;
63
+ const existingTransform = img.style.transform;
64
+ if (existingTransform) {
65
+ const visibleCenterX = (left + 1 - right) * 50;
66
+ const visibleCenterY = (top + 1 - bottom) * 50;
67
+ img.style.transformOrigin = `${visibleCenterX}% ${visibleCenterY}%`;
68
+ }
69
+ if (top !== 0) {
70
+ const cropTransform = `translateY(${-top * 100}%)`;
71
+ img.style.transform = existingTransform ? `${existingTransform} ${cropTransform}` : cropTransform;
72
+ }
59
73
  img.style.objectFit = "fill";
60
74
  }
61
75
  /**
@@ -127,4 +141,4 @@ function renderImageFragment(fragment, block, _measure, _context, options = {})
127
141
  return containerEl;
128
142
  }
129
143
  //#endregion
130
- export { IMAGE_CLASS_NAMES, applyImageVisualAttrs, hasImageVisualAttrs, renderImageFragment, wrapImageWithCrop };
144
+ export { IMAGE_CLASS_NAMES, applyImageVisualAttrs, hasImageCrop, hasImageVisualAttrs, renderImageFragment, wrapImageWithCrop };
@@ -1,6 +1,7 @@
1
1
  import { document_d_exports } from "../types/document.js";
2
2
  import { FootnoteContent, HeaderFooterContent, ImageRun, Page, isFloatingImageRun } from "../layout-engine/types.js";
3
3
  import { RenderContext, emuToPixels } from "./renderUtils.js";
4
+ import { AnchoredImagePosition, PageGeometry } from "./anchoredImagePosition.js";
4
5
  import { BlockLookup } from "./index.js";
5
6
 
6
7
  //#region src/layout-painter/renderPage.d.ts
@@ -157,32 +158,6 @@ declare function resolveHeaderFooterFloatLeft(width: number, h: {
157
158
  align?: string;
158
159
  alignment?: string;
159
160
  } | undefined, layout: HeaderFooterLayoutInfo): string;
160
- /**
161
- * Page geometry needed to translate OOXML `relativeFrom` anchors into
162
- * painter coordinates. All values are in CSS pixels. Mirrors the upstream
163
- * (eigenpal docx-editor) `PageGeometry` shape introduced in #424 so the
164
- * anchor math stays identical across the two codebases.
165
- */
166
- type PageGeometry = {
167
- pageWidth: number;
168
- pageHeight: number;
169
- marginLeft: number;
170
- marginTop: number;
171
- marginRight: number;
172
- marginBottom: number;
173
- contentWidth: number;
174
- contentHeight: number;
175
- };
176
- /**
177
- * Resolved on-page coordinates for an anchored floating image.
178
- * `x` / `y` are in content-area-relative pixels (content origin = (0, 0)).
179
- * `side` feeds the text-wrap exclusion zone helper.
180
- */
181
- type AnchoredImagePosition = {
182
- x: number;
183
- y: number;
184
- side: "left" | "right";
185
- };
186
161
  /**
187
162
  * Resolve the on-page coordinates of an anchored floating image.
188
163
  *
@@ -290,4 +265,4 @@ declare function findPageShellForPmPos(container: HTMLElement, pmPos: number): {
290
265
  isExact: boolean;
291
266
  } | null;
292
267
  //#endregion
293
- export { AnchoredImagePosition, FootnoteRenderItem, type HeaderFooterContent, HeaderFooterLayoutInfo, PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, PageFloatingImage, PageGeometry, PainterPaintedDetail, type RenderContext, RenderPageOptions, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, floatingTableReservesBand, getDefaultPageFontFamily, isFloatingImageRun, renderAllPagesNow, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft };
268
+ export { type AnchoredImagePosition, FootnoteRenderItem, type HeaderFooterContent, HeaderFooterLayoutInfo, PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, PageFloatingImage, type PageGeometry, PainterPaintedDetail, type RenderContext, RenderPageOptions, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, floatingTableReservesBand, getDefaultPageFontFamily, isFloatingImageRun, renderAllPagesNow, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft };