@stll/folio-core 0.47.5 → 0.47.6

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.
@@ -1,5 +1,5 @@
1
1
  import { document_d_exports } from "../types/document.js";
2
- import { EditorView } from "prosemirror-view";
2
+ import { DirectEditorProps, EditorView } from "prosemirror-view";
3
3
  //#region src/controller/headerFooterEditorManager.d.ts
4
4
  type HeaderFooterPartKind = "header" | "footer";
5
5
  type HeaderFooterPartKey = {
@@ -14,6 +14,7 @@ type HeaderFooterEditorTransaction = {
14
14
  view: EditorView;
15
15
  };
16
16
  type HeaderFooterEditorManagerDeps = {
17
+ createView?: ((mountNode: HTMLElement, props: DirectEditorProps) => EditorView) | undefined;
17
18
  getDocument: () => document_d_exports.Document | null;
18
19
  getHost: () => HTMLElement | null;
19
20
  getStyles: () => document_d_exports.StyleDefinitions | null | undefined;
@@ -12,6 +12,7 @@ import { schema } from "../prosemirror/schema/index.js";
12
12
  import { EditorState } from "prosemirror-state";
13
13
  import { EditorView } from "prosemirror-view";
14
14
  //#region src/controller/headerFooterEditorManager.ts
15
+ const createEditorView = (mountNode, props) => new EditorView(mountNode, props);
15
16
  const DETACHED_WATERMARK_HOST = Symbol.for("stll.detachedWatermarkHost");
16
17
  const headerFooterToProseDocWithDetachedWatermarkHost = (headerFooter, options) => {
17
18
  return headerFooterToProseDoc(headerFooter.content.map((block, blockIndex) => {
@@ -109,7 +110,7 @@ const createHeaderFooterEditorManager = (deps) => {
109
110
  mountNode.dataset["hfRId"] = part.rId;
110
111
  mountNode.dataset["hfKind"] = part.kind;
111
112
  host.append(mountNode);
112
- const view = new EditorView(mountNode, {
113
+ const view = (deps.createView ?? createEditorView)(mountNode, {
113
114
  state: buildInitialState(headerFooter, styles, theme, numbering, manager),
114
115
  dispatchTransaction(transaction) {
115
116
  const nextState = view.state.apply(transaction);
@@ -1,7 +1,7 @@
1
1
  import { document_d_exports } from "../types/document.js";
2
2
  import { NoteStoryKey } from "../types/editor-story.js";
3
3
  import { Plugin } from "prosemirror-state";
4
- import { EditorView } from "prosemirror-view";
4
+ import { DirectEditorProps, EditorView } from "prosemirror-view";
5
5
  //#region src/controller/noteEditorManager.d.ts
6
6
  type NoteStoryKind = NoteStoryKey["kind"];
7
7
  type NoteEditorTransaction = NoteStoryKey & {
@@ -10,6 +10,7 @@ type NoteEditorTransaction = NoteStoryKey & {
10
10
  view: EditorView;
11
11
  };
12
12
  type NoteEditorManagerDeps = {
13
+ createView?: ((mountNode: HTMLElement, props: DirectEditorProps) => EditorView) | undefined;
13
14
  getDocument: () => document_d_exports.Document | null;
14
15
  getHost: () => HTMLElement | null;
15
16
  getPlugins?: (() => Plugin[]) | undefined;
@@ -12,6 +12,7 @@ import { EditorState } from "prosemirror-state";
12
12
  import { EditorView } from "prosemirror-view";
13
13
  //#region src/controller/noteEditorManager.ts
14
14
  /** Framework-neutral lifecycle owner for editable footnote and endnote stories. */
15
+ const createEditorView = (mountNode, props) => new EditorView(mountNode, props);
15
16
  const EMPTY_PLUGINS = [];
16
17
  const storyMapKey = ({ kind, noteId }) => `${kind}:${String(noteId)}`;
17
18
  const isNormalFootnote = (note) => !isSeparatorFootnote(note);
@@ -110,7 +111,7 @@ const createNoteEditorManager = (deps) => {
110
111
  mountNode.dataset["noteId"] = String(storyKey.noteId);
111
112
  host.append(mountNode);
112
113
  const proseDocument = noteToProseDocument(note, styles, theme);
113
- const view = new EditorView(mountNode, {
114
+ const view = (deps.createView ?? createEditorView)(mountNode, {
114
115
  state: buildInitialState(proseDocument, styles, numbering, manager, externalPlugins),
115
116
  dispatchTransaction(transaction) {
116
117
  view.updateState(view.state.apply(transaction));
@@ -1,6 +1,7 @@
1
1
  import { parseCommentsExtended } from "./commentParser.js";
2
2
  import { withoutOrphanCommentRanges } from "./commentRangeIntegrity.js";
3
3
  import { hasUnsynthesizedReplyRanges } from "./commentReplyMarkers.js";
4
+ import { detectDocxConformanceClass } from "./conformance.js";
4
5
  import { validateFolioDocumentModel } from "./modelValidation.js";
5
6
  import { parseNumbering } from "./numberingParser.js";
6
7
  import { isUnsafePackagePath } from "./packageParts.js";
@@ -13,6 +14,7 @@ import { serializeDocument } from "./serializer/documentSerializer.js";
13
14
  import { serializeEndnotes, serializeFootnotes } from "./serializer/noteSerializer.js";
14
15
  import { serializeNumberingXml } from "./serializer/numberingSerializer.js";
15
16
  import { readRootNamespaceBindings } from "./serializer/partNamespaces.js";
17
+ import { DOCX_CONFORMANCE_CLASSES } from "@stll/docx-core/model";
16
18
  //#region src/docx/selectiveSave.ts
17
19
  /**
18
20
  * Splice edited footnote/endnote paragraphs into their parts and return the
@@ -217,8 +219,9 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
217
219
  }
218
220
  const sourceCommentsFile = zip.file("word/comments.xml");
219
221
  if (hasComments || sourceCommentsFile) {
220
- const sourceBindings = sourceCommentsFile ? readRootNamespaceBindings(await sourceCommentsFile.async("text")) : void 0;
221
- updates.set("word/comments.xml", serializeComments(commentPlan, sourceBindings));
222
+ const sourceCommentsXml = sourceCommentsFile ? await sourceCommentsFile.async("text") : void 0;
223
+ const commentsXml = serializeComments(commentPlan, sourceCommentsXml === void 0 ? void 0 : readRootNamespaceBindings(sourceCommentsXml));
224
+ if (commentsXml !== sourceCommentsXml) updates.set("word/comments.xml", commentsXml);
222
225
  }
223
226
  if (hasComments) {
224
227
  const ctFile = zip.file("[Content_Types].xml");
@@ -245,6 +248,12 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
245
248
  const corePropsXml = await corePropsFile.async("text");
246
249
  updates.set("docProps/core.xml", updateCoreProperties(corePropsXml, { updateModifiedDate: true }));
247
250
  }
251
+ if ([...updates.keys()].some((path) => {
252
+ const lowerPath = path.toLowerCase();
253
+ return lowerPath.startsWith("word/") && (lowerPath.endsWith(".xml") || lowerPath.endsWith(".rels"));
254
+ })) {
255
+ if (detectDocxConformanceClass(await findZipEntryCaseInsensitive(zip, "word/document.xml")?.async("text") ?? null) === DOCX_CONFORMANCE_CLASSES.STRICT) return null;
256
+ }
248
257
  return await applyUpdatesToZip(zip, updates);
249
258
  } catch {
250
259
  return null;
@@ -1,4 +1,4 @@
1
- import { CharacterSpacingAttrs, CharacterStyleAttrs, CommentAttrs, EmphasisMarkAttrs, FontFamilyAttrs, FontSizeAttrs, FootnoteRefAttrs, HighlightAttrs, HyperlinkAttrs, InlineWrapperAttrs, InlineWrapperLayer, LanguageAttrs, RunFormattingOverrideAttrs, RunIdentityMarkAttrs, RunPropertyChangeMarkAttrs, RunShadingAttrs, StrikeAttrs, TextColorAttrs, TextEffectAttrs, TrackedChangeMarkAttrs, UnderlineAttrs } from "../schema/marks.js";
1
+ import { CharacterSpacingAttrs, CharacterStyleAttrs, CommentAttrs, EmphasisMarkAttrs, FontFamilyAttrs, FontSizeAttrs, FootnoteRefAttrs, HighlightAttrs, HyperlinkAttrs, InlineWrapperAttrs, InlineWrapperLayer, LanguageAttrs, RunFormattingOverrideAttrs, RunIdentityMarkAttrs, RunPropertyChangeMarkAttrs, RunShadingAttrs, StrikeAttrs, TextColorAttrs, TextEffectAttrs, TrackedChangeMarkAttrs, TrackedRevisionAncestor, UnderlineAttrs } from "../schema/marks.js";
2
2
  import { BlockSdtAttrs, FieldAttrs, HardBreakAttrs, ImageAttrs, MathAttrs, PageBreakRunAttrs, ParagraphAttrs, PreservedBlockAttrs, PreservedXmlAttrs, SdtAttrs, ShapeAttrs, SymbolAttrs, TabAttrs, TableAttrs, TableCellAttrs, TableRowAttrs, TextBoxAttrs } from "../schema/nodes.js";
3
3
  import "../schema/index.js";
4
4
  import { Mark, Node } from "prosemirror-model";
@@ -76,6 +76,7 @@ declare const readFootnoteRefMarkAttrs: (mark: Mark) => ReadProseMirrorAttrsResu
76
76
  declare const expectFootnoteRefMarkAttrs: (mark: Mark) => FootnoteRefAttrs;
77
77
  declare const readCommentMarkAttrs: (mark: Mark) => ReadProseMirrorAttrsResult<CommentAttrs>;
78
78
  declare const expectCommentMarkAttrs: (mark: Mark) => CommentAttrs;
79
+ declare const isTrackedRevisionAncestorArray: (value: unknown) => value is readonly TrackedRevisionAncestor[];
79
80
  declare const readTrackedChangeMarkAttrs: (mark: Mark) => ReadProseMirrorAttrsResult<TrackedChangeMarkAttrs>;
80
81
  declare const expectTrackedChangeMarkAttrs: (mark: Mark) => TrackedChangeMarkAttrs;
81
82
  declare const readRunPropertyChangeMarkAttrs: (mark: Mark) => ReadProseMirrorAttrsResult<RunPropertyChangeMarkAttrs>;
@@ -118,4 +119,4 @@ declare const mergeTableRowAttrs: (node: Node, patch: NodeAttrPatch<TableRowAttr
118
119
  */
119
120
  declare const mergeTableCellAttrs: (node: Node, patch: NodeAttrPatch<TableCellAttrs>) => TableCellAttrs;
120
121
  //#endregion
121
- export { ProseMirrorAttrIssue, ReadProseMirrorAttrsResult, expectBlockSdtAttrs, expectCharacterSpacingMarkAttrs, expectCharacterStyleMarkAttrs, expectCommentMarkAttrs, expectEmphasisMarkAttrs, expectFieldAttrs, expectFontFamilyMarkAttrs, expectFontSizeMarkAttrs, expectFootnoteRefMarkAttrs, expectHardBreakAttrs, expectHighlightMarkAttrs, expectHyperlinkMarkAttrs, expectImageAttrs, expectInlineWrapperMarkAttrs, expectLanguageMarkAttrs, expectMathAttrs, expectPageBreakRunAttrs, expectParagraphAttrs, expectPreservedBlockAttrs, expectPreservedXmlAttrs, expectRunFormattingOverrideMarkAttrs, expectRunIdentityMarkAttrs, expectRunPropertyChangeMarkAttrs, expectRunShadingMarkAttrs, expectSdtAttrs, expectShapeAttrs, expectStrikeMarkAttrs, expectSymbolAttrs, expectTabAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs, expectTextColorMarkAttrs, expectTextEffectMarkAttrs, expectTrackedChangeMarkAttrs, expectUnderlineMarkAttrs, mergeImageAttrs, mergeParagraphAttrs, mergeTableAttrs, mergeTableCellAttrs, mergeTableRowAttrs, readBlockSdtAttrs, readCharacterSpacingMarkAttrs, readCharacterStyleMarkAttrs, readCommentMarkAttrs, readEmphasisMarkAttrs, readFieldAttrs, readFontFamilyMarkAttrs, readFontSizeMarkAttrs, readFootnoteRefMarkAttrs, readHardBreakAttrs, readHighlightMarkAttrs, readHyperlinkMarkAttrs, readImageAttrs, readInlineWrapperMarkAttrs, readInlineWrapperStack, readLanguageMarkAttrs, readMathAttrs, readPageBreakRunAttrs, readParagraphAttrs, readPreservedBlockAttrs, readPreservedXmlAttrs, readRunFormattingOverrideMarkAttrs, readRunIdentityMarkAttrs, readRunPropertyChangeMarkAttrs, readRunShadingMarkAttrs, readSdtAttrs, readShapeAttrs, readStrikeMarkAttrs, readSymbolAttrs, readTabAttrs, readTableAttrs, readTableCellAttrs, readTableRowAttrs, readTextBoxAttrs, readTextColorMarkAttrs, readTextEffectMarkAttrs, readTrackedChangeMarkAttrs, readUnderlineMarkAttrs };
122
+ export { ProseMirrorAttrIssue, ReadProseMirrorAttrsResult, expectBlockSdtAttrs, expectCharacterSpacingMarkAttrs, expectCharacterStyleMarkAttrs, expectCommentMarkAttrs, expectEmphasisMarkAttrs, expectFieldAttrs, expectFontFamilyMarkAttrs, expectFontSizeMarkAttrs, expectFootnoteRefMarkAttrs, expectHardBreakAttrs, expectHighlightMarkAttrs, expectHyperlinkMarkAttrs, expectImageAttrs, expectInlineWrapperMarkAttrs, expectLanguageMarkAttrs, expectMathAttrs, expectPageBreakRunAttrs, expectParagraphAttrs, expectPreservedBlockAttrs, expectPreservedXmlAttrs, expectRunFormattingOverrideMarkAttrs, expectRunIdentityMarkAttrs, expectRunPropertyChangeMarkAttrs, expectRunShadingMarkAttrs, expectSdtAttrs, expectShapeAttrs, expectStrikeMarkAttrs, expectSymbolAttrs, expectTabAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs, expectTextColorMarkAttrs, expectTextEffectMarkAttrs, expectTrackedChangeMarkAttrs, expectUnderlineMarkAttrs, isTrackedRevisionAncestorArray, mergeImageAttrs, mergeParagraphAttrs, mergeTableAttrs, mergeTableCellAttrs, mergeTableRowAttrs, readBlockSdtAttrs, readCharacterSpacingMarkAttrs, readCharacterStyleMarkAttrs, readCommentMarkAttrs, readEmphasisMarkAttrs, readFieldAttrs, readFontFamilyMarkAttrs, readFontSizeMarkAttrs, readFootnoteRefMarkAttrs, readHardBreakAttrs, readHighlightMarkAttrs, readHyperlinkMarkAttrs, readImageAttrs, readInlineWrapperMarkAttrs, readInlineWrapperStack, readLanguageMarkAttrs, readMathAttrs, readPageBreakRunAttrs, readParagraphAttrs, readPreservedBlockAttrs, readPreservedXmlAttrs, readRunFormattingOverrideMarkAttrs, readRunIdentityMarkAttrs, readRunPropertyChangeMarkAttrs, readRunShadingMarkAttrs, readSdtAttrs, readShapeAttrs, readStrikeMarkAttrs, readSymbolAttrs, readTabAttrs, readTableAttrs, readTableCellAttrs, readTableRowAttrs, readTextBoxAttrs, readTextColorMarkAttrs, readTextEffectMarkAttrs, readTrackedChangeMarkAttrs, readUnderlineMarkAttrs };
@@ -82,6 +82,12 @@ const NOTE_TYPES = ["footnote", "endnote"];
82
82
  const NOTE_REF_VERT_ALIGNS = ["baseline", "superscript"];
83
83
  const TEXT_BOX_DOCX_PLACEMENTS = ["standalone", "inlineWithPrevious"];
84
84
  const TRACKED_CHANGE_MOVE_KINDS = ["moveTo", "moveFrom"];
85
+ const TRACKED_REVISION_ANCESTOR_TYPES = [
86
+ "insertion",
87
+ "deletion",
88
+ "moveFrom",
89
+ "moveTo"
90
+ ];
85
91
  const TEXT_BOX_TRACKED_CHANGE_TYPES = [
86
92
  "insertion",
87
93
  "deletion",
@@ -792,6 +798,7 @@ const readCommentMarkAttrs = (mark) => {
792
798
  return attrsResult(attrs, issues);
793
799
  };
794
800
  const expectCommentMarkAttrs = (mark) => expectCachedMarkAttrs(mark, commentAttrsCache, readCommentMarkAttrs, "comment attrs");
801
+ const isTrackedRevisionAncestorArray = (value) => Array.isArray(value) && value.length > 0 && value.every((ancestor) => isRecord(ancestor) && TRACKED_REVISION_ANCESTOR_TYPES.some((type) => type === ancestor["type"]) && typeof ancestor["revisionId"] === "number" && Number.isSafeInteger(ancestor["revisionId"]) && ancestor["revisionId"] >= 0 && typeof ancestor["author"] === "string" && (ancestor["date"] === void 0 || typeof ancestor["date"] === "string") && (ancestor["utcDate"] === void 0 || typeof ancestor["utcDate"] === "string") && (ancestor["initials"] === void 0 || typeof ancestor["initials"] === "string") && typeof ancestor["outerWrapperCount"] === "number" && Number.isSafeInteger(ancestor["outerWrapperCount"]) && ancestor["outerWrapperCount"] >= 0);
795
802
  const readTrackedChangeMarkAttrs = (mark) => {
796
803
  const attrs = attrsRecord(mark.attrs);
797
804
  const issues = [];
@@ -801,6 +808,16 @@ const readTrackedChangeMarkAttrs = (mark) => {
801
808
  optionalString(attrs, "date", `${mark.type.name}.attrs.date`, issues);
802
809
  optionalString(attrs, "utcDate", `${mark.type.name}.attrs.utcDate`, issues);
803
810
  optionalString(attrs, "initials", `${mark.type.name}.attrs.initials`, issues);
811
+ const outerWrapperCount = attrs["_docxOuterWrapperCount"];
812
+ if (outerWrapperCount !== void 0 && outerWrapperCount !== null && (typeof outerWrapperCount !== "number" || !Number.isSafeInteger(outerWrapperCount) || outerWrapperCount < 0)) issues.push({
813
+ path: `${mark.type.name}.attrs._docxOuterWrapperCount`,
814
+ message: "Expected a non-negative safe integer."
815
+ });
816
+ const ancestors = attrs["_docxRevisionAncestors"];
817
+ if (ancestors !== void 0 && ancestors !== null && !isTrackedRevisionAncestorArray(ancestors)) issues.push({
818
+ path: `${mark.type.name}.attrs._docxRevisionAncestors`,
819
+ message: "Expected a non-empty array of valid revision ancestors."
820
+ });
804
821
  optionalOneOf(attrs, "moveKind", `${mark.type.name}.attrs.moveKind`, issues, TRACKED_CHANGE_MOVE_KINDS);
805
822
  optionalOneOf(attrs, "provenance", `${mark.type.name}.attrs.provenance`, issues, TRACKED_CHANGE_PROVENANCE_VALUES);
806
823
  optionalString(attrs, "suggestionId", `${mark.type.name}.attrs.suggestionId`, issues);
@@ -2700,4 +2717,4 @@ const optionalEmptyHyperlinkArray = (value, issues) => {
2700
2717
  }
2701
2718
  };
2702
2719
  //#endregion
2703
- export { expectBlockSdtAttrs, expectCharacterSpacingMarkAttrs, expectCharacterStyleMarkAttrs, expectCommentMarkAttrs, expectEmphasisMarkAttrs, expectFieldAttrs, expectFontFamilyMarkAttrs, expectFontSizeMarkAttrs, expectFootnoteRefMarkAttrs, expectHardBreakAttrs, expectHighlightMarkAttrs, expectHyperlinkMarkAttrs, expectImageAttrs, expectInlineWrapperMarkAttrs, expectLanguageMarkAttrs, expectMathAttrs, expectPageBreakRunAttrs, expectParagraphAttrs, expectPreservedBlockAttrs, expectPreservedXmlAttrs, expectRunFormattingOverrideMarkAttrs, expectRunIdentityMarkAttrs, expectRunPropertyChangeMarkAttrs, expectRunShadingMarkAttrs, expectSdtAttrs, expectShapeAttrs, expectStrikeMarkAttrs, expectSymbolAttrs, expectTabAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs, expectTextColorMarkAttrs, expectTextEffectMarkAttrs, expectTrackedChangeMarkAttrs, expectUnderlineMarkAttrs, mergeImageAttrs, mergeParagraphAttrs, mergeTableAttrs, mergeTableCellAttrs, mergeTableRowAttrs, readBlockSdtAttrs, readCharacterSpacingMarkAttrs, readCharacterStyleMarkAttrs, readCommentMarkAttrs, readEmphasisMarkAttrs, readFieldAttrs, readFontFamilyMarkAttrs, readFontSizeMarkAttrs, readFootnoteRefMarkAttrs, readHardBreakAttrs, readHighlightMarkAttrs, readHyperlinkMarkAttrs, readImageAttrs, readInlineWrapperMarkAttrs, readInlineWrapperStack, readLanguageMarkAttrs, readMathAttrs, readPageBreakRunAttrs, readParagraphAttrs, readPreservedBlockAttrs, readPreservedXmlAttrs, readRunFormattingOverrideMarkAttrs, readRunIdentityMarkAttrs, readRunPropertyChangeMarkAttrs, readRunShadingMarkAttrs, readSdtAttrs, readShapeAttrs, readStrikeMarkAttrs, readSymbolAttrs, readTabAttrs, readTableAttrs, readTableCellAttrs, readTableRowAttrs, readTextBoxAttrs, readTextColorMarkAttrs, readTextEffectMarkAttrs, readTrackedChangeMarkAttrs, readUnderlineMarkAttrs };
2720
+ export { expectBlockSdtAttrs, expectCharacterSpacingMarkAttrs, expectCharacterStyleMarkAttrs, expectCommentMarkAttrs, expectEmphasisMarkAttrs, expectFieldAttrs, expectFontFamilyMarkAttrs, expectFontSizeMarkAttrs, expectFootnoteRefMarkAttrs, expectHardBreakAttrs, expectHighlightMarkAttrs, expectHyperlinkMarkAttrs, expectImageAttrs, expectInlineWrapperMarkAttrs, expectLanguageMarkAttrs, expectMathAttrs, expectPageBreakRunAttrs, expectParagraphAttrs, expectPreservedBlockAttrs, expectPreservedXmlAttrs, expectRunFormattingOverrideMarkAttrs, expectRunIdentityMarkAttrs, expectRunPropertyChangeMarkAttrs, expectRunShadingMarkAttrs, expectSdtAttrs, expectShapeAttrs, expectStrikeMarkAttrs, expectSymbolAttrs, expectTabAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs, expectTextColorMarkAttrs, expectTextEffectMarkAttrs, expectTrackedChangeMarkAttrs, expectUnderlineMarkAttrs, isTrackedRevisionAncestorArray, mergeImageAttrs, mergeParagraphAttrs, mergeTableAttrs, mergeTableCellAttrs, mergeTableRowAttrs, readBlockSdtAttrs, readCharacterSpacingMarkAttrs, readCharacterStyleMarkAttrs, readCommentMarkAttrs, readEmphasisMarkAttrs, readFieldAttrs, readFontFamilyMarkAttrs, readFontSizeMarkAttrs, readFootnoteRefMarkAttrs, readHardBreakAttrs, readHighlightMarkAttrs, readHyperlinkMarkAttrs, readImageAttrs, readInlineWrapperMarkAttrs, readInlineWrapperStack, readLanguageMarkAttrs, readMathAttrs, readPageBreakRunAttrs, readParagraphAttrs, readPreservedBlockAttrs, readPreservedXmlAttrs, readRunFormattingOverrideMarkAttrs, readRunIdentityMarkAttrs, readRunPropertyChangeMarkAttrs, readRunShadingMarkAttrs, readSdtAttrs, readShapeAttrs, readStrikeMarkAttrs, readSymbolAttrs, readTabAttrs, readTableAttrs, readTableCellAttrs, readTableRowAttrs, readTextBoxAttrs, readTextColorMarkAttrs, readTextEffectMarkAttrs, readTrackedChangeMarkAttrs, readUnderlineMarkAttrs };
@@ -2,7 +2,7 @@ import { sameStatedParagraphNumbering as sameStatedParagraphNumbering$1 } from "
2
2
  import { joinProseParagraphsWithRightPropertySource } from "../../docx/paragraphPropertySource.js";
3
3
  import { appendHeadlessInlineResolution } from "../../internal/headlessRevisionResolution.js";
4
4
  import { stateAllowsHeadlessRevisionResolution } from "../../internal/headlessRevisionResolutionGuard.js";
5
- import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs } from "../attrs/index.js";
5
+ import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs, expectTrackedChangeMarkAttrs } from "../attrs/index.js";
6
6
  import { addedBreakCarrierBefore, finalParagraphsOf, paragraphEndsItsContainer } from "../containerFinalParagraph.js";
7
7
  import { resolveParagraphDefaultTextFormatting } from "../conversion/toProseDoc.js";
8
8
  import { markChangedParagraphRanges, markStructuralChange, markTrackedSectionEndpointRemoval } from "../extensions/features/ParagraphChangeTrackerExtension.js";
@@ -19,6 +19,7 @@ import { reconcileTableGridAfterColumnRemoval } from "../tableGridMutation.js";
19
19
  import { holdsNoContent } from "../zeroWidthAnchors.js";
20
20
  import { hasSerializableParagraphPropertyChange, paragraphPropertiesSnapshot, paragraphRejectAttrPatch, paragraphRejectOriginalFormatting, removeParagraphPropertyChanges, sectionRejectProperties, tableCellRejectAttrPatch, tablePropertyExceptionsRejectAttrPatch, tableRejectAttrPatch, tableRowRejectAttrPatch } from "./propertyChangeScope.js";
21
21
  import { hasMatchingCollapsedTableCellMerge, resolveCollapsedTableCellMerge, resolveVisibleTableCellMerge } from "./tableCellMergeResolution.js";
22
+ import { panic } from "better-result";
22
23
  import { PARAGRAPH_MARK_CHANGE_KINDS, REVIEW_CARRIERS } from "@stll/docx-core/model";
23
24
  import { TableMap, removeRow } from "prosemirror-tables";
24
25
  import { Mapping } from "prosemirror-transform";
@@ -55,6 +56,33 @@ function removeCommentMark(commentId) {
55
56
  return true;
56
57
  };
57
58
  }
59
+ const revisionLayerOf = (mark) => {
60
+ const attrs = expectTrackedChangeMarkAttrs(mark);
61
+ let type;
62
+ if (mark.type.name === "insertion") type = attrs.moveKind === "moveTo" ? "moveTo" : "insertion";
63
+ else type = attrs.moveKind === "moveFrom" ? "moveFrom" : "deletion";
64
+ return {
65
+ type,
66
+ revisionId: attrs.revisionId,
67
+ author: attrs.author,
68
+ ...attrs.date ? { date: attrs.date } : {},
69
+ ...attrs.utcDate ? { utcDate: attrs.utcDate } : {},
70
+ ...attrs.initials ? { initials: attrs.initials } : {},
71
+ outerWrapperCount: attrs._docxOuterWrapperCount ?? 0
72
+ };
73
+ };
74
+ const revisionLayerRemovesContent = (layer, mode) => mode === "accept" ? layer.type === "deletion" || layer.type === "moveFrom" : layer.type === "insertion" || layer.type === "moveTo";
75
+ const containsNestedRevision = (doc) => {
76
+ let found = false;
77
+ doc.descendants((node) => {
78
+ if (found) return false;
79
+ for (const mark of node.marks) if ((mark.type.name === "insertion" || mark.type.name === "deletion") && (expectTrackedChangeMarkAttrs(mark)._docxRevisionAncestors?.length ?? 0) > 0) {
80
+ found = true;
81
+ return false;
82
+ }
83
+ });
84
+ return found;
85
+ };
58
86
  /**
59
87
  * Resolve a tracked change: accept or reject.
60
88
  * - Accept: keep insertions (remove mark), delete deletions (remove text)
@@ -76,7 +104,7 @@ function resolveChange(from, to, mode, revisionIds, execution = "legacy") {
76
104
  const removeType = mode === "accept" ? deletionType : insertionType;
77
105
  const revisionSet = revisionIds === void 0 ? null : new Set(revisionIds);
78
106
  const removeKeptMarksInBulk = revisionSet === null && keepType !== void 0;
79
- const canBulkInlineResolution = execution === "headless-bulk-inline" && revisionSet === null && from === 0 && to === state.doc.content.size && stateAllowsHeadlessRevisionResolution(state);
107
+ const canBulkInlineResolution = execution === "headless-bulk-inline" && revisionSet === null && from === 0 && to === state.doc.content.size && stateAllowsHeadlessRevisionResolution(state) && !containsNestedRevision(state.doc);
80
108
  const matchesRevision = (mark) => revisionSet === null || typeof mark.attrs["revisionId"] === "number" && revisionSet.has(mark.attrs["revisionId"]);
81
109
  if (dispatch) {
82
110
  const tr = state.tr;
@@ -189,6 +217,36 @@ function resolveChange(from, to, mode, revisionIds, execution = "legacy") {
189
217
  revisionSet,
190
218
  styleResolver
191
219
  });
220
+ const nestedMark = node.marks.find((mark) => (mark.type === insertionType || mark.type === deletionType) && (expectTrackedChangeMarkAttrs(mark)._docxRevisionAncestors?.length ?? 0) > 0);
221
+ if (nestedMark) {
222
+ const path = [...expectTrackedChangeMarkAttrs(nestedMark)._docxRevisionAncestors ?? [], revisionLayerOf(nestedMark)];
223
+ const selected = (layer) => revisionSet === null || revisionSet.has(layer.revisionId);
224
+ if (path.some(selected)) if (path.some((layer) => selected(layer) && revisionLayerRemovesContent(layer, mode))) deleteRanges.push({
225
+ from: rangeFrom,
226
+ to: rangeTo
227
+ });
228
+ else {
229
+ tr.removeMark(rangeFrom, rangeTo, nestedMark);
230
+ const remaining = path.filter((layer) => !selected(layer));
231
+ const active = remaining.at(-1);
232
+ if (active) {
233
+ const type = active.type === "insertion" || active.type === "moveTo" ? insertionType : deletionType;
234
+ if (!type) panic("A nested tracked revision has no editor mark type");
235
+ tr.addMark(rangeFrom, rangeTo, type.create({
236
+ revisionId: active.revisionId,
237
+ author: active.author,
238
+ date: active.date ?? null,
239
+ utcDate: active.utcDate ?? null,
240
+ initials: active.initials ?? null,
241
+ moveKind: active.type === "moveTo" || active.type === "moveFrom" ? active.type : null,
242
+ _historicalFormatting: active.type === "deletion" || active.type === "moveFrom" ? true : null,
243
+ _docxOuterWrapperCount: active.outerWrapperCount,
244
+ _docxRevisionAncestors: remaining.length > 1 ? remaining.slice(0, -1) : null
245
+ }));
246
+ }
247
+ }
248
+ return true;
249
+ }
192
250
  if (removesNode) deleteRanges.push({
193
251
  from: rangeFrom,
194
252
  to: rangeTo
@@ -794,7 +852,7 @@ function findAIEditRevisionRange(state, revisionIds) {
794
852
  includeRange(pos, pos + node.nodeSize);
795
853
  break;
796
854
  }
797
- if ((mark.type === insertionType || mark.type === deletionType) && typeof mark.attrs["revisionId"] === "number" && idSet.has(mark.attrs["revisionId"])) {
855
+ if ((mark.type === insertionType || mark.type === deletionType) && (idSet.has(expectTrackedChangeMarkAttrs(mark).revisionId) || (expectTrackedChangeMarkAttrs(mark)._docxRevisionAncestors ?? []).some((ancestor) => idSet.has(ancestor.revisionId)))) {
798
856
  includeRange(pos, pos + node.nodeSize);
799
857
  break;
800
858
  }
@@ -1162,12 +1162,10 @@ const nestInlineWrappers = (stack, content) => {
1162
1162
  * The emitted sequence with each wrapper group nested back into the wrappers
1163
1163
  * `toProseDoc` lifted off it.
1164
1164
  *
1165
- * The revision stays outermost. folio already writes a revision outside the
1166
- * hyperlink it spans, the parse leg is revision-owned, and accepting or
1167
- * rejecting one is a range operation over the revision's own content; a
1168
- * wrapper placed outside two revisions would also have to mint one revision id
1169
- * per wrapper. So a revision in the group keeps its place and takes the nest
1170
- * inside it, and the items around it share a nest of their own.
1165
+ * A revision may be inside some wrapper layers and outside others. Its mark
1166
+ * records how many layers preceded it in the source, since a ProseMirror mark
1167
+ * set cannot express that order. Adjacent items with the same outer layers
1168
+ * share one wrapper rather than splitting it around each revision.
1171
1169
  *
1172
1170
  * A group with nothing in it writes no wrapper: an authored wrapper whose
1173
1171
  * content was deleted or rejected is gone, and an empty one says nothing.
@@ -1185,10 +1183,83 @@ const nestInsideHyperlink = (item, stack, hyperlinkIndex, indices) => {
1185
1183
  }
1186
1184
  return false;
1187
1185
  };
1188
- const nestInlineWrapperGroups = (items, groups, hyperlinkIndices) => {
1186
+ const nestRevisionAwareInlineWrappers = (items, stack, revisionOuterWrapperCounts, layerIndex = 0) => {
1187
+ if (layerIndex === stack.length) return [...items];
1188
+ const nested = [];
1189
+ let pending = [];
1190
+ const closeLayer = () => {
1191
+ if (pending.length === 0) return;
1192
+ const layer = stack[layerIndex];
1193
+ if (layer === void 0) panic("An inline wrapper layer disappeared during reconstruction");
1194
+ nested.push(inlineWrapperMember(layer, nestRevisionAwareInlineWrappers(pending, stack, revisionOuterWrapperCounts, layerIndex + 1)));
1195
+ pending = [];
1196
+ };
1197
+ for (const item of items) {
1198
+ if (!isRevisionWrapper(item) || (revisionOuterWrapperCounts.get(item) ?? 0) > layerIndex) {
1199
+ pending.push(item);
1200
+ continue;
1201
+ }
1202
+ closeLayer();
1203
+ item.content = [nestInlineWrappers(stack.slice(layerIndex), item.content)];
1204
+ nested.push(item);
1205
+ }
1206
+ closeLayer();
1207
+ return nested;
1208
+ };
1209
+ const sameRevisionLayer = (left, right) => left.type === right.type && left.revisionId === right.revisionId && left.author === right.author && left.date === right.date && left.utcDate === right.utcDate && left.initials === right.initials && left.outerWrapperCount === right.outerWrapperCount;
1210
+ const revisionInfoFromLayer = (layer) => ({
1211
+ id: layer.revisionId,
1212
+ author: layer.author,
1213
+ ...layer.date ? { date: layer.date } : {},
1214
+ ...layer.utcDate ? { utcDate: {
1215
+ attribute: DATE_UTC_ATTRIBUTE,
1216
+ value: layer.utcDate
1217
+ } } : {},
1218
+ ...layer.initials ? { initials: layer.initials } : {}
1219
+ });
1220
+ const nestRevisionAncestors = (items, ancestorsByWrapper, revisionOuterWrapperCounts) => {
1221
+ const nested = [];
1222
+ const open = [];
1223
+ for (const item of items) {
1224
+ if (!isRevisionWrapper(item)) {
1225
+ open.length = 0;
1226
+ nested.push(item);
1227
+ continue;
1228
+ }
1229
+ const ownLayer = {
1230
+ type: item.type,
1231
+ revisionId: item.info.id,
1232
+ author: item.info.author,
1233
+ ...item.info.date ? { date: item.info.date } : {},
1234
+ ...item.info.utcDate ? { utcDate: item.info.utcDate.value } : {},
1235
+ ...item.info.initials ? { initials: item.info.initials } : {},
1236
+ outerWrapperCount: revisionOuterWrapperCounts.get(item) ?? 0
1237
+ };
1238
+ const path = [...ancestorsByWrapper.get(item) ?? [], ownLayer];
1239
+ let shared = 0;
1240
+ while (shared < open.length && path[shared] && sameRevisionLayer(open[shared].layer, path[shared])) shared += 1;
1241
+ open.length = shared;
1242
+ for (const layer of path.slice(shared)) {
1243
+ const wrapper = createTrackedRunWrapper(layer.type, revisionInfoFromLayer(layer));
1244
+ revisionOuterWrapperCounts.set(wrapper, layer.outerWrapperCount);
1245
+ const parent = open.at(-1)?.wrapper;
1246
+ if (parent) parent.content.push(wrapper);
1247
+ else nested.push(wrapper);
1248
+ open.push({
1249
+ layer,
1250
+ wrapper
1251
+ });
1252
+ }
1253
+ const owner = open.at(-1)?.wrapper;
1254
+ if (!owner) panic("A tracked revision path has no owner");
1255
+ owner.content.push(...item.content);
1256
+ }
1257
+ return nested;
1258
+ };
1259
+ const nestInlineWrapperGroups = (items, groups, hyperlinkIndices, revisionOuterWrapperCounts, ancestorsByWrapper) => {
1189
1260
  const nested = [];
1190
1261
  for (const [index, group] of groups.entries()) {
1191
- const slice = items.slice(group.start, groups[index + 1]?.start ?? items.length);
1262
+ const slice = nestRevisionAncestors(items.slice(group.start, groups[index + 1]?.start ?? items.length), ancestorsByWrapper, revisionOuterWrapperCounts);
1192
1263
  if (group.stack.length === 0) {
1193
1264
  nested.push(...slice);
1194
1265
  continue;
@@ -1205,24 +1276,18 @@ const nestInlineWrapperGroups = (items, groups, hyperlinkIndices) => {
1205
1276
  nested.push(...slice);
1206
1277
  continue;
1207
1278
  }
1208
- let pending = [];
1209
- const closeNest = () => {
1210
- if (pending.length === 0) return;
1211
- nested.push(nestInlineWrappers(outerStack, pending));
1212
- pending = [];
1213
- };
1214
- for (const item of slice) {
1215
- if (!isRevisionWrapper(item)) {
1216
- pending.push(item);
1217
- continue;
1218
- }
1219
- closeNest();
1220
- if (item.content.length > 0) item.content = [nestInlineWrappers(outerStack, item.content)];
1221
- nested.push(item);
1279
+ nested.push(...nestRevisionAwareInlineWrappers(slice, outerStack, revisionOuterWrapperCounts));
1280
+ }
1281
+ const coalesced = [];
1282
+ for (const item of nested) {
1283
+ const previous = coalesced.at(-1);
1284
+ if (previous && isRevisionWrapper(previous) && isRevisionWrapper(item) && previous.type === item.type && JSON.stringify(previous.info) === JSON.stringify(item.info) && revisionOuterWrapperCounts.get(previous) === revisionOuterWrapperCounts.get(item)) {
1285
+ previous.content.push(...item.content);
1286
+ continue;
1222
1287
  }
1223
- closeNest();
1288
+ coalesced.push(item);
1224
1289
  }
1225
- return nested;
1290
+ return coalesced;
1226
1291
  };
1227
1292
  function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, textBoxAnchorMarkers, skipLeadingRenderedPageBreak = false, inheritedFormattingOverride) {
1228
1293
  const content = [];
@@ -1252,6 +1317,8 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1252
1317
  let currentTrackedChange;
1253
1318
  const sourceRunOwners = /* @__PURE__ */ new WeakMap();
1254
1319
  const sourceHyperlinkIndices = /* @__PURE__ */ new WeakMap();
1320
+ const revisionOuterWrapperCounts = /* @__PURE__ */ new WeakMap();
1321
+ const revisionAncestorsByWrapper = /* @__PURE__ */ new WeakMap();
1255
1322
  const openedComments = /* @__PURE__ */ new Set();
1256
1323
  const createIndexedHyperlink = (mark) => {
1257
1324
  const hyperlink = createHyperlink(mark);
@@ -1416,16 +1483,13 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1416
1483
  } } : {},
1417
1484
  ...changeAttrs.initials ? { initials: changeAttrs.initials } : {}
1418
1485
  };
1419
- if (insertionMark) content.push({
1420
- type: changeAttrs.moveKind === "moveTo" ? "moveTo" : "insertion",
1421
- info,
1422
- content: [anchoredContent]
1423
- });
1424
- else content.push({
1425
- type: changeAttrs.moveKind === "moveFrom" ? "moveFrom" : "deletion",
1426
- info,
1427
- content: [anchoredContent]
1428
- });
1486
+ let type;
1487
+ if (insertionMark) type = changeAttrs.moveKind === "moveTo" ? "moveTo" : "insertion";
1488
+ else type = changeAttrs.moveKind === "moveFrom" ? "moveFrom" : "deletion";
1489
+ const wrapper = createTrackedRunWrapper(type, info, anchoredContent);
1490
+ revisionOuterWrapperCounts.set(wrapper, changeAttrs._docxOuterWrapperCount ?? inlineWrapperStackOf(node).length);
1491
+ revisionAncestorsByWrapper.set(wrapper, changeAttrs._docxRevisionAncestors ?? []);
1492
+ content.push(wrapper);
1429
1493
  return;
1430
1494
  }
1431
1495
  if (insertionMark || deletionMark) {
@@ -1447,12 +1511,15 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1447
1511
  let type;
1448
1512
  if (insertionMark) type = changeAttrs.moveKind === "moveTo" ? "moveTo" : "insertion";
1449
1513
  else type = changeAttrs.moveKind === "moveFrom" ? "moveFrom" : "deletion";
1450
- const trackedChangeKey = `${type}:${JSON.stringify(info)}`;
1514
+ const outerWrapperCount = changeAttrs._docxOuterWrapperCount ?? inlineWrapperStackOf(node).length;
1515
+ const trackedChangeKey = `${type}:${JSON.stringify(info)}:${outerWrapperCount}:${JSON.stringify(changeAttrs._docxRevisionAncestors ?? [])}`;
1451
1516
  if (linkMark) {
1452
1517
  const linkKey = getLinkKey(linkMark);
1453
1518
  if (!currentTrackedChange || currentTrackedChange.type !== "hyperlink" || currentTrackedChange.key !== trackedChangeKey || currentTrackedChange.hyperlinkKey !== linkKey) {
1454
1519
  const hyperlink = createIndexedHyperlink(linkMark);
1455
1520
  const wrapper = createTrackedRunWrapper(type, info, hyperlink);
1521
+ revisionOuterWrapperCounts.set(wrapper, outerWrapperCount);
1522
+ revisionAncestorsByWrapper.set(wrapper, changeAttrs._docxRevisionAncestors ?? []);
1456
1523
  content.push(wrapper);
1457
1524
  currentTrackedChange = {
1458
1525
  type: "hyperlink",
@@ -1486,6 +1553,8 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1486
1553
  }
1487
1554
  if (!currentTrackedChange || currentTrackedChange.type !== "direct" || currentTrackedChange.key !== trackedChangeKey) {
1488
1555
  const wrapper = createTrackedRunWrapper(type, info);
1556
+ revisionOuterWrapperCounts.set(wrapper, outerWrapperCount);
1557
+ revisionAncestorsByWrapper.set(wrapper, changeAttrs._docxRevisionAncestors ?? []);
1489
1558
  content.push(wrapper);
1490
1559
  currentTrackedChange = {
1491
1560
  type: "direct",
@@ -1640,7 +1709,7 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1640
1709
  type: "commentRangeEnd",
1641
1710
  id: commentId
1642
1711
  });
1643
- return nestInlineWrapperGroups(content, wrapperGroups, sourceHyperlinkIndices);
1712
+ return nestInlineWrapperGroups(content, wrapperGroups, sourceHyperlinkIndices, revisionOuterWrapperCounts, revisionAncestorsByWrapper);
1644
1713
  }
1645
1714
  function createTrackedChangeRun({ baseParagraphFormatting, inheritedFormatting, marks, node, paragraphMarkFormatting, paragraphMarkPrecedesStyle, styleResolver }) {
1646
1715
  const formattingContext = {
@@ -2001,7 +2070,7 @@ function restoreRunRecord(run, marks) {
2001
2070
  }
2002
2071
  const identityMark = marks.find((mark) => mark.type.name === RUN_IDENTITY_MARK_NAME);
2003
2072
  if (!identityMark) return;
2004
- const { preservedAttributes, preserved } = expectRunIdentityMarkAttrs(identityMark);
2073
+ const { preservedAttributes, preserved, emptyFormatting } = expectRunIdentityMarkAttrs(identityMark);
2005
2074
  if (preservedAttributes && preservedAttributes.length > 0) run.preservedAttributes = preservedAttributes.map(({ namespace, name, value }) => namespace === void 0 ? {
2006
2075
  name,
2007
2076
  value
@@ -2014,6 +2083,7 @@ function restoreRunRecord(run, marks) {
2014
2083
  ...run.formatting,
2015
2084
  preserved
2016
2085
  };
2086
+ if (emptyFormatting && run.formatting === void 0) run.formatting = {};
2017
2087
  }
2018
2088
  function getRunFormattingFromMarks(marks, options) {
2019
2089
  if (!marks || marks.length === 0 && !options) return;
@@ -16,7 +16,7 @@ import { mergeParagraphFormatting, mergeParagraphTabStops } from "../../utils/pa
16
16
  import { tableOfContentsStyleLevel } from "../../utils/tableOfContentsStyle.js";
17
17
  import { mergeTextFormatting } from "../../utils/textFormattingMerge.js";
18
18
  import { emuToPixels, emuToStrokePixels } from "../../utils/units.js";
19
- import { expectInlineWrapperMarkAttrs } from "../attrs/index.js";
19
+ import { expectInlineWrapperMarkAttrs, expectTrackedChangeMarkAttrs } from "../attrs/index.js";
20
20
  import { authoredTransformAttrs } from "../authoredTransformAttrs.js";
21
21
  import { setAutospacingBaseValue } from "../autospacingBase.js";
22
22
  import { planEmptyRanges } from "../emptyRangeAnchor.js";
@@ -77,16 +77,17 @@ const createTextBoxGroupIdFactory = () => {
77
77
  * paragraph's and the hyperlink's — so the next payload field is added once
78
78
  * rather than to whichever site the author happened to open.
79
79
  *
80
- * Minted when the run holds a page break (the leaves it is cut into have to be
81
- * rejoined into one `w:r` on save), an attribute remainder, or a `w:rPr` sink.
82
- * 57.5% of corpus files hold none of the three and mint nothing.
80
+ * Minted when the run holds a page break or multiple content items including
81
+ * an inline atom (the leaves have to be rejoined into one `w:r` on save), an
82
+ * attribute remainder, a `w:rPr` sink, or an authored empty `w:rPr`.
83
83
  */
84
84
  const runIdentityMark = (run, nextRunIdentityId) => {
85
85
  const payload = {
86
86
  preservedAttributes: run.preservedAttributes,
87
- preserved: run.formatting?.preserved
87
+ preserved: run.formatting?.preserved,
88
+ emptyFormatting: run.formatting !== void 0 && Object.keys(run.formatting).length === 0 ? true : void 0
88
89
  };
89
- if (!runHasPageBreakContent(run) && !hasRunIdentityPayload(payload)) return null;
90
+ if (!runHasPageBreakContent(run) && !runHasMixedContent(run) && !hasRunIdentityPayload(payload)) return null;
90
91
  return schema.mark(RUN_IDENTITY_MARK_NAME, runIdentityAttrs(nextRunIdentityId(), payload));
91
92
  };
92
93
  /** Keep imported hyperlink identity unique across every nested conversion scope. */
@@ -396,8 +397,8 @@ function convertParagraph(paragraph, styleResolver, context, extraRunFormatting,
396
397
  type: "direct"
397
398
  }], { ordinaryFormatting: ordinaryDefaultRunFormatting });
398
399
  const defaultRunFormatting = defaultToggleCascade.formatting;
399
- if (extraRunFormatting !== void 0) {
400
- attrs._tableRunFormatting = extraRunFormatting;
400
+ if (extraRunFormatting !== void 0 || paragraph.content.length > 0) {
401
+ if (extraRunFormatting !== void 0) attrs._tableRunFormatting = extraRunFormatting;
401
402
  if (defaultRunFormatting) attrs.defaultTextFormatting = defaultRunFormatting;
402
403
  else delete attrs.defaultTextFormatting;
403
404
  }
@@ -606,13 +607,21 @@ function anchorPointComment(nodes, commentId) {
606
607
  * and it is what lets the save leg hoist a revision covering all of them back
607
608
  * around the control instead of losing it.
608
609
  */
609
- const withTrackedRunMark = (node, mark) => {
610
- if (node.marks.some(({ type }) => type.name === "insertion" || type.name === "deletion")) return node;
610
+ const withTrackedRunMark = (node, mark, ancestor) => {
611
611
  if (node.type.name === "sdt") {
612
612
  const marked = [];
613
- for (let index = 0; index < node.childCount; index += 1) marked.push(withTrackedRunMark(node.child(index), mark));
613
+ for (let index = 0; index < node.childCount; index += 1) marked.push(withTrackedRunMark(node.child(index), mark, ancestor));
614
614
  return recreateProseNodeWithParagraphPropertySource(node, { content: marked });
615
615
  }
616
+ const nestedMark = node.marks.find(({ type }) => type.name === "insertion" || type.name === "deletion");
617
+ if (nestedMark) {
618
+ const nestedAttrs = expectTrackedChangeMarkAttrs(nestedMark);
619
+ const updatedMark = schema.mark(nestedMark.type.name, {
620
+ ...nestedMark.attrs,
621
+ _docxRevisionAncestors: [ancestor, ...nestedAttrs._docxRevisionAncestors ?? []]
622
+ });
623
+ return node.mark(updatedMark.addToSet(node.marks));
624
+ }
616
625
  if (trackedRunInlineAtomDisposition(node) === "outside-wrapper") panic(`Inline atom ${JSON.stringify(node.type.name)} cannot occur in a tracked-run wrapper`);
617
626
  if (canCarryTrackedRunMark(node)) return node.mark(mark.addToSet(node.marks));
618
627
  return node;
@@ -652,7 +661,7 @@ function convertTrackedChange(change, markType, nextHyperlinkInstanceIndex, next
652
661
  } else if (item.type === "insertion" || item.type === "deletion" || item.type === "moveFrom" || item.type === "moveTo") {
653
662
  const nestedMarkType = item.type === "insertion" || item.type === "moveTo" ? "insertion" : "deletion";
654
663
  const nestedMoveKind = item.type === "moveFrom" || item.type === "moveTo" ? item.type : null;
655
- itemNodes.push(...convertTrackedChange(item, nestedMarkType, nextHyperlinkInstanceIndex, nextRunIdentityId, runFormattingResolvers, styleResolver, nestedMoveKind, textBoxAnchors));
664
+ itemNodes.push(...convertTrackedChange(item, nestedMarkType, nextHyperlinkInstanceIndex, nextRunIdentityId, runFormattingResolvers, styleResolver, nestedMoveKind, textBoxAnchors, [...wrappedBy, ...stack]));
656
665
  } else if (item.type === "inlineSdt") {
657
666
  const sdtNode = convertInlineSdt(item, nextHyperlinkInstanceIndex, nextRunIdentityId, getTrackedRunFormatting, runFormattingResolvers, styleResolver, textBoxAnchors, [...wrappedBy, ...stack]);
658
667
  if (sdtNode) itemNodes.push(sdtNode);
@@ -681,9 +690,19 @@ function convertTrackedChange(change, markType, nextHyperlinkInstanceIndex, next
681
690
  utcDate: change.info.utcDate?.value ?? null,
682
691
  initials: change.info.initials ?? null,
683
692
  moveKind,
693
+ _docxOuterWrapperCount: wrappedBy.length,
684
694
  ...markType === "deletion" ? { _historicalFormatting: true } : {}
685
695
  });
686
- return nodes.map((node) => withTrackedRunMark(node, mark));
696
+ const ancestor = {
697
+ type: change.type,
698
+ revisionId: change.info.id,
699
+ author: change.info.author,
700
+ ...change.info.date ? { date: change.info.date } : {},
701
+ ...change.info.utcDate ? { utcDate: change.info.utcDate.value } : {},
702
+ ...change.info.initials ? { initials: change.info.initials } : {},
703
+ outerWrapperCount: wrappedBy.length
704
+ };
705
+ return nodes.map((node) => withTrackedRunMark(node, mark, ancestor));
687
706
  }
688
707
  function paragraphFormattingToAttrs(paragraph, styleResolver, tableParagraphOverlay) {
689
708
  const formatting = paragraph.formatting;
@@ -1691,6 +1710,7 @@ function convertRun(run, resolvedStyleFormatting, nextRunIdentityId, styleResolv
1691
1710
  return nodes;
1692
1711
  }
1693
1712
  const runHasPageBreakContent = (run) => run.content.some((content) => content.type === "break" && content.breakType === "page");
1713
+ const runHasMixedContent = (run) => run.content.length > 1 && run.content.some((content) => content.type !== "text");
1694
1714
  /**
1695
1715
  * Whether a field's result holds an explicit page break.
1696
1716
  *
@@ -44,13 +44,10 @@ type SchemaMarkName = keyof typeof MARK_EXTENSIONS;
44
44
  * Five of these marks are containers on the save leg as well, and their
45
45
  * relative order is that leg's rather than a choice made here.
46
46
  * `extractParagraphContent` opens a comment range around everything a leaf
47
- * produces (it flushes the open run and hyperlink and drops the tracked wrapper
48
- * before pushing the marker: `fromProseDoc.ts:2359-2369`), then the revision
49
- * wrapper (`fromProseDoc.ts:2530`), then the wrapper stack inside that revision
50
- * (`nestInlineWrapperGroups`, `fromProseDoc.ts:2200`), then the hyperlink and
51
- * the runs it holds (`fromProseDoc.ts:2567`). So the save writes
52
- * `w:commentRangeStart … w:ins > w:bdo > w:hyperlink > w:r`, and the editor DOM
53
- * nests `span.docx-comment > span.docx-insertion > bdo > a`.
47
+ * produces. For an editor-created revision, the existing wrapper stack owns
48
+ * the change, followed by the hyperlink and the runs it holds. So the save
49
+ * writes `w:commentRangeStart … w:bdo > w:ins > w:hyperlink > w:r`, and the
50
+ * editor DOM nests `span.docx-comment > bdo > span.docx-insertion > a`.
54
51
  * `markNestingOrder.test.ts` holds the two legs to each other.
55
52
  *
56
53
  * `runIdentity` is the fifth: `w:r` is the element it names, and it is the
@@ -60,6 +57,6 @@ type SchemaMarkName = keyof typeof MARK_EXTENSIONS;
60
57
  * own in OOXML, so the save leg ranks it against nothing and its place here is
61
58
  * a presentation decision.
62
59
  */
63
- declare const MARK_NESTING_ORDER: readonly ["bold", "italic", "underline", "strike", "textColor", "runShading", "highlight", "fontSize", "fontFamily", "language", "superscript", "subscript", "allCaps", "smallCaps", "footnoteRef", "characterSpacing", "emboss", "imprint", "hidden", "textShadow", "emphasisMark", "textOutline", "rtl", "textEffect", "runFormattingOverride", "characterStyle", "comment", "insertion", "deletion", "inlineWrapper", "hyperlink", "runPropertyChange", "runIdentity"];
60
+ declare const MARK_NESTING_ORDER: readonly ["bold", "italic", "underline", "strike", "textColor", "runShading", "highlight", "fontSize", "fontFamily", "language", "superscript", "subscript", "allCaps", "smallCaps", "footnoteRef", "characterSpacing", "emboss", "imprint", "hidden", "textShadow", "emphasisMark", "textOutline", "rtl", "textEffect", "runFormattingOverride", "characterStyle", "comment", "inlineWrapper", "insertion", "deletion", "hyperlink", "runPropertyChange", "runIdentity"];
64
61
  //#endregion
65
62
  export { MARK_EXTENSIONS, MARK_NESTING_ORDER, SchemaMarkName };
@@ -83,13 +83,10 @@ const markNestingOrder = (names) => names;
83
83
  * Five of these marks are containers on the save leg as well, and their
84
84
  * relative order is that leg's rather than a choice made here.
85
85
  * `extractParagraphContent` opens a comment range around everything a leaf
86
- * produces (it flushes the open run and hyperlink and drops the tracked wrapper
87
- * before pushing the marker: `fromProseDoc.ts:2359-2369`), then the revision
88
- * wrapper (`fromProseDoc.ts:2530`), then the wrapper stack inside that revision
89
- * (`nestInlineWrapperGroups`, `fromProseDoc.ts:2200`), then the hyperlink and
90
- * the runs it holds (`fromProseDoc.ts:2567`). So the save writes
91
- * `w:commentRangeStart … w:ins > w:bdo > w:hyperlink > w:r`, and the editor DOM
92
- * nests `span.docx-comment > span.docx-insertion > bdo > a`.
86
+ * produces. For an editor-created revision, the existing wrapper stack owns
87
+ * the change, followed by the hyperlink and the runs it holds. So the save
88
+ * writes `w:commentRangeStart … w:bdo > w:ins > w:hyperlink > w:r`, and the
89
+ * editor DOM nests `span.docx-comment > bdo > span.docx-insertion > a`.
93
90
  * `markNestingOrder.test.ts` holds the two legs to each other.
94
91
  *
95
92
  * `runIdentity` is the fifth: `w:r` is the element it names, and it is the
@@ -127,9 +124,9 @@ const MARK_NESTING_ORDER = markNestingOrder([
127
124
  "runFormattingOverride",
128
125
  "characterStyle",
129
126
  "comment",
127
+ "inlineWrapper",
130
128
  "insertion",
131
129
  "deletion",
132
- "inlineWrapper",
133
130
  "hyperlink",
134
131
  "runPropertyChange",
135
132
  "runIdentity"
@@ -105,7 +105,8 @@ const RunIdentityExtension = createMarkExtension({
105
105
  attrs: {
106
106
  id: {},
107
107
  preservedAttributes: { default: null },
108
- preserved: { default: null }
108
+ preserved: { default: null },
109
+ emptyFormatting: { default: null }
109
110
  },
110
111
  inclusive: false,
111
112
  parseDOM: [{
@@ -1,5 +1,5 @@
1
1
  import { AUTHOR_COLORS, getAuthorColorIdx } from "../../../utils/authorColors.js";
2
- import { expectTrackedChangeMarkAttrs } from "../../attrs/index.js";
2
+ import { expectTrackedChangeMarkAttrs, isTrackedRevisionAncestorArray } from "../../attrs/index.js";
3
3
  import { createMarkExtension } from "../create.js";
4
4
  //#region src/prosemirror/extensions/marks/TrackedChangeExtensions.ts
5
5
  /**
@@ -31,6 +31,15 @@ const SUGGESTION_TINT = "var(--suggestion-bg, color-mix(in oklch, #6d3bd6 12%, t
31
31
  const SUGGESTION_TINT_LAYER = `linear-gradient(${SUGGESTION_TINT}, ${SUGGESTION_TINT})`;
32
32
  const suggestedInsertionStyle = () => `color: ${SUGGESTION_COLOR}; text-decoration: underline; text-decoration-style: dotted; text-decoration-color: ${SUGGESTION_COLOR}; background-image: ${SUGGESTION_TINT_LAYER};`;
33
33
  const suggestedDeletionStyle = () => `color: ${SUGGESTION_COLOR}; text-decoration: line-through; text-decoration-style: dotted; text-decoration-color: ${SUGGESTION_COLOR}; background-image: ${SUGGESTION_TINT_LAYER};`;
34
+ const revisionAncestorsFromDom = (serialized) => {
35
+ if (serialized === void 0) return null;
36
+ try {
37
+ const value = JSON.parse(serialized);
38
+ return isTrackedRevisionAncestorArray(value) ? value : false;
39
+ } catch {
40
+ return false;
41
+ }
42
+ };
34
43
  /**
35
44
  * Insertion mark — text added in tracked changes
36
45
  * Renders with per-author colored underline.
@@ -47,21 +56,27 @@ const InsertionExtension = createMarkExtension({
47
56
  moveKind: { default: null },
48
57
  initials: { default: null },
49
58
  provenance: { default: "user" },
50
- suggestionId: { default: null }
59
+ suggestionId: { default: null },
60
+ _docxOuterWrapperCount: { default: null },
61
+ _docxRevisionAncestors: { default: null }
51
62
  },
52
63
  inclusive: false,
53
64
  parseDOM: [{
54
65
  tag: "span.docx-insertion",
55
66
  getAttrs(dom) {
67
+ const ancestors = revisionAncestorsFromDom(dom.dataset["revisionAncestors"]);
68
+ if (ancestors === false) return false;
56
69
  return {
57
70
  revisionId: Number.parseInt(dom.dataset["revisionId"] ?? "0", 10),
58
71
  author: dom.dataset["author"] ?? "",
59
- date: dom.dataset["date"] ?? null
72
+ date: dom.dataset["date"] ?? null,
73
+ _docxOuterWrapperCount: dom.dataset["outerWrapperCount"] ? Number(dom.dataset["outerWrapperCount"]) : null,
74
+ _docxRevisionAncestors: ancestors
60
75
  };
61
76
  }
62
77
  }],
63
78
  toDOM(mark) {
64
- const { revisionId, author, date, provenance, suggestionId } = expectTrackedChangeMarkAttrs(mark);
79
+ const { revisionId, author, date, provenance, suggestionId, _docxOuterWrapperCount, _docxRevisionAncestors } = expectTrackedChangeMarkAttrs(mark);
65
80
  const idx = getAuthorColorIdx(author);
66
81
  const color = AUTHOR_COLORS[idx] ?? "#000000";
67
82
  const titleParts = [author, date ? new Date(date).toLocaleDateString() : ""].filter(Boolean);
@@ -76,6 +91,8 @@ const InsertionExtension = createMarkExtension({
76
91
  ...suggested ? { "data-provenance": "suggested" } : {},
77
92
  ...suggested && suggestionId ? { "data-suggestion-id": suggestionId } : {},
78
93
  ...date ? { "data-date": date } : {},
94
+ ..._docxOuterWrapperCount != null ? { "data-outer-wrapper-count": String(_docxOuterWrapperCount) } : {},
95
+ ..._docxRevisionAncestors ? { "data-revision-ancestors": JSON.stringify(_docxRevisionAncestors) } : {},
79
96
  ...titleParts.length > 0 ? { title: `${suggested ? "Suggested" : "Inserted"}: ${titleParts.join(", ")}` } : {},
80
97
  style: suggested ? suggestedInsertionStyle() : insertionStyle(color)
81
98
  },
@@ -101,21 +118,27 @@ const DeletionExtension = createMarkExtension({
101
118
  initials: { default: null },
102
119
  provenance: { default: "user" },
103
120
  suggestionId: { default: null },
104
- _historicalFormatting: { default: null }
121
+ _historicalFormatting: { default: null },
122
+ _docxOuterWrapperCount: { default: null },
123
+ _docxRevisionAncestors: { default: null }
105
124
  },
106
125
  inclusive: false,
107
126
  parseDOM: [{
108
127
  tag: "span.docx-deletion",
109
128
  getAttrs(dom) {
129
+ const ancestors = revisionAncestorsFromDom(dom.dataset["revisionAncestors"]);
130
+ if (ancestors === false) return false;
110
131
  return {
111
132
  revisionId: Number.parseInt(dom.dataset["revisionId"] ?? "0", 10),
112
133
  author: dom.dataset["author"] ?? "",
113
- date: dom.dataset["date"] ?? null
134
+ date: dom.dataset["date"] ?? null,
135
+ _docxOuterWrapperCount: dom.dataset["outerWrapperCount"] ? Number(dom.dataset["outerWrapperCount"]) : null,
136
+ _docxRevisionAncestors: ancestors
114
137
  };
115
138
  }
116
139
  }],
117
140
  toDOM(mark) {
118
- const { revisionId, author, date, provenance, suggestionId } = expectTrackedChangeMarkAttrs(mark);
141
+ const { revisionId, author, date, provenance, suggestionId, _docxOuterWrapperCount, _docxRevisionAncestors } = expectTrackedChangeMarkAttrs(mark);
119
142
  const idx = getAuthorColorIdx(author);
120
143
  const color = AUTHOR_COLORS[idx] ?? "#000000";
121
144
  const titleParts = [author, date ? new Date(date).toLocaleDateString() : ""].filter(Boolean);
@@ -130,6 +153,8 @@ const DeletionExtension = createMarkExtension({
130
153
  ...suggested ? { "data-provenance": "suggested" } : {},
131
154
  ...suggested && suggestionId ? { "data-suggestion-id": suggestionId } : {},
132
155
  ...date ? { "data-date": date } : {},
156
+ ..._docxOuterWrapperCount != null ? { "data-outer-wrapper-count": String(_docxOuterWrapperCount) } : {},
157
+ ..._docxRevisionAncestors ? { "data-revision-ancestors": JSON.stringify(_docxRevisionAncestors) } : {},
133
158
  ...titleParts.length > 0 ? { title: `${suggested ? "Suggested deletion" : "Deleted"}: ${titleParts.join(", ")}` } : {},
134
159
  style: suggested ? suggestedDeletionStyle() : deletionStyle(color)
135
160
  },
@@ -13,16 +13,16 @@ declare const RUN_IDENTITY_MARK_NAME = "runIdentity";
13
13
  type RunIdentityPayload = {
14
14
  preservedAttributes?: readonly document_d_exports.PreservedAttribute[] | undefined;
15
15
  preserved?: document_d_exports.PreservedMarkup | undefined;
16
+ emptyFormatting?: boolean | undefined;
16
17
  };
17
18
  /** A run identity's attrs in canonical form. */
18
- declare const runIdentityAttrs: (id: number, { preservedAttributes, preserved }?: RunIdentityPayload) => RunIdentityMarkAttrs;
19
+ declare const runIdentityAttrs: (id: number, { preservedAttributes, preserved, emptyFormatting }?: RunIdentityPayload) => RunIdentityMarkAttrs;
19
20
  /**
20
21
  * Whether a run holds markup the mark exists to carry.
21
22
  *
22
- * The mint condition, minus the page break: a run with neither remainder nor
23
- * sink and no break to rejoin its leaves across needs no identity, and 57.5%
24
- * of corpus files hold none.
23
+ * The payload condition. The conversion also mints an identity for runs whose
24
+ * page break or mixed inline atoms split one authored run into several leaves.
25
25
  */
26
- declare const hasRunIdentityPayload: ({ preservedAttributes, preserved }: RunIdentityPayload) => boolean;
26
+ declare const hasRunIdentityPayload: ({ preservedAttributes, preserved, emptyFormatting }: RunIdentityPayload) => boolean;
27
27
  //#endregion
28
28
  export { RUN_IDENTITY_ATTRIBUTE, RUN_IDENTITY_MARK_NAME, RunIdentityPayload, hasRunIdentityPayload, runIdentityAttrs };
@@ -18,19 +18,19 @@ const preservedMarkup = ({ children }) => ({ children: (children ?? []).map(({ i
18
18
  xml
19
19
  })) });
20
20
  /** A run identity's attrs in canonical form. */
21
- const runIdentityAttrs = (id, { preservedAttributes, preserved } = {}) => {
21
+ const runIdentityAttrs = (id, { preservedAttributes, preserved, emptyFormatting } = {}) => {
22
22
  const attrs = { id };
23
23
  if (preservedAttributes && preservedAttributes.length > 0) attrs.preservedAttributes = preservedAttributes.map(preservedAttribute);
24
24
  if (preserved && (preserved.children?.length ?? 0) > 0) attrs.preserved = preservedMarkup(preserved);
25
+ if (emptyFormatting) attrs.emptyFormatting = true;
25
26
  return attrs;
26
27
  };
27
28
  /**
28
29
  * Whether a run holds markup the mark exists to carry.
29
30
  *
30
- * The mint condition, minus the page break: a run with neither remainder nor
31
- * sink and no break to rejoin its leaves across needs no identity, and 57.5%
32
- * of corpus files hold none.
31
+ * The payload condition. The conversion also mints an identity for runs whose
32
+ * page break or mixed inline atoms split one authored run into several leaves.
33
33
  */
34
- const hasRunIdentityPayload = ({ preservedAttributes, preserved }) => (preservedAttributes?.length ?? 0) > 0 || (preserved?.children?.length ?? 0) > 0;
34
+ const hasRunIdentityPayload = ({ preservedAttributes, preserved, emptyFormatting }) => (preservedAttributes?.length ?? 0) > 0 || (preserved?.children?.length ?? 0) > 0 || emptyFormatting === true;
35
35
  //#endregion
36
36
  export { RUN_IDENTITY_ATTRIBUTE, RUN_IDENTITY_MARK_NAME, hasRunIdentityPayload, runIdentityAttrs };
@@ -1,4 +1,4 @@
1
- import { CharacterSpacingAttrs, CharacterStyleAttrs, CommentAttrs, EmphasisMarkAttrs, FontFamilyAttrs, FontSizeAttrs, FootnoteRefAttrs, HighlightAttrs, HyperlinkAttrs, InlineWrapperAttrs, InlineWrapperKind, InlineWrapperLayer, LanguageAttrs, RunFormattingOverrideAttrs, RunIdentityMarkAttrs, RunPropertyChangeMarkAttrs, RunShadingAttrs, StrikeAttrs, TextColorAttrs, TextEffectAttrs, TrackedChangeMarkAttrs, UnderlineAttrs } from "./marks.js";
1
+ import { CharacterSpacingAttrs, CharacterStyleAttrs, CommentAttrs, EmphasisMarkAttrs, FontFamilyAttrs, FontSizeAttrs, FootnoteRefAttrs, HighlightAttrs, HyperlinkAttrs, InlineWrapperAttrs, InlineWrapperKind, InlineWrapperLayer, LanguageAttrs, RunFormattingOverrideAttrs, RunIdentityMarkAttrs, RunPropertyChangeMarkAttrs, RunShadingAttrs, StrikeAttrs, TextColorAttrs, TextEffectAttrs, TrackedChangeMarkAttrs, TrackedRevisionAncestor, UnderlineAttrs } from "./marks.js";
2
2
  import { BlockSdtAttrs, BookmarkBoundaryAttrs, FieldAttrs, HardBreakAttrs, ImageAttrs, ImagePositionAttrs, MathAttrs, PageBreakRunAttrs, ParagraphAttrs, ParagraphPropertyChangeAttrs, PreservedBlockAttrs, PreservedXmlAttrs, PreservedXmlLevel, SdtAttrs, ShapeAttrs, SymbolAttrs, TabAttrs, TableAttrs, TableCellAttrs, TableRowAttrs, TextBoxAttrs } from "./nodes.js";
3
3
  import { ExtensionManager } from "../extensions/ExtensionManager.js";
4
4
  //#region src/prosemirror/schema/index.d.ts
@@ -11,4 +11,4 @@ type DocxSchema = typeof schema;
11
11
  type DocxNode = ReturnType<typeof schema.node>;
12
12
  type DocxMark = ReturnType<typeof schema.mark>;
13
13
  //#endregion
14
- export { type BlockSdtAttrs, type BookmarkBoundaryAttrs, type CharacterSpacingAttrs, type CharacterStyleAttrs, type CommentAttrs, DocxMark, DocxNode, DocxSchema, type EmphasisMarkAttrs, type FieldAttrs, type FontFamilyAttrs, type FontSizeAttrs, type FootnoteRefAttrs, type HardBreakAttrs, type HighlightAttrs, type HyperlinkAttrs, type ImageAttrs, type ImagePositionAttrs, type InlineWrapperAttrs, type InlineWrapperKind, type InlineWrapperLayer, type LanguageAttrs, type MathAttrs, type PageBreakRunAttrs, type ParagraphAttrs, type ParagraphPropertyChangeAttrs, type PreservedBlockAttrs, type PreservedXmlAttrs, type PreservedXmlLevel, type RunFormattingOverrideAttrs, type RunIdentityMarkAttrs, type RunPropertyChangeMarkAttrs, type RunShadingAttrs, type SdtAttrs, type ShapeAttrs, type StrikeAttrs, type SymbolAttrs, type TabAttrs, type TableAttrs, type TableCellAttrs, type TableRowAttrs, type TextBoxAttrs, type TextColorAttrs, type TextEffectAttrs, type TrackedChangeMarkAttrs, type UnderlineAttrs, schema, singletonManager };
14
+ export { type BlockSdtAttrs, type BookmarkBoundaryAttrs, type CharacterSpacingAttrs, type CharacterStyleAttrs, type CommentAttrs, DocxMark, DocxNode, DocxSchema, type EmphasisMarkAttrs, type FieldAttrs, type FontFamilyAttrs, type FontSizeAttrs, type FootnoteRefAttrs, type HardBreakAttrs, type HighlightAttrs, type HyperlinkAttrs, type ImageAttrs, type ImagePositionAttrs, type InlineWrapperAttrs, type InlineWrapperKind, type InlineWrapperLayer, type LanguageAttrs, type MathAttrs, type PageBreakRunAttrs, type ParagraphAttrs, type ParagraphPropertyChangeAttrs, type PreservedBlockAttrs, type PreservedXmlAttrs, type PreservedXmlLevel, type RunFormattingOverrideAttrs, type RunIdentityMarkAttrs, type RunPropertyChangeMarkAttrs, type RunShadingAttrs, type SdtAttrs, type ShapeAttrs, type StrikeAttrs, type SymbolAttrs, type TabAttrs, type TableAttrs, type TableCellAttrs, type TableRowAttrs, type TextBoxAttrs, type TextColorAttrs, type TextEffectAttrs, type TrackedChangeMarkAttrs, type TrackedRevisionAncestor, type UnderlineAttrs, schema, singletonManager };
@@ -99,6 +99,15 @@ type CommentAttrs = {
99
99
  */
100
100
  declare const TRACKED_CHANGE_PROVENANCE_VALUES: readonly ["user", "suggested"];
101
101
  type TrackedChangeProvenance = (typeof TRACKED_CHANGE_PROVENANCE_VALUES)[number];
102
+ type TrackedRevisionAncestor = {
103
+ type: "insertion" | "deletion" | "moveFrom" | "moveTo";
104
+ revisionId: number;
105
+ author: string;
106
+ date?: string;
107
+ utcDate?: string;
108
+ initials?: string;
109
+ outerWrapperCount: number;
110
+ };
102
111
  type TrackedChangeMarkAttrs = {
103
112
  revisionId: number;
104
113
  author: string;
@@ -108,6 +117,10 @@ type TrackedChangeMarkAttrs = {
108
117
  /** Optional author initials (w:initials) carried through the round-trip. */
109
118
  initials?: string;
110
119
  moveKind?: "moveTo" | "moveFrom";
120
+ /** Number of inline-wrapper layers authored outside this revision. */
121
+ _docxOuterWrapperCount?: number;
122
+ /** Outer revisions, in authored order, that ProseMirror cannot hold as nested marks. */
123
+ _docxRevisionAncestors?: readonly TrackedRevisionAncestor[];
111
124
  /** Defaults to `"user"`; `"suggested"` for AI-proposed, non-serialized edits. */
112
125
  provenance: TrackedChangeProvenance;
113
126
  /**
@@ -136,7 +149,7 @@ type RunPropertyChangeMarkAttrs = {
136
149
  * in the schema would force a resolution.
137
150
  *
138
151
  * Minted only when the run has something to carry — a page break, an
139
- * attribute remainder, or a property-set remainder — so a document that
152
+ * attribute remainder, or authored run properties — so a document that
140
153
  * carries none pays nothing.
141
154
  */
142
155
  type RunIdentityMarkAttrs = {
@@ -146,6 +159,8 @@ type RunIdentityMarkAttrs = {
146
159
  preservedAttributes?: readonly document_d_exports.PreservedAttribute[];
147
160
  /** The `w:rPr` children no reader took a value from, with their ordinals. */
148
161
  preserved?: document_d_exports.PreservedMarkup;
162
+ /** Preserve an authored empty `w:rPr`, distinct from no run properties. */
163
+ emptyFormatting?: true;
149
164
  };
150
165
  /**
151
166
  * One transparent wrapper a leaf sits inside, minus the content ProseMirror
@@ -289,4 +304,4 @@ type HyperlinkAttrs = {
289
304
  _docxHyperlinkIndex?: number;
290
305
  };
291
306
  //#endregion
292
- export { AuthoredRunFormattingValues, COMPLEX_SCRIPT_RUN_PROPERTY_KEYS, CharacterSpacingAttrs, CharacterStyleAttrs, CommentAttrs, ComplexScriptRunPropertyKey, EmphasisMarkAttrs, FontFamilyAttrs, FontSizeAttrs, FootnoteRefAttrs, HighlightAttrs, HyperlinkAttrs, InlineWrapperAttrs, InlineWrapperKind, InlineWrapperLayer, LanguageAttrs, RUN_FORMATTING_BOOLEAN_PROPERTIES, RUN_FORMATTING_PROPERTY_SPECS, RUN_FORMATTING_VALUE_PROPERTIES, RunFormattingBooleanProperty, RunFormattingOverrideAttrs, RunFormattingValueProperty, RunIdentityMarkAttrs, RunPropertyChangeMarkAttrs, RunShadingAttrs, StrikeAttrs, TRACKED_CHANGE_PROVENANCE_VALUES, TextColorAttrs, TextEffectAttrs, TrackedChangeMarkAttrs, TrackedChangeProvenance, UnderlineAttrs };
307
+ export { AuthoredRunFormattingValues, COMPLEX_SCRIPT_RUN_PROPERTY_KEYS, CharacterSpacingAttrs, CharacterStyleAttrs, CommentAttrs, ComplexScriptRunPropertyKey, EmphasisMarkAttrs, FontFamilyAttrs, FontSizeAttrs, FootnoteRefAttrs, HighlightAttrs, HyperlinkAttrs, InlineWrapperAttrs, InlineWrapperKind, InlineWrapperLayer, LanguageAttrs, RUN_FORMATTING_BOOLEAN_PROPERTIES, RUN_FORMATTING_PROPERTY_SPECS, RUN_FORMATTING_VALUE_PROPERTIES, RunFormattingBooleanProperty, RunFormattingOverrideAttrs, RunFormattingValueProperty, RunIdentityMarkAttrs, RunPropertyChangeMarkAttrs, RunShadingAttrs, StrikeAttrs, TRACKED_CHANGE_PROVENANCE_VALUES, TextColorAttrs, TextEffectAttrs, TrackedChangeMarkAttrs, TrackedChangeProvenance, TrackedRevisionAncestor, UnderlineAttrs };
@@ -1,4 +1,4 @@
1
- import { expectRunPropertyChangeMarkAttrs } from "../attrs/index.js";
1
+ import { expectRunPropertyChangeMarkAttrs, expectTrackedChangeMarkAttrs } from "../attrs/index.js";
2
2
  import { nodePropertyRevisionSites, propertyRevisionMetadata, propertyRevisionRecords } from "../revisionCarriers.js";
3
3
  import { getTableCellMergeChange } from "../tableCellMergeRevision.js";
4
4
  //#region src/prosemirror/utils/extractTrackedChanges.ts
@@ -46,6 +46,7 @@ function extractTrackedChanges(state) {
46
46
  const runPropertyChangeType = schema.marks["runPropertyChange"];
47
47
  if (!insertionType && !deletionType && !runPropertyChangeType) return EMPTY_RESULT;
48
48
  const raw = [];
49
+ const nestedRevisionIds = /* @__PURE__ */ new Set();
49
50
  const commentToRevision = /* @__PURE__ */ new Map();
50
51
  const commentMetadata = /* @__PURE__ */ new Map();
51
52
  const rowRevisionScopes = [];
@@ -223,14 +224,28 @@ function extractTrackedChanges(state) {
223
224
  if (mark.type === insertionType || mark.type === deletionType) {
224
225
  tcMark = mark;
225
226
  if (foldIntoRowRevision(rowRevisionScopes.at(-1), mark)) continue;
227
+ const attrs = expectTrackedChangeMarkAttrs(mark);
228
+ if (attrs._docxRevisionAncestors?.length) nestedRevisionIds.add(attrs.revisionId);
229
+ for (const ancestor of attrs._docxRevisionAncestors ?? []) {
230
+ nestedRevisionIds.add(ancestor.revisionId);
231
+ raw.push({
232
+ type: ancestor.type === "insertion" || ancestor.type === "moveTo" ? "insertion" : "deletion",
233
+ text: inlineText,
234
+ author: ancestor.author,
235
+ date: ancestor.date,
236
+ from: pos,
237
+ to: pos + node.nodeSize,
238
+ revisionId: ancestor.revisionId
239
+ });
240
+ }
226
241
  raw.push({
227
242
  type: mark.type === insertionType ? "insertion" : "deletion",
228
243
  text: inlineText,
229
- author: mark.attrs["author"] || "",
230
- date: mark.attrs["date"],
244
+ author: attrs.author,
245
+ date: attrs.date,
231
246
  from: pos,
232
247
  to: pos + node.nodeSize,
233
- revisionId: mark.attrs["revisionId"]
248
+ revisionId: attrs.revisionId
234
249
  });
235
250
  }
236
251
  }
@@ -380,7 +395,7 @@ function extractTrackedChanges(state) {
380
395
  merged.push({ ...entry });
381
396
  continue;
382
397
  }
383
- const key = `${entry.type}|${entry.author}|${entry.date ?? ""}`;
398
+ const key = `${entry.type}|${entry.author}|${entry.date ?? ""}|${nestedRevisionIds.has(entry.revisionId) ? entry.revisionId : ""}`;
384
399
  const group = inlineGroups.get(key);
385
400
  if (group) {
386
401
  const sep = group.to === entry.from ? "" : " ";
@@ -403,7 +418,7 @@ function extractTrackedChanges(state) {
403
418
  for (let i = 0; i < merged.length; i++) {
404
419
  const curr = merged[i];
405
420
  const next = merged[i + 1];
406
- if (curr.type === "deletion" && next && next.type === "insertion" && curr.author === next.author && curr.to === next.from) {
421
+ if (curr.type === "deletion" && next && next.type === "insertion" && !nestedRevisionIds.has(curr.revisionId) && !nestedRevisionIds.has(next.revisionId) && curr.author === next.author && curr.to === next.from) {
407
422
  final.push({
408
423
  type: "replacement",
409
424
  text: next.text,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/folio-core",
3
- "version": "0.47.5",
3
+ "version": "0.47.6",
4
4
  "description": "Headless, framework-neutral core of folio: the OOXML (.docx) parser, document model, ProseMirror integration, and page-layout engine. No React.",
5
5
  "keywords": [
6
6
  "document-model",