@stll/folio-core 0.11.0 → 0.13.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 (263) hide show
  1. package/dist/ai-edits/apply.d.ts +2 -1
  2. package/dist/ai-edits/apply.js +693 -63
  3. package/dist/ai-edits/block-identity.d.ts +4 -0
  4. package/dist/ai-edits/block-identity.js +9 -0
  5. package/dist/ai-edits/blockRange.d.ts +28 -1
  6. package/dist/ai-edits/blockRange.js +68 -2
  7. package/dist/ai-edits/clean-text.js +2 -1
  8. package/dist/ai-edits/headless.d.ts +4 -4
  9. package/dist/ai-edits/headless.js +9 -9
  10. package/dist/ai-edits/index.d.ts +2 -2
  11. package/dist/ai-edits/index.js +2 -2
  12. package/dist/ai-edits/read.d.ts +7 -7
  13. package/dist/ai-edits/read.js +110 -3
  14. package/dist/ai-edits/snapshot.js +21 -3
  15. package/dist/ai-edits/table-cell-mutations.d.ts +48 -0
  16. package/dist/ai-edits/table-cell-mutations.js +238 -0
  17. package/dist/ai-edits/table-mutation-plan.d.ts +38 -0
  18. package/dist/ai-edits/table-mutation-plan.js +90 -0
  19. package/dist/ai-edits/table-row-column-mutations.d.ts +108 -0
  20. package/dist/ai-edits/table-row-column-mutations.js +300 -0
  21. package/dist/ai-edits/table-targets.d.ts +32 -0
  22. package/dist/ai-edits/table-targets.js +70 -0
  23. package/dist/ai-edits/types.d.ts +62 -1
  24. package/dist/ai-edits/word-diff.js +21 -0
  25. package/dist/compat/eigenpal.d.ts +3 -3
  26. package/dist/compat/eigenpal.js +3 -3
  27. package/dist/content-controls/mutateContentControls.js +1 -1
  28. package/dist/controller/activeEditorStory.d.ts +36 -0
  29. package/dist/controller/activeEditorStory.js +30 -0
  30. package/dist/controller/contentControlWidgetController.d.ts +55 -0
  31. package/dist/controller/contentControlWidgetController.js +141 -0
  32. package/dist/controller/fontReadiness.js +2 -0
  33. package/dist/controller/headerFooterEditorManager.js +2 -2
  34. package/dist/controller/hiddenEditorApi.js +2 -2
  35. package/dist/controller/hiddenEditorManager.d.ts +12 -1
  36. package/dist/controller/hiddenEditorManager.js +44 -12
  37. package/dist/controller/layoutPipeline.d.ts +2 -0
  38. package/dist/controller/layoutPipeline.js +17 -2
  39. package/dist/controller/noteEditorManager.d.ts +36 -0
  40. package/dist/controller/noteEditorManager.js +210 -0
  41. package/dist/document-operations.d.ts +43 -11
  42. package/dist/document-operations.js +186 -18
  43. package/dist/docx/blockContentParser.js +21 -7
  44. package/dist/docx/commentParser.js +6 -1
  45. package/dist/docx/commentRangeIntegrity.d.ts +6 -0
  46. package/dist/docx/commentRangeIntegrity.js +186 -0
  47. package/dist/docx/conformance.js +62 -1
  48. package/dist/docx/documentGrid.d.ts +8 -0
  49. package/dist/docx/documentGrid.js +8 -0
  50. package/dist/docx/drawingUtils.js +1 -1
  51. package/dist/docx/encryption/agileDecryption.js +12 -2
  52. package/dist/docx/encryption/compoundFile.js +6 -1
  53. package/dist/docx/encryption/encryptionInfo.d.ts +9 -1
  54. package/dist/docx/encryption/encryptionInfo.js +18 -2
  55. package/dist/docx/encryption/openEncryptedDocx.js +1 -1
  56. package/dist/docx/ensureParaIds.js +8 -7
  57. package/dist/docx/fieldParser.js +1 -1
  58. package/dist/docx/groupDrawingParser.js +10 -5
  59. package/dist/docx/hyperlinkParser.js +1 -1
  60. package/dist/docx/imageParser.js +10 -6
  61. package/dist/docx/metadataPrivacy.js +17 -3
  62. package/dist/docx/notePropertiesParser.js +1 -1
  63. package/dist/docx/numberingParser.js +1 -1
  64. package/dist/docx/paragraphParser.js +70 -61
  65. package/dist/docx/paragraphTextBoxEnrichment.d.ts +2 -1
  66. package/dist/docx/paragraphTextBoxEnrichment.js +67 -13
  67. package/dist/docx/paragraphTraversal.js +1 -1
  68. package/dist/docx/parser.js +3 -3
  69. package/dist/docx/rezip.js +20 -9
  70. package/dist/docx/runParser.js +17 -6
  71. package/dist/docx/sdtProperties.js +1 -1
  72. package/dist/docx/sectionParser.js +18 -5
  73. package/dist/docx/selectiveSave.js +5 -1
  74. package/dist/docx/selectiveXmlPatch.d.ts +22 -1
  75. package/dist/docx/selectiveXmlPatch.js +61 -1
  76. package/dist/docx/serializer/blockSdtSerializer.js +3 -2
  77. package/dist/docx/serializer/commentSerializer.js +5 -6
  78. package/dist/docx/serializer/documentSerializer.js +2 -2
  79. package/dist/docx/serializer/headerFooterSerializer.js +2 -2
  80. package/dist/docx/serializer/noteSerializer.js +2 -2
  81. package/dist/docx/serializer/paragraphSerializer.js +23 -15
  82. package/dist/docx/serializer/runSerializer.js +25 -21
  83. package/dist/docx/serializer/sectionPropertiesSerializer.js +2 -1
  84. package/dist/docx/serializer/stylesSerializer.js +1 -1
  85. package/dist/docx/serializer/tableSerializer.d.ts +5 -4
  86. package/dist/docx/serializer/tableSerializer.js +26 -20
  87. package/dist/docx/serializer/xmlUtils.d.ts +13 -1
  88. package/dist/docx/serializer/xmlUtils.js +27 -1
  89. package/dist/docx/server/applyDocxXmlPatchProposal.d.ts +87 -0
  90. package/dist/docx/server/applyDocxXmlPatchProposal.js +204 -0
  91. package/dist/docx/server/boundedArchive.d.ts +14 -3
  92. package/dist/docx/server/boundedArchive.js +56 -11
  93. package/dist/docx/server/evaluateDocxXmlPatchProposal.d.ts +85 -0
  94. package/dist/docx/server/evaluateDocxXmlPatchProposal.js +361 -0
  95. package/dist/docx/server/extractDocxText.js +55 -8
  96. package/dist/docx/server/inspectDocxPackage.d.ts +70 -0
  97. package/dist/docx/server/inspectDocxPackage.js +217 -0
  98. package/dist/docx/server/validateDocxConformance.d.ts +41 -0
  99. package/dist/docx/server/validateDocxConformance.js +336 -0
  100. package/dist/docx/settingsParser.js +26 -3
  101. package/dist/docx/shapeParser.js +1 -1
  102. package/dist/docx/styleParser.js +4 -2
  103. package/dist/docx/tableParser.d.ts +8 -1
  104. package/dist/docx/tableParser.js +31 -10
  105. package/dist/docx/textBoxParser.d.ts +3 -3
  106. package/dist/docx/textBoxParser.js +16 -12
  107. package/dist/docx/unzip.js +1 -0
  108. package/dist/docx/vmlImageParser.js +11 -3
  109. package/dist/docx/xmlParser.js +11 -1
  110. package/dist/docx/xmlSafety.d.ts +5 -0
  111. package/dist/docx/xmlSafety.js +9 -0
  112. package/dist/fields/resolveFieldValues.js +1 -3
  113. package/dist/fonts/embeddedFonts.d.ts +33 -5
  114. package/dist/fonts/embeddedFonts.js +37 -8
  115. package/dist/i18n/messages/catalogs.gen.d.ts +17 -0
  116. package/dist/i18n/messages/catalogs.gen.js +17 -0
  117. package/dist/i18n/messages/messages.gen.d.ts +1 -0
  118. package/dist/index.d.ts +3 -3
  119. package/dist/index.js +3 -3
  120. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  121. package/dist/layout-bridge/convert/footnoteLayout.js +47 -40
  122. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  123. package/dist/layout-bridge/convert/headerFooterLayout.js +3 -0
  124. package/dist/layout-bridge/convert/templatePreviewFlow.js +3 -3
  125. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -2
  126. package/dist/layout-bridge/convert/toFlowBlocks.js +151 -20
  127. package/dist/layout-bridge/dom/noteStoryDom.d.ts +6 -0
  128. package/dist/layout-bridge/dom/noteStoryDom.js +17 -0
  129. package/dist/layout-bridge/engine/clickToPosition.js +2 -2
  130. package/dist/layout-engine/index.d.ts +2 -2
  131. package/dist/layout-engine/index.js +58 -30
  132. package/dist/layout-engine/justificationCompatibility.d.ts +6 -0
  133. package/dist/layout-engine/justificationCompatibility.js +8 -0
  134. package/dist/layout-engine/keep-together.js +11 -1
  135. package/dist/layout-engine/measure/cache.js +23 -5
  136. package/dist/layout-engine/measure/effectiveLineBreakPolicy.js +8 -2
  137. package/dist/layout-engine/measure/lineBreakProvider.d.ts +2 -2
  138. package/dist/layout-engine/measure/lineBreakProvider.js +56 -37
  139. package/dist/layout-engine/measure/measureBlocks.js +15 -4
  140. package/dist/layout-engine/measure/measureContainer.js +1 -1
  141. package/dist/layout-engine/measure/measureHelpers.d.ts +11 -2
  142. package/dist/layout-engine/measure/measureHelpers.js +15 -4
  143. package/dist/layout-engine/measure/measureParagraph.js +85 -25
  144. package/dist/layout-engine/measure/measureProvider.js +4 -3
  145. package/dist/layout-engine/measure/tableCellFlow.js +5 -2
  146. package/dist/layout-engine/measure/textBoxParagraphLayout.d.ts +9 -7
  147. package/dist/layout-engine/measure/textBoxParagraphLayout.js +22 -6
  148. package/dist/layout-engine/measure/textMeasurementPolicy.d.ts +3 -1
  149. package/dist/layout-engine/measure/textMeasurementPolicy.js +7 -1
  150. package/dist/layout-engine/paginator.js +5 -1
  151. package/dist/layout-engine/paragraphSpacing.d.ts +28 -4
  152. package/dist/layout-engine/paragraphSpacing.js +107 -13
  153. package/dist/layout-engine/renderedBreakReconciliation.js +1 -1
  154. package/dist/layout-engine/types.d.ts +48 -15
  155. package/dist/layout-painter/registry/modules/textBox.js +5 -1
  156. package/dist/layout-painter/renderImage.d.ts +12 -1
  157. package/dist/layout-painter/renderImage.js +21 -4
  158. package/dist/layout-painter/renderPage.d.ts +5 -2
  159. package/dist/layout-painter/renderPage.js +59 -14
  160. package/dist/layout-painter/renderParagraph.js +55 -15
  161. package/dist/layout-painter/renderTable.d.ts +5 -1
  162. package/dist/layout-painter/renderTable.js +7 -3
  163. package/dist/layout-painter/renderTextBox.d.ts +2 -1
  164. package/dist/layout-painter/renderTextBox.js +25 -14
  165. package/dist/layout-painter/renderWatermark.d.ts +1 -1
  166. package/dist/layout-painter/renderWatermark.js +2 -1
  167. package/dist/managers/DocumentLoaderManager.js +1 -1
  168. package/dist/markdown/fromMarkdown.js +1 -1
  169. package/dist/model.js +1 -1
  170. package/dist/prosemirror/attrs/index.d.ts +4 -2
  171. package/dist/prosemirror/attrs/index.js +175 -18
  172. package/dist/prosemirror/commands/comments.d.ts +79 -4
  173. package/dist/prosemirror/commands/comments.js +622 -13
  174. package/dist/prosemirror/commands/contentControls.js +1 -1
  175. package/dist/prosemirror/commands/hyperlink.js +15 -1
  176. package/dist/prosemirror/commands/image.js +7 -1
  177. package/dist/prosemirror/commands/index.d.ts +2 -2
  178. package/dist/prosemirror/commands/index.js +2 -2
  179. package/dist/prosemirror/commands/pageBreak.js +1 -1
  180. package/dist/prosemirror/commands/propertyChangeScope.d.ts +2 -2
  181. package/dist/prosemirror/commands/propertyChangeScope.js +3 -2
  182. package/dist/prosemirror/commands/tableCellMergeResolution.d.ts +9 -0
  183. package/dist/prosemirror/commands/tableCellMergeResolution.js +181 -0
  184. package/dist/prosemirror/commentIdAllocator.js +13 -3
  185. package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -1
  186. package/dist/prosemirror/conversion/fromProseDoc.js +404 -62
  187. package/dist/prosemirror/conversion/index.js +1 -1
  188. package/dist/prosemirror/conversion/sdtAttrs.d.ts +9 -0
  189. package/dist/prosemirror/conversion/sdtAttrs.js +49 -0
  190. package/dist/prosemirror/conversion/toProseDoc.d.ts +2 -1
  191. package/dist/prosemirror/conversion/toProseDoc.js +364 -103
  192. package/dist/prosemirror/extensions/ExtensionManager.js +1 -1
  193. package/dist/prosemirror/extensions/StarterKit.js +6 -1
  194. package/dist/prosemirror/extensions/core/ParagraphExtension.js +6 -2
  195. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +1 -0
  196. package/dist/prosemirror/extensions/features/ImagePasteExtension.js +10 -2
  197. package/dist/prosemirror/extensions/features/ListExtension.js +1 -1
  198. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +10 -1
  199. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +93 -2
  200. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +2 -1
  201. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +6 -2
  202. package/dist/prosemirror/extensions/features/pasteCleanup.js +15 -2
  203. package/dist/prosemirror/extensions/marks/CharacterSpacingExtension.js +1 -1
  204. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +30 -4
  205. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.d.ts +9 -1
  206. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +70 -11
  207. package/dist/prosemirror/extensions/nodes/FieldExtension.js +1 -1
  208. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -0
  209. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +22 -0
  210. package/dist/prosemirror/extensions/nodes/TableExtension.d.ts +1 -1
  211. package/dist/prosemirror/extensions/nodes/TableExtension.js +14 -6
  212. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +13 -0
  213. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +41 -0
  214. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +5 -2
  215. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +4 -1
  216. package/dist/prosemirror/index.d.ts +1 -1
  217. package/dist/prosemirror/index.js +1 -1
  218. package/dist/prosemirror/plugins/contentControlWidgets.d.ts +36 -10
  219. package/dist/prosemirror/plugins/contentControlWidgets.js +128 -94
  220. package/dist/prosemirror/plugins/revisionIds.d.ts +23 -0
  221. package/dist/prosemirror/plugins/revisionIds.js +57 -0
  222. package/dist/prosemirror/plugins/selectionTracker.js +1 -0
  223. package/dist/prosemirror/plugins/suggestionMode.js +5 -4
  224. package/dist/prosemirror/runFormattingMarkNames.d.ts +14 -0
  225. package/dist/prosemirror/runFormattingMarkNames.js +40 -0
  226. package/dist/prosemirror/schema/index.d.ts +2 -2
  227. package/dist/prosemirror/schema/marks.d.ts +30 -3
  228. package/dist/prosemirror/schema/marks.js +15 -0
  229. package/dist/prosemirror/schema/nodes.d.ts +111 -6
  230. package/dist/prosemirror/selectionState.js +1 -0
  231. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  232. package/dist/prosemirror/tableCellMergeRevision.d.ts +9 -0
  233. package/dist/prosemirror/tableCellMergeRevision.js +22 -0
  234. package/dist/prosemirror/textBoxAnchorAttrs.d.ts +9 -0
  235. package/dist/prosemirror/textBoxAnchorAttrs.js +33 -0
  236. package/dist/prosemirror/utils/extractTrackedChanges.d.ts +1 -1
  237. package/dist/prosemirror/utils/extractTrackedChanges.js +62 -14
  238. package/dist/prosemirror/validation.js +9 -1
  239. package/dist/render-dom/RemoteSelectionOverlay.js +2 -2
  240. package/dist/server.d.ts +7 -3
  241. package/dist/server.js +7 -3
  242. package/dist/utils/clipboard.d.ts +10 -1
  243. package/dist/utils/clipboard.js +33 -6
  244. package/dist/utils/colorResolver.d.ts +8 -1
  245. package/dist/utils/colorResolver.js +35 -5
  246. package/dist/utils/fontFamilyMerge.js +9 -11
  247. package/dist/utils/fontLoader.js +3 -2
  248. package/dist/utils/fontResolver.d.ts +2 -1
  249. package/dist/utils/fontResolver.js +31 -7
  250. package/dist/utils/fontWeights.d.ts +1 -1
  251. package/dist/utils/fontWeights.js +1 -1
  252. package/dist/utils/hexId.d.ts +11 -1
  253. package/dist/utils/hexId.js +11 -1
  254. package/dist/utils/paragraphFormattingMerge.js +1 -0
  255. package/dist/utils/sanitizeImageSrc.d.ts +21 -0
  256. package/dist/utils/sanitizeImageSrc.js +30 -0
  257. package/dist/utils/stripXmlDeclarations.d.ts +11 -0
  258. package/dist/utils/stripXmlDeclarations.js +35 -0
  259. package/dist/version-comparison.d.ts +22 -2
  260. package/dist/version-comparison.js +41 -12
  261. package/dist/watermark/index.d.ts +11 -1
  262. package/dist/watermark/index.js +22 -1
  263. package/package.json +3 -2
@@ -0,0 +1,217 @@
1
+ import { getAttributeAnyPrefix, getLocalName, parseXmlDocument } from "../xmlParser.js";
2
+ import { DocxArchiveError, loadDocxArchive } from "./boundedArchive.js";
3
+ import { TaggedError } from "better-result";
4
+ import { createHash } from "node:crypto";
5
+ //#region src/docx/server/inspectDocxPackage.ts
6
+ const FOLIO_DOCX_PACKAGE_INSPECTION_VERSION = 1;
7
+ const FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS = Object.freeze({
8
+ maxXmlParts: 16,
9
+ maxXmlPartBytes: 8 * 1024 * 1024,
10
+ maxXmlTotalBytes: 16 * 1024 * 1024
11
+ });
12
+ const FOLIO_DOCX_PACKAGE_INSPECTION_ERROR_CODES = Object.freeze([
13
+ "invalid-limits",
14
+ "too-many-xml-parts",
15
+ "duplicate-xml-part",
16
+ "part-not-found",
17
+ "part-not-xml",
18
+ "xml-part-too-large",
19
+ "xml-total-too-large",
20
+ "xml-decode-failed"
21
+ ]);
22
+ var FolioDocxPackageInspectionError = class extends TaggedError("FolioDocxPackageInspectionError")() {};
23
+ const CONTENT_TYPES_PATH = "[Content_Types].xml";
24
+ const CONTENT_TYPES_READ_LIMIT = 4 * 1024 * 1024;
25
+ const XML_PATH_PATTERN = /(?:\.xml|\.rels)$/iu;
26
+ const XML_CONTENT_TYPES = /* @__PURE__ */ new Set(["application/xml", "text/xml"]);
27
+ const comparePackagePaths = (left, right) => {
28
+ if (left < right) return -1;
29
+ return left === right ? 0 : 1;
30
+ };
31
+ const decodeXml = (bytes, part) => {
32
+ let encoding = "utf-8";
33
+ if (bytes.length >= 2 && (bytes[0] === 255 && bytes[1] === 254 || bytes[0] === 60 && bytes[1] === 0)) encoding = "utf-16le";
34
+ else if (bytes.length >= 2 && (bytes[0] === 254 && bytes[1] === 255 || bytes[0] === 0 && bytes[1] === 60)) encoding = "utf-16be";
35
+ try {
36
+ return new TextDecoder(encoding, { fatal: true }).decode(bytes);
37
+ } catch (cause) {
38
+ throw new FolioDocxPackageInspectionError({
39
+ message: `Failed to decode XML package part "${part}"`,
40
+ code: "xml-decode-failed",
41
+ part,
42
+ cause
43
+ });
44
+ }
45
+ };
46
+ const parseContentTypes = (xml) => {
47
+ const declarations = {
48
+ defaults: /* @__PURE__ */ new Map(),
49
+ overrides: /* @__PURE__ */ new Map()
50
+ };
51
+ if (xml === null) return declarations;
52
+ const root = parseXmlDocument(xml);
53
+ if (root === null || getLocalName(root.name ?? "") !== "Types") return declarations;
54
+ for (const element of root.elements ?? []) {
55
+ if (element.type !== "element") continue;
56
+ const localName = getLocalName(element.name ?? "");
57
+ if (localName === "Default") {
58
+ const extension = getAttributeAnyPrefix(element, "Extension")?.toLowerCase();
59
+ const contentType = getAttributeAnyPrefix(element, "ContentType");
60
+ if (extension !== void 0 && contentType !== null) declarations.defaults.set(extension, contentType);
61
+ continue;
62
+ }
63
+ if (localName !== "Override") continue;
64
+ const partName = getAttributeAnyPrefix(element, "PartName")?.replace(/^\//u, "");
65
+ const contentType = getAttributeAnyPrefix(element, "ContentType");
66
+ if (partName !== void 0 && contentType !== null) declarations.overrides.set(partName, contentType);
67
+ }
68
+ return declarations;
69
+ };
70
+ const extensionFor = (path) => {
71
+ const filename = path.slice(path.lastIndexOf("/") + 1);
72
+ const separator = filename.lastIndexOf(".");
73
+ return separator === -1 ? null : filename.slice(separator + 1).toLowerCase();
74
+ };
75
+ const contentTypeFor = (path, declarations) => {
76
+ const override = declarations.overrides.get(path);
77
+ if (override !== void 0) return override;
78
+ const extension = extensionFor(path);
79
+ return extension === null ? null : declarations.defaults.get(extension) ?? null;
80
+ };
81
+ const isXmlPart = (path, contentType) => {
82
+ if (contentType !== null) return XML_CONTENT_TYPES.has(contentType) || contentType.endsWith("+xml");
83
+ return XML_PATH_PATTERN.test(path);
84
+ };
85
+ const toPackagePart = (entry, declarations) => {
86
+ const contentType = contentTypeFor(entry.path, declarations);
87
+ return {
88
+ path: entry.path,
89
+ kind: isXmlPart(entry.path, contentType) ? "xml" : "binary",
90
+ contentType,
91
+ declaredUncompressedBytes: entry.declaredUncompressedBytes
92
+ };
93
+ };
94
+ const requireNonNegativeInteger = (value, name) => {
95
+ if (Number.isSafeInteger(value) && value >= 0) return value;
96
+ throw new FolioDocxPackageInspectionError({
97
+ message: `${name} must be a non-negative safe integer`,
98
+ code: "invalid-limits"
99
+ });
100
+ };
101
+ const resolveLimits = (limits) => ({
102
+ maxXmlParts: requireNonNegativeInteger(limits?.maxXmlParts ?? FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS.maxXmlParts, "maxXmlParts"),
103
+ maxXmlPartBytes: requireNonNegativeInteger(limits?.maxXmlPartBytes ?? FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS.maxXmlPartBytes, "maxXmlPartBytes"),
104
+ maxXmlTotalBytes: requireNonNegativeInteger(limits?.maxXmlTotalBytes ?? FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS.maxXmlTotalBytes, "maxXmlTotalBytes")
105
+ });
106
+ const validateRequestedParts = ({ requestedPaths, partsByPath, limits }) => {
107
+ if (requestedPaths.length > limits.maxXmlParts) throw new FolioDocxPackageInspectionError({
108
+ message: `Requested ${requestedPaths.length} XML parts (max ${limits.maxXmlParts})`,
109
+ code: "too-many-xml-parts"
110
+ });
111
+ const seen = /* @__PURE__ */ new Set();
112
+ let declaredTotalBytes = 0;
113
+ for (const path of requestedPaths) {
114
+ if (seen.has(path)) throw new FolioDocxPackageInspectionError({
115
+ message: `XML package part "${path}" was requested more than once`,
116
+ code: "duplicate-xml-part",
117
+ part: path
118
+ });
119
+ seen.add(path);
120
+ const part = partsByPath.get(path);
121
+ if (part === void 0) throw new FolioDocxPackageInspectionError({
122
+ message: `Package part "${path}" does not exist`,
123
+ code: "part-not-found",
124
+ part: path
125
+ });
126
+ if (part.kind !== "xml") throw new FolioDocxPackageInspectionError({
127
+ message: `Package part "${path}" is not declared as XML`,
128
+ code: "part-not-xml",
129
+ part: path
130
+ });
131
+ const declaredBytes = part.declaredUncompressedBytes;
132
+ if (declaredBytes === null) continue;
133
+ if (declaredBytes > limits.maxXmlPartBytes) throw new FolioDocxPackageInspectionError({
134
+ message: `XML package part "${path}" exceeds the per-part inspection limit`,
135
+ code: "xml-part-too-large",
136
+ part: path
137
+ });
138
+ declaredTotalBytes += declaredBytes;
139
+ if (declaredTotalBytes > limits.maxXmlTotalBytes) throw new FolioDocxPackageInspectionError({
140
+ message: "Requested XML package parts exceed the cumulative inspection limit",
141
+ code: "xml-total-too-large"
142
+ });
143
+ }
144
+ };
145
+ const readRequestedXmlParts = async ({ paths, partsByPath, limits, readEntryUint8 }) => {
146
+ const selected = [];
147
+ let totalBytes = 0;
148
+ for (const path of paths) {
149
+ const remainingTotalBytes = limits.maxXmlTotalBytes - totalBytes;
150
+ const readLimit = Math.min(limits.maxXmlPartBytes, remainingTotalBytes);
151
+ let bytes;
152
+ try {
153
+ bytes = await readEntryUint8(path, { maxBytes: readLimit });
154
+ } catch (cause) {
155
+ if (cause instanceof DocxArchiveError && cause.reason === "entry-too-large") {
156
+ const cumulativeLimitReached = readLimit < limits.maxXmlPartBytes;
157
+ throw new FolioDocxPackageInspectionError({
158
+ message: cumulativeLimitReached ? "Requested XML package parts exceed the cumulative inspection limit" : `XML package part "${path}" exceeds the per-part inspection limit`,
159
+ code: cumulativeLimitReached ? "xml-total-too-large" : "xml-part-too-large",
160
+ part: path,
161
+ cause
162
+ });
163
+ }
164
+ throw cause;
165
+ }
166
+ if (bytes === null) throw new FolioDocxPackageInspectionError({
167
+ message: `Package part "${path}" does not exist`,
168
+ code: "part-not-found",
169
+ part: path
170
+ });
171
+ totalBytes += bytes.byteLength;
172
+ if (totalBytes > limits.maxXmlTotalBytes) throw new FolioDocxPackageInspectionError({
173
+ message: "Requested XML package parts exceed the cumulative inspection limit",
174
+ code: "xml-total-too-large"
175
+ });
176
+ selected.push({
177
+ path,
178
+ contentType: partsByPath.get(path)?.contentType ?? null,
179
+ byteLength: bytes.byteLength,
180
+ sha256: createHash("sha256").update(bytes).digest("hex"),
181
+ text: decodeXml(bytes, path)
182
+ });
183
+ }
184
+ return selected;
185
+ };
186
+ /**
187
+ * Inspect package metadata and only the explicitly requested XML part bodies.
188
+ * This reports archive contents, not package conformance; use the conformance
189
+ * report when an operation requires a validity decision.
190
+ */
191
+ const inspectDocxPackage = async (bytes, options = {}) => {
192
+ const limits = resolveLimits(options.limits);
193
+ const requestedPaths = options.xmlParts ?? [];
194
+ const archive = await loadDocxArchive(bytes, options.archive);
195
+ const contentTypesBytes = await archive.readEntryUint8(CONTENT_TYPES_PATH, { maxBytes: CONTENT_TYPES_READ_LIMIT });
196
+ const declarations = parseContentTypes(contentTypesBytes === null ? null : decodeXml(contentTypesBytes, CONTENT_TYPES_PATH));
197
+ const parts = archive.entryMetadata.filter(({ directory }) => !directory).map((entry) => toPackagePart(entry, declarations)).toSorted(({ path: left }, { path: right }) => comparePackagePaths(left, right));
198
+ const partsByPath = new Map(parts.map((part) => [part.path, part]));
199
+ validateRequestedParts({
200
+ requestedPaths,
201
+ partsByPath,
202
+ limits
203
+ });
204
+ return {
205
+ version: 1,
206
+ parts,
207
+ xmlParts: await readRequestedXmlParts({
208
+ paths: requestedPaths,
209
+ partsByPath,
210
+ limits,
211
+ readEntryUint8: archive.readEntryUint8
212
+ }),
213
+ limits
214
+ };
215
+ };
216
+ //#endregion
217
+ export { FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS, FOLIO_DOCX_PACKAGE_INSPECTION_ERROR_CODES, FOLIO_DOCX_PACKAGE_INSPECTION_VERSION, FolioDocxPackageInspectionError, inspectDocxPackage, readRequestedXmlParts };
@@ -0,0 +1,41 @@
1
+ import { document_d_exports } from "../../types/document.js";
2
+ import { DocxArchiveOptions } from "./boundedArchive.js";
3
+
4
+ //#region src/docx/server/validateDocxConformance.d.ts
5
+ declare const FOLIO_DOCX_CONFORMANCE_REPORT_VERSION: 1;
6
+ declare const FOLIO_DOCX_CONFORMANCE_PROFILE: "folio-supported-v1";
7
+ declare const FOLIO_DOCX_CONFORMANCE_CHECKS: readonly ["archive-safety", "required-parts", "xml-well-formedness", "package-roots", "conformance-class", "canonical-model"];
8
+ declare const FOLIO_DOCX_CONFORMANCE_ISSUE_CODES: readonly ["archive-load-failed", "archive-invalid-options", "archive-input-too-large", "archive-too-many-entries", "archive-entry-too-large", "archive-total-too-large", "required-part-missing", "xml-doctype-forbidden", "xml-not-well-formed", "xml-read-failed", "required-xml-unreadable", "package-root-invalid", "conformance-class-unknown", "model-invalid", "model-warning", "parser-recovery", "parser-unsupported", "parser-failed", "encrypted-container", "container-not-zip"];
9
+ type FolioDocxConformanceCheckId = (typeof FOLIO_DOCX_CONFORMANCE_CHECKS)[number];
10
+ type FolioDocxConformanceIssueCode = (typeof FOLIO_DOCX_CONFORMANCE_ISSUE_CODES)[number];
11
+ type FolioDocxConformanceCheckStatus = "passed" | "failed" | "indeterminate" | "not-run";
12
+ type FolioDocxConformanceStatus = "invalid" | "conformant" | "indeterminate";
13
+ type FolioDocxConformanceIssue = {
14
+ readonly check: FolioDocxConformanceCheckId;
15
+ readonly code: FolioDocxConformanceIssueCode;
16
+ readonly message: string;
17
+ readonly severity: "error" | "warning";
18
+ readonly part?: string;
19
+ readonly modelPath?: string;
20
+ readonly count?: number;
21
+ };
22
+ type FolioDocxConformanceCheck = {
23
+ readonly id: FolioDocxConformanceCheckId;
24
+ readonly status: FolioDocxConformanceCheckStatus;
25
+ };
26
+ type FolioDocxConformanceReport = {
27
+ readonly version: typeof FOLIO_DOCX_CONFORMANCE_REPORT_VERSION;
28
+ readonly profile: typeof FOLIO_DOCX_CONFORMANCE_PROFILE;
29
+ readonly status: FolioDocxConformanceStatus;
30
+ readonly conformanceClass: document_d_exports.DocxConformanceClass;
31
+ readonly checks: readonly FolioDocxConformanceCheck[];
32
+ readonly issues: readonly FolioDocxConformanceIssue[];
33
+ readonly unverifiedStandardsDimensions: readonly ["complete-schema-constraints", "markup-compatibility-processing", "consumer-specific-rendering"];
34
+ };
35
+ type ValidateDocxConformanceOptions = {
36
+ readonly archive?: DocxArchiveOptions;
37
+ };
38
+ /** Assess a DOCX package against Folio's named, versioned support profile. */
39
+ declare const validateDocxConformance: (bytes: ArrayBuffer | Uint8Array, options?: ValidateDocxConformanceOptions) => Promise<FolioDocxConformanceReport>;
40
+ //#endregion
41
+ export { FOLIO_DOCX_CONFORMANCE_CHECKS, FOLIO_DOCX_CONFORMANCE_ISSUE_CODES, FOLIO_DOCX_CONFORMANCE_PROFILE, FOLIO_DOCX_CONFORMANCE_REPORT_VERSION, FolioDocxConformanceCheck, FolioDocxConformanceCheckId, FolioDocxConformanceCheckStatus, FolioDocxConformanceIssue, FolioDocxConformanceIssueCode, FolioDocxConformanceReport, FolioDocxConformanceStatus, ValidateDocxConformanceOptions, validateDocxConformance };
@@ -0,0 +1,336 @@
1
+ import { findChildByLocalName, getAttributeAnyPrefix, getLocalName, getNamespacePrefix, parseXmlDocument } from "../xmlParser.js";
2
+ import { detectDocxConformanceClass } from "../conformance.js";
3
+ import { DocxModelValidationError, validateFolioDocumentModel } from "../modelValidation.js";
4
+ import { DOCX_CONTAINER_TYPES, detectDocxContainerType } from "../encryption/containerFormat.js";
5
+ import { DocxParseError, parseDocx } from "../parser.js";
6
+ import { DocxArchiveError, loadDocxArchive } from "./boundedArchive.js";
7
+ import { getDocxXmlSafetyIssue } from "../xmlSafety.js";
8
+ import { DOCX_CONFORMANCE_CLASSES } from "@stll/docx-core/model";
9
+ //#region src/docx/server/validateDocxConformance.ts
10
+ const FOLIO_DOCX_CONFORMANCE_REPORT_VERSION = 1;
11
+ const FOLIO_DOCX_CONFORMANCE_PROFILE = "folio-supported-v1";
12
+ const FOLIO_DOCX_CONFORMANCE_CHECKS = Object.freeze([
13
+ "archive-safety",
14
+ "required-parts",
15
+ "xml-well-formedness",
16
+ "package-roots",
17
+ "conformance-class",
18
+ "canonical-model"
19
+ ]);
20
+ const FOLIO_DOCX_CONFORMANCE_ISSUE_CODES = Object.freeze([
21
+ "archive-load-failed",
22
+ "archive-invalid-options",
23
+ "archive-input-too-large",
24
+ "archive-too-many-entries",
25
+ "archive-entry-too-large",
26
+ "archive-total-too-large",
27
+ "required-part-missing",
28
+ "xml-doctype-forbidden",
29
+ "xml-not-well-formed",
30
+ "xml-read-failed",
31
+ "required-xml-unreadable",
32
+ "package-root-invalid",
33
+ "conformance-class-unknown",
34
+ "model-invalid",
35
+ "model-warning",
36
+ "parser-recovery",
37
+ "parser-unsupported",
38
+ "parser-failed",
39
+ "encrypted-container",
40
+ "container-not-zip"
41
+ ]);
42
+ const REQUIRED_PARTS = Object.freeze([
43
+ "[Content_Types].xml",
44
+ "_rels/.rels",
45
+ "word/document.xml"
46
+ ]);
47
+ const XML_PART_PATTERN = /(?:\.xml|\.rels)$/u;
48
+ const DOCUMENT_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml";
49
+ const CONTENT_TYPES_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/content-types";
50
+ const RELATIONSHIPS_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/relationships";
51
+ const UNVERIFIED_STANDARDS_DIMENSIONS = Object.freeze([
52
+ "complete-schema-constraints",
53
+ "markup-compatibility-processing",
54
+ "consumer-specific-rendering"
55
+ ]);
56
+ const createMutableReport = () => ({
57
+ conformanceClass: DOCX_CONFORMANCE_CLASSES.UNKNOWN,
58
+ checks: new Map(FOLIO_DOCX_CONFORMANCE_CHECKS.map((id) => [id, "not-run"])),
59
+ issues: []
60
+ });
61
+ const addIssue = (report, issue) => {
62
+ report.issues.push(issue);
63
+ };
64
+ const finishReport = (report) => {
65
+ const checks = FOLIO_DOCX_CONFORMANCE_CHECKS.map((id) => ({
66
+ id,
67
+ status: report.checks.get(id) ?? "not-run"
68
+ }));
69
+ const hasFailedCheck = checks.some(({ status }) => status === "failed");
70
+ const hasIndeterminateCheck = checks.some(({ status }) => status === "indeterminate");
71
+ let status = "conformant";
72
+ if (hasFailedCheck) status = "invalid";
73
+ else if (hasIndeterminateCheck || checks.some(({ status: checkStatus }) => checkStatus === "not-run")) status = "indeterminate";
74
+ return {
75
+ version: 1,
76
+ profile: FOLIO_DOCX_CONFORMANCE_PROFILE,
77
+ status,
78
+ conformanceClass: report.conformanceClass,
79
+ checks,
80
+ issues: report.issues,
81
+ unverifiedStandardsDimensions: UNVERIFIED_STANDARDS_DIMENSIONS
82
+ };
83
+ };
84
+ const validateRequiredParts = (archive, report) => {
85
+ const entries = new Set(archive.entries);
86
+ const missingParts = REQUIRED_PARTS.filter((path) => !entries.has(path));
87
+ if (missingParts.length === 0) {
88
+ report.checks.set("required-parts", "passed");
89
+ return true;
90
+ }
91
+ report.checks.set("required-parts", "failed");
92
+ for (const part of missingParts) addIssue(report, {
93
+ check: "required-parts",
94
+ code: "required-part-missing",
95
+ message: "A required package part is missing.",
96
+ severity: "error",
97
+ part
98
+ });
99
+ return false;
100
+ };
101
+ const validateXmlParts = async (archive, report) => {
102
+ const xmlByPath = /* @__PURE__ */ new Map();
103
+ let hasInvalidXml = false;
104
+ for (const part of archive.entries.filter((path) => XML_PART_PATTERN.test(path)).toSorted()) {
105
+ const xml = await archive.readEntryString(part);
106
+ if (xml === null) continue;
107
+ xmlByPath.set(part, xml);
108
+ const safetyIssue = getDocxXmlSafetyIssue(xml);
109
+ if (safetyIssue === "doctype-forbidden") {
110
+ hasInvalidXml = true;
111
+ addIssue(report, {
112
+ check: "xml-well-formedness",
113
+ code: "xml-doctype-forbidden",
114
+ message: "XML package parts must not declare a document type.",
115
+ severity: "error",
116
+ part
117
+ });
118
+ continue;
119
+ }
120
+ if (safetyIssue === "not-well-formed") {
121
+ hasInvalidXml = true;
122
+ addIssue(report, {
123
+ check: "xml-well-formedness",
124
+ code: "xml-not-well-formed",
125
+ message: "An XML package part is not well formed.",
126
+ severity: "error",
127
+ part
128
+ });
129
+ }
130
+ }
131
+ if (hasInvalidXml) {
132
+ report.checks.set("xml-well-formedness", "failed");
133
+ return null;
134
+ }
135
+ report.checks.set("xml-well-formedness", "passed");
136
+ const contentTypesXml = xmlByPath.get("[Content_Types].xml");
137
+ const packageRelationshipsXml = xmlByPath.get("_rels/.rels");
138
+ const documentXml = xmlByPath.get("word/document.xml");
139
+ if (contentTypesXml === void 0 || packageRelationshipsXml === void 0 || documentXml === void 0) {
140
+ report.checks.set("xml-well-formedness", "indeterminate");
141
+ addIssue(report, {
142
+ check: "xml-well-formedness",
143
+ code: "required-xml-unreadable",
144
+ message: "A required XML package part could not be read.",
145
+ severity: "error"
146
+ });
147
+ return null;
148
+ }
149
+ return {
150
+ contentTypesXml,
151
+ packageRelationshipsXml,
152
+ documentXml
153
+ };
154
+ };
155
+ const hasRootNamespace = ({ name, attributes, expected }) => {
156
+ const prefix = getNamespacePrefix(name);
157
+ const attribute = prefix === null ? "xmlns" : `xmlns:${prefix}`;
158
+ return attributes?.[attribute] === expected;
159
+ };
160
+ const hasDocumentContentType = (contentTypesXml) => {
161
+ const root = parseXmlDocument(contentTypesXml);
162
+ if (root === null || getLocalName(root.name ?? "") !== "Types" || !hasRootNamespace({
163
+ name: root.name ?? "",
164
+ attributes: root.attributes,
165
+ expected: CONTENT_TYPES_NAMESPACE
166
+ })) return false;
167
+ return root.elements?.some((element) => element.type === "element" && getLocalName(element.name ?? "") === "Override" && getAttributeAnyPrefix(element, "PartName") === "/word/document.xml" && getAttributeAnyPrefix(element, "ContentType") === DOCUMENT_CONTENT_TYPE) ?? false;
168
+ };
169
+ const hasMainDocumentRelationship = (packageRelationshipsXml) => {
170
+ const root = parseXmlDocument(packageRelationshipsXml);
171
+ if (root === null || getLocalName(root.name ?? "") !== "Relationships" || !hasRootNamespace({
172
+ name: root.name ?? "",
173
+ attributes: root.attributes,
174
+ expected: RELATIONSHIPS_NAMESPACE
175
+ })) return false;
176
+ return root.elements?.some((element) => {
177
+ if (element.type !== "element" || getLocalName(element.name ?? "") !== "Relationship") return false;
178
+ const type = getAttributeAnyPrefix(element, "Type");
179
+ const target = getAttributeAnyPrefix(element, "Target")?.replace(/^\.?\//u, "");
180
+ return type?.endsWith("/officeDocument") === true && target === "word/document.xml" && getAttributeAnyPrefix(element, "TargetMode") !== "External";
181
+ }) ?? false;
182
+ };
183
+ const hasDocumentRoot = (documentXml) => {
184
+ const root = parseXmlDocument(documentXml);
185
+ return root !== null && getLocalName(root.name ?? "") === "document" && findChildByLocalName(root, "body") !== null;
186
+ };
187
+ const validatePackageRoots = (xml, report) => {
188
+ const invalidParts = [];
189
+ if (!hasDocumentContentType(xml.contentTypesXml)) invalidParts.push("[Content_Types].xml");
190
+ if (!hasMainDocumentRelationship(xml.packageRelationshipsXml)) invalidParts.push("_rels/.rels");
191
+ if (!hasDocumentRoot(xml.documentXml)) invalidParts.push("word/document.xml");
192
+ if (invalidParts.length === 0) {
193
+ report.checks.set("package-roots", "passed");
194
+ return true;
195
+ }
196
+ report.checks.set("package-roots", "failed");
197
+ for (const part of invalidParts) addIssue(report, {
198
+ check: "package-roots",
199
+ code: "package-root-invalid",
200
+ message: "A required package part does not declare the expected root or main document binding.",
201
+ severity: "error",
202
+ part
203
+ });
204
+ return false;
205
+ };
206
+ const validateConformanceClass = (documentXml, report) => {
207
+ report.conformanceClass = detectDocxConformanceClass(documentXml);
208
+ if (report.conformanceClass !== DOCX_CONFORMANCE_CLASSES.UNKNOWN) {
209
+ report.checks.set("conformance-class", "passed");
210
+ return;
211
+ }
212
+ report.checks.set("conformance-class", "indeterminate");
213
+ addIssue(report, {
214
+ check: "conformance-class",
215
+ code: "conformance-class-unknown",
216
+ message: "The main document namespace does not identify a supported conformance class.",
217
+ severity: "warning",
218
+ part: "word/document.xml"
219
+ });
220
+ };
221
+ const findModelValidationError = (error) => {
222
+ let current = error;
223
+ while (current instanceof Error) {
224
+ if (current instanceof DocxModelValidationError) return current;
225
+ current = "cause" in current ? current.cause : void 0;
226
+ }
227
+ return null;
228
+ };
229
+ const validateCanonicalModel = async (bytes, report) => {
230
+ try {
231
+ const document = await parseDocx(bytes, {
232
+ detectVariables: false,
233
+ preloadFonts: false
234
+ });
235
+ const validation = validateFolioDocumentModel(document);
236
+ const warningCount = document.warnings?.length ?? 0;
237
+ if (!validation.valid) report.checks.set("canonical-model", "failed");
238
+ else if (warningCount > 0) {
239
+ report.checks.set("canonical-model", "indeterminate");
240
+ addIssue(report, {
241
+ check: "canonical-model",
242
+ code: "parser-recovery",
243
+ message: "The canonical parser reported recovery or preservation warnings.",
244
+ severity: "warning",
245
+ count: warningCount
246
+ });
247
+ } else report.checks.set("canonical-model", "passed");
248
+ for (const issue of validation.issues) addIssue(report, {
249
+ check: "canonical-model",
250
+ code: issue.severity === "error" ? "model-invalid" : "model-warning",
251
+ message: issue.message,
252
+ severity: issue.severity,
253
+ modelPath: issue.path
254
+ });
255
+ } catch (error) {
256
+ const modelError = findModelValidationError(error);
257
+ if (modelError !== null) {
258
+ report.checks.set("canonical-model", "failed");
259
+ for (const issue of modelError.issues) addIssue(report, {
260
+ check: "canonical-model",
261
+ code: issue.severity === "error" ? "model-invalid" : "model-warning",
262
+ message: issue.message,
263
+ severity: issue.severity,
264
+ modelPath: issue.path
265
+ });
266
+ return;
267
+ }
268
+ report.checks.set("canonical-model", "indeterminate");
269
+ addIssue(report, {
270
+ check: "canonical-model",
271
+ code: error instanceof DocxParseError ? "parser-unsupported" : "parser-failed",
272
+ message: "The canonical document model could not be assessed.",
273
+ severity: "warning"
274
+ });
275
+ }
276
+ };
277
+ /** Assess a DOCX package against Folio's named, versioned support profile. */
278
+ const validateDocxConformance = async (bytes, options = {}) => {
279
+ const report = createMutableReport();
280
+ const containerType = detectDocxContainerType(bytes);
281
+ if (containerType === DOCX_CONTAINER_TYPES.CFB) {
282
+ report.checks.set("archive-safety", "indeterminate");
283
+ addIssue(report, {
284
+ check: "archive-safety",
285
+ code: "encrypted-container",
286
+ message: "Encrypted containers require decryption before this profile can assess them.",
287
+ severity: "warning"
288
+ });
289
+ return finishReport(report);
290
+ }
291
+ if (containerType !== DOCX_CONTAINER_TYPES.ZIP) {
292
+ report.checks.set("archive-safety", "failed");
293
+ addIssue(report, {
294
+ check: "archive-safety",
295
+ code: "container-not-zip",
296
+ message: "The input is not a supported DOCX package container.",
297
+ severity: "error"
298
+ });
299
+ return finishReport(report);
300
+ }
301
+ let archive;
302
+ try {
303
+ archive = await loadDocxArchive(bytes, options.archive);
304
+ report.checks.set("archive-safety", "passed");
305
+ } catch (error) {
306
+ report.checks.set("archive-safety", "failed");
307
+ addIssue(report, {
308
+ check: "archive-safety",
309
+ code: error instanceof DocxArchiveError ? `archive-${error.reason}` : "archive-load-failed",
310
+ message: "The DOCX package could not be loaded within the configured safety limits.",
311
+ severity: "error"
312
+ });
313
+ return finishReport(report);
314
+ }
315
+ if (!validateRequiredParts(archive, report)) return finishReport(report);
316
+ let xml;
317
+ try {
318
+ xml = await validateXmlParts(archive, report);
319
+ } catch (error) {
320
+ report.checks.set("xml-well-formedness", "indeterminate");
321
+ addIssue(report, {
322
+ check: "xml-well-formedness",
323
+ code: error instanceof DocxArchiveError ? `archive-${error.reason}` : "xml-read-failed",
324
+ message: "The XML package parts could not be read within the configured safety limits.",
325
+ severity: "warning"
326
+ });
327
+ return finishReport(report);
328
+ }
329
+ if (xml === null) return finishReport(report);
330
+ if (!validatePackageRoots(xml, report)) return finishReport(report);
331
+ validateConformanceClass(xml.documentXml, report);
332
+ await validateCanonicalModel(bytes, report);
333
+ return finishReport(report);
334
+ };
335
+ //#endregion
336
+ export { FOLIO_DOCX_CONFORMANCE_CHECKS, FOLIO_DOCX_CONFORMANCE_ISSUE_CODES, FOLIO_DOCX_CONFORMANCE_PROFILE, FOLIO_DOCX_CONFORMANCE_REPORT_VERSION, validateDocxConformance };
@@ -1,4 +1,4 @@
1
- import { findChild, getAttribute, parseBooleanElement, parseXmlDocument } from "./xmlParser.js";
1
+ import { findChild, findChildren, getAttribute, parseBooleanElement, parseXmlDocument } from "./xmlParser.js";
2
2
  //#region src/docx/settingsParser.ts
3
3
  /** OOXML default per §17.6.13 when `w:defaultTabStop` is absent. */
4
4
  const DEFAULT_TAB_STOP_TWIPS = 720;
@@ -10,6 +10,19 @@ const DEFAULT_TAB_STOP_TWIPS = 720;
10
10
  const MAX_TAB_STOP_TWIPS = 31680;
11
11
  const MAX_HYPHENATION_ZONE_TWIPS = 31680;
12
12
  const MAX_CONSECUTIVE_HYPHEN_LIMIT = 255;
13
+ /**
14
+ * Floor on `w:defaultTabStop`, in twips (1/12 inch). A near-zero value would
15
+ * force the layout engine to generate thousands of tab stops across a
16
+ * single line; below this floor we substitute the OOXML default instead.
17
+ */
18
+ const MIN_TAB_STOP_TWIPS = 120;
19
+ /**
20
+ * Sanity cap on parsed `w:noLineBreaksBefore`/`w:noLineBreaksAfter`
21
+ * character lists. Typical kinsoku overrides list a handful of prohibited
22
+ * characters; anything past this is corruption or a hostile input crafted
23
+ * to inflate the per-character membership set built at measure time.
24
+ */
25
+ const MAX_KINSOKU_CHARACTERS_LENGTH = 128;
13
26
  function parseSettings(xml) {
14
27
  const root = xml ? parseXmlDocument(xml) : null;
15
28
  const settings = { defaultTabStop: parseDefaultTabStop(root) };
@@ -35,6 +48,8 @@ function parseSettings(xml) {
35
48
  const noLineBreaksBefore = parseKinsokuOverride(root, "noLineBreaksBefore");
36
49
  const noLineBreaksAfter = parseKinsokuOverride(root, "noLineBreaksAfter");
37
50
  const compat = root ? findChild(root, "w", "compat") : null;
51
+ const compatibilityMode = parseCompatibilityMode(compat);
52
+ if (compatibilityMode !== void 0) settings.compatibilityMode = compatibilityMode;
38
53
  const splitPageBreakAndParagraphMark = compat ? findChild(compat, "w", "splitPgBreakAndParaMark") : null;
39
54
  if (splitPageBreakAndParagraphMark && parseBooleanElement(splitPageBreakAndParagraphMark)) settings.splitPageBreakAndParagraphMark = true;
40
55
  const applyBreakingRules = compat ? findChild(compat, "w", "applyBreakingRules") : null;
@@ -46,6 +61,14 @@ function parseSettings(xml) {
46
61
  };
47
62
  return settings;
48
63
  }
64
+ function parseCompatibilityMode(compat) {
65
+ if (!compat) return;
66
+ const setting = findChildren(compat, "w", "compatSetting").find((candidate) => getAttribute(candidate, "w", "name") === "compatibilityMode");
67
+ const raw = setting ? getAttribute(setting, "w", "val") : null;
68
+ if (raw === null || !/^\d+$/u.test(raw)) return;
69
+ const parsed = Number.parseInt(raw, 10);
70
+ return Number.isSafeInteger(parsed) ? parsed : void 0;
71
+ }
49
72
  function parseOnOffSetting(root, name) {
50
73
  if (!root) return;
51
74
  const element = findChild(root, "w", name);
@@ -66,7 +89,7 @@ function parseKinsokuOverride(root, name) {
66
89
  if (!characters) return;
67
90
  const language = getAttribute(element, "w", "lang") || void 0;
68
91
  return {
69
- characters,
92
+ characters: Array.from(characters).slice(0, MAX_KINSOKU_CHARACTERS_LENGTH).join(""),
70
93
  ...language ? { language } : {}
71
94
  };
72
95
  }
@@ -77,7 +100,7 @@ function parseDefaultTabStop(root) {
77
100
  const raw = getAttribute(el, "w", "val");
78
101
  if (raw === null) return 720;
79
102
  const parsed = Number.parseInt(raw, 10);
80
- if (!Number.isFinite(parsed) || parsed <= 0 || parsed > MAX_TAB_STOP_TWIPS) return 720;
103
+ if (!Number.isFinite(parsed) || parsed < MIN_TAB_STOP_TWIPS || parsed > MAX_TAB_STOP_TWIPS) return 720;
81
104
  return parsed;
82
105
  }
83
106
  //#endregion
@@ -1,5 +1,5 @@
1
- import { findAllDeep, findChildByLocalName, findChildrenByLocalName, getAttribute, parseNumericAttribute } from "./xmlParser.js";
2
1
  import { ShapeOutlineStyleSchema, ShapeTypeSchema, narrowEnum } from "./parserEnums.js";
2
+ import { findAllDeep, findChildByLocalName, findChildrenByLocalName, getAttribute, parseNumericAttribute } from "./xmlParser.js";
3
3
  import { parseAnchorPosition, parseAnchorWrap, parseColorElement, parseFill } from "./drawingUtils.js";
4
4
  //#region src/docx/shapeParser.ts
5
5
  /** Convert OOXML rotation (1/60000ths of a degree) to degrees. */