@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.
- package/dist/ai-edits/apply.d.ts +2 -1
- package/dist/ai-edits/apply.js +693 -63
- package/dist/ai-edits/block-identity.d.ts +4 -0
- package/dist/ai-edits/block-identity.js +9 -0
- package/dist/ai-edits/blockRange.d.ts +28 -1
- package/dist/ai-edits/blockRange.js +68 -2
- package/dist/ai-edits/clean-text.js +2 -1
- package/dist/ai-edits/headless.d.ts +4 -4
- package/dist/ai-edits/headless.js +9 -9
- package/dist/ai-edits/index.d.ts +2 -2
- package/dist/ai-edits/index.js +2 -2
- package/dist/ai-edits/read.d.ts +7 -7
- package/dist/ai-edits/read.js +110 -3
- package/dist/ai-edits/snapshot.js +21 -3
- package/dist/ai-edits/table-cell-mutations.d.ts +48 -0
- package/dist/ai-edits/table-cell-mutations.js +238 -0
- package/dist/ai-edits/table-mutation-plan.d.ts +38 -0
- package/dist/ai-edits/table-mutation-plan.js +90 -0
- package/dist/ai-edits/table-row-column-mutations.d.ts +108 -0
- package/dist/ai-edits/table-row-column-mutations.js +300 -0
- package/dist/ai-edits/table-targets.d.ts +32 -0
- package/dist/ai-edits/table-targets.js +70 -0
- package/dist/ai-edits/types.d.ts +62 -1
- package/dist/ai-edits/word-diff.js +21 -0
- package/dist/compat/eigenpal.d.ts +3 -3
- package/dist/compat/eigenpal.js +3 -3
- package/dist/content-controls/mutateContentControls.js +1 -1
- package/dist/controller/activeEditorStory.d.ts +36 -0
- package/dist/controller/activeEditorStory.js +30 -0
- package/dist/controller/contentControlWidgetController.d.ts +55 -0
- package/dist/controller/contentControlWidgetController.js +141 -0
- package/dist/controller/fontReadiness.js +2 -0
- package/dist/controller/headerFooterEditorManager.js +2 -2
- package/dist/controller/hiddenEditorApi.js +2 -2
- package/dist/controller/hiddenEditorManager.d.ts +12 -1
- package/dist/controller/hiddenEditorManager.js +44 -12
- package/dist/controller/layoutPipeline.d.ts +2 -0
- package/dist/controller/layoutPipeline.js +17 -2
- package/dist/controller/noteEditorManager.d.ts +36 -0
- package/dist/controller/noteEditorManager.js +210 -0
- package/dist/document-operations.d.ts +43 -11
- package/dist/document-operations.js +186 -18
- package/dist/docx/blockContentParser.js +21 -7
- package/dist/docx/commentParser.js +6 -1
- package/dist/docx/commentRangeIntegrity.d.ts +6 -0
- package/dist/docx/commentRangeIntegrity.js +186 -0
- package/dist/docx/conformance.js +62 -1
- package/dist/docx/documentGrid.d.ts +8 -0
- package/dist/docx/documentGrid.js +8 -0
- package/dist/docx/drawingUtils.js +1 -1
- package/dist/docx/encryption/agileDecryption.js +12 -2
- package/dist/docx/encryption/compoundFile.js +6 -1
- package/dist/docx/encryption/encryptionInfo.d.ts +9 -1
- package/dist/docx/encryption/encryptionInfo.js +18 -2
- package/dist/docx/encryption/openEncryptedDocx.js +1 -1
- package/dist/docx/ensureParaIds.js +8 -7
- package/dist/docx/fieldParser.js +1 -1
- package/dist/docx/groupDrawingParser.js +10 -5
- package/dist/docx/hyperlinkParser.js +1 -1
- package/dist/docx/imageParser.js +10 -6
- package/dist/docx/metadataPrivacy.js +17 -3
- package/dist/docx/notePropertiesParser.js +1 -1
- package/dist/docx/numberingParser.js +1 -1
- package/dist/docx/paragraphParser.js +70 -61
- package/dist/docx/paragraphTextBoxEnrichment.d.ts +2 -1
- package/dist/docx/paragraphTextBoxEnrichment.js +67 -13
- package/dist/docx/paragraphTraversal.js +1 -1
- package/dist/docx/parser.js +3 -3
- package/dist/docx/rezip.js +20 -9
- package/dist/docx/runParser.js +17 -6
- package/dist/docx/sdtProperties.js +1 -1
- package/dist/docx/sectionParser.js +18 -5
- package/dist/docx/selectiveSave.js +5 -1
- package/dist/docx/selectiveXmlPatch.d.ts +22 -1
- package/dist/docx/selectiveXmlPatch.js +61 -1
- package/dist/docx/serializer/blockSdtSerializer.js +3 -2
- package/dist/docx/serializer/commentSerializer.js +5 -6
- package/dist/docx/serializer/documentSerializer.js +2 -2
- package/dist/docx/serializer/headerFooterSerializer.js +2 -2
- package/dist/docx/serializer/noteSerializer.js +2 -2
- package/dist/docx/serializer/paragraphSerializer.js +23 -15
- package/dist/docx/serializer/runSerializer.js +25 -21
- package/dist/docx/serializer/sectionPropertiesSerializer.js +2 -1
- package/dist/docx/serializer/stylesSerializer.js +1 -1
- package/dist/docx/serializer/tableSerializer.d.ts +5 -4
- package/dist/docx/serializer/tableSerializer.js +26 -20
- package/dist/docx/serializer/xmlUtils.d.ts +13 -1
- package/dist/docx/serializer/xmlUtils.js +27 -1
- package/dist/docx/server/applyDocxXmlPatchProposal.d.ts +87 -0
- package/dist/docx/server/applyDocxXmlPatchProposal.js +204 -0
- package/dist/docx/server/boundedArchive.d.ts +14 -3
- package/dist/docx/server/boundedArchive.js +56 -11
- package/dist/docx/server/evaluateDocxXmlPatchProposal.d.ts +85 -0
- package/dist/docx/server/evaluateDocxXmlPatchProposal.js +361 -0
- package/dist/docx/server/extractDocxText.js +55 -8
- package/dist/docx/server/inspectDocxPackage.d.ts +70 -0
- package/dist/docx/server/inspectDocxPackage.js +217 -0
- package/dist/docx/server/validateDocxConformance.d.ts +41 -0
- package/dist/docx/server/validateDocxConformance.js +336 -0
- package/dist/docx/settingsParser.js +26 -3
- package/dist/docx/shapeParser.js +1 -1
- package/dist/docx/styleParser.js +4 -2
- package/dist/docx/tableParser.d.ts +8 -1
- package/dist/docx/tableParser.js +31 -10
- package/dist/docx/textBoxParser.d.ts +3 -3
- package/dist/docx/textBoxParser.js +16 -12
- package/dist/docx/unzip.js +1 -0
- package/dist/docx/vmlImageParser.js +11 -3
- package/dist/docx/xmlParser.js +11 -1
- package/dist/docx/xmlSafety.d.ts +5 -0
- package/dist/docx/xmlSafety.js +9 -0
- package/dist/fields/resolveFieldValues.js +1 -3
- package/dist/fonts/embeddedFonts.d.ts +33 -5
- package/dist/fonts/embeddedFonts.js +37 -8
- package/dist/i18n/messages/catalogs.gen.d.ts +17 -0
- package/dist/i18n/messages/catalogs.gen.js +17 -0
- package/dist/i18n/messages/messages.gen.d.ts +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
- package/dist/layout-bridge/convert/footnoteLayout.js +47 -40
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
- package/dist/layout-bridge/convert/headerFooterLayout.js +3 -0
- package/dist/layout-bridge/convert/templatePreviewFlow.js +3 -3
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -2
- package/dist/layout-bridge/convert/toFlowBlocks.js +151 -20
- package/dist/layout-bridge/dom/noteStoryDom.d.ts +6 -0
- package/dist/layout-bridge/dom/noteStoryDom.js +17 -0
- package/dist/layout-bridge/engine/clickToPosition.js +2 -2
- package/dist/layout-engine/index.d.ts +2 -2
- package/dist/layout-engine/index.js +58 -30
- package/dist/layout-engine/justificationCompatibility.d.ts +6 -0
- package/dist/layout-engine/justificationCompatibility.js +8 -0
- package/dist/layout-engine/keep-together.js +11 -1
- package/dist/layout-engine/measure/cache.js +23 -5
- package/dist/layout-engine/measure/effectiveLineBreakPolicy.js +8 -2
- package/dist/layout-engine/measure/lineBreakProvider.d.ts +2 -2
- package/dist/layout-engine/measure/lineBreakProvider.js +56 -37
- package/dist/layout-engine/measure/measureBlocks.js +15 -4
- package/dist/layout-engine/measure/measureContainer.js +1 -1
- package/dist/layout-engine/measure/measureHelpers.d.ts +11 -2
- package/dist/layout-engine/measure/measureHelpers.js +15 -4
- package/dist/layout-engine/measure/measureParagraph.js +85 -25
- package/dist/layout-engine/measure/measureProvider.js +4 -3
- package/dist/layout-engine/measure/tableCellFlow.js +5 -2
- package/dist/layout-engine/measure/textBoxParagraphLayout.d.ts +9 -7
- package/dist/layout-engine/measure/textBoxParagraphLayout.js +22 -6
- package/dist/layout-engine/measure/textMeasurementPolicy.d.ts +3 -1
- package/dist/layout-engine/measure/textMeasurementPolicy.js +7 -1
- package/dist/layout-engine/paginator.js +5 -1
- package/dist/layout-engine/paragraphSpacing.d.ts +28 -4
- package/dist/layout-engine/paragraphSpacing.js +107 -13
- package/dist/layout-engine/renderedBreakReconciliation.js +1 -1
- package/dist/layout-engine/types.d.ts +48 -15
- package/dist/layout-painter/registry/modules/textBox.js +5 -1
- package/dist/layout-painter/renderImage.d.ts +12 -1
- package/dist/layout-painter/renderImage.js +21 -4
- package/dist/layout-painter/renderPage.d.ts +5 -2
- package/dist/layout-painter/renderPage.js +59 -14
- package/dist/layout-painter/renderParagraph.js +55 -15
- package/dist/layout-painter/renderTable.d.ts +5 -1
- package/dist/layout-painter/renderTable.js +7 -3
- package/dist/layout-painter/renderTextBox.d.ts +2 -1
- package/dist/layout-painter/renderTextBox.js +25 -14
- package/dist/layout-painter/renderWatermark.d.ts +1 -1
- package/dist/layout-painter/renderWatermark.js +2 -1
- package/dist/managers/DocumentLoaderManager.js +1 -1
- package/dist/markdown/fromMarkdown.js +1 -1
- package/dist/model.js +1 -1
- package/dist/prosemirror/attrs/index.d.ts +4 -2
- package/dist/prosemirror/attrs/index.js +175 -18
- package/dist/prosemirror/commands/comments.d.ts +79 -4
- package/dist/prosemirror/commands/comments.js +622 -13
- package/dist/prosemirror/commands/contentControls.js +1 -1
- package/dist/prosemirror/commands/hyperlink.js +15 -1
- package/dist/prosemirror/commands/image.js +7 -1
- package/dist/prosemirror/commands/index.d.ts +2 -2
- package/dist/prosemirror/commands/index.js +2 -2
- package/dist/prosemirror/commands/pageBreak.js +1 -1
- package/dist/prosemirror/commands/propertyChangeScope.d.ts +2 -2
- package/dist/prosemirror/commands/propertyChangeScope.js +3 -2
- package/dist/prosemirror/commands/tableCellMergeResolution.d.ts +9 -0
- package/dist/prosemirror/commands/tableCellMergeResolution.js +181 -0
- package/dist/prosemirror/commentIdAllocator.js +13 -3
- package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -1
- package/dist/prosemirror/conversion/fromProseDoc.js +404 -62
- package/dist/prosemirror/conversion/index.js +1 -1
- package/dist/prosemirror/conversion/sdtAttrs.d.ts +9 -0
- package/dist/prosemirror/conversion/sdtAttrs.js +49 -0
- package/dist/prosemirror/conversion/toProseDoc.d.ts +2 -1
- package/dist/prosemirror/conversion/toProseDoc.js +364 -103
- package/dist/prosemirror/extensions/ExtensionManager.js +1 -1
- package/dist/prosemirror/extensions/StarterKit.js +6 -1
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +6 -2
- package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +1 -0
- package/dist/prosemirror/extensions/features/ImagePasteExtension.js +10 -2
- package/dist/prosemirror/extensions/features/ListExtension.js +1 -1
- package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +10 -1
- package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +93 -2
- package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +2 -1
- package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +6 -2
- package/dist/prosemirror/extensions/features/pasteCleanup.js +15 -2
- package/dist/prosemirror/extensions/marks/CharacterSpacingExtension.js +1 -1
- package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +30 -4
- package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.d.ts +9 -1
- package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +70 -11
- package/dist/prosemirror/extensions/nodes/FieldExtension.js +1 -1
- package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -0
- package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +22 -0
- package/dist/prosemirror/extensions/nodes/TableExtension.d.ts +1 -1
- package/dist/prosemirror/extensions/nodes/TableExtension.js +14 -6
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +13 -0
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +41 -0
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +5 -2
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +4 -1
- package/dist/prosemirror/index.d.ts +1 -1
- package/dist/prosemirror/index.js +1 -1
- package/dist/prosemirror/plugins/contentControlWidgets.d.ts +36 -10
- package/dist/prosemirror/plugins/contentControlWidgets.js +128 -94
- package/dist/prosemirror/plugins/revisionIds.d.ts +23 -0
- package/dist/prosemirror/plugins/revisionIds.js +57 -0
- package/dist/prosemirror/plugins/selectionTracker.js +1 -0
- package/dist/prosemirror/plugins/suggestionMode.js +5 -4
- package/dist/prosemirror/runFormattingMarkNames.d.ts +14 -0
- package/dist/prosemirror/runFormattingMarkNames.js +40 -0
- package/dist/prosemirror/schema/index.d.ts +2 -2
- package/dist/prosemirror/schema/marks.d.ts +30 -3
- package/dist/prosemirror/schema/marks.js +15 -0
- package/dist/prosemirror/schema/nodes.d.ts +111 -6
- package/dist/prosemirror/selectionState.js +1 -0
- package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
- package/dist/prosemirror/tableCellMergeRevision.d.ts +9 -0
- package/dist/prosemirror/tableCellMergeRevision.js +22 -0
- package/dist/prosemirror/textBoxAnchorAttrs.d.ts +9 -0
- package/dist/prosemirror/textBoxAnchorAttrs.js +33 -0
- package/dist/prosemirror/utils/extractTrackedChanges.d.ts +1 -1
- package/dist/prosemirror/utils/extractTrackedChanges.js +62 -14
- package/dist/prosemirror/validation.js +9 -1
- package/dist/render-dom/RemoteSelectionOverlay.js +2 -2
- package/dist/server.d.ts +7 -3
- package/dist/server.js +7 -3
- package/dist/utils/clipboard.d.ts +10 -1
- package/dist/utils/clipboard.js +33 -6
- package/dist/utils/colorResolver.d.ts +8 -1
- package/dist/utils/colorResolver.js +35 -5
- package/dist/utils/fontFamilyMerge.js +9 -11
- package/dist/utils/fontLoader.js +3 -2
- package/dist/utils/fontResolver.d.ts +2 -1
- package/dist/utils/fontResolver.js +31 -7
- package/dist/utils/fontWeights.d.ts +1 -1
- package/dist/utils/fontWeights.js +1 -1
- package/dist/utils/hexId.d.ts +11 -1
- package/dist/utils/hexId.js +11 -1
- package/dist/utils/paragraphFormattingMerge.js +1 -0
- package/dist/utils/sanitizeImageSrc.d.ts +21 -0
- package/dist/utils/sanitizeImageSrc.js +30 -0
- package/dist/utils/stripXmlDeclarations.d.ts +11 -0
- package/dist/utils/stripXmlDeclarations.js +35 -0
- package/dist/version-comparison.d.ts +22 -2
- package/dist/version-comparison.js +41 -12
- package/dist/watermark/index.d.ts +11 -1
- package/dist/watermark/index.js +22 -1
- 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
|
|
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
|
package/dist/docx/shapeParser.js
CHANGED
|
@@ -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. */
|