@stll/folio-core 0.35.1 → 0.36.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 (80) hide show
  1. package/dist/ai-edits/apply.js +370 -118
  2. package/dist/ai-edits/headless.js +16 -10
  3. package/dist/ai-edits/index.d.ts +2 -2
  4. package/dist/ai-edits/index.js +2 -2
  5. package/dist/ai-edits/snapshot.js +6 -1
  6. package/dist/ai-edits/types.d.ts +19 -6
  7. package/dist/ai-edits/word-diff.d.ts +18 -3
  8. package/dist/ai-edits/word-diff.js +556 -67
  9. package/dist/compare/compare.js +7 -16
  10. package/dist/compare/plan.js +8 -5
  11. package/dist/compare/verification.d.ts +8 -9
  12. package/dist/compare/verification.js +51 -46
  13. package/dist/compat/eigenpal.d.ts +2 -2
  14. package/dist/compat/eigenpal.js +2 -2
  15. package/dist/controller/headerFooterEditorManager.js +5 -5
  16. package/dist/controller/hiddenEditorApi.js +1 -1
  17. package/dist/controller/noteEditorManager.js +2 -2
  18. package/dist/document-operations.d.ts +5 -3
  19. package/dist/document-operations.js +32 -5
  20. package/dist/docx/commentParser.js +2 -4
  21. package/dist/docx/commentRangeIntegrity.js +2 -4
  22. package/dist/docx/documentClone.d.ts +2 -0
  23. package/dist/docx/documentClone.js +2 -0
  24. package/dist/docx/headerFooterParser.js +2 -4
  25. package/dist/docx/normalizeBaseDirection.js +5 -7
  26. package/dist/docx/paragraphParser.js +27 -29
  27. package/dist/docx/paragraphPropertySource.d.ts +49 -0
  28. package/dist/docx/paragraphPropertySource.js +113 -0
  29. package/dist/docx/runConsolidator.js +2 -4
  30. package/dist/docx/runParser.js +2 -15
  31. package/dist/docx/sectionParser.js +1 -15
  32. package/dist/docx/serializer/paragraphSerializer.js +287 -30
  33. package/dist/docx/serializer/runSerializer.js +2 -10
  34. package/dist/docx/serializer/sectionPropertiesSerializer.js +2 -10
  35. package/dist/docx/serializer/tableSerializer.js +4 -23
  36. package/dist/docx/serializer/trackedChangeAttributes.d.ts +9 -0
  37. package/dist/docx/serializer/trackedChangeAttributes.js +21 -0
  38. package/dist/docx/server/createBilingualDocument.js +8 -10
  39. package/dist/docx/server/evaluateDocxXmlPatchProposal.js +22 -13
  40. package/dist/docx/server/validateDocxConformance.js +17 -13
  41. package/dist/docx/tableParser.js +1 -37
  42. package/dist/docx/trackedChangeInfo.d.ts +11 -0
  43. package/dist/docx/trackedChangeInfo.js +38 -0
  44. package/dist/docx/verbatimCapture.d.ts +20 -9
  45. package/dist/docx/verbatimCapture.js +208 -8
  46. package/dist/docx/xmlParser.d.ts +3 -1
  47. package/dist/docx/xmlParser.js +6 -1
  48. package/dist/docx/xmlSafety.d.ts +1 -1
  49. package/dist/docx/xmlSafety.js +157 -0
  50. package/dist/index.d.ts +2 -2
  51. package/dist/index.js +2 -2
  52. package/dist/layout-bridge/convert/headerFooterLayout.js +4 -4
  53. package/dist/layout-bridge/convert/toFlowBlocks.js +12 -7
  54. package/dist/markdown/renderBlock.js +7 -10
  55. package/dist/prosemirror/attrs/index.js +15 -0
  56. package/dist/prosemirror/commands/comments.js +365 -32
  57. package/dist/prosemirror/commands/pageBreak.js +1 -1
  58. package/dist/prosemirror/commands/propertyChangeScope.d.ts +26 -4
  59. package/dist/prosemirror/commands/propertyChangeScope.js +65 -3
  60. package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -4
  61. package/dist/prosemirror/conversion/fromProseDoc.js +130 -18
  62. package/dist/prosemirror/conversion/toProseDoc.js +27 -12
  63. package/dist/prosemirror/extensions/core/ParagraphExtension.js +48 -4
  64. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -1
  65. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +20 -7
  66. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +2 -0
  67. package/dist/prosemirror/paragraphAlignment.d.ts +12 -0
  68. package/dist/prosemirror/paragraphAlignment.js +13 -0
  69. package/dist/prosemirror/plugins/suggestionMode.js +1 -1
  70. package/dist/prosemirror/schema/marks.d.ts +2 -0
  71. package/dist/prosemirror/schema/nodes.d.ts +12 -1
  72. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  73. package/dist/redline.js +4 -1
  74. package/dist/server.d.ts +2 -2
  75. package/dist/server.js +2 -2
  76. package/dist/utils/mergeDocumentContent.js +3 -3
  77. package/dist/utils/replaceText.js +2 -1
  78. package/dist/version-comparison.js +6 -4
  79. package/dist/watermark/index.js +2 -4
  80. package/package.json +6 -1
@@ -1,3 +1,4 @@
1
+ import { cloneParagraphWithPropertySource } from "./paragraphPropertySource.js";
1
2
  //#region src/docx/commentRangeIntegrity.ts
2
3
  const withoutOrphanCommentRanges = (doc) => {
3
4
  const validCommentIds = new Set((doc.package.document.comments ?? []).map((comment) => comment.id));
@@ -176,10 +177,7 @@ const withoutOrphanParagraphMarkers = (paragraph, validCommentIds) => {
176
177
  content.push(item);
177
178
  }
178
179
  if (!changed) return paragraph;
179
- return {
180
- ...paragraph,
181
- content
182
- };
180
+ return cloneParagraphWithPropertySource(paragraph, { content });
183
181
  };
184
182
  const isCommentMarker = (content) => content.type === "commentRangeStart" || content.type === "commentRangeEnd" || content.type === "commentReference";
185
183
  //#endregion
@@ -0,0 +1,2 @@
1
+ import { cloneDocumentWithParagraphPropertySources } from "./paragraphPropertySource.js";
2
+ export { cloneDocumentWithParagraphPropertySources };
@@ -0,0 +1,2 @@
1
+ import { cloneDocumentWithParagraphPropertySources } from "./paragraphPropertySource.js";
2
+ export { cloneDocumentWithParagraphPropertySources };
@@ -1,6 +1,7 @@
1
1
  import { parseBlockContent } from "./blockContentParser.js";
2
2
  import { parseFooterReference, parseFooterReferences, parseHeaderReference, parseHeaderReferences } from "./headerFooterRefParser.js";
3
3
  import { assignHeaderFooterVerbatimXml } from "./headerFooterVerbatim.js";
4
+ import { cloneParagraphWithPropertySource } from "./paragraphPropertySource.js";
4
5
  import { parseWatermark } from "./watermarkParser.js";
5
6
  import { collectXmlnsDeclarations, parseXml } from "./xmlParser.js";
6
7
  //#region src/docx/headerFooterParser.ts
@@ -37,10 +38,7 @@ function parseHeader(headerXml, hdrFtrType = "default", styles = null, theme = n
37
38
  });
38
39
  if (watermarkResult) {
39
40
  const host = result.content.at(watermarkResult.blockIndex);
40
- if (host?.type === "paragraph") result.content[watermarkResult.blockIndex] = {
41
- ...host,
42
- content: []
43
- };
41
+ if (host?.type === "paragraph") result.content[watermarkResult.blockIndex] = cloneParagraphWithPropertySource(host, { content: [] });
44
42
  }
45
43
  assignHeaderFooterVerbatimXml(result, headerXml);
46
44
  return result;
@@ -1,5 +1,6 @@
1
1
  import { createStyleResolver } from "../prosemirror/styles/styleResolver.js";
2
2
  import { detectBaseDirection } from "../utils/baseDirection.js";
3
+ import { cloneParagraphWithoutPropertySource } from "./paragraphPropertySource.js";
3
4
  //#region src/docx/normalizeBaseDirection.ts
4
5
  const runText = (run) => {
5
6
  let text = "";
@@ -32,13 +33,10 @@ const paragraphText = (paragraph) => paragraphContentText(paragraph.content);
32
33
  const normalizeParagraph = (paragraph, styles) => {
33
34
  if ((paragraph.formatting?.bidi ?? styles.resolveParagraphStyle(paragraph.formatting?.styleId).paragraphFormatting?.bidi) != null) return paragraph;
34
35
  if (detectBaseDirection(paragraphText(paragraph)) !== "rtl") return paragraph;
35
- return {
36
- ...paragraph,
37
- formatting: {
38
- ...paragraph.formatting,
39
- bidi: true
40
- }
41
- };
36
+ return cloneParagraphWithoutPropertySource(paragraph, { formatting: {
37
+ ...paragraph.formatting,
38
+ bidi: true
39
+ } });
42
40
  };
43
41
  const normalizeTable = (table, styles) => ({
44
42
  ...table,
@@ -1,3 +1,4 @@
1
+ import { canonicalJson } from "../utils/canonicalJson.js";
1
2
  import { isValidHexColor } from "../utils/colorResolver.js";
2
3
  import { isValidHexId } from "../utils/hexId.js";
3
4
  import { parseBookmarkEnd as parseBookmarkEnd$1, parseBookmarkStart as parseBookmarkStart$1 } from "./bookmarkParser.js";
@@ -5,15 +6,17 @@ import { parseFieldType } from "./fieldParser.js";
5
6
  import { parseHyperlink as parseHyperlink$1, parseHyperlinkChild } from "./hyperlinkParser.js";
6
7
  import { markerFormattingFromLevel } from "./numberingParser.js";
7
8
  import { paraIdInRange } from "./paraIdRangeNormalization.js";
9
+ import { assignParagraphPropertySource } from "./paragraphPropertySource.js";
8
10
  import { BorderStyleSchema, FrameWrapSchema, FrameXAlignSchema, FrameYAlignSchema, LineSpacingRuleSchema, ParagraphAlignmentSchema, ShadingPatternSchema, TabLeaderSchema, TabStopAlignmentSchema, ThemeColorSlotSchema, narrowEnum } from "./parserEnums.js";
9
11
  import { consolidateParagraphContent } from "./runConsolidator.js";
10
12
  import { parseRun, parseRunProperties } from "./runParser.js";
11
13
  import { parseSdtProperties } from "./sdtProperties.js";
12
14
  import { parseSectionProperties } from "./sectionParser.js";
15
+ import { parsePropertyChangeInfo, parseTrackedChangeInfo } from "./trackedChangeInfo.js";
13
16
  import { captureVerbatimXml } from "./verbatimCapture.js";
14
- import { WORDPROCESSINGML_NAMESPACE_URIS, findChild, findChildByNamespaceUri, findChildren, getAttribute, getChildElements, getLocalName, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseNumberingLevelAttribute, parseNumericAttribute } from "./xmlParser.js";
17
+ import { WORDPROCESSINGML_NAMESPACE_URIS, cloneElement, findChild, findChildByNamespaceUri, findChildren, findChildrenByNamespaceUri, getAttribute, getChildElements, getLocalName, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseNumberingLevelAttribute, parseNumericAttribute } from "./xmlParser.js";
15
18
  import { panic } from "better-result";
16
- import { PARAGRAPH_MARK_CHANGE_KINDS, normalizeRevisionId } from "@stll/docx-core/model";
19
+ import { PARAGRAPH_MARK_CHANGE_KINDS } from "@stll/docx-core/model";
17
20
  //#region src/docx/paragraphParser.ts
18
21
  /**
19
22
  * Extract plain text from a math element (recursive text content extraction)
@@ -373,6 +376,21 @@ function parseParagraphProperties(pPr, theme, styles) {
373
376
  }
374
377
  return Object.keys(formatting).length > 0 ? formatting : void 0;
375
378
  }
379
+ /**
380
+ * Capture the authored paragraph properties separately from structural and
381
+ * revision children, which have their own model fields and lifecycles.
382
+ */
383
+ const captureParagraphPropertySource = (pPr) => captureVerbatimXml(cloneElement(pPr, { elements: (pPr.elements ?? []).flatMap((child) => {
384
+ if (child.type !== "element") return [child];
385
+ const localName = getLocalName(child.name);
386
+ if (WORDPROCESSINGML_NAMESPACE_URIS.has(child.namespaceUri ?? "") && (localName === "sectPr" || localName === "pPrChange")) return [];
387
+ if (localName !== "rPr" || !WORDPROCESSINGML_NAMESPACE_URIS.has(child.namespaceUri ?? "")) return [child];
388
+ return [cloneElement(child, { elements: (child.elements ?? []).filter((runProperty) => {
389
+ if (runProperty.type !== "element") return true;
390
+ const runPropertyName = getLocalName(runProperty.name);
391
+ return !WORDPROCESSINGML_NAMESPACE_URIS.has(runProperty.namespaceUri ?? "") || !PARAGRAPH_MARK_CHANGE_KINDS.some((kind) => kind === runPropertyName);
392
+ }) })];
393
+ }) }));
376
394
  const RENDERED_BREAK_INLINE_WRAPPERS = /* @__PURE__ */ new Set([
377
395
  "hyperlink",
378
396
  "smartTag",
@@ -482,34 +500,10 @@ function normalizeDeletionContentElement(node) {
482
500
  if (node.elements) result.elements = node.elements.map(normalizeDeletionContentElement);
483
501
  return result;
484
502
  }
485
- function parseTrackedChangeInfo(node) {
486
- const rawId = getAttribute(node, "w", "id");
487
- const parsedId = rawId ? Number.parseInt(rawId, 10) : 0;
488
- const rawAuthor = getAttribute(node, "w", "author");
489
- const rawDate = getAttribute(node, "w", "date");
490
- const author = rawAuthor?.trim() ?? "";
491
- const date = rawDate?.trim() ?? "";
492
- const initials = (getAttribute(node, "w", "initials") ?? "").trim();
493
- const info = {
494
- id: normalizeRevisionId(parsedId),
495
- author: author.length > 0 ? author : "Unknown"
496
- };
497
- if (date.length > 0) info.date = date;
498
- if (initials.length > 0) info.initials = initials;
499
- return info;
500
- }
501
- function parsePropertyChangeInfo(node) {
502
- const base = parseTrackedChangeInfo(node);
503
- const rsid = (getAttribute(node, "w", "rsid") ?? "").trim();
504
- return rsid.length > 0 ? {
505
- ...base,
506
- rsid
507
- } : base;
508
- }
509
503
  function parseParagraphPropertyChanges(pPr, theme, styles, currentFormatting) {
510
504
  if (!pPr) return;
511
- const changes = findChildren(pPr, "w", "pPrChange").map((changeElement) => {
512
- const previousFormatting = parseParagraphProperties(findChild(changeElement, "w", "pPr"), theme, styles ?? void 0);
505
+ const changes = findChildrenByNamespaceUri(pPr, WORDPROCESSINGML_NAMESPACE_URIS, "pPrChange").map((changeElement) => {
506
+ const previousFormatting = parseParagraphProperties(findChildByNamespaceUri(changeElement, WORDPROCESSINGML_NAMESPACE_URIS, "pPr"), theme, styles ?? void 0);
513
507
  const change = {
514
508
  type: "paragraphPropertyChange",
515
509
  info: parsePropertyChangeInfo(changeElement)
@@ -528,7 +522,7 @@ function parseParagraphPropertyChanges(pPr, theme, styles, currentFormatting) {
528
522
  */
529
523
  function parseParagraphMarkChange(pPr) {
530
524
  if (!pPr) return;
531
- const rPr = findChild(pPr, "w", "rPr");
525
+ const rPr = findChildByNamespaceUri(pPr, WORDPROCESSINGML_NAMESPACE_URIS, "rPr");
532
526
  if (!rPr) return;
533
527
  for (const kind of PARAGRAPH_MARK_CHANGE_KINDS) {
534
528
  const element = findChildByNamespaceUri(rPr, WORDPROCESSINGML_NAMESPACE_URIS, kind);
@@ -1200,6 +1194,10 @@ function parseParagraph(node, styles, theme, numbering, rels = null, media = nul
1200
1194
  }
1201
1195
  }
1202
1196
  }
1197
+ if (pPr) assignParagraphPropertySource(paragraph, {
1198
+ xml: captureParagraphPropertySource(pPr),
1199
+ formattingJson: canonicalJson(paragraph.formatting ?? {})
1200
+ });
1203
1201
  return paragraph;
1204
1202
  }
1205
1203
  /**
@@ -0,0 +1,49 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ import { Transaction } from "prosemirror-state";
3
+ import { Fragment, Mark, Node } from "prosemirror-model";
4
+ //#region src/docx/paragraphPropertySource.d.ts
5
+ type ParagraphPropertySource = {
6
+ xml: string;
7
+ formattingJson: string;
8
+ };
9
+ declare const assignParagraphPropertySource: (paragraph: document_d_exports.Paragraph, source: ParagraphPropertySource) => void;
10
+ declare const getParagraphPropertySource: (paragraph: document_d_exports.Paragraph) => ParagraphPropertySource | undefined;
11
+ declare const copyParagraphPropertySource: (target: document_d_exports.Paragraph, source: document_d_exports.Paragraph) => void;
12
+ type ParagraphCloneOverrides = Omit<Partial<document_d_exports.Paragraph>, "type">;
13
+ /** Clone a paragraph while deliberately retaining its parsed `w:pPr` owner. */
14
+ declare const cloneParagraphWithPropertySource: (paragraph: document_d_exports.Paragraph, overrides: ParagraphCloneOverrides) => document_d_exports.Paragraph;
15
+ /** Clone a paragraph whose formatting provenance is deliberately no longer applicable. */
16
+ declare const cloneParagraphWithoutPropertySource: (paragraph: document_d_exports.Paragraph, overrides: ParagraphCloneOverrides) => document_d_exports.Paragraph;
17
+ /**
18
+ * Deep-clone a document and transfer each private paragraph capture across the
19
+ * exact graph clone. `structuredClone` preserves graph topology, so any count
20
+ * mismatch is an internal invariant failure rather than a position heuristic.
21
+ */
22
+ declare const cloneDocumentWithParagraphPropertySources: (document: document_d_exports.Document) => document_d_exports.Document;
23
+ declare const linkProseParagraphPropertySource: (proseParagraph: Node, sourceParagraph: document_d_exports.Paragraph) => void;
24
+ type RecreateProseNodeOptions = {
25
+ attrs?: Node["attrs"];
26
+ content?: Fragment | Node | readonly Node[] | null;
27
+ marks?: readonly Mark[];
28
+ };
29
+ /** Rebuild a PM node and retain paragraph provenance when the node is one. */
30
+ declare const recreateProseNodeWithParagraphPropertySource: (source: Node, options?: RecreateProseNodeOptions) => Node;
31
+ type SetProseParagraphMarkupOptions = {
32
+ attrs: Node["attrs"];
33
+ ownership: "preserve" | "transfer-allocated-id";
34
+ pos: number;
35
+ transaction: Transaction;
36
+ };
37
+ /** Replace paragraph markup without losing its private parser-owner link. */
38
+ declare const setProseParagraphMarkupWithPropertySource: ({ attrs, ownership, pos, transaction }: SetProseParagraphMarkupOptions) => void;
39
+ /**
40
+ * Carry the parser-owned source identity across load-time paraId allocation.
41
+ * The generated id is safe to use later because it was assigned while the
42
+ * ProseMirror node still had an unambiguous source paragraph owner.
43
+ */
44
+ declare const transferProseParagraphPropertySource: (target: Node, source: Node, paraId: string) => void;
45
+ declare const linkParagraphPropertySourceCandidate: (target: document_d_exports.Paragraph, source: Node) => void;
46
+ declare const getParagraphPropertySourceCandidate: (paragraph: document_d_exports.Paragraph) => document_d_exports.Paragraph | undefined;
47
+ declare const getParagraphPropertySourceTransferId: (paragraph: document_d_exports.Paragraph) => string | undefined;
48
+ //#endregion
49
+ export { assignParagraphPropertySource, cloneDocumentWithParagraphPropertySources, cloneParagraphWithPropertySource, cloneParagraphWithoutPropertySource, copyParagraphPropertySource, getParagraphPropertySource, getParagraphPropertySourceCandidate, getParagraphPropertySourceTransferId, linkParagraphPropertySourceCandidate, linkProseParagraphPropertySource, recreateProseNodeWithParagraphPropertySource, setProseParagraphMarkupWithPropertySource, transferProseParagraphPropertySource };
@@ -0,0 +1,113 @@
1
+ import { visitDocxParagraphs } from "./paragraphTraversal.js";
2
+ import { panic } from "better-result";
3
+ //#region src/docx/paragraphPropertySource.ts
4
+ const paragraphPropertySources = /* @__PURE__ */ new WeakMap();
5
+ const paragraphPropertySourceOwners = /* @__PURE__ */ new WeakMap();
6
+ const proseParagraphSourceOwners = /* @__PURE__ */ new WeakMap();
7
+ const paragraphPropertySourceCandidates = /* @__PURE__ */ new WeakMap();
8
+ const paragraphPropertySourceTransferIds = /* @__PURE__ */ new WeakMap();
9
+ const assignParagraphPropertySource = (paragraph, source) => {
10
+ paragraphPropertySources.set(paragraph, source);
11
+ paragraphPropertySourceOwners.set(paragraph, paragraph);
12
+ };
13
+ const getParagraphPropertySource = (paragraph) => paragraphPropertySources.get(paragraph);
14
+ const copyParagraphPropertySource = (target, source) => {
15
+ const propertySource = paragraphPropertySources.get(source);
16
+ if (propertySource) {
17
+ paragraphPropertySources.set(target, { ...propertySource });
18
+ paragraphPropertySourceOwners.set(target, paragraphPropertySourceOwners.get(source) ?? source);
19
+ }
20
+ const transferId = paragraphPropertySourceTransferIds.get(source);
21
+ if (transferId) paragraphPropertySourceTransferIds.set(target, transferId);
22
+ const candidate = paragraphPropertySourceCandidates.get(source);
23
+ if (candidate) paragraphPropertySourceCandidates.set(target, candidate);
24
+ };
25
+ /** Clone a paragraph while deliberately retaining its parsed `w:pPr` owner. */
26
+ const cloneParagraphWithPropertySource = (paragraph, overrides) => {
27
+ const cloned = {
28
+ ...paragraph,
29
+ ...overrides
30
+ };
31
+ copyParagraphPropertySource(cloned, paragraph);
32
+ return cloned;
33
+ };
34
+ /** Clone a paragraph whose formatting provenance is deliberately no longer applicable. */
35
+ const cloneParagraphWithoutPropertySource = (paragraph, overrides) => ({
36
+ ...paragraph,
37
+ ...overrides
38
+ });
39
+ const paragraphsIn = (document) => {
40
+ const paragraphs = [];
41
+ visitDocxParagraphs({
42
+ documentBody: document.package.document,
43
+ headers: document.package.headers,
44
+ footers: document.package.footers,
45
+ footnotes: document.package.footnotes,
46
+ endnotes: document.package.endnotes
47
+ }, (paragraph) => paragraphs.push(paragraph));
48
+ return paragraphs;
49
+ };
50
+ /**
51
+ * Deep-clone a document and transfer each private paragraph capture across the
52
+ * exact graph clone. `structuredClone` preserves graph topology, so any count
53
+ * mismatch is an internal invariant failure rather than a position heuristic.
54
+ */
55
+ const cloneDocumentWithParagraphPropertySources = (document) => {
56
+ const cloned = structuredClone(document);
57
+ const sources = paragraphsIn(document);
58
+ const targets = paragraphsIn(cloned);
59
+ if (sources.length !== targets.length) panic("The cloned document changed paragraph graph ownership.");
60
+ for (const [index, source] of sources.entries()) {
61
+ const target = targets.at(index);
62
+ if (!target) panic("The cloned document lost a paragraph owner.");
63
+ copyParagraphPropertySource(target, source);
64
+ }
65
+ return cloned;
66
+ };
67
+ const linkProseParagraphPropertySource = (proseParagraph, sourceParagraph) => {
68
+ if (paragraphPropertySources.has(sourceParagraph)) proseParagraphSourceOwners.set(proseParagraph, paragraphPropertySourceOwners.get(sourceParagraph) ?? sourceParagraph);
69
+ };
70
+ /** Carry a parser-linked paragraph owner across an immutable PM node rebuild. */
71
+ const copyProseParagraphPropertySource = (target, source) => {
72
+ if (target.type.name !== "paragraph" || source.type.name !== "paragraph") return;
73
+ const sourceOwner = proseParagraphSourceOwners.get(source);
74
+ if (sourceOwner) proseParagraphSourceOwners.set(target, sourceOwner);
75
+ };
76
+ /** Rebuild a PM node and retain paragraph provenance when the node is one. */
77
+ const recreateProseNodeWithParagraphPropertySource = (source, options = {}) => {
78
+ const target = source.type.create(options.attrs ?? source.attrs, options.content === void 0 ? source.content : options.content, options.marks ?? source.marks);
79
+ copyProseParagraphPropertySource(target, source);
80
+ return target;
81
+ };
82
+ /** Replace paragraph markup without losing its private parser-owner link. */
83
+ const setProseParagraphMarkupWithPropertySource = ({ attrs, ownership, pos, transaction }) => {
84
+ const source = transaction.doc.nodeAt(pos);
85
+ transaction.setNodeMarkup(pos, void 0, attrs);
86
+ const target = transaction.doc.nodeAt(pos);
87
+ if (!source || !target) return;
88
+ if (ownership === "preserve") {
89
+ copyProseParagraphPropertySource(target, source);
90
+ return;
91
+ }
92
+ const paraId = target.attrs["paraId"];
93
+ if (typeof paraId === "string") transferProseParagraphPropertySource(target, source, paraId);
94
+ };
95
+ /**
96
+ * Carry the parser-owned source identity across load-time paraId allocation.
97
+ * The generated id is safe to use later because it was assigned while the
98
+ * ProseMirror node still had an unambiguous source paragraph owner.
99
+ */
100
+ const transferProseParagraphPropertySource = (target, source, paraId) => {
101
+ const sourceOwner = proseParagraphSourceOwners.get(source);
102
+ if (!sourceOwner) return;
103
+ proseParagraphSourceOwners.set(target, sourceOwner);
104
+ paragraphPropertySourceTransferIds.set(sourceOwner, paraId);
105
+ };
106
+ const linkParagraphPropertySourceCandidate = (target, source) => {
107
+ const sourceOwner = proseParagraphSourceOwners.get(source);
108
+ if (sourceOwner) paragraphPropertySourceCandidates.set(target, sourceOwner);
109
+ };
110
+ const getParagraphPropertySourceCandidate = (paragraph) => paragraphPropertySourceCandidates.get(paragraph);
111
+ const getParagraphPropertySourceTransferId = (paragraph) => paragraphPropertySourceTransferIds.get(paragraph);
112
+ //#endregion
113
+ export { assignParagraphPropertySource, cloneDocumentWithParagraphPropertySources, cloneParagraphWithPropertySource, cloneParagraphWithoutPropertySource, copyParagraphPropertySource, getParagraphPropertySource, getParagraphPropertySourceCandidate, getParagraphPropertySourceTransferId, linkParagraphPropertySourceCandidate, linkProseParagraphPropertySource, recreateProseNodeWithParagraphPropertySource, setProseParagraphMarkupWithPropertySource, transferProseParagraphPropertySource };
@@ -1,3 +1,4 @@
1
+ import { cloneParagraphWithPropertySource } from "./paragraphPropertySource.js";
1
2
  //#region src/docx/runConsolidator.ts
2
3
  /**
3
4
  * Check if two TextFormatting objects are equivalent
@@ -193,10 +194,7 @@ function consolidateParagraphContent(content) {
193
194
  */
194
195
  function consolidateParagraph(paragraph) {
195
196
  if (paragraph.content.length === 0) return paragraph;
196
- return {
197
- ...paragraph,
198
- content: consolidateParagraphContent(paragraph.content)
199
- };
197
+ return cloneParagraphWithPropertySource(paragraph, { content: consolidateParagraphContent(paragraph.content) });
200
198
  }
201
199
  /**
202
200
  * Get the number of runs in a paragraph (for debugging/metrics)
@@ -7,10 +7,11 @@ import { parseShapeFromDrawing, shouldPreserveRawShapeDrawing } from "./shapePar
7
7
  import { isTextBoxDrawing } from "./textBoxParser.js";
8
8
  import { requiresXmlSpacePreserve } from "./textWhitespace.js";
9
9
  import { resolveThemeFontRef } from "./themeParser.js";
10
+ import { parsePropertyChangeInfo } from "./trackedChangeInfo.js";
10
11
  import { captureVerbatimXml } from "./verbatimCapture.js";
11
12
  import { parseVmlImageContent } from "./vmlImageParser.js";
12
13
  import { cloneWithXmlnsDeclarations, findAllDeep, findChild, findChildren, getAttribute, getChildElements, getLocalName, getTextContent, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute } from "./xmlParser.js";
13
- import { DRAWING_RAW_XML_MODES, normalizeRevisionId } from "@stll/docx-core/model";
14
+ import { DRAWING_RAW_XML_MODES } from "@stll/docx-core/model";
14
15
  //#region src/docx/runParser.ts
15
16
  /**
16
17
  * Sanity cap on `w:lang` `@w:val`/`@w:eastAsia`/`@w:bidi` tag length. BCP-47
@@ -341,20 +342,6 @@ function parseRunProperties(rPr, theme, _styles) {
341
342
  }
342
343
  return Object.keys(formatting).length > 0 ? formatting : void 0;
343
344
  }
344
- function parsePropertyChangeInfo(changeElement) {
345
- const rawId = getAttribute(changeElement, "w", "id");
346
- const parsedId = rawId ? Number.parseInt(rawId, 10) : 0;
347
- const author = (getAttribute(changeElement, "w", "author") ?? "").trim();
348
- const date = (getAttribute(changeElement, "w", "date") ?? "").trim();
349
- const rsid = (getAttribute(changeElement, "w", "rsid") ?? "").trim();
350
- const info = {
351
- id: normalizeRevisionId(parsedId),
352
- author: author.length > 0 ? author : "Unknown"
353
- };
354
- if (date.length > 0) info.date = date;
355
- if (rsid.length > 0) info.rsid = rsid;
356
- return info;
357
- }
358
345
  function parseRunPropertyChanges(rPr, theme, styles, currentFormatting) {
359
346
  if (!rPr) return;
360
347
  const changes = findChildren(rPr, "w", "rPrChange").map((changeElement) => {
@@ -1,8 +1,8 @@
1
1
  import { parseFooterReference, parseHeaderReference } from "./headerFooterRefParser.js";
2
2
  import { parseEndnoteProperties, parseFootnoteProperties } from "./notePropertiesParser.js";
3
3
  import { BorderStyleSchema, NumberFormatSchema, ThemeColorSlotSchema, narrowEnum } from "./parserEnums.js";
4
+ import { parsePropertyChangeInfo } from "./trackedChangeInfo.js";
4
5
  import { findChild, findChildren, getAttribute, getChildElements, getLocalName, parseBooleanElement, parseNumericAttribute } from "./xmlParser.js";
5
- import { normalizeRevisionId } from "@stll/docx-core/model";
6
6
  //#region src/docx/sectionParser.ts
7
7
  /**
8
8
  * Sanity cap on `w:cols/@w:num`. Word's column picker tops out well below
@@ -61,20 +61,6 @@ function parseColorValue(colorStr, themeColor, themeTint, themeShade) {
61
61
  if (themeShade) color.themeShade = themeShade;
62
62
  return Object.keys(color).length > 0 ? color : void 0;
63
63
  }
64
- function parsePropertyChangeInfo(node) {
65
- const rawId = getAttribute(node, "w", "id");
66
- const parsedId = rawId ? Number.parseInt(rawId, 10) : 0;
67
- const author = (getAttribute(node, "w", "author") ?? "").trim();
68
- const date = (getAttribute(node, "w", "date") ?? "").trim();
69
- const rsid = (getAttribute(node, "w", "rsid") ?? "").trim();
70
- const info = {
71
- id: normalizeRevisionId(parsedId),
72
- author: author.length > 0 ? author : "Unknown"
73
- };
74
- if (date.length > 0) info.date = date;
75
- if (rsid.length > 0) info.rsid = rsid;
76
- return info;
77
- }
78
64
  /**
79
65
  * Parse a border element for page borders
80
66
  */