@ansonlai/docx-redline-js 0.10.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.
@@ -1,4 +1,4 @@
1
- // @ansonlai/docx-redline-js standalone CommonJS bundle v0.10.0 — https://github.com/AnsonLai/docx-redline-js
1
+ // @ansonlai/docx-redline-js standalone CommonJS bundle 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;
@@ -8065,6 +8065,7 @@ __export(index_exports, {
8065
8065
  isLikelyStructuredTableSourceParagraph: () => isLikelyStructuredTableSourceParagraph,
8066
8066
  isMarkdownTableText: () => isMarkdownTableText,
8067
8067
  isNodeVisibleInRevisionView: () => isNodeVisibleInRevisionView,
8068
+ mergeCommentParts: () => mergeCommentParts,
8068
8069
  mergeNumberingParts: () => mergeNumberingParts,
8069
8070
  mergeNumberingXmlBySchemaOrder: () => mergeNumberingXmlBySchemaOrder,
8070
8071
  normalizeBodySectionOrderStandalone: () => normalizeBodySectionOrderStandalone,
@@ -10595,6 +10596,7 @@ function paragraphMarkPPrXml(kind, ctx = {}) {
10595
10596
  }
10596
10597
 
10597
10598
  // services/table-reconciliation.js
10599
+ var cellMarkdown = (text2, options) => options?.plainTextContent === true ? { cleanText: String(text2 ?? ""), formatHints: [] } : preprocessMarkdown(text2);
10598
10600
  function generateTableOoxml(tableData, options = {}) {
10599
10601
  const {
10600
10602
  generateRedlines = false,
@@ -10628,7 +10630,7 @@ function generateTableOoxml(tableData, options = {}) {
10628
10630
  let cellsXml = "";
10629
10631
  for (let c = 0; c < numCols; c++) {
10630
10632
  const cellText = row[c] || "";
10631
- const { cleanText, formatHints } = preprocessMarkdown(cellText);
10633
+ const { cleanText, formatHints } = cellMarkdown(cellText, options);
10632
10634
  const runModel = [{
10633
10635
  kind: generateRedlines ? RunKind.INSERTION : RunKind.TEXT,
10634
10636
  text: cleanText,
@@ -10743,7 +10745,7 @@ function serializeVirtualGridToOoxml(grid, operations, options) {
10743
10745
  let cellsXml = "";
10744
10746
  const rowInsertMeta = generateRedlines ? createRevisionMetadata(author, revisionIdAllocator) : null;
10745
10747
  for (const cellText of op.cells) {
10746
- const { cleanText, formatHints } = preprocessMarkdown(cellText);
10748
+ const { cleanText, formatHints } = cellMarkdown(cellText, options);
10747
10749
  const runModel = [{
10748
10750
  kind: generateRedlines ? RunKind.INSERTION : RunKind.TEXT,
10749
10751
  text: cleanText,
@@ -10817,7 +10819,7 @@ function reconcileMultiBlockCellContent(cell, newText, options) {
10817
10819
  }
10818
10820
  return;
10819
10821
  }
10820
- const { cleanText, formatHints } = preprocessMarkdown(line);
10822
+ const { cleanText, formatHints } = cellMarkdown(line, options);
10821
10823
  const diffOps = computeWordLevelDiffOps(block.acceptedText, cleanText);
10822
10824
  const model = applyPatches(splitRunsAtDiffBoundaries(block.runModel, diffOps), diffOps, {
10823
10825
  generateRedlines,
@@ -10827,7 +10829,7 @@ function reconcileMultiBlockCellContent(cell, newText, options) {
10827
10829
  out += serializeToOoxml(model, block.pPr, formatHints, { author, generateRedlines, revisionIdAllocator });
10828
10830
  });
10829
10831
  for (let i2 = cell.blocks.length; i2 < lines.length; i2++) {
10830
- const { cleanText, formatHints } = preprocessMarkdown(lines[i2]);
10832
+ const { cleanText, formatHints } = cellMarkdown(lines[i2], options);
10831
10833
  const runModel = [{
10832
10834
  kind: generateRedlines ? RunKind.INSERTION : RunKind.TEXT,
10833
10835
  text: cleanText,
@@ -10846,7 +10848,7 @@ function reconcileCellContent(cell, newText, options) {
10846
10848
  return reconcileMultiBlockCellContent(cell, newText, options);
10847
10849
  }
10848
10850
  const { generateRedlines, author, revisionIdAllocator = null } = options;
10849
- const { cleanText, formatHints } = preprocessMarkdown(newText);
10851
+ const { cleanText, formatHints } = cellMarkdown(newText, options);
10850
10852
  const oldText = cell.getText();
10851
10853
  const diffOps = computeWordLevelDiffOps(oldText, cleanText);
10852
10854
  const baseBlock = cell.blocks[0] || { runModel: [], pPr: null };
@@ -10892,7 +10894,7 @@ function parseTable(text2) {
10892
10894
  }
10893
10895
  const isSeparatorLine = (line) => {
10894
10896
  const normalized = line.replace(/\s+/g, "");
10895
- return /^\|:?-{3,}:?(\|:?-{3,}:?)+\|?$/.test(normalized);
10897
+ return /^\|:?-{3,}:?(?:(\|:?-{3,}:?)+\|?|\|)$/.test(normalized);
10896
10898
  };
10897
10899
  const hasHeader = lines.some(isSeparatorLine);
10898
10900
  const dataLines = lines.filter((line) => !isSeparatorLine(line));
@@ -11476,7 +11478,7 @@ var ReconciliationPipeline = class {
11476
11478
  };
11477
11479
 
11478
11480
  // pipeline/structured-content.js
11479
- var TABLE_SEPARATOR = /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?\s*$/;
11481
+ var TABLE_SEPARATOR = /^\s*(?:\|\s*:?-{3,}:?\s*\||\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?)\s*$/;
11480
11482
  function isBlank(line) {
11481
11483
  return !line || line.trim().length === 0;
11482
11484
  }
@@ -15133,7 +15135,7 @@ function keepUnchangedTextOutsideWrappers(diffs, mapping) {
15133
15135
  function noChanges(serializer, xmlDoc) {
15134
15136
  return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: false });
15135
15137
  }
15136
- function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines = true) {
15138
+ function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines = true, extra = {}) {
15137
15139
  const tableNodes = getElementsByTagNSOrTag(xmlDoc, NS_W, "tbl");
15138
15140
  const newTableData = parseTable(modifiedText);
15139
15141
  const hasNewContent = newTableData.rows.length > 0 || newTableData.headers.length > 0;
@@ -15149,7 +15151,8 @@ function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, auth
15149
15151
  const options = {
15150
15152
  generateRedlines,
15151
15153
  author,
15152
- revisionIdAllocator: getRevisionIdAllocatorForDocument(xmlDoc)
15154
+ revisionIdAllocator: getRevisionIdAllocatorForDocument(xmlDoc),
15155
+ plainTextContent: extra.plainTextContent === true
15153
15156
  };
15154
15157
  const reconciledOxml = serializeVirtualGridToOoxml(oldGrid, operations, options);
15155
15158
  const wrappedOxml = `<root xmlns:w="${NS_W}">${reconciledOxml}</root>`;
@@ -15169,14 +15172,14 @@ function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, auth
15169
15172
  targetTable.parentNode.replaceChild(importedTable, targetTable);
15170
15173
  return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: true });
15171
15174
  }
15172
- function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines) {
15175
+ function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines, extra = {}) {
15173
15176
  const revisionIdAllocator = getRevisionIdAllocatorForDocument(xmlDoc);
15174
15177
  const tableData = parseTable(modifiedText);
15175
15178
  if (!tableData || tableData.rows.length === 0 && tableData.headers.length === 0) {
15176
15179
  log("[OxmlEngine] Failed to parse table data from Markdown");
15177
15180
  return noChanges(serializer, xmlDoc);
15178
15181
  }
15179
- const tableOoxml = generateTableOoxml(tableData, { generateRedlines, author, revisionIdAllocator, trackAsBlock: true });
15182
+ const tableOoxml = generateTableOoxml(tableData, { generateRedlines, author, revisionIdAllocator, trackAsBlock: true, plainTextContent: extra.plainTextContent === true });
15180
15183
  const tableDoc = parseOoxmlSafe(`<root xmlns:w="${NS_W}">${tableOoxml}</root>`, "application/xml").doc;
15181
15184
  if (!tableDoc) return noChanges(serializer, xmlDoc);
15182
15185
  const tableParseError = getXmlParseError(tableDoc);
@@ -16375,11 +16378,11 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
16375
16378
  if (isMarkdownTable && !hasTables) {
16376
16379
  recordRouteSelection(options, "table", { transformation: "text-to-table" });
16377
16380
  log("[OxmlEngine] Text-to-table transformation: generating new table from Markdown");
16378
- return finalize(applyTextToTableTransformation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines));
16381
+ return finalize(applyTextToTableTransformation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines, { plainTextContent: options.plainTextContent === true }));
16379
16382
  }
16380
16383
  if (hasTables && isMarkdownTable) {
16381
16384
  recordRouteSelection(options, "table", { transformation: "table-reconciliation" });
16382
- return finalize(applyTableReconciliation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines));
16385
+ return finalize(applyTableReconciliation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines, { plainTextContent: options.plainTextContent === true }));
16383
16386
  }
16384
16387
  if (usesSurgicalTextMode) {
16385
16388
  recordRouteSelection(options, "surgical", { tableScoped: hasTables });
@@ -20675,232 +20678,6 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, doc
20675
20678
  };
20676
20679
  }
20677
20680
 
20678
- // engine/formatting-removal.js
20679
- function removeNode2(node) {
20680
- if (node?.parentNode) {
20681
- node.parentNode.removeChild(node);
20682
- }
20683
- }
20684
- function removeFormattingFromRPr(rPr, formatTypes = ["all"]) {
20685
- if (!rPr) return null;
20686
- const rPrClone = rPr.cloneNode(true);
20687
- if (formatTypes.includes("all")) {
20688
- const toRemove = [
20689
- "w:b",
20690
- "w:i",
20691
- "w:u",
20692
- "w:strike",
20693
- "w:dstrike",
20694
- "w:color",
20695
- "w:sz",
20696
- "w:szCs",
20697
- "w:rFonts",
20698
- "w:highlight",
20699
- "w:vertAlign",
20700
- "w:spacing",
20701
- "w:w",
20702
- "w:kern",
20703
- "w:position"
20704
- ];
20705
- toRemove.forEach((tag) => {
20706
- const elements = rPrClone.querySelectorAll(`${tag}, ${tag.replace("w:", "")}`);
20707
- elements.forEach(removeNode2);
20708
- });
20709
- } else {
20710
- const tagMap = {
20711
- "bold": "w:b",
20712
- "italic": "w:i",
20713
- "underline": "w:u",
20714
- "strikethrough": "w:strike",
20715
- "doubleStrike": "w:dstrike",
20716
- "color": "w:color",
20717
- "highlight": "w:highlight",
20718
- "fontSize": "w:sz",
20719
- "fontSizeCs": "w:szCs",
20720
- // Complex script font size
20721
- "fontFamily": "w:rFonts",
20722
- "superscript": "w:vertAlign",
20723
- "subscript": "w:vertAlign"
20724
- };
20725
- formatTypes.forEach((type) => {
20726
- const tag = tagMap[type];
20727
- if (tag) {
20728
- const elements = rPrClone.querySelectorAll(`${tag}, ${tag.replace("w:", "")}`);
20729
- elements.forEach(removeNode2);
20730
- }
20731
- });
20732
- }
20733
- return rPrClone.children.length > 0 ? rPrClone : null;
20734
- }
20735
- function applyFormattingRemovalToOoxml(ooxmlString, targetText, formatTypes) {
20736
- if (!targetText || !ooxmlString) return ooxmlString;
20737
- const doc = parseOoxml(ooxmlString);
20738
- if (!doc) return ooxmlString;
20739
- const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
20740
- const runs = doc.getElementsByTagNameNS(NS_W11, "r");
20741
- const insertions = doc.getElementsByTagNameNS(NS_W11, "ins");
20742
- const allRuns = [...Array.from(runs)];
20743
- for (const ins of insertions) {
20744
- const insideRuns = ins.getElementsByTagNameNS(NS_W11, "r");
20745
- allRuns.push(...Array.from(insideRuns));
20746
- }
20747
- for (const run of allRuns) {
20748
- const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
20749
- const runText = Array.from(textNodes).map((t) => t.textContent).join("");
20750
- if (runText.includes(targetText) || runText === targetText) {
20751
- const rPrElements = run.getElementsByTagNameNS(NS_W11, "rPr");
20752
- if (rPrElements.length > 0) {
20753
- const rPr = rPrElements[0];
20754
- const newRPr = removeFormattingFromRPr(rPr, formatTypes);
20755
- if (newRPr) {
20756
- if (rPr.parentNode) {
20757
- rPr.parentNode.replaceChild(newRPr, rPr);
20758
- }
20759
- } else {
20760
- removeNode2(rPr);
20761
- }
20762
- }
20763
- }
20764
- }
20765
- return serializeOoxml(doc);
20766
- }
20767
- var HIGHLIGHT_COLOR_MAP = {
20768
- "yellow": "yellow",
20769
- "green": "green",
20770
- "cyan": "cyan",
20771
- "magenta": "magenta",
20772
- "blue": "blue",
20773
- "red": "red",
20774
- "darkblue": "darkBlue",
20775
- "darkcyan": "darkCyan",
20776
- "darkgreen": "darkGreen",
20777
- "darkmagenta": "darkMagenta",
20778
- "darkred": "darkRed",
20779
- "darkyellow": "darkYellow",
20780
- "gray25": "lightGray",
20781
- "gray50": "darkGray",
20782
- "black": "black",
20783
- "white": "white"
20784
- };
20785
- function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
20786
- const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
20787
- const ooxmlColor = HIGHLIGHT_COLOR_MAP[color.toLowerCase()] || "yellow";
20788
- const generateRedlines = options?.generateRedlines ?? false;
20789
- const author = options?.author || getDefaultAuthor();
20790
- let rPrElement = rPr;
20791
- if (!rPrElement) {
20792
- rPrElement = createWordElement(doc, "w:rPr");
20793
- } else {
20794
- rPrElement = rPr.cloneNode(true);
20795
- }
20796
- let previousRPrState = null;
20797
- if (generateRedlines) {
20798
- previousRPrState = createWordElement(doc, "w:rPr");
20799
- Array.from(rPrElement.childNodes).forEach((child) => {
20800
- if (child.nodeName !== "w:rPrChange") {
20801
- previousRPrState.appendChild(child.cloneNode(true));
20802
- }
20803
- });
20804
- }
20805
- const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W11, "highlight");
20806
- Array.from(existingHighlight).forEach(removeNode2);
20807
- const highlightEl = createWordElement(doc, "w:highlight");
20808
- highlightEl.setAttributeNS(NS_W11, "w:val", ooxmlColor);
20809
- rPrElement.appendChild(highlightEl);
20810
- if (generateRedlines && previousRPrState) {
20811
- const rPrChange = createWordElement(doc, "w:rPrChange");
20812
- const metadata = createRevisionMetadata(author, doc);
20813
- rPrChange.setAttribute("w:id", String(metadata.id));
20814
- rPrChange.setAttribute("w:author", metadata.author);
20815
- rPrChange.setAttribute("w:date", metadata.date);
20816
- rPrChange.appendChild(previousRPrState);
20817
- const existingChange = rPrElement.getElementsByTagNameNS(NS_W11, "rPrChange");
20818
- Array.from(existingChange).forEach(removeNode2);
20819
- rPrElement.appendChild(rPrChange);
20820
- }
20821
- return rPrElement;
20822
- }
20823
- function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", options = {}) {
20824
- if (!targetText || !ooxmlString) return ooxmlString;
20825
- const doc = parseOoxml(ooxmlString);
20826
- if (!doc) return ooxmlString;
20827
- let revisionIdAllocator;
20828
- if (options?._revisionIdAllocator instanceof RevisionIdAllocator) {
20829
- revisionIdAllocator = options._revisionIdAllocator;
20830
- seedRevisionIdsFromDocument(doc, revisionIdAllocator);
20831
- } else {
20832
- revisionIdAllocator = createRevisionIdAllocator(doc);
20833
- }
20834
- const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
20835
- const getRunText = (run) => {
20836
- const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
20837
- return Array.from(textNodes).map((t) => t.textContent).join("");
20838
- };
20839
- const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W11, "r"));
20840
- const sourceRunsWithClaimedRevisionIds = /* @__PURE__ */ new WeakSet();
20841
- const cloneRunWithText2 = (sourceRun, text2, shouldHighlight) => {
20842
- const clonedRun = sourceRun.cloneNode(true);
20843
- if (sourceRunsWithClaimedRevisionIds.has(sourceRun)) {
20844
- refreshRunPropertyChangeIds(clonedRun, revisionIdAllocator);
20845
- } else {
20846
- sourceRunsWithClaimedRevisionIds.add(sourceRun);
20847
- }
20848
- const textNodes = clonedRun.getElementsByTagNameNS(NS_W11, "t");
20849
- Array.from(textNodes).forEach(removeNode2);
20850
- const newText = createWordElement(doc, "w:t");
20851
- newText.setAttribute("xml:space", "preserve");
20852
- newText.textContent = text2;
20853
- clonedRun.appendChild(newText);
20854
- if (shouldHighlight) {
20855
- const rPrElements = clonedRun.getElementsByTagNameNS(NS_W11, "rPr");
20856
- const existingRPr = rPrElements.length > 0 ? rPrElements[0] : null;
20857
- const newRPr = injectHighlightIntoRPr(doc, existingRPr, color, options);
20858
- if (existingRPr) {
20859
- clonedRun.replaceChild(newRPr, existingRPr);
20860
- } else {
20861
- clonedRun.insertBefore(newRPr, clonedRun.firstChild);
20862
- }
20863
- }
20864
- return clonedRun;
20865
- };
20866
- for (const run of allRuns) {
20867
- const runText = getRunText(run);
20868
- if (!runText) continue;
20869
- const matchIndexes = [];
20870
- let searchOffset = 0;
20871
- while (searchOffset <= runText.length - targetText.length) {
20872
- const matchIndex = runText.indexOf(targetText, searchOffset);
20873
- if (matchIndex === -1) break;
20874
- matchIndexes.push(matchIndex);
20875
- searchOffset = matchIndex + targetText.length;
20876
- }
20877
- if (matchIndexes.length === 0) continue;
20878
- const parent = run.parentNode;
20879
- if (!parent) {
20880
- console.warn("[Highlight] Run parent is null; skipping. Likely already processed.");
20881
- continue;
20882
- }
20883
- const fragment = doc.createDocumentFragment();
20884
- let cursor = 0;
20885
- for (const matchIndex of matchIndexes) {
20886
- if (matchIndex > cursor) {
20887
- fragment.appendChild(cloneRunWithText2(run, runText.slice(cursor, matchIndex), false));
20888
- }
20889
- fragment.appendChild(cloneRunWithText2(
20890
- run,
20891
- runText.slice(matchIndex, matchIndex + targetText.length),
20892
- true
20893
- ));
20894
- cursor = matchIndex + targetText.length;
20895
- }
20896
- if (cursor < runText.length) {
20897
- fragment.appendChild(cloneRunWithText2(run, runText.slice(cursor), false));
20898
- }
20899
- parent.replaceChild(fragment, run);
20900
- }
20901
- return serializeOoxml(doc);
20902
- }
20903
-
20904
20681
  // services/package-parts.js
20905
20682
  var WORD_ML = "application/vnd.openxmlformats-officedocument.wordprocessingml";
20906
20683
  var REL_OFFICE_DOC = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
@@ -21188,31 +20965,34 @@ async function ensureNumberingArtifactsInZip(zip, numberingXmlList, options = {}
21188
20965
  }
21189
20966
  }
21190
20967
  }
20968
+ function mergeCommentsPartXml(existingXml, commentsXml, options = {}) {
20969
+ if (!existingXml || options.replaceExisting === true) return commentsXml;
20970
+ const existingDoc = parseXmlStrictStandalone(existingXml, "word/comments.xml (existing)");
20971
+ const incomingDoc = parseXmlStrictStandalone(commentsXml, "word/comments.xml (incoming)");
20972
+ const incomingComments = Array.from(incomingDoc.documentElement.getElementsByTagNameNS(NS_W7, "comment"));
20973
+ if (incomingComments.length === 0) return existingXml;
20974
+ const existingRoot = existingDoc.documentElement;
20975
+ const usedIds = new Set(
20976
+ Array.from(existingRoot.getElementsByTagNameNS(NS_W7, "comment")).map((comment) => comment.getAttribute("w:id") || comment.getAttribute("id")).filter(Boolean)
20977
+ );
20978
+ for (const incomingComment of incomingComments) {
20979
+ const id = incomingComment.getAttribute("w:id") || incomingComment.getAttribute("id");
20980
+ if (id && usedIds.has(id)) {
20981
+ throw Object.assign(new Error(`Duplicate comment id: ${id}`), { code: "DUPLICATE_COMMENT_ID", commentId: id });
20982
+ }
20983
+ if (id) usedIds.add(id);
20984
+ existingRoot.appendChild(existingDoc.importNode(incomingComment, true));
20985
+ }
20986
+ return createSerializer().serializeToString(existingDoc);
20987
+ }
21191
20988
  async function ensureCommentsArtifactsInZip(zip, commentsXml, options = {}) {
21192
20989
  const onInfo = typeof options?.onInfo === "function" ? options.onInfo : () => {
21193
20990
  };
21194
20991
  if (!commentsXml) return;
21195
20992
  const serializer = createSerializer();
21196
20993
  const existingText = await readZipText(zip, COMMENTS_PATH);
21197
- if (!existingText || options.replaceExisting === true) {
21198
- onInfo("[Demo] Adding comments.xml");
21199
- zip.file(COMMENTS_PATH, commentsXml);
21200
- } else {
21201
- const existingDoc = parseXmlStrictStandalone(existingText, "word/comments.xml (existing)");
21202
- const incomingDoc = parseXmlStrictStandalone(commentsXml, "word/comments.xml (incoming)");
21203
- const existingRoot = existingDoc.documentElement;
21204
- const existingIds = new Set(
21205
- Array.from(existingRoot.getElementsByTagNameNS(NS_W7, "comment")).map((comment) => comment.getAttribute("w:id") || comment.getAttribute("id")).filter(Boolean)
21206
- );
21207
- for (const incomingComment of Array.from(incomingDoc.documentElement.getElementsByTagNameNS(NS_W7, "comment"))) {
21208
- const id = incomingComment.getAttribute("w:id") || incomingComment.getAttribute("id");
21209
- if (id && existingIds.has(id)) {
21210
- throw new Error(`Duplicate comment id: ${id}`);
21211
- }
21212
- existingRoot.appendChild(existingDoc.importNode(incomingComment, true));
21213
- }
21214
- zip.file(COMMENTS_PATH, serializer.serializeToString(existingDoc));
21215
- }
20994
+ if (!existingText || options.replaceExisting === true) onInfo("[Demo] Adding comments.xml");
20995
+ zip.file(COMMENTS_PATH, mergeCommentsPartXml(existingText, commentsXml, options));
21216
20996
  const ctText = await readZipText(zip, CONTENT_TYPES_PATH);
21217
20997
  if (ctText) {
21218
20998
  const ctDoc = parseXmlStrictStandalone(ctText, CONTENT_TYPES_PATH);
@@ -21448,6 +21228,318 @@ async function validateDocxPackage(zip) {
21448
21228
  }
21449
21229
  }
21450
21230
 
21231
+ // services/comment-parts-merge.js
21232
+ var PART_KEYS = Object.freeze({
21233
+ comments: "commentsXml",
21234
+ commentsExtended: "commentsExtendedXml",
21235
+ commentsIds: "commentsIdsXml",
21236
+ commentsExtensible: "commentsExtensibleXml"
21237
+ });
21238
+ var partsOf = (parts) => Object.fromEntries(Object.values(PART_KEYS).map((key) => [key, parts?.[key] || null]));
21239
+ function mergeCommentParts(parts = {}, output = {}) {
21240
+ const before = partsOf(parts);
21241
+ const after = { ...before };
21242
+ const fail = (error2) => ({ status: "error", hasChanges: false, parts: before, changedParts: [], addedParts: [], error: error2 });
21243
+ const commentsMode = output?.commentsXmlMode === "replace" ? "replace" : "merge";
21244
+ const extendedMode = output?.commentsExtendedXmlMode === "replace" ? "replace" : "merge";
21245
+ for (const [label, value] of [
21246
+ ...Object.values(PART_KEYS).map((key) => [`parts.${key}`, before[key]]),
21247
+ ["output.commentsXml", output?.commentsXml],
21248
+ ["output.commentsExtendedXml", output?.commentsExtendedXml]
21249
+ ]) {
21250
+ if (value != null && typeof value !== "string") {
21251
+ return fail({ code: "INVALID_COMMENT_PARTS", message: `${label} must be an XML string, null or omitted (got ${typeof value}).` });
21252
+ }
21253
+ }
21254
+ const requirePart = (xml, part, rootName, label) => {
21255
+ let doc;
21256
+ try {
21257
+ doc = parseXmlStrictStandalone(xml, label);
21258
+ } catch (error2) {
21259
+ throw Object.assign(error2, { part });
21260
+ }
21261
+ if (doc.documentElement?.localName !== rootName) {
21262
+ throw Object.assign(new Error(`${label}: expected a <${rootName}> root element, found <${doc.documentElement?.localName || "none"}>.`), { code: "INVALID_COMMENT_PARTS", part });
21263
+ }
21264
+ return doc;
21265
+ };
21266
+ try {
21267
+ if (output?.commentsXml) {
21268
+ const incomingDoc = requirePart(output.commentsXml, "comments", "comments", "word/comments.xml (incoming)");
21269
+ const seen = /* @__PURE__ */ new Set();
21270
+ for (const node of Array.from(incomingDoc.getElementsByTagNameNS("*", "comment"))) {
21271
+ const id = node.getAttribute("w:id") || node.getAttribute("id");
21272
+ if (!id) continue;
21273
+ if (seen.has(id)) {
21274
+ throw Object.assign(new Error(`Duplicate comment id: ${id}`), { code: "DUPLICATE_COMMENT_ID", commentId: id, part: "comments" });
21275
+ }
21276
+ seen.add(id);
21277
+ }
21278
+ if (before.commentsXml) requirePart(before.commentsXml, "comments", "comments", "word/comments.xml (existing)");
21279
+ if (before.commentsIdsXml) requirePart(before.commentsIdsXml, "commentsIds", "commentsIds", "word/commentsIds.xml (existing)");
21280
+ if (before.commentsExtensibleXml) requirePart(before.commentsExtensibleXml, "commentsExtensible", "commentsExtensible", "word/commentsExtensible.xml (existing)");
21281
+ after.commentsXml = mergeCommentsPartXml(before.commentsXml, output.commentsXml, { replaceExisting: commentsMode === "replace" });
21282
+ Object.assign(after, reconcileCommentSiblingParts({
21283
+ commentsXml: after.commentsXml,
21284
+ commentsIdsXml: after.commentsIdsXml,
21285
+ commentsExtensibleXml: after.commentsExtensibleXml
21286
+ }));
21287
+ }
21288
+ if (output?.commentsExtendedXml) {
21289
+ if (extendedMode !== "replace" && before.commentsExtendedXml) {
21290
+ return fail({
21291
+ code: "UNSUPPORTED_EXISTING_COMMENT_THREAD_METADATA",
21292
+ part: "commentsExtended",
21293
+ 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.'
21294
+ });
21295
+ }
21296
+ requirePart(output.commentsExtendedXml, "commentsExtended", "commentsEx", "word/commentsExtended.xml (incoming)");
21297
+ after.commentsExtendedXml = output.commentsExtendedXml;
21298
+ }
21299
+ } catch (error2) {
21300
+ return fail({
21301
+ code: error2?.code || "COMMENT_PARTS_MERGE_FAILED",
21302
+ message: error2?.message || String(error2),
21303
+ ...error2?.part || error2?.code === "DUPLICATE_COMMENT_ID" ? { part: error2.part || "comments" } : {},
21304
+ ...error2?.commentId ? { commentId: error2.commentId } : {}
21305
+ });
21306
+ }
21307
+ const changedParts = [];
21308
+ const addedParts = [];
21309
+ for (const [name, key] of Object.entries(PART_KEYS)) {
21310
+ if (after[key] === before[key]) continue;
21311
+ changedParts.push(name);
21312
+ if (!before[key]) addedParts.push(name);
21313
+ }
21314
+ return { status: "ok", hasChanges: changedParts.length > 0, parts: after, changedParts, addedParts };
21315
+ }
21316
+
21317
+ // engine/formatting-removal.js
21318
+ function removeNode2(node) {
21319
+ if (node?.parentNode) {
21320
+ node.parentNode.removeChild(node);
21321
+ }
21322
+ }
21323
+ function removeFormattingFromRPr(rPr, formatTypes = ["all"]) {
21324
+ if (!rPr) return null;
21325
+ const rPrClone = rPr.cloneNode(true);
21326
+ if (formatTypes.includes("all")) {
21327
+ const toRemove = [
21328
+ "w:b",
21329
+ "w:i",
21330
+ "w:u",
21331
+ "w:strike",
21332
+ "w:dstrike",
21333
+ "w:color",
21334
+ "w:sz",
21335
+ "w:szCs",
21336
+ "w:rFonts",
21337
+ "w:highlight",
21338
+ "w:vertAlign",
21339
+ "w:spacing",
21340
+ "w:w",
21341
+ "w:kern",
21342
+ "w:position"
21343
+ ];
21344
+ toRemove.forEach((tag) => {
21345
+ const elements = rPrClone.querySelectorAll(`${tag}, ${tag.replace("w:", "")}`);
21346
+ elements.forEach(removeNode2);
21347
+ });
21348
+ } else {
21349
+ const tagMap = {
21350
+ "bold": "w:b",
21351
+ "italic": "w:i",
21352
+ "underline": "w:u",
21353
+ "strikethrough": "w:strike",
21354
+ "doubleStrike": "w:dstrike",
21355
+ "color": "w:color",
21356
+ "highlight": "w:highlight",
21357
+ "fontSize": "w:sz",
21358
+ "fontSizeCs": "w:szCs",
21359
+ // Complex script font size
21360
+ "fontFamily": "w:rFonts",
21361
+ "superscript": "w:vertAlign",
21362
+ "subscript": "w:vertAlign"
21363
+ };
21364
+ formatTypes.forEach((type) => {
21365
+ const tag = tagMap[type];
21366
+ if (tag) {
21367
+ const elements = rPrClone.querySelectorAll(`${tag}, ${tag.replace("w:", "")}`);
21368
+ elements.forEach(removeNode2);
21369
+ }
21370
+ });
21371
+ }
21372
+ return rPrClone.children.length > 0 ? rPrClone : null;
21373
+ }
21374
+ function applyFormattingRemovalToOoxml(ooxmlString, targetText, formatTypes) {
21375
+ if (!targetText || !ooxmlString) return ooxmlString;
21376
+ const doc = parseOoxml(ooxmlString);
21377
+ if (!doc) return ooxmlString;
21378
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
21379
+ const runs = doc.getElementsByTagNameNS(NS_W11, "r");
21380
+ const insertions = doc.getElementsByTagNameNS(NS_W11, "ins");
21381
+ const allRuns = [...Array.from(runs)];
21382
+ for (const ins of insertions) {
21383
+ const insideRuns = ins.getElementsByTagNameNS(NS_W11, "r");
21384
+ allRuns.push(...Array.from(insideRuns));
21385
+ }
21386
+ for (const run of allRuns) {
21387
+ const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
21388
+ const runText = Array.from(textNodes).map((t) => t.textContent).join("");
21389
+ if (runText.includes(targetText) || runText === targetText) {
21390
+ const rPrElements = run.getElementsByTagNameNS(NS_W11, "rPr");
21391
+ if (rPrElements.length > 0) {
21392
+ const rPr = rPrElements[0];
21393
+ const newRPr = removeFormattingFromRPr(rPr, formatTypes);
21394
+ if (newRPr) {
21395
+ if (rPr.parentNode) {
21396
+ rPr.parentNode.replaceChild(newRPr, rPr);
21397
+ }
21398
+ } else {
21399
+ removeNode2(rPr);
21400
+ }
21401
+ }
21402
+ }
21403
+ }
21404
+ return serializeOoxml(doc);
21405
+ }
21406
+ var HIGHLIGHT_COLOR_MAP = {
21407
+ "yellow": "yellow",
21408
+ "green": "green",
21409
+ "cyan": "cyan",
21410
+ "magenta": "magenta",
21411
+ "blue": "blue",
21412
+ "red": "red",
21413
+ "darkblue": "darkBlue",
21414
+ "darkcyan": "darkCyan",
21415
+ "darkgreen": "darkGreen",
21416
+ "darkmagenta": "darkMagenta",
21417
+ "darkred": "darkRed",
21418
+ "darkyellow": "darkYellow",
21419
+ "gray25": "lightGray",
21420
+ "gray50": "darkGray",
21421
+ "black": "black",
21422
+ "white": "white"
21423
+ };
21424
+ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
21425
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
21426
+ const ooxmlColor = HIGHLIGHT_COLOR_MAP[color.toLowerCase()] || "yellow";
21427
+ const generateRedlines = options?.generateRedlines ?? false;
21428
+ const author = options?.author || getDefaultAuthor();
21429
+ let rPrElement = rPr;
21430
+ if (!rPrElement) {
21431
+ rPrElement = createWordElement(doc, "w:rPr");
21432
+ } else {
21433
+ rPrElement = rPr.cloneNode(true);
21434
+ }
21435
+ let previousRPrState = null;
21436
+ if (generateRedlines) {
21437
+ previousRPrState = createWordElement(doc, "w:rPr");
21438
+ Array.from(rPrElement.childNodes).forEach((child) => {
21439
+ if (child.nodeName !== "w:rPrChange") {
21440
+ previousRPrState.appendChild(child.cloneNode(true));
21441
+ }
21442
+ });
21443
+ }
21444
+ const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W11, "highlight");
21445
+ Array.from(existingHighlight).forEach(removeNode2);
21446
+ const highlightEl = createWordElement(doc, "w:highlight");
21447
+ highlightEl.setAttributeNS(NS_W11, "w:val", ooxmlColor);
21448
+ rPrElement.appendChild(highlightEl);
21449
+ if (generateRedlines && previousRPrState) {
21450
+ const rPrChange = createWordElement(doc, "w:rPrChange");
21451
+ const metadata = createRevisionMetadata(author, doc);
21452
+ rPrChange.setAttribute("w:id", String(metadata.id));
21453
+ rPrChange.setAttribute("w:author", metadata.author);
21454
+ rPrChange.setAttribute("w:date", metadata.date);
21455
+ rPrChange.appendChild(previousRPrState);
21456
+ const existingChange = rPrElement.getElementsByTagNameNS(NS_W11, "rPrChange");
21457
+ Array.from(existingChange).forEach(removeNode2);
21458
+ rPrElement.appendChild(rPrChange);
21459
+ }
21460
+ return rPrElement;
21461
+ }
21462
+ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", options = {}) {
21463
+ if (!targetText || !ooxmlString) return ooxmlString;
21464
+ const doc = parseOoxml(ooxmlString);
21465
+ if (!doc) return ooxmlString;
21466
+ let revisionIdAllocator;
21467
+ if (options?._revisionIdAllocator instanceof RevisionIdAllocator) {
21468
+ revisionIdAllocator = options._revisionIdAllocator;
21469
+ seedRevisionIdsFromDocument(doc, revisionIdAllocator);
21470
+ } else {
21471
+ revisionIdAllocator = createRevisionIdAllocator(doc);
21472
+ }
21473
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
21474
+ const getRunText = (run) => {
21475
+ const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
21476
+ return Array.from(textNodes).map((t) => t.textContent).join("");
21477
+ };
21478
+ const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W11, "r"));
21479
+ const sourceRunsWithClaimedRevisionIds = /* @__PURE__ */ new WeakSet();
21480
+ const cloneRunWithText2 = (sourceRun, text2, shouldHighlight) => {
21481
+ const clonedRun = sourceRun.cloneNode(true);
21482
+ if (sourceRunsWithClaimedRevisionIds.has(sourceRun)) {
21483
+ refreshRunPropertyChangeIds(clonedRun, revisionIdAllocator);
21484
+ } else {
21485
+ sourceRunsWithClaimedRevisionIds.add(sourceRun);
21486
+ }
21487
+ const textNodes = clonedRun.getElementsByTagNameNS(NS_W11, "t");
21488
+ Array.from(textNodes).forEach(removeNode2);
21489
+ const newText = createWordElement(doc, "w:t");
21490
+ newText.setAttribute("xml:space", "preserve");
21491
+ newText.textContent = text2;
21492
+ clonedRun.appendChild(newText);
21493
+ if (shouldHighlight) {
21494
+ const rPrElements = clonedRun.getElementsByTagNameNS(NS_W11, "rPr");
21495
+ const existingRPr = rPrElements.length > 0 ? rPrElements[0] : null;
21496
+ const newRPr = injectHighlightIntoRPr(doc, existingRPr, color, options);
21497
+ if (existingRPr) {
21498
+ clonedRun.replaceChild(newRPr, existingRPr);
21499
+ } else {
21500
+ clonedRun.insertBefore(newRPr, clonedRun.firstChild);
21501
+ }
21502
+ }
21503
+ return clonedRun;
21504
+ };
21505
+ for (const run of allRuns) {
21506
+ const runText = getRunText(run);
21507
+ if (!runText) continue;
21508
+ const matchIndexes = [];
21509
+ let searchOffset = 0;
21510
+ while (searchOffset <= runText.length - targetText.length) {
21511
+ const matchIndex = runText.indexOf(targetText, searchOffset);
21512
+ if (matchIndex === -1) break;
21513
+ matchIndexes.push(matchIndex);
21514
+ searchOffset = matchIndex + targetText.length;
21515
+ }
21516
+ if (matchIndexes.length === 0) continue;
21517
+ const parent = run.parentNode;
21518
+ if (!parent) {
21519
+ console.warn("[Highlight] Run parent is null; skipping. Likely already processed.");
21520
+ continue;
21521
+ }
21522
+ const fragment = doc.createDocumentFragment();
21523
+ let cursor = 0;
21524
+ for (const matchIndex of matchIndexes) {
21525
+ if (matchIndex > cursor) {
21526
+ fragment.appendChild(cloneRunWithText2(run, runText.slice(cursor, matchIndex), false));
21527
+ }
21528
+ fragment.appendChild(cloneRunWithText2(
21529
+ run,
21530
+ runText.slice(matchIndex, matchIndex + targetText.length),
21531
+ true
21532
+ ));
21533
+ cursor = matchIndex + targetText.length;
21534
+ }
21535
+ if (cursor < runText.length) {
21536
+ fragment.appendChild(cloneRunWithText2(run, runText.slice(cursor), false));
21537
+ }
21538
+ parent.replaceChild(fragment, run);
21539
+ }
21540
+ return serializeOoxml(doc);
21541
+ }
21542
+
21451
21543
  // orchestration/route-plan.js
21452
21544
  var RoutePlanKind = Object.freeze({
21453
21545
  STRUCTURED_LIST_DIRECT: "structured_list_direct",
@@ -25152,7 +25244,7 @@ function deriveParagraphThenTableAppend(modifiedText) {
25152
25244
  const tableLines = lines.slice(1);
25153
25245
  if (!tableLines.every((line) => line.trim().startsWith("|"))) return null;
25154
25246
  const separator = tableLines[1].replace(/\s+/g, "");
25155
- if (!/^\|:?-{3,}:?(\|:?-{3,}:?)+\|?$/.test(separator)) return null;
25247
+ if (!/^\|:?-{3,}:?(?:(\|:?-{3,}:?)+\|?|\|)$/.test(separator)) return null;
25156
25248
  return { paragraphMarkdown: lines[0].trim(), tableText: tableLines.join("\n") };
25157
25249
  }
25158
25250
 
@@ -25782,7 +25874,10 @@ async function buildInsertedPlainParagraph(xmlDoc, anchorParagraph, text2, revis
25782
25874
  String(text2 || ""),
25783
25875
  {
25784
25876
  author,
25785
- generateRedlines: false
25877
+ generateRedlines: false,
25878
+ // A caller's structuredContent: false keeps inserted paragraphs plain too: no headings,
25879
+ // and inline Markdown markers stay literal.
25880
+ ...options.plainTextContent === true ? { structuredContent: false, plainTextContent: true } : {}
25786
25881
  }
25787
25882
  );
25788
25883
  let sourceParagraph = null;
@@ -27308,7 +27403,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
27308
27403
  author,
27309
27404
  // A wholly inserted paragraph must track its own paragraph mark; otherwise Word's
27310
27405
  // Reject All removes the text but leaves an empty paragraph behind.
27311
- { generateRedlines, trackParagraphMark: true }
27406
+ { generateRedlines, trackParagraphMark: true, plainTextContent: options.plainTextContent === true }
27312
27407
  );
27313
27408
  const imported = xmlDoc.importNode(plainParagraph, true);
27314
27409
  ensureParagraphIdsOnImportedNode(imported, operationSession);
@@ -27447,6 +27542,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
27447
27542
  existingRevisions: options.existingRevisions || "merge-same-author",
27448
27543
  structuredContent: options.structuredContent !== false,
27449
27544
  explicitStructuredContent: options.explicitStructuredContent === true,
27545
+ plainTextContent: options.plainTextContent === true,
27450
27546
  _revisionIdAllocator: revisionIdAllocator,
27451
27547
  _isolatedTableCell: useTableScope
27452
27548
  }) : await applyRedlineToOxml2(scopedXml, originalTextForApply, effectiveModifiedText, {