@stll/folio-core 0.12.0 → 0.14.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 (138) hide show
  1. package/dist/ai-edits/apply.d.ts +2 -1
  2. package/dist/ai-edits/apply.js +118 -40
  3. package/dist/ai-edits/blockRange.js +1 -1
  4. package/dist/ai-edits/clean-text.js +2 -1
  5. package/dist/ai-edits/headless.js +3 -3
  6. package/dist/ai-edits/snapshot.js +21 -3
  7. package/dist/ai-edits/table-cell-mutations.js +2 -1
  8. package/dist/ai-edits/table-row-column-mutations.d.ts +9 -1
  9. package/dist/ai-edits/table-row-column-mutations.js +1 -1
  10. package/dist/ai-edits/types.d.ts +28 -1
  11. package/dist/ai-edits/word-diff.js +21 -0
  12. package/dist/compat/eigenpal.d.ts +3 -3
  13. package/dist/compat/eigenpal.js +3 -3
  14. package/dist/controller/headerFooterEditorManager.js +1 -1
  15. package/dist/controller/hiddenEditorManager.js +12 -1
  16. package/dist/controller/layoutPipeline.js +1 -1
  17. package/dist/controller/noteEditorManager.js +1 -1
  18. package/dist/document-operations.d.ts +13 -13
  19. package/dist/document-operations.js +31 -21
  20. package/dist/docx/blockContentParser.js +19 -5
  21. package/dist/docx/commentRangeIntegrity.d.ts +6 -0
  22. package/dist/docx/commentRangeIntegrity.js +186 -0
  23. package/dist/docx/conformance.js +57 -3
  24. package/dist/docx/encryption/agileDecryption.js +12 -2
  25. package/dist/docx/encryption/compoundFile.js +6 -1
  26. package/dist/docx/encryption/encryptionInfo.d.ts +9 -1
  27. package/dist/docx/encryption/encryptionInfo.js +18 -2
  28. package/dist/docx/encryption/openEncryptedDocx.js +1 -1
  29. package/dist/docx/ensureParaIds.js +7 -6
  30. package/dist/docx/groupDrawingParser.js +10 -5
  31. package/dist/docx/hyperlinkParser.js +1 -1
  32. package/dist/docx/imageParser.js +10 -6
  33. package/dist/docx/metadataPrivacy.js +17 -3
  34. package/dist/docx/paragraphParser.js +7 -3
  35. package/dist/docx/rezip.js +19 -8
  36. package/dist/docx/runParser.js +13 -4
  37. package/dist/docx/sectionParser.js +17 -4
  38. package/dist/docx/selectiveSave.js +4 -0
  39. package/dist/docx/selectiveXmlPatch.d.ts +22 -1
  40. package/dist/docx/selectiveXmlPatch.js +61 -1
  41. package/dist/docx/serializer/blockSdtSerializer.js +3 -2
  42. package/dist/docx/serializer/commentSerializer.js +4 -4
  43. package/dist/docx/serializer/paragraphSerializer.js +16 -14
  44. package/dist/docx/serializer/runSerializer.js +21 -19
  45. package/dist/docx/serializer/sectionPropertiesSerializer.js +2 -1
  46. package/dist/docx/serializer/tableSerializer.js +9 -7
  47. package/dist/docx/serializer/xmlUtils.d.ts +13 -1
  48. package/dist/docx/serializer/xmlUtils.js +27 -1
  49. package/dist/docx/server/docxToMarkdown.d.ts +15 -0
  50. package/dist/docx/server/docxToMarkdown.js +17 -0
  51. package/dist/docx/server/extractDocxText.js +55 -8
  52. package/dist/docx/settingsParser.js +15 -2
  53. package/dist/docx/tableParser.d.ts +8 -1
  54. package/dist/docx/tableParser.js +29 -8
  55. package/dist/docx/unzip.js +1 -0
  56. package/dist/docx/vmlImageParser.js +11 -3
  57. package/dist/docx/xmlParser.js +11 -1
  58. package/dist/fonts/embeddedFonts.d.ts +33 -5
  59. package/dist/fonts/embeddedFonts.js +37 -8
  60. package/dist/index.d.ts +3 -3
  61. package/dist/index.js +3 -3
  62. package/dist/layout-bridge/convert/toFlowBlocks.js +15 -0
  63. package/dist/layout-engine/index.js +19 -12
  64. package/dist/layout-engine/measure/cache.js +2 -0
  65. package/dist/layout-engine/measure/effectiveLineBreakPolicy.js +8 -2
  66. package/dist/layout-engine/measure/floatingTablePosition.d.ts +9 -0
  67. package/dist/layout-engine/measure/floatingTablePosition.js +17 -0
  68. package/dist/layout-engine/measure/index.d.ts +2 -1
  69. package/dist/layout-engine/measure/index.js +2 -1
  70. package/dist/layout-engine/measure/lineBreakProvider.d.ts +2 -2
  71. package/dist/layout-engine/measure/lineBreakProvider.js +3 -3
  72. package/dist/layout-engine/measure/measureBlocks.js +11 -8
  73. package/dist/layout-engine/measure/measureHelpers.d.ts +10 -1
  74. package/dist/layout-engine/measure/measureHelpers.js +12 -1
  75. package/dist/layout-engine/measure/measureParagraph.js +16 -0
  76. package/dist/layout-engine/types.d.ts +22 -2
  77. package/dist/layout-painter/renderImage.d.ts +12 -1
  78. package/dist/layout-painter/renderImage.js +21 -4
  79. package/dist/layout-painter/renderPage.js +5 -8
  80. package/dist/layout-painter/renderParagraph.js +33 -9
  81. package/dist/layout-painter/renderTable.js +2 -1
  82. package/dist/layout-painter/renderWatermark.d.ts +1 -1
  83. package/dist/layout-painter/renderWatermark.js +2 -1
  84. package/dist/markdown/fromMarkdown.js +1 -1
  85. package/dist/prosemirror/attrs/index.d.ts +1 -1
  86. package/dist/prosemirror/attrs/index.js +21 -0
  87. package/dist/prosemirror/commands/comments.d.ts +76 -1
  88. package/dist/prosemirror/commands/comments.js +401 -28
  89. package/dist/prosemirror/commands/hyperlink.js +15 -1
  90. package/dist/prosemirror/commands/image.js +7 -1
  91. package/dist/prosemirror/commands/index.d.ts +2 -2
  92. package/dist/prosemirror/commands/index.js +2 -2
  93. package/dist/prosemirror/commands/tableCellMergeResolution.js +1 -1
  94. package/dist/prosemirror/commentIdAllocator.js +13 -3
  95. package/dist/prosemirror/conversion/fromProseDoc.js +122 -5
  96. package/dist/prosemirror/conversion/index.js +1 -1
  97. package/dist/prosemirror/conversion/toProseDoc.js +20 -8
  98. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -1
  99. package/dist/prosemirror/extensions/features/ImagePasteExtension.js +10 -2
  100. package/dist/prosemirror/extensions/features/pasteCleanup.js +15 -2
  101. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +30 -4
  102. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +49 -13
  103. package/dist/prosemirror/extensions/nodes/TableExtension.d.ts +1 -1
  104. package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -3
  105. package/dist/prosemirror/index.d.ts +1 -1
  106. package/dist/prosemirror/index.js +1 -1
  107. package/dist/prosemirror/plugins/contentControlWidgets.d.ts +2 -2
  108. package/dist/prosemirror/plugins/contentControlWidgets.js +4 -4
  109. package/dist/prosemirror/plugins/revisionIds.d.ts +23 -0
  110. package/dist/prosemirror/plugins/revisionIds.js +57 -0
  111. package/dist/prosemirror/plugins/suggestionMode.js +4 -3
  112. package/dist/prosemirror/runFormattingMarkNames.d.ts +14 -0
  113. package/dist/prosemirror/runFormattingMarkNames.js +40 -0
  114. package/dist/prosemirror/schema/index.d.ts +1 -1
  115. package/dist/prosemirror/schema/marks.d.ts +27 -4
  116. package/dist/prosemirror/schema/marks.js +15 -0
  117. package/dist/prosemirror/schema/nodes.d.ts +50 -2
  118. package/dist/prosemirror/utils/tabCalculator.js +3 -3
  119. package/dist/render-dom/RemoteSelectionOverlay.js +2 -2
  120. package/dist/server.d.ts +3 -2
  121. package/dist/server.js +4 -3
  122. package/dist/utils/clipboard.d.ts +10 -1
  123. package/dist/utils/clipboard.js +33 -6
  124. package/dist/utils/colorResolver.d.ts +8 -1
  125. package/dist/utils/colorResolver.js +35 -5
  126. package/dist/utils/fontResolver.d.ts +2 -1
  127. package/dist/utils/fontResolver.js +9 -3
  128. package/dist/utils/hexId.d.ts +11 -1
  129. package/dist/utils/hexId.js +11 -1
  130. package/dist/utils/sanitizeImageSrc.d.ts +21 -0
  131. package/dist/utils/sanitizeImageSrc.js +30 -0
  132. package/dist/utils/stripXmlDeclarations.d.ts +11 -0
  133. package/dist/utils/stripXmlDeclarations.js +35 -0
  134. package/dist/version-comparison.d.ts +22 -2
  135. package/dist/version-comparison.js +41 -12
  136. package/dist/watermark/index.d.ts +11 -1
  137. package/dist/watermark/index.js +22 -1
  138. package/package.json +3 -2
@@ -25,6 +25,16 @@ import { EditorView } from "prosemirror-view";
25
25
  const normalizeSeedState = (state) => ensureBaseDirectionInState(ensureParaIdsInState(state));
26
26
  const normalizeLocalSeedState = (state) => ensureBaseDirectionInState(state);
27
27
  const isObjectRecord = (value) => typeof value === "object" && value !== null;
28
+ /**
29
+ * A remote collaborator's awareness `user.color` is read from another
30
+ * peer's Yjs awareness state — effectively untrusted network input — and
31
+ * gets fed straight into `element.style.background`/`backgroundColor` (see
32
+ * `RemoteSelectionOverlay`). Restrict it to a plain 6-digit hex color before
33
+ * accepting it so a crafted value (e.g. a CSS `url(...)` payload) can't ride
34
+ * along as a CSS-injection / beacon vector; anything else falls back to the
35
+ * default selection color below.
36
+ */
37
+ const AWARENESS_HEX_COLOR_PATTERN = /^#[0-9A-Fa-f]{6}$/u;
28
38
  const isYSyncState = (value, yjs) => {
29
39
  if (!isObjectRecord(value)) return false;
30
40
  const binding = value["binding"];
@@ -48,8 +58,9 @@ const readAwarenessUser = (state, clientId) => {
48
58
  name: `User ${clientId}`
49
59
  };
50
60
  const user = state["user"];
61
+ const rawColor = user["color"];
51
62
  return {
52
- color: typeof user["color"] === "string" ? user["color"] : "var(--doc-image-selection)",
63
+ color: typeof rawColor === "string" && AWARENESS_HEX_COLOR_PATTERN.test(rawColor) ? rawColor : "var(--doc-image-selection)",
53
64
  name: typeof user["name"] === "string" ? user["name"] : `User ${clientId}`
54
65
  };
55
66
  };
@@ -1,3 +1,4 @@
1
+ import { getDocumentWatermark } from "../watermark/index.js";
1
2
  import "../layout-engine/types.js";
2
3
  import { resolveEffectiveParagraphSpacingTree } from "../layout-engine/paragraphSpacing.js";
3
4
  import { renderPages } from "../layout-painter/renderPage.js";
@@ -20,7 +21,6 @@ import { resolveJustificationCompatibility } from "../layout-engine/justificatio
20
21
  import { measureBlocks, measureSingleBlockWithoutFloatingZones } from "../layout-engine/measure/measureBlocks.js";
21
22
  import { tryBuildIncrementalMeasures } from "../paged-layout/incrementalMeasure.js";
22
23
  import { computePerBlockMeasureInputs } from "../paged-layout/sectionBlockWidths.js";
23
- import { getDocumentWatermark } from "../watermark/index.js";
24
24
  //#region src/controller/layoutPipeline.ts
25
25
  function bodyMarginsClearHeaderFooter({ authoredMargins, preparedHeader, preparedFooter }) {
26
26
  const headerBottom = preparedHeader ? (authoredMargins.header ?? 0) + (preparedHeader.marginPushBottom ?? preparedHeader.height) : authoredMargins.top;
@@ -1,4 +1,3 @@
1
- import { proseDocToBlocks } from "../prosemirror/conversion/fromProseDoc.js";
2
1
  import { ExtensionManager } from "../prosemirror/extensions/ExtensionManager.js";
3
2
  import { ensureBaseDirectionInState } from "../prosemirror/extensions/features/AutoBidiDetectionExtension.js";
4
3
  import { createDocumentStylesPlugin } from "../prosemirror/plugins/documentStyles.js";
@@ -6,6 +5,7 @@ import { ensureParaIdsInState } from "../prosemirror/extensions/features/ParaIdA
6
5
  import { createStarterKit } from "../prosemirror/extensions/StarterKit.js";
7
6
  import { schema } from "../prosemirror/schema/index.js";
8
7
  import { footnoteToProseDoc } from "../prosemirror/conversion/toProseDoc.js";
8
+ import { proseDocToBlocks } from "../prosemirror/conversion/fromProseDoc.js";
9
9
  import { isSeparatorEndnote, isSeparatorFootnote } from "../docx/footnoteParser.js";
10
10
  import { EditorState } from "prosemirror-state";
11
11
  import { EditorView } from "prosemirror-view";
@@ -4,7 +4,7 @@ import { FolioAIEditView } from "./ai-edits/apply.js";
4
4
  //#region src/document-operations.d.ts
5
5
  declare const FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION: 1;
6
6
  declare const FOLIO_DOCUMENT_OPERATION_TYPES: readonly ["replaceInBlock", "replaceRange", "commentOnRange", "formatRange", "insertAfterBlock", "insertBeforeBlock", "replaceBlock", "deleteBlock", "commentOnBlock", "insertSignatureTable", "insertTableRow", "deleteTableRow", "insertTableColumn", "deleteTableColumn", "mergeTableCells", "splitTableCell"];
7
- declare const FOLIO_DOCUMENT_OPERATION_MODES: readonly ["direct", "tracked-changes"];
7
+ declare const FOLIO_DOCUMENT_OPERATION_MODES: readonly ["direct", "tracked-changes", "suggested"];
8
8
  declare const FOLIO_DOCUMENT_OPERATION_STORIES: readonly ["main", "header", "footer", "footnote", "endnote"];
9
9
  declare const FOLIO_DOCUMENT_OPERATION_PRECONDITIONS: readonly ["blockTextHash"];
10
10
  declare const FOLIO_DOCUMENT_OPERATION_BATCH_MODES: readonly ["best-effort", "atomic"];
@@ -13,20 +13,20 @@ type FolioDocumentOperationMode = FolioAIEditApplyMode;
13
13
  type FolioDocumentOperationPrecondition = FolioAIEditPrecondition;
14
14
  type FolioDocumentOperationType = FolioDocumentOperation["type"];
15
15
  declare const FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE: Readonly<{
16
- readonly replaceInBlock: readonly ["direct", "tracked-changes"];
17
- readonly replaceRange: readonly ["direct", "tracked-changes"];
16
+ readonly replaceInBlock: readonly ["direct", "tracked-changes", "suggested"];
17
+ readonly replaceRange: readonly ["direct", "tracked-changes", "suggested"];
18
18
  readonly commentOnRange: readonly ["direct", "tracked-changes"];
19
- readonly formatRange: readonly ["direct", "tracked-changes"];
20
- readonly insertAfterBlock: readonly ["direct", "tracked-changes"];
21
- readonly insertBeforeBlock: readonly ["direct", "tracked-changes"];
22
- readonly replaceBlock: readonly ["direct", "tracked-changes"];
23
- readonly deleteBlock: readonly ["direct", "tracked-changes"];
19
+ readonly formatRange: readonly ["direct", "tracked-changes", "suggested"];
20
+ readonly insertAfterBlock: readonly ["direct", "tracked-changes", "suggested"];
21
+ readonly insertBeforeBlock: readonly ["direct", "tracked-changes", "suggested"];
22
+ readonly replaceBlock: readonly ["direct", "tracked-changes", "suggested"];
23
+ readonly deleteBlock: readonly ["direct", "tracked-changes", "suggested"];
24
24
  readonly commentOnBlock: readonly ["direct", "tracked-changes"];
25
- readonly insertSignatureTable: readonly ["direct"];
26
- readonly insertTableRow: readonly ["direct", "tracked-changes"];
27
- readonly deleteTableRow: readonly ["direct", "tracked-changes"];
28
- readonly insertTableColumn: readonly ["direct", "tracked-changes"];
29
- readonly deleteTableColumn: readonly ["direct", "tracked-changes"];
25
+ readonly insertSignatureTable: readonly ["direct", "suggested"];
26
+ readonly insertTableRow: readonly ["direct", "tracked-changes", "suggested"];
27
+ readonly deleteTableRow: readonly ["direct", "tracked-changes", "suggested"];
28
+ readonly insertTableColumn: readonly ["direct", "tracked-changes", "suggested"];
29
+ readonly deleteTableColumn: readonly ["direct", "tracked-changes", "suggested"];
30
30
  readonly mergeTableCells: readonly ["direct", "tracked-changes"];
31
31
  readonly splitTableCell: readonly ["direct", "tracked-changes"];
32
32
  }>;
@@ -20,7 +20,11 @@ const FOLIO_DOCUMENT_OPERATION_TYPES = Object.freeze([
20
20
  "mergeTableCells",
21
21
  "splitTableCell"
22
22
  ]);
23
- const FOLIO_DOCUMENT_OPERATION_MODES = Object.freeze(["direct", "tracked-changes"]);
23
+ const FOLIO_DOCUMENT_OPERATION_MODES = Object.freeze([
24
+ "direct",
25
+ "tracked-changes",
26
+ "suggested"
27
+ ]);
24
28
  const FOLIO_DOCUMENT_OPERATION_STORIES = Object.freeze([
25
29
  "main",
26
30
  "header",
@@ -30,23 +34,24 @@ const FOLIO_DOCUMENT_OPERATION_STORIES = Object.freeze([
30
34
  ]);
31
35
  const FOLIO_DOCUMENT_OPERATION_PRECONDITIONS = Object.freeze(["blockTextHash"]);
32
36
  const FOLIO_DOCUMENT_OPERATION_BATCH_MODES = Object.freeze(["best-effort", "atomic"]);
33
- const DIRECT_AND_TRACKED_MODES = FOLIO_DOCUMENT_OPERATION_MODES;
34
- const DIRECT_ONLY_MODES = Object.freeze(["direct"]);
37
+ const DIRECT_AND_TRACKED_MODES = Object.freeze(["direct", "tracked-changes"]);
38
+ const DIRECT_TRACKED_AND_SUGGESTED_MODES = FOLIO_DOCUMENT_OPERATION_MODES;
39
+ const DIRECT_AND_SUGGESTED_MODES = Object.freeze(["direct", "suggested"]);
35
40
  const FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE = Object.freeze({
36
- replaceInBlock: DIRECT_AND_TRACKED_MODES,
37
- replaceRange: DIRECT_AND_TRACKED_MODES,
41
+ replaceInBlock: DIRECT_TRACKED_AND_SUGGESTED_MODES,
42
+ replaceRange: DIRECT_TRACKED_AND_SUGGESTED_MODES,
38
43
  commentOnRange: DIRECT_AND_TRACKED_MODES,
39
- formatRange: DIRECT_AND_TRACKED_MODES,
40
- insertAfterBlock: DIRECT_AND_TRACKED_MODES,
41
- insertBeforeBlock: DIRECT_AND_TRACKED_MODES,
42
- replaceBlock: DIRECT_AND_TRACKED_MODES,
43
- deleteBlock: DIRECT_AND_TRACKED_MODES,
44
+ formatRange: DIRECT_TRACKED_AND_SUGGESTED_MODES,
45
+ insertAfterBlock: DIRECT_TRACKED_AND_SUGGESTED_MODES,
46
+ insertBeforeBlock: DIRECT_TRACKED_AND_SUGGESTED_MODES,
47
+ replaceBlock: DIRECT_TRACKED_AND_SUGGESTED_MODES,
48
+ deleteBlock: DIRECT_TRACKED_AND_SUGGESTED_MODES,
44
49
  commentOnBlock: DIRECT_AND_TRACKED_MODES,
45
- insertSignatureTable: DIRECT_ONLY_MODES,
46
- insertTableRow: DIRECT_AND_TRACKED_MODES,
47
- deleteTableRow: DIRECT_AND_TRACKED_MODES,
48
- insertTableColumn: DIRECT_AND_TRACKED_MODES,
49
- deleteTableColumn: DIRECT_AND_TRACKED_MODES,
50
+ insertSignatureTable: DIRECT_AND_SUGGESTED_MODES,
51
+ insertTableRow: DIRECT_TRACKED_AND_SUGGESTED_MODES,
52
+ deleteTableRow: DIRECT_TRACKED_AND_SUGGESTED_MODES,
53
+ insertTableColumn: DIRECT_TRACKED_AND_SUGGESTED_MODES,
54
+ deleteTableColumn: DIRECT_TRACKED_AND_SUGGESTED_MODES,
50
55
  mergeTableCells: DIRECT_AND_TRACKED_MODES,
51
56
  splitTableCell: DIRECT_AND_TRACKED_MODES
52
57
  });
@@ -63,8 +68,8 @@ const DOCUMENT_OPERATION_CAPABILITIES = Object.freeze({
63
68
  const getFolioDocumentOperationCapabilities = () => DOCUMENT_OPERATION_CAPABILITIES;
64
69
  const includesDocumentOperationMode = (supportedModes, mode) => supportedModes.includes(mode);
65
70
  const isFolioDocumentOperationModeSupported = (operationType, mode) => {
71
+ if (!Object.hasOwn(FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, operationType)) return false;
66
72
  const supportedModes = FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE[operationType];
67
- if (supportedModes === void 0) return false;
68
73
  return includesDocumentOperationMode(supportedModes, mode);
69
74
  };
70
75
  const isSupportedFolioDocumentOperationVersion = (value) => value === 1;
@@ -204,7 +209,8 @@ const COMMON_OPERATION_KEYS = [
204
209
  "blockId",
205
210
  "severity",
206
211
  "area",
207
- "precondition"
212
+ "precondition",
213
+ "suggestionId"
208
214
  ];
209
215
  const parseSignatureParties = (value, path) => {
210
216
  const parties = value["parties"];
@@ -232,10 +238,12 @@ const parseDocumentOperation = (value, index) => {
232
238
  const type = readString(value, "type", path);
233
239
  const reviewMeta = readReviewMeta(value, path);
234
240
  const precondition = readOptionalPrecondition(value, path);
241
+ const suggestionId = readOptionalString(value, "suggestionId", path);
235
242
  const comment = readOptionalComment(value, path);
236
243
  const operationMeta = {
237
244
  ...reviewMeta,
238
- ...precondition !== void 0 && { precondition }
245
+ ...precondition !== void 0 && { precondition },
246
+ ...suggestionId !== void 0 && { suggestionId }
239
247
  };
240
248
  if (type === "replaceRange") {
241
249
  assertAllowedKeys(value, path, [
@@ -246,7 +254,8 @@ const parseDocumentOperation = (value, index) => {
246
254
  "comment",
247
255
  "severity",
248
256
  "area",
249
- "precondition"
257
+ "precondition",
258
+ "suggestionId"
250
259
  ]);
251
260
  return {
252
261
  ...operationMeta,
@@ -284,7 +293,8 @@ const parseDocumentOperation = (value, index) => {
284
293
  "formatting",
285
294
  "severity",
286
295
  "area",
287
- "precondition"
296
+ "precondition",
297
+ "suggestionId"
288
298
  ]);
289
299
  return {
290
300
  ...operationMeta,
@@ -510,7 +520,7 @@ const parseFolioDocumentOperationBatch = (value) => {
510
520
  const operations = value["operations"];
511
521
  if (!Array.isArray(operations)) return invalidBatch("$.operations", "expected an array");
512
522
  const mode = value["mode"];
513
- if (mode !== void 0 && mode !== "direct" && mode !== "tracked-changes") return invalidBatch("$.mode", "expected \"direct\" or \"tracked-changes\" when provided");
523
+ if (mode !== void 0 && mode !== "direct" && mode !== "tracked-changes" && mode !== "suggested") return invalidBatch("$.mode", "expected \"direct\", \"tracked-changes\", or \"suggested\" when provided");
514
524
  const atomic = readOptionalBoolean(value, "atomic", "$");
515
525
  const dryRun = readOptionalBoolean(value, "dryRun", "$");
516
526
  const parsedOperations = operations.map(parseDocumentOperation);
@@ -53,7 +53,7 @@ const formatNumber = (value, numFmt) => {
53
53
  default: return String(value);
54
54
  }
55
55
  };
56
- const computeListMarker = (paragraph, { numbering, listCounters, abstractCounters, restartedNumIds, previousList }) => {
56
+ const computeListMarker = (paragraph, { numbering, listCounters, abstractCounters, restartedNumIds, previousList, siblingNumIdsByAbstractNumId }) => {
57
57
  const listRendering = paragraph.listRendering;
58
58
  if (!listRendering || !numbering) {
59
59
  previousList.abstractNumId = null;
@@ -73,6 +73,14 @@ const computeListMarker = (paragraph, { numbering, listCounters, abstractCounter
73
73
  const counters = listCounters.get(numId);
74
74
  if (!counters) return;
75
75
  const abstractNumId = numbering.getAbstractNumId(numId);
76
+ if (firstEncounter && abstractNumId !== null) {
77
+ let siblingNumIds = siblingNumIdsByAbstractNumId.get(abstractNumId);
78
+ if (!siblingNumIds) {
79
+ siblingNumIds = /* @__PURE__ */ new Set();
80
+ siblingNumIdsByAbstractNumId.set(abstractNumId, siblingNumIds);
81
+ }
82
+ siblingNumIds.add(numId);
83
+ }
76
84
  const styleNumbering = paragraph.formatting?.numPrFromStyle;
77
85
  let resumedAbstractCounters;
78
86
  const resumesRestartedInstance = firstEncounter && listRendering.startOverride === void 0 && abstractNumId !== null && previousList.abstractNumId === abstractNumId && previousList.numId !== null && previousList.numId !== numId && restartedNumIds.has(previousList.numId);
@@ -101,9 +109,13 @@ const computeListMarker = (paragraph, { numbering, listCounters, abstractCounter
101
109
  counters[level + 1] = (childCounter === void 0 || Number.isNaN(childCounter) ? 0 : childCounter) + childAdvances;
102
110
  }
103
111
  if (abstractNumId !== null) {
104
- if (resumedAbstractCounters) for (const [otherNumId, otherCounters] of listCounters) {
105
- if (otherNumId === numId || numbering.getAbstractNumId(otherNumId) !== abstractNumId || !otherCounters.every((value, index) => Object.is(value, resumedAbstractCounters[index]))) continue;
106
- for (let i = 0; i < otherCounters.length; i += 1) otherCounters[i] = counters[i] ?? NaN;
112
+ if (resumedAbstractCounters) {
113
+ const siblingNumIds = siblingNumIdsByAbstractNumId.get(abstractNumId);
114
+ for (const otherNumId of siblingNumIds ?? []) {
115
+ const otherCounters = listCounters.get(otherNumId);
116
+ if (otherNumId === numId || !otherCounters || !otherCounters.every((value, index) => Object.is(value, resumedAbstractCounters[index]))) continue;
117
+ for (let i = 0; i < otherCounters.length; i += 1) otherCounters[i] = counters[i] ?? NaN;
118
+ }
107
119
  }
108
120
  abstractCounters.set(abstractNumId, [...counters]);
109
121
  }
@@ -140,6 +152,7 @@ const parseBlockContent = (parent, styles, theme, numbering, rels, media, option
140
152
  fromStyle: false,
141
153
  numId: null
142
154
  },
155
+ siblingNumIdsByAbstractNumId: /* @__PURE__ */ new Map(),
143
156
  options: {
144
157
  ...options,
145
158
  rootXmlns: mergeXmlnsDeclarations(options?.rootXmlns ?? {}, parent)
@@ -160,7 +173,8 @@ const parseBlockContentWithState = (parent, styles, theme, numbering, rels, medi
160
173
  listCounters: state.listCounters,
161
174
  abstractCounters: state.abstractCounters,
162
175
  restartedNumIds: state.restartedNumIds,
163
- previousList: state.previousList
176
+ previousList: state.previousList,
177
+ siblingNumIdsByAbstractNumId: state.siblingNumIdsByAbstractNumId
164
178
  });
165
179
  content.push(paragraph);
166
180
  continue;
@@ -0,0 +1,6 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+
3
+ //#region src/docx/commentRangeIntegrity.d.ts
4
+ declare const withoutOrphanCommentRanges: (doc: document_d_exports.Document) => document_d_exports.Document;
5
+ //#endregion
6
+ export { withoutOrphanCommentRanges };
@@ -0,0 +1,186 @@
1
+ //#region src/docx/commentRangeIntegrity.ts
2
+ const withoutOrphanCommentRanges = (doc) => {
3
+ const validCommentIds = new Set((doc.package.document.comments ?? []).map((comment) => comment.id));
4
+ const document = withoutOrphanDocumentBodyMarkers(doc.package.document, validCommentIds);
5
+ const headers = withoutOrphanHeaderFooterMapMarkers(doc.package.headers, validCommentIds);
6
+ const footers = withoutOrphanHeaderFooterMapMarkers(doc.package.footers, validCommentIds);
7
+ const footnotes = withoutOrphanNoteMarkers(doc.package.footnotes, validCommentIds);
8
+ const endnotes = withoutOrphanNoteMarkers(doc.package.endnotes, validCommentIds);
9
+ if (!document && !headers && !footers && !footnotes && !endnotes) return doc;
10
+ return {
11
+ ...doc,
12
+ package: {
13
+ ...doc.package,
14
+ ...document ? { document } : {},
15
+ ...headers ? { headers } : {},
16
+ ...footers ? { footers } : {},
17
+ ...footnotes ? { footnotes } : {},
18
+ ...endnotes ? { endnotes } : {}
19
+ }
20
+ };
21
+ };
22
+ const withoutOrphanDocumentBodyMarkers = (document, validCommentIds) => {
23
+ const content = withoutOrphanBlockMarkers(document.content, validCommentIds);
24
+ const comments = withoutOrphanCommentBodyMarkers(document.comments, validCommentIds);
25
+ if (!content && !comments) return null;
26
+ return {
27
+ ...document,
28
+ ...content ? { content } : {},
29
+ ...comments ? { comments } : {}
30
+ };
31
+ };
32
+ const withoutOrphanCommentBodyMarkers = (comments, validCommentIds) => {
33
+ if (!comments) return null;
34
+ let changed = false;
35
+ const next = [];
36
+ for (const comment of comments) {
37
+ const content = withoutOrphanParagraphListMarkers(comment.content, validCommentIds);
38
+ if (!content) {
39
+ next.push(comment);
40
+ continue;
41
+ }
42
+ changed = true;
43
+ next.push({
44
+ ...comment,
45
+ content
46
+ });
47
+ }
48
+ return changed ? next : null;
49
+ };
50
+ const withoutOrphanParagraphListMarkers = (paragraphs, validCommentIds) => {
51
+ let changed = false;
52
+ const next = [];
53
+ for (const paragraph of paragraphs) {
54
+ const nextParagraph = withoutOrphanParagraphMarkers(paragraph, validCommentIds);
55
+ if (nextParagraph !== paragraph) changed = true;
56
+ next.push(nextParagraph);
57
+ }
58
+ return changed ? next : null;
59
+ };
60
+ const withoutOrphanHeaderFooterMapMarkers = (map, validCommentIds) => {
61
+ if (!map) return null;
62
+ let changed = false;
63
+ const next = /* @__PURE__ */ new Map();
64
+ for (const [rId, headerFooter] of map) {
65
+ const content = withoutOrphanBlockMarkers(headerFooter.content, validCommentIds);
66
+ if (!content) {
67
+ next.set(rId, headerFooter);
68
+ continue;
69
+ }
70
+ changed = true;
71
+ next.set(rId, {
72
+ ...headerFooter,
73
+ content
74
+ });
75
+ }
76
+ return changed ? next : null;
77
+ };
78
+ const withoutOrphanNoteMarkers = (notes, validCommentIds) => {
79
+ if (!notes) return null;
80
+ let changed = false;
81
+ const next = [];
82
+ for (const note of notes) {
83
+ const content = withoutOrphanBlockMarkers(note.content, validCommentIds);
84
+ if (!content) {
85
+ next.push(note);
86
+ continue;
87
+ }
88
+ changed = true;
89
+ next.push({
90
+ ...note,
91
+ content
92
+ });
93
+ }
94
+ return changed ? next : null;
95
+ };
96
+ const withoutOrphanBlockMarkers = (blocks, validCommentIds) => {
97
+ let changed = false;
98
+ const next = [];
99
+ for (const block of blocks) {
100
+ const nextBlock = withoutOrphanBlockMarker(block, validCommentIds);
101
+ if (nextBlock !== block) changed = true;
102
+ next.push(nextBlock);
103
+ }
104
+ return changed ? next : null;
105
+ };
106
+ const withoutOrphanBlockMarker = (block, validCommentIds) => {
107
+ if (block.type === "paragraph") return withoutOrphanParagraphMarkers(block, validCommentIds);
108
+ if (block.type === "table") return withoutOrphanTableMarkers(block, validCommentIds);
109
+ if (!("content" in block) || !block.content) return block;
110
+ const content = withoutOrphanBlockMarkers(block.content, validCommentIds);
111
+ if (!content) return block;
112
+ return {
113
+ ...block,
114
+ content
115
+ };
116
+ };
117
+ const withoutOrphanTableMarkers = (table, validCommentIds) => {
118
+ let changed = false;
119
+ const rows = [];
120
+ for (const row of table.rows) {
121
+ const nextRow = withoutOrphanTableRowMarkers(row, validCommentIds);
122
+ if (nextRow !== row) changed = true;
123
+ rows.push(nextRow);
124
+ }
125
+ if (!changed) return table;
126
+ return {
127
+ ...table,
128
+ rows
129
+ };
130
+ };
131
+ const withoutOrphanTableRowMarkers = (row, validCommentIds) => {
132
+ let changed = false;
133
+ const cells = [];
134
+ for (const cell of row.cells) {
135
+ const nextCell = withoutOrphanTableCellMarkers(cell, validCommentIds);
136
+ if (nextCell !== cell) changed = true;
137
+ cells.push(nextCell);
138
+ }
139
+ if (!changed) return row;
140
+ return {
141
+ ...row,
142
+ cells
143
+ };
144
+ };
145
+ const withoutOrphanTableCellMarkers = (cell, validCommentIds) => {
146
+ const content = withoutOrphanTableCellBlockMarkers(cell.content, validCommentIds);
147
+ if (!content) return cell;
148
+ return {
149
+ ...cell,
150
+ content
151
+ };
152
+ };
153
+ const withoutOrphanTableCellBlockMarkers = (blocks, validCommentIds) => {
154
+ let changed = false;
155
+ const next = [];
156
+ for (const block of blocks) {
157
+ const nextBlock = withoutOrphanTableCellBlockMarker(block, validCommentIds);
158
+ if (nextBlock !== block) changed = true;
159
+ next.push(nextBlock);
160
+ }
161
+ return changed ? next : null;
162
+ };
163
+ const withoutOrphanTableCellBlockMarker = (block, validCommentIds) => {
164
+ if (block.type === "paragraph") return withoutOrphanParagraphMarkers(block, validCommentIds);
165
+ if (block.type === "table") return withoutOrphanTableMarkers(block, validCommentIds);
166
+ return block;
167
+ };
168
+ const withoutOrphanParagraphMarkers = (paragraph, validCommentIds) => {
169
+ let changed = false;
170
+ const content = [];
171
+ for (const item of paragraph.content) {
172
+ if (isCommentMarker(item) && !validCommentIds.has(item.id)) {
173
+ changed = true;
174
+ continue;
175
+ }
176
+ content.push(item);
177
+ }
178
+ if (!changed) return paragraph;
179
+ return {
180
+ ...paragraph,
181
+ content
182
+ };
183
+ };
184
+ const isCommentMarker = (content) => content.type === "commentRangeStart" || content.type === "commentRangeEnd" || content.type === "commentReference";
185
+ //#endregion
186
+ export { withoutOrphanCommentRanges };
@@ -4,11 +4,65 @@ import { DOCX_CONFORMANCE_CLASSES } from "@stll/docx-core/model";
4
4
  const STRICT_MAIN_NAMESPACE = "http://purl.oclc.org/ooxml/wordprocessingml/main";
5
5
  const TRANSITIONAL_MAIN_NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
6
6
  const ROOT_START_TAG_SCAN_LIMIT = 64 * 1024;
7
- const ROOT_START_TAG_PATTERN = /^(?:\uFEFF|\s|<\?[\s\S]*?\?>|<!--[\s\S]*?-->)*(?<tag><[^\s/>]+(?:[^>"']|"[^"]*"|'[^']*')*>)/u;
7
+ const XML_WHITESPACE = /\s/u;
8
+ /**
9
+ * Find the root start tag within a bounded prefix of `document.xml`,
10
+ * without backtracking.
11
+ *
12
+ * Equivalent in captured-tag semantics to the previous
13
+ * `/^(?:\uFEFF|\s|<\?[\s\S]*?\?>|<!--[\s\S]*?-->)*(?<tag><[^\s/>]+(?:[^>"']|"[^"]*"|'[^']*')*>)/u`
14
+ * pattern, but as a linear hand-rolled scanner: the original alternation of
15
+ * greedy/lazy `[\s\S]*?` spans over an attacker-controlled prefix was
16
+ * susceptible to catastrophic backtracking on crafted input. This walks
17
+ * `scanTarget` once, tracking in-quote state, and returns `undefined` in
18
+ * every case the regex would have failed to match (unterminated PI/comment,
19
+ * no tag found before the scan limit, malformed tag open).
20
+ */
21
+ function scanRootStartTag(scanTarget) {
22
+ const length = scanTarget.length;
23
+ let index = 0;
24
+ for (;;) {
25
+ if (index >= length) return;
26
+ const character = scanTarget[index];
27
+ if (character === "" || XML_WHITESPACE.test(character ?? "")) {
28
+ index += 1;
29
+ continue;
30
+ }
31
+ if (scanTarget.startsWith("<?", index)) {
32
+ const closeIndex = scanTarget.indexOf("?>", index + 2);
33
+ if (closeIndex === -1) return;
34
+ index = closeIndex + 2;
35
+ continue;
36
+ }
37
+ if (scanTarget.startsWith("<!--", index)) {
38
+ const closeIndex = scanTarget.indexOf("-->", index + 4);
39
+ if (closeIndex === -1) return;
40
+ index = closeIndex + 3;
41
+ continue;
42
+ }
43
+ break;
44
+ }
45
+ if (scanTarget[index] !== "<") return;
46
+ const nameStart = index + 1;
47
+ const firstNameChar = scanTarget[nameStart];
48
+ if (firstNameChar === void 0 || firstNameChar === "/" || firstNameChar === ">" || XML_WHITESPACE.test(firstNameChar)) return;
49
+ let quote;
50
+ for (let cursor = nameStart; cursor < length; cursor++) {
51
+ const character = scanTarget[cursor];
52
+ if (quote) {
53
+ if (character === quote) quote = void 0;
54
+ continue;
55
+ }
56
+ if (character === "\"" || character === "'") {
57
+ quote = character;
58
+ continue;
59
+ }
60
+ if (character === ">") return scanTarget.slice(index, cursor + 1);
61
+ }
62
+ }
8
63
  const detectDocxConformanceClass = (documentXml) => {
9
64
  if (documentXml === null) return DOCX_CONFORMANCE_CLASSES.UNKNOWN;
10
- const scanTarget = documentXml.length <= ROOT_START_TAG_SCAN_LIMIT ? documentXml : documentXml.slice(0, ROOT_START_TAG_SCAN_LIMIT);
11
- const rootStartTag = ROOT_START_TAG_PATTERN.exec(scanTarget)?.groups?.["tag"];
65
+ const rootStartTag = scanRootStartTag(documentXml.length <= ROOT_START_TAG_SCAN_LIMIT ? documentXml : documentXml.slice(0, ROOT_START_TAG_SCAN_LIMIT));
12
66
  const root = (rootStartTag === void 0 ? null : parseXmlDocument(rootStartTag.endsWith("/>") ? rootStartTag : `${rootStartTag.slice(0, -1)}/>`)) ?? parseXmlDocument(documentXml);
13
67
  return detectDocxConformanceClassFromRoot(root);
14
68
  };
@@ -1,5 +1,6 @@
1
1
  import { DOCX_ENCRYPTION_ERROR_CODES, DocxEncryptionError } from "./errors.js";
2
2
  import { bytesEqual, joinBytes, padToBlock, passwordToUtf16Le, toArrayBuffer, writeUint32Le } from "./cryptoBytes.js";
3
+ import { MAX_SPIN_COUNT } from "./encryptionInfo.js";
3
4
  //#region src/docx/encryption/agileDecryption.ts
4
5
  /**
5
6
  * Agile Encryption decryption (Office 2010+) using Web Crypto.
@@ -115,11 +116,20 @@ const decryptAesCbc = async (rawKey, iv, ciphertext) => {
115
116
  }, key, toArrayBuffer(paddedCiphertext));
116
117
  return new Uint8Array(decrypted).subarray(0, ciphertext.length);
117
118
  };
118
- /** [MS-OFFCRYPTO] 2.3.4.9 — iterated hash of password + salt. */
119
+ /**
120
+ * [MS-OFFCRYPTO] 2.3.4.9 — iterated hash of password + salt.
121
+ *
122
+ * `iterations` is expected to already be validated by
123
+ * `encryptionInfo.ts`'s `spinCount` parsing, but this loop drives real
124
+ * WebCrypto work per iteration, so it is clamped again here as well —
125
+ * this function must stay safe to call with an untrusted iteration count
126
+ * regardless of caller.
127
+ */
119
128
  const hashPassword = async (password, salt, iterations, hashName) => {
120
129
  const algorithm = webHashName(hashName);
130
+ const boundedIterations = Math.min(Math.max(iterations, 0), MAX_SPIN_COUNT);
121
131
  let state = await digest(algorithm, joinBytes([salt, passwordToUtf16Le(password)]));
122
- for (let i = 0; i < iterations; i++) state = await digest(algorithm, joinBytes([writeUint32Le(i), state]));
132
+ for (let i = 0; i < boundedIterations; i++) state = await digest(algorithm, joinBytes([writeUint32Le(i), state]));
123
133
  return state;
124
134
  };
125
135
  /** [MS-OFFCRYPTO] 2.3.4.9 — finalize a derived key with a block key suffix. */
@@ -83,9 +83,14 @@ const loadSector = (file, header, sectorId) => {
83
83
  };
84
84
  const loadFatSectorIds = (file, header) => {
85
85
  const ids = [...header.headerDifat];
86
+ const maxDifatSectors = Math.ceil(file.length / header.sectorBytes);
87
+ const stepLimit = Math.min(header.difatSectorCount, maxDifatSectors);
88
+ const seen = /* @__PURE__ */ new Set();
86
89
  let chain = header.difatStart;
87
- for (let step = 0; step < header.difatSectorCount; step++) {
90
+ for (let step = 0; step < stepLimit; step++) {
88
91
  if (chain === SECTOR_END || chain === SECTOR_FREE) break;
92
+ if (seen.has(chain)) throw new Error(`DIFAT chain loop at sector ${chain}`);
93
+ seen.add(chain);
89
94
  const sector = loadSector(file, header, chain);
90
95
  const view = viewOf(sector);
91
96
  const slotsPerSector = header.sectorBytes / 4 - 1;
@@ -27,6 +27,14 @@ type AgileEncryptionDescriptor = {
27
27
  scheme: "agile";
28
28
  material: AgileKeyMaterial;
29
29
  };
30
+ /**
31
+ * Cap on `encryptedKey/@spinCount`. MS-OFFCRYPTO's documented default is
32
+ * 100,000; a hostile/corrupt value here would drive `hashPassword`'s
33
+ * per-iteration WebCrypto digest loop (agileDecryption.ts) an
34
+ * attacker-chosen number of times, so reject anything past a generous
35
+ * multiple of that default instead of trusting the file.
36
+ */
37
+ declare const MAX_SPIN_COUNT = 200000;
30
38
  declare const parseAgileEncryptionInfo: (stream: Uint8Array) => AgileEncryptionDescriptor;
31
39
  //#endregion
32
- export { AgileEncryptionDescriptor, AgileKeyMaterial, parseAgileEncryptionInfo };
40
+ export { AgileEncryptionDescriptor, AgileKeyMaterial, MAX_SPIN_COUNT, parseAgileEncryptionInfo };
@@ -68,6 +68,22 @@ const requireInt = (attrs, name, element) => {
68
68
  return parsed;
69
69
  };
70
70
  const requireBase64 = (attrs, name, element) => decodeBase64(requireAttr(attrs, name, element));
71
+ /**
72
+ * Cap on `encryptedKey/@spinCount`. MS-OFFCRYPTO's documented default is
73
+ * 100,000; a hostile/corrupt value here would drive `hashPassword`'s
74
+ * per-iteration WebCrypto digest loop (agileDecryption.ts) an
75
+ * attacker-chosen number of times, so reject anything past a generous
76
+ * multiple of that default instead of trusting the file.
77
+ */
78
+ const MAX_SPIN_COUNT = 2e5;
79
+ const requireSpinCount = (attrs, element) => {
80
+ const spinCount = requireInt(attrs, "spinCount", element);
81
+ if (spinCount < 0 || spinCount > 2e5) throw new DocxEncryptionError({
82
+ code: DOCX_ENCRYPTION_ERROR_CODES.DECRYPTION_FAILED,
83
+ message: `EncryptionInfo <${element}> attribute "spinCount" out of range: ${spinCount}`
84
+ });
85
+ return spinCount;
86
+ };
71
87
  const parseAgileXml = (xmlBytes) => {
72
88
  const xml = new TextDecoder("utf-8").decode(xmlBytes);
73
89
  const keyDataTag = findElement(xml, "keyData");
@@ -88,7 +104,7 @@ const parseAgileXml = (xmlBytes) => {
88
104
  cipherBlockBytes: requireInt(keyData, "blockSize", "keyData"),
89
105
  cipherName: requireAttr(keyData, "cipherAlgorithm", "keyData"),
90
106
  cipherMode: requireAttr(keyData, "cipherChaining", "keyData"),
91
- passwordIterations: requireInt(encryptedKey, "spinCount", "encryptedKey"),
107
+ passwordIterations: requireSpinCount(encryptedKey, "encryptedKey"),
92
108
  passwordSalt: requireBase64(encryptedKey, "saltValue", "encryptedKey"),
93
109
  passwordHash: requireAttr(encryptedKey, "hashAlgorithm", "encryptedKey"),
94
110
  passwordKeyBits: requireInt(encryptedKey, "keyBits", "encryptedKey"),
@@ -125,4 +141,4 @@ const parseAgileEncryptionInfo = (stream) => {
125
141
  });
126
142
  };
127
143
  //#endregion
128
- export { parseAgileEncryptionInfo };
144
+ export { MAX_SPIN_COUNT, parseAgileEncryptionInfo };
@@ -1,9 +1,9 @@
1
1
  import { DOCX_ENCRYPTION_ERROR_CODES, DocxEncryptionError } from "./errors.js";
2
2
  import { toArrayBuffer } from "./cryptoBytes.js";
3
+ import { parseAgileEncryptionInfo } from "./encryptionInfo.js";
3
4
  import { decryptAgileEncryptedPackage } from "./agileDecryption.js";
4
5
  import { readEncryptedPackageStreams } from "./compoundFile.js";
5
6
  import { DOCX_CONTAINER_TYPES, detectDocxContainerType } from "./containerFormat.js";
6
- import { parseAgileEncryptionInfo } from "./encryptionInfo.js";
7
7
  //#region src/docx/encryption/openEncryptedDocx.ts
8
8
  /**
9
9
  * Decrypt password-protected OOXML before ZIP extraction.