@stll/folio-core 0.64.1 → 0.66.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 (62) hide show
  1. package/dist/ai-edits/apply.js +31 -9
  2. package/dist/ai-edits/headless.js +3 -3
  3. package/dist/compare/reproducible-package.d.ts +5 -0
  4. package/dist/compare/reproducible-package.js +10 -23
  5. package/dist/docx/archiveInflation.d.ts +8 -103
  6. package/dist/docx/archiveInflation.js +8 -145
  7. package/dist/docx/attributeRemainder.js +1 -1
  8. package/dist/docx/canonicalStoryRepack.js +2 -2
  9. package/dist/docx/documentSectionFacts.js +1 -1
  10. package/dist/docx/ensureParaIds.js +9 -24
  11. package/dist/docx/importedIdCollisions.js +1 -1
  12. package/dist/docx/mathToMathml.js +1 -1
  13. package/dist/docx/metadataPrivacy.js +38 -48
  14. package/dist/docx/numericIdNormalization.js +2 -2
  15. package/dist/docx/packageParts.d.ts +4 -4
  16. package/dist/docx/packageParts.js +17 -24
  17. package/dist/docx/parser.js +5 -6
  18. package/dist/docx/removeHeaderFooterParts.d.ts +2 -2
  19. package/dist/docx/removeHeaderFooterParts.js +19 -36
  20. package/dist/docx/revisionIdNormalization.js +2 -2
  21. package/dist/docx/rezip.d.ts +7 -7
  22. package/dist/docx/rezip.js +144 -532
  23. package/dist/docx/sdtProperties.js +1 -2
  24. package/dist/docx/selectedXmlIdentityScan.d.ts +1 -1
  25. package/dist/docx/selectedXmlIdentityScan.js +1 -1
  26. package/dist/docx/selectiveSave.js +37 -41
  27. package/dist/docx/selectiveSaveFlags.d.ts +1 -1
  28. package/dist/docx/selectiveSaveTripwire.js +5 -11
  29. package/dist/docx/selectiveXmlPatch.js +2 -2
  30. package/dist/docx/server/applyDocxXmlPatchProposal.js +20 -41
  31. package/dist/docx/server/boundedArchive.d.ts +2 -3
  32. package/dist/docx/server/boundedArchive.js +78 -99
  33. package/dist/docx/server/inspectDocxPackage.d.ts +1 -1
  34. package/dist/docx/server/inspectDocxPackage.js +1 -1
  35. package/dist/docx/server/validateDocxConformance.js +1 -1
  36. package/dist/docx/shapeAlternateContent.js +1 -1
  37. package/dist/docx/storyBlockReplay.js +1 -1
  38. package/dist/docx/unzip.d.ts +2 -3
  39. package/dist/docx/unzip.js +127 -155
  40. package/dist/docx/verbatimCapture.js +1 -1
  41. package/dist/docx/xmlParser.d.ts +5 -36
  42. package/dist/docx/xmlParser.js +14 -78
  43. package/dist/docx/xmlSafety.js +5 -5
  44. package/dist/layout-painter/renderPage.js +4 -2
  45. package/dist/markdown/countParagraphList.d.ts +18 -0
  46. package/dist/markdown/countParagraphList.js +90 -0
  47. package/dist/markdown/renderParagraph.js +2 -62
  48. package/dist/markdown/renderRuns.js +4 -1
  49. package/dist/prosemirror/conversion/fromProseDoc.js +5 -3
  50. package/dist/prosemirror/conversion/toProseDoc.js +4 -1
  51. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +4 -1
  52. package/dist/prosemirror/replacedAnnotations.d.ts +6 -0
  53. package/dist/prosemirror/replacedAnnotations.js +30 -8
  54. package/dist/prosemirror/schema/nodes.d.ts +7 -0
  55. package/dist/prosemirror/textBoxAnchorAttrs.js +9 -1
  56. package/package.json +3 -5
  57. package/dist/docx/streamingXmlParser.d.ts +0 -60
  58. package/dist/docx/streamingXmlParser.js +0 -406
  59. package/dist/docx/xmlNamespaceContext.d.ts +0 -6
  60. package/dist/docx/xmlNamespaceContext.js +0 -53
  61. package/dist/docx/xmlResourceLimits.d.ts +0 -107
  62. package/dist/docx/xmlResourceLimits.js +0 -194
@@ -1,8 +1,8 @@
1
1
  import { decodeText } from "../../utils/textEncoding.js";
2
- import { compressionRatioLimitFor, countCentralDirectoryRecords, createInflationBudget, exceedsCompressionRatio, getZipEntrySizes, inflateEntryWithinLimits } from "../archiveInflation.js";
3
- import { FOLIO_XML_RESOURCE_LIMITS, assertXmlResourceLimits, createXmlPackageBudget } from "../xmlResourceLimits.js";
4
- import { TaggedError } from "better-result";
5
- import JSZip from "jszip";
2
+ import { DOCX_COMPRESSION_RATIO_GRACE_BYTES, isRatioBoundedPart } from "../archiveInflation.js";
3
+ import { Result, TaggedError } from "better-result";
4
+ import { FOLIO_XML_RESOURCE_LIMITS, assertXmlResourceLimits, createXmlPackageBudget } from "@stll/docx-core/xml/limits";
5
+ import { ZipArchive, ZipError } from "@stll/docx-core/zip";
6
6
  // src/docx/server/boundedArchive.ts
7
7
  const DOCX_MAX_ENTRY_BYTES = 134217728;
8
8
  const DOCX_MAX_TOTAL_BYTES = 268435456;
@@ -11,27 +11,42 @@ const DOCX_MAX_INPUT_BYTES = 52428800;
11
11
  const DocxArchiveErrorBase = TaggedError("DocxArchiveError");
12
12
  /** Error raised when a DOCX archive cannot be loaded within configured limits. */
13
13
  var DocxArchiveError = class extends DocxArchiveErrorBase {};
14
- const readLimitError = ({ limit, path, maxEntryBytes, maxTotalBytes, maxCompressionRatio }) => {
15
- switch (limit) {
16
- case "declared-size": return new DocxArchiveError({
17
- message: `DOCX entry "${path}" inflated past its declared size`,
18
- reason: "entry-too-large"
14
+ /** The archive error for a read the ZIP reader refused. */
15
+ const archiveErrorFor = (error, { maxEntryBytes, maxTotalBytes, maxEntries, maxCompressionRatio }) => {
16
+ const entry = error.path === void 0 ? "" : ` "${error.path}"`;
17
+ switch (error.reason) {
18
+ case "too-many-entries": return new DocxArchiveError({
19
+ message: `DOCX archive holds more than ${maxEntries} entries`,
20
+ reason: "too-many-entries"
19
21
  });
20
- case "compression-ratio": return new DocxArchiveError({
21
- message: `DOCX entry "${path}" exceeded the ${maxCompressionRatio}:1 compression ratio limit`,
22
- reason: "compression-ratio-exceeded"
22
+ case "entry-too-large": return new DocxArchiveError({
23
+ message: `DOCX entry${entry} declares more than ${maxEntryBytes} bytes`,
24
+ reason: "entry-too-large"
23
25
  });
24
- case "entry": return new DocxArchiveError({
25
- message: `DOCX entry "${path}" exceeded the ${maxEntryBytes}-byte limit`,
26
+ case "declared-size-exceeded": return new DocxArchiveError({
27
+ message: `DOCX entry${entry} inflated past its declared size`,
26
28
  reason: "entry-too-large"
27
29
  });
28
- case "total":
29
- case "aborted": return new DocxArchiveError({
30
- message: `DOCX archive exceeded the ${maxTotalBytes}-byte cumulative limit while reading "${path}"`,
30
+ case "total-too-large": return new DocxArchiveError({
31
+ message: `DOCX archive declares more than ${maxTotalBytes} cumulative bytes`,
31
32
  reason: "total-too-large"
32
33
  });
34
+ case "compression-ratio": return new DocxArchiveError({
35
+ message: error.path === void 0 ? `DOCX archive declares more than ${maxCompressionRatio} bytes per archive byte` : `DOCX entry${entry} declares more than ${maxCompressionRatio} bytes per compressed byte`,
36
+ reason: "compression-ratio-exceeded"
37
+ });
38
+ case "malformed":
39
+ case "unsupported":
40
+ case "invalid-entry-name":
41
+ case "duplicate-entry":
42
+ case "corrupt-entry": return new DocxArchiveError({
43
+ message: "Failed to parse DOCX archive",
44
+ reason: "load-failed",
45
+ cause: error
46
+ });
33
47
  }
34
48
  };
49
+ const toArchiveError = (cause, limits) => cause instanceof ZipError ? archiveErrorFor(cause, limits) : cause;
35
50
  const resolveByteLimit = ({ value, fallback, name }) => {
36
51
  const limit = value ?? fallback;
37
52
  if (!Number.isSafeInteger(limit) || limit < 0) throw new DocxArchiveError({
@@ -40,7 +55,6 @@ const resolveByteLimit = ({ value, fallback, name }) => {
40
55
  });
41
56
  return limit;
42
57
  };
43
- const asBytes = (bytes) => bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
44
58
  const loadDocxArchive = async (bytes, options = {}) => {
45
59
  const maxInputBytes = resolveByteLimit({
46
60
  value: options.maxInputBytes,
@@ -71,103 +85,68 @@ const loadDocxArchive = async (bytes, options = {}) => {
71
85
  message: `DOCX input contains ${bytes.byteLength} bytes (max ${maxInputBytes})`,
72
86
  reason: "input-too-large"
73
87
  });
74
- if (countCentralDirectoryRecords(asBytes(bytes), maxEntries) > maxEntries) throw new DocxArchiveError({
75
- message: `DOCX archive holds more than ${maxEntries} entries`,
76
- reason: "too-many-entries"
77
- });
78
- let zip;
79
- try {
80
- zip = await JSZip.loadAsync(bytes);
81
- } catch (cause) {
82
- throw new DocxArchiveError({
83
- message: "Failed to parse DOCX archive",
84
- reason: "load-failed",
85
- cause
86
- });
87
- }
88
- const archiveEntries = Object.values(zip.files);
89
- if (archiveEntries.length > maxEntries) throw new DocxArchiveError({
90
- message: `DOCX archive declares ${archiveEntries.length} entries (max ${maxEntries})`,
91
- reason: "too-many-entries"
92
- });
93
- let declaredTotalBytes = 0;
94
- let declaredTotalKnown = true;
95
- for (const entry of archiveEntries) {
96
- if (entry.dir) continue;
97
- const { compressedBytes, uncompressedBytes } = getZipEntrySizes(entry);
98
- if (uncompressedBytes === null) {
99
- declaredTotalKnown = false;
100
- continue;
101
- }
102
- if (uncompressedBytes > maxEntryBytes) throw new DocxArchiveError({
103
- message: `DOCX entry "${entry.name}" declares ${uncompressedBytes} bytes (max ${maxEntryBytes})`,
104
- reason: "entry-too-large"
105
- });
106
- if (exceedsCompressionRatio({
107
- inflatedBytes: uncompressedBytes,
108
- compressedBytes,
109
- maxRatio: compressionRatioLimitFor(entry.name, maxCompressionRatio)
110
- })) throw new DocxArchiveError({
111
- message: `DOCX entry "${entry.name}" declares more than ${maxCompressionRatio} bytes per compressed byte`,
112
- reason: "compression-ratio-exceeded"
113
- });
114
- declaredTotalBytes += uncompressedBytes;
115
- }
116
- if (declaredTotalKnown && declaredTotalBytes > maxTotalBytes) throw new DocxArchiveError({
117
- message: `DOCX archive declares ${declaredTotalBytes} cumulative bytes (max ${maxTotalBytes})`,
118
- reason: "total-too-large"
119
- });
120
- if (exceedsCompressionRatio({
121
- inflatedBytes: declaredTotalBytes,
122
- compressedBytes: bytes.byteLength,
123
- maxRatio: maxCompressionRatio
124
- })) throw new DocxArchiveError({
125
- message: `DOCX archive declares more than ${maxCompressionRatio} bytes per archive byte`,
126
- reason: "compression-ratio-exceeded"
88
+ const limits = {
89
+ maxEntryBytes,
90
+ maxTotalBytes,
91
+ maxEntries,
92
+ maxCompressionRatio
93
+ };
94
+ const read = await Result.tryPromise({
95
+ try: async () => await ZipArchive.read(bytes, {
96
+ limits: {
97
+ maxEntries,
98
+ maxEntryBytes,
99
+ maxTotalBytes,
100
+ maxCompressionRatio,
101
+ compressionRatioGraceBytes: DOCX_COMPRESSION_RATIO_GRACE_BYTES
102
+ },
103
+ isCompressionRatioExempt: (path) => !isRatioBoundedPart(path)
104
+ }),
105
+ catch: (cause) => toArchiveError(cause, limits)
127
106
  });
107
+ if (Result.isError(read)) throw read.error;
108
+ const zip = read.value;
109
+ const archiveEntries = zip.entries();
128
110
  const xmlLimits = {
129
111
  ...FOLIO_XML_RESOURCE_LIMITS,
130
112
  ...options.xmlLimits
131
113
  };
132
114
  const xmlBudget = createXmlPackageBudget();
133
115
  const countedParts = /* @__PURE__ */ new Set();
134
- const inflationBudget = createInflationBudget(maxTotalBytes);
135
- let readChain = Promise.resolve();
116
+ let readBytes = 0;
136
117
  const readEntry = async (path, readOptions = {}) => {
137
118
  const requestedMaxBytes = resolveByteLimit({
138
119
  value: readOptions.maxBytes,
139
120
  fallback: maxEntryBytes,
140
121
  name: "DOCX entry read byte limit"
141
122
  });
142
- const work = async () => {
143
- const entry = zip.file(path);
144
- if (!entry) return null;
145
- const entryLimit = Math.min(requestedMaxBytes, maxEntryBytes);
146
- const result = await inflateEntryWithinLimits({
147
- entry,
148
- maxEntryBytes: entryLimit,
149
- maxCompressionRatio: compressionRatioLimitFor(path, maxCompressionRatio),
150
- budget: inflationBudget
151
- });
152
- if (!result.ok) throw readLimitError({
153
- limit: result.limit,
154
- path,
155
- maxEntryBytes: entryLimit,
156
- maxTotalBytes,
157
- maxCompressionRatio
158
- });
159
- return result.bytes;
160
- };
161
- const next = readChain.then(work, work);
162
- readChain = next.then(() => void 0, () => void 0);
163
- return await next;
123
+ const entry = zip.entry(path);
124
+ if (!entry) return null;
125
+ const entryLimit = Math.min(requestedMaxBytes, maxEntryBytes);
126
+ if (entry.size > entryLimit) throw new DocxArchiveError({
127
+ message: `DOCX entry "${path}" exceeded the ${entryLimit}-byte limit`,
128
+ reason: "entry-too-large"
129
+ });
130
+ if (readBytes + entry.size > maxTotalBytes) throw new DocxArchiveError({
131
+ message: `DOCX archive exceeded the ${maxTotalBytes}-byte cumulative limit while reading "${path}"`,
132
+ reason: "total-too-large"
133
+ });
134
+ readBytes += entry.size;
135
+ const content = await Result.tryPromise({
136
+ try: async () => await entry.bytes(),
137
+ catch: (cause) => toArchiveError(cause, limits)
138
+ });
139
+ if (Result.isError(content)) {
140
+ readBytes -= entry.size;
141
+ throw content.error;
142
+ }
143
+ return content.value;
164
144
  };
165
145
  return {
166
- entries: Object.freeze(archiveEntries.map(({ name }) => name)),
146
+ entries: Object.freeze(archiveEntries.map(({ path }) => path)),
167
147
  entryMetadata: Object.freeze(archiveEntries.map((entry) => ({
168
- path: entry.name,
169
- directory: entry.dir,
170
- declaredUncompressedBytes: getZipEntrySizes(entry).uncompressedBytes
148
+ path: entry.path,
149
+ declaredUncompressedBytes: entry.size
171
150
  }))),
172
151
  async readEntryString(path) {
173
152
  const content = await readEntry(path);
@@ -30,7 +30,7 @@ export type FolioDocxPackagePart = {
30
30
  readonly path: string;
31
31
  readonly kind: FolioDocxPackagePartKind;
32
32
  readonly contentType: string | null;
33
- readonly declaredUncompressedBytes: number | null;
33
+ readonly declaredUncompressedBytes: number;
34
34
  };
35
35
  export type FolioDocxInspectedXmlPart = {
36
36
  readonly path: string;
@@ -200,7 +200,7 @@ const inspectDocxPackage = async (bytes, options = {}) => {
200
200
  const archive = await loadDocxArchive(bytes, options.archive);
201
201
  const contentTypesBytes = await archive.readEntryUint8(CONTENT_TYPES_PATH, { maxBytes: CONTENT_TYPES_READ_LIMIT });
202
202
  const declarations = parseContentTypes(contentTypesBytes === null ? null : decodeXml(contentTypesBytes, CONTENT_TYPES_PATH));
203
- const parts = archive.entryMetadata.filter(({ directory }) => !directory).map((entry) => toPackagePart(entry, declarations)).toSorted(({ path: left }, { path: right }) => comparePackagePaths(left, right));
203
+ const parts = archive.entryMetadata.map((entry) => toPackagePart(entry, declarations)).toSorted(({ path: left }, { path: right }) => comparePackagePaths(left, right));
204
204
  const partsByPath = new Map(parts.map((part) => [part.path, part]));
205
205
  validateRequestedParts({
206
206
  requestedPaths,
@@ -4,10 +4,10 @@ import { DOCX_CONTAINER_TYPES, detectDocxContainerType } from "../encryption/con
4
4
  import { DocxModelValidationError, validateFolioDocumentModel } from "../modelValidation.js";
5
5
  import { DocxParseError, parseDocx } from "../parser.js";
6
6
  import { findChildByLocalName, getAttributeAnyPrefix, getLocalName, getNamespacePrefix, parseXmlDocument } from "../xmlParser.js";
7
- import { XmlResourceLimitError } from "../xmlResourceLimits.js";
8
7
  import { getDocxXmlSafetyIssue } from "../xmlSafety.js";
9
8
  import { DocxArchiveError, loadDocxArchive } from "./boundedArchive.js";
10
9
  import { DOCX_CONFORMANCE_CLASSES } from "@stll/docx-core/model";
10
+ import { XmlResourceLimitError } from "@stll/docx-core/xml/limits";
11
11
  // src/docx/server/validateDocxConformance.ts
12
12
  const FOLIO_DOCX_CONFORMANCE_REPORT_VERSION = 1;
13
13
  const FOLIO_DOCX_CONFORMANCE_PROFILE = "folio-supported-v1";
@@ -1,8 +1,8 @@
1
1
  import { canonicalJson } from "../utils/canonicalJson.js";
2
2
  import { captureVerbatimXml, isSafeCapturedXmlDocument } from "./verbatimCapture.js";
3
3
  import { NAMESPACES, WORDPROCESSINGML_NAMESPACE_URIS, cloneWithXmlnsDeclarations, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, parseXmlDocument } from "./xmlParser.js";
4
- import { assertXmlResourceLimits } from "./xmlResourceLimits.js";
5
4
  import { panic } from "better-result";
5
+ import { assertXmlResourceLimits } from "@stll/docx-core/xml/limits";
6
6
  // src/docx/shapeAlternateContent.ts
7
7
  /**
8
8
  * Every modeled field, text body included: the Fallback repeats the shape's
@@ -1,11 +1,11 @@
1
1
  import { canonicalJson } from "../utils/canonicalJson.js";
2
2
  import { spliceXml } from "./selectiveXmlPatch.js";
3
- import { getXmlSourceRange, parseStreamingXmlWithSourceRanges } from "./streamingXmlParser.js";
4
3
  import { TRANSITIONAL_NAMESPACE_BY_STRICT_URI } from "./strictValueEncodings.gen.js";
5
4
  import { captureSourceProfileXml, isSafeCapturedXmlDocument } from "./verbatimCapture.js";
6
5
  import { WORDPROCESSINGML_NAMESPACE_URIS, getAttributeByNamespaceUri, getChildElements, getLocalName, getNamespaceUri } from "./xmlParser.js";
7
6
  import { escapeXmlAttribute } from "@stll/docx-core";
8
7
  import { DOCX_CONFORMANCE_CLASSES } from "@stll/docx-core/model";
8
+ import { getXmlSourceRange, parseStreamingXmlWithSourceRanges } from "@stll/docx-core/xml";
9
9
  // src/docx/storyBlockReplay.ts
10
10
  const BLOCK_ELEMENT_NAMES = {
11
11
  paragraph: "p",
@@ -1,4 +1,4 @@
1
- import JSZip from "jszip";
1
+ import { ZipArchive } from "@stll/docx-core/zip";
2
2
  export declare class DocxSecurityError extends Error {
3
3
  constructor(message: string);
4
4
  }
@@ -75,7 +75,7 @@ export type RawDocxContent = {
75
75
  media: Map<string, ArrayBuffer>;
76
76
  fonts: Map<string, ArrayBuffer>;
77
77
  allXml: Map<string, string>;
78
- originalZip: JSZip;
78
+ originalZip: ZipArchive;
79
79
  originalBuffer: ArrayBuffer;
80
80
  warnings: string[];
81
81
  /** True when the input was a password-protected CFB container. */
@@ -100,7 +100,6 @@ export type UnzipDocxBehavior = {
100
100
  export declare function unzipDocx(buffer: ArrayBuffer, options?: DocxUnzipOptions, { verifyUnreadEntries }?: UnzipDocxBehavior): Promise<RawDocxContent>;
101
101
  /** Keep extracted projections and the source ZIP aligned after parser normalization. */
102
102
  export declare const replaceRawDocxXmlParts: (content: RawDocxContent, parts: ReadonlyMap<string, string>) => Promise<void>;
103
- export declare function getEntryUncompressedSize(file: JSZip.JSZipObject): number | null;
104
103
  /**
105
104
  * Get a list of all files in the DOCX
106
105
  *