@stll/folio-core 0.47.5 → 0.47.7

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 { 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.7",
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",