@stll/folio-core 0.56.0 → 0.57.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 (142) hide show
  1. package/dist/ai-edits/apply.js +45 -14
  2. package/dist/ai-edits/batch-claims.d.ts +3 -1
  3. package/dist/ai-edits/batch-claims.js +25 -1
  4. package/dist/ai-edits/table-row-column-mutations.js +17 -3
  5. package/dist/ai-suggestions/apply.js +20 -1
  6. package/dist/compare/inline-atoms.js +40 -16
  7. package/dist/compare/inline-provenance.js +12 -5
  8. package/dist/compat/eigenpal.d.ts +2 -1
  9. package/dist/compat/eigenpal.js +2 -1
  10. package/dist/controller/canonicalClipboard.js +7 -1
  11. package/dist/controller/canonicalComments.d.ts +41 -0
  12. package/dist/controller/canonicalComments.js +152 -0
  13. package/dist/controller/canonicalComposition.d.ts +4 -0
  14. package/dist/controller/canonicalComposition.js +43 -11
  15. package/dist/controller/canonicalInlineProjection.d.ts +6 -1
  16. package/dist/controller/canonicalInlineProjection.js +86 -3
  17. package/dist/controller/canonicalInput.js +10 -1
  18. package/dist/controller/canonicalInputTimer.d.ts +10 -1
  19. package/dist/controller/canonicalInputTimer.js +24 -1
  20. package/dist/controller/canonicalPublicOperations.d.ts +2 -2
  21. package/dist/controller/canonicalPublicOperations.js +107 -15
  22. package/dist/controller/canonicalSelection.d.ts +18 -0
  23. package/dist/controller/canonicalSelection.js +32 -0
  24. package/dist/controller/canonicalSession.d.ts +3 -0
  25. package/dist/controller/canonicalSession.js +58 -24
  26. package/dist/controller/canonicalStoryEditor.d.ts +2 -0
  27. package/dist/controller/canonicalStoryEditor.js +20 -0
  28. package/dist/controller/folioEditor.js +13 -0
  29. package/dist/controller/headerFooterEditorManager.d.ts +1 -1
  30. package/dist/controller/headerFooterEditorManager.js +25 -11
  31. package/dist/controller/hiddenEditorApi.d.ts +13 -2
  32. package/dist/controller/hiddenEditorApi.js +4 -0
  33. package/dist/controller/hiddenEditorManager.d.ts +3 -1
  34. package/dist/controller/hiddenEditorManager.js +166 -13
  35. package/dist/controller/noteEditorManager.d.ts +1 -1
  36. package/dist/controller/noteEditorManager.js +28 -13
  37. package/dist/docx/builtInStyles.js +13 -6
  38. package/dist/docx/containerChildren.gen.d.ts +3 -1
  39. package/dist/docx/containerChildren.gen.js +33 -1
  40. package/dist/docx/fieldParser.js +14 -2
  41. package/dist/docx/hyperlinkParser.d.ts +1 -1
  42. package/dist/docx/hyperlinkParser.js +20 -1
  43. package/dist/docx/inlineWrapperContent.d.ts +4 -4
  44. package/dist/docx/inlineWrapperContent.js +4 -4
  45. package/dist/docx/linkedSdtPreservation.d.ts +10 -0
  46. package/dist/docx/linkedSdtPreservation.js +37 -0
  47. package/dist/docx/paragraphParser.js +88 -67
  48. package/dist/docx/paragraphPropertySource.d.ts +22 -1
  49. package/dist/docx/paragraphPropertySource.js +107 -1
  50. package/dist/docx/preservedRunContent.js +2 -1
  51. package/dist/docx/relsParser.js +26 -2
  52. package/dist/docx/rezip.js +9 -6
  53. package/dist/docx/selectiveSave.js +10 -8
  54. package/dist/docx/serializer/commentSerializer.d.ts +4 -1
  55. package/dist/docx/serializer/commentSerializer.js +7 -3
  56. package/dist/docx/serializer/paragraphSerializer.js +207 -94
  57. package/dist/docx/server/materializeYjsDocx.js +14 -6
  58. package/dist/docx/server/migrateYjsAttrSchema.js +17 -5
  59. package/dist/i18n/messages/ar.gen.js +4 -0
  60. package/dist/i18n/messages/catalogs.gen.d.ts +68 -0
  61. package/dist/i18n/messages/catalogs.gen.js +68 -0
  62. package/dist/i18n/messages/cs.gen.js +4 -0
  63. package/dist/i18n/messages/de.gen.js +4 -0
  64. package/dist/i18n/messages/en.gen.js +4 -0
  65. package/dist/i18n/messages/es.gen.js +4 -0
  66. package/dist/i18n/messages/et.gen.js +4 -0
  67. package/dist/i18n/messages/fr.gen.js +4 -0
  68. package/dist/i18n/messages/he.gen.js +4 -0
  69. package/dist/i18n/messages/hi.gen.js +4 -0
  70. package/dist/i18n/messages/hu.gen.js +4 -0
  71. package/dist/i18n/messages/lt.gen.js +4 -0
  72. package/dist/i18n/messages/lv.gen.js +4 -0
  73. package/dist/i18n/messages/messages.gen.d.ts +4 -0
  74. package/dist/i18n/messages/pl.gen.js +4 -0
  75. package/dist/i18n/messages/pt-BR.gen.js +4 -0
  76. package/dist/i18n/messages/sk.gen.js +4 -0
  77. package/dist/i18n/messages/tr.gen.js +4 -0
  78. package/dist/i18n/messages/zh-CN.gen.js +4 -0
  79. package/dist/index.d.ts +2 -1
  80. package/dist/index.js +2 -1
  81. package/dist/internal/revisionResolutionInline.js +25 -3
  82. package/dist/markdown/renderRuns.js +1 -11
  83. package/dist/panel-layout.d.ts +11 -9
  84. package/dist/panel-layout.js +13 -9
  85. package/dist/prosemirror/addTrackedDeletionMark.d.ts +18 -0
  86. package/dist/prosemirror/addTrackedDeletionMark.js +87 -0
  87. package/dist/prosemirror/attrs/index.js +5 -0
  88. package/dist/prosemirror/commands/comments.js +60 -35
  89. package/dist/prosemirror/commands/propertyChangeScope.js +3 -6
  90. package/dist/prosemirror/commands/resolveParagraphProperties.js +22 -13
  91. package/dist/prosemirror/commentOps.js +15 -1
  92. package/dist/prosemirror/conversion/fromProseDoc.js +22 -3
  93. package/dist/prosemirror/conversion/toProseDoc.js +119 -18
  94. package/dist/prosemirror/extensions/core/ParagraphExtension.js +22 -5
  95. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +74 -25
  96. package/dist/prosemirror/extensions/marks/FootnoteRefExtension.js +11 -0
  97. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +22 -14
  98. package/dist/prosemirror/extensions/marks/markUtils.js +8 -7
  99. package/dist/prosemirror/extensions/marks/noteReferenceDeletion.js +5 -2
  100. package/dist/prosemirror/fieldRepresentation.d.ts +8 -0
  101. package/dist/prosemirror/fieldRepresentation.js +18 -0
  102. package/dist/prosemirror/hyperlinkRemoval.d.ts +13 -1
  103. package/dist/prosemirror/hyperlinkRemoval.js +28 -9
  104. package/dist/prosemirror/listNumbering.js +11 -5
  105. package/dist/prosemirror/noteReferenceOccurrenceId.d.ts +5 -0
  106. package/dist/prosemirror/noteReferenceOccurrenceId.js +5 -0
  107. package/dist/prosemirror/noteReferenceOccurrences.d.ts +21 -0
  108. package/dist/prosemirror/noteReferenceOccurrences.js +185 -0
  109. package/dist/prosemirror/noteReferenceReview.js +14 -1
  110. package/dist/prosemirror/plugins/documentStyles.d.ts +4 -1
  111. package/dist/prosemirror/plugins/documentStyles.js +17 -10
  112. package/dist/prosemirror/plugins/suggestionMode.js +47 -49
  113. package/dist/prosemirror/preservedXmlReview.d.ts +6 -0
  114. package/dist/prosemirror/preservedXmlReview.js +35 -0
  115. package/dist/prosemirror/runFormattingFromMarks.js +2 -2
  116. package/dist/prosemirror/runFormattingReconciliation.js +5 -4
  117. package/dist/prosemirror/runPropertyChangeResolution.d.ts +8 -1
  118. package/dist/prosemirror/runPropertyChangeResolution.js +24 -2
  119. package/dist/prosemirror/runStyleFormatting.d.ts +3 -1
  120. package/dist/prosemirror/runStyleFormatting.js +3 -1
  121. package/dist/prosemirror/schema/marks.d.ts +1 -0
  122. package/dist/prosemirror/schema/nodes.d.ts +2 -0
  123. package/dist/prosemirror/styles/paragraphStyleCascade.js +4 -1
  124. package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +15 -15
  125. package/dist/prosemirror/styles/resolvedStyleAttrs.js +69 -28
  126. package/dist/prosemirror/textInput.d.ts +5 -1
  127. package/dist/prosemirror/textInput.js +3 -3
  128. package/dist/prosemirror/trackedRevisionPath.d.ts +5 -1
  129. package/dist/prosemirror/trackedRevisionPath.js +39 -1
  130. package/dist/prosemirror/validation.js +3 -5
  131. package/dist/prosemirror/yjsDocumentMetadata.d.ts +9 -3
  132. package/dist/prosemirror/yjsDocumentMetadata.js +37 -8
  133. package/dist/types/canonicalCapabilities.d.ts +6 -20
  134. package/dist/types/canonicalCapabilities.js +6 -20
  135. package/dist/types/canonicalComments.d.ts +51 -0
  136. package/dist/types/canonicalComments.js +0 -0
  137. package/dist/types/canonicalSections.d.ts +12 -0
  138. package/dist/types/canonicalSections.js +0 -0
  139. package/dist/utils/headingCollector.js +45 -4
  140. package/dist/utils/outlineDepth.d.ts +9 -0
  141. package/dist/utils/outlineDepth.js +9 -0
  142. package/package.json +2 -2
@@ -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 { planCommentParts, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
13
+ import { 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";
@@ -201,6 +201,7 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
201
201
  if (!validateFolioDocumentModel(doc).valid) return null;
202
202
  const comments = doc.package.document.comments ?? [];
203
203
  const hasComments = comments.length > 0;
204
+ const ownsCommentsPart = hasOwnedCommentsPart(doc);
204
205
  const commentPlan = planCommentParts(comments);
205
206
  try {
206
207
  const zip = await (await import("jszip")).default.loadAsync(originalBuffer);
@@ -259,25 +260,26 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
259
260
  updates.set("word/document.xml", patchedDocXml);
260
261
  }
261
262
  }
262
- const sourceCommentsFile = zip.file("word/comments.xml");
263
- if (hasComments || sourceCommentsFile) {
263
+ const sourceCommentsFile = findZipEntryCaseInsensitive(zip, "word/comments.xml");
264
+ const commentsPartPath = sourceCommentsFile?.name ?? "word/comments.xml";
265
+ if (hasComments || ownsCommentsPart || sourceCommentsFile) {
264
266
  const sourceCommentsXml = sourceCommentsFile ? await sourceCommentsFile.async("text") : void 0;
265
267
  const commentsXml = serializeComments(commentPlan, sourceCommentsXml === void 0 ? void 0 : readRootNamespaceBindings(sourceCommentsXml));
266
- if (commentsXml !== sourceCommentsXml) updates.set("word/comments.xml", commentsXml);
268
+ if (commentsXml !== sourceCommentsXml) updates.set(commentsPartPath, commentsXml);
267
269
  }
268
- if (hasComments) {
270
+ if (hasComments || ownsCommentsPart) {
269
271
  const ctFile = zip.file("[Content_Types].xml");
270
272
  if (ctFile) {
271
273
  const ctXml = await ctFile.async("text");
272
- if (!ctXml.includes("/word/comments.xml")) updates.set("[Content_Types].xml", ctXml.replace("</Types>", `<Override PartName="/word/comments.xml" ContentType="${COMMENTS_CONTENT_TYPE}"/></Types>`));
274
+ if (!ctXml.toLowerCase().includes("/word/comments.xml")) updates.set("[Content_Types].xml", ctXml.replace("</Types>", `<Override PartName="/${commentsPartPath}" ContentType="${COMMENTS_CONTENT_TYPE}"/></Types>`));
273
275
  }
274
276
  const relsPath = "word/_rels/document.xml.rels";
275
277
  const relsFile = zip.file(relsPath);
276
278
  if (relsFile) {
277
279
  const relsXml = await relsFile.async("text");
278
- if (!relsXml.includes("comments.xml")) {
280
+ if (!relsXml.toLowerCase().includes("comments.xml")) {
279
281
  const maxId = findMaxRId(relsXml);
280
- updates.set(relsPath, relsXml.replace("</Relationships>", `<Relationship Id="rId${maxId + 1}" Type="${RELATIONSHIP_TYPES.comments}" Target="comments.xml"/></Relationships>`));
282
+ updates.set(relsPath, relsXml.replace("</Relationships>", `<Relationship Id="rId${maxId + 1}" Type="${RELATIONSHIP_TYPES.comments}" Target="${commentsPartPath.slice(5)}"/></Relationships>`));
281
283
  }
282
284
  }
283
285
  }
@@ -1,5 +1,8 @@
1
+ import { document_d_exports } from "../../types/document.js";
1
2
  import { Comment } from "../../types/content.js";
2
3
  //#region src/docx/serializer/commentSerializer.d.ts
4
+ /** An owned empty part survives deletion of its final thread. */
5
+ declare const hasOwnedCommentsPart: (document: document_d_exports.Document) => boolean;
3
6
  /**
4
7
  * What every comment part is written from: one ordered list of comment ids, and
5
8
  * the facts each id carries.
@@ -52,4 +55,4 @@ declare function serializeComments({ order, byId }: CommentPartPlan, sourceBindi
52
55
  */
53
56
  declare function serializeCommentsExtended(plan: CommentPartPlan): string | null;
54
57
  //#endregion
55
- export { CommentPartPlan, planCommentParts, serializeComments, serializeCommentsExtended };
58
+ export { CommentPartPlan, hasOwnedCommentsPart, planCommentParts, serializeComments, serializeCommentsExtended };
@@ -5,12 +5,15 @@ import { serializeParagraph } from "./paragraphSerializer.js";
5
5
  import { serializePartElement } from "./partNamespaces.js";
6
6
  import { serializeTextFormatting } from "./textFormattingSerializer.js";
7
7
  import { escapeXmlAttribute } from "@stll/docx-core";
8
+ import { COMMENT_PART_RELATIONSHIPS } from "@stll/docx-core/model";
8
9
  //#region src/docx/serializer/commentSerializer.ts
9
10
  /**
10
11
  * Comment Serializer
11
12
  *
12
13
  * Serializes Comment[] to OOXML comments.xml format.
13
14
  */
15
+ /** An owned empty part survives deletion of its final thread. */
16
+ const hasOwnedCommentsPart = (document) => [...document.package.relationships?.values() ?? []].some(({ type }) => type === COMMENT_PART_RELATIONSHIPS.comments.type);
14
17
  const DEFAULT_ANNOTATION_REFERENCE_PROPERTIES = "<w:rPr><w:rStyle w:val=\"CommentReference\"/></w:rPr>";
15
18
  const PARAGRAPH_PROPERTIES_END = "</w:pPr>";
16
19
  const serializeAnnotationReference = (formatting) => {
@@ -169,7 +172,7 @@ function buildCommentExtendedEntries({ order, byId, threadParaIdById, threadedId
169
172
  entries.push({
170
173
  paraId,
171
174
  ...parentParaId !== void 0 ? { paraIdParent: parentParaId } : {},
172
- done: done ?? false
175
+ ...done === void 0 ? {} : { done }
173
176
  });
174
177
  }
175
178
  return entries.length > 0 ? entries : null;
@@ -185,7 +188,8 @@ function serializeCommentsExtended(plan) {
185
188
  if (!entries) return null;
186
189
  const body = entries.map((entry) => {
187
190
  const parentAttr = entry.paraIdParent !== void 0 ? ` w15:paraIdParent="${escapeXmlAttribute(entry.paraIdParent)}"` : "";
188
- return `<w15:commentEx w15:paraId="${escapeXmlAttribute(entry.paraId)}"${parentAttr} w15:done="${entry.done ? "1" : "0"}"/>`;
191
+ const doneAttr = entry.done === void 0 ? "" : ` w15:done="${entry.done ? "1" : "0"}"`;
192
+ return `<w15:commentEx w15:paraId="${escapeXmlAttribute(entry.paraId)}"${parentAttr}${doneAttr}/>`;
189
193
  }).join("");
190
194
  return XML_DECLARATION + serializePartElement({
191
195
  partPath: "word/commentsExtended.xml",
@@ -196,4 +200,4 @@ function serializeCommentsExtended(plan) {
196
200
  });
197
201
  }
198
202
  //#endregion
199
- export { planCommentParts, serializeComments, serializeCommentsExtended };
203
+ export { hasOwnedCommentsPart, planCommentParts, serializeComments, serializeCommentsExtended };
@@ -1,14 +1,14 @@
1
1
  import { modelParagraphFormattingEmission, serializeParagraphPropertySet } from "../../internal/paragraphFormattingSerialization.js";
2
2
  import { serializePreservedAttributes } from "../attributeRemainder.js";
3
- import { CONTAINER_CHILDREN } from "../containerChildren.gen.js";
3
+ import { CONTAINER_CHILDREN, TRACKED_RUN_WORDPROCESSING_CHILDREN } from "../containerChildren.gen.js";
4
4
  import { fieldStateAttributes } from "../fieldState.js";
5
5
  import { serializeEmptyMarkProperties } from "../paragraphMarkPropertyPresence.js";
6
- import { getParagraphPropertySource, paragraphPropertySourceMatchesEmission } from "../paragraphPropertySource.js";
6
+ import { getParagraphPropertySource, paragraphFormattingWithAuthoredIndentation, paragraphPropertySourceMatchesAuthoredIndentation, paragraphPropertySourceMatchesEmission } from "../paragraphPropertySource.js";
7
7
  import { ReviewResolutionProvenanceError, paragraphMarkResolutionJoinAttributes, serializeParagraphMarkResolutionJoin, serializeResolutionJoins } from "../reviewResolutionProvenance.js";
8
8
  import { DATE_UTC_ATTRIBUTE } from "../trackedChangeInfo.js";
9
9
  import { toTransitionalNamespaceUri } from "../transitionalSpelling.js";
10
10
  import { captureVerbatimXml, createCapturedXmlSanitizer } from "../verbatimCapture.js";
11
- import { NAMESPACES, OOXML_NAMESPACE_SCOPE, cloneElement, getChildElements, getLocalName, parseXml } from "../xmlParser.js";
11
+ import { NAMESPACES, OOXML_NAMESPACE_SCOPE, WORDPROCESSINGML_NAMESPACE_URIS, cloneElement, getChildElements, getLocalName, parseXml } from "../xmlParser.js";
12
12
  import { writeGroupTextBoxesBack } from "./groupTextBoxWriteBack.js";
13
13
  import { markupRangeAttributes, moveBookmarkAttributes, serializeBookmarkMarker } from "./markupRangeAttributes.js";
14
14
  import { serializeRun, serializeShapeTextBody } from "./runSerializer.js";
@@ -248,7 +248,7 @@ const withParagraphMarkChange = (sourceXml, mark) => {
248
248
  }
249
249
  return captureVerbatimXml(cloneElement(root, { elements }));
250
250
  };
251
- const serializeParagraphFormattingWithOptions = (formatting, { propertyChanges, paragraphMarkChange, propertySource, sectionProperties } = {}) => {
251
+ const serializeParagraphFormattingWithOptions = (formatting, { propertyChanges, paragraphMarkChange, propertySource, sourceParagraph, sectionProperties } = {}) => {
252
252
  const modeledFormatting = modelParagraphFormattingEmission(formatting);
253
253
  const serializablePropertyChangeCount = propertyChanges?.length ?? 0;
254
254
  if (serializablePropertyChangeCount > 1) panic("A paragraph cannot serialize more than one w:pPrChange", { count: serializablePropertyChangeCount });
@@ -260,13 +260,13 @@ const serializeParagraphFormattingWithOptions = (formatting, { propertyChanges,
260
260
  sectionPropertiesXml,
261
261
  ...propertyChangesXml
262
262
  ].some((xml) => xml.includes(`${DATE_UTC_ATTRIBUTE}=`));
263
- const verifiedSource = verifiedParagraphPropertySource(modeledFormatting, propertySource);
263
+ const verifiedSource = sourceParagraph && !paragraphPropertySourceMatchesAuthoredIndentation(sourceParagraph) ? null : verifiedParagraphPropertySource(modeledFormatting, propertySource);
264
264
  if (verifiedSource !== null && (!composedChildrenUseDateUtc || !sourceShadowsDateUtcPrefix(verifiedSource))) {
265
265
  const sourceWithMark = paragraphMarkChange ? withParagraphMarkChange(verifiedSource, paragraphMarkChange) : verifiedSource;
266
266
  return withTrailingParagraphPropertyChildren(sourceWithMark, [sectionPropertiesXml, ...propertyChangesXml]);
267
267
  }
268
268
  return serializeParagraphPropertySet({
269
- formatting,
269
+ formatting: sourceParagraph ? paragraphFormattingWithAuthoredIndentation(sourceParagraph) : formatting,
270
270
  markPropertiesPrefixXml: paragraphMarkXml,
271
271
  sectionPropertiesXml,
272
272
  propertyChangesXml
@@ -320,15 +320,7 @@ function serializeHyperlink(hyperlink, disposition = "kept") {
320
320
  /** Serialize a simple field without changing its authored OOXML field form. */
321
321
  function serializeSimpleField(field) {
322
322
  const attrs = [`w:instr="${escapeXmlAttribute(field.instruction)}"`, ...fieldStateAttributes(field)];
323
- const contentXml = field.content.map((item) => {
324
- switch (item.type) {
325
- case "run": return serializeRun(item);
326
- case "hyperlink": return serializeHyperlink(item);
327
- case "inlineWrapper": return serializeParagraphContent(item);
328
- case "preservedInline": return item.xml;
329
- default: return item;
330
- }
331
- }).join("");
323
+ const contentXml = field.content.map((item) => serializeParagraphContent(item)).join("");
332
324
  return `<w:fldSimple ${attrs.join(" ")}>${contentXml}</w:fldSimple>`;
333
325
  }
334
326
  const replayableFormFieldDataXml = createCapturedXmlSanitizer({
@@ -369,27 +361,67 @@ function serializeRemovedPreservedInline(item) {
369
361
  if (item.foldedListNumber) throw new UnrepresentableTrackedCaptureError({ message: "A folded list-number field holds markup that cannot be written as deleted content." });
370
362
  return item.xml;
371
363
  }
372
- var UnrepresentableTrackedSimpleFieldError = class extends TaggedError("UnrepresentableTrackedSimpleFieldError") {};
373
- /** A revision can contain run-level field characters, but not `w:fldSimple`. */
374
- function serializeTrackedSimpleField(field) {
375
- const fieldResult = [];
364
+ var UnrepresentableTrackedFieldChildError = class extends TaggedError("UnrepresentableTrackedFieldChildError") {};
365
+ const TRACKED_FIELD_CAPTURE_NAMES = new Set(TRACKED_RUN_WORDPROCESSING_CHILDREN);
366
+ const replayableFieldDataXml = createCapturedXmlSanitizer({
367
+ allowedLocalNames: /* @__PURE__ */ new Set(["fldData"]),
368
+ allowedNamespaceUris: WORDPROCESSINGML_NAMESPACE_URIS,
369
+ inheritedNamespaceScope: OOXML_NAMESPACE_SCOPE
370
+ });
371
+ /** Lower a tracked field into run-level structure before revision/link segmentation. */
372
+ const trackedSimpleFieldContent = (field) => {
373
+ const formatting = field.content.find((item) => item.type === "run")?.formatting;
374
+ const structuralRun = (content) => ({
375
+ type: "run",
376
+ content,
377
+ ...formatting && { formatting }
378
+ });
379
+ let fieldDataXml = null;
380
+ const result = [];
376
381
  for (const item of field.content) {
377
- if (item.type !== "run") throw new UnrepresentableTrackedSimpleFieldError({
378
- message: `A tracked simple field with ${item.type} result content cannot be serialized as valid OOXML.`,
379
- contentType: item.type
380
- });
381
- fieldResult.push(item);
382
+ if (item.type !== "preservedInline") {
383
+ result.push(item);
384
+ continue;
385
+ }
386
+ const captured = parseXml(item.xml, OOXML_NAMESPACE_SCOPE);
387
+ const root = captured.elements?.at(0);
388
+ if (captured.elements?.length !== 1 || !root || !WORDPROCESSINGML_NAMESPACE_URIS.has(root.namespaceUri ?? "")) throw new UnrepresentableTrackedFieldChildError({ message: "A tracked field capture has no representable revision-level home." });
389
+ const name = getLocalName(root.name);
390
+ if (name === "fldData") {
391
+ const data = replayableFieldDataXml(item.xml);
392
+ if (!data || fieldDataXml !== null) throw new UnrepresentableTrackedFieldChildError({ message: "A tracked field requires at most one valid fldData child." });
393
+ fieldDataXml = data;
394
+ continue;
395
+ }
396
+ if (!TRACKED_FIELD_CAPTURE_NAMES.has(name)) throw new UnrepresentableTrackedFieldChildError({ message: `A tracked field capture cannot lower ${name} into a revision wrapper.` });
397
+ result.push(item);
382
398
  }
383
- return serializeComplexField({
384
- type: "complexField",
385
- instruction: field.instruction,
386
- fieldType: field.fieldType,
387
- fieldCode: [],
388
- fieldResult,
389
- ...field.fldLock !== void 0 && { fldLock: field.fldLock },
390
- ...field.dirty !== void 0 && { dirty: field.dirty }
391
- });
392
- }
399
+ return [
400
+ fieldDataXml ? structuralRun([{
401
+ type: "preservedXml",
402
+ text: "",
403
+ xml: `<w:fldChar w:fldCharType="begin"${fieldStateAttributes(field).map((attr) => ` ${attr}`).join("")}>${fieldDataXml}</w:fldChar>`
404
+ }]) : structuralRun([{
405
+ type: "fieldChar",
406
+ charType: "begin",
407
+ ...field.fldLock !== void 0 && { fldLock: field.fldLock },
408
+ ...field.dirty !== void 0 && { dirty: field.dirty }
409
+ }]),
410
+ ...field.instruction.length ? [structuralRun([{
411
+ type: "instrText",
412
+ text: field.instruction
413
+ }])] : [],
414
+ structuralRun([{
415
+ type: "fieldChar",
416
+ charType: "separate"
417
+ }]),
418
+ ...result,
419
+ structuralRun([{
420
+ type: "fieldChar",
421
+ charType: "end"
422
+ }])
423
+ ];
424
+ };
393
425
  /**
394
426
  * Serialize an inline SDT (w:sdt).
395
427
  *
@@ -459,63 +491,143 @@ function trackedChangeTag(change) {
459
491
  }
460
492
  }
461
493
  function serializeTrackedChange(tag, change) {
462
- if (change.resolutionJoins !== void 0 && change.content.some((item) => item.type === "hyperlink")) throw new ReviewResolutionProvenanceError({
494
+ return emitTrackedInlineSegments(trackedChangeSegments({
495
+ tag,
496
+ change
497
+ }));
498
+ }
499
+ const emitTrackedInlineSegments = (segments) => segments.map((segment) => {
500
+ switch (segment.type) {
501
+ case "plain": return segment.xml;
502
+ case "hyperlink": return `<w:hyperlink${hyperlinkAttributes(segment.hyperlink)}>${segment.xml}</w:hyperlink>`;
503
+ default: return segment;
504
+ }
505
+ }).join("");
506
+ /** Wrap contiguous plain content once, and carry wrappers inside each lifted link. */
507
+ const wrapTrackedInlineSegments = (segments, wrap) => {
508
+ if (segments.length === 0) return [{
509
+ type: "plain",
510
+ xml: wrap("")
511
+ }];
512
+ const result = [];
513
+ const pending = [];
514
+ const flush = () => {
515
+ if (pending.length === 0) return;
516
+ result.push({
517
+ type: "plain",
518
+ xml: wrap(pending.join(""))
519
+ });
520
+ pending.length = 0;
521
+ };
522
+ for (const segment of segments) switch (segment.type) {
523
+ case "plain":
524
+ pending.push(segment.xml);
525
+ break;
526
+ case "hyperlink":
527
+ flush();
528
+ result.push({
529
+ type: "hyperlink",
530
+ hyperlink: segment.hyperlink,
531
+ xml: wrap(segment.xml)
532
+ });
533
+ break;
534
+ default: return segment;
535
+ }
536
+ flush();
537
+ return result;
538
+ };
539
+ const trackedChangeSegments = ({ tag, change }) => {
540
+ const disposition = tag === "del" || tag === "moveFrom" ? "removed" : "kept";
541
+ const segments = walkTrackedInlineContent(change.content, disposition);
542
+ if (change.resolutionJoins !== void 0 && segments.some((segment) => segment.type === "hyperlink")) throw new ReviewResolutionProvenanceError({
463
543
  attribute: "resolutionJoins",
464
544
  reason: "unrepresentable",
465
545
  message: "Tracked-resolution provenance cannot be repartitioned across serialized hyperlinks."
466
546
  });
467
547
  const attrs = serializeTrackedChangeAttributes(change.info) + serializeResolutionJoins(change.resolutionJoins);
468
- const disposition = tag === "del" || tag === "moveFrom" ? "removed" : "kept";
469
- const serializeContentRun = (run) => serializeInlineRun(run, disposition);
470
- const serializeWrappedItem = (item) => {
471
- switch (item.type) {
472
- case "run": return serializeContentRun(item);
473
- case "simpleField":
474
- case "complexField": {
475
- const xml = item.type === "simpleField" ? serializeTrackedSimpleField(item) : serializeComplexField(item);
476
- return disposition === "removed" ? rewriteRunTextAsDeleted(xml) : xml;
477
- }
478
- case "mathEquation": return item.ommlXml;
479
- case "insertion":
480
- case "deletion":
481
- case "moveFrom":
482
- case "moveTo": return serializeTrackedChange(trackedChangeTag(item), item);
483
- case "bookmarkStart":
484
- case "bookmarkEnd": return serializeBookmarkMarker(item);
485
- case "moveFromRangeStart": return serializeMoveRangeStart("moveFromRangeStart", item);
486
- case "moveFromRangeEnd": return `<w:moveFromRangeEnd ${markupRangeAttributes(item).join(" ")}/>`;
487
- case "moveToRangeStart": return serializeMoveRangeStart("moveToRangeStart", item);
488
- case "moveToRangeEnd": return `<w:moveToRangeEnd ${markupRangeAttributes(item).join(" ")}/>`;
489
- case "preservedInline": return disposition === "removed" ? serializeRemovedPreservedInline(item) : item.xml;
490
- case "inlineWrapper":
491
- case "inlineSdt": return serializeParagraphContent(item, disposition);
492
- default: return item;
493
- }
494
- };
495
- const open = `<w:${tag} ${attrs}>`;
496
- const close = `</w:${tag}>`;
497
- const wrap = (inner) => inner.length === 0 ? "" : `${open}${inner}${close}`;
498
- if (change.content.length === 0) return `${open}${close}`;
548
+ return wrapTrackedInlineSegments(segments, (xml) => `<w:${tag} ${attrs}>${xml}</w:${tag}>`);
549
+ };
550
+ var UnrepresentableNestedHyperlinkError = class extends TaggedError("UnrepresentableNestedHyperlinkError") {};
551
+ /** One exhaustive recursive owner for all containers inside a tracked revision. */
552
+ const walkTrackedInlineContent = (content, disposition) => {
499
553
  const segments = [];
500
- const pending = [];
501
- const flushPending = () => {
502
- if (pending.length > 0) {
503
- segments.push(wrap(pending.join("")));
504
- pending.length = 0;
554
+ for (const item of content) switch (item.type) {
555
+ case "hyperlink": {
556
+ const children = walkTrackedInlineContent(item.children, disposition);
557
+ if (children.some((child) => child.type === "hyperlink")) throw new UnrepresentableNestedHyperlinkError({ message: "A tracked hyperlink cannot contain another hyperlink." });
558
+ segments.push({
559
+ type: "hyperlink",
560
+ hyperlink: item,
561
+ xml: emitTrackedInlineSegments(children)
562
+ });
563
+ break;
505
564
  }
506
- };
507
- for (const item of change.content) {
508
- if (item.type === "hyperlink") {
509
- flushPending();
510
- const childrenXml = item.children.map((child) => serializeHyperlinkChild(child, disposition)).join("");
511
- segments.push(`<w:hyperlink${hyperlinkAttributes(item)}>${open}${childrenXml}${close}</w:hyperlink>`);
512
- continue;
565
+ case "simpleField":
566
+ segments.push(...walkTrackedInlineContent(trackedSimpleFieldContent(item), disposition));
567
+ break;
568
+ case "insertion":
569
+ case "deletion":
570
+ case "moveFrom":
571
+ case "moveTo":
572
+ segments.push(...trackedChangeSegments({
573
+ tag: trackedChangeTag(item),
574
+ change: item
575
+ }));
576
+ break;
577
+ case "inlineWrapper":
578
+ segments.push(...wrapTrackedInlineSegments(walkTrackedInlineContent(item.content, disposition), (xml) => serializeInlineWrapperMarkup(item, xml)));
579
+ break;
580
+ case "inlineSdt":
581
+ segments.push({
582
+ type: "plain",
583
+ xml: serializeSdtWrapper(item.properties, emitTrackedInlineSegments(walkTrackedInlineContent(item.content, disposition)))
584
+ });
585
+ break;
586
+ case "complexField": {
587
+ const xml = serializeComplexField(item);
588
+ segments.push({
589
+ type: "plain",
590
+ xml: disposition === "removed" ? rewriteRunTextAsDeleted(xml) : xml
591
+ });
592
+ break;
593
+ }
594
+ case "preservedInline": {
595
+ const pending = getChildElements(parseXml(item.xml, OOXML_NAMESPACE_SCOPE));
596
+ while (pending.length > 0) {
597
+ const captured = pending.pop();
598
+ if (!captured) continue;
599
+ const wordprocessing = WORDPROCESSINGML_NAMESPACE_URIS.has(captured.namespaceUri ?? "");
600
+ const name = getLocalName(captured.name);
601
+ if (wordprocessing && (name === "p" || name === "sdt")) continue;
602
+ if (wordprocessing && name === "hyperlink") throw new UnrepresentableTrackedCaptureError({ message: "A tracked capture containing a hyperlink requires typed content for lifting." });
603
+ pending.push(...getChildElements(captured));
604
+ }
605
+ segments.push({
606
+ type: "plain",
607
+ xml: disposition === "removed" ? serializeRemovedPreservedInline(item) : item.xml
608
+ });
609
+ break;
513
610
  }
514
- pending.push(serializeWrappedItem(item));
611
+ case "run":
612
+ case "mathEquation":
613
+ case "bookmarkStart":
614
+ case "bookmarkEnd":
615
+ case "moveFromRangeStart":
616
+ case "moveFromRangeEnd":
617
+ case "moveToRangeStart":
618
+ case "moveToRangeEnd":
619
+ case "commentRangeStart":
620
+ case "commentRangeEnd":
621
+ case "commentReference":
622
+ segments.push({
623
+ type: "plain",
624
+ xml: serializeParagraphContent(item, disposition)
625
+ });
626
+ break;
627
+ default: return item;
515
628
  }
516
- flushPending();
517
- return segments.join("");
518
- }
629
+ return segments;
630
+ };
519
631
  /** The properties element each tagged wrapper declares ahead of its content. */
520
632
  const TAGGED_WRAPPER_PROPERTIES = {
521
633
  smartTag: "smartTagPr",
@@ -552,6 +664,17 @@ function serializeTaggedWrapper(kind, wrapper, inner) {
552
664
  function serializeCommentReferenceRun(id) {
553
665
  return `<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="${id}"/></w:r>`;
554
666
  }
667
+ function serializeInlineWrapperMarkup(content, inner) {
668
+ switch (content.kind) {
669
+ case "bidi": {
670
+ const tag = content.control === "override" ? "bdo" : "dir";
671
+ return `<w:${tag}${content.direction === void 0 ? "" : ` w:val="${escapeXmlAttribute(content.direction)}"`}>${inner}</w:${tag}>`;
672
+ }
673
+ case "smartTag": return serializeTaggedWrapper("smartTag", content, inner);
674
+ case "customXml": return serializeTaggedWrapper("customXml", content, inner);
675
+ default: return content;
676
+ }
677
+ }
555
678
  /**
556
679
  * Serialize a single paragraph content item.
557
680
  *
@@ -580,18 +703,7 @@ function serializeParagraphContent(content, disposition = "kept") {
580
703
  case "moveFromRangeEnd": return `<w:moveFromRangeEnd ${markupRangeAttributes(content).join(" ")}/>`;
581
704
  case "moveToRangeStart": return serializeMoveRangeStart("moveToRangeStart", content);
582
705
  case "moveToRangeEnd": return `<w:moveToRangeEnd ${markupRangeAttributes(content).join(" ")}/>`;
583
- case "inlineWrapper": {
584
- const inner = content.content.map((child) => serializeParagraphContent(child, disposition)).join("");
585
- switch (content.kind) {
586
- case "bidi": {
587
- const tag = content.control === "override" ? "bdo" : "dir";
588
- return `<w:${tag}${content.direction === void 0 ? "" : ` w:val="${escapeXmlAttribute(content.direction)}"`}>${inner}</w:${tag}>`;
589
- }
590
- case "smartTag": return serializeTaggedWrapper("smartTag", content, inner);
591
- case "customXml": return serializeTaggedWrapper("customXml", content, inner);
592
- default: return content;
593
- }
594
- }
706
+ case "inlineWrapper": return serializeInlineWrapperMarkup(content, content.content.map((child) => serializeParagraphContent(child, disposition)).join(""));
595
707
  case "mathEquation": return content.ommlXml || "";
596
708
  case "preservedInline": return content.xml;
597
709
  default: return "";
@@ -617,6 +729,7 @@ function serializeParagraph(paragraph) {
617
729
  propertyChanges: paragraph.propertyChanges,
618
730
  paragraphMarkChange: paragraph.pPrMark,
619
731
  propertySource: getParagraphPropertySource(paragraph),
732
+ sourceParagraph: paragraph,
620
733
  sectionProperties: paragraph.sectionProperties
621
734
  }));
622
735
  let pendingRenderedPageBreak = paragraph.renderedPageBreakBefore === true;
@@ -1,6 +1,6 @@
1
1
  import { fromProseDoc } from "../../prosemirror/conversion/fromProseDoc.js";
2
2
  import { schema } from "../../prosemirror/schema/index.js";
3
- import { applyAttrSchemaMigrations, readYjsAttrSchemaVersion, readYjsParagraphSourceContract, withParagraphSourceContract } from "../../prosemirror/yjsDocumentMetadata.js";
3
+ import { FolioYjsNoteReferenceSchemaError, applyAttrSchemaMigrations, readYjsAttrSchemaVersion, readYjsParagraphSourceContract, withParagraphSourceContract } from "../../prosemirror/yjsDocumentMetadata.js";
4
4
  import { ParagraphPropertySourceValidationError } from "../paragraphPropertySource.js";
5
5
  import { parseDocx } from "../parser.js";
6
6
  import { repackDocx } from "../rezip.js";
@@ -55,11 +55,19 @@ const readProseMirrorDocument = (yjsUpdate) => {
55
55
  });
56
56
  return withParagraphSourceContract(initProseMirrorDoc(fragment, schema).doc, contract);
57
57
  },
58
- catch: (cause) => cause instanceof FolioYjsDocxMaterializationError ? cause : new FolioYjsDocxMaterializationError({
59
- code: "invalid_update",
60
- message: "Yjs update is not a valid Folio collaboration snapshot.",
61
- cause
62
- })
58
+ catch: (cause) => {
59
+ if (cause instanceof FolioYjsDocxMaterializationError) return cause;
60
+ if (cause instanceof FolioYjsNoteReferenceSchemaError) return new FolioYjsDocxMaterializationError({
61
+ code: "stale_attr_schema",
62
+ message: cause.message,
63
+ cause
64
+ });
65
+ return new FolioYjsDocxMaterializationError({
66
+ code: "invalid_update",
67
+ message: "Yjs update is not a valid Folio collaboration snapshot.",
68
+ cause
69
+ });
70
+ }
63
71
  });
64
72
  ydoc.destroy();
65
73
  if (parsed.isOk()) return parsed.value;
@@ -1,4 +1,4 @@
1
- import { applyAttrSchemaMigrations, readYjsAttrSchemaVersion } from "../../prosemirror/yjsDocumentMetadata.js";
1
+ import { FolioYjsNoteReferenceSchemaError, applyAttrSchemaMigrations, readYjsAttrSchemaVersion } from "../../prosemirror/yjsDocumentMetadata.js";
2
2
  import { FOLIO_YJS_PROSEMIRROR_FRAGMENT_NAME } from "./materializeYjsDocx.js";
3
3
  import { Result, TaggedError } from "better-result";
4
4
  import * as Y from "yjs";
@@ -72,22 +72,34 @@ const migrateFolioYjsSnapshot = (update) => {
72
72
  }));
73
73
  }
74
74
  const fromVersion = read.value;
75
- if (fromVersion === 11) {
75
+ const migratedAttrs = Result.try({
76
+ try: () => applyAttrSchemaMigrations(ydoc, ydoc.getXmlFragment(FOLIO_YJS_PROSEMIRROR_FRAGMENT_NAME), fromVersion),
77
+ catch: (cause) => new FolioYjsSnapshotMigrationError({
78
+ cause,
79
+ code: "unsupported_version",
80
+ message: cause instanceof FolioYjsNoteReferenceSchemaError ? cause.message : "Cannot migrate the collaboration attribute schema."
81
+ })
82
+ });
83
+ if (migratedAttrs.isErr()) {
84
+ ydoc.destroy();
85
+ return Result.err(migratedAttrs.error);
86
+ }
87
+ if (fromVersion === 12) {
76
88
  ydoc.destroy();
77
89
  return Result.ok({
78
90
  fromVersion,
79
91
  paragraphsRewritten: 0,
80
- toVersion: 11,
92
+ toVersion: 12,
81
93
  update
82
94
  });
83
95
  }
84
- const paragraphsRewritten = applyAttrSchemaMigrations(ydoc, ydoc.getXmlFragment(FOLIO_YJS_PROSEMIRROR_FRAGMENT_NAME), fromVersion);
96
+ const paragraphsRewritten = migratedAttrs.value;
85
97
  const migrated = Y.encodeStateAsUpdate(ydoc);
86
98
  ydoc.destroy();
87
99
  return Result.ok({
88
100
  fromVersion,
89
101
  paragraphsRewritten,
90
- toVersion: 11,
102
+ toVersion: 12,
91
103
  update: migrated
92
104
  });
93
105
  };
@@ -337,6 +337,10 @@ const messages = { "folio": {
337
337
  "editor": {
338
338
  "editing": "تحرير",
339
339
  "editingDescription": "تحرير المستند مباشرةً",
340
+ "outlineDepthAll": "كل المستويات",
341
+ "outlineDepthLabel": "إظهار العناوين",
342
+ "outlineDepthThree": "المستويات 1–3",
343
+ "outlineDepthTwo": "المستويان 1–2",
340
344
  "outlineTitle": "المخطط",
341
345
  "showDocumentOutline": "مخطط المستند",
342
346
  "suggesting": "تعقب التغييرات",