@stll/folio-core 0.59.0 → 0.60.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 (116) hide show
  1. package/README.md +10 -1
  2. package/dist/compat/eigenpal.d.ts +2 -2
  3. package/dist/compat/eigenpal.js +2 -2
  4. package/dist/controller/canonicalSession.js +3 -2
  5. package/dist/controller/canonicalStructure.js +10 -7
  6. package/dist/controller/fontReadiness.js +1 -1
  7. package/dist/controller/layoutPipeline.js +7 -1
  8. package/dist/controller/layoutSession.d.ts +2 -0
  9. package/dist/display-list/build/fontTable.js +7 -5
  10. package/dist/display-list/build/furniture.d.ts +1 -0
  11. package/dist/display-list/build/furniture.js +1 -0
  12. package/dist/display-list/build/watermarkPrimitives.d.ts +2 -1
  13. package/dist/display-list/build/watermarkPrimitives.js +9 -3
  14. package/dist/display-list/editor/pageRenderer.js +1 -0
  15. package/dist/docx/bookmarkIdNormalization.d.ts +10 -0
  16. package/dist/docx/bookmarkIdNormalization.js +100 -0
  17. package/dist/docx/bookmarkParser.js +4 -2
  18. package/dist/docx/importedIdCollisions.d.ts +136 -0
  19. package/dist/docx/importedIdCollisions.js +215 -0
  20. package/dist/docx/numberingParser.js +2 -2
  21. package/dist/docx/paragraphPropertySource.js +1 -2
  22. package/dist/docx/parseContext.d.ts +2 -1
  23. package/dist/docx/parseContext.js +1 -0
  24. package/dist/docx/parseWarningMessage.js +1 -0
  25. package/dist/docx/parser.js +5 -1
  26. package/dist/docx/relsParser.js +1 -1
  27. package/dist/docx/rezip.js +2 -1
  28. package/dist/docx/selectedXmlIdentityScan.d.ts +10 -0
  29. package/dist/docx/selectedXmlIdentityScan.js +36 -0
  30. package/dist/docx/selectiveSave.js +5 -4
  31. package/dist/docx/selectiveXmlPatch.js +44 -52
  32. package/dist/docx/serializer/commentSerializer.d.ts +3 -1
  33. package/dist/docx/serializer/commentSerializer.js +11 -2
  34. package/dist/docx/serializer/runSerializer.js +2 -2
  35. package/dist/docx/serializer/trackedChangeAttributes.d.ts +1 -3
  36. package/dist/docx/serializer/trackedChangeAttributes.js +1 -11
  37. package/dist/fonts/embeddedFontFingerprint.d.ts +5 -0
  38. package/dist/fonts/embeddedFontFingerprint.js +18 -0
  39. package/dist/fonts/embeddedFonts.d.ts +1 -1
  40. package/dist/fonts/embeddedFonts.js +1 -1
  41. package/dist/fonts/headlessMeasure.js +0 -0
  42. package/dist/headless-layout.js +1 -0
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.js +2 -2
  45. package/dist/layout-bridge/convert/flowConversionShared.d.ts +2 -0
  46. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -1
  47. package/dist/layout-bridge/convert/footnoteLayout.js +3 -0
  48. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  49. package/dist/layout-bridge/convert/headerFooterLayout.js +2 -0
  50. package/dist/layout-bridge/convert/paragraphConversion.js +8 -1
  51. package/dist/layout-bridge/convert/toFlowBlocks.js +25 -1
  52. package/dist/layout-engine/measure/cache.js +39 -7
  53. package/dist/layout-engine/measure/lineTypography.js +1 -0
  54. package/dist/layout-engine/measure/listMarkerWidth.d.ts +1 -0
  55. package/dist/layout-engine/measure/listMarkerWidth.js +1 -0
  56. package/dist/layout-engine/measure/measureContainer.js +3 -4
  57. package/dist/layout-engine/measure/measureHelpers.d.ts +6 -9
  58. package/dist/layout-engine/measure/measureHelpers.js +21 -18
  59. package/dist/layout-engine/measure/measureParagraph.js +2 -0
  60. package/dist/layout-engine/measure/measureTypes.d.ts +2 -0
  61. package/dist/layout-engine/types.d.ts +4 -1
  62. package/dist/layout-painter/renderPage.d.ts +1 -0
  63. package/dist/layout-painter/renderPage.js +11 -3
  64. package/dist/layout-painter/renderParagraph.js +13 -3
  65. package/dist/layout-painter/renderWatermark.d.ts +1 -0
  66. package/dist/layout-painter/renderWatermark.js +7 -3
  67. package/dist/managers/editorShortcuts.d.ts +2 -1
  68. package/dist/managers/editorShortcuts.js +2 -1
  69. package/dist/prosemirror/canonicalSelectionRange.d.ts +4 -1
  70. package/dist/prosemirror/canonicalSelectionRange.js +9 -3
  71. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.d.ts +8 -2
  72. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +39 -3
  73. package/dist/prosemirror/extensions/features/EmptyParagraphFormatExtension.d.ts +8 -2
  74. package/dist/prosemirror/extensions/features/EmptyParagraphFormatExtension.js +4 -3
  75. package/dist/prosemirror/extensions/features/ImageDragExtension.js +4 -3
  76. package/dist/prosemirror/extensions/features/JoinedRunStyleExtension.js +4 -3
  77. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +9 -3
  78. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +6 -5
  79. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +8 -2
  80. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +11 -3
  81. package/dist/prosemirror/extensions/marks/RunIdentityExtension.js +4 -3
  82. package/dist/prosemirror/extensions/marks/markUtils.js +1 -1
  83. package/dist/prosemirror/pluginKeyOwner.d.ts +13 -0
  84. package/dist/prosemirror/pluginKeyOwner.js +35 -0
  85. package/dist/prosemirror/plugins/aiCitationDecorations.d.ts +11 -4
  86. package/dist/prosemirror/plugins/aiCitationDecorations.js +16 -3
  87. package/dist/prosemirror/plugins/aiSuggestionDecorations.d.ts +11 -4
  88. package/dist/prosemirror/plugins/aiSuggestionDecorations.js +18 -3
  89. package/dist/prosemirror/plugins/anonymizationDecorations.d.ts +11 -11
  90. package/dist/prosemirror/plugins/anonymizationDecorations.js +23 -13
  91. package/dist/prosemirror/plugins/autocompleteSuggestion.d.ts +8 -2
  92. package/dist/prosemirror/plugins/autocompleteSuggestion.js +27 -5
  93. package/dist/prosemirror/plugins/contentControlWidgets.d.ts +8 -2
  94. package/dist/prosemirror/plugins/contentControlWidgets.js +4 -3
  95. package/dist/prosemirror/plugins/createDocScanPlugin.d.ts +6 -5
  96. package/dist/prosemirror/plugins/createDocScanPlugin.js +1 -1
  97. package/dist/prosemirror/plugins/documentNumbering.js +5 -3
  98. package/dist/prosemirror/plugins/documentStyleState.d.ts +8 -2
  99. package/dist/prosemirror/plugins/documentStyleState.js +2 -2
  100. package/dist/prosemirror/plugins/documentStyles.js +1 -1
  101. package/dist/prosemirror/plugins/selectionTracker.d.ts +8 -2
  102. package/dist/prosemirror/plugins/selectionTracker.js +4 -3
  103. package/dist/prosemirror/plugins/suggestionMode.d.ts +8 -2
  104. package/dist/prosemirror/plugins/suggestionMode.js +4 -3
  105. package/dist/prosemirror/plugins/templateDirectives.d.ts +8 -2
  106. package/dist/prosemirror/plugins/templateDirectives.js +19 -4
  107. package/dist/prosemirror/plugins/templatePreviewValues.d.ts +8 -2
  108. package/dist/prosemirror/plugins/templatePreviewValues.js +27 -5
  109. package/dist/prosemirror/plugins/templateSlashMenu.d.ts +8 -2
  110. package/dist/prosemirror/plugins/templateSlashMenu.js +23 -5
  111. package/dist/prosemirror/plugins/transactionInvariants.js +23 -3
  112. package/dist/utils/fontLoader.d.ts +2 -2
  113. package/dist/utils/fontLoader.js +2 -7
  114. package/dist/utils/fontResolver.d.ts +7 -3
  115. package/dist/utils/fontResolver.js +6 -8
  116. package/package.json +24 -8
@@ -0,0 +1,215 @@
1
+ import { paraIdAttribute, textIdAttribute } from "./paraIdAttribute.js";
2
+ import { REVISION_ELEMENT_NAMES } from "./revisionIdNormalization.js";
3
+ import { scanSelectedStreamingXmlElements } from "./selectedXmlIdentityScan.js";
4
+ import { OOXML_NAMESPACES } from "./serializer/partNamespaces.js";
5
+ import { toTransitionalNamespaceUri } from "./transitionalSpelling.js";
6
+ import { resolveNamespaceUri } from "./xmlNamespaceContext.js";
7
+ import { NAMESPACES, getLocalName, getNamespaceUri } from "./xmlParser.js";
8
+ import { panic } from "better-result";
9
+ import { IMPORTED_ID_COLLISION_KINDS, PARSE_WARNING_CODES } from "@stll/docx-core/model";
10
+ //#region src/docx/importedIdCollisions.ts
11
+ const WORD_2010_NAMESPACE = "http://schemas.microsoft.com/office/word/2010/wordml";
12
+ const PACKAGE_RELATIONSHIP_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/relationships";
13
+ const paragraphIdentityForKind = (kind, element) => {
14
+ switch (kind) {
15
+ case IMPORTED_ID_COLLISION_KINDS.paraId: return paraIdAttribute(element);
16
+ case IMPORTED_ID_COLLISION_KINDS.textId: return textIdAttribute(element);
17
+ default: return;
18
+ }
19
+ };
20
+ /** Definition sites only: paired markers and references never claim an identity. */
21
+ const IMPORTED_ID_COLLISION_REGISTRY = {
22
+ bookmark: {
23
+ kind: IMPORTED_ID_COLLISION_KINDS.bookmark,
24
+ scope: "part",
25
+ sites: [{
26
+ namespace: NAMESPACES.w,
27
+ elements: /* @__PURE__ */ new Set(["bookmarkStart"])
28
+ }],
29
+ attribute: "id",
30
+ attributeNamespace: NAMESPACES.w,
31
+ encoding: "decimal"
32
+ },
33
+ comment: {
34
+ kind: IMPORTED_ID_COLLISION_KINDS.comment,
35
+ scope: "part",
36
+ sites: [{
37
+ namespace: NAMESPACES.w,
38
+ elements: /* @__PURE__ */ new Set(["comment"])
39
+ }],
40
+ attribute: "id",
41
+ attributeNamespace: NAMESPACES.w,
42
+ encoding: "decimal"
43
+ },
44
+ footnote: {
45
+ kind: IMPORTED_ID_COLLISION_KINDS.footnote,
46
+ scope: "part",
47
+ sites: [{
48
+ namespace: NAMESPACES.w,
49
+ elements: /* @__PURE__ */ new Set(["footnote"])
50
+ }],
51
+ attribute: "id",
52
+ attributeNamespace: NAMESPACES.w,
53
+ encoding: "decimal"
54
+ },
55
+ endnote: {
56
+ kind: IMPORTED_ID_COLLISION_KINDS.endnote,
57
+ scope: "part",
58
+ sites: [{
59
+ namespace: NAMESPACES.w,
60
+ elements: /* @__PURE__ */ new Set(["endnote"])
61
+ }],
62
+ attribute: "id",
63
+ attributeNamespace: NAMESPACES.w,
64
+ encoding: "decimal"
65
+ },
66
+ revision: {
67
+ kind: IMPORTED_ID_COLLISION_KINDS.revision,
68
+ scope: "package",
69
+ sites: [{
70
+ namespace: NAMESPACES.w,
71
+ elements: REVISION_ELEMENT_NAMES
72
+ }],
73
+ attribute: "id",
74
+ attributeNamespace: NAMESPACES.w,
75
+ encoding: "decimal"
76
+ },
77
+ paraId: {
78
+ kind: IMPORTED_ID_COLLISION_KINDS.paraId,
79
+ scope: "part",
80
+ sites: [
81
+ {
82
+ namespace: NAMESPACES.w,
83
+ elements: /* @__PURE__ */ new Set(["p"])
84
+ },
85
+ {
86
+ namespace: NAMESPACES.w15,
87
+ elements: /* @__PURE__ */ new Set(["commentEx"])
88
+ },
89
+ {
90
+ namespace: OOXML_NAMESPACES.w16cex.uri,
91
+ elements: /* @__PURE__ */ new Set(["comment"])
92
+ }
93
+ ],
94
+ attribute: "paraId",
95
+ attributeNamespace: WORD_2010_NAMESPACE,
96
+ encoding: "hex"
97
+ },
98
+ textId: {
99
+ kind: IMPORTED_ID_COLLISION_KINDS.textId,
100
+ scope: "part",
101
+ sites: [{
102
+ namespace: NAMESPACES.w,
103
+ elements: /* @__PURE__ */ new Set(["p"])
104
+ }],
105
+ attribute: "textId",
106
+ attributeNamespace: WORD_2010_NAMESPACE,
107
+ encoding: "hex"
108
+ },
109
+ numId: {
110
+ kind: IMPORTED_ID_COLLISION_KINDS.numId,
111
+ scope: "part",
112
+ sites: [{
113
+ namespace: NAMESPACES.w,
114
+ elements: /* @__PURE__ */ new Set(["num"])
115
+ }],
116
+ attribute: "numId",
117
+ attributeNamespace: NAMESPACES.w,
118
+ encoding: "decimal"
119
+ },
120
+ abstractNumId: {
121
+ kind: IMPORTED_ID_COLLISION_KINDS.abstractNumId,
122
+ scope: "part",
123
+ sites: [{
124
+ namespace: NAMESPACES.w,
125
+ elements: /* @__PURE__ */ new Set(["abstractNum"])
126
+ }],
127
+ attribute: "abstractNumId",
128
+ attributeNamespace: NAMESPACES.w,
129
+ encoding: "decimal"
130
+ },
131
+ drawing: {
132
+ kind: IMPORTED_ID_COLLISION_KINDS.drawing,
133
+ scope: "package",
134
+ sites: [{
135
+ namespace: NAMESPACES.wp,
136
+ elements: /* @__PURE__ */ new Set(["docPr"])
137
+ }],
138
+ attribute: "id",
139
+ attributeNamespace: null,
140
+ encoding: "decimal"
141
+ },
142
+ relationship: {
143
+ kind: IMPORTED_ID_COLLISION_KINDS.relationship,
144
+ scope: "part",
145
+ sites: [{
146
+ namespace: PACKAGE_RELATIONSHIP_NAMESPACE,
147
+ elements: /* @__PURE__ */ new Set(["Relationship"])
148
+ }],
149
+ attribute: "Id",
150
+ attributeNamespace: null,
151
+ encoding: "string"
152
+ }
153
+ };
154
+ const normalizedIdentity = (value, encoding) => {
155
+ if (encoding === "string") return value;
156
+ if (encoding === "hex") return /^[0-9a-f]{8}$/iu.test(value) ? value.toUpperCase() : void 0;
157
+ const spelling = value.trim();
158
+ if (!/^[+-]?\d+$/u.test(spelling)) return void 0;
159
+ const digits = spelling.replace(/^[+-]?0*/u, "");
160
+ return digits === "" ? "0" : `${spelling.startsWith("-") ? "-" : ""}${digits}`;
161
+ };
162
+ /** Reports repeated source definitions without changing the package or choosing a winner. */
163
+ const diagnoseImportedIdCollisions = (parts, context) => {
164
+ const rules = Object.values(IMPORTED_ID_COLLISION_REGISTRY);
165
+ const packageSeen = /* @__PURE__ */ new Map();
166
+ for (const [part, xml] of parts) {
167
+ const partSeen = /* @__PURE__ */ new Map();
168
+ const scoped = context.scoped({ part });
169
+ scanSelectedStreamingXmlElements(xml, ({ element, attributeValueSpans }) => {
170
+ if (element.name === void 0) return;
171
+ const namespace = toTransitionalNamespaceUri(getNamespaceUri(element) ?? "");
172
+ const localName = getLocalName(element.name);
173
+ for (const rule of rules) {
174
+ const kind = rule.kind;
175
+ if (!rule.sites.some((site) => namespace === site.namespace && site.elements.has(localName))) continue;
176
+ const paragraphIdentity = paragraphIdentityForKind(kind, element);
177
+ const seen = rule.scope === "package" ? packageSeen : partSeen;
178
+ for (const [name, rawValue] of Object.entries(element.attributes ?? {})) {
179
+ if (getLocalName(name) !== rule.attribute || rawValue === void 0) continue;
180
+ const colon = name.indexOf(":");
181
+ const attributeNamespace = colon < 0 ? null : toTransitionalNamespaceUri(resolveNamespaceUri(element.namespaceScope, name.slice(0, colon)) ?? "");
182
+ if (rule.encoding === "hex") {
183
+ if (String(rawValue) !== paragraphIdentity) continue;
184
+ } else if (attributeNamespace !== rule.attributeNamespace) continue;
185
+ const value = String(rawValue);
186
+ const identity = normalizedIdentity(value, rule.encoding);
187
+ if (identity === void 0) continue;
188
+ const key = `${kind}:${identity}`;
189
+ const span = attributeValueSpans.get(name);
190
+ if (span === void 0) panic("Missing imported identity source span");
191
+ const offset = span.start;
192
+ const first = seen.get(key);
193
+ if (first === void 0) {
194
+ seen.set(key, {
195
+ part,
196
+ offset
197
+ });
198
+ break;
199
+ }
200
+ scoped.warn({
201
+ code: PARSE_WARNING_CODES.duplicateImportedId,
202
+ identityKind: kind,
203
+ element: element.name,
204
+ at: `offset ${offset}`,
205
+ value,
206
+ detail: `${kind} identity repeats the definition at offset ${first.offset} in ${first.part}.`
207
+ });
208
+ break;
209
+ }
210
+ }
211
+ });
212
+ }
213
+ };
214
+ //#endregion
215
+ export { IMPORTED_ID_COLLISION_REGISTRY, diagnoseImportedIdCollisions };
@@ -398,9 +398,9 @@ function getCachedNumberingMap(definitions) {
398
398
  */
399
399
  function createNumberingMap(definitions) {
400
400
  const abstractMap = /* @__PURE__ */ new Map();
401
- for (const abs of definitions.abstractNums) abstractMap.set(abs.abstractNumId, abs);
401
+ for (const abs of definitions.abstractNums) if (!abstractMap.has(abs.abstractNumId)) abstractMap.set(abs.abstractNumId, abs);
402
402
  const numMap = /* @__PURE__ */ new Map();
403
- for (const num of definitions.nums) numMap.set(num.numId, num);
403
+ for (const num of definitions.nums) if (!numMap.has(num.numId)) numMap.set(num.numId, num);
404
404
  return {
405
405
  definitions,
406
406
  getLevel(numId, ilvl) {
@@ -5,7 +5,6 @@ import { parseParagraphProperties } from "./paragraphProperties.js";
5
5
  import { visitDocxParagraphs } from "./paragraphTraversal.js";
6
6
  import { parseXmlDocument } from "./xmlParser.js";
7
7
  import { TaggedError, panic } from "better-result";
8
- import { PluginKey } from "prosemirror-state";
9
8
  //#region src/docx/paragraphPropertySource.ts
10
9
  const paragraphPropertySourceEmissionFingerprint = Symbol("paragraphPropertySourceEmissionFingerprint");
11
10
  const paragraphPropertySourceIndentationBaseline = Symbol("paragraphPropertySourceIndentationBaseline");
@@ -793,7 +792,7 @@ const createProseParagraphWithPropertySource = (nodeType, sourceParagraph, optio
793
792
  linkProseParagraphPropertySource(paragraph, sourceParagraph);
794
793
  return paragraph;
795
794
  };
796
- const paragraphPropertySourceTransfersKey = new PluginKey("paragraphPropertySourceTransfers");
795
+ const paragraphPropertySourceTransfersKey = "stll.folio.meta.paragraphPropertySourceTransfers";
797
796
  /** Join adjacent paragraphs when revision semantics deliberately select the right owner. */
798
797
  const joinProseParagraphsWithRightPropertySource = ({ attrs, pos, transaction }) => {
799
798
  const $pos = transaction.doc.resolve(pos);
@@ -1,8 +1,9 @@
1
- import { ParseWarning, ParseWarningCode } from "@stll/docx-core/model";
1
+ import { ImportedIdCollisionKind, ParseWarning, ParseWarningCode } from "@stll/docx-core/model";
2
2
  //#region src/docx/parseContext.d.ts
3
3
  /** What a call site states; the collector supplies the rest. */
4
4
  type ParseWarningReport = {
5
5
  code: ParseWarningCode;
6
+ identityKind?: ImportedIdCollisionKind;
6
7
  /** The element as written, prefix included. */
7
8
  element?: string;
8
9
  /** The best position this part can name, e.g. `style "Heading1"`. */
@@ -30,6 +30,7 @@ const createParseWarningCollector = (part = PACKAGE_PART) => {
30
30
  retainedByCode.set(report.code, kept + 1);
31
31
  retained.push({
32
32
  code: report.code,
33
+ ...report.identityKind === void 0 ? {} : { identityKind: report.identityKind },
33
34
  location: {
34
35
  ...location,
35
36
  ...report.element === void 0 ? {} : { element: report.element },
@@ -22,6 +22,7 @@ const PARSE_WARNING_MESSAGES = {
22
22
  [PARSE_WARNING_CODES.documentPartMissing]: () => "No document.xml found in DOCX",
23
23
  [PARSE_WARNING_CODES.documentModelIssue]: (warning) => warning.detail ?? "The document model reported an issue.",
24
24
  [PARSE_WARNING_CODES.duplicateCommentId]: (warning) => `Dropped ${plural(warning.count, "comment")} repeating a w:id another comment already defines.`,
25
+ [PARSE_WARNING_CODES.duplicateImportedId]: (warning) => `Repeated imported identity${quoted(warning.value)}${where(warning)}: ${warning.detail ?? "multiple definitions share an identity."}`,
25
26
  [PARSE_WARNING_CODES.missingCommentId]: (warning) => `Dropped ${plural(warning.count, "comment")} with no readable w:id.`,
26
27
  [PARSE_WARNING_CODES.duplicateNoteId]: (warning) => `Dropped ${plural(warning.count, "note")} repeating a w:id another note already defines.`,
27
28
  [PARSE_WARNING_CODES.danglingCommentReference]: (warning) => `Removed ${plural(warning.count, "dangling comment reference marker")} whose comments.xml entries are missing.`,
@@ -2,6 +2,7 @@ import { toArrayBuffer } from "../utils/docxInput.js";
2
2
  import { loadFontsWithMapping } from "../utils/fontLoader.js";
3
3
  import { MAX_PACKAGE_TIFF_PIXELS, convertTiffToPngDataUrl, isTiffMimeType } from "../utils/tiffConverter.js";
4
4
  import { ALT_CHUNK_PLAIN_TEXT_LIMIT_BYTES, getAltChunkRelationshipId, isAltChunkMarkup } from "./altChunk.js";
5
+ import { normalizeBookmarkIdsInXmlParts } from "./bookmarkIdNormalization.js";
5
6
  import { DUPLICATE_COMMENT_ID_WARNING, normalizeCommentIds } from "./commentIdNormalization.js";
6
7
  import { parseComments } from "./commentParser.js";
7
8
  import { DANGLING_COMMENT_REFERENCE_WARNING, UNBALANCED_COMMENT_RANGE_WARNING, normalizeCommentReferences } from "./commentReferenceNormalization.js";
@@ -16,6 +17,7 @@ import { parseEndnotes, parseFootnotes } from "./footnoteParser.js";
16
17
  import { parseFooter, parseHeader } from "./headerFooterParser.js";
17
18
  import { DANGLING_FOOTER_REFERENCE_WARNING, DANGLING_HEADER_REFERENCE_WARNING, assignHeaderFooterRoles, normalizeHeaderFooterReferences } from "./headerFooterReferenceNormalization.js";
18
19
  import { assignHeaderFooterVerbatimXml, canReplayHeaderFooterVerbatim, captureDocumentSourceBaseline, captureHeaderFooterPackageBaselines, refreshHeaderFooterVerbatimFingerprint } from "./headerFooterVerbatim.js";
20
+ import { diagnoseImportedIdCollisions } from "./importedIdCollisions.js";
19
21
  import { extractMetafileRaster, isMetafileMimeType } from "./metafileRaster.js";
20
22
  import { renderEmfSvg } from "./metafileSvg.js";
21
23
  import { DocxModelValidationError, formatDocumentModelIssues, validateFolioDocumentModel } from "./modelValidation.js";
@@ -34,7 +36,7 @@ import { parseSettings } from "./settingsParser.js";
34
36
  import { parseStylesPackage } from "./styleParser.js";
35
37
  import { applyThemeFontLang, parseTheme } from "./themeParser.js";
36
38
  import { UNBALANCED_MOVE_RANGE_WARNING, normalizeTrackedMoveRanges } from "./trackedMoveRangeNormalization.js";
37
- import { getEntryUncompressedSize, getMediaMimeType, mediaToDataUrl, unzipDocx } from "./unzip.js";
39
+ import { getEntryUncompressedSize, getMediaMimeType, mediaToDataUrl, replaceRawDocxXmlParts, unzipDocx } from "./unzip.js";
38
40
  import { getAttribute, getChildElements, getLocalName, parseXml, parseXmlDocument } from "./xmlParser.js";
39
41
  import { FOLIO_XML_RESOURCE_LIMITS } from "./xmlResourceLimits.js";
40
42
  import { TaggedError, panic } from "better-result";
@@ -192,6 +194,8 @@ async function parseDocxWithPreviewBudget(input, options, previewBudget) {
192
194
  password,
193
195
  extractAllXml: false
194
196
  }));
197
+ diagnoseImportedIdCollisions(raw.allXml, parseContext);
198
+ await replaceRawDocxXmlParts(raw, normalizeBookmarkIdsInXmlParts(raw.allXml));
195
199
  const repairedDocumentTree = await normalizeRawDocxNumericIds(raw);
196
200
  const paragraphPropertySourceDigest = sha256Hex(raw.originalBuffer);
197
201
  if (raw.wasEncrypted) parseContext.warn({ code: PARSE_WARNING_CODES.packageDecrypted });
@@ -86,7 +86,7 @@ function parseRelationships(relsXml) {
86
86
  };
87
87
  if (targetMode === "External") relationship.targetMode = "External";
88
88
  else if (targetMode === "Internal") relationship.targetMode = "Internal";
89
- map.set(id, relationship);
89
+ if (!map.has(id)) map.set(id, relationship);
90
90
  }
91
91
  return map;
92
92
  }
@@ -25,7 +25,7 @@ import { RELATIONSHIP_TYPES, parseRelationships, resolveRelativePath } from "./r
25
25
  import { removeResolvedHeaderFooterParts } from "./removeHeaderFooterParts.js";
26
26
  import { normalizeRevisionIdsInXmlParts } from "./revisionIdNormalization.js";
27
27
  import { buildPatchedNotePartXml, collectChangedNoteParaIds, collectParaIds, patchNumberingDefinitions, spliceXml } from "./selectiveXmlPatch.js";
28
- import { hasOwnedCommentsPart, planCommentParts, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
28
+ import { canReplayCommentsPart, hasOwnedCommentsPart, planCommentParts, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
29
29
  import { serializeDocument } from "./serializer/documentSerializer.js";
30
30
  import { serializeFontTableXml } from "./serializer/fontTableSerializer.js";
31
31
  import { serializeHeaderFooter } from "./serializer/headerFooterSerializer.js";
@@ -201,6 +201,7 @@ async function serializeCommentsToZip(doc, zip, compressionLevel) {
201
201
  const comments = doc.package.document.comments ?? [];
202
202
  const sourceCommentsFile = findZipEntryCaseInsensitive(zip, "word/comments.xml");
203
203
  const sourceCommentsXml = sourceCommentsFile ? await sourceCommentsFile.async("text") : void 0;
204
+ if (sourceCommentsXml !== void 0 && canReplayCommentsPart(doc, sourceCommentsXml)) return;
204
205
  if (comments.length === 0) {
205
206
  const ownsCommentsPart = hasOwnedCommentsPart(doc);
206
207
  if (sourceCommentsXml === void 0 ? !ownsCommentsPart : !hasCommentEntries(sourceCommentsXml)) return;
@@ -0,0 +1,10 @@
1
+ import { scanStreamingXmlElements } from "./streamingXmlParser.js";
2
+ //#region src/docx/selectedXmlIdentityScan.d.ts
3
+ /** Identity census follows the parser's first-Choice, otherwise-Fallback policy. */
4
+ declare const scanSelectedStreamingXmlElements: (xml: string, visitor: Parameters<typeof scanStreamingXmlElements>[1]) => {
5
+ status: "unsupported";
6
+ } | {
7
+ status: "scanned";
8
+ };
9
+ //#endregion
10
+ export { scanSelectedStreamingXmlElements };
@@ -0,0 +1,36 @@
1
+ import { scanStreamingXmlElements } from "./streamingXmlParser.js";
2
+ import { NAMESPACES, getLocalName, getNamespaceUri, selectAlternateContentBranch } from "./xmlParser.js";
3
+ //#region src/docx/selectedXmlIdentityScan.ts
4
+ const isCompatibilityElement = (element, name) => getNamespaceUri(element) === NAMESPACES.mc && getLocalName(element.name) === name;
5
+ /** Identity census follows the parser's first-Choice, otherwise-Fallback policy. */
6
+ const scanSelectedStreamingXmlElements = (xml, visitor) => {
7
+ if (!xml.includes("AlternateContent")) return scanStreamingXmlElements(xml, visitor);
8
+ const alternatives = /* @__PURE__ */ new Map();
9
+ const positions = /* @__PURE__ */ new WeakMap();
10
+ const scanned = scanStreamingXmlElements(xml, ({ element, parent, nameEnd }) => {
11
+ positions.set(element, nameEnd);
12
+ if (isCompatibilityElement(element, "AlternateContent")) {
13
+ element.elements = [];
14
+ alternatives.set(nameEnd, element);
15
+ }
16
+ if (!parent || !isCompatibilityElement(parent, "AlternateContent")) return;
17
+ parent.elements?.push(element);
18
+ });
19
+ if (scanned.status === "unsupported") return scanned;
20
+ const selectedPositions = /* @__PURE__ */ new Map();
21
+ for (const [position, alternative] of alternatives) {
22
+ const selected = selectAlternateContentBranch(alternative);
23
+ selectedPositions.set(position, selected === void 0 ? void 0 : positions.get(selected));
24
+ }
25
+ const active = /* @__PURE__ */ new WeakMap();
26
+ return scanStreamingXmlElements(xml, (entry) => {
27
+ const { element, parent, nameEnd } = entry;
28
+ positions.set(element, nameEnd);
29
+ let included = parent === void 0 || active.get(parent) === true;
30
+ if (parent && isCompatibilityElement(parent, "AlternateContent")) included &&= nameEnd === selectedPositions.get(positions.get(parent) ?? -1);
31
+ active.set(element, included);
32
+ if (included) visitor(entry);
33
+ });
34
+ };
35
+ //#endregion
36
+ export { scanSelectedStreamingXmlElements };
@@ -10,7 +10,7 @@ import { RELATIONSHIP_TYPES } from "./relsParser.js";
10
10
  import { COMMENTS_CONTENT_TYPE, COMMENTS_EXTENDED_PART_LOWER, addCommentsExtendedOverride, addCommentsExtendedRelationship, applyUpdatesToZip, collectHeaderFooterUpdates, findMaxRId, hasModelDrivenPictureWatermark, hasUnmaterializedHeaderFooter, hasUnmaterializedHyperlinkBindings, hasUnmaterializedInlineResources, planAddedStyles, publishCanonicalImageResources, updateCoreProperties, withoutAttachedTemplate } from "./rezip.js";
11
11
  import "./selectiveSaveFlags.js";
12
12
  import { buildPatchedDocumentXml, buildPatchedNoteXml, collectAddedNumberingDefs, collectParaIds, patchNumberingDefinitions } from "./selectiveXmlPatch.js";
13
- import { hasOwnedCommentsPart, planCommentParts, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
13
+ import { canReplayCommentsPart, hasOwnedCommentsPart, planCommentParts, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
14
14
  import { serializeDocument } from "./serializer/documentSerializer.js";
15
15
  import { serializeEndnotes, serializeFootnotes } from "./serializer/noteSerializer.js";
16
16
  import { serializeNumberingXml } from "./serializer/numberingSerializer.js";
@@ -262,8 +262,9 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
262
262
  }
263
263
  const sourceCommentsFile = findZipEntryCaseInsensitive(zip, "word/comments.xml");
264
264
  const commentsPartPath = sourceCommentsFile?.name ?? "word/comments.xml";
265
- if (hasComments || ownsCommentsPart || sourceCommentsFile) {
266
- const sourceCommentsXml = sourceCommentsFile ? await sourceCommentsFile.async("text") : void 0;
265
+ const sourceCommentsXml = sourceCommentsFile ? await sourceCommentsFile.async("text") : void 0;
266
+ const commentsUnchanged = sourceCommentsXml !== void 0 && canReplayCommentsPart(doc, sourceCommentsXml);
267
+ if (!commentsUnchanged && (hasComments || ownsCommentsPart || sourceCommentsFile)) {
267
268
  const commentsXml = serializeComments(commentPlan, sourceCommentsXml === void 0 ? void 0 : readRootNamespaceBindings(sourceCommentsXml));
268
269
  if (commentsXml !== sourceCommentsXml) updates.set(commentsPartPath, commentsXml);
269
270
  }
@@ -283,7 +284,7 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
283
284
  }
284
285
  }
285
286
  }
286
- if (!await patchCommentsExtended(zip, commentPlan, updates)) return null;
287
+ if (!commentsUnchanged && !await patchCommentsExtended(zip, commentPlan, updates)) return null;
287
288
  if (!await patchNumberingPart(zip, doc, updates)) return null;
288
289
  const styles = await planAddedStyles(doc, zip);
289
290
  switch (styles.type) {
@@ -1,6 +1,7 @@
1
1
  import { patchBreaksCommentRangeBalance } from "./commentRangeIntegrity.js";
2
2
  import { resolveParagraphIdentities } from "./paraIdAttribute.js";
3
3
  import { readRootNamespaceBindings } from "./serializer/partNamespaces.js";
4
+ import { scanStreamingXmlElements } from "./streamingXmlParser.js";
4
5
  import { captureVerbatimXml } from "./verbatimCapture.js";
5
6
  import { PARAGRAPH_SCAN_NAMES, hasCanonicalWordprocessingPrefixes, matchCloseTag, matchOpenTag, resolveSelectiveScanPrefixes, splicesAsCanonical } from "./wordprocessingPrefixes.js";
6
7
  import { resolveNamespaceUri } from "./xmlNamespaceContext.js";
@@ -650,36 +651,32 @@ function extractElementByIdAttr(xml, openLiteral, closeTag, idAttr, id) {
650
651
  const qualifiedName = (prefix, localName) => prefix.length === 0 ? localName : `${prefix}:${localName}`;
651
652
  const collectNoteElementSyntax = (xml, elementName) => {
652
653
  const byId = /* @__PURE__ */ new Map();
653
- const root = parseXmlDocument(xml);
654
- for (const element of getChildElements(root)) {
655
- if (getLocalName(element.name) !== elementName || !WORDPROCESSINGML_NAMESPACE_URIS.has(getNamespaceUri(element) ?? "")) continue;
654
+ let root;
655
+ scanStreamingXmlElements(xml, ({ element, parent, nameEnd }) => {
656
+ if (parent === void 0) {
657
+ root = element;
658
+ return;
659
+ }
660
+ if (parent !== root || getLocalName(element.name) !== elementName || !WORDPROCESSINGML_NAMESPACE_URIS.has(getNamespaceUri(element) ?? "")) return;
656
661
  const idAttribute = findAttributeByNamespaceUri(element, WORDPROCESSINGML_NAMESPACE_URIS, "id");
657
- if (!element.name || !idAttribute) continue;
662
+ if (!element.name || !idAttribute) return;
663
+ const spelling = idAttribute.value.trim();
664
+ if (!/^[+-]?\d+$/u.test(spelling) || !Number.isSafeInteger(Number(spelling))) return;
665
+ const id = String(Number(spelling));
666
+ const offsets = scanElementRange(xml, nameEnd - element.name.length - 1, `<${element.name}`, `</${element.name}>`);
667
+ if (!offsets) return;
658
668
  const syntax = {
659
- elementName: element.name,
660
- idAttributeName: idAttribute.name,
669
+ offsets,
661
670
  elementPrefix: getNamespacePrefix(element.name) ?? "",
662
671
  attributePrefix: getNamespacePrefix(idAttribute.name) ?? ""
663
672
  };
664
- const entries = byId.get(idAttribute.value);
673
+ const entries = byId.get(id);
665
674
  if (entries) entries.push(syntax);
666
- else byId.set(idAttribute.value, [syntax]);
667
- }
675
+ else byId.set(id, [syntax]);
676
+ });
668
677
  return byId;
669
678
  };
670
- const syntaxLiteral = (syntax) => ({
671
- openLiteral: `<${syntax.elementName}`,
672
- closeTag: `</${syntax.elementName}>`,
673
- idAttr: syntax.idAttributeName
674
- });
675
- const extractNoteElement = (xml, syntax, id) => {
676
- const { openLiteral, closeTag, idAttr } = syntaxLiteral(syntax);
677
- return extractElementByIdAttr(xml, openLiteral, closeTag, idAttr, id);
678
- };
679
- const findNoteElement = (xml, syntax, id) => {
680
- const { openLiteral, closeTag, idAttr } = syntaxLiteral(syntax);
681
- return findElementByIdAttr(xml, openLiteral, closeTag, idAttr, id);
682
- };
679
+ const extractNoteElement = (xml, syntax) => xml.slice(syntax.offsets.start, syntax.offsets.end);
683
680
  const rewriteWordprocessingPrefixes = (xml, { source, target, sourceXmlnsDeclarations }) => {
684
681
  let rewritten = "";
685
682
  let quote = null;
@@ -824,7 +821,7 @@ function buildPatchedNotePartXml({ originalXml, baselineXml, serializedXml, repl
824
821
  const replacementSyntaxEntries = replacementElements.get(id);
825
822
  if (originalSyntaxEntries === void 0) {
826
823
  const replacementSyntax = currentSyntaxEntries.length === 1 && replacementSyntaxEntries?.length === 1 ? replacementSyntaxEntries[0] : void 0;
827
- const replacementNote = replacementSyntax ? extractNoteElement(replacementXml, replacementSyntax, id) : null;
824
+ const replacementNote = replacementSyntax ? extractNoteElement(replacementXml, replacementSyntax) : null;
828
825
  if (!replacementSyntax || !replacementNote) return {
829
826
  type: "refused",
830
827
  reason: "unroutable-paragraph"
@@ -836,24 +833,20 @@ function buildPatchedNotePartXml({ originalXml, baselineXml, serializedXml, repl
836
833
  });
837
834
  continue;
838
835
  }
839
- if (currentSyntaxEntries.length !== 1 || originalSyntaxEntries?.length !== 1 || replacementSyntaxEntries?.length !== 1) return {
836
+ if (currentSyntaxEntries.length !== 1 || !originalSyntaxEntries?.length || replacementSyntaxEntries?.length !== 1) return {
840
837
  type: "refused",
841
838
  reason: "unroutable-paragraph"
842
839
  };
843
840
  const currentSyntax = currentSyntaxEntries[0];
844
- const originalSyntax = originalSyntaxEntries[0];
841
+ const originalSyntax = originalSyntaxEntries.at(0);
845
842
  const replacementSyntax = replacementSyntaxEntries[0];
846
843
  if (!currentSyntax || !originalSyntax || !replacementSyntax) return {
847
844
  type: "refused",
848
845
  reason: "unroutable-paragraph"
849
846
  };
850
- const currentNote = extractNoteElement(serializedXml, currentSyntax, id);
851
- const originalOffsets = findNoteElement(originalXml, originalSyntax, id);
852
- const replacementNote = extractNoteElement(replacementXml, replacementSyntax, id);
853
- if (!currentNote || !originalOffsets || !replacementNote) return {
854
- type: "refused",
855
- reason: "unroutable-paragraph"
856
- };
847
+ const currentNote = extractNoteElement(serializedXml, currentSyntax);
848
+ const originalOffsets = originalSyntax.offsets;
849
+ const replacementNote = extractNoteElement(replacementXml, replacementSyntax);
857
850
  const originalNote = originalXml.slice(originalOffsets.start, originalOffsets.end);
858
851
  const currentParagraphs = paragraphRanges(currentNote, currentSyntax.elementPrefix);
859
852
  const originalParagraphs = paragraphRanges(originalNote, originalSyntax.elementPrefix);
@@ -861,7 +854,7 @@ function buildPatchedNotePartXml({ originalXml, baselineXml, serializedXml, repl
861
854
  const noteChangedParaIds = [...collectParaIds(currentNote).keys()].filter((paraId) => unroutedChangedParaIds.has(paraId));
862
855
  const baselineSyntaxEntries = baselineElements.get(id);
863
856
  const baselineSyntax = baselineSyntaxEntries?.length === 1 ? baselineSyntaxEntries[0] : void 0;
864
- const baselineNote = baselineSyntax ? extractNoteElement(baselineXml, baselineSyntax, id) : null;
857
+ const baselineNote = baselineSyntax ? extractNoteElement(baselineXml, baselineSyntax) : null;
865
858
  const reshaped = baselineSyntax !== void 0 && baselineNote !== null && baselineNote !== currentNote && noteShapeChanged({
866
859
  note: baselineNote,
867
860
  prefix: baselineSyntax.elementPrefix
@@ -910,26 +903,29 @@ function buildPatchedNotePartXml({ originalXml, baselineXml, serializedXml, repl
910
903
  for (const [id, baselineSyntaxEntries] of baselineElements) {
911
904
  if (currentElements.has(id) || baselineSyntaxEntries.length !== 1) continue;
912
905
  const originalSyntaxEntries = originalElements.get(id);
913
- const originalSyntax = originalSyntaxEntries?.length === 1 ? originalSyntaxEntries[0] : void 0;
914
- const originalOffsets = originalSyntax && findNoteElement(originalXml, originalSyntax, id);
915
- if (!originalOffsets) return {
906
+ if (!originalSyntaxEntries?.length) return {
916
907
  type: "refused",
917
908
  reason: "unroutable-paragraph"
918
909
  };
919
- const removal = {
920
- start: originalOffsets.start,
921
- end: originalOffsets.end,
922
- newXml: ""
923
- };
924
- paragraphSplices.push(removal);
925
- noteSplices.push(removal);
910
+ for (const { offsets } of originalSyntaxEntries) {
911
+ const removal = {
912
+ ...offsets,
913
+ newXml: ""
914
+ };
915
+ paragraphSplices.push(removal);
916
+ noteSplices.push(removal);
917
+ }
926
918
  }
927
919
  if (unroutedChangedParaIds.size > 0) return {
928
920
  type: "refused",
929
921
  reason: "unroutable-paragraph"
930
922
  };
931
923
  if (addedNotes.length > 0) {
932
- const appended = appendNotesSplice(originalXml, originalElements, addedNotes, { sourceXmlnsDeclarations: replacementXmlnsDeclarations });
924
+ const appended = appendNotesSplice({
925
+ originalElements,
926
+ addedNotes,
927
+ sourceXmlnsDeclarations: replacementXmlnsDeclarations
928
+ });
933
929
  if (!appended) return {
934
930
  type: "refused",
935
931
  reason: "unroutable-paragraph"
@@ -951,15 +947,11 @@ function buildPatchedNotePartXml({ originalXml, baselineXml, serializedXml, repl
951
947
  xml: wholeNotes
952
948
  };
953
949
  }
954
- /**
955
- * Insert new notes after the part's last note, spelled with that note's
956
- * prefixes. Null when the part holds no note to follow.
957
- */
958
- const appendNotesSplice = (originalXml, originalElements, addedNotes, { sourceXmlnsDeclarations }) => {
950
+ const appendNotesSplice = ({ originalElements, addedNotes, sourceXmlnsDeclarations }) => {
959
951
  let last = null;
960
- for (const [id, entries] of originalElements) for (const syntax of entries) {
961
- const offsets = findNoteElement(originalXml, syntax, id);
962
- if (offsets && (!last || offsets.end > last.end)) last = {
952
+ for (const entries of originalElements.values()) for (const syntax of entries) {
953
+ const offsets = syntax.offsets;
954
+ if (!last || offsets.end > last.end) last = {
963
955
  end: offsets.end,
964
956
  syntax
965
957
  };
@@ -1,6 +1,8 @@
1
1
  import { document_d_exports } from "../../types/document.js";
2
2
  import { Comment } from "../../types/content.js";
3
3
  //#region src/docx/serializer/commentSerializer.d.ts
4
+ /** An unrelated body edit must retain unmodelled duplicate comment definitions. */
5
+ declare const canReplayCommentsPart: (document: document_d_exports.Document, sourceXml: string) => boolean;
4
6
  /** An owned empty part survives deletion of its final thread. */
5
7
  declare const hasOwnedCommentsPart: (document: document_d_exports.Document) => boolean;
6
8
  /**
@@ -55,4 +57,4 @@ declare function serializeComments({ order, byId }: CommentPartPlan, sourceBindi
55
57
  */
56
58
  declare function serializeCommentsExtended(plan: CommentPartPlan): string | null;
57
59
  //#endregion
58
- export { CommentPartPlan, hasOwnedCommentsPart, planCommentParts, serializeComments, serializeCommentsExtended };
60
+ export { CommentPartPlan, canReplayCommentsPart, hasOwnedCommentsPart, planCommentParts, serializeComments, serializeCommentsExtended };
@@ -1,10 +1,13 @@
1
+ import { canonicalJson } from "../../utils/canonicalJson.js";
1
2
  import { deterministicHexId } from "../../utils/hexId.js";
2
3
  import { commentThreadParaId } from "../commentThreadKey.js";
3
4
  import { serializeWithPreservedChildren } from "../containerChildren.js";
5
+ import { getDocumentSourceBaseline } from "../headerFooterVerbatim.js";
4
6
  import { serializeParagraph } from "./paragraphSerializer.js";
5
7
  import { serializePartElement } from "./partNamespaces.js";
6
8
  import { serializeTextFormatting } from "./textFormattingSerializer.js";
7
- import { escapeXmlAttribute } from "@stll/docx-core";
9
+ import { Result } from "better-result";
10
+ import { assertValidOoxmlNumericIds, escapeXmlAttribute } from "@stll/docx-core";
8
11
  import { COMMENT_PART_RELATIONSHIPS } from "@stll/docx-core/model";
9
12
  //#region src/docx/serializer/commentSerializer.ts
10
13
  /**
@@ -12,6 +15,12 @@ import { COMMENT_PART_RELATIONSHIPS } from "@stll/docx-core/model";
12
15
  *
13
16
  * Serializes Comment[] to OOXML comments.xml format.
14
17
  */
18
+ /** An unrelated body edit must retain unmodelled duplicate comment definitions. */
19
+ const canReplayCommentsPart = (document, sourceXml) => {
20
+ if (Result.try(() => assertValidOoxmlNumericIds(sourceXml, "word/comments.xml")).isErr()) return false;
21
+ const baseline = getDocumentSourceBaseline(document);
22
+ return baseline.type === "captured" && canonicalJson(document.package.document.comments ?? []) === canonicalJson(baseline.body.comments ?? []);
23
+ };
15
24
  /** An owned empty part survives deletion of its final thread. */
16
25
  const hasOwnedCommentsPart = (document) => [...document.package.relationships?.values() ?? []].some(({ type }) => type === COMMENT_PART_RELATIONSHIPS.comments.type);
17
26
  const DEFAULT_ANNOTATION_REFERENCE_PROPERTIES = "<w:rPr><w:rStyle w:val=\"CommentReference\"/></w:rPr>";
@@ -200,4 +209,4 @@ function serializeCommentsExtended(plan) {
200
209
  });
201
210
  }
202
211
  //#endregion
203
- export { hasOwnedCommentsPart, planCommentParts, serializeComments, serializeCommentsExtended };
212
+ export { canReplayCommentsPart, hasOwnedCommentsPart, planCommentParts, serializeComments, serializeCommentsExtended };