@stll/folio-core 0.48.0 → 0.50.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 (170) hide show
  1. package/dist/ai-edits/apply.js +565 -121
  2. package/dist/ai-edits/headless.d.ts +37 -1
  3. package/dist/ai-edits/headless.js +47 -16
  4. package/dist/ai-edits/minimal-replacement.d.ts +64 -0
  5. package/dist/ai-edits/minimal-replacement.js +282 -0
  6. package/dist/controller/layoutPipeline.js +11 -1
  7. package/dist/display-list/build/paragraphPrimitives.js +63 -7
  8. package/dist/display-list/build/textBoxPrimitives.js +1 -1
  9. package/dist/display-list/build/unsupported.d.ts +2 -2
  10. package/dist/display-list/build/unsupported.js +0 -0
  11. package/dist/display-list/dom/renderDisplayListToDom.d.ts +10 -1
  12. package/dist/display-list/dom/renderDisplayListToDom.js +8 -8
  13. package/dist/display-list/html/renderDisplayListToHtml.d.ts +21 -0
  14. package/dist/display-list/html/renderDisplayListToHtml.js +126 -0
  15. package/dist/display-list/selectDisplayPages.d.ts +6 -0
  16. package/dist/display-list/selectDisplayPages.js +44 -0
  17. package/dist/docx/blockContentParser.js +2 -2
  18. package/dist/docx/bulletMarkers.d.ts +18 -2
  19. package/dist/docx/bulletMarkers.js +27 -2
  20. package/dist/docx/drawingGroupChildren.d.ts +60 -0
  21. package/dist/docx/drawingGroupChildren.js +176 -0
  22. package/dist/docx/drawingUtils.js +3 -0
  23. package/dist/docx/groupDrawingParser.d.ts +7 -1
  24. package/dist/docx/groupDrawingParser.js +123 -32
  25. package/dist/docx/packageParts.d.ts +45 -1
  26. package/dist/docx/packageParts.js +93 -2
  27. package/dist/docx/paraIdAttribute.d.ts +3 -2
  28. package/dist/docx/paragraphParser.js +91 -7
  29. package/dist/docx/paragraphTextBoxEnrichment.js +126 -4
  30. package/dist/docx/runParser.js +10 -0
  31. package/dist/docx/selectiveSaveFlags.d.ts +10 -3
  32. package/dist/docx/selectiveSaveFlags.js +1 -1
  33. package/dist/docx/selectiveXmlPatch.d.ts +56 -34
  34. package/dist/docx/selectiveXmlPatch.js +222 -115
  35. package/dist/docx/serializer/groupTextBoxWriteBack.d.ts +12 -0
  36. package/dist/docx/serializer/groupTextBoxWriteBack.js +101 -0
  37. package/dist/docx/serializer/paragraphSerializer.js +5 -4
  38. package/dist/docx/serializer/runSerializer.d.ts +5 -1
  39. package/dist/docx/serializer/runSerializer.js +4 -3
  40. package/dist/docx/shapeAlternateContent.d.ts +25 -0
  41. package/dist/docx/shapeAlternateContent.js +52 -0
  42. package/dist/docx/textBoxParser.d.ts +10 -2
  43. package/dist/docx/textBoxParser.js +9 -3
  44. package/dist/export-pdf.d.ts +35 -2
  45. package/dist/export-pdf.js +51 -20
  46. package/dist/fonts/fontsourceFaces.d.ts +47 -0
  47. package/dist/fonts/fontsourceFaces.js +230 -0
  48. package/dist/internal/headlessRevisionResolution.js +50 -13
  49. package/dist/layout-bridge/convert/flowBlockPostprocessing.d.ts +8 -0
  50. package/dist/layout-bridge/convert/flowBlockPostprocessing.js +106 -0
  51. package/dist/layout-bridge/convert/flowBorders.d.ts +17 -0
  52. package/dist/layout-bridge/convert/flowBorders.js +31 -0
  53. package/dist/layout-bridge/convert/flowConversionShared.d.ts +111 -0
  54. package/dist/layout-bridge/convert/flowConversionShared.js +23 -0
  55. package/dist/layout-bridge/convert/footnoteLayout.d.ts +2 -1
  56. package/dist/layout-bridge/convert/footnoteLayout.js +2 -1
  57. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +2 -1
  58. package/dist/layout-bridge/convert/imageConversion.d.ts +33 -0
  59. package/dist/layout-bridge/convert/imageConversion.js +118 -0
  60. package/dist/layout-bridge/convert/listMarkers.d.ts +28 -0
  61. package/dist/layout-bridge/convert/listMarkers.js +98 -0
  62. package/dist/layout-bridge/convert/noteReferences.d.ts +14 -0
  63. package/dist/layout-bridge/convert/noteReferences.js +29 -0
  64. package/dist/layout-bridge/convert/pageBreakSplitting.d.ts +16 -0
  65. package/dist/layout-bridge/convert/pageBreakSplitting.js +140 -0
  66. package/dist/layout-bridge/convert/paragraphAttrs.d.ts +20 -0
  67. package/dist/layout-bridge/convert/paragraphAttrs.js +234 -0
  68. package/dist/layout-bridge/convert/paragraphConversion.d.ts +12 -0
  69. package/dist/layout-bridge/convert/paragraphConversion.js +112 -0
  70. package/dist/layout-bridge/convert/paragraphRuns.d.ts +15 -0
  71. package/dist/layout-bridge/convert/paragraphRuns.js +281 -0
  72. package/dist/layout-bridge/convert/runFormattingMerge.d.ts +19 -0
  73. package/dist/layout-bridge/convert/runFormattingMerge.js +167 -0
  74. package/dist/layout-bridge/convert/runMarkFormatting.d.ts +11 -0
  75. package/dist/layout-bridge/convert/runMarkFormatting.js +291 -0
  76. package/dist/layout-bridge/convert/sectionBoundaries.d.ts +35 -0
  77. package/dist/layout-bridge/convert/sectionBoundaries.js +114 -0
  78. package/dist/layout-bridge/convert/tableConversion.d.ts +12 -0
  79. package/dist/layout-bridge/convert/tableConversion.js +303 -0
  80. package/dist/layout-bridge/convert/textBoxFill.d.ts +20 -0
  81. package/dist/layout-bridge/convert/textBoxFill.js +42 -0
  82. package/dist/layout-bridge/convert/textFormattingConversion.d.ts +22 -0
  83. package/dist/layout-bridge/convert/textFormattingConversion.js +120 -0
  84. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +5 -113
  85. package/dist/layout-bridge/convert/toFlowBlocks.js +11 -2060
  86. package/dist/layout-bridge/engine/hitTest.js +1 -1
  87. package/dist/layout-bridge/engine/measuring/index.d.ts +2 -1
  88. package/dist/layout-bridge/engine/measuring/index.js +2 -1
  89. package/dist/layout-bridge/engine/selectionRects.js +1 -1
  90. package/dist/layout-engine/imageLayout.d.ts +9 -0
  91. package/dist/layout-engine/imageLayout.js +48 -0
  92. package/dist/layout-engine/index.d.ts +4 -19
  93. package/dist/layout-engine/index.js +14 -910
  94. package/dist/layout-engine/keep-together.d.ts +5 -2
  95. package/dist/layout-engine/keep-together.js +5 -1
  96. package/dist/layout-engine/layoutFlowShared.d.ts +6 -0
  97. package/dist/layout-engine/layoutFlowShared.js +8 -0
  98. package/dist/layout-engine/measure/advanceComposition.js +49 -3
  99. package/dist/layout-engine/measure/crossRunWords.d.ts +45 -0
  100. package/dist/layout-engine/measure/crossRunWords.js +172 -0
  101. package/dist/layout-engine/measure/floatingLineClearance.d.ts +24 -0
  102. package/dist/layout-engine/measure/floatingLineClearance.js +44 -0
  103. package/dist/layout-engine/measure/inlineObjectMetrics.d.ts +6 -0
  104. package/dist/layout-engine/measure/inlineObjectMetrics.js +52 -0
  105. package/dist/layout-engine/measure/justification.d.ts +32 -0
  106. package/dist/layout-engine/measure/justification.js +83 -0
  107. package/dist/layout-engine/measure/lineFitting.d.ts +48 -0
  108. package/dist/layout-engine/measure/lineFitting.js +145 -0
  109. package/dist/layout-engine/measure/lineTypography.d.ts +44 -0
  110. package/dist/layout-engine/measure/lineTypography.js +124 -0
  111. package/dist/layout-engine/measure/measureBlocks.js +7 -4
  112. package/dist/layout-engine/measure/measureContainer.js +46 -3
  113. package/dist/layout-engine/measure/measureHelpers.js +13 -3
  114. package/dist/layout-engine/measure/measureParagraph.d.ts +1 -20
  115. package/dist/layout-engine/measure/measureParagraph.js +41 -780
  116. package/dist/layout-engine/measure/paragraphMeasureShared.d.ts +109 -0
  117. package/dist/layout-engine/measure/paragraphMeasureShared.js +82 -0
  118. package/dist/layout-engine/measure/paragraphTabs.d.ts +18 -0
  119. package/dist/layout-engine/measure/paragraphTabs.js +78 -0
  120. package/dist/layout-engine/measure/smallCapsCasing.d.ts +56 -0
  121. package/dist/layout-engine/measure/smallCapsCasing.js +88 -0
  122. package/dist/layout-engine/paginator.d.ts +2 -0
  123. package/dist/layout-engine/paginator.js +5 -1
  124. package/dist/layout-engine/paragraphLayout.d.ts +23 -0
  125. package/dist/layout-engine/paragraphLayout.js +204 -0
  126. package/dist/layout-engine/paragraphSpacing.d.ts +4 -2
  127. package/dist/layout-engine/paragraphSpacing.js +2 -1
  128. package/dist/layout-engine/sectionLayout.d.ts +45 -0
  129. package/dist/layout-engine/sectionLayout.js +216 -0
  130. package/dist/layout-engine/tableLayout.d.ts +17 -0
  131. package/dist/layout-engine/tableLayout.js +369 -0
  132. package/dist/layout-engine/tableRowBreak.d.ts +8 -1
  133. package/dist/layout-engine/tableRowBreak.js +22 -1
  134. package/dist/layout-engine/textBoxLayout.d.ts +15 -0
  135. package/dist/layout-engine/textBoxLayout.js +137 -0
  136. package/dist/layout-engine/types.d.ts +21 -2
  137. package/dist/layout-painter/renderPage.js +1 -0
  138. package/dist/layout-painter/renderParagraph.js +41 -4
  139. package/dist/layout-painter/renderTextBox.d.ts +12 -2
  140. package/dist/layout-painter/renderTextBox.js +26 -2
  141. package/dist/paged-layout/sectionBlockWidths.d.ts +2 -1
  142. package/dist/paged-layout/sectionBlockWidths.js +1 -1
  143. package/dist/prosemirror/alternateContentAttrs.d.ts +9 -0
  144. package/dist/prosemirror/alternateContentAttrs.js +35 -0
  145. package/dist/prosemirror/attrs/index.js +90 -0
  146. package/dist/prosemirror/commands/comments.js +51 -41
  147. package/dist/prosemirror/contentControlRevisions.d.ts +43 -0
  148. package/dist/prosemirror/contentControlRevisions.js +84 -0
  149. package/dist/prosemirror/conversion/fromProseDoc.js +46 -22
  150. package/dist/prosemirror/conversion/toProseDoc.js +42 -19
  151. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.d.ts +7 -7
  152. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -7
  153. package/dist/prosemirror/extensions/nodes/FieldExtension.js +6 -3
  154. package/dist/prosemirror/extensions/nodes/SdtExtension.js +9 -2
  155. package/dist/prosemirror/extensions/nodes/ShapeExtension.js +1 -0
  156. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +3 -0
  157. package/dist/prosemirror/listMarker.js +2 -2
  158. package/dist/prosemirror/paragraphDirection.d.ts +29 -2
  159. package/dist/prosemirror/paragraphDirection.js +21 -2
  160. package/dist/prosemirror/plugins/suggestionMode.js +37 -8
  161. package/dist/prosemirror/rejoinRunCarriers.d.ts +27 -0
  162. package/dist/prosemirror/rejoinRunCarriers.js +66 -0
  163. package/dist/prosemirror/runIdentityAcrossRevisions.d.ts +10 -0
  164. package/dist/prosemirror/runIdentityAcrossRevisions.js +90 -0
  165. package/dist/prosemirror/schema/nodes.d.ts +37 -5
  166. package/dist/server.d.ts +3 -2
  167. package/dist/types/content.d.ts +2 -2
  168. package/package.json +2 -2
  169. package/dist/layout-engine/justifiedLineFit.d.ts +0 -7
  170. package/dist/layout-engine/justifiedLineFit.js +0 -6
@@ -1,7 +1,13 @@
1
1
  import { document_d_exports } from "../types/document.js";
2
2
  import { XmlElement } from "./xmlParser.js";
3
3
  import { PreviewLedger } from "./previewBudget.js";
4
+ import { GroupTextBoxFrame } from "./drawingGroupChildren.js";
4
5
  //#region src/docx/groupDrawingParser.d.ts
6
+ /**
7
+ * The text boxes a group preview does not draw, which the paragraph parser
8
+ * lifts out as text boxes of their own. Undefined for any other image.
9
+ */
10
+ declare const groupPreviewTextBoxes: (image: document_d_exports.Image) => readonly GroupTextBoxFrame[] | undefined;
5
11
  /**
6
12
  * Whether a `w:drawing` carries a WordprocessingGroup payload. A group this
7
13
  * module declines to rasterize has no editable projection either — the shape
@@ -11,4 +17,4 @@ declare const isGroupDrawing: (drawing: XmlElement) => boolean;
11
17
  /** Parse a WordprocessingGroup drawing into a safe SVG-backed image preview. */
12
18
  declare const parseGroupDrawing: (drawing: XmlElement, previews: PreviewLedger, rels?: document_d_exports.RelationshipMap, media?: Map<string, document_d_exports.MediaFile>) => document_d_exports.Image | null;
13
19
  //#endregion
14
- export { isGroupDrawing, parseGroupDrawing };
20
+ export { groupPreviewTextBoxes, isGroupDrawing, parseGroupDrawing };
@@ -1,4 +1,5 @@
1
1
  import { emuToPixels } from "../utils/units.js";
2
+ import { collectGroupTextBoxes, readTransform } from "./drawingGroupChildren.js";
2
3
  import { parseImage, resolveImageData } from "./imageParser.js";
3
4
  import { findAllDeep, findChildByLocalName, findChildrenByLocalName, getAttribute, getChildElements, getLocalName, getTextContent, parseNumericAttribute } from "./xmlParser.js";
4
5
  import { escapeXmlAttribute, escapeXmlText } from "@stll/docx-core";
@@ -9,6 +10,7 @@ const DEFAULT_FONT_HALF_POINTS = 22;
9
10
  const DEFAULT_LINE_WIDTH_EMU = 9525;
10
11
  const HALF_POINT_TO_EMU = 6350;
11
12
  const MAX_GROUP_SHAPES = 256;
13
+ const MAX_GROUP_DEPTH = 16;
12
14
  const CROP_SCALE = 1e5;
13
15
  const MAX_PATH_COMMANDS = 1e4;
14
16
  const MAX_TEXT_CHARACTERS = 2e4;
@@ -79,34 +81,66 @@ const pathData = (path) => {
79
81
  }
80
82
  return commands.join(" ");
81
83
  };
82
- const renderGeometry = (wsp) => {
84
+ /**
85
+ * A child's own `a:xfrm` rotation and flips, applied about its centre in the
86
+ * coordinate space it is drawn in (ECMA-376 §20.1.7.6).
87
+ */
88
+ const withChildTransform = (element, piece) => {
89
+ if (!piece) return piece;
90
+ const transform = readTransform(findChildByLocalName(findChildByLocalName(element, "spPr"), "xfrm"));
91
+ return wrapTransformed(transform, piece);
92
+ };
93
+ const wrapTransformed = (transform, piece) => {
94
+ if (transform.rotation === 0 && !transform.flipH && !transform.flipV) return piece;
95
+ const centerX = transform.x + transform.width / 2;
96
+ const centerY = transform.y + transform.height / 2;
97
+ const operations = [];
98
+ if (transform.rotation !== 0) operations.push(`rotate(${transform.rotation} ${centerX} ${centerY})`);
99
+ if (transform.flipH || transform.flipV) operations.push(`translate(${centerX} ${centerY}) scale(${transform.flipH ? -1 : 1} ${transform.flipV ? -1 : 1}) translate(${-centerX} ${-centerY})`);
100
+ return `<g transform="${operations.join(" ")}">${piece}</g>`;
101
+ };
102
+ /**
103
+ * `unitScale` is how many EMUs one unit of the child coordinate space spans.
104
+ * Line widths are authored in EMUs, so they are divided by it to be drawn in
105
+ * the space the geometry is in.
106
+ */
107
+ const renderGeometry = (wsp, unitScale) => {
83
108
  const { x, y, width, height } = childTransform(wsp);
84
- if (width <= 0 || height <= 0) return "";
85
109
  const spPr = findChildByLocalName(wsp, "spPr");
86
110
  const style = findChildByLocalName(wsp, "style");
87
111
  const styleStroke = colorFrom(findChildByLocalName(style, "lnRef"), "none");
88
112
  const fill = colorFrom(spPr, "none");
89
113
  const line = findChildByLocalName(spPr, "ln");
90
114
  const stroke = colorFrom(line, styleStroke);
91
- const strokeWidth = line ? parseNumericAttribute(line, null, "w") ?? DEFAULT_LINE_WIDTH_EMU : 0;
115
+ const strokeWidth = (line ? parseNumericAttribute(line, null, "w") ?? DEFAULT_LINE_WIDTH_EMU : 0) / unitScale;
116
+ const strokeColor = stroke === "none" ? "none" : `#${stroke}`;
117
+ if (width <= 0 || height <= 0) {
118
+ if (width < 0 || height < 0 || width === 0 && height === 0 || strokeColor === "none") return "";
119
+ return `<line x1="${x}" y1="${y}" x2="${x + width}" y2="${y + height}" stroke="${strokeColor}" stroke-width="${strokeWidth}"/>`;
120
+ }
92
121
  const paths = findAllDeep(findChildByLocalName(spPr, "custGeom"), "a", "path");
93
- if (paths.length === 0) return `<rect x="${x}" y="${y}" width="${width}" height="${height}" fill="${fill === "none" ? "none" : `#${fill}`}" stroke="${stroke === "none" ? "none" : `#${stroke}`}" stroke-width="${strokeWidth}"/>`;
122
+ if (paths.length === 0) return `<rect x="${x}" y="${y}" width="${width}" height="${height}" fill="${fill === "none" ? "none" : `#${fill}`}" stroke="${strokeColor}" stroke-width="${strokeWidth}"/>`;
94
123
  return paths.map((path) => {
95
124
  const viewWidth = numericAttr(path, "w") || width;
96
125
  const viewHeight = numericAttr(path, "h") || height;
97
126
  const d = pathData(path);
98
127
  if (!d) return "";
99
- return `<path d="${d}" transform="translate(${x} ${y}) scale(${width / viewWidth} ${height / viewHeight})" fill="${fill === "none" ? "none" : `#${fill}`}" stroke="${stroke === "none" ? "none" : `#${stroke}`}" stroke-width="${strokeWidth}"/>`;
128
+ return `<path d="${d}" transform="translate(${x} ${y}) scale(${width / viewWidth} ${height / viewHeight})" fill="${fill === "none" ? "none" : `#${fill}`}" stroke="${strokeColor}" stroke-width="${strokeWidth}"/>`;
100
129
  }).join("");
101
130
  };
102
- const renderTextBox = (wsp) => {
131
+ /**
132
+ * An approximate rendering of a text box the layout does not host (an inline
133
+ * group, or one placed by alignment rather than offset). Font sizes are in
134
+ * EMUs and the drawing is in child units, hence `unitScale`.
135
+ */
136
+ const renderTextBox = (wsp, unitScale) => {
103
137
  const { x, y, width, height } = childTransform(wsp);
104
138
  if (width <= 0 || height <= 0) return "";
105
139
  const paragraphs = findAllDeep(wsp, "w", "p");
106
140
  const firstSize = findAllDeep(wsp, "w", "sz").at(0);
107
141
  const halfPoints = numericAttr(firstSize ?? null, "val") || DEFAULT_FONT_HALF_POINTS;
108
142
  const color = colorFrom(findAllDeep(wsp, "w", "color").at(0) ?? null, DEFAULT_TEXT_COLOR);
109
- const fontSize = halfPoints * HALF_POINT_TO_EMU;
143
+ const fontSize = halfPoints * HALF_POINT_TO_EMU / unitScale;
110
144
  const maxCharacters = Math.max(1, Math.floor(width / (fontSize * .38)));
111
145
  const lines = paragraphs.flatMap((paragraph) => wrapLine(getTextContent(paragraph).slice(0, MAX_TEXT_CHARACTERS), maxCharacters)).map(escapeXmlText);
112
146
  if (lines.length === 0) return "";
@@ -137,38 +171,92 @@ const renderPicture = (picture, index, rels, media) => {
137
171
  return `<defs><clipPath id="${clipId}"><rect x="${x}" y="${y}" width="${width}" height="${height}"/></clipPath></defs><g clip-path="url(#${clipId})">${image}</g>`;
138
172
  };
139
173
  const groupViewBox = (group, width, height) => {
140
- const transform = findChildByLocalName(findChildByLocalName(group, "grpSpPr"), "xfrm");
141
- const childOffset = findChildByLocalName(transform, "chOff");
142
- const childExtent = findChildByLocalName(transform, "chExt");
143
- const childWidth = numericAttr(childExtent, "cx");
144
- const childHeight = numericAttr(childExtent, "cy");
174
+ const transform = readTransform(findChildByLocalName(findChildByLocalName(group, "grpSpPr"), "xfrm"));
145
175
  return {
146
- x: numericAttr(childOffset, "x"),
147
- y: numericAttr(childOffset, "y"),
148
- width: childWidth > 0 ? childWidth : width,
149
- height: childHeight > 0 ? childHeight : height
176
+ x: transform.childX,
177
+ y: transform.childY,
178
+ width: transform.childWidth > 0 ? transform.childWidth : width,
179
+ height: transform.childHeight > 0 ? transform.childHeight : height
150
180
  };
151
181
  };
152
- const createSvg = (group, width, height, rels, media) => {
153
- const children = getChildElements(group).slice(0, MAX_GROUP_SHAPES);
154
- let content = "";
155
- for (const [index, child] of children.entries()) {
156
- const localName = getLocalName(child.name ?? "");
157
- let piece;
158
- if (localName === "pic") piece = renderPicture(child, index, rels, media);
159
- else if (localName === "wsp") piece = findChildByLocalName(child, "txbx") ? renderTextBox(child) : renderGeometry(child);
160
- else continue;
161
- content += piece;
162
- if (content.length > MAX_SVG_CHARACTERS) return null;
163
- }
164
- if (!content) return null;
182
+ /**
183
+ * A nested group's child space drawn inside its frame in the parent's space,
184
+ * then turned by the group's own rotation and flips.
185
+ */
186
+ const nestedGroupTransform = (transform) => {
187
+ const scaleX = transform.childWidth > 0 ? transform.width / transform.childWidth : 1;
188
+ const scaleY = transform.childHeight > 0 ? transform.height / transform.childHeight : 1;
189
+ return `translate(${transform.x} ${transform.y}) scale(${scaleX} ${scaleY}) translate(${-transform.childX} ${-transform.childY})`;
190
+ };
191
+ const averageScale = (scale) => (scale.x + scale.y) / 2 || 1;
192
+ const createSvg = (group, width, height, rels, media, hostedTextBoxes) => {
193
+ let total = 0;
194
+ let shapes = 0;
195
+ let serial = 0;
196
+ /** One container's markup, or null once the budget is spent. */
197
+ const render = (container, scale, depth) => {
198
+ let content = "";
199
+ for (const child of getChildElements(container)) {
200
+ const index = serial;
201
+ serial += 1;
202
+ if (shapes >= MAX_GROUP_SHAPES) break;
203
+ const localName = getLocalName(child.name ?? "");
204
+ let piece;
205
+ if (localName === "pic") {
206
+ shapes += 1;
207
+ piece = withChildTransform(child, renderPicture(child, index, rels, media));
208
+ } else if (localName === "wsp") {
209
+ shapes += 1;
210
+ if (hostedTextBoxes.has(child)) continue;
211
+ const unitScale = averageScale(scale);
212
+ piece = withChildTransform(child, findChildByLocalName(child, "txbx") ? `${renderGeometry(child, unitScale)}${renderTextBox(child, unitScale)}` : renderGeometry(child, unitScale));
213
+ } else if (localName === "grpSp" && depth < MAX_GROUP_DEPTH) {
214
+ const transform = readTransform(findChildByLocalName(findChildByLocalName(child, "grpSpPr"), "xfrm"));
215
+ const scaleX = transform.childWidth > 0 ? transform.width / transform.childWidth : 1;
216
+ const scaleY = transform.childHeight > 0 ? transform.height / transform.childHeight : 1;
217
+ const nested = render(child, {
218
+ x: scale.x * scaleX,
219
+ y: scale.y * scaleY
220
+ }, depth + 1);
221
+ if (nested === null) return null;
222
+ piece = nested ? wrapTransformed(transform, `<g transform="${nestedGroupTransform(transform)}">${nested}</g>`) : "";
223
+ total -= nested.length;
224
+ } else continue;
225
+ content += piece;
226
+ total += piece.length;
227
+ if (total > MAX_SVG_CHARACTERS) return null;
228
+ }
229
+ return content;
230
+ };
165
231
  const viewBox = groupViewBox(group, width, height);
232
+ const content = render(group, {
233
+ x: width / viewBox.width,
234
+ y: height / viewBox.height
235
+ }, 0);
236
+ if (content === null || !content && hostedTextBoxes.size === 0) return null;
166
237
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}" width="${emuToPixels(width)}" height="${emuToPixels(height)}">${content}</svg>`;
167
238
  };
168
239
  const groupElement = (drawing) => {
169
240
  return findChildByLocalName(findAllDeep(drawing, "a", "graphicData").at(0) ?? null, "wgp");
170
241
  };
171
242
  /**
243
+ * Whether the layout can place the group's text boxes itself. A floating group
244
+ * positioned by offsets on both axes puts each child at the group's offset
245
+ * plus the child's frame, which a text box anchored beside the group can state
246
+ * exactly; an inline group or one placed by alignment has no such offset.
247
+ */
248
+ const hostsTextBoxes = (image) => {
249
+ const position = image.position;
250
+ return image.wrap.type !== "inline" && image.anchor?.useSimplePosition !== true && position !== void 0 && position.horizontal.alignment === void 0 && position.horizontal.posOffset !== void 0 && position.vertical.alignment === void 0 && position.vertical.posOffset !== void 0;
251
+ };
252
+ /** The text boxes each group preview leaves to the layout, keyed by the preview. */
253
+ const hostedTextBoxFrames = /* @__PURE__ */ new WeakMap();
254
+ /**
255
+ * The text boxes a group preview does not draw, which the paragraph parser
256
+ * lifts out as text boxes of their own. Undefined for any other image.
257
+ */
258
+ const groupPreviewTextBoxes = (image) => hostedTextBoxFrames.get(image);
259
+ /**
172
260
  * Whether a `w:drawing` carries a WordprocessingGroup payload. A group this
173
261
  * module declines to rasterize has no editable projection either — the shape
174
262
  * model holds one shape, not a group — so the caller must preserve it raw.
@@ -180,10 +268,13 @@ const parseGroupDrawing = (drawing, previews, rels, media) => {
180
268
  if (!group) return null;
181
269
  const image = parseImage(drawing, void 0, void 0);
182
270
  if (!image || image.size.width <= 0 || image.size.height <= 0) return null;
183
- const svg = createSvg(group, image.size.width, image.size.height, rels, media);
271
+ const hosted = hostsTextBoxes(image) ? collectGroupTextBoxes(group, image.size.width, image.size.height) : [];
272
+ const svg = createSvg(group, image.size.width, image.size.height, rels, media, new Set(hosted.map(({ wsp }) => wsp)));
184
273
  if (!svg || svg.length > MAX_SVG_CHARACTERS) return null;
185
274
  const { rId: _rId, src: _src, mimeType: _mimeType, filename: _filename, ...frame } = image;
186
- return previews.svgImage("wpGroup", svg, frame);
275
+ const preview = previews.svgImage("wpGroup", svg, frame);
276
+ if (hosted.length > 0) hostedTextBoxFrames.set(preview, hosted);
277
+ return preview;
187
278
  };
188
279
  //#endregion
189
- export { isGroupDrawing, parseGroupDrawing };
280
+ export { groupPreviewTextBoxes, isGroupDrawing, parseGroupDrawing };
@@ -19,6 +19,16 @@ type PackageReferenceRepair = {
19
19
  danglingRelationships: string[];
20
20
  /** Parts a content-type override named that the package does not hold. */
21
21
  danglingOverrides: string[];
22
+ /**
23
+ * `<part>|<id>` pairs: an `r:*`-style attribute in `part` named a
24
+ * relationship id that reconciliation just removed from `part`'s own
25
+ * `.rels` (because that relationship's target was itself a dangling
26
+ * reference). Dropping the relationship without also dropping the
27
+ * attribute that names it would trade one dangling reference for another —
28
+ * a `.rels` entry pointing nowhere becomes a content attribute pointing at
29
+ * nothing in its own `.rels`, which is no more valid.
30
+ */
31
+ orphanedIdReferences: string[];
22
32
  };
23
33
  /**
24
34
  * Make the package internally consistent before it is written: every internal
@@ -31,5 +41,39 @@ type PackageReferenceRepair = {
31
41
  * which is what the repack tests assert about their own output.
32
42
  */
33
43
  declare const reconcilePackageReferences: (zip: JSZip, compressionLevel: number) => Promise<PackageReferenceRepair>;
44
+ /**
45
+ * What {@link checkPackageIntegrity} found wrong with a package. Both fields
46
+ * are empty for a package where every internal relationship resolves and
47
+ * every reference to one does too — the state {@link reconcilePackageReferences}
48
+ * is meant to leave behind, and the state a save must never regress from.
49
+ */
50
+ type PackageIntegrityReport = {
51
+ /**
52
+ * `<part>|<id>` pairs: an `r:*`-style attribute in `part` names a
53
+ * relationship id absent from `part`'s own `.rels` — the id was never
54
+ * declared, or a repair (this module's or any other) dropped it without
55
+ * also dropping the attribute that names it.
56
+ */
57
+ unresolvedReferences: string[];
58
+ /**
59
+ * `<relsPart>|<target>` pairs: an internal (non-external) relationship in
60
+ * `relsPart` names a target the package does not hold.
61
+ */
62
+ danglingRelationshipTargets: string[];
63
+ };
64
+ /**
65
+ * Verify the two invariants an OPC package must hold for a conforming
66
+ * consumer to accept it: every `r:*` reference in an XML part resolves in
67
+ * that part's own `.rels`, and every internal relationship target names a
68
+ * part the package actually holds.
69
+ *
70
+ * A read-only counterpart to {@link reconcilePackageReferences} — that
71
+ * function repairs a package by removing what does not resolve; this one
72
+ * only reports what still does not, so a test can assert a save produced
73
+ * nothing for it to find. Cheap enough to run after every repack in a test
74
+ * or a corpus-gate invariant: one pass over the `.rels` parts building an id
75
+ * census, one pass over the XML parts checking references against it.
76
+ */
77
+ declare const checkPackageIntegrity: (zip: JSZip) => Promise<PackageIntegrityReport>;
34
78
  //#endregion
35
- export { PackageReferenceRepair, isUnsafePackagePath, reconcilePackageReferences, removeUnsafeEntries };
79
+ export { PackageIntegrityReport, PackageReferenceRepair, checkPackageIntegrity, isUnsafePackagePath, reconcilePackageReferences, removeUnsafeEntries };
@@ -3,9 +3,25 @@ import { resolveRelativePath } from "./relsParser.js";
3
3
  const CONTENT_TYPE_ELEMENT = /<(?:Default|Override)\b[^>]*?\/?>/giu;
4
4
  const PART_NAME_ATTRIBUTE = /\bPartName\s*=\s*(?<quote>["'])(?<value>[^"']*)\k<quote>/u;
5
5
  const RELATIONSHIP_ELEMENT = /<Relationship\b[^>]*?(?:\/>|>\s*<\/Relationship>)/giu;
6
+ const RELATIONSHIP_ID_ATTRIBUTE = /\bId\s*=\s*(?<quote>["'])(?<value>[^"']*)\k<quote>/u;
6
7
  const TARGET_ATTRIBUTE = /\bTarget\s*=\s*(?<quote>["'])(?<value>[^"']*)\k<quote>/u;
7
8
  const TARGET_MODE_ATTRIBUTE = /\bTargetMode\s*=\s*(?<quote>["'])(?<value>[^"']*)\k<quote>/u;
8
9
  const CONTENT_TYPES_PATTERN = /^\[Content_Types\]\.xml$/iu;
10
+ /** Escape a string for literal use inside a `RegExp`. */
11
+ const escapeRegExp = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&");
12
+ /**
13
+ * The XML part a `.rels` part describes: `word/_rels/header1.xml.rels` ->
14
+ * `word/header1.xml`; the package-level `_rels/.rels` has no single owning
15
+ * part (its relationships are resolved by type, never by `r:id`), so it maps
16
+ * to `undefined`.
17
+ */
18
+ const owningPartPath = (relsPath) => {
19
+ const slash = relsPath.lastIndexOf("/_rels/");
20
+ if (slash === -1) return;
21
+ const directory = relsPath.slice(0, slash);
22
+ const fileName = relsPath.slice(slash + 7).replace(/\.rels$/iu, "");
23
+ return fileName ? `${directory ? `${directory}/` : ""}${fileName}` : void 0;
24
+ };
9
25
  /** Normalize a package path for comparison: lower case, no leading slash. */
10
26
  const normalizePartPath = (path) => (path.startsWith("/") ? path.slice(1) : path).toLowerCase();
11
27
  /**
@@ -80,8 +96,10 @@ const reconcilePackageReferences = async (zip, compressionLevel) => {
80
96
  const present = presentPartPaths(zip);
81
97
  const repair = {
82
98
  danglingRelationships: [],
83
- danglingOverrides: []
99
+ danglingOverrides: [],
100
+ orphanedIdReferences: []
84
101
  };
102
+ const removedIdsByOwningPart = /* @__PURE__ */ new Map();
85
103
  for (const { path, xml } of await readRelationshipParts(zip)) {
86
104
  let dropped = false;
87
105
  const pruned = xml.replaceAll(RELATIONSHIP_ELEMENT, (element) => {
@@ -89,6 +107,13 @@ const reconcilePackageReferences = async (zip, compressionLevel) => {
89
107
  if (target === null || isPresent(present, target)) return element;
90
108
  repair.danglingRelationships.push(target);
91
109
  dropped = true;
110
+ const id = RELATIONSHIP_ID_ATTRIBUTE.exec(element)?.groups?.["value"];
111
+ const owningPart = owningPartPath(path);
112
+ if (id !== void 0 && owningPart !== void 0) {
113
+ const ids = removedIdsByOwningPart.get(owningPart) ?? /* @__PURE__ */ new Set();
114
+ ids.add(id);
115
+ removedIdsByOwningPart.set(owningPart, ids);
116
+ }
92
117
  return "";
93
118
  });
94
119
  if (dropped) writePart({
@@ -98,6 +123,28 @@ const reconcilePackageReferences = async (zip, compressionLevel) => {
98
123
  compressionLevel
99
124
  });
100
125
  }
126
+ for (const [partPath, ids] of removedIdsByOwningPart) {
127
+ const file = zip.file(partPath);
128
+ if (!file) continue;
129
+ const idPattern = [...ids].map((id) => escapeRegExp(id)).join("|");
130
+ const referencePattern = new RegExp(`\\s+r:[A-Za-z]+\\s*=\\s*(?<quote>["'])(?<id>${idPattern})\\k<quote>`, "gu");
131
+ const xml = await file.async("text");
132
+ const removedIds = /* @__PURE__ */ new Set();
133
+ const scrubbed = xml.replaceAll(referencePattern, (...args) => {
134
+ const groups = args.at(-1);
135
+ if (groups?.id !== void 0) removedIds.add(groups.id);
136
+ return "";
137
+ });
138
+ if (removedIds.size > 0) {
139
+ writePart({
140
+ zip,
141
+ path: partPath,
142
+ xml: scrubbed,
143
+ compressionLevel
144
+ });
145
+ for (const id of removedIds) repair.orphanedIdReferences.push(`${partPath}|${id}`);
146
+ }
147
+ }
101
148
  const contentTypes = zip.file(CONTENT_TYPES_PATTERN)[0];
102
149
  if (!contentTypes) return repair;
103
150
  let droppedOverride = false;
@@ -116,5 +163,49 @@ const reconcilePackageReferences = async (zip, compressionLevel) => {
116
163
  });
117
164
  return repair;
118
165
  };
166
+ /** Every `r:*`-style attribute value found anywhere in an XML part's text. */
167
+ const REFERENCE_ATTRIBUTE = /\br:[A-Za-z]+\s*=\s*(?<quote>["'])(?<id>[^"']*)\k<quote>/gu;
168
+ /**
169
+ * Verify the two invariants an OPC package must hold for a conforming
170
+ * consumer to accept it: every `r:*` reference in an XML part resolves in
171
+ * that part's own `.rels`, and every internal relationship target names a
172
+ * part the package actually holds.
173
+ *
174
+ * A read-only counterpart to {@link reconcilePackageReferences} — that
175
+ * function repairs a package by removing what does not resolve; this one
176
+ * only reports what still does not, so a test can assert a save produced
177
+ * nothing for it to find. Cheap enough to run after every repack in a test
178
+ * or a corpus-gate invariant: one pass over the `.rels` parts building an id
179
+ * census, one pass over the XML parts checking references against it.
180
+ */
181
+ const checkPackageIntegrity = async (zip) => {
182
+ const present = presentPartPaths(zip);
183
+ const report = {
184
+ unresolvedReferences: [],
185
+ danglingRelationshipTargets: []
186
+ };
187
+ const idsByOwningPart = /* @__PURE__ */ new Map();
188
+ for (const { path, xml } of await readRelationshipParts(zip)) {
189
+ const owningPart = owningPartPath(path);
190
+ const ids = /* @__PURE__ */ new Set();
191
+ for (const element of xml.match(RELATIONSHIP_ELEMENT) ?? []) {
192
+ const id = RELATIONSHIP_ID_ATTRIBUTE.exec(element)?.groups?.["value"];
193
+ if (id !== void 0) ids.add(id);
194
+ const target = relationshipTarget(element, path);
195
+ if (target !== null && !isPresent(present, target)) report.danglingRelationshipTargets.push(`${path}|${target}`);
196
+ }
197
+ if (owningPart !== void 0) idsByOwningPart.set(owningPart, ids);
198
+ }
199
+ for (const [path, file] of Object.entries(zip.files)) {
200
+ if (file.dir || !normalizePartPath(path).endsWith(".xml")) continue;
201
+ const xml = await file.async("text");
202
+ const ids = idsByOwningPart.get(path);
203
+ for (const match of xml.matchAll(REFERENCE_ATTRIBUTE)) {
204
+ const id = match.groups?.["id"];
205
+ if (id !== void 0 && !ids?.has(id)) report.unresolvedReferences.push(`${path}|${id}`);
206
+ }
207
+ }
208
+ return report;
209
+ };
119
210
  //#endregion
120
- export { isUnsafePackagePath, reconcilePackageReferences, removeUnsafeEntries };
211
+ export { checkPackageIntegrity, isUnsafePackagePath, reconcilePackageReferences, removeUnsafeEntries };
@@ -85,8 +85,9 @@ declare const alignParagraphOrdinals: (base: readonly ParagraphIdentity[], revis
85
85
  * describe one document — the source part and the model parsed from it — so
86
86
  * {@link alignParagraphOrdinals}'s `consistent` is the whole of the question: a
87
87
  * package with ids on some paragraphs proves its own alignment through them,
88
- * and one with none is aligned vacuously behind the paragraph-count check the
89
- * caller already runs.
88
+ * and one with none is aligned exactly when the two sequences have the same
89
+ * length. The save asks this per story (main flow, text boxes), so a text box
90
+ * the model re-reads cannot misalign the main flow.
90
91
  */
91
92
  type ParagraphIdentityPlan = {
92
93
  identities: readonly ParagraphIdentity[];
@@ -676,6 +676,81 @@ const FIELD_INLINE_WRAPPER_HANDLERS = {
676
676
  pPr: CAPTURE
677
677
  };
678
678
  /**
679
+ * The container children a complex field's result may hold and still be
680
+ * assembled into a `ComplexField`: its runs and the zero-width markers read
681
+ * beside them. Any other child is inline content (a hyperlink, a simple field,
682
+ * a content control, a revision, a wrapper, an equation) that `fieldResult`
683
+ * has no place for.
684
+ */
685
+ const FIELD_TRANSPARENT_CHILDREN = /* @__PURE__ */ new Set([
686
+ "r",
687
+ "pPr",
688
+ "smartTagPr",
689
+ "customXmlPr",
690
+ "bookmarkStart",
691
+ "bookmarkEnd",
692
+ "commentRangeStart",
693
+ "commentRangeEnd",
694
+ "moveFromRangeStart",
695
+ "moveFromRangeEnd",
696
+ "moveToRangeStart",
697
+ "moveToRangeEnd",
698
+ "proofErr",
699
+ "permStart",
700
+ "permEnd",
701
+ "customXmlDelRangeStart",
702
+ "customXmlDelRangeEnd",
703
+ "customXmlInsRangeStart",
704
+ "customXmlInsRangeEnd",
705
+ "customXmlMoveFromRangeStart",
706
+ "customXmlMoveFromRangeEnd",
707
+ "customXmlMoveToRangeStart",
708
+ "customXmlMoveToRangeEnd"
709
+ ]);
710
+ const isWordprocessingChild = (child) => {
711
+ const namespace = getNamespaceUri(child);
712
+ return namespace === void 0 || WORDPROCESSINGML_NAMESPACE_URIS.has(namespace);
713
+ };
714
+ /**
715
+ * The `w:r` children whose `begin` opens a complex field that this container's
716
+ * walk closes.
717
+ *
718
+ * A field is assembled from the runs between its `begin` and its `end`, so it
719
+ * exists only when both are runs of this container with nothing between them
720
+ * that a `ComplexField` cannot hold. A TOC opened here and closed in a later
721
+ * paragraph, a field whose result holds a `w:hyperlink` (a TOC's `\h` entries),
722
+ * and an outer field whose result holds a nested field are none of these; their
723
+ * runs are ordinary content, read in source order alongside the siblings
724
+ * between them. Mirrors the `w:r` handler run by run: a run with a `begin`
725
+ * opens a field, and a run with an `end` closes the one open.
726
+ */
727
+ const assembledFieldBeginsOf = (container) => {
728
+ const assembled = /* @__PURE__ */ new Set();
729
+ let open;
730
+ for (const child of getChildElements(container)) {
731
+ const wordprocessing = isWordprocessingChild(child);
732
+ const localName = getLocalName(child.name);
733
+ if (!wordprocessing || localName !== "r") {
734
+ if (!wordprocessing || !FIELD_TRANSPARENT_CHILDREN.has(localName)) open = void 0;
735
+ continue;
736
+ }
737
+ let hasBegin = false;
738
+ let hasEnd = false;
739
+ for (const runChild of getChildElements(child)) {
740
+ if (getLocalName(runChild.name) !== "fldChar" || !isWordprocessingChild(runChild)) continue;
741
+ const charType = getAttribute(runChild, "w", "fldCharType");
742
+ if (charType === "end") hasEnd = true;
743
+ else if (charType !== "separate") hasBegin = true;
744
+ }
745
+ if (hasBegin) open = child;
746
+ if (hasEnd && open !== void 0) {
747
+ assembled.add(open);
748
+ open = void 0;
749
+ }
750
+ }
751
+ return assembled;
752
+ };
753
+ /**
679
754
  * A transparent wrapper at paragraph level. All four hold ordinary inline
680
755
  * content and say something about it rather than about the text, so the
681
756
  * recursion is this same walk and `inlineWrapperOf` reads what the element
@@ -714,8 +789,12 @@ const PARAGRAPH_CONTENT_HANDLERS = {
714
789
  }
715
790
  else if (content.type === "instrText") instrText += content.text;
716
791
  if (hasFieldBegin) {
717
- if (scan.inComplexField && scan.afterSeparator) {
718
- contents.push(...scan.complexFieldResultRuns);
792
+ scan.assembledFieldBegins ??= assembledFieldBeginsOf(scan.container);
793
+ if (!scan.assembledFieldBegins.has(child)) hasFieldBegin = false;
794
+ }
795
+ if (hasFieldBegin) {
796
+ if (scan.inComplexField) {
797
+ contents.push(...scan.complexFieldOpenRuns);
719
798
  scan.inComplexField = false;
720
799
  }
721
800
  scan.inComplexField = true;
@@ -751,7 +830,11 @@ const PARAGRAPH_CONTENT_HANDLERS = {
751
830
  }
752
831
  if (hasFieldEnd) {
753
832
  let resultRuns = scan.complexFieldResultRuns;
754
- if (resultRuns.length === 0 && scan.complexFieldFallbackDisplay !== void 0 && isLegacyFormCheckboxInstruction(scan.complexFieldInstr)) resultRuns = [createLegacyFormCheckboxResultRun(scan.complexFieldFallbackDisplay, scan.complexFieldFormatting)];
833
+ let resultIsFallback = false;
834
+ if (resultRuns.length === 0 && scan.complexFieldFallbackDisplay !== void 0 && isLegacyFormCheckboxInstruction(scan.complexFieldInstr)) {
835
+ resultRuns = [createLegacyFormCheckboxResultRun(scan.complexFieldFallbackDisplay, scan.complexFieldFormatting)];
836
+ resultIsFallback = true;
837
+ }
755
838
  if (resultRuns.length === 0 && !scan.afterSeparator && endOriginalValue !== void 0) resultRuns = [{
756
839
  type: "run",
757
840
  content: [{
@@ -767,6 +850,7 @@ const PARAGRAPH_CONTENT_HANDLERS = {
767
850
  fieldResult: resultRuns,
768
851
  ...scan.complexFieldState
769
852
  };
853
+ if (resultIsFallback) complexField.fieldResultIsFallback = true;
770
854
  if (scan.complexFieldFormatting) complexField.formatting = scan.complexFieldFormatting;
771
855
  contents.push(complexField);
772
856
  if (commentReferenceId !== null) contents.push({
@@ -922,7 +1006,9 @@ function parseParagraphContents(paraElement, styles, theme, _numbering, rels, me
922
1006
  afterSeparator: false,
923
1007
  complexFieldState: {},
924
1008
  complexFieldFallbackDisplay: void 0,
925
- complexFieldFormatting: void 0
1009
+ complexFieldFormatting: void 0,
1010
+ container: paraElement,
1011
+ assembledFieldBegins: void 0
926
1012
  };
927
1013
  const preserved = dispatchChildrenWithContext({
928
1014
  element: paraElement,
@@ -1096,9 +1182,7 @@ function parseParagraph(node, styles, theme, numbering, rels = null, media = nul
1096
1182
  if (!paragraph.formatting) paragraph.formatting = {};
1097
1183
  const directInd = pPr ? findChild(pPr, "w", "ind") : null;
1098
1184
  const hasDirectLeft = hasAttributeAnySpelling(directInd, "CT_Ind @left");
1099
- const directFirstLine = directInd ? parseNumericAttribute(directInd, "w", "firstLine") : void 0;
1100
- const directHanging = directInd ? parseNumericAttribute(directInd, "w", "hanging") : void 0;
1101
- const hasDirectFirstLineOrHanging = directFirstLine !== void 0 && directFirstLine !== 0 || directHanging !== void 0 && directHanging !== 0;
1185
+ const hasDirectFirstLineOrHanging = parseNumericAttribute(directInd, "w", "firstLine") !== void 0 || parseNumericAttribute(directInd, "w", "hanging") !== void 0;
1102
1186
  if (!hasDirectLeft && !chainInd.left && level.pPr.indentLeft !== void 0) paragraph.formatting.indentLeft = level.pPr.indentLeft;
1103
1187
  if (!hasDirectFirstLineOrHanging && !chainInd.firstLine && numberingLevelHasMarkerSlot(level)) {
1104
1188
  if (level.pPr.indentFirstLine !== void 0) paragraph.formatting.indentFirstLine = level.pPr.indentFirstLine;