@ansonlai/docx-redline-js 0.9.0 → 0.11.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 (36) hide show
  1. package/ARCHITECTURE.md +691 -669
  2. package/CHANGELOG.md +67 -1
  3. package/README.md +39 -8
  4. package/core/word-xml.js +160 -118
  5. package/dist/docx-redline-js.esm.js +528 -312
  6. package/dist/docx-redline-js.esm.js.map +4 -4
  7. package/dist/docx-redline-js.esm.min.js +94 -94
  8. package/dist/docx-redline-js.esm.min.js.map +4 -4
  9. package/dist/docx-redline.bundle.cjs +528 -312
  10. package/dist/docx-redline.bundle.cjs.map +4 -4
  11. package/dist/docx-redline.bundle.js +528 -312
  12. package/dist/docx-redline.bundle.js.map +4 -4
  13. package/docs/AGENT_KNOWLEDGE_BASE.md +64 -5
  14. package/docs/SKILL_AUTHORING.md +5 -1
  15. package/docs/TESTING.md +10 -1
  16. package/docs/schemas/document-operations.schema.json +1 -1
  17. package/engine/oxml-engine.js +748 -736
  18. package/engine/table-mode.js +7 -4
  19. package/index.d.ts +29 -2
  20. package/index.js +3 -0
  21. package/node/cli.js +993 -992
  22. package/package.json +1 -1
  23. package/pipeline/content-analysis.js +2 -1
  24. package/pipeline/markdown-processor.js +5 -2
  25. package/pipeline/structured-content.js +2 -1
  26. package/services/comment-parts-merge.js +124 -0
  27. package/services/comment-replies.js +1 -1
  28. package/services/document-operation-applier.js +1 -0
  29. package/services/document-operation-contract.js +1 -0
  30. package/services/document-operation-mutations.js +3362 -3346
  31. package/services/inspection-comparison.js +82 -0
  32. package/services/operation-heuristics.js +1 -1
  33. package/services/operation-preflight.js +42 -33
  34. package/services/revision-fidelity-verifier.js +5 -19
  35. package/services/standalone-docx-plumbing.js +38 -22
  36. package/services/table-reconciliation.js +11 -6
@@ -1,4 +1,4 @@
1
- // @ansonlai/docx-redline-js v0.9.0 — https://github.com/AnsonLai/docx-redline-js
1
+ // @ansonlai/docx-redline-js v0.11.0 — https://github.com/AnsonLai/docx-redline-js
2
2
  var __create = Object.create;
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -1663,7 +1663,10 @@ var MARKDOWN_PATTERNS = [
1663
1663
  { regex: /\*\*\+\+(.+?)\+\+\*\*/, format: { bold: true, underline: true } },
1664
1664
  // Bold: **text** or __text__
1665
1665
  { regex: /\*\*(.+?)\*\*/, format: { bold: true } },
1666
- { regex: /__(.+?)__/, format: { bold: true } },
1666
+ // Underscore delimiters follow CommonMark flanking rules: not intraword, not part of a
1667
+ // longer underscore run, and never around whitespace. So signature lines ("_____")
1668
+ // and snake_case identifiers stay literal text.
1669
+ { regex: /(?<![A-Za-z0-9_])__(?=[^_\s])(.+?)(?<=[^_\s])__(?![A-Za-z0-9_])/, format: { bold: true } },
1667
1670
  // Underline: ++text++
1668
1671
  { regex: /\+\+(.+?)\+\+/, format: { underline: true } },
1669
1672
  // Strikethrough: ~~text~~ or ~text~
@@ -1671,7 +1674,7 @@ var MARKDOWN_PATTERNS = [
1671
1674
  { regex: /~(.+?)~/, format: { strikethrough: true } },
1672
1675
  // Italic: *text* or _text_ (using lookahead only for compatibility)
1673
1676
  { regex: /\*(?!\*)(.+?)\*(?!\*)/, format: { italic: true } },
1674
- { regex: /_(?!_)(.+?)_(?!_)/, format: { italic: true } }
1677
+ { regex: /(?<![A-Za-z0-9_])_(?=[^_\s])(.+?)(?<=[^_\s])_(?![A-Za-z0-9_])/, format: { italic: true } }
1675
1678
  ];
1676
1679
  function preprocessMarkdown(text2) {
1677
1680
  if (!text2) {
@@ -3842,6 +3845,23 @@ function withOoxmlSourceType(result) {
3842
3845
  }
3843
3846
  return { ...result, sourceType: classifyOoxmlSourceType(result.oxml) };
3844
3847
  }
3848
+ function hasUnattributedTrackedChange(node) {
3849
+ if (!node) return false;
3850
+ for (const localName4 of TRACKED_CHANGE_NAMES) {
3851
+ for (const change of wordElementsByLocalName(node, localName4)) {
3852
+ const author = change.getAttribute?.("w:author") || change.getAttribute?.("author") || (typeof change.getAttributeNS === "function" ? change.getAttributeNS(NS_W, "author") : null);
3853
+ if (!author || !String(author).trim()) return true;
3854
+ }
3855
+ }
3856
+ return false;
3857
+ }
3858
+ function resolveExistingRevisionsPolicy(policy, node, author) {
3859
+ if (policy !== "auto") return policy;
3860
+ const nodes = (Array.isArray(node) ? node : [node]).filter(Boolean);
3861
+ const own = String(author || "").trim().toLowerCase();
3862
+ const foreign = nodes.some((item) => containsTrackedChanges(item) && (hasUnattributedTrackedChange(item) || getTrackedChangeAuthors(item).some((name) => name.trim().toLowerCase() !== own)));
3863
+ return foreign ? "slice-cross-author" : "merge-same-author";
3864
+ }
3845
3865
 
3846
3866
  // engine/paragraph-mark-revisions.js
3847
3867
  function localNameOf(node) {
@@ -3948,6 +3968,7 @@ function paragraphMarkPPrXml(kind, ctx = {}) {
3948
3968
  }
3949
3969
 
3950
3970
  // services/table-reconciliation.js
3971
+ var cellMarkdown = (text2, options) => options?.plainTextContent === true ? { cleanText: String(text2 ?? ""), formatHints: [] } : preprocessMarkdown(text2);
3951
3972
  function generateTableOoxml(tableData, options = {}) {
3952
3973
  const {
3953
3974
  generateRedlines = false,
@@ -3981,7 +4002,7 @@ function generateTableOoxml(tableData, options = {}) {
3981
4002
  let cellsXml = "";
3982
4003
  for (let c = 0; c < numCols; c++) {
3983
4004
  const cellText = row[c] || "";
3984
- const { cleanText, formatHints } = preprocessMarkdown(cellText);
4005
+ const { cleanText, formatHints } = cellMarkdown(cellText, options);
3985
4006
  const runModel = [{
3986
4007
  kind: generateRedlines ? RunKind.INSERTION : RunKind.TEXT,
3987
4008
  text: cleanText,
@@ -4096,7 +4117,7 @@ function serializeVirtualGridToOoxml(grid, operations, options) {
4096
4117
  let cellsXml = "";
4097
4118
  const rowInsertMeta = generateRedlines ? createRevisionMetadata(author, revisionIdAllocator) : null;
4098
4119
  for (const cellText of op.cells) {
4099
- const { cleanText, formatHints } = preprocessMarkdown(cellText);
4120
+ const { cleanText, formatHints } = cellMarkdown(cellText, options);
4100
4121
  const runModel = [{
4101
4122
  kind: generateRedlines ? RunKind.INSERTION : RunKind.TEXT,
4102
4123
  text: cleanText,
@@ -4170,7 +4191,7 @@ function reconcileMultiBlockCellContent(cell, newText, options) {
4170
4191
  }
4171
4192
  return;
4172
4193
  }
4173
- const { cleanText, formatHints } = preprocessMarkdown(line);
4194
+ const { cleanText, formatHints } = cellMarkdown(line, options);
4174
4195
  const diffOps = computeWordLevelDiffOps(block.acceptedText, cleanText);
4175
4196
  const model = applyPatches(splitRunsAtDiffBoundaries(block.runModel, diffOps), diffOps, {
4176
4197
  generateRedlines,
@@ -4180,7 +4201,7 @@ function reconcileMultiBlockCellContent(cell, newText, options) {
4180
4201
  out += serializeToOoxml(model, block.pPr, formatHints, { author, generateRedlines, revisionIdAllocator });
4181
4202
  });
4182
4203
  for (let i2 = cell.blocks.length; i2 < lines.length; i2++) {
4183
- const { cleanText, formatHints } = preprocessMarkdown(lines[i2]);
4204
+ const { cleanText, formatHints } = cellMarkdown(lines[i2], options);
4184
4205
  const runModel = [{
4185
4206
  kind: generateRedlines ? RunKind.INSERTION : RunKind.TEXT,
4186
4207
  text: cleanText,
@@ -4199,7 +4220,7 @@ function reconcileCellContent(cell, newText, options) {
4199
4220
  return reconcileMultiBlockCellContent(cell, newText, options);
4200
4221
  }
4201
4222
  const { generateRedlines, author, revisionIdAllocator = null } = options;
4202
- const { cleanText, formatHints } = preprocessMarkdown(newText);
4223
+ const { cleanText, formatHints } = cellMarkdown(newText, options);
4203
4224
  const oldText = cell.getText();
4204
4225
  const diffOps = computeWordLevelDiffOps(oldText, cleanText);
4205
4226
  const baseBlock = cell.blocks[0] || { runModel: [], pPr: null };
@@ -4245,7 +4266,7 @@ function parseTable(text2) {
4245
4266
  }
4246
4267
  const isSeparatorLine = (line) => {
4247
4268
  const normalized = line.replace(/\s+/g, "");
4248
- return /^\|:?-{3,}:?(\|:?-{3,}:?)+\|?$/.test(normalized);
4269
+ return /^\|:?-{3,}:?(?:(\|:?-{3,}:?)+\|?|\|)$/.test(normalized);
4249
4270
  };
4250
4271
  const hasHeader = lines.some(isSeparatorLine);
4251
4272
  const dataLines = lines.filter((line) => !isSeparatorLine(line));
@@ -4829,7 +4850,7 @@ var ReconciliationPipeline = class {
4829
4850
  };
4830
4851
 
4831
4852
  // pipeline/structured-content.js
4832
- var TABLE_SEPARATOR = /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?\s*$/;
4853
+ var TABLE_SEPARATOR = /^\s*(?:\|\s*:?-{3,}:?\s*\||\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?)\s*$/;
4833
4854
  function isBlank(line) {
4834
4855
  return !line || line.trim().length === 0;
4835
4856
  }
@@ -8486,7 +8507,7 @@ function keepUnchangedTextOutsideWrappers(diffs, mapping) {
8486
8507
  function noChanges(serializer, xmlDoc) {
8487
8508
  return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: false });
8488
8509
  }
8489
- function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines = true) {
8510
+ function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines = true, extra = {}) {
8490
8511
  const tableNodes = getElementsByTagNSOrTag(xmlDoc, NS_W, "tbl");
8491
8512
  const newTableData = parseTable(modifiedText);
8492
8513
  const hasNewContent = newTableData.rows.length > 0 || newTableData.headers.length > 0;
@@ -8502,7 +8523,8 @@ function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, auth
8502
8523
  const options = {
8503
8524
  generateRedlines,
8504
8525
  author,
8505
- revisionIdAllocator: getRevisionIdAllocatorForDocument(xmlDoc)
8526
+ revisionIdAllocator: getRevisionIdAllocatorForDocument(xmlDoc),
8527
+ plainTextContent: extra.plainTextContent === true
8506
8528
  };
8507
8529
  const reconciledOxml = serializeVirtualGridToOoxml(oldGrid, operations, options);
8508
8530
  const wrappedOxml = `<root xmlns:w="${NS_W}">${reconciledOxml}</root>`;
@@ -8522,14 +8544,14 @@ function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, auth
8522
8544
  targetTable.parentNode.replaceChild(importedTable, targetTable);
8523
8545
  return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: true });
8524
8546
  }
8525
- function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines) {
8547
+ function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines, extra = {}) {
8526
8548
  const revisionIdAllocator = getRevisionIdAllocatorForDocument(xmlDoc);
8527
8549
  const tableData = parseTable(modifiedText);
8528
8550
  if (!tableData || tableData.rows.length === 0 && tableData.headers.length === 0) {
8529
8551
  log("[OxmlEngine] Failed to parse table data from Markdown");
8530
8552
  return noChanges(serializer, xmlDoc);
8531
8553
  }
8532
- const tableOoxml = generateTableOoxml(tableData, { generateRedlines, author, revisionIdAllocator, trackAsBlock: true });
8554
+ const tableOoxml = generateTableOoxml(tableData, { generateRedlines, author, revisionIdAllocator, trackAsBlock: true, plainTextContent: extra.plainTextContent === true });
8533
8555
  const tableDoc = parseOoxmlSafe(`<root xmlns:w="${NS_W}">${tableOoxml}</root>`, "application/xml").doc;
8534
8556
  if (!tableDoc) return noChanges(serializer, xmlDoc);
8535
8557
  const tableParseError = getXmlParseError(tableDoc);
@@ -9352,7 +9374,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
9352
9374
  const operationWarnings = [];
9353
9375
  let normalizedExistingRevisions = false;
9354
9376
  let preservedInsertedParagraphMark = null;
9355
- const existingRevisionsPolicy = options.existingRevisions || "merge-same-author";
9377
+ let existingRevisionsPolicy = options.existingRevisions || "merge-same-author";
9356
9378
  const keepNormalizedNoOp = existingRevisionsPolicy === "accept-all-first-keep-normalized";
9357
9379
  const finalize = (result) => {
9358
9380
  const withStatus = { ...result };
@@ -9400,6 +9422,10 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
9400
9422
  const parsed = parseOoxmlSafe(inputOoxml, "text/xml");
9401
9423
  parseWarnings = parsed.warnings;
9402
9424
  let xmlDoc = parsed.doc;
9425
+ if (existingRevisionsPolicy === "auto") {
9426
+ existingRevisionsPolicy = resolveExistingRevisionsPolicy("auto", xmlDoc, author);
9427
+ options = { ...options, existingRevisions: existingRevisionsPolicy };
9428
+ }
9403
9429
  const parseError = xmlDoc ? getXmlParseError(xmlDoc) : null;
9404
9430
  if (parsed.error || parseError) {
9405
9431
  const message = parsed.error?.message || parseError?.textContent || "Could not parse OOXML input.";
@@ -9434,7 +9460,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
9434
9460
  if (existingRevisionsPolicy === "merge-same-author" || existingRevisionsPolicy === "slice-cross-author") {
9435
9461
  const authors = getTrackedChangeAuthors(xmlDoc);
9436
9462
  const currentAuthor = String(author || "").trim().toLowerCase();
9437
- const isSameAuthor = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === currentAuthor);
9463
+ const isSameAuthor = authors.length > 0 && !hasUnattributedTrackedChange(xmlDoc) && authors.every((a) => a.trim().toLowerCase() === currentAuthor);
9438
9464
  if (isSameAuthor) {
9439
9465
  const commentIds = getCommentIdsInOoxml(xmlDoc);
9440
9466
  if (commentIds.length > 0) {
@@ -9591,7 +9617,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
9591
9617
  sanitizedText = structuredAnalysis.normalizedMarkdown;
9592
9618
  }
9593
9619
  }
9594
- const { cleanText: cleanModifiedText, formatHints } = preprocessMarkdown(sanitizedText);
9620
+ const { cleanText: cleanModifiedText, formatHints } = options.plainTextContent === true ? { cleanText: sanitizedText, formatHints: [] } : preprocessMarkdown(sanitizedText);
9595
9621
  const { existingFormatHints, textSpans, paragraphs } = extractFormattingFromOoxml(xmlDoc);
9596
9622
  const hasLiteralTextChanges = existingRevisionsPolicy === "slice-cross-author" ? cleanModifiedText !== originalText : cleanModifiedText.trim() !== originalText.trim();
9597
9623
  const hasListStructureChange = !hasLiteralTextChanges && options.explicitStructuredContent === true && isListTargetLoose(cleanModifiedText) && !sourceMatchesListShape(paragraphs, cleanModifiedText);
@@ -9724,11 +9750,11 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
9724
9750
  if (isMarkdownTable && !hasTables) {
9725
9751
  recordRouteSelection(options, "table", { transformation: "text-to-table" });
9726
9752
  log("[OxmlEngine] Text-to-table transformation: generating new table from Markdown");
9727
- return finalize(applyTextToTableTransformation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines));
9753
+ return finalize(applyTextToTableTransformation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines, { plainTextContent: options.plainTextContent === true }));
9728
9754
  }
9729
9755
  if (hasTables && isMarkdownTable) {
9730
9756
  recordRouteSelection(options, "table", { transformation: "table-reconciliation" });
9731
- return finalize(applyTableReconciliation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines));
9757
+ return finalize(applyTableReconciliation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines, { plainTextContent: options.plainTextContent === true }));
9732
9758
  }
9733
9759
  if (usesSurgicalTextMode) {
9734
9760
  recordRouteSelection(options, "surgical", { tableScoped: hasTables });
@@ -13423,6 +13449,70 @@ function inspectDocumentParts(parts, options = {}) {
13423
13449
  };
13424
13450
  }
13425
13451
 
13452
+ // services/inspection-comparison.js
13453
+ var commentIdentity = (comment) => [comment.author ?? null, comment.text ?? "", comment.anchoredText || ""];
13454
+ function reviewSegments(segments = []) {
13455
+ return segments.filter((segment) => segment.kind !== "baseline").reduce((result, segment) => {
13456
+ const next = {
13457
+ kind: segment.kind,
13458
+ author: segment.author || null,
13459
+ text: segment.text,
13460
+ acceptedStart: segment.acceptedStart ?? null,
13461
+ rejectedStart: segment.rejectedStart ?? null
13462
+ };
13463
+ const previous = result.at(-1);
13464
+ const adjacent = (key) => previous[key] == null && next[key] == null || previous[key] != null && previous[key] + previous.text.length === next[key];
13465
+ if (previous && previous.kind === next.kind && previous.author === next.author && adjacent("acceptedStart") && adjacent("rejectedStart")) previous.text += next.text;
13466
+ else result.push(next);
13467
+ return result;
13468
+ }, []);
13469
+ }
13470
+ function inspectionReviewShape(inspection) {
13471
+ const comments = inspection?.comments || [];
13472
+ return {
13473
+ paragraphs: (inspection?.paragraphs || []).map((paragraph) => ({
13474
+ text: paragraph.exactText,
13475
+ inTable: paragraph.inTable === true,
13476
+ table: paragraph.table ? [paragraph.table.rowIndex, paragraph.table.cellIndex] : null,
13477
+ list: paragraph.list ? [paragraph.list.format, paragraph.list.level, paragraph.list.label] : null,
13478
+ review: {
13479
+ present: paragraph.hasRevisions === true,
13480
+ authors: [...paragraph.revisionAuthors || []].sort(),
13481
+ segments: reviewSegments(paragraph.segments)
13482
+ }
13483
+ })),
13484
+ comments: comments.map((comment) => {
13485
+ const parent = comment.parentCommentId != null ? comments.find((item) => String(item.id) === String(comment.parentCommentId)) : null;
13486
+ return [
13487
+ ...commentIdentity(comment),
13488
+ comment.done === true,
13489
+ parent ? commentIdentity(parent) : comment.parentCommentId != null || comment.parentParaId ? "unresolved parent" : null
13490
+ ];
13491
+ }).map((item) => JSON.stringify(item)).sort()
13492
+ };
13493
+ }
13494
+ function compareDocumentInspections(expected, actual) {
13495
+ const left = inspectionReviewShape(expected);
13496
+ const right = inspectionReviewShape(actual);
13497
+ const differences = [];
13498
+ const note = (path, a, b) => {
13499
+ if (differences.length < 10 && JSON.stringify(a) !== JSON.stringify(b)) differences.push({ path, expected: a, actual: b });
13500
+ };
13501
+ note("paragraphs.length", left.paragraphs.length, right.paragraphs.length);
13502
+ const count = Math.max(left.paragraphs.length, right.paragraphs.length);
13503
+ for (let index = 0; index < count; index++) {
13504
+ const a = left.paragraphs[index] ?? null;
13505
+ const b = right.paragraphs[index] ?? null;
13506
+ if (!a || !b) {
13507
+ note(`paragraphs[${index}]`, a, b);
13508
+ continue;
13509
+ }
13510
+ for (const key of ["text", "inTable", "table", "list", "review"]) note(`paragraphs[${index}].${key}`, a[key], b[key]);
13511
+ }
13512
+ note("comments", left.comments, right.comments);
13513
+ return { matches: differences.length === 0, differences };
13514
+ }
13515
+
13426
13516
  // services/comment-locator.js
13427
13517
  function createParagraphTextIndex(paragraph, options = {}) {
13428
13518
  const revisionView = options.revisionView === "current" ? "accepted" : options.revisionView || "accepted";
@@ -13897,7 +13987,7 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, doc
13897
13987
  extendedDoc = parsed.doc;
13898
13988
  }
13899
13989
  const occupied = usedParaIds(commentsDoc, extendedDoc);
13900
- if (documentXml) for (const match of String(documentXml).matchAll(/w14:paraId="([0-9A-Fa-f]{8})"/g)) occupied.add(match[1].toUpperCase());
13990
+ if (documentXml) for (const match of String(documentXml).matchAll(/\bw14:paraId="([0-9A-Fa-f]{8})"/g)) occupied.add(match[1].toUpperCase());
13901
13991
  const parentParagraph = commentThreadParagraph(parent);
13902
13992
  if (!parentParagraph) return { status: "error", error: { code: "PARENT_COMMENT_INVALID", message: `Parent comment '${parentCommentId}' has no paragraph.` } };
13903
13993
  let parentParaId = commentParaId(parent);
@@ -13960,232 +14050,6 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, doc
13960
14050
  };
13961
14051
  }
13962
14052
 
13963
- // engine/formatting-removal.js
13964
- function removeNode2(node) {
13965
- if (node?.parentNode) {
13966
- node.parentNode.removeChild(node);
13967
- }
13968
- }
13969
- function removeFormattingFromRPr(rPr, formatTypes = ["all"]) {
13970
- if (!rPr) return null;
13971
- const rPrClone = rPr.cloneNode(true);
13972
- if (formatTypes.includes("all")) {
13973
- const toRemove = [
13974
- "w:b",
13975
- "w:i",
13976
- "w:u",
13977
- "w:strike",
13978
- "w:dstrike",
13979
- "w:color",
13980
- "w:sz",
13981
- "w:szCs",
13982
- "w:rFonts",
13983
- "w:highlight",
13984
- "w:vertAlign",
13985
- "w:spacing",
13986
- "w:w",
13987
- "w:kern",
13988
- "w:position"
13989
- ];
13990
- toRemove.forEach((tag) => {
13991
- const elements = rPrClone.querySelectorAll(`${tag}, ${tag.replace("w:", "")}`);
13992
- elements.forEach(removeNode2);
13993
- });
13994
- } else {
13995
- const tagMap = {
13996
- "bold": "w:b",
13997
- "italic": "w:i",
13998
- "underline": "w:u",
13999
- "strikethrough": "w:strike",
14000
- "doubleStrike": "w:dstrike",
14001
- "color": "w:color",
14002
- "highlight": "w:highlight",
14003
- "fontSize": "w:sz",
14004
- "fontSizeCs": "w:szCs",
14005
- // Complex script font size
14006
- "fontFamily": "w:rFonts",
14007
- "superscript": "w:vertAlign",
14008
- "subscript": "w:vertAlign"
14009
- };
14010
- formatTypes.forEach((type) => {
14011
- const tag = tagMap[type];
14012
- if (tag) {
14013
- const elements = rPrClone.querySelectorAll(`${tag}, ${tag.replace("w:", "")}`);
14014
- elements.forEach(removeNode2);
14015
- }
14016
- });
14017
- }
14018
- return rPrClone.children.length > 0 ? rPrClone : null;
14019
- }
14020
- function applyFormattingRemovalToOoxml(ooxmlString, targetText, formatTypes) {
14021
- if (!targetText || !ooxmlString) return ooxmlString;
14022
- const doc = parseOoxml(ooxmlString);
14023
- if (!doc) return ooxmlString;
14024
- const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
14025
- const runs = doc.getElementsByTagNameNS(NS_W11, "r");
14026
- const insertions = doc.getElementsByTagNameNS(NS_W11, "ins");
14027
- const allRuns = [...Array.from(runs)];
14028
- for (const ins of insertions) {
14029
- const insideRuns = ins.getElementsByTagNameNS(NS_W11, "r");
14030
- allRuns.push(...Array.from(insideRuns));
14031
- }
14032
- for (const run of allRuns) {
14033
- const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
14034
- const runText = Array.from(textNodes).map((t) => t.textContent).join("");
14035
- if (runText.includes(targetText) || runText === targetText) {
14036
- const rPrElements = run.getElementsByTagNameNS(NS_W11, "rPr");
14037
- if (rPrElements.length > 0) {
14038
- const rPr = rPrElements[0];
14039
- const newRPr = removeFormattingFromRPr(rPr, formatTypes);
14040
- if (newRPr) {
14041
- if (rPr.parentNode) {
14042
- rPr.parentNode.replaceChild(newRPr, rPr);
14043
- }
14044
- } else {
14045
- removeNode2(rPr);
14046
- }
14047
- }
14048
- }
14049
- }
14050
- return serializeOoxml(doc);
14051
- }
14052
- var HIGHLIGHT_COLOR_MAP = {
14053
- "yellow": "yellow",
14054
- "green": "green",
14055
- "cyan": "cyan",
14056
- "magenta": "magenta",
14057
- "blue": "blue",
14058
- "red": "red",
14059
- "darkblue": "darkBlue",
14060
- "darkcyan": "darkCyan",
14061
- "darkgreen": "darkGreen",
14062
- "darkmagenta": "darkMagenta",
14063
- "darkred": "darkRed",
14064
- "darkyellow": "darkYellow",
14065
- "gray25": "lightGray",
14066
- "gray50": "darkGray",
14067
- "black": "black",
14068
- "white": "white"
14069
- };
14070
- function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
14071
- const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
14072
- const ooxmlColor = HIGHLIGHT_COLOR_MAP[color.toLowerCase()] || "yellow";
14073
- const generateRedlines = options?.generateRedlines ?? false;
14074
- const author = options?.author || getDefaultAuthor();
14075
- let rPrElement = rPr;
14076
- if (!rPrElement) {
14077
- rPrElement = createWordElement(doc, "w:rPr");
14078
- } else {
14079
- rPrElement = rPr.cloneNode(true);
14080
- }
14081
- let previousRPrState = null;
14082
- if (generateRedlines) {
14083
- previousRPrState = createWordElement(doc, "w:rPr");
14084
- Array.from(rPrElement.childNodes).forEach((child) => {
14085
- if (child.nodeName !== "w:rPrChange") {
14086
- previousRPrState.appendChild(child.cloneNode(true));
14087
- }
14088
- });
14089
- }
14090
- const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W11, "highlight");
14091
- Array.from(existingHighlight).forEach(removeNode2);
14092
- const highlightEl = createWordElement(doc, "w:highlight");
14093
- highlightEl.setAttributeNS(NS_W11, "w:val", ooxmlColor);
14094
- rPrElement.appendChild(highlightEl);
14095
- if (generateRedlines && previousRPrState) {
14096
- const rPrChange = createWordElement(doc, "w:rPrChange");
14097
- const metadata = createRevisionMetadata(author, doc);
14098
- rPrChange.setAttribute("w:id", String(metadata.id));
14099
- rPrChange.setAttribute("w:author", metadata.author);
14100
- rPrChange.setAttribute("w:date", metadata.date);
14101
- rPrChange.appendChild(previousRPrState);
14102
- const existingChange = rPrElement.getElementsByTagNameNS(NS_W11, "rPrChange");
14103
- Array.from(existingChange).forEach(removeNode2);
14104
- rPrElement.appendChild(rPrChange);
14105
- }
14106
- return rPrElement;
14107
- }
14108
- function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", options = {}) {
14109
- if (!targetText || !ooxmlString) return ooxmlString;
14110
- const doc = parseOoxml(ooxmlString);
14111
- if (!doc) return ooxmlString;
14112
- let revisionIdAllocator;
14113
- if (options?._revisionIdAllocator instanceof RevisionIdAllocator) {
14114
- revisionIdAllocator = options._revisionIdAllocator;
14115
- seedRevisionIdsFromDocument(doc, revisionIdAllocator);
14116
- } else {
14117
- revisionIdAllocator = createRevisionIdAllocator(doc);
14118
- }
14119
- const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
14120
- const getRunText = (run) => {
14121
- const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
14122
- return Array.from(textNodes).map((t) => t.textContent).join("");
14123
- };
14124
- const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W11, "r"));
14125
- const sourceRunsWithClaimedRevisionIds = /* @__PURE__ */ new WeakSet();
14126
- const cloneRunWithText2 = (sourceRun, text2, shouldHighlight) => {
14127
- const clonedRun = sourceRun.cloneNode(true);
14128
- if (sourceRunsWithClaimedRevisionIds.has(sourceRun)) {
14129
- refreshRunPropertyChangeIds(clonedRun, revisionIdAllocator);
14130
- } else {
14131
- sourceRunsWithClaimedRevisionIds.add(sourceRun);
14132
- }
14133
- const textNodes = clonedRun.getElementsByTagNameNS(NS_W11, "t");
14134
- Array.from(textNodes).forEach(removeNode2);
14135
- const newText = createWordElement(doc, "w:t");
14136
- newText.setAttribute("xml:space", "preserve");
14137
- newText.textContent = text2;
14138
- clonedRun.appendChild(newText);
14139
- if (shouldHighlight) {
14140
- const rPrElements = clonedRun.getElementsByTagNameNS(NS_W11, "rPr");
14141
- const existingRPr = rPrElements.length > 0 ? rPrElements[0] : null;
14142
- const newRPr = injectHighlightIntoRPr(doc, existingRPr, color, options);
14143
- if (existingRPr) {
14144
- clonedRun.replaceChild(newRPr, existingRPr);
14145
- } else {
14146
- clonedRun.insertBefore(newRPr, clonedRun.firstChild);
14147
- }
14148
- }
14149
- return clonedRun;
14150
- };
14151
- for (const run of allRuns) {
14152
- const runText = getRunText(run);
14153
- if (!runText) continue;
14154
- const matchIndexes = [];
14155
- let searchOffset = 0;
14156
- while (searchOffset <= runText.length - targetText.length) {
14157
- const matchIndex = runText.indexOf(targetText, searchOffset);
14158
- if (matchIndex === -1) break;
14159
- matchIndexes.push(matchIndex);
14160
- searchOffset = matchIndex + targetText.length;
14161
- }
14162
- if (matchIndexes.length === 0) continue;
14163
- const parent = run.parentNode;
14164
- if (!parent) {
14165
- console.warn("[Highlight] Run parent is null; skipping. Likely already processed.");
14166
- continue;
14167
- }
14168
- const fragment = doc.createDocumentFragment();
14169
- let cursor = 0;
14170
- for (const matchIndex of matchIndexes) {
14171
- if (matchIndex > cursor) {
14172
- fragment.appendChild(cloneRunWithText2(run, runText.slice(cursor, matchIndex), false));
14173
- }
14174
- fragment.appendChild(cloneRunWithText2(
14175
- run,
14176
- runText.slice(matchIndex, matchIndex + targetText.length),
14177
- true
14178
- ));
14179
- cursor = matchIndex + targetText.length;
14180
- }
14181
- if (cursor < runText.length) {
14182
- fragment.appendChild(cloneRunWithText2(run, runText.slice(cursor), false));
14183
- }
14184
- parent.replaceChild(fragment, run);
14185
- }
14186
- return serializeOoxml(doc);
14187
- }
14188
-
14189
14053
  // services/package-parts.js
14190
14054
  var WORD_ML = "application/vnd.openxmlformats-officedocument.wordprocessingml";
14191
14055
  var REL_OFFICE_DOC = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
@@ -14473,31 +14337,34 @@ async function ensureNumberingArtifactsInZip(zip, numberingXmlList, options = {}
14473
14337
  }
14474
14338
  }
14475
14339
  }
14340
+ function mergeCommentsPartXml(existingXml, commentsXml, options = {}) {
14341
+ if (!existingXml || options.replaceExisting === true) return commentsXml;
14342
+ const existingDoc = parseXmlStrictStandalone(existingXml, "word/comments.xml (existing)");
14343
+ const incomingDoc = parseXmlStrictStandalone(commentsXml, "word/comments.xml (incoming)");
14344
+ const incomingComments = Array.from(incomingDoc.documentElement.getElementsByTagNameNS(NS_W7, "comment"));
14345
+ if (incomingComments.length === 0) return existingXml;
14346
+ const existingRoot = existingDoc.documentElement;
14347
+ const usedIds = new Set(
14348
+ Array.from(existingRoot.getElementsByTagNameNS(NS_W7, "comment")).map((comment) => comment.getAttribute("w:id") || comment.getAttribute("id")).filter(Boolean)
14349
+ );
14350
+ for (const incomingComment of incomingComments) {
14351
+ const id = incomingComment.getAttribute("w:id") || incomingComment.getAttribute("id");
14352
+ if (id && usedIds.has(id)) {
14353
+ throw Object.assign(new Error(`Duplicate comment id: ${id}`), { code: "DUPLICATE_COMMENT_ID", commentId: id });
14354
+ }
14355
+ if (id) usedIds.add(id);
14356
+ existingRoot.appendChild(existingDoc.importNode(incomingComment, true));
14357
+ }
14358
+ return createSerializer().serializeToString(existingDoc);
14359
+ }
14476
14360
  async function ensureCommentsArtifactsInZip(zip, commentsXml, options = {}) {
14477
14361
  const onInfo = typeof options?.onInfo === "function" ? options.onInfo : () => {
14478
14362
  };
14479
14363
  if (!commentsXml) return;
14480
14364
  const serializer = createSerializer();
14481
14365
  const existingText = await readZipText(zip, COMMENTS_PATH);
14482
- if (!existingText || options.replaceExisting === true) {
14483
- onInfo("[Demo] Adding comments.xml");
14484
- zip.file(COMMENTS_PATH, commentsXml);
14485
- } else {
14486
- const existingDoc = parseXmlStrictStandalone(existingText, "word/comments.xml (existing)");
14487
- const incomingDoc = parseXmlStrictStandalone(commentsXml, "word/comments.xml (incoming)");
14488
- const existingRoot = existingDoc.documentElement;
14489
- const existingIds = new Set(
14490
- Array.from(existingRoot.getElementsByTagNameNS(NS_W7, "comment")).map((comment) => comment.getAttribute("w:id") || comment.getAttribute("id")).filter(Boolean)
14491
- );
14492
- for (const incomingComment of Array.from(incomingDoc.documentElement.getElementsByTagNameNS(NS_W7, "comment"))) {
14493
- const id = incomingComment.getAttribute("w:id") || incomingComment.getAttribute("id");
14494
- if (id && existingIds.has(id)) {
14495
- throw new Error(`Duplicate comment id: ${id}`);
14496
- }
14497
- existingRoot.appendChild(existingDoc.importNode(incomingComment, true));
14498
- }
14499
- zip.file(COMMENTS_PATH, serializer.serializeToString(existingDoc));
14500
- }
14366
+ if (!existingText || options.replaceExisting === true) onInfo("[Demo] Adding comments.xml");
14367
+ zip.file(COMMENTS_PATH, mergeCommentsPartXml(existingText, commentsXml, options));
14501
14368
  const ctText = await readZipText(zip, CONTENT_TYPES_PATH);
14502
14369
  if (ctText) {
14503
14370
  const ctDoc = parseXmlStrictStandalone(ctText, CONTENT_TYPES_PATH);
@@ -14733,6 +14600,318 @@ async function validateDocxPackage(zip) {
14733
14600
  }
14734
14601
  }
14735
14602
 
14603
+ // services/comment-parts-merge.js
14604
+ var PART_KEYS = Object.freeze({
14605
+ comments: "commentsXml",
14606
+ commentsExtended: "commentsExtendedXml",
14607
+ commentsIds: "commentsIdsXml",
14608
+ commentsExtensible: "commentsExtensibleXml"
14609
+ });
14610
+ var partsOf = (parts) => Object.fromEntries(Object.values(PART_KEYS).map((key) => [key, parts?.[key] || null]));
14611
+ function mergeCommentParts(parts = {}, output = {}) {
14612
+ const before = partsOf(parts);
14613
+ const after = { ...before };
14614
+ const fail = (error2) => ({ status: "error", hasChanges: false, parts: before, changedParts: [], addedParts: [], error: error2 });
14615
+ const commentsMode = output?.commentsXmlMode === "replace" ? "replace" : "merge";
14616
+ const extendedMode = output?.commentsExtendedXmlMode === "replace" ? "replace" : "merge";
14617
+ for (const [label, value] of [
14618
+ ...Object.values(PART_KEYS).map((key) => [`parts.${key}`, before[key]]),
14619
+ ["output.commentsXml", output?.commentsXml],
14620
+ ["output.commentsExtendedXml", output?.commentsExtendedXml]
14621
+ ]) {
14622
+ if (value != null && typeof value !== "string") {
14623
+ return fail({ code: "INVALID_COMMENT_PARTS", message: `${label} must be an XML string, null or omitted (got ${typeof value}).` });
14624
+ }
14625
+ }
14626
+ const requirePart = (xml, part, rootName, label) => {
14627
+ let doc;
14628
+ try {
14629
+ doc = parseXmlStrictStandalone(xml, label);
14630
+ } catch (error2) {
14631
+ throw Object.assign(error2, { part });
14632
+ }
14633
+ if (doc.documentElement?.localName !== rootName) {
14634
+ throw Object.assign(new Error(`${label}: expected a <${rootName}> root element, found <${doc.documentElement?.localName || "none"}>.`), { code: "INVALID_COMMENT_PARTS", part });
14635
+ }
14636
+ return doc;
14637
+ };
14638
+ try {
14639
+ if (output?.commentsXml) {
14640
+ const incomingDoc = requirePart(output.commentsXml, "comments", "comments", "word/comments.xml (incoming)");
14641
+ const seen = /* @__PURE__ */ new Set();
14642
+ for (const node of Array.from(incomingDoc.getElementsByTagNameNS("*", "comment"))) {
14643
+ const id = node.getAttribute("w:id") || node.getAttribute("id");
14644
+ if (!id) continue;
14645
+ if (seen.has(id)) {
14646
+ throw Object.assign(new Error(`Duplicate comment id: ${id}`), { code: "DUPLICATE_COMMENT_ID", commentId: id, part: "comments" });
14647
+ }
14648
+ seen.add(id);
14649
+ }
14650
+ if (before.commentsXml) requirePart(before.commentsXml, "comments", "comments", "word/comments.xml (existing)");
14651
+ if (before.commentsIdsXml) requirePart(before.commentsIdsXml, "commentsIds", "commentsIds", "word/commentsIds.xml (existing)");
14652
+ if (before.commentsExtensibleXml) requirePart(before.commentsExtensibleXml, "commentsExtensible", "commentsExtensible", "word/commentsExtensible.xml (existing)");
14653
+ after.commentsXml = mergeCommentsPartXml(before.commentsXml, output.commentsXml, { replaceExisting: commentsMode === "replace" });
14654
+ Object.assign(after, reconcileCommentSiblingParts({
14655
+ commentsXml: after.commentsXml,
14656
+ commentsIdsXml: after.commentsIdsXml,
14657
+ commentsExtensibleXml: after.commentsExtensibleXml
14658
+ }));
14659
+ }
14660
+ if (output?.commentsExtendedXml) {
14661
+ if (extendedMode !== "replace" && before.commentsExtendedXml) {
14662
+ return fail({
14663
+ code: "UNSUPPORTED_EXISTING_COMMENT_THREAD_METADATA",
14664
+ part: "commentsExtended",
14665
+ message: 'The output carries a commentsExtended delta (mode "merge") for parts that already have thread metadata; a delta cannot be merged into an existing commentsExtended.xml. Nothing was merged.'
14666
+ });
14667
+ }
14668
+ requirePart(output.commentsExtendedXml, "commentsExtended", "commentsEx", "word/commentsExtended.xml (incoming)");
14669
+ after.commentsExtendedXml = output.commentsExtendedXml;
14670
+ }
14671
+ } catch (error2) {
14672
+ return fail({
14673
+ code: error2?.code || "COMMENT_PARTS_MERGE_FAILED",
14674
+ message: error2?.message || String(error2),
14675
+ ...error2?.part || error2?.code === "DUPLICATE_COMMENT_ID" ? { part: error2.part || "comments" } : {},
14676
+ ...error2?.commentId ? { commentId: error2.commentId } : {}
14677
+ });
14678
+ }
14679
+ const changedParts = [];
14680
+ const addedParts = [];
14681
+ for (const [name, key] of Object.entries(PART_KEYS)) {
14682
+ if (after[key] === before[key]) continue;
14683
+ changedParts.push(name);
14684
+ if (!before[key]) addedParts.push(name);
14685
+ }
14686
+ return { status: "ok", hasChanges: changedParts.length > 0, parts: after, changedParts, addedParts };
14687
+ }
14688
+
14689
+ // engine/formatting-removal.js
14690
+ function removeNode2(node) {
14691
+ if (node?.parentNode) {
14692
+ node.parentNode.removeChild(node);
14693
+ }
14694
+ }
14695
+ function removeFormattingFromRPr(rPr, formatTypes = ["all"]) {
14696
+ if (!rPr) return null;
14697
+ const rPrClone = rPr.cloneNode(true);
14698
+ if (formatTypes.includes("all")) {
14699
+ const toRemove = [
14700
+ "w:b",
14701
+ "w:i",
14702
+ "w:u",
14703
+ "w:strike",
14704
+ "w:dstrike",
14705
+ "w:color",
14706
+ "w:sz",
14707
+ "w:szCs",
14708
+ "w:rFonts",
14709
+ "w:highlight",
14710
+ "w:vertAlign",
14711
+ "w:spacing",
14712
+ "w:w",
14713
+ "w:kern",
14714
+ "w:position"
14715
+ ];
14716
+ toRemove.forEach((tag) => {
14717
+ const elements = rPrClone.querySelectorAll(`${tag}, ${tag.replace("w:", "")}`);
14718
+ elements.forEach(removeNode2);
14719
+ });
14720
+ } else {
14721
+ const tagMap = {
14722
+ "bold": "w:b",
14723
+ "italic": "w:i",
14724
+ "underline": "w:u",
14725
+ "strikethrough": "w:strike",
14726
+ "doubleStrike": "w:dstrike",
14727
+ "color": "w:color",
14728
+ "highlight": "w:highlight",
14729
+ "fontSize": "w:sz",
14730
+ "fontSizeCs": "w:szCs",
14731
+ // Complex script font size
14732
+ "fontFamily": "w:rFonts",
14733
+ "superscript": "w:vertAlign",
14734
+ "subscript": "w:vertAlign"
14735
+ };
14736
+ formatTypes.forEach((type) => {
14737
+ const tag = tagMap[type];
14738
+ if (tag) {
14739
+ const elements = rPrClone.querySelectorAll(`${tag}, ${tag.replace("w:", "")}`);
14740
+ elements.forEach(removeNode2);
14741
+ }
14742
+ });
14743
+ }
14744
+ return rPrClone.children.length > 0 ? rPrClone : null;
14745
+ }
14746
+ function applyFormattingRemovalToOoxml(ooxmlString, targetText, formatTypes) {
14747
+ if (!targetText || !ooxmlString) return ooxmlString;
14748
+ const doc = parseOoxml(ooxmlString);
14749
+ if (!doc) return ooxmlString;
14750
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
14751
+ const runs = doc.getElementsByTagNameNS(NS_W11, "r");
14752
+ const insertions = doc.getElementsByTagNameNS(NS_W11, "ins");
14753
+ const allRuns = [...Array.from(runs)];
14754
+ for (const ins of insertions) {
14755
+ const insideRuns = ins.getElementsByTagNameNS(NS_W11, "r");
14756
+ allRuns.push(...Array.from(insideRuns));
14757
+ }
14758
+ for (const run of allRuns) {
14759
+ const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
14760
+ const runText = Array.from(textNodes).map((t) => t.textContent).join("");
14761
+ if (runText.includes(targetText) || runText === targetText) {
14762
+ const rPrElements = run.getElementsByTagNameNS(NS_W11, "rPr");
14763
+ if (rPrElements.length > 0) {
14764
+ const rPr = rPrElements[0];
14765
+ const newRPr = removeFormattingFromRPr(rPr, formatTypes);
14766
+ if (newRPr) {
14767
+ if (rPr.parentNode) {
14768
+ rPr.parentNode.replaceChild(newRPr, rPr);
14769
+ }
14770
+ } else {
14771
+ removeNode2(rPr);
14772
+ }
14773
+ }
14774
+ }
14775
+ }
14776
+ return serializeOoxml(doc);
14777
+ }
14778
+ var HIGHLIGHT_COLOR_MAP = {
14779
+ "yellow": "yellow",
14780
+ "green": "green",
14781
+ "cyan": "cyan",
14782
+ "magenta": "magenta",
14783
+ "blue": "blue",
14784
+ "red": "red",
14785
+ "darkblue": "darkBlue",
14786
+ "darkcyan": "darkCyan",
14787
+ "darkgreen": "darkGreen",
14788
+ "darkmagenta": "darkMagenta",
14789
+ "darkred": "darkRed",
14790
+ "darkyellow": "darkYellow",
14791
+ "gray25": "lightGray",
14792
+ "gray50": "darkGray",
14793
+ "black": "black",
14794
+ "white": "white"
14795
+ };
14796
+ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
14797
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
14798
+ const ooxmlColor = HIGHLIGHT_COLOR_MAP[color.toLowerCase()] || "yellow";
14799
+ const generateRedlines = options?.generateRedlines ?? false;
14800
+ const author = options?.author || getDefaultAuthor();
14801
+ let rPrElement = rPr;
14802
+ if (!rPrElement) {
14803
+ rPrElement = createWordElement(doc, "w:rPr");
14804
+ } else {
14805
+ rPrElement = rPr.cloneNode(true);
14806
+ }
14807
+ let previousRPrState = null;
14808
+ if (generateRedlines) {
14809
+ previousRPrState = createWordElement(doc, "w:rPr");
14810
+ Array.from(rPrElement.childNodes).forEach((child) => {
14811
+ if (child.nodeName !== "w:rPrChange") {
14812
+ previousRPrState.appendChild(child.cloneNode(true));
14813
+ }
14814
+ });
14815
+ }
14816
+ const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W11, "highlight");
14817
+ Array.from(existingHighlight).forEach(removeNode2);
14818
+ const highlightEl = createWordElement(doc, "w:highlight");
14819
+ highlightEl.setAttributeNS(NS_W11, "w:val", ooxmlColor);
14820
+ rPrElement.appendChild(highlightEl);
14821
+ if (generateRedlines && previousRPrState) {
14822
+ const rPrChange = createWordElement(doc, "w:rPrChange");
14823
+ const metadata = createRevisionMetadata(author, doc);
14824
+ rPrChange.setAttribute("w:id", String(metadata.id));
14825
+ rPrChange.setAttribute("w:author", metadata.author);
14826
+ rPrChange.setAttribute("w:date", metadata.date);
14827
+ rPrChange.appendChild(previousRPrState);
14828
+ const existingChange = rPrElement.getElementsByTagNameNS(NS_W11, "rPrChange");
14829
+ Array.from(existingChange).forEach(removeNode2);
14830
+ rPrElement.appendChild(rPrChange);
14831
+ }
14832
+ return rPrElement;
14833
+ }
14834
+ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", options = {}) {
14835
+ if (!targetText || !ooxmlString) return ooxmlString;
14836
+ const doc = parseOoxml(ooxmlString);
14837
+ if (!doc) return ooxmlString;
14838
+ let revisionIdAllocator;
14839
+ if (options?._revisionIdAllocator instanceof RevisionIdAllocator) {
14840
+ revisionIdAllocator = options._revisionIdAllocator;
14841
+ seedRevisionIdsFromDocument(doc, revisionIdAllocator);
14842
+ } else {
14843
+ revisionIdAllocator = createRevisionIdAllocator(doc);
14844
+ }
14845
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
14846
+ const getRunText = (run) => {
14847
+ const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
14848
+ return Array.from(textNodes).map((t) => t.textContent).join("");
14849
+ };
14850
+ const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W11, "r"));
14851
+ const sourceRunsWithClaimedRevisionIds = /* @__PURE__ */ new WeakSet();
14852
+ const cloneRunWithText2 = (sourceRun, text2, shouldHighlight) => {
14853
+ const clonedRun = sourceRun.cloneNode(true);
14854
+ if (sourceRunsWithClaimedRevisionIds.has(sourceRun)) {
14855
+ refreshRunPropertyChangeIds(clonedRun, revisionIdAllocator);
14856
+ } else {
14857
+ sourceRunsWithClaimedRevisionIds.add(sourceRun);
14858
+ }
14859
+ const textNodes = clonedRun.getElementsByTagNameNS(NS_W11, "t");
14860
+ Array.from(textNodes).forEach(removeNode2);
14861
+ const newText = createWordElement(doc, "w:t");
14862
+ newText.setAttribute("xml:space", "preserve");
14863
+ newText.textContent = text2;
14864
+ clonedRun.appendChild(newText);
14865
+ if (shouldHighlight) {
14866
+ const rPrElements = clonedRun.getElementsByTagNameNS(NS_W11, "rPr");
14867
+ const existingRPr = rPrElements.length > 0 ? rPrElements[0] : null;
14868
+ const newRPr = injectHighlightIntoRPr(doc, existingRPr, color, options);
14869
+ if (existingRPr) {
14870
+ clonedRun.replaceChild(newRPr, existingRPr);
14871
+ } else {
14872
+ clonedRun.insertBefore(newRPr, clonedRun.firstChild);
14873
+ }
14874
+ }
14875
+ return clonedRun;
14876
+ };
14877
+ for (const run of allRuns) {
14878
+ const runText = getRunText(run);
14879
+ if (!runText) continue;
14880
+ const matchIndexes = [];
14881
+ let searchOffset = 0;
14882
+ while (searchOffset <= runText.length - targetText.length) {
14883
+ const matchIndex = runText.indexOf(targetText, searchOffset);
14884
+ if (matchIndex === -1) break;
14885
+ matchIndexes.push(matchIndex);
14886
+ searchOffset = matchIndex + targetText.length;
14887
+ }
14888
+ if (matchIndexes.length === 0) continue;
14889
+ const parent = run.parentNode;
14890
+ if (!parent) {
14891
+ console.warn("[Highlight] Run parent is null; skipping. Likely already processed.");
14892
+ continue;
14893
+ }
14894
+ const fragment = doc.createDocumentFragment();
14895
+ let cursor = 0;
14896
+ for (const matchIndex of matchIndexes) {
14897
+ if (matchIndex > cursor) {
14898
+ fragment.appendChild(cloneRunWithText2(run, runText.slice(cursor, matchIndex), false));
14899
+ }
14900
+ fragment.appendChild(cloneRunWithText2(
14901
+ run,
14902
+ runText.slice(matchIndex, matchIndex + targetText.length),
14903
+ true
14904
+ ));
14905
+ cursor = matchIndex + targetText.length;
14906
+ }
14907
+ if (cursor < runText.length) {
14908
+ fragment.appendChild(cloneRunWithText2(run, runText.slice(cursor), false));
14909
+ }
14910
+ parent.replaceChild(fragment, run);
14911
+ }
14912
+ return serializeOoxml(doc);
14913
+ }
14914
+
14736
14915
  // orchestration/route-plan.js
14737
14916
  var RoutePlanKind = Object.freeze({
14738
14917
  STRUCTURED_LIST_DIRECT: "structured_list_direct",
@@ -16199,6 +16378,7 @@ var SUPPORTED_OPERATION_TYPES = /* @__PURE__ */ new Set([
16199
16378
  "highlight"
16200
16379
  ]);
16201
16380
  var EXISTING_REVISIONS_POLICIES = Object.freeze([
16381
+ "auto",
16202
16382
  "merge-same-author",
16203
16383
  "slice-cross-author",
16204
16384
  "reject-input",
@@ -18436,7 +18616,7 @@ function deriveParagraphThenTableAppend(modifiedText) {
18436
18616
  const tableLines = lines.slice(1);
18437
18617
  if (!tableLines.every((line) => line.trim().startsWith("|"))) return null;
18438
18618
  const separator = tableLines[1].replace(/\s+/g, "");
18439
- if (!/^\|:?-{3,}:?(\|:?-{3,}:?)+\|?$/.test(separator)) return null;
18619
+ if (!/^\|:?-{3,}:?(?:(\|:?-{3,}:?)+\|?|\|)$/.test(separator)) return null;
18440
18620
  return { paragraphMarkdown: lines[0].trim(), tableText: tableLines.join("\n") };
18441
18621
  }
18442
18622
 
@@ -19066,7 +19246,10 @@ async function buildInsertedPlainParagraph(xmlDoc, anchorParagraph, text2, revis
19066
19246
  String(text2 || ""),
19067
19247
  {
19068
19248
  author,
19069
- generateRedlines: false
19249
+ generateRedlines: false,
19250
+ // A caller's structuredContent: false keeps inserted paragraphs plain too: no headings,
19251
+ // and inline Markdown markers stay literal.
19252
+ ...options.plainTextContent === true ? { structuredContent: false, plainTextContent: true } : {}
19070
19253
  }
19071
19254
  );
19072
19255
  let sourceParagraph = null;
@@ -20253,13 +20436,17 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
20253
20436
  }
20254
20437
  };
20255
20438
  }
20439
+ const autoExistingRevisions = options.existingRevisions === "auto";
20440
+ if (autoExistingRevisions) {
20441
+ options = { ...options, existingRevisions: resolveExistingRevisionsPolicy("auto", targetParagraph, author) };
20442
+ }
20256
20443
  const hasRevisions = containsTrackedChanges(targetParagraph);
20257
20444
  const existingPolicy = options.existingRevisions || "merge-same-author";
20258
20445
  if (hasRevisions && existingPolicy !== "accept-all-first" && existingPolicy !== "accept-all-first-keep-normalized") {
20259
20446
  if (existingPolicy === "merge-same-author" || existingPolicy === "slice-cross-author") {
20260
20447
  const authors = getTrackedChangeAuthors(targetParagraph);
20261
20448
  const opAuthor = String(author || "").trim().toLowerCase();
20262
- const allSame = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === opAuthor);
20449
+ const allSame = authors.length > 0 && !hasUnattributedTrackedChange(targetParagraph) && authors.every((a) => a.trim().toLowerCase() === opAuthor);
20263
20450
  if (!allSame && existingPolicy === "merge-same-author") {
20264
20451
  return {
20265
20452
  documentXml,
@@ -20268,7 +20455,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
20268
20455
  status: "error",
20269
20456
  error: {
20270
20457
  code: "EXISTING_REVISIONS",
20271
- message: `Target paragraph contains tracked changes from another author (${authors.length ? authors.join(", ") : "unattributed"}). Use existingRevisions: "slice-cross-author" for a surgical edit that preserves reviewer history; accepting or rejecting revisions requires separate authorization.`,
20458
+ message: `Target paragraph contains tracked changes from another author (${[...authors, ...hasUnattributedTrackedChange(targetParagraph) ? ["unattributed"] : []].join(", ")}). Use existingRevisions: "slice-cross-author" for a surgical edit that preserves reviewer history; accepting or rejecting revisions requires separate authorization.`,
20272
20459
  revisionAuthors: authors,
20273
20460
  currentPolicy: existingPolicy
20274
20461
  }
@@ -20410,6 +20597,9 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
20410
20597
  onWarn
20411
20598
  }) : typeof targetText === "string" && /\r?\n/.test(targetText) ? findReconstructionParagraphRange(xmlDoc, targetText) : null;
20412
20599
  const hasExplicitRangeScope = Array.isArray(explicitRangeParagraphs) && explicitRangeParagraphs.length > 0;
20600
+ if (autoExistingRevisions && hasExplicitRangeScope) {
20601
+ options = { ...options, existingRevisions: resolveExistingRevisionsPolicy("auto", explicitRangeParagraphs, author) };
20602
+ }
20413
20603
  if (!useTableScope && hasExplicitRangeScope) {
20414
20604
  const insertionEntries = buildExplicitRangeInsertionEntries(explicitRangeParagraphs, effectiveModifiedText);
20415
20605
  if (insertionEntries && insertionEntries.length > 0) {
@@ -20585,7 +20775,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
20585
20775
  author,
20586
20776
  // A wholly inserted paragraph must track its own paragraph mark; otherwise Word's
20587
20777
  // Reject All removes the text but leaves an empty paragraph behind.
20588
- { generateRedlines, trackParagraphMark: true }
20778
+ { generateRedlines, trackParagraphMark: true, plainTextContent: options.plainTextContent === true }
20589
20779
  );
20590
20780
  const imported = xmlDoc.importNode(plainParagraph, true);
20591
20781
  ensureParagraphIdsOnImportedNode(imported, operationSession);
@@ -20724,6 +20914,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
20724
20914
  existingRevisions: options.existingRevisions || "merge-same-author",
20725
20915
  structuredContent: options.structuredContent !== false,
20726
20916
  explicitStructuredContent: options.explicitStructuredContent === true,
20917
+ plainTextContent: options.plainTextContent === true,
20727
20918
  _revisionIdAllocator: revisionIdAllocator,
20728
20919
  _isolatedTableCell: useTableScope
20729
20920
  }) : await applyRedlineToOxml2(scopedXml, originalTextForApply, effectiveModifiedText, {
@@ -20735,6 +20926,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
20735
20926
  pairReplacements: options.pairReplacements === true,
20736
20927
  insertionAffinity: options.insertionAffinity || null,
20737
20928
  literalTextEdit: options.literalTextEdit === true,
20929
+ plainTextContent: options.plainTextContent === true,
20738
20930
  _revisionIdAllocator: revisionIdAllocator,
20739
20931
  _isolatedTableCell: useTableScope
20740
20932
  });
@@ -21745,7 +21937,31 @@ function verifyUntrackedLocality({ before, after, targetSourceIndex, beforeViews
21745
21937
  }
21746
21938
  return violations.length ? { ok: false, violations } : { ok: true };
21747
21939
  }
21748
- function verifyNumberingReferences({ before, after, numberingXmls, beforeViews = null, afterViews = null }) {
21940
+ var numberingDefinitionCache = /* @__PURE__ */ new Map();
21941
+ var NUMBERING_DEFINITION_CACHE_SIZE = 8;
21942
+ function parseNumberingDefinitions(text2) {
21943
+ const cached = numberingDefinitionCache.get(text2);
21944
+ if (cached) return cached;
21945
+ const abstractLevels = /* @__PURE__ */ new Map();
21946
+ for (const match of text2.matchAll(/<(?:\w+:)?abstractNum\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?abstractNum>/g)) {
21947
+ const id = /abstractNumId="([^"]*)"/.exec(match[1])?.[1];
21948
+ if (id == null) continue;
21949
+ const levels = abstractLevels.get(id) || /* @__PURE__ */ new Set();
21950
+ for (const lvl of match[2].matchAll(/<(?:\w+:)?lvl\b[^>]*?ilvl="([^"]*)"/g)) levels.add(lvl[1]);
21951
+ abstractLevels.set(id, levels);
21952
+ }
21953
+ const numToAbstract = /* @__PURE__ */ new Map();
21954
+ for (const match of text2.matchAll(/<(?:\w+:)?num\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?num>/g)) {
21955
+ const id = /numId="([^"]*)"/.exec(match[1])?.[1];
21956
+ const abstractId = /abstractNumId[^>]*?val="([^"]*)"/.exec(match[2])?.[1];
21957
+ if (id != null && abstractId != null) numToAbstract.set(id, abstractId);
21958
+ }
21959
+ const definitions = { abstractLevels, numToAbstract };
21960
+ if (numberingDefinitionCache.size >= NUMBERING_DEFINITION_CACHE_SIZE) numberingDefinitionCache.delete(numberingDefinitionCache.keys().next().value);
21961
+ numberingDefinitionCache.set(text2, definitions);
21962
+ return definitions;
21963
+ }
21964
+ function verifyNumberingReferences({ before, after, numberingXmls }) {
21749
21965
  const references = (documentNode) => {
21750
21966
  const found = /* @__PURE__ */ new Map();
21751
21967
  for (const numIdNode of getAllReferencedNumIdElements(documentNode)) {
@@ -21761,21 +21977,7 @@ function verifyNumberingReferences({ before, after, numberingXmls, beforeViews =
21761
21977
  const used = references(after);
21762
21978
  if (used.size === 0) return { ok: true };
21763
21979
  const known = references(before);
21764
- const text2 = (numberingXmls || []).filter(Boolean).join("\n");
21765
- const abstractLevels = /* @__PURE__ */ new Map();
21766
- for (const match of text2.matchAll(/<(?:\w+:)?abstractNum\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?abstractNum>/g)) {
21767
- const id = /abstractNumId="([^"]*)"/.exec(match[1])?.[1];
21768
- if (id == null) continue;
21769
- const levels = abstractLevels.get(id) || /* @__PURE__ */ new Set();
21770
- for (const lvl of match[2].matchAll(/<(?:\w+:)?lvl\b[^>]*?ilvl="([^"]*)"/g)) levels.add(lvl[1]);
21771
- abstractLevels.set(id, levels);
21772
- }
21773
- const numToAbstract = /* @__PURE__ */ new Map();
21774
- for (const match of text2.matchAll(/<(?:\w+:)?num\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?num>/g)) {
21775
- const id = /numId="([^"]*)"/.exec(match[1])?.[1];
21776
- const abstractId = /abstractNumId[^>]*?val="([^"]*)"/.exec(match[2])?.[1];
21777
- if (id != null && abstractId != null) numToAbstract.set(id, abstractId);
21778
- }
21980
+ const { abstractLevels, numToAbstract } = parseNumberingDefinitions((numberingXmls || []).filter(Boolean).join("\n"));
21779
21981
  const violations = [];
21780
21982
  for (const [numId, levels] of used) {
21781
21983
  if (known.has(numId)) continue;
@@ -22124,6 +22326,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
22124
22326
  ...operation.existingRevisions ? { existingRevisions: operation.existingRevisions } : {},
22125
22327
  structuredContent: typeof operation.structuredContent === "boolean" ? operation.structuredContent : options.structuredContent !== false,
22126
22328
  explicitStructuredContent: operation.structuredContent === true,
22329
+ plainTextContent: operation.structuredContent === false || operation.structuredContent == null && options.structuredContent === false,
22127
22330
  ...operation._localizedReplacementCompilation && operation.structuredContent !== true ? { literalTextEdit: true } : {},
22128
22331
  pairReplacements: typeof operation.pairReplacements === "boolean" ? operation.pairReplacements : options.pairReplacements !== false,
22129
22332
  ...operation.insertionAffinity ? { insertionAffinity: operation.insertionAffinity } : {},
@@ -23378,35 +23581,44 @@ function preflightOperations(documentXml, operations, author, options = {}) {
23378
23581
  const anchor = operation.operationKind === "comment" ? operation.textToComment || paragraphText : operation.operationKind === "highlight" ? operation.textToHighlight : operation.operationKind === "format" ? operation.textToFormat : null;
23379
23582
  const anchorResolution = operation.operationKind === "comment" && anchor != null ? resolveTextInParagraphIndex(createParagraphTextIndex(paragraph, { revisionView: targetView }), anchor) : null;
23380
23583
  const anchorFound = anchor == null || (anchorResolution ? anchorResolution.found : paragraphText.includes(anchor));
23584
+ const rangeParagraphs = (opType) => {
23585
+ if (operation.targetEndRef) {
23586
+ return resolveParagraphRangeByRefs(xmlDoc, operation.targetRef, operation.targetEndRef, {
23587
+ opType,
23588
+ targetRefSnapshot: options.targetRefSnapshot || null,
23589
+ onInfo: options.onInfo,
23590
+ onWarn: options.onWarn
23591
+ });
23592
+ }
23593
+ if (!operation.targetEndDescriptor) return [paragraph];
23594
+ const endResolved = resolveTargetParagraph(xmlDoc, {
23595
+ targetText: operation.targetEndDescriptor.text,
23596
+ targetRef: operation.targetEndDescriptor.index,
23597
+ targetDescriptor: operation.targetEndDescriptor,
23598
+ opType,
23599
+ strictAmbiguity: strictTargets,
23600
+ paragraphMetadataIndex: currentMetadataIndex,
23601
+ metadataIndices,
23602
+ onInfo: options.onInfo,
23603
+ onWarn: options.onWarn
23604
+ });
23605
+ const allParagraphs = getDocumentParagraphNodes(xmlDoc);
23606
+ const startIndex = allParagraphs.indexOf(paragraph);
23607
+ const endIndex = allParagraphs.indexOf(endResolved?.paragraph);
23608
+ return startIndex >= 0 && endIndex >= startIndex ? allParagraphs.slice(startIndex, endIndex + 1) : null;
23609
+ };
23381
23610
  const hasRevisions = containsTrackedChanges(paragraph);
23382
- const existingPolicy = operation.existingRevisions || options.existingRevisions || "merge-same-author";
23611
+ const requestedPolicy = operation.existingRevisions || options.existingRevisions || "merge-same-author";
23612
+ const existingPolicy = resolveExistingRevisionsPolicy(
23613
+ requestedPolicy,
23614
+ requestedPolicy === "auto" && operation.operationKind === "redline" && (operation.targetEndRef || operation.targetEndDescriptor) ? rangeParagraphs("redline") || paragraph : paragraph,
23615
+ authorUsed
23616
+ );
23383
23617
  const deletingWholeParagraph = operation.operationKind === "redline" && operation.modified === "";
23384
23618
  const commentIds = deletingWholeParagraph ? getCommentIdsInParagraph(paragraph) : [];
23385
23619
  let error2 = null;
23386
23620
  if (operation.operationKind === "restore") {
23387
- let restorationParagraphs = operation.targetEndRef ? resolveParagraphRangeByRefs(xmlDoc, operation.targetRef, operation.targetEndRef, {
23388
- opType: "restore",
23389
- targetRefSnapshot: options.targetRefSnapshot || null,
23390
- onInfo: options.onInfo,
23391
- onWarn: options.onWarn
23392
- }) : [paragraph];
23393
- if (!operation.targetEndRef && operation.targetEndDescriptor) {
23394
- const endResolved = resolveTargetParagraph(xmlDoc, {
23395
- targetText: operation.targetEndDescriptor.text,
23396
- targetRef: operation.targetEndDescriptor.index,
23397
- targetDescriptor: operation.targetEndDescriptor,
23398
- opType: "restore",
23399
- strictAmbiguity: strictTargets,
23400
- paragraphMetadataIndex: currentMetadataIndex,
23401
- metadataIndices,
23402
- onInfo: options.onInfo,
23403
- onWarn: options.onWarn
23404
- });
23405
- const allParagraphs = getDocumentParagraphNodes(xmlDoc);
23406
- const startIndex = allParagraphs.indexOf(paragraph);
23407
- const endIndex = allParagraphs.indexOf(endResolved?.paragraph);
23408
- restorationParagraphs = startIndex >= 0 && endIndex >= startIndex ? allParagraphs.slice(startIndex, endIndex + 1) : null;
23409
- }
23621
+ const restorationParagraphs = rangeParagraphs("restore");
23410
23622
  const replacements = Array.isArray(operation.modified) ? operation.modified : [operation.modified];
23411
23623
  if (!restorationParagraphs || restorationParagraphs[0] !== paragraph || replacements.length !== restorationParagraphs.length) {
23412
23624
  error2 = {
@@ -23453,11 +23665,11 @@ function preflightOperations(documentXml, operations, author, options = {}) {
23453
23665
  if (existingPolicy === "merge-same-author" || existingPolicy === "slice-cross-author") {
23454
23666
  const authors = getTrackedChangeAuthors(paragraph);
23455
23667
  const opAuthor = String(authorUsed || "").trim().toLowerCase();
23456
- const allSame = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === opAuthor);
23668
+ const allSame = authors.length > 0 && !hasUnattributedTrackedChange(paragraph) && authors.every((a) => a.trim().toLowerCase() === opAuthor);
23457
23669
  if (!allSame && existingPolicy === "merge-same-author") {
23458
23670
  error2 = {
23459
23671
  code: "EXISTING_REVISIONS",
23460
- message: `Target paragraph contains tracked changes from another author (${authors.length ? authors.join(", ") : "unattributed"}). Use existingRevisions: "slice-cross-author" for a surgical edit that preserves reviewer history; accepting or rejecting revisions requires separate authorization.`,
23672
+ message: `Target paragraph contains tracked changes from another author (${[...authors, ...hasUnattributedTrackedChange(paragraph) ? ["unattributed"] : []].join(", ")}). Use existingRevisions: "slice-cross-author" for a surgical edit that preserves reviewer history; accepting or rejecting revisions requires separate authorization.`,
23461
23673
  revisionAuthors: authors,
23462
23674
  currentPolicy: existingPolicy
23463
23675
  };
@@ -25543,6 +25755,7 @@ export {
25543
25755
  canonicalNumberingDefinition,
25544
25756
  clearSingleLineListFallbackExplicitSequence,
25545
25757
  collectContiguousListParagraphBlock,
25758
+ compareDocumentInspections,
25546
25759
  computeDocumentPartsRevisionToken,
25547
25760
  computePackageRevisionToken,
25548
25761
  computeRevisionToken,
@@ -25594,9 +25807,11 @@ export {
25594
25807
  injectCommentsIntoPackage2 as injectCommentsIntoPackage,
25595
25808
  insertBodyElementBeforeSectPr,
25596
25809
  inspectDocumentParts,
25810
+ inspectionReviewShape,
25597
25811
  isLikelyStructuredTableSourceParagraph,
25598
25812
  isMarkdownTableText,
25599
25813
  isNodeVisibleInRevisionView,
25814
+ mergeCommentParts,
25600
25815
  mergeNumberingParts,
25601
25816
  mergeNumberingXmlBySchemaOrder,
25602
25817
  normalizeBodySectionOrderStandalone,
@@ -25623,6 +25838,7 @@ export {
25623
25838
  repairKnownContentTypes,
25624
25839
  reserveNextNumberingId,
25625
25840
  reserveNextNumberingIdPair,
25841
+ resolveExistingRevisionsPolicy,
25626
25842
  resolveParagraphRangeByRefs,
25627
25843
  resolveSingleLineListFallbackNumberingAction,
25628
25844
  resolveTargetParagraph,