@stll/folio-core 0.6.1 → 0.8.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 (107) hide show
  1. package/dist/ai-edits/blockRange.d.ts +13 -2
  2. package/dist/ai-edits/blockRange.js +23 -2
  3. package/dist/ai-edits/index.d.ts +4 -2
  4. package/dist/ai-edits/index.js +3 -1
  5. package/dist/ai-edits/scoped-reading.d.ts +9 -0
  6. package/dist/ai-edits/scoped-reading.js +60 -0
  7. package/dist/ai-edits/snapshot.js +13 -4
  8. package/dist/ai-edits/types.d.ts +44 -2
  9. package/dist/compat/eigenpal.d.ts +5 -2
  10. package/dist/compat/eigenpal.js +4 -1
  11. package/dist/controller/fontReadiness.d.ts +29 -0
  12. package/dist/controller/fontReadiness.js +139 -0
  13. package/dist/controller/layoutPipeline.js +86 -11
  14. package/dist/docx/blockContentParser.js +51 -8
  15. package/dist/docx/capabilities.d.ts +41 -0
  16. package/dist/docx/capabilities.js +322 -0
  17. package/dist/docx/compatibility.d.ts +34 -2
  18. package/dist/docx/compatibility.js +128 -38
  19. package/dist/docx/conformance.d.ts +6 -0
  20. package/dist/docx/conformance.js +18 -0
  21. package/dist/docx/ensureParaIds.d.ts +29 -0
  22. package/dist/docx/ensureParaIds.js +423 -0
  23. package/dist/docx/groupDrawingParser.d.ts +1 -1
  24. package/dist/docx/groupDrawingParser.js +49 -8
  25. package/dist/docx/numberingParser.js +1 -0
  26. package/dist/docx/paragraphParser.js +9 -0
  27. package/dist/docx/paragraphTextBoxEnrichment.js +141 -2
  28. package/dist/docx/parser.js +2 -0
  29. package/dist/docx/runParser.js +11 -2
  30. package/dist/docx/selectiveXmlPatch.d.ts +10 -1
  31. package/dist/docx/selectiveXmlPatch.js +1 -1
  32. package/dist/docx/serializer/paragraphSerializer.js +4 -0
  33. package/dist/docx/serializer/runSerializer.js +7 -2
  34. package/dist/docx/serializer/tableSerializer.js +4 -0
  35. package/dist/docx/server/boundedArchive.d.ts +24 -0
  36. package/dist/docx/server/boundedArchive.js +106 -0
  37. package/dist/docx/server/extractDocxText.d.ts +23 -0
  38. package/dist/docx/server/extractDocxText.js +154 -0
  39. package/dist/docx/tableParser.js +8 -0
  40. package/dist/docx/textBoxParser.js +6 -1
  41. package/dist/docx/vmlImageParser.js +145 -1
  42. package/dist/index.d.ts +5 -2
  43. package/dist/index.js +4 -1
  44. package/dist/layout-bridge/convert/headerFooterLayout.js +16 -2
  45. package/dist/layout-bridge/convert/paragraphFrames.d.ts +7 -0
  46. package/dist/layout-bridge/convert/paragraphFrames.js +136 -0
  47. package/dist/layout-bridge/convert/toFlowBlocks.js +106 -13
  48. package/dist/layout-bridge/engine/hitTest.js +2 -1
  49. package/dist/layout-bridge/engine/selectionRects.js +2 -1
  50. package/dist/layout-engine/headerFooterRefs.d.ts +7 -0
  51. package/dist/layout-engine/headerFooterRefs.js +40 -0
  52. package/dist/layout-engine/index.d.ts +3 -2
  53. package/dist/layout-engine/index.js +125 -72
  54. package/dist/layout-engine/keep-together.d.ts +7 -5
  55. package/dist/layout-engine/keep-together.js +20 -4
  56. package/dist/layout-engine/measure/cache.js +2 -0
  57. package/dist/layout-engine/measure/listMarkerWidth.d.ts +3 -1
  58. package/dist/layout-engine/measure/listMarkerWidth.js +23 -4
  59. package/dist/layout-engine/measure/measureBlocks.d.ts +3 -0
  60. package/dist/layout-engine/measure/measureBlocks.js +133 -51
  61. package/dist/layout-engine/measure/measureParagraph.js +42 -15
  62. package/dist/layout-engine/measure/tableCellGrid.d.ts +15 -0
  63. package/dist/layout-engine/measure/tableCellGrid.js +62 -0
  64. package/dist/layout-engine/measure/textBoxParagraphLayout.d.ts +18 -0
  65. package/dist/layout-engine/measure/textBoxParagraphLayout.js +32 -0
  66. package/dist/layout-engine/paginator.d.ts +2 -1
  67. package/dist/layout-engine/paginator.js +17 -8
  68. package/dist/layout-engine/paragraphFrame.d.ts +22 -0
  69. package/dist/layout-engine/paragraphFrame.js +17 -0
  70. package/dist/layout-engine/paragraphSequence.d.ts +7 -0
  71. package/dist/layout-engine/paragraphSequence.js +25 -0
  72. package/dist/layout-engine/paragraphSpacing.d.ts +17 -0
  73. package/dist/layout-engine/paragraphSpacing.js +30 -0
  74. package/dist/layout-engine/renderedBreakReconciliation.d.ts +58 -0
  75. package/dist/layout-engine/renderedBreakReconciliation.js +63 -0
  76. package/dist/layout-engine/types.d.ts +31 -10
  77. package/dist/layout-engine/types.js +15 -2
  78. package/dist/layout-painter/renderPage.js +2 -2
  79. package/dist/layout-painter/renderParagraph.js +94 -10
  80. package/dist/layout-painter/renderTable.js +42 -12
  81. package/dist/layout-painter/renderTextBox.js +17 -7
  82. package/dist/managers/DocumentLoaderManager.js +1 -1
  83. package/dist/paged-layout/sectionBlockWidths.d.ts +3 -0
  84. package/dist/paged-layout/sectionBlockWidths.js +9 -0
  85. package/dist/paged-layout/sectionGeometry.js +1 -1
  86. package/dist/prosemirror/attrs/index.js +11 -0
  87. package/dist/prosemirror/conversion/fromProseDoc.js +2 -0
  88. package/dist/prosemirror/conversion/toProseDoc.js +29 -23
  89. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
  90. package/dist/prosemirror/extensions/features/ListExtension.js +1 -0
  91. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +68 -16
  92. package/dist/prosemirror/extensions/nodes/TableExtension.js +1 -0
  93. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +2 -1
  94. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +7 -0
  95. package/dist/prosemirror/schema/nodes.d.ts +6 -3
  96. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  97. package/dist/prosemirror/utils/tabCalculator.js +1 -1
  98. package/dist/server.d.ts +6 -2
  99. package/dist/server.js +5 -1
  100. package/dist/types/index.d.ts +2 -1
  101. package/dist/utils/createDocument.js +26 -18
  102. package/dist/utils/formatToStyle.js +3 -3
  103. package/dist/utils/hexId.d.ts +8 -1
  104. package/dist/utils/hexId.js +11 -3
  105. package/dist/utils/units.d.ts +6 -6
  106. package/dist/utils/units.js +8 -8
  107. package/package.json +1 -1
@@ -3,6 +3,6 @@ import { XmlElement } from "./xmlParser.js";
3
3
 
4
4
  //#region src/docx/groupDrawingParser.d.ts
5
5
  /** Parse a WordprocessingGroup drawing into a safe SVG-backed image preview. */
6
- declare const parseGroupDrawing: (drawing: XmlElement) => document_d_exports.Image | null;
6
+ declare const parseGroupDrawing: (drawing: XmlElement, rels?: document_d_exports.RelationshipMap, media?: Map<string, document_d_exports.MediaFile>) => document_d_exports.Image | null;
7
7
  //#endregion
8
8
  export { parseGroupDrawing };
@@ -1,6 +1,6 @@
1
- import { findAllDeep, findChildByLocalName, findChildrenByLocalName, getAttribute, getLocalName, getTextContent, parseNumericAttribute } from "./xmlParser.js";
1
+ import { findAllDeep, findChildByLocalName, findChildrenByLocalName, getAttribute, getChildElements, getLocalName, getTextContent, parseNumericAttribute } from "./xmlParser.js";
2
2
  import { emuToPixels } from "../utils/units.js";
3
- import { parseImage } from "./imageParser.js";
3
+ import { parseImage, resolveImageData } from "./imageParser.js";
4
4
  //#region src/docx/groupDrawingParser.ts
5
5
  const HEX_COLOR = /^[0-9A-Fa-f]{6}$/u;
6
6
  const DEFAULT_TEXT_COLOR = "000000";
@@ -8,6 +8,7 @@ const DEFAULT_FONT_HALF_POINTS = 22;
8
8
  const DEFAULT_LINE_WIDTH_EMU = 9525;
9
9
  const HALF_POINT_TO_EMU = 6350;
10
10
  const MAX_GROUP_SHAPES = 256;
11
+ const CROP_SCALE = 1e5;
11
12
  const MAX_PATH_COMMANDS = 1e4;
12
13
  const MAX_TEXT_CHARACTERS = 2e4;
13
14
  const MAX_SVG_CHARACTERS = 1e6;
@@ -110,18 +111,58 @@ const renderTextBox = (wsp) => {
110
111
  const lineStep = lineHeight / fontSize * svgFontSize;
111
112
  return `<text x="0" y="${svgFontSize}" transform="translate(${x} ${y}) scale(${scale})" font-family="Arial, sans-serif" font-size="${svgFontSize}" fill="#${color}">${lines.map((line, index) => `<tspan x="0" dy="${index === 0 ? 0 : lineStep}">${line}</tspan>`).join("")}</text>`;
112
113
  };
113
- const createSvg = (group, width, height) => {
114
- const content = findChildrenByLocalName(group, "wsp").slice(0, MAX_GROUP_SHAPES).map((wsp) => findChildByLocalName(wsp, "txbx") ? renderTextBox(wsp) : renderGeometry(wsp)).join("");
115
- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${emuToPixels(width)}" height="${emuToPixels(height)}">${content}</svg>`;
114
+ const renderPicture = (picture, index, rels, media) => {
115
+ const { x, y, width, height } = childTransform(picture);
116
+ if (width <= 0 || height <= 0) return "";
117
+ const blipFill = findChildByLocalName(picture, "blipFill");
118
+ const blip = findChildByLocalName(blipFill, "blip");
119
+ const { src } = resolveImageData(getAttribute(blip, "r", "embed") ?? getAttribute(blip, "r", "link") ?? "", rels, media);
120
+ if (!src) return "";
121
+ const sourceRect = findChildByLocalName(blipFill, "srcRect");
122
+ const left = Math.max(0, numericAttr(sourceRect, "l")) / CROP_SCALE;
123
+ const top = Math.max(0, numericAttr(sourceRect, "t")) / CROP_SCALE;
124
+ const right = Math.max(0, numericAttr(sourceRect, "r")) / CROP_SCALE;
125
+ const bottom = Math.max(0, numericAttr(sourceRect, "b")) / CROP_SCALE;
126
+ const visibleWidth = 1 - left - right;
127
+ const visibleHeight = 1 - top - bottom;
128
+ if (visibleWidth <= 0 || visibleHeight <= 0) return "";
129
+ const image = `<image x="${x - width * left / visibleWidth}" y="${y - height * top / visibleHeight}" width="${width / visibleWidth}" height="${height / visibleHeight}" href="${escapeXml(src)}" preserveAspectRatio="none"/>`;
130
+ if (left === 0 && top === 0 && right === 0 && bottom === 0) return image;
131
+ const clipId = `group-picture-${index}`;
132
+ return `<defs><clipPath id="${clipId}"><rect x="${x}" y="${y}" width="${width}" height="${height}"/></clipPath></defs><g clip-path="url(#${clipId})">${image}</g>`;
133
+ };
134
+ const groupViewBox = (group, width, height) => {
135
+ const transform = findChildByLocalName(findChildByLocalName(group, "grpSpPr"), "xfrm");
136
+ const childOffset = findChildByLocalName(transform, "chOff");
137
+ const childExtent = findChildByLocalName(transform, "chExt");
138
+ const childWidth = numericAttr(childExtent, "cx");
139
+ const childHeight = numericAttr(childExtent, "cy");
140
+ return {
141
+ x: numericAttr(childOffset, "x"),
142
+ y: numericAttr(childOffset, "y"),
143
+ width: childWidth > 0 ? childWidth : width,
144
+ height: childHeight > 0 ? childHeight : height
145
+ };
146
+ };
147
+ const createSvg = (group, width, height, rels, media) => {
148
+ const content = getChildElements(group).slice(0, MAX_GROUP_SHAPES).map((child, index) => {
149
+ const localName = getLocalName(child.name ?? "");
150
+ if (localName === "pic") return renderPicture(child, index, rels, media);
151
+ if (localName !== "wsp") return "";
152
+ return findChildByLocalName(child, "txbx") ? renderTextBox(child) : renderGeometry(child);
153
+ }).join("");
154
+ if (!content) return null;
155
+ const viewBox = groupViewBox(group, width, height);
156
+ 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>`;
116
157
  };
117
158
  /** Parse a WordprocessingGroup drawing into a safe SVG-backed image preview. */
118
- const parseGroupDrawing = (drawing) => {
159
+ const parseGroupDrawing = (drawing, rels, media) => {
119
160
  const group = findChildByLocalName(findAllDeep(drawing, "a", "graphicData").at(0) ?? null, "wgp");
120
161
  if (!group) return null;
121
162
  const image = parseImage(drawing, void 0, void 0);
122
163
  if (!image || image.size.width <= 0 || image.size.height <= 0) return null;
123
- const svg = createSvg(group, image.size.width, image.size.height);
124
- if (svg.length > MAX_SVG_CHARACTERS) return null;
164
+ const svg = createSvg(group, image.size.width, image.size.height, rels, media);
165
+ if (!svg || svg.length > MAX_SVG_CHARACTERS) return null;
125
166
  image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
126
167
  image.mimeType = "image/svg+xml";
127
168
  image.filename = "wordprocessing-group.svg";
@@ -604,6 +604,7 @@ function computeListRendering(numPr, numbering) {
604
604
  if (markerFont) rendering.markerFontFamily = markerFont;
605
605
  if (level.rPr?.fontSize) rendering.markerFontSize = level.rPr.fontSize / 2;
606
606
  if (level.rPr?.allCaps) rendering.markerAllCaps = true;
607
+ if (level.lvlJc) rendering.markerAlignment = level.lvlJc;
607
608
  if (level.suffix) rendering.markerSuffix = level.suffix;
608
609
  if (instance?.abstractNumId !== void 0) rendering.abstractNumId = instance.abstractNumId;
609
610
  if (overrideForLevel?.startOverride !== void 0) rendering.startOverride = overrideForLevel.startOverride;
@@ -110,10 +110,18 @@ function parseTabStops(tabs) {
110
110
  function parseFrameProperties(framePr) {
111
111
  if (!framePr) return;
112
112
  const frame = {};
113
+ const dropCap = getAttribute(framePr, "w", "dropCap");
114
+ if (dropCap === "none" || dropCap === "drop" || dropCap === "margin") frame.dropCap = dropCap;
115
+ const lines = parseNumericAttribute(framePr, "w", "lines");
116
+ if (lines !== void 0) frame.lines = lines;
113
117
  const w = parseNumericAttribute(framePr, "w", "w");
114
118
  if (w !== void 0) frame.width = w;
115
119
  const h = parseNumericAttribute(framePr, "w", "h");
116
120
  if (h !== void 0) frame.height = h;
121
+ const hSpace = parseNumericAttribute(framePr, "w", "hSpace");
122
+ if (hSpace !== void 0) frame.hSpace = hSpace;
123
+ const vSpace = parseNumericAttribute(framePr, "w", "vSpace");
124
+ if (vSpace !== void 0) frame.vSpace = vSpace;
117
125
  const hAnchor = getAttribute(framePr, "w", "hAnchor");
118
126
  if (hAnchor === "text" || hAnchor === "margin" || hAnchor === "page") frame.hAnchor = hAnchor;
119
127
  const vAnchor = getAttribute(framePr, "w", "vAnchor");
@@ -974,6 +982,7 @@ function parseParagraph(node, styles, theme, numbering, rels = null, media = nul
974
982
  if (markerFont) listRendering.markerFontFamily = markerFont;
975
983
  if (level.rPr?.fontSize) listRendering.markerFontSize = level.rPr.fontSize / 2;
976
984
  if (level.rPr?.allCaps) listRendering.markerAllCaps = true;
985
+ if (level.lvlJc) listRendering.markerAlignment = level.lvlJc;
977
986
  if (level.suffix) listRendering.markerSuffix = level.suffix;
978
987
  let implicitChildLevelAdvances = 0;
979
988
  const foldedMarkerSuffix = [];
@@ -1,7 +1,77 @@
1
- import { findDeep, getChildElements, getLocalName } from "./xmlParser.js";
1
+ import { findDeep, getAttribute, getChildElements, getLocalName } from "./xmlParser.js";
2
+ import { pixelsToEmu } from "../utils/units.js";
2
3
  import { getTextBoxContentElement, isTextBoxDrawing, parseTextBox, parseTextBoxContent } from "./textBoxParser.js";
3
4
  import { parseParagraph } from "./paragraphParser.js";
4
5
  //#region src/docx/paragraphTextBoxEnrichment.ts
6
+ const VML_HORIZONTAL_RELATIVES = /* @__PURE__ */ new Set([
7
+ "character",
8
+ "column",
9
+ "insideMargin",
10
+ "leftMargin",
11
+ "margin",
12
+ "outsideMargin",
13
+ "page",
14
+ "rightMargin"
15
+ ]);
16
+ const VML_VERTICAL_RELATIVES = /* @__PURE__ */ new Set([
17
+ "insideMargin",
18
+ "line",
19
+ "margin",
20
+ "outsideMargin",
21
+ "page",
22
+ "paragraph",
23
+ "topMargin",
24
+ "bottomMargin"
25
+ ]);
26
+ const parseVmlStyle = (value) => {
27
+ const declarations = {};
28
+ for (const declaration of value?.split(";") ?? []) {
29
+ const separator = declaration.indexOf(":");
30
+ if (separator < 0) continue;
31
+ const key = declaration.slice(0, separator).trim().toLowerCase();
32
+ if (key) declarations[key] = declaration.slice(separator + 1).trim();
33
+ }
34
+ return declarations;
35
+ };
36
+ const vmlLengthToPixels = (value) => {
37
+ const match = /^(?<amount>-?(?:\d+(?:\.\d+)?|\.\d+))\s*(?<unit>pt|in|px|cm|mm|pc)?$/iu.exec(value?.trim() ?? "");
38
+ const amount = Number.parseFloat(match?.groups?.["amount"] ?? "");
39
+ if (!Number.isFinite(amount)) return;
40
+ switch (match?.groups?.["unit"]?.toLowerCase()) {
41
+ case "pt": return amount / 72 * 96;
42
+ case "in": return amount * 96;
43
+ case "cm": return amount / 2.54 * 96;
44
+ case "mm": return amount / 25.4 * 96;
45
+ case "pc": return amount * 16;
46
+ case "px":
47
+ case void 0: return amount;
48
+ default: return;
49
+ }
50
+ };
51
+ const horizontalRelativeTo = (value) => {
52
+ for (const relative of VML_HORIZONTAL_RELATIVES) if (relative.toLowerCase() === value?.toLowerCase()) return relative;
53
+ return "character";
54
+ };
55
+ const verticalRelativeTo = (value) => {
56
+ for (const relative of VML_VERTICAL_RELATIVES) if (relative.toLowerCase() === value?.toLowerCase()) return relative;
57
+ return "paragraph";
58
+ };
59
+ const parseVmlInsets = (textBoxEl) => {
60
+ const [left, top, right, bottom, extra] = getAttribute(textBoxEl, null, "inset")?.split(",").map((value) => vmlLengthToPixels(value)) ?? [];
61
+ if (extra !== void 0 || left === void 0 || top === void 0 || right === void 0 || bottom === void 0) return;
62
+ return {
63
+ left: pixelsToEmu(left),
64
+ top: pixelsToEmu(top),
65
+ right: pixelsToEmu(right),
66
+ bottom: pixelsToEmu(bottom)
67
+ };
68
+ };
69
+ const parseVmlFill = (shapeEl) => getAttribute(shapeEl, null, "filled")?.toLowerCase() === "f" ? { type: "none" } : void 0;
70
+ const vmlWrapType = (positioned, zIndex) => {
71
+ if (!positioned) return "inline";
72
+ if (Number.isFinite(zIndex) && zIndex < 0) return "behind";
73
+ return "inFront";
74
+ };
5
75
  const enrichParagraphTextBoxes = (paragraph, paraXml, styles, theme, numbering, rels, media) => {
6
76
  const xmlChildren = getChildElements(paraXml);
7
77
  let parsedIndex = 0;
@@ -11,7 +81,7 @@ const enrichParagraphTextBoxes = (paragraph, paraXml, styles, theme, numbering,
11
81
  if (parsedIndex < paragraph.content.length && paragraph.content[parsedIndex]?.type !== "run") parsedIndex += 1;
12
82
  continue;
13
83
  }
14
- const { textBoxDrawings, hasNonTextBoxContent } = scanRunForTextBoxDrawings(xmlChild);
84
+ const { textBoxDrawings, vmlTextBoxes, hasNonTextBoxContent } = scanRunForTextBoxDrawings(xmlChild);
15
85
  const parsedContent = paragraph.content[parsedIndex];
16
86
  const parsedRun = parsedContent?.type === "run" ? parsedContent : void 0;
17
87
  const targetRun = parsedRun ?? (hasNonTextBoxContent ? lastConsumedRun : void 0);
@@ -33,6 +103,7 @@ const enrichParagraphTextBoxes = (paragraph, paraXml, styles, theme, numbering,
33
103
  ...textBox.outline !== void 0 ? { outline: textBox.outline } : {},
34
104
  textBody: {
35
105
  content: textBox.content,
106
+ ...textBox.autoFit !== void 0 ? { autoFit: textBox.autoFit } : {},
36
107
  ...textBox.margins !== void 0 ? { margins: textBox.margins } : {}
37
108
  }
38
109
  };
@@ -52,6 +123,24 @@ const enrichParagraphTextBoxes = (paragraph, paraXml, styles, theme, numbering,
52
123
  parsedIndex += 1;
53
124
  }
54
125
  }
126
+ for (const pictEl of vmlTextBoxes) {
127
+ const shape = parseVmlTextBoxShape(pictEl, styles, theme, numbering, rels, media);
128
+ if (!shape) continue;
129
+ const shapeContent = {
130
+ type: "shape",
131
+ shape
132
+ };
133
+ if (targetRun && hasNonTextBoxContent) targetRun.content.push(shapeContent);
134
+ else {
135
+ const newRun = {
136
+ type: "run",
137
+ content: [shapeContent]
138
+ };
139
+ paragraph.content.splice(parsedIndex, 0, newRun);
140
+ lastConsumedRun = newRun;
141
+ parsedIndex += 1;
142
+ }
143
+ }
55
144
  if (hasNonTextBoxContent && parsedRun) {
56
145
  lastConsumedRun = parsedRun;
57
146
  parsedIndex += 1;
@@ -60,6 +149,7 @@ const enrichParagraphTextBoxes = (paragraph, paraXml, styles, theme, numbering,
60
149
  };
61
150
  const scanRunForTextBoxDrawings = (xmlRun) => {
62
151
  const textBoxDrawings = [];
152
+ const vmlTextBoxes = [];
63
153
  let hasNonTextBoxContent = false;
64
154
  const visitDrawing = (drawingEl) => {
65
155
  if (isTextBoxDrawing(drawingEl)) {
@@ -75,6 +165,11 @@ const scanRunForTextBoxDrawings = (xmlRun) => {
75
165
  visitDrawing(el);
76
166
  continue;
77
167
  }
168
+ if (name === "pict") {
169
+ if (findDeep(el, "v", "textbox")) vmlTextBoxes.push(el);
170
+ else hasNonTextBoxContent = true;
171
+ continue;
172
+ }
78
173
  if (name === "AlternateContent") {
79
174
  const branches = getChildElements(el);
80
175
  const choice = branches.find((branch) => getLocalName(branch.name ?? "") === "Choice");
@@ -97,8 +192,52 @@ const scanRunForTextBoxDrawings = (xmlRun) => {
97
192
  }
98
193
  return {
99
194
  textBoxDrawings,
195
+ vmlTextBoxes,
100
196
  hasNonTextBoxContent
101
197
  };
102
198
  };
199
+ const parseVmlTextBoxShape = (pictEl, styles, theme, numbering, rels, media) => {
200
+ const shapeEl = findDeep(pictEl, "v", "shape");
201
+ const textBoxEl = shapeEl ? findDeep(shapeEl, "v", "textbox") : null;
202
+ const contentEl = textBoxEl ? findDeep(textBoxEl, "w", "txbxContent") : null;
203
+ if (!shapeEl || !textBoxEl || !contentEl) return null;
204
+ const style = parseVmlStyle(getAttribute(shapeEl, null, "style"));
205
+ const width = vmlLengthToPixels(style["width"]);
206
+ const height = vmlLengthToPixels(style["height"]);
207
+ if (width === void 0 || height === void 0 || width <= 0 || height <= 0) return null;
208
+ const left = vmlLengthToPixels(style["margin-left"] ?? style["left"]);
209
+ const top = vmlLengthToPixels(style["margin-top"] ?? style["top"]);
210
+ const positioned = style["position"]?.toLowerCase() === "absolute";
211
+ const zIndex = Number.parseInt(style["z-index"] ?? "", 10);
212
+ const margins = parseVmlInsets(textBoxEl);
213
+ const fill = parseVmlFill(shapeEl);
214
+ const shape = {
215
+ type: "shape",
216
+ shapeType: "textBox",
217
+ size: {
218
+ width: pixelsToEmu(width),
219
+ height: pixelsToEmu(height)
220
+ },
221
+ ...fill === void 0 ? {} : { fill },
222
+ wrap: { type: vmlWrapType(positioned, zIndex) },
223
+ textBody: {
224
+ content: parseTextBoxContent(contentEl, parseParagraph, null, styles, theme, numbering, rels ?? void 0, media ?? void 0),
225
+ ...margins === void 0 ? {} : { margins }
226
+ }
227
+ };
228
+ const id = getAttribute(shapeEl, null, "id");
229
+ if (id) shape.id = id;
230
+ if (positioned) shape.position = {
231
+ horizontal: {
232
+ relativeTo: horizontalRelativeTo(style["mso-position-horizontal-relative"]),
233
+ ...left === void 0 ? {} : { posOffset: pixelsToEmu(left) }
234
+ },
235
+ vertical: {
236
+ relativeTo: verticalRelativeTo(style["mso-position-vertical-relative"]),
237
+ ...top === void 0 ? {} : { posOffset: pixelsToEmu(top) }
238
+ }
239
+ };
240
+ return shape;
241
+ };
103
242
  //#endregion
104
243
  export { enrichParagraphTextBoxes };
@@ -11,6 +11,7 @@ import { toArrayBuffer } from "../utils/docxInput.js";
11
11
  import { loadFontsWithMapping } from "../utils/fontLoader.js";
12
12
  import { convertTiffToPngDataUrl, isTiffMimeType } from "../utils/tiffConverter.js";
13
13
  import { normalizeCommentReferences } from "./commentReferenceNormalization.js";
14
+ import { detectDocxConformanceClass } from "./conformance.js";
14
15
  import { extractAllTemplateVariables, parseDocumentBody } from "./documentParser.js";
15
16
  import { normalizeHeaderFooterReferences } from "./headerFooterReferenceNormalization.js";
16
17
  import { extractMetafileRaster, isMetafileMimeType } from "./metafileRaster.js";
@@ -167,6 +168,7 @@ async function parseDocx(input, options = {}) {
167
168
  onProgress("Assembling document...", 95);
168
169
  const document = {
169
170
  package: {
171
+ conformanceClass: detectDocxConformanceClass(raw.documentXml),
170
172
  document: documentBody,
171
173
  settings,
172
174
  ...styleDefinitions !== void 0 ? { styles: styleDefinitions } : {},
@@ -1,4 +1,4 @@
1
- import { cloneWithXmlnsDeclarations, elementToXml, findChild, findChildren, getAttribute, getChildElements, getTextContent, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute } from "./xmlParser.js";
1
+ import { cloneWithXmlnsDeclarations, elementToXml, findAllDeep, findChild, findChildren, getAttribute, getChildElements, getTextContent, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute } from "./xmlParser.js";
2
2
  import { EmphasisMarkSchema, FontThemeSchema, HighlightColorSchema, ShadingPatternSchema, TextEffectSchema, ThemeColorSlotSchema, UnderlineStyleSchema, narrowEnum } from "./parserEnums.js";
3
3
  import { parseImage } from "./imageParser.js";
4
4
  import { parseGroupDrawing } from "./groupDrawingParser.js";
@@ -443,7 +443,7 @@ function parseInstrText(element) {
443
443
  * `shapeParser.parseShapeFromDrawing` into a `ShapeContent`.
444
444
  */
445
445
  function parseDrawingContent(element, rels, media) {
446
- const groupImage = parseGroupDrawing(element);
446
+ const groupImage = parseGroupDrawing(element, rels ?? void 0, media ?? void 0);
447
447
  if (groupImage) return {
448
448
  type: "drawing",
449
449
  image: groupImage,
@@ -544,6 +544,15 @@ function parseRunContents(runElement, rels, media, rootXmlns = {}) {
544
544
  const alternateChildren = getChildElements(child);
545
545
  const choiceEl = alternateChildren.find((el) => getLocalName(el.name) === "Choice");
546
546
  const fallbackEl = alternateChildren.find((el) => getLocalName(el.name) === "Fallback");
547
+ const groupedChoiceDrawing = choiceEl ? getChildElements(choiceEl).find((element) => getLocalName(element.name) === "drawing" && findAllDeep(element, "wpg", "wgp").length > 0) : void 0;
548
+ if (groupedChoiceDrawing) {
549
+ const groupedDrawing = parseDrawingContent(groupedChoiceDrawing, rels, media);
550
+ if (groupedDrawing?.type === "drawing" && groupedDrawing.image.src) {
551
+ groupedDrawing.rawXml = elementToXml(child);
552
+ contents.push(groupedDrawing);
553
+ break;
554
+ }
555
+ }
547
556
  const fallbackPict = fallbackEl ? getChildElements(fallbackEl).find((el) => getLocalName(el.name) === "pict") : void 0;
548
557
  const fallbackVml = fallbackPict ? parseVmlImageContent(fallbackPict, rels, media, rootXmlns) : null;
549
558
  if (fallbackVml?.image.src) {
@@ -6,6 +6,15 @@
6
6
  * unchanged content byte-for-byte. Uses string offset tracking
7
7
  * with proper tag depth counting (not regex) to handle nested elements.
8
8
  */
9
+ /**
10
+ * Whether `char` ends an element's tag name in XML — a whitespace separator
11
+ * (space, tab, CR, or LF, all valid before attributes per XML 1.0 §3.1), the
12
+ * tag close `>`, or a self-close `/`. Manual tag scanners must accept every
13
+ * whitespace form, not just a literal space, so newline-formatted markup
14
+ * (`<w:p\n w14:paraId="…">`) is still recognized as the element rather than
15
+ * mistaken for a longer-named sibling.
16
+ */
17
+ declare function isXmlNameBoundary(char: string | undefined): boolean;
9
18
  /**
10
19
  * Find the exact string start and end offsets of a <w:p> element
11
20
  * identified by its w14:paraId attribute.
@@ -100,4 +109,4 @@ declare function collectChangedNumberingDefs(baselineXml: string, currentXml: st
100
109
  */
101
110
  declare function buildPatchedNumberingXml(originalXml: string, currentXml: string, changed: ChangedNumberingDefs): string | null;
102
111
  //#endregion
103
- export { ChangedNumberingDefs, PatchSafetyOptions, PatchValidationResult, buildPatchedDocumentXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectChangedNumberingDefs, collectParaIds, countParagraphElements, extractParagraphXml, findParagraphOffsets, validatePatchSafety };
112
+ export { ChangedNumberingDefs, PatchSafetyOptions, PatchValidationResult, buildPatchedDocumentXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectChangedNumberingDefs, collectParaIds, countParagraphElements, extractParagraphXml, findParagraphOffsets, isXmlNameBoundary, validatePatchSafety };
@@ -484,4 +484,4 @@ function escapeRegExp(str) {
484
484
  return str.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
485
485
  }
486
486
  //#endregion
487
- export { buildPatchedDocumentXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectChangedNumberingDefs, collectParaIds, countParagraphElements, extractParagraphXml, findParagraphOffsets, validatePatchSafety };
487
+ export { buildPatchedDocumentXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectChangedNumberingDefs, collectParaIds, countParagraphElements, extractParagraphXml, findParagraphOffsets, isXmlNameBoundary, validatePatchSafety };
@@ -112,8 +112,12 @@ function serializeNumbering(numPr) {
112
112
  function serializeFrameProperties(frame) {
113
113
  if (!frame) return "";
114
114
  const attrs = [];
115
+ if (frame.dropCap) attrs.push(`w:dropCap="${frame.dropCap}"`);
116
+ if (frame.lines !== void 0) attrs.push(`w:lines="${intAttr(frame.lines)}"`);
115
117
  if (frame.width !== void 0) attrs.push(`w:w="${intAttr(frame.width)}"`);
116
118
  if (frame.height !== void 0) attrs.push(`w:h="${intAttr(frame.height)}"`);
119
+ if (frame.hSpace !== void 0) attrs.push(`w:hSpace="${intAttr(frame.hSpace)}"`);
120
+ if (frame.vSpace !== void 0) attrs.push(`w:vSpace="${intAttr(frame.vSpace)}"`);
117
121
  if (frame.hAnchor) attrs.push(`w:hAnchor="${frame.hAnchor}"`);
118
122
  if (frame.vAnchor) attrs.push(`w:vAnchor="${frame.vAnchor}"`);
119
123
  if (frame.x !== void 0) attrs.push(`w:x="${frame.x}"`);
@@ -456,13 +456,18 @@ function serializeShapeContent(content) {
456
456
  if (tb.margins.right != null) bpAttrs.push(`rIns="${intAttr(tb.margins.right)}"`);
457
457
  if (tb.margins.bottom != null) bpAttrs.push(`bIns="${intAttr(tb.margins.bottom)}"`);
458
458
  }
459
+ let autoFitXml = "";
460
+ if (tb.autoFit === "shape") autoFitXml = "<a:spAutoFit/>";
461
+ else if (tb.autoFit === "normal") autoFitXml = "<a:normAutofit/>";
462
+ else if (tb.autoFit === "none") autoFitXml = "<a:noAutofit/>";
463
+ const bodyPrXml = autoFitXml ? `<wps:bodyPr ${bpAttrs.join(" ")}>${autoFitXml}</wps:bodyPr>` : `<wps:bodyPr ${bpAttrs.join(" ")}/>`;
459
464
  if (isTextBox) textBody = [
460
465
  "<wps:txbx><w:txbxContent>",
461
466
  serializeShapeTextBody(tb.content),
462
467
  "</w:txbxContent></wps:txbx>",
463
- `<wps:bodyPr ${bpAttrs.join(" ")}/>`
468
+ bodyPrXml
464
469
  ].join("");
465
- else textBody = [`<wps:bodyPr ${bpAttrs.join(" ")}/>`].join("");
470
+ else textBody = bodyPrXml;
466
471
  }
467
472
  const graphic = [
468
473
  "<a:graphic xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\">",
@@ -172,6 +172,10 @@ function serializeTablePropertyChange(change) {
172
172
  function serializeTableRowFormatting(formatting, propertyChanges, structuralChange) {
173
173
  const parts = [];
174
174
  if (formatting) {
175
+ if (formatting.gridBefore) parts.push(`<w:gridBefore w:val="${intAttr(formatting.gridBefore)}"/>`);
176
+ if (formatting.widthBefore) parts.push(`<w:wBefore w:w="${intAttr(formatting.widthBefore.value)}" w:type="${formatting.widthBefore.type}"/>`);
177
+ if (formatting.gridAfter) parts.push(`<w:gridAfter w:val="${intAttr(formatting.gridAfter)}"/>`);
178
+ if (formatting.widthAfter) parts.push(`<w:wAfter w:w="${intAttr(formatting.widthAfter.value)}" w:type="${formatting.widthAfter.type}"/>`);
175
179
  if (formatting.cantSplit) parts.push("<w:cantSplit/>");
176
180
  if (formatting.header) parts.push("<w:tblHeader/>");
177
181
  if (formatting.height) {
@@ -0,0 +1,24 @@
1
+ //#region src/docx/server/boundedArchive.d.ts
2
+ declare const DOCX_MAX_ENTRY_BYTES: number;
3
+ declare const DOCX_MAX_TOTAL_BYTES: number;
4
+ declare const DOCX_MAX_ENTRIES = 4096;
5
+ declare const DocxArchiveError_base: import("better-result").TaggedErrorClass<"DocxArchiveError", {
6
+ message: string;
7
+ reason: "load-failed" | "too-many-entries" | "entry-too-large" | "total-too-large";
8
+ cause?: unknown;
9
+ }>;
10
+ /** Error raised when a DOCX archive cannot be loaded within configured limits. */
11
+ declare class DocxArchiveError extends DocxArchiveError_base {}
12
+ type DocxArchiveOptions = {
13
+ maxEntryBytes?: number;
14
+ maxTotalBytes?: number;
15
+ maxEntries?: number;
16
+ };
17
+ type DocxArchive = {
18
+ entries: readonly string[];
19
+ readEntryString: (path: string) => Promise<string | null>;
20
+ readEntryUint8: (path: string) => Promise<Uint8Array | null>;
21
+ };
22
+ declare const loadDocxArchive: (bytes: ArrayBuffer | Uint8Array, options?: DocxArchiveOptions) => Promise<DocxArchive>;
23
+ //#endregion
24
+ export { DOCX_MAX_ENTRIES, DOCX_MAX_ENTRY_BYTES, DOCX_MAX_TOTAL_BYTES, DocxArchive, DocxArchiveError, DocxArchiveOptions, loadDocxArchive };
@@ -0,0 +1,106 @@
1
+ import { TaggedError } from "better-result";
2
+ import JSZip from "jszip";
3
+ //#region src/docx/server/boundedArchive.ts
4
+ const DOCX_MAX_ENTRY_BYTES = 128 * 1024 * 1024;
5
+ const DOCX_MAX_TOTAL_BYTES = 256 * 1024 * 1024;
6
+ const DOCX_MAX_ENTRIES = 4096;
7
+ /** Error raised when a DOCX archive cannot be loaded within configured limits. */
8
+ var DocxArchiveError = class extends TaggedError("DocxArchiveError")() {};
9
+ const collectStream = async ({ stream, maxEntryBytes, remainingBytes, maxTotalBytes, path }) => await new Promise((resolve, reject) => {
10
+ const chunks = [];
11
+ let entryBytes = 0;
12
+ const fail = (reason, message) => {
13
+ const destroy = Reflect.get(stream, "destroy");
14
+ if (typeof destroy === "function") Reflect.apply(destroy, stream, []);
15
+ reject(new DocxArchiveError({
16
+ message,
17
+ reason
18
+ }));
19
+ };
20
+ stream.on("data", (chunk) => {
21
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
22
+ entryBytes += bytes.length;
23
+ if (entryBytes > maxEntryBytes) {
24
+ fail("entry-too-large", `DOCX entry "${path}" exceeded the ${maxEntryBytes}-byte limit`);
25
+ return;
26
+ }
27
+ if (entryBytes > remainingBytes) {
28
+ fail("total-too-large", `DOCX archive exceeded the ${maxTotalBytes}-byte cumulative limit while reading "${path}"`);
29
+ return;
30
+ }
31
+ chunks.push(bytes);
32
+ });
33
+ stream.on("end", () => resolve(Buffer.concat(chunks)));
34
+ stream.on("error", reject);
35
+ });
36
+ const loadDocxArchive = async (bytes, options = {}) => {
37
+ const maxEntryBytes = options.maxEntryBytes ?? 134217728;
38
+ const maxTotalBytes = options.maxTotalBytes ?? 268435456;
39
+ const maxEntries = options.maxEntries ?? 4096;
40
+ let zip;
41
+ try {
42
+ zip = await JSZip.loadAsync(bytes);
43
+ } catch (cause) {
44
+ throw new DocxArchiveError({
45
+ message: "Failed to parse DOCX archive",
46
+ reason: "load-failed",
47
+ cause
48
+ });
49
+ }
50
+ const archiveEntries = Object.values(zip.files);
51
+ if (archiveEntries.length > maxEntries) throw new DocxArchiveError({
52
+ message: `DOCX archive declares ${archiveEntries.length} entries (max ${maxEntries})`,
53
+ reason: "too-many-entries"
54
+ });
55
+ let declaredTotalBytes = 0;
56
+ for (const entry of archiveEntries) {
57
+ const data = "_data" in entry ? entry._data : void 0;
58
+ const declaredBytes = typeof data === "object" && data !== null && "uncompressedSize" in data ? data.uncompressedSize : void 0;
59
+ if (typeof declaredBytes !== "number" || !Number.isFinite(declaredBytes)) {
60
+ declaredTotalBytes = NaN;
61
+ break;
62
+ }
63
+ if (declaredBytes > maxEntryBytes) throw new DocxArchiveError({
64
+ message: `DOCX entry "${entry.name}" declares ${declaredBytes} bytes (max ${maxEntryBytes})`,
65
+ reason: "entry-too-large"
66
+ });
67
+ declaredTotalBytes += declaredBytes;
68
+ }
69
+ if (Number.isFinite(declaredTotalBytes) && declaredTotalBytes > maxTotalBytes) throw new DocxArchiveError({
70
+ message: `DOCX archive declares ${declaredTotalBytes} cumulative bytes (max ${maxTotalBytes})`,
71
+ reason: "total-too-large"
72
+ });
73
+ let totalBytesRead = 0;
74
+ let readChain = Promise.resolve();
75
+ const readEntry = async (path) => {
76
+ const work = async () => {
77
+ const entry = zip.file(path);
78
+ if (!entry) return null;
79
+ const buffer = await collectStream({
80
+ stream: entry.nodeStream("nodebuffer"),
81
+ maxEntryBytes,
82
+ remainingBytes: maxTotalBytes - totalBytesRead,
83
+ maxTotalBytes,
84
+ path
85
+ });
86
+ totalBytesRead += buffer.length;
87
+ return buffer;
88
+ };
89
+ const next = readChain.then(work, work);
90
+ readChain = next.then(() => void 0, () => void 0);
91
+ return await next;
92
+ };
93
+ return {
94
+ entries: Object.freeze(archiveEntries.map(({ name }) => name)),
95
+ async readEntryString(path) {
96
+ const buffer = await readEntry(path);
97
+ return buffer === null ? null : buffer.toString("utf-8");
98
+ },
99
+ async readEntryUint8(path) {
100
+ const buffer = await readEntry(path);
101
+ return buffer === null ? null : new Uint8Array(buffer);
102
+ }
103
+ };
104
+ };
105
+ //#endregion
106
+ export { DOCX_MAX_ENTRIES, DOCX_MAX_ENTRY_BYTES, DOCX_MAX_TOTAL_BYTES, DocxArchiveError, loadDocxArchive };
@@ -0,0 +1,23 @@
1
+ //#region src/docx/server/extractDocxText.d.ts
2
+ /** Document part containing an extracted paragraph. */
3
+ type DocxParagraphSource = "header" | "body" | "footer";
4
+ /** Paragraph text and lightweight formatting metadata from a DOCX archive. */
5
+ type ExtractedDocxParagraph = {
6
+ index: number;
7
+ text: string;
8
+ source: DocxParagraphSource;
9
+ style?: string;
10
+ bold?: boolean;
11
+ fontSize?: number;
12
+ alignment?: "left" | "center" | "right" | "both";
13
+ };
14
+ /** Accepted-revision paragraph text extracted in deterministic part order. */
15
+ type ExtractedDocxText = {
16
+ paragraphs: ExtractedDocxParagraph[];
17
+ charCount: number;
18
+ view: "accepted";
19
+ };
20
+ /** Extract paragraph text and formatting metadata from a DOCX archive. */
21
+ declare const extractDocxText: (bytes: ArrayBuffer | Uint8Array) => Promise<ExtractedDocxText>;
22
+ //#endregion
23
+ export { DocxParagraphSource, ExtractedDocxParagraph, ExtractedDocxText, extractDocxText };