@ansonlai/docx-redline-js 0.1.3 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/AGENTS.md +91 -5
  2. package/ARCHITECTURE.md +62 -9
  3. package/README.md +94 -3
  4. package/core/types.js +35 -8
  5. package/core/word-xml.js +90 -0
  6. package/dist/docx-redline-js.esm.js +1149 -367
  7. package/dist/docx-redline-js.esm.js.map +4 -4
  8. package/dist/docx-redline-js.esm.min.js +78 -74
  9. package/dist/docx-redline-js.esm.min.js.map +4 -4
  10. package/docs/VALIDATION.md +48 -0
  11. package/docs/plans/2026-03-01-release-0.1.4-design.md +31 -0
  12. package/docs/plans/2026-03-01-release-0.1.4.md +108 -0
  13. package/engine/format-application.js +13 -14
  14. package/engine/format-span-application.js +7 -6
  15. package/engine/formatting-removal.js +15 -12
  16. package/engine/oxml-engine.js +146 -55
  17. package/engine/reconstruction-mapper.js +35 -8
  18. package/engine/reconstruction-mode.js +14 -13
  19. package/engine/reconstruction-writer.js +97 -78
  20. package/engine/rpr-helpers.js +34 -32
  21. package/engine/run-builders.js +150 -39
  22. package/engine/surgical-diff-application.js +216 -0
  23. package/engine/surgical-mode.js +84 -519
  24. package/engine/surgical-run-splitting.js +96 -0
  25. package/engine/surgical-spans.js +169 -0
  26. package/engine/table-cell-context.js +15 -13
  27. package/engine/table-mode.js +39 -35
  28. package/index.d.ts +148 -0
  29. package/index.js +26 -19
  30. package/package.json +8 -1
  31. package/pipeline/ingestion-export.js +1 -0
  32. package/pipeline/ingestion-paragraph.js +37 -12
  33. package/pipeline/ingestion-table.js +11 -8
  34. package/scripts/build.mjs +35 -0
  35. package/scripts/check-types.mjs +28 -0
  36. package/scripts/export-validation-fixtures.mjs +68 -0
  37. package/scripts/run-tests.mjs +43 -0
  38. package/scripts/word-com-smoke.ps1 +48 -0
  39. package/services/comment-locator.js +10 -9
  40. package/services/revision-comment-management.js +501 -0
  41. package/services/standalone-operation-runner.js +119 -69
  42. package/services/table-reconciliation.js +7 -8
@@ -1,4 +1,4 @@
1
- // @ansonlai/docx-redline-js v0.1.3 — https://github.com/AnsonLai/docx-redline-js
1
+ // @ansonlai/docx-redline-js v0.1.6 — 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;
@@ -1797,6 +1797,23 @@ function createRevisionMetadata(author) {
1797
1797
  date: getRevisionTimestamp()
1798
1798
  };
1799
1799
  }
1800
+ function seedRevisionIdsFromDocument(xmlDoc) {
1801
+ let maxFound = -1;
1802
+ const elements = Array.from(xmlDoc?.getElementsByTagName?.("*") || []);
1803
+ for (const element of elements) {
1804
+ for (const attr of Array.from(element.attributes || [])) {
1805
+ if ((attr.localName || "").toLowerCase() !== "id") continue;
1806
+ const parsed = Number.parseInt(attr.value, 10);
1807
+ if (Number.isFinite(parsed)) {
1808
+ maxFound = Math.max(maxFound, parsed);
1809
+ }
1810
+ }
1811
+ }
1812
+ if (maxFound >= revisionIdCounter) {
1813
+ revisionIdCounter = maxFound + 1;
1814
+ }
1815
+ return revisionIdCounter;
1816
+ }
1800
1817
 
1801
1818
  // core/paragraph-offset-policy.js
1802
1819
  var PARAGRAPH_BOUNDARY_TEXT = "\n";
@@ -2016,6 +2033,26 @@ function getNodeHandlers(runModel) {
2016
2033
  }
2017
2034
  return { offset, text: "" };
2018
2035
  });
2036
+ handlers.set("moveFrom", (child, offset) => {
2037
+ const deletionEntry = processDeletion(child, offset);
2038
+ if (deletionEntry) {
2039
+ runModel.push(deletionEntry);
2040
+ }
2041
+ return { offset, text: "" };
2042
+ });
2043
+ handlers.set("moveTo", (child, offset) => processNodeRecursive(child, offset, runModel));
2044
+ for (const markerName of ["moveFromRangeStart", "moveFromRangeEnd", "moveToRangeStart", "moveToRangeEnd"]) {
2045
+ handlers.set(markerName, (child, offset) => {
2046
+ runModel.push({
2047
+ kind: RunKind.BOOKMARK,
2048
+ nodeXml: serializeXml(child),
2049
+ startOffset: offset,
2050
+ endOffset: offset,
2051
+ text: ""
2052
+ });
2053
+ return { offset, text: "" };
2054
+ });
2055
+ }
2019
2056
  handlers.set("bookmarkStart", (child, offset) => {
2020
2057
  runModel.push({
2021
2058
  kind: RunKind.BOOKMARK,
@@ -2127,9 +2164,13 @@ function processDeletion(delElement, offset) {
2127
2164
  function ingestTableToVirtualGrid(tableNode) {
2128
2165
  const tblGrid = getFirstElementByTagNS(tableNode, NS_W, "tblGrid");
2129
2166
  const gridCols = tblGrid ? getElementsByTagNS(tblGrid, NS_W, "gridCol") : [];
2130
- const colCount = gridCols.length;
2131
- const trElements = getElementsByTagNS(tableNode, NS_W, "tr");
2167
+ const trElements = getElementsByTagNSOrTag(tableNode, NS_W, "tr");
2132
2168
  const rowCount = trElements.length;
2169
+ const inferredColCount = trElements.reduce((max, tr) => {
2170
+ const tcElements = getElementsByTagNSOrTag(tr, NS_W, "tc");
2171
+ return Math.max(max, tcElements.length);
2172
+ }, 0);
2173
+ const colCount = gridCols.length || inferredColCount;
2133
2174
  const grid = Array.from(
2134
2175
  { length: rowCount },
2135
2176
  () => Array.from({ length: colCount }, () => null)
@@ -2138,7 +2179,7 @@ function ingestTableToVirtualGrid(tableNode) {
2138
2179
  const vMergeOrigins = /* @__PURE__ */ new Map();
2139
2180
  for (let rowIdx = 0; rowIdx < trElements.length; rowIdx++) {
2140
2181
  const tr = trElements[rowIdx];
2141
- const tcElements = getElementsByTagNS(tr, NS_W, "tc");
2182
+ const tcElements = getElementsByTagNSOrTag(tr, NS_W, "tc");
2142
2183
  let gridCol = 0;
2143
2184
  for (let tcIdx = 0; tcIdx < tcElements.length; tcIdx++) {
2144
2185
  const tc = tcElements[tcIdx];
@@ -3243,8 +3284,7 @@ var NumberingService = class {
3243
3284
  // services/table-reconciliation.js
3244
3285
  function generateTableOoxml(tableData, options = {}) {
3245
3286
  const { generateRedlines = false, author = "AI" } = options;
3246
- const date = getRevisionTimestamp();
3247
- const revId = generateRedlines ? getNextRevisionId() : null;
3287
+ const tableInsertMeta = generateRedlines ? createRevisionMetadata(author) : null;
3248
3288
  const numCols = tableData.headers?.length || (tableData.rows?.[0]?.length || 1);
3249
3289
  const tblPr = `
3250
3290
  <w:tblPr>
@@ -3287,8 +3327,8 @@ function generateTableOoxml(tableData, options = {}) {
3287
3327
  rowsXml += `<w:tr>${trPr}${cellsXml}</w:tr>`;
3288
3328
  }
3289
3329
  let tableXml = `<w:tbl>${tblPr}${tblGrid}${rowsXml}</w:tbl>`;
3290
- if (generateRedlines && revId) {
3291
- tableXml = `<w:ins w:id="${revId}" w:author="${escapeXml(author)}" w:date="${date}">${tableXml}</w:ins>`;
3330
+ if (tableInsertMeta) {
3331
+ tableXml = `<w:ins w:id="${tableInsertMeta.id}" w:author="${escapeXml(tableInsertMeta.author)}" w:date="${tableInsertMeta.date}">${tableXml}</w:ins>`;
3292
3332
  }
3293
3333
  return tableXml;
3294
3334
  }
@@ -3931,6 +3971,55 @@ var ReconciliationPipeline = class {
3931
3971
  }
3932
3972
  };
3933
3973
 
3974
+ // core/word-xml.js
3975
+ function isWordElement(node, localName) {
3976
+ if (!node || node.nodeType !== 1) return false;
3977
+ if (node.namespaceURI === NS_W && node.localName === localName) return true;
3978
+ const nodeName = String(node.nodeName || "");
3979
+ return nodeName === `w:${localName}` || nodeName === localName;
3980
+ }
3981
+ function createWordElement(xmlDoc, qualifiedName) {
3982
+ return typeof xmlDoc.createElementNS === "function" ? xmlDoc.createElementNS(NS_W, qualifiedName) : xmlDoc.createElement(qualifiedName);
3983
+ }
3984
+ function wordElementsByLocalName(xmlDoc, localName) {
3985
+ const namespaced = Array.from(xmlDoc?.getElementsByTagNameNS?.(NS_W, localName) || []);
3986
+ if (namespaced.length > 0) return namespaced;
3987
+ return Array.from(xmlDoc?.getElementsByTagName?.("*") || []).filter((node) => isWordElement(node, localName));
3988
+ }
3989
+ function containsTrackedChanges(xmlDoc) {
3990
+ const trackedChangeNames = [
3991
+ "ins",
3992
+ "del",
3993
+ "moveFrom",
3994
+ "moveTo",
3995
+ "moveFromRangeStart",
3996
+ "moveFromRangeEnd",
3997
+ "moveToRangeStart",
3998
+ "moveToRangeEnd",
3999
+ "rPrChange",
4000
+ "pPrChange",
4001
+ "cellIns",
4002
+ "cellDel"
4003
+ ];
4004
+ return trackedChangeNames.some((localName) => wordElementsByLocalName(xmlDoc, localName).length > 0);
4005
+ }
4006
+ function classifyOoxmlSourceType(oxml) {
4007
+ const trimmed = String(oxml || "").trim();
4008
+ if (/^<\?xml\b[^>]*>\s*<pkg:package\b/i.test(trimmed) || /^<pkg:package\b/i.test(trimmed)) {
4009
+ return "package";
4010
+ }
4011
+ if (/^<\?xml\b[^>]*>\s*<(?:w:)?document\b/i.test(trimmed) || /^<(?:w:)?document\b/i.test(trimmed)) {
4012
+ return "document";
4013
+ }
4014
+ return "fragment";
4015
+ }
4016
+ function withOoxmlSourceType(result) {
4017
+ if (!result || typeof result !== "object" || result.sourceType || typeof result.oxml !== "string") {
4018
+ return result;
4019
+ }
4020
+ return { ...result, sourceType: classifyOoxmlSourceType(result.oxml) };
4021
+ }
4022
+
3934
4023
  // engine/rpr-helpers.js
3935
4024
  var RPR_SCHEMA_ORDER = [
3936
4025
  "w:rStyle",
@@ -4018,28 +4107,28 @@ function _applyOverrides(xmlDoc, rPr, formatFlags, mode) {
4018
4107
  }
4019
4108
  }
4020
4109
  if (applyBold) {
4021
- const b = xmlDoc.createElement("w:b");
4110
+ const b = createWordElement(xmlDoc, "w:b");
4022
4111
  b.setAttribute("w:val", mode === "add" ? "1" : "0");
4023
4112
  insertRPrChildInOrder(rPr, b);
4024
- const bCs = xmlDoc.createElement("w:bCs");
4113
+ const bCs = createWordElement(xmlDoc, "w:bCs");
4025
4114
  bCs.setAttribute("w:val", mode === "add" ? "1" : "0");
4026
4115
  insertRPrChildInOrder(rPr, bCs);
4027
4116
  }
4028
4117
  if (applyItalic) {
4029
- const i = xmlDoc.createElement("w:i");
4118
+ const i = createWordElement(xmlDoc, "w:i");
4030
4119
  i.setAttribute("w:val", mode === "add" ? "1" : "0");
4031
4120
  insertRPrChildInOrder(rPr, i);
4032
- const iCs = xmlDoc.createElement("w:iCs");
4121
+ const iCs = createWordElement(xmlDoc, "w:iCs");
4033
4122
  iCs.setAttribute("w:val", mode === "add" ? "1" : "0");
4034
4123
  insertRPrChildInOrder(rPr, iCs);
4035
4124
  }
4036
4125
  if (applyUnderline) {
4037
- const u = xmlDoc.createElement("w:u");
4126
+ const u = createWordElement(xmlDoc, "w:u");
4038
4127
  u.setAttribute("w:val", mode === "add" ? "single" : "none");
4039
4128
  insertRPrChildInOrder(rPr, u);
4040
4129
  }
4041
4130
  if (applyStrike) {
4042
- const strike = xmlDoc.createElement("w:strike");
4131
+ const strike = createWordElement(xmlDoc, "w:strike");
4043
4132
  strike.setAttribute("w:val", mode === "add" ? "1" : "0");
4044
4133
  insertRPrChildInOrder(rPr, strike);
4045
4134
  }
@@ -4080,7 +4169,7 @@ function isFormattingElementEnabled(element, isUnderline) {
4080
4169
 
4081
4170
  // engine/format-extraction.js
4082
4171
  var NS_W3 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
4083
- function isWordElement(node, localName) {
4172
+ function isWordElement2(node, localName) {
4084
4173
  if (!node || node.nodeType !== 1) return false;
4085
4174
  if (node.namespaceURI === NS_W3 && node.localName === localName) return true;
4086
4175
  const nodeName = String(node.nodeName || "");
@@ -4097,7 +4186,7 @@ function collectParagraphRuns(paragraph) {
4097
4186
  const node = stack.pop();
4098
4187
  if (!node || node.nodeType !== 1) continue;
4099
4188
  if (isExcludedRevisionContainer(node)) continue;
4100
- if (isWordElement(node, "r")) {
4189
+ if (isWordElement2(node, "r")) {
4101
4190
  runs.push(node);
4102
4191
  continue;
4103
4192
  }
@@ -4132,7 +4221,7 @@ function buildTextSpansFromParagraphs(paragraphs) {
4132
4221
  for (const run of runs) {
4133
4222
  const rPr = getFirstElementByTag(run, "w:rPr");
4134
4223
  Array.from(run.childNodes || []).forEach((rc) => {
4135
- if (isWordElement(rc, "t")) {
4224
+ if (isWordElement2(rc, "t")) {
4136
4225
  const text = rc.textContent || "";
4137
4226
  if (text.length > 0) {
4138
4227
  textSpans.push({
@@ -4146,7 +4235,7 @@ function buildTextSpansFromParagraphs(paragraphs) {
4146
4235
  });
4147
4236
  charOffset += text.length;
4148
4237
  }
4149
- } else if (isWordElement(rc, "br") || isWordElement(rc, "cr") || isWordElement(rc, "tab") || isWordElement(rc, "noBreakHyphen")) {
4238
+ } else if (isWordElement2(rc, "br") || isWordElement2(rc, "cr") || isWordElement2(rc, "tab") || isWordElement2(rc, "noBreakHyphen")) {
4150
4239
  textSpans.push({
4151
4240
  charStart: charOffset,
4152
4241
  charEnd: charOffset + 1,
@@ -4167,7 +4256,7 @@ function buildTextSpansFromParagraphs(paragraphs) {
4167
4256
  function processRunForFormatting(run, paragraph, charOffset, textSpans, formatHints, pFormat = null) {
4168
4257
  let rPr = null;
4169
4258
  for (const child of Array.from(run.childNodes)) {
4170
- if (isWordElement(child, "rPr")) {
4259
+ if (isWordElement2(child, "rPr")) {
4171
4260
  rPr = child;
4172
4261
  break;
4173
4262
  }
@@ -4182,7 +4271,7 @@ function processRunForFormatting(run, paragraph, charOffset, textSpans, formatHi
4182
4271
  format.hasFormatting = format.bold || format.italic || format.underline || format.strikethrough;
4183
4272
  let currentOffset = charOffset;
4184
4273
  for (const child of Array.from(run.childNodes)) {
4185
- if (isWordElement(child, "t")) {
4274
+ if (isWordElement2(child, "t")) {
4186
4275
  const text = child.textContent || "";
4187
4276
  if (text.length > 0) {
4188
4277
  const start = currentOffset;
@@ -4220,9 +4309,9 @@ function extractFormattingFromOoxml(xmlDoc) {
4220
4309
  const p = paragraphs[pIndex];
4221
4310
  let pRPr = null;
4222
4311
  for (const child of Array.from(p.childNodes)) {
4223
- if (isWordElement(child, "pPr")) {
4312
+ if (isWordElement2(child, "pPr")) {
4224
4313
  for (const pChild of Array.from(child.childNodes)) {
4225
- if (isWordElement(pChild, "rPr")) {
4314
+ if (isWordElement2(pChild, "rPr")) {
4226
4315
  pRPr = pChild;
4227
4316
  break;
4228
4317
  }
@@ -4246,7 +4335,7 @@ function extractFormattingFromOoxml(xmlDoc) {
4246
4335
 
4247
4336
  // engine/run-builders.js
4248
4337
  function createTrackChange(xmlDoc, type, run, author) {
4249
- const wrapper = xmlDoc.createElement(type === "ins" ? "w:ins" : "w:del");
4338
+ const wrapper = createWordElement(xmlDoc, type === "ins" ? "w:ins" : "w:del");
4250
4339
  const metadata = createRevisionMetadata(author);
4251
4340
  wrapper.setAttribute("w:id", String(metadata.id));
4252
4341
  wrapper.setAttribute("w:author", metadata.author);
@@ -4256,10 +4345,60 @@ function createTrackChange(xmlDoc, type, run, author) {
4256
4345
  }
4257
4346
  return wrapper;
4258
4347
  }
4348
+ function getDirectWordChild(node, localName) {
4349
+ return Array.from(node?.childNodes || []).find((child) => child.nodeType === 1 && (child.localName === localName || child.nodeName === `w:${localName}`)) || null;
4350
+ }
4351
+ function ensureParagraphProperties(xmlDoc, paragraph) {
4352
+ let pPr = getDirectWordChild(paragraph, "pPr");
4353
+ if (!pPr) {
4354
+ pPr = createWordElement(xmlDoc, "w:pPr");
4355
+ paragraph.insertBefore(pPr, paragraph.firstChild || null);
4356
+ } else if (paragraph.firstChild !== pPr) {
4357
+ paragraph.insertBefore(pPr, paragraph.firstChild || null);
4358
+ }
4359
+ return pPr;
4360
+ }
4361
+ function ensureParagraphMarkRunProperties(xmlDoc, pPr) {
4362
+ let rPr = getDirectWordChild(pPr, "rPr");
4363
+ if (!rPr) {
4364
+ rPr = createWordElement(xmlDoc, "w:rPr");
4365
+ pPr.appendChild(rPr);
4366
+ } else if (pPr.lastChild !== rPr) {
4367
+ pPr.appendChild(rPr);
4368
+ }
4369
+ return rPr;
4370
+ }
4371
+ function markParagraphMark(xmlDoc, paragraph, author, type) {
4372
+ const pPr = ensureParagraphProperties(xmlDoc, paragraph);
4373
+ const rPr = ensureParagraphMarkRunProperties(xmlDoc, pPr);
4374
+ for (const child of Array.from(rPr.childNodes || [])) {
4375
+ if (child.nodeType !== 1) continue;
4376
+ if (child.localName === "ins" || child.localName === "del" || child.nodeName === "w:ins" || child.nodeName === "w:del") {
4377
+ rPr.removeChild(child);
4378
+ }
4379
+ }
4380
+ const marker = createWordElement(xmlDoc, type === "ins" ? "w:ins" : "w:del");
4381
+ const metadata = createRevisionMetadata(author);
4382
+ marker.setAttribute("w:id", String(metadata.id));
4383
+ marker.setAttribute("w:author", metadata.author);
4384
+ marker.setAttribute("w:date", metadata.date);
4385
+ rPr.appendChild(marker);
4386
+ return marker;
4387
+ }
4388
+ function markParagraphMarkInserted(xmlDoc, paragraph, author) {
4389
+ return markParagraphMark(xmlDoc, paragraph, author, "ins");
4390
+ }
4391
+ function markParagraphMarkDeleted(xmlDoc, paragraph, author) {
4392
+ return markParagraphMark(xmlDoc, paragraph, author, "del");
4393
+ }
4259
4394
  function createTextRun(xmlDoc, text, rPr, isDelete) {
4260
- const run = xmlDoc.createElement("w:r");
4395
+ const run = createWordElement(xmlDoc, "w:r");
4261
4396
  if (rPr) run.appendChild(rPr.cloneNode(true));
4262
- const textEl = xmlDoc.createElement(isDelete ? "w:delText" : "w:t");
4397
+ if (!isDelete) {
4398
+ appendVisibleTextPieces(xmlDoc, run, text);
4399
+ return run;
4400
+ }
4401
+ const textEl = createWordElement(xmlDoc, isDelete ? "w:delText" : "w:t");
4263
4402
  textEl.setAttribute("xml:space", "preserve");
4264
4403
  textEl.textContent = text;
4265
4404
  run.appendChild(textEl);
@@ -4296,16 +4435,45 @@ function createFormattedRuns(xmlDoc, text, baseRPr, formatHints, baseOffset, aut
4296
4435
  return runs;
4297
4436
  }
4298
4437
  function createTextRunWithRPrElement(xmlDoc, text, rPrElement, isDelete) {
4299
- const run = xmlDoc.createElement("w:r");
4438
+ const run = createWordElement(xmlDoc, "w:r");
4300
4439
  if (rPrElement) run.appendChild(rPrElement);
4301
- const textEl = xmlDoc.createElement(isDelete ? "w:delText" : "w:t");
4440
+ if (!isDelete) {
4441
+ appendVisibleTextPieces(xmlDoc, run, text);
4442
+ return run;
4443
+ }
4444
+ const textEl = createWordElement(xmlDoc, isDelete ? "w:delText" : "w:t");
4302
4445
  textEl.setAttribute("xml:space", "preserve");
4303
4446
  textEl.textContent = text;
4304
4447
  run.appendChild(textEl);
4305
4448
  return run;
4306
4449
  }
4450
+ function appendVisibleTextPieces(xmlDoc, run, text) {
4451
+ const source = String(text || "");
4452
+ const parts = source.split(/(\t|\n|\u2011)/);
4453
+ for (const part of parts) {
4454
+ if (!part) continue;
4455
+ if (part === " ") {
4456
+ run.appendChild(createWordElement(xmlDoc, "w:tab"));
4457
+ continue;
4458
+ }
4459
+ if (part === "\n") {
4460
+ run.appendChild(createWordElement(xmlDoc, "w:br"));
4461
+ continue;
4462
+ }
4463
+ if (part === "\u2011") {
4464
+ run.appendChild(createWordElement(xmlDoc, "w:noBreakHyphen"));
4465
+ continue;
4466
+ }
4467
+ const textEl = createWordElement(xmlDoc, "w:t");
4468
+ if (/^\s|\s$/.test(part)) {
4469
+ textEl.setAttribute("xml:space", "preserve");
4470
+ }
4471
+ textEl.textContent = part;
4472
+ run.appendChild(textEl);
4473
+ }
4474
+ }
4307
4475
  function injectFormattingToRPr(xmlDoc, baseRPr, format, author, generateRedlines) {
4308
- const rPr = xmlDoc.createElement("w:rPr");
4476
+ const rPr = createWordElement(xmlDoc, "w:rPr");
4309
4477
  if (baseRPr) {
4310
4478
  Array.from(baseRPr.childNodes).forEach((child) => {
4311
4479
  if (!["w:b", "w:bCs", "w:i", "w:iCs", "w:u", "w:strike", "w:rPrChange"].includes(child.nodeName)) {
@@ -4318,7 +4486,7 @@ function injectFormattingToRPr(xmlDoc, baseRPr, format, author, generateRedlines
4318
4486
  createRPrChange(xmlDoc, rPr, author, baseRPr);
4319
4487
  }
4320
4488
  const syncElement = (tagName, isOn, valOn = null, valOff = "0") => {
4321
- const el = xmlDoc.createElement(tagName);
4489
+ const el = createWordElement(xmlDoc, tagName);
4322
4490
  if (isOn) {
4323
4491
  if (valOn) el.setAttribute("w:val", valOn);
4324
4492
  } else if (valOff) {
@@ -4348,11 +4516,12 @@ function injectFormattingToRPr(xmlDoc, baseRPr, format, author, generateRedlines
4348
4516
  return rPr;
4349
4517
  }
4350
4518
  function snapshotAndAttachRPrChange(xmlDoc, rPr, author, dateStr, sourceNode) {
4351
- const rPrChange = xmlDoc.createElement("w:rPrChange");
4352
- rPrChange.setAttribute("w:id", String(createRevisionMetadata(author).id));
4353
- rPrChange.setAttribute("w:author", author);
4354
- rPrChange.setAttribute("w:date", dateStr);
4355
- const previousRPr = xmlDoc.createElement("w:rPr");
4519
+ const rPrChange = createWordElement(xmlDoc, "w:rPrChange");
4520
+ const metadata = createRevisionMetadata(author);
4521
+ rPrChange.setAttribute("w:id", String(metadata.id));
4522
+ rPrChange.setAttribute("w:author", metadata.author);
4523
+ rPrChange.setAttribute("w:date", dateStr || metadata.date);
4524
+ const previousRPr = createWordElement(xmlDoc, "w:rPr");
4356
4525
  const source = sourceNode || rPr;
4357
4526
  Array.from(source.childNodes).forEach((child) => {
4358
4527
  if (child.nodeName !== "w:rPrChange") {
@@ -4368,12 +4537,12 @@ function snapshotAndAttachRPrChange(xmlDoc, rPr, author, dateStr, sourceNode) {
4368
4537
  return rPrChange;
4369
4538
  }
4370
4539
  function createRPrChange(xmlDoc, rPr, author, previousRPrArg) {
4371
- snapshotAndAttachRPrChange(xmlDoc, rPr, author, getRevisionTimestamp(), previousRPrArg || rPr);
4540
+ snapshotAndAttachRPrChange(xmlDoc, rPr, author, null, previousRPrArg || rPr);
4372
4541
  }
4373
4542
 
4374
4543
  // engine/format-paragraph-targeting.js
4375
4544
  var NS_W4 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
4376
- function isWordElement2(node, localName) {
4545
+ function isWordElement3(node, localName) {
4377
4546
  if (!node || node.nodeType !== 1) return false;
4378
4547
  if (node.namespaceURI === NS_W4 && node.localName === localName) return true;
4379
4548
  const nodeName = String(node.nodeName || "");
@@ -4475,7 +4644,7 @@ function findTargetParagraphInfo(paragraphInfos, originalText) {
4475
4644
  function getContainingParagraph(node) {
4476
4645
  let current = node;
4477
4646
  while (current) {
4478
- if (isWordElement2(current, "p")) return current;
4647
+ if (isWordElement3(current, "p")) return current;
4479
4648
  current = current.parentNode;
4480
4649
  }
4481
4650
  return null;
@@ -4486,13 +4655,13 @@ function buildParagraphTextFromSpans(spans) {
4486
4655
  for (const span of spans) {
4487
4656
  if (!span || !span.textElement) continue;
4488
4657
  const textElement = span.textElement;
4489
- if (isWordElement2(textElement, "t")) {
4658
+ if (isWordElement3(textElement, "t")) {
4490
4659
  text += span.textElement.textContent || "";
4491
- } else if (isWordElement2(textElement, "tab")) {
4660
+ } else if (isWordElement3(textElement, "tab")) {
4492
4661
  text += " ";
4493
- } else if (isWordElement2(textElement, "br") || isWordElement2(textElement, "cr")) {
4662
+ } else if (isWordElement3(textElement, "br") || isWordElement3(textElement, "cr")) {
4494
4663
  text += "\n";
4495
- } else if (isWordElement2(textElement, "noBreakHyphen")) {
4664
+ } else if (isWordElement3(textElement, "noBreakHyphen")) {
4496
4665
  text += "\u2011";
4497
4666
  }
4498
4667
  }
@@ -4556,7 +4725,7 @@ function splitSpanAtOffset(xmlDoc, span, absoluteOffset) {
4556
4725
 
4557
4726
  // engine/format-application.js
4558
4727
  var NS_W5 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
4559
- function isWordElement3(node, localName) {
4728
+ function isWordElement4(node, localName) {
4560
4729
  if (!node || node.nodeType !== 1) return false;
4561
4730
  if (node.namespaceURI === NS_W5 && node.localName === localName) return true;
4562
4731
  const nodeName = String(node.nodeName || "");
@@ -4586,7 +4755,6 @@ function normalizePrecomputedFormatContext(precomputedContext) {
4586
4755
  function applyFormatRemovalAsSurgicalReplacement(xmlDoc, textSpans, existingFormatHints, serializer, author, generateRedlines = true) {
4587
4756
  let hasAnyChanges = false;
4588
4757
  const processedRuns = /* @__PURE__ */ new Set();
4589
- const dateStr = getRevisionTimestamp();
4590
4758
  log(`[OxmlEngine] Surgical format removal: ${existingFormatHints.length} hints to process (using w:rPrChange)`);
4591
4759
  for (const hint of existingFormatHints) {
4592
4760
  const run = hint.run;
@@ -4596,11 +4764,11 @@ function applyFormatRemovalAsSurgicalReplacement(xmlDoc, textSpans, existingForm
4596
4764
  log("[OxmlEngine] Processing run for surgical format removal, format:", hint.format);
4597
4765
  let rPr = getFirstElementByTag(run, "w:rPr");
4598
4766
  if (!rPr) {
4599
- rPr = xmlDoc.createElement("w:rPr");
4767
+ rPr = createWordElement(xmlDoc, "w:rPr");
4600
4768
  run.insertBefore(rPr, run.firstChild);
4601
4769
  }
4602
4770
  if (generateRedlines) {
4603
- snapshotAndAttachRPrChange(xmlDoc, rPr, author || getDefaultAuthor(), dateStr);
4771
+ snapshotAndAttachRPrChange(xmlDoc, rPr, author || getDefaultAuthor());
4604
4772
  }
4605
4773
  applyFormatOverridesToRPr(xmlDoc, rPr, hint.format);
4606
4774
  hasAnyChanges = true;
@@ -4626,7 +4794,7 @@ function applyFormatAdditionsAsSurgicalReplacement(xmlDoc, textSpans, formatHint
4626
4794
  const orderedSpans = currentSpans.slice().sort((a, b) => a.charStart - b.charStart || a.charEnd - b.charEnd);
4627
4795
  const getOverlappingHints = createFormatHintOverlapLookup(formatHints);
4628
4796
  for (const span of orderedSpans) {
4629
- if (!span || !span.textElement || !isWordElement3(span.textElement, "t")) continue;
4797
+ if (!span || !span.textElement || !isWordElement4(span.textElement, "t")) continue;
4630
4798
  const applicableHints = getOverlappingHints(span.charStart, span.charEnd);
4631
4799
  if (applicableHints.length === 0) continue;
4632
4800
  const mergedDesiredFormat = mergeFormats(...applicableHints.map((h) => h.format));
@@ -4746,7 +4914,7 @@ function createFormatHintOverlapLookup(formatHints) {
4746
4914
  var W14_NS = "http://schemas.microsoft.com/office/word/2010/wordml";
4747
4915
  function detectTableCellContext(xmlDoc, originalText, options = {}) {
4748
4916
  const { targetParagraphId = null } = options;
4749
- const tables = getElementsByTag(xmlDoc, "w:tbl");
4917
+ const tables = getElementsByTagNSOrTag(xmlDoc, NS_W, "tbl");
4750
4918
  if (tables.length === 0) {
4751
4919
  return { hasTableWrapper: false, isTableCellParagraph: false, paragraphs: [], paragraph: null, tableElement: null };
4752
4920
  }
@@ -4754,7 +4922,7 @@ function detectTableCellContext(xmlDoc, originalText, options = {}) {
4754
4922
  const paragraphsInCells = allParagraphs.filter((p) => {
4755
4923
  let parent = p.parentNode;
4756
4924
  while (parent) {
4757
- if (parent.nodeName === "w:tc") return true;
4925
+ if (isWordElement(parent, "tc")) return true;
4758
4926
  parent = parent.parentNode;
4759
4927
  }
4760
4928
  return false;
@@ -4777,7 +4945,7 @@ function detectTableCellContext(xmlDoc, originalText, options = {}) {
4777
4945
  const normalizedTarget = originalText.trim();
4778
4946
  if (!targetParagraph) {
4779
4947
  for (const p of paragraphsInCells) {
4780
- const textNodes = getElementsByTag(p, "w:t");
4948
+ const textNodes = getElementsByTagNSOrTag(p, NS_W, "t");
4781
4949
  let paragraphText = "";
4782
4950
  for (const t of textNodes) {
4783
4951
  paragraphText += t.textContent || "";
@@ -4824,89 +4992,247 @@ function getParagraphId(paragraph) {
4824
4992
  return paragraph.getAttribute("w14:paraId") || paragraph.getAttribute("w:paraId") || paragraph.getAttribute("paraId") || null;
4825
4993
  }
4826
4994
 
4827
- // engine/surgical-mode.js
4828
- function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, author, formatHints, generateRedlines = true, targetParagraph = null) {
4995
+ // engine/surgical-spans.js
4996
+ function getRunChildText(child) {
4997
+ if (isWordElement(child, "t")) return child.textContent || "";
4998
+ if (isWordElement(child, "br") || isWordElement(child, "cr")) return "\n";
4999
+ if (isWordElement(child, "tab")) return " ";
5000
+ if (isWordElement(child, "noBreakHyphen")) return "\u2011";
5001
+ return "";
5002
+ }
5003
+ function isTextLikeRunChild(child) {
5004
+ return isWordElement(child, "t") || isWordElement(child, "br") || isWordElement(child, "cr") || isWordElement(child, "tab") || isWordElement(child, "noBreakHyphen");
5005
+ }
5006
+ function buildSurgicalTextSpans(paragraphs) {
4829
5007
  let fullText = "";
4830
5008
  const textSpans = [];
4831
- const allParagraphs = targetParagraph ? [targetParagraph] : getDocumentParagraphs(xmlDoc);
4832
- allParagraphs.forEach((p, pIndex) => {
4833
- const container = p.parentNode;
4834
- for (let child = p.firstChild; child; child = child.nextSibling) {
4835
- if (child.nodeName === "w:r") {
4836
- const runResult = processRunElement(child, p, container, fullText.length, textSpans);
4837
- fullText += runResult.text;
4838
- } else if (child.nodeName === "w:hyperlink") {
5009
+ paragraphs.forEach((paragraph, paragraphIndex) => {
5010
+ const container = paragraph.parentNode;
5011
+ for (let child = paragraph.firstChild; child; child = child.nextSibling) {
5012
+ if (isWordElement(child, "r")) {
5013
+ fullText += processRunElement(child, paragraph, container, fullText.length, textSpans).text;
5014
+ } else if (isWordElement(child, "hyperlink")) {
4839
5015
  for (let hc = child.firstChild; hc; hc = hc.nextSibling) {
4840
- if (hc.nodeName === "w:r") {
4841
- const runResult = processRunElement(hc, p, container, fullText.length, textSpans);
4842
- fullText += runResult.text;
5016
+ if (isWordElement(hc, "r")) {
5017
+ fullText += processRunElement(hc, paragraph, container, fullText.length, textSpans).text;
4843
5018
  }
4844
5019
  }
4845
5020
  }
4846
5021
  }
4847
- fullText = appendParagraphBoundary(fullText, pIndex, allParagraphs.length);
5022
+ fullText = appendParagraphBoundary(fullText, paragraphIndex, paragraphs.length);
4848
5023
  });
4849
- const diffs = computeWordDiffs(fullText, modifiedText);
4850
- const spanIndex = buildSpanIndex(textSpans);
4851
- let originalPos = 0;
4852
- let newPos = 0;
4853
- const processedSpans = /* @__PURE__ */ new Set();
4854
- for (const [op, text] of diffs) {
4855
- if (op === 0) {
4856
- const len = text.length;
4857
- const startPos = originalPos;
4858
- const endPos = originalPos + len;
4859
- forEachOverlappingSpan(spanIndex, startPos, endPos, (span) => {
4860
- const overlapStartOriginal = Math.max(span.charStart, startPos);
4861
- const overlapEndOriginal = Math.min(span.charEnd, endPos);
4862
- const segmentLen = overlapEndOriginal - overlapStartOriginal;
4863
- const relativeOffset = overlapStartOriginal - startPos;
4864
- const overlapStartNew = newPos + relativeOffset;
4865
- const overlapEndNew = overlapStartNew + segmentLen;
4866
- const applicableHints = getApplicableFormatHints(formatHints, overlapStartNew, overlapEndNew);
4867
- reconcileFormattingForTextSpan(xmlDoc, span, overlapStartOriginal, overlapEndOriginal, applicableHints, author, generateRedlines);
5024
+ return { fullText, textSpans };
5025
+ }
5026
+ function processRunElement(run, paragraph, container, currentOffset, textSpans) {
5027
+ const rPr = getFirstElementByTag(run, "w:rPr");
5028
+ let localOffset = currentOffset;
5029
+ const textParts = [];
5030
+ for (let child = run.firstChild; child; child = child.nextSibling) {
5031
+ if (isWordElement(child, "t")) {
5032
+ const text = child.textContent || "";
5033
+ if (text.length === 0) continue;
5034
+ textSpans.push({
5035
+ charStart: localOffset,
5036
+ charEnd: localOffset + text.length,
5037
+ textElement: child,
5038
+ runElement: run,
5039
+ paragraph,
5040
+ container,
5041
+ rPr
4868
5042
  });
4869
- originalPos += len;
4870
- newPos += len;
4871
- } else if (op === -1) {
4872
- processDelete(xmlDoc, spanIndex, originalPos, originalPos + text.length, processedSpans, author, generateRedlines);
4873
- originalPos += text.length;
4874
- } else if (op === 1) {
4875
- const textWithoutNewlines = text.replace(/\n/g, " ");
4876
- if (textWithoutNewlines.trim().length > 0) {
4877
- processInsert(xmlDoc, spanIndex, originalPos, textWithoutNewlines, processedSpans, author, formatHints, newPos, generateRedlines);
4878
- }
4879
- newPos += text.length;
5043
+ localOffset += text.length;
5044
+ textParts.push(text);
5045
+ } else if (isTextLikeRunChild(child)) {
5046
+ const text = getRunChildText(child);
5047
+ textSpans.push({
5048
+ charStart: localOffset,
5049
+ charEnd: localOffset + 1,
5050
+ textElement: child,
5051
+ runElement: run,
5052
+ paragraph,
5053
+ container,
5054
+ rPr
5055
+ });
5056
+ localOffset += 1;
5057
+ textParts.push(text);
4880
5058
  }
4881
5059
  }
4882
- return { oxml: serializer.serializeToString(xmlDoc), hasChanges: true };
5060
+ return { text: textParts.join("") };
5061
+ }
5062
+ function buildSpanIndex(textSpans) {
5063
+ const spans = textSpans.slice().sort((a, b) => a.charStart - b.charStart || a.charEnd - b.charEnd);
5064
+ const starts = spans.map((span) => span.charStart);
5065
+ const ends = spans.map((span) => span.charEnd);
5066
+ return { spans, starts, ends };
5067
+ }
5068
+ function forEachOverlappingSpan(spanIndex, startPos, endPos, callback) {
5069
+ if (endPos <= startPos || spanIndex.spans.length === 0) {
5070
+ return;
5071
+ }
5072
+ let index = upperBound(spanIndex.ends, startPos);
5073
+ while (index < spanIndex.spans.length) {
5074
+ const span = spanIndex.spans[index];
5075
+ if (span.charStart >= endPos) {
5076
+ break;
5077
+ }
5078
+ callback(span);
5079
+ index++;
5080
+ }
5081
+ }
5082
+ function findContainingSpan(spanIndex, pos) {
5083
+ if (spanIndex.spans.length === 0) return null;
5084
+ const index = upperBound(spanIndex.starts, pos) - 1;
5085
+ if (index < 0) return null;
5086
+ const span = spanIndex.spans[index];
5087
+ return pos >= span.charStart && pos < span.charEnd ? span : null;
5088
+ }
5089
+ function findFirstSpanEndingAt(spanIndex, pos) {
5090
+ const index = lowerBound(spanIndex.ends, pos);
5091
+ if (index < spanIndex.spans.length && spanIndex.ends[index] === pos) {
5092
+ return spanIndex.spans[index];
5093
+ }
5094
+ return null;
5095
+ }
5096
+ function findLastSpanEndingBeforeOrAt(spanIndex, pos) {
5097
+ const index = upperBound(spanIndex.ends, pos) - 1;
5098
+ if (index >= 0) {
5099
+ return spanIndex.spans[index];
5100
+ }
5101
+ return null;
5102
+ }
5103
+ function upperBound(values, target) {
5104
+ let left = 0;
5105
+ let right = values.length;
5106
+ while (left < right) {
5107
+ const middle = left + right >> 1;
5108
+ if (values[middle] <= target) {
5109
+ left = middle + 1;
5110
+ } else {
5111
+ right = middle;
5112
+ }
5113
+ }
5114
+ return left;
4883
5115
  }
5116
+ function lowerBound(values, target) {
5117
+ let left = 0;
5118
+ let right = values.length;
5119
+ while (left < right) {
5120
+ const middle = left + right >> 1;
5121
+ if (values[middle] < target) {
5122
+ left = middle + 1;
5123
+ } else {
5124
+ right = middle;
5125
+ }
5126
+ }
5127
+ return left;
5128
+ }
5129
+
5130
+ // engine/surgical-run-splitting.js
5131
+ function getRunContentPieces(runElement) {
5132
+ const pieces = [];
5133
+ let offset = 0;
5134
+ for (const child of Array.from(runElement.childNodes || [])) {
5135
+ if (isWordElement(child, "rPr")) continue;
5136
+ if (!isTextLikeRunChild(child)) continue;
5137
+ const text = getRunChildText(child);
5138
+ if (text.length === 0) continue;
5139
+ pieces.push({
5140
+ node: child,
5141
+ start: offset,
5142
+ end: offset + text.length,
5143
+ text
5144
+ });
5145
+ offset += text.length;
5146
+ }
5147
+ return pieces;
5148
+ }
5149
+ function getRunTextLength(pieces) {
5150
+ if (pieces.length === 0) return 0;
5151
+ return pieces[pieces.length - 1].end;
5152
+ }
5153
+ function sliceRunPieces(xmlDoc, pieces, start, end, asDeletedText) {
5154
+ const sliced = [];
5155
+ if (end <= start) return sliced;
5156
+ pieces.forEach((piece) => {
5157
+ const overlapStart = Math.max(start, piece.start);
5158
+ const overlapEnd = Math.min(end, piece.end);
5159
+ if (overlapEnd <= overlapStart) return;
5160
+ const localStart = overlapStart - piece.start;
5161
+ const localEnd = overlapEnd - piece.start;
5162
+ const text = piece.text.slice(localStart, localEnd);
5163
+ sliced.push(cloneRunPiece(xmlDoc, piece.node, text, asDeletedText));
5164
+ });
5165
+ return sliced;
5166
+ }
5167
+ function createRunFromPieces(xmlDoc, pieces, rPr) {
5168
+ const run = createWordElement(xmlDoc, "w:r");
5169
+ if (rPr) run.appendChild(rPr.cloneNode(true));
5170
+ pieces.forEach((piece) => run.appendChild(piece));
5171
+ return run;
5172
+ }
5173
+ function insertRunPiecesBefore(xmlDoc, parent, referenceNode, pieces, rPr) {
5174
+ if (pieces.length === 0) return null;
5175
+ const run = createRunFromPieces(xmlDoc, pieces, rPr);
5176
+ parent.insertBefore(run, referenceNode);
5177
+ return run;
5178
+ }
5179
+ function cloneRunPiece(xmlDoc, sourceNode, text, asDeletedText) {
5180
+ if (asDeletedText) {
5181
+ const delText = createWordElement(xmlDoc, "w:delText");
5182
+ delText.setAttribute("xml:space", "preserve");
5183
+ delText.textContent = text;
5184
+ return delText;
5185
+ }
5186
+ if (isWordElement(sourceNode, "t")) {
5187
+ const textNode2 = sourceNode.cloneNode(false);
5188
+ textNode2.textContent = text;
5189
+ if (/^\s|\s$/.test(text)) {
5190
+ textNode2.setAttribute("xml:space", "preserve");
5191
+ }
5192
+ return textNode2;
5193
+ }
5194
+ if (text === "\n" && (isWordElement(sourceNode, "br") || isWordElement(sourceNode, "cr"))) {
5195
+ return sourceNode.cloneNode(true);
5196
+ }
5197
+ if (text === " " && isWordElement(sourceNode, "tab")) {
5198
+ return sourceNode.cloneNode(true);
5199
+ }
5200
+ if (text === "\u2011" && isWordElement(sourceNode, "noBreakHyphen")) {
5201
+ return sourceNode.cloneNode(true);
5202
+ }
5203
+ const textNode = createWordElement(xmlDoc, "w:t");
5204
+ textNode.setAttribute("xml:space", "preserve");
5205
+ textNode.textContent = text;
5206
+ return textNode;
5207
+ }
5208
+
5209
+ // engine/surgical-diff-application.js
4884
5210
  function reconcileFormattingForTextSpan(xmlDoc, span, start, end, applicableHints, author, generateRedlines) {
4885
5211
  const desiredFormat = {};
4886
5212
  if (applicableHints.length > 0) {
4887
5213
  applicableHints.forEach((h) => Object.assign(desiredFormat, h.format));
4888
5214
  }
4889
5215
  const rPr = span.rPr;
4890
- const hasElement = (tagName) => {
5216
+ const hasElement = (localName) => {
4891
5217
  if (!rPr) return false;
4892
5218
  for (let node = rPr.firstChild; node; node = node.nextSibling) {
4893
- if (node.nodeName === tagName) {
5219
+ if (isWordElement(node, localName)) {
4894
5220
  return true;
4895
5221
  }
4896
5222
  }
4897
5223
  return false;
4898
5224
  };
4899
5225
  const existingFormat = {
4900
- bold: hasElement("w:b"),
4901
- italic: hasElement("w:i"),
4902
- underline: hasElement("w:u"),
4903
- strikethrough: hasElement("w:strike")
5226
+ bold: hasElement("b"),
5227
+ italic: hasElement("i"),
5228
+ underline: hasElement("u"),
5229
+ strikethrough: hasElement("strike")
4904
5230
  };
4905
5231
  const formatsToCheck = ["bold", "italic", "underline", "strikethrough"];
4906
5232
  const changesNeeded = formatsToCheck.some((f) => !!desiredFormat[f] !== existingFormat[f]);
4907
- if (!changesNeeded) return;
5233
+ if (!changesNeeded) return false;
4908
5234
  const parent = span.runElement.parentNode;
4909
- if (!parent) return;
5235
+ if (!parent) return false;
4910
5236
  const fullText = span.textElement.textContent || "";
4911
5237
  const runStart = span.charStart;
4912
5238
  const localStart = start - runStart;
@@ -4926,109 +5252,54 @@ function reconcileFormattingForTextSpan(xmlDoc, span, start, end, applicableHint
4926
5252
  parent.insertBefore(afterRun, span.runElement);
4927
5253
  }
4928
5254
  parent.removeChild(span.runElement);
5255
+ return true;
4929
5256
  }
4930
- function processRunElement(r, p, container, currentOffset, textSpans) {
4931
- const rPr = getFirstElementByTag(r, "w:rPr");
4932
- let localOffset = currentOffset;
4933
- const textParts = [];
4934
- for (let rc = r.firstChild; rc; rc = rc.nextSibling) {
4935
- if (rc.nodeName === "w:t") {
4936
- const text = rc.textContent || "";
4937
- if (text.length > 0) {
4938
- textSpans.push({
4939
- charStart: localOffset,
4940
- charEnd: localOffset + text.length,
4941
- textElement: rc,
4942
- runElement: r,
4943
- paragraph: p,
4944
- container,
4945
- rPr
4946
- });
4947
- localOffset += text.length;
4948
- textParts.push(text);
4949
- }
4950
- } else if (rc.nodeName === "w:br" || rc.nodeName === "w:cr") {
4951
- textSpans.push({
4952
- charStart: localOffset,
4953
- charEnd: localOffset + 1,
4954
- textElement: rc,
4955
- runElement: r,
4956
- paragraph: p,
4957
- container,
4958
- rPr
4959
- });
4960
- localOffset += 1;
4961
- textParts.push("\n");
4962
- } else if (rc.nodeName === "w:tab") {
4963
- textSpans.push({
4964
- charStart: localOffset,
4965
- charEnd: localOffset + 1,
4966
- textElement: rc,
4967
- runElement: r,
4968
- paragraph: p,
4969
- container,
4970
- rPr
4971
- });
4972
- localOffset += 1;
4973
- textParts.push(" ");
4974
- } else if (rc.nodeName === "w:noBreakHyphen") {
4975
- textSpans.push({
4976
- charStart: localOffset,
4977
- charEnd: localOffset + 1,
4978
- textElement: rc,
4979
- runElement: r,
4980
- paragraph: p,
4981
- container,
4982
- rPr
4983
- });
4984
- localOffset += 1;
4985
- textParts.push("\u2011");
4986
- }
4987
- }
4988
- return { nextOffset: localOffset, text: textParts.join("") };
4989
- }
4990
- function processDelete(xmlDoc, spanIndex, startPos, endPos, processedSpans, author, generateRedlines) {
5257
+ function processDelete(xmlDoc, spanIndex, startPos, endPos, author, generateRedlines) {
5258
+ const spans = [];
4991
5259
  forEachOverlappingSpan(spanIndex, startPos, endPos, (span) => {
4992
- if (processedSpans.has(span.textElement)) return;
4993
- const deleteStart = Math.max(0, startPos - span.charStart);
4994
- const deleteEnd = Math.min(span.charEnd - span.charStart, endPos - span.charStart);
4995
- const originalText = span.textElement.textContent || "";
4996
- const beforeText = originalText.substring(0, deleteStart);
4997
- const deletedText = originalText.substring(deleteStart, deleteEnd);
4998
- const afterText = originalText.substring(deleteEnd);
4999
- if (deletedText.length === 0) return;
5000
- const parent = span.runElement.parentNode;
5260
+ spans.push(span);
5261
+ });
5262
+ if (spans.length === 0) return false;
5263
+ const spansByRun = /* @__PURE__ */ new Map();
5264
+ spans.forEach((span) => {
5265
+ if (!span.runElement?.parentNode) return;
5266
+ if (!spansByRun.has(span.runElement)) spansByRun.set(span.runElement, []);
5267
+ spansByRun.get(span.runElement).push(span);
5268
+ });
5269
+ let changed = false;
5270
+ spansByRun.forEach((runSpans, runElement) => {
5271
+ const parent = runElement.parentNode;
5001
5272
  if (!parent) return;
5002
- if (beforeText.length === 0 && afterText.length === 0) {
5003
- const delRun = createTextRun(xmlDoc, deletedText, span.rPr, true);
5004
- if (generateRedlines) {
5005
- const delWrapper = createTrackChange(xmlDoc, "del", delRun, author);
5006
- parent.insertBefore(delWrapper, span.runElement);
5007
- }
5008
- parent.removeChild(span.runElement);
5009
- } else {
5010
- const oldRun = span.runElement;
5011
- if (beforeText.length > 0) {
5012
- const beforeRun = createTextRun(xmlDoc, beforeText, span.rPr, false);
5013
- parent.insertBefore(beforeRun, oldRun);
5014
- }
5015
- const delRun = createTextRun(xmlDoc, deletedText, span.rPr, true);
5016
- if (generateRedlines) {
5017
- const delWrapper = createTrackChange(xmlDoc, "del", delRun, author);
5018
- parent.insertBefore(delWrapper, oldRun);
5019
- }
5020
- if (afterText.length > 0) {
5021
- const afterRun = createTextRun(xmlDoc, afterText, span.rPr, false);
5022
- parent.insertBefore(afterRun, oldRun);
5023
- span.runElement = afterRun;
5024
- span.textElement = getFirstElementByTag(afterRun, "w:t") || getFirstElementByTag(afterRun, "t");
5025
- }
5026
- parent.removeChild(oldRun);
5027
- }
5028
- processedSpans.add(span.textElement);
5273
+ const pieces = getRunContentPieces(runElement);
5274
+ if (pieces.length === 0) return;
5275
+ let deleteStart = Infinity;
5276
+ let deleteEnd = -Infinity;
5277
+ runSpans.forEach((span) => {
5278
+ const piece = pieces.find((candidate) => candidate.node === span.textElement);
5279
+ if (!piece) return;
5280
+ const spanDeleteStart = Math.max(0, startPos - span.charStart);
5281
+ const spanDeleteEnd = Math.min(span.charEnd - span.charStart, endPos - span.charStart);
5282
+ if (spanDeleteEnd <= spanDeleteStart) return;
5283
+ deleteStart = Math.min(deleteStart, piece.start + spanDeleteStart);
5284
+ deleteEnd = Math.max(deleteEnd, piece.start + spanDeleteEnd);
5285
+ });
5286
+ if (!Number.isFinite(deleteStart) || deleteEnd <= deleteStart) return;
5287
+ const beforePieces = sliceRunPieces(xmlDoc, pieces, 0, deleteStart, false);
5288
+ const deletedPieces = sliceRunPieces(xmlDoc, pieces, deleteStart, deleteEnd, true);
5289
+ const afterPieces = sliceRunPieces(xmlDoc, pieces, deleteEnd, getRunTextLength(pieces), false);
5290
+ insertRunPiecesBefore(xmlDoc, parent, runElement, beforePieces, runSpans[0].rPr);
5291
+ if (generateRedlines && deletedPieces.length > 0) {
5292
+ const delRun = createRunFromPieces(xmlDoc, deletedPieces, runSpans[0].rPr);
5293
+ const delWrapper = createTrackChange(xmlDoc, "del", delRun, author);
5294
+ parent.insertBefore(delWrapper, runElement);
5295
+ }
5296
+ insertRunPiecesBefore(xmlDoc, parent, runElement, afterPieces, runSpans[0].rPr);
5297
+ parent.removeChild(runElement);
5298
+ changed = true;
5029
5299
  });
5300
+ return changed;
5030
5301
  }
5031
- function processInsert(xmlDoc, spanIndex, pos, text, processedSpans, author, formatHints = [], insertOffset = 0, generateRedlines = true) {
5302
+ function processInsert(xmlDoc, spanIndex, pos, text, author, formatHints = [], insertOffset = 0, generateRedlines = true, fallbackParagraph = null) {
5032
5303
  let targetSpan = findContainingSpan(spanIndex, pos);
5033
5304
  if (!targetSpan && pos > 0) {
5034
5305
  targetSpan = findFirstSpanEndingAt(spanIndex, pos);
@@ -5039,99 +5310,99 @@ function processInsert(xmlDoc, spanIndex, pos, text, processedSpans, author, for
5039
5310
  if (!targetSpan && spanIndex.spans.length > 0) {
5040
5311
  targetSpan = spanIndex.spans[spanIndex.spans.length - 1];
5041
5312
  }
5042
- if (targetSpan) {
5043
- const applicableHints = getApplicableFormatHints(formatHints, insertOffset, insertOffset + text.length);
5044
- const baseRPr = targetSpan.rPr;
5045
- const parent = targetSpan.runElement.parentNode;
5046
- if (parent) {
5047
- const referenceNode = pos === targetSpan.charStart ? targetSpan.runElement : targetSpan.runElement.nextSibling;
5048
- if (applicableHints.length === 0) {
5049
- const insRun = createTextRun(xmlDoc, text, baseRPr, false);
5050
- if (generateRedlines) {
5051
- const insWrapper = createTrackChange(xmlDoc, "ins", insRun, author);
5052
- parent.insertBefore(insWrapper, referenceNode);
5053
- } else {
5054
- parent.insertBefore(insRun, referenceNode);
5055
- }
5056
- } else {
5057
- const runs = createFormattedRuns(xmlDoc, text, baseRPr, applicableHints, insertOffset, author, generateRedlines);
5058
- if (generateRedlines) {
5059
- const insWrapper = createTrackChange(xmlDoc, "ins", null, author);
5060
- runs.forEach((run) => insWrapper.appendChild(run));
5061
- parent.insertBefore(insWrapper, referenceNode);
5062
- } else {
5063
- runs.forEach((run) => parent.insertBefore(run, referenceNode));
5064
- }
5065
- }
5066
- }
5067
- }
5068
- }
5069
- function buildSpanIndex(textSpans) {
5070
- const spans = textSpans.slice().sort((a, b) => a.charStart - b.charStart || a.charEnd - b.charEnd);
5071
- const starts = spans.map((span) => span.charStart);
5072
- const ends = spans.map((span) => span.charEnd);
5073
- return { spans, starts, ends };
5074
- }
5075
- function upperBound(values, target) {
5076
- let left = 0;
5077
- let right = values.length;
5078
- while (left < right) {
5079
- const middle = left + right >> 1;
5080
- if (values[middle] <= target) {
5081
- left = middle + 1;
5082
- } else {
5083
- right = middle;
5084
- }
5313
+ if (!targetSpan) {
5314
+ if (!fallbackParagraph) return false;
5315
+ insertTextRuns(xmlDoc, fallbackParagraph, null, text, null, author, formatHints, insertOffset, generateRedlines);
5316
+ return true;
5085
5317
  }
5086
- return left;
5087
- }
5088
- function lowerBound(values, target) {
5089
- let left = 0;
5090
- let right = values.length;
5091
- while (left < right) {
5092
- const middle = left + right >> 1;
5093
- if (values[middle] < target) {
5094
- left = middle + 1;
5095
- } else {
5096
- right = middle;
5097
- }
5318
+ const parent = targetSpan.runElement.parentNode;
5319
+ if (!parent) {
5320
+ if (!fallbackParagraph) return false;
5321
+ insertTextRuns(xmlDoc, fallbackParagraph, null, text, targetSpan.rPr, author, formatHints, insertOffset, generateRedlines);
5322
+ return true;
5098
5323
  }
5099
- return left;
5100
- }
5101
- function forEachOverlappingSpan(spanIndex, startPos, endPos, callback) {
5102
- if (endPos <= startPos || spanIndex.spans.length === 0) {
5103
- return;
5324
+ const pieces = getRunContentPieces(targetSpan.runElement);
5325
+ const targetPiece = pieces.find((piece) => piece.node === targetSpan.textElement);
5326
+ const localInsertPos = targetPiece ? targetPiece.start + Math.max(0, Math.min(pos - targetSpan.charStart, targetSpan.charEnd - targetSpan.charStart)) : pos <= targetSpan.charStart ? 0 : getRunTextLength(pieces);
5327
+ if (localInsertPos > 0 && localInsertPos < getRunTextLength(pieces)) {
5328
+ const beforePieces = sliceRunPieces(xmlDoc, pieces, 0, localInsertPos, false);
5329
+ const afterPieces = sliceRunPieces(xmlDoc, pieces, localInsertPos, getRunTextLength(pieces), false);
5330
+ insertRunPiecesBefore(xmlDoc, parent, targetSpan.runElement, beforePieces, targetSpan.rPr);
5331
+ insertTextRuns(xmlDoc, parent, targetSpan.runElement, text, targetSpan.rPr, author, formatHints, insertOffset, generateRedlines);
5332
+ insertRunPiecesBefore(xmlDoc, parent, targetSpan.runElement, afterPieces, targetSpan.rPr);
5333
+ parent.removeChild(targetSpan.runElement);
5334
+ return true;
5104
5335
  }
5105
- let index = upperBound(spanIndex.ends, startPos);
5106
- while (index < spanIndex.spans.length) {
5107
- const span = spanIndex.spans[index];
5108
- if (span.charStart >= endPos) {
5109
- break;
5336
+ const referenceNode = pos <= targetSpan.charStart ? targetSpan.runElement : targetSpan.runElement.nextSibling;
5337
+ insertTextRuns(xmlDoc, parent, referenceNode, text, targetSpan.rPr, author, formatHints, insertOffset, generateRedlines);
5338
+ return true;
5339
+ }
5340
+ function insertTextRuns(xmlDoc, parent, referenceNode, text, baseRPr, author, formatHints, insertOffset, generateRedlines) {
5341
+ const applicableHints = getApplicableFormatHints(formatHints, insertOffset, insertOffset + text.length);
5342
+ if (applicableHints.length === 0) {
5343
+ const insRun = createTextRun(xmlDoc, text, baseRPr, false);
5344
+ if (generateRedlines) {
5345
+ const insWrapper = createTrackChange(xmlDoc, "ins", insRun, author);
5346
+ parent.insertBefore(insWrapper, referenceNode);
5347
+ } else {
5348
+ parent.insertBefore(insRun, referenceNode);
5110
5349
  }
5111
- callback(span);
5112
- index++;
5350
+ return;
5113
5351
  }
5114
- }
5115
- function findContainingSpan(spanIndex, pos) {
5116
- if (spanIndex.spans.length === 0) return null;
5117
- const index = upperBound(spanIndex.starts, pos) - 1;
5118
- if (index < 0) return null;
5119
- const span = spanIndex.spans[index];
5120
- return pos >= span.charStart && pos < span.charEnd ? span : null;
5121
- }
5122
- function findFirstSpanEndingAt(spanIndex, pos) {
5123
- const index = lowerBound(spanIndex.ends, pos);
5124
- if (index < spanIndex.spans.length && spanIndex.ends[index] === pos) {
5125
- return spanIndex.spans[index];
5352
+ const runs = createFormattedRuns(xmlDoc, text, baseRPr, applicableHints, insertOffset, author, generateRedlines);
5353
+ if (generateRedlines) {
5354
+ const insWrapper = createTrackChange(xmlDoc, "ins", null, author);
5355
+ runs.forEach((run) => insWrapper.appendChild(run));
5356
+ parent.insertBefore(insWrapper, referenceNode);
5357
+ } else {
5358
+ runs.forEach((run) => parent.insertBefore(run, referenceNode));
5126
5359
  }
5127
- return null;
5128
5360
  }
5129
- function findLastSpanEndingBeforeOrAt(spanIndex, pos) {
5130
- const index = upperBound(spanIndex.ends, pos) - 1;
5131
- if (index >= 0) {
5132
- return spanIndex.spans[index];
5361
+
5362
+ // engine/surgical-mode.js
5363
+ function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, author, formatHints, generateRedlines = true, targetParagraph = null) {
5364
+ const allParagraphs = targetParagraph ? [targetParagraph] : getDocumentParagraphs(xmlDoc);
5365
+ const { fullText, textSpans } = buildSurgicalTextSpans(allParagraphs);
5366
+ const diffs = computeWordDiffs(fullText, modifiedText);
5367
+ const spanIndex = buildSpanIndex(textSpans);
5368
+ let originalPos = 0;
5369
+ let newPos = 0;
5370
+ let hasChanges = false;
5371
+ for (const [op, text] of diffs) {
5372
+ if (op === 0) {
5373
+ const len = text.length;
5374
+ const startPos = originalPos;
5375
+ const endPos = originalPos + len;
5376
+ forEachOverlappingSpan(spanIndex, startPos, endPos, (span) => {
5377
+ const overlapStartOriginal = Math.max(span.charStart, startPos);
5378
+ const overlapEndOriginal = Math.min(span.charEnd, endPos);
5379
+ const segmentLen = overlapEndOriginal - overlapStartOriginal;
5380
+ const relativeOffset = overlapStartOriginal - startPos;
5381
+ const overlapStartNew = newPos + relativeOffset;
5382
+ const overlapEndNew = overlapStartNew + segmentLen;
5383
+ const applicableHints = getApplicableFormatHints(formatHints, overlapStartNew, overlapEndNew);
5384
+ if (reconcileFormattingForTextSpan(xmlDoc, span, overlapStartOriginal, overlapEndOriginal, applicableHints, author, generateRedlines)) {
5385
+ hasChanges = true;
5386
+ }
5387
+ });
5388
+ originalPos += len;
5389
+ newPos += len;
5390
+ } else if (op === -1) {
5391
+ if (processDelete(xmlDoc, spanIndex, originalPos, originalPos + text.length, author, generateRedlines)) {
5392
+ hasChanges = true;
5393
+ }
5394
+ originalPos += text.length;
5395
+ } else if (op === 1) {
5396
+ const textWithoutNewlines = text.replace(/\n/g, " ");
5397
+ if (textWithoutNewlines.trim().length > 0) {
5398
+ if (processInsert(xmlDoc, spanIndex, originalPos, textWithoutNewlines, author, formatHints, newPos, generateRedlines, allParagraphs[0] || null)) {
5399
+ hasChanges = true;
5400
+ }
5401
+ }
5402
+ newPos += text.length;
5403
+ }
5133
5404
  }
5134
- return null;
5405
+ return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges });
5135
5406
  }
5136
5407
 
5137
5408
  // engine/reconstruction-mapper.js
@@ -5206,6 +5477,7 @@ function buildReconstructionMapping(xmlDoc, modifiedText) {
5206
5477
  const escapedToken = tokenString.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
5207
5478
  processedModifiedText = processedModifiedText.replace(new RegExp(escapedToken, "g"), char);
5208
5479
  });
5480
+ processedModifiedText = preserveReferencePlaceholders(originalFullText, processedModifiedText, referenceMap);
5209
5481
  const containerFragments = /* @__PURE__ */ new Map();
5210
5482
  uniqueContainers.forEach((container) => {
5211
5483
  containerFragments.set(container, xmlDoc.createDocumentFragment());
@@ -5256,6 +5528,25 @@ function buildReconstructionMapping(xmlDoc, modifiedText) {
5256
5528
  isParagraphStart: (index) => paragraphStarts.has(index)
5257
5529
  };
5258
5530
  }
5531
+ function preserveReferencePlaceholders(originalFullText, modifiedText, referenceMap) {
5532
+ let result = modifiedText;
5533
+ for (const referenceChar of referenceMap.keys()) {
5534
+ if (result.includes(referenceChar)) continue;
5535
+ const originalIndex = originalFullText.indexOf(referenceChar);
5536
+ if (originalIndex < 0) continue;
5537
+ const prefix = originalFullText.slice(0, originalIndex);
5538
+ const suffix = originalFullText.slice(originalIndex + referenceChar.length);
5539
+ if (prefix && result.startsWith(prefix)) {
5540
+ result = `${result.slice(0, prefix.length)}${referenceChar}${result.slice(prefix.length)}`;
5541
+ continue;
5542
+ }
5543
+ if (suffix && result.endsWith(suffix)) {
5544
+ const insertAt = result.length - suffix.length;
5545
+ result = `${result.slice(0, insertAt)}${referenceChar}${result.slice(insertAt)}`;
5546
+ }
5547
+ }
5548
+ return result;
5549
+ }
5259
5550
  function processChildNode(child, originalFullText, propertyMap, sentinelMap, referenceMap, tokenToCharMap, nextCharCode) {
5260
5551
  if (child.nodeName === "w:r") {
5261
5552
  return processRunForReconstruction(child, originalFullText, propertyMap, sentinelMap, referenceMap, tokenToCharMap, nextCharCode);
@@ -5360,7 +5651,7 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
5360
5651
  isParagraphStart
5361
5652
  } = context;
5362
5653
  const createNewParagraph = (pPr) => {
5363
- const newParagraph = xmlDoc.createElement("w:p");
5654
+ const newParagraph = createWordElement(xmlDoc, "w:p");
5364
5655
  if (pPr) newParagraph.appendChild(pPr.cloneNode(true));
5365
5656
  return newParagraph;
5366
5657
  };
@@ -5372,6 +5663,7 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
5372
5663
  }
5373
5664
  let currentOriginalIndex = 0;
5374
5665
  let currentInsertOffset = 0;
5666
+ const emittedCommentMarkers = /* @__PURE__ */ new WeakSet();
5375
5667
  for (const [op, text] of diffs) {
5376
5668
  if (op === 0 || op === -1) {
5377
5669
  const type = op === 0 ? "equal" : "delete";
@@ -5381,7 +5673,7 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
5381
5673
  const properties = getRunProperties(chunkStart);
5382
5674
  const chunkLength = getPropertySpanLength(chunkStart, text.length - offset);
5383
5675
  const chunk = text.substring(offset, offset + chunkLength);
5384
- appendTextToCurrent(
5676
+ const appendResult = appendTextToCurrent(
5385
5677
  xmlDoc,
5386
5678
  chunk,
5387
5679
  type,
@@ -5398,8 +5690,10 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
5398
5690
  author,
5399
5691
  formatHints,
5400
5692
  currentInsertOffset,
5401
- generateRedlines
5693
+ generateRedlines,
5694
+ emittedCommentMarkers
5402
5695
  );
5696
+ currentParagraph = appendResult.currentParagraph;
5403
5697
  if (op === 0) {
5404
5698
  currentInsertOffset += chunkLength;
5405
5699
  }
@@ -5410,7 +5704,7 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
5410
5704
  }
5411
5705
  if (op === 1) {
5412
5706
  const properties = currentOriginalIndex > 0 && !isParagraphStart(currentOriginalIndex) ? getRunProperties(currentOriginalIndex - 1) : getRunProperties(currentOriginalIndex);
5413
- appendTextToCurrent(
5707
+ const appendResult = appendTextToCurrent(
5414
5708
  xmlDoc,
5415
5709
  text,
5416
5710
  "insert",
@@ -5427,8 +5721,10 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
5427
5721
  author,
5428
5722
  formatHints,
5429
5723
  currentInsertOffset,
5430
- generateRedlines
5724
+ generateRedlines,
5725
+ emittedCommentMarkers
5431
5726
  );
5727
+ currentParagraph = appendResult.currentParagraph;
5432
5728
  currentInsertOffset += text.length;
5433
5729
  }
5434
5730
  }
@@ -5454,17 +5750,18 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
5454
5750
  });
5455
5751
  return { oxml: serializer.serializeToString(xmlDoc), hasChanges: true };
5456
5752
  }
5457
- function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, currentParagraphRef, containerFragments, sentinelMapByStart, referenceMap, replacementContainers, getParagraphInfo, createNewParagraph, author, formatHints = [], insertOffset = 0, generateRedlines = true) {
5753
+ function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, currentParagraphRef, containerFragments, sentinelMapByStart, referenceMap, replacementContainers, getParagraphInfo, createNewParagraph, author, formatHints = [], insertOffset = 0, generateRedlines = true, emittedCommentMarkers = /* @__PURE__ */ new WeakSet()) {
5458
5754
  let localBaseIndex = baseIndex;
5459
5755
  let localInsertOffset = insertOffset;
5460
5756
  let localParagraph = currentParagraphRef;
5461
5757
  const parts = text.split(/([\n\uFFFC]|[\uE000-\uF8FF])/);
5462
5758
  parts.forEach((part) => {
5463
5759
  const sentinelsAtOffset = sentinelMapByStart.get(localBaseIndex) || [];
5464
- const commentMarkers = sentinelsAtOffset.filter((sentinel) => sentinel.isCommentMarker);
5760
+ const commentMarkers = sentinelsAtOffset.filter((sentinel) => sentinel.isCommentMarker && !emittedCommentMarkers.has(sentinel.node));
5465
5761
  commentMarkers.forEach((marker) => {
5762
+ emittedCommentMarkers.add(marker.node);
5466
5763
  if (marker.node.nodeName === "w:commentReference") {
5467
- const run = xmlDoc.createElement("w:r");
5764
+ const run = createWordElement(xmlDoc, "w:r");
5468
5765
  run.appendChild(marker.node.cloneNode(true));
5469
5766
  localParagraph.appendChild(run);
5470
5767
  } else {
@@ -5472,14 +5769,17 @@ function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, curren
5472
5769
  }
5473
5770
  });
5474
5771
  if (part === "\n") {
5475
- if (type !== "delete") {
5476
- const info = getParagraphInfo(localBaseIndex);
5477
- const nextParagraph = createNewParagraph(info.pPr);
5478
- const fragment = containerFragments.get(info.container);
5479
- if (fragment) {
5480
- fragment.appendChild(nextParagraph);
5481
- localParagraph = nextParagraph;
5482
- }
5772
+ const info = getParagraphInfo(localBaseIndex + 1);
5773
+ const nextParagraph = createNewParagraph(info.pPr);
5774
+ if (generateRedlines && type === "insert") {
5775
+ markParagraphMarkInserted(xmlDoc, nextParagraph, author);
5776
+ } else if (generateRedlines && type === "delete") {
5777
+ markParagraphMarkDeleted(xmlDoc, nextParagraph, author);
5778
+ }
5779
+ const fragment = containerFragments.get(info.container);
5780
+ if (fragment) {
5781
+ fragment.appendChild(nextParagraph);
5782
+ localParagraph = nextParagraph;
5483
5783
  }
5484
5784
  localBaseIndex++;
5485
5785
  if (type !== "delete") localInsertOffset++;
@@ -5507,7 +5807,7 @@ function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, curren
5507
5807
  const refNode = referenceMap.get(part);
5508
5808
  if (refNode) {
5509
5809
  const clone = refNode.cloneNode(true);
5510
- const run = xmlDoc.createElement("w:r");
5810
+ const run = createWordElement(xmlDoc, "w:r");
5511
5811
  if (rPr) run.appendChild(rPr.cloneNode(true));
5512
5812
  run.appendChild(clone);
5513
5813
  localParagraph.appendChild(run);
@@ -5525,9 +5825,9 @@ function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, curren
5525
5825
  localParagraph.appendChild(wrapperClone);
5526
5826
  }
5527
5827
  if (type === "delete") {
5528
- const run = xmlDoc.createElement("w:r");
5828
+ const run = createWordElement(xmlDoc, "w:r");
5529
5829
  if (rPr) run.appendChild(rPr.cloneNode(true));
5530
- const delText = xmlDoc.createElement("w:delText");
5830
+ const delText = createWordElement(xmlDoc, "w:delText");
5531
5831
  delText.setAttribute("xml:space", "preserve");
5532
5832
  delText.textContent = part;
5533
5833
  run.appendChild(delText);
@@ -5551,16 +5851,17 @@ function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, curren
5551
5851
  }
5552
5852
  localBaseIndex += part.length;
5553
5853
  });
5854
+ return { currentParagraph: localParagraph };
5554
5855
  }
5555
5856
 
5556
5857
  // engine/reconstruction-mode.js
5557
5858
  function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer, author, formatHints, generateRedlines = true) {
5558
5859
  const mapping = buildReconstructionMapping(xmlDoc, modifiedText);
5559
5860
  if (mapping.paragraphs.length === 0) {
5560
- return { oxml: serializer.serializeToString(xmlDoc), hasChanges: false };
5861
+ return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: false });
5561
5862
  }
5562
5863
  const diffs = computeWordDiffs(mapping.originalFullText, mapping.processedModifiedText);
5563
- return applyReconstructionDiffs(
5864
+ return withOoxmlSourceType(applyReconstructionDiffs(
5564
5865
  xmlDoc,
5565
5866
  diffs,
5566
5867
  mapping,
@@ -5568,15 +5869,15 @@ function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer,
5568
5869
  author,
5569
5870
  formatHints,
5570
5871
  generateRedlines
5571
- );
5872
+ ));
5572
5873
  }
5573
5874
 
5574
5875
  // engine/table-mode.js
5575
5876
  function noChanges(serializer, xmlDoc) {
5576
- return { oxml: serializer.serializeToString(xmlDoc), hasChanges: false };
5877
+ return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: false });
5577
5878
  }
5578
5879
  function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines = true) {
5579
- const tableNodes = getElementsByTag(xmlDoc, "w:tbl");
5880
+ const tableNodes = getElementsByTagNSOrTag(xmlDoc, NS_W, "tbl");
5580
5881
  const newTableData = parseTable(modifiedText);
5581
5882
  const hasNewContent = newTableData.rows.length > 0 || newTableData.headers.length > 0;
5582
5883
  if (tableNodes.length === 0 || !hasNewContent) {
@@ -5598,14 +5899,14 @@ function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, auth
5598
5899
  error("[OxmlEngine] Failed to parse reconciled table OOXML:", parseError.textContent);
5599
5900
  return noChanges(serializer, xmlDoc);
5600
5901
  }
5601
- const newTableNode = getFirstElementByTag(reconciledDoc, "w:tbl");
5902
+ const newTableNode = getFirstElementByTagNSOrTag(reconciledDoc, NS_W, "tbl");
5602
5903
  if (!newTableNode) {
5603
5904
  error("[OxmlEngine] No table found in reconciled OOXML");
5604
5905
  return noChanges(serializer, xmlDoc);
5605
5906
  }
5606
5907
  const importedTable = xmlDoc.importNode(newTableNode, true);
5607
5908
  targetTable.parentNode.replaceChild(importedTable, targetTable);
5608
- return { oxml: serializer.serializeToString(xmlDoc), hasChanges: true };
5909
+ return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: true });
5609
5910
  }
5610
5911
  function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines) {
5611
5912
  const tableData = parseTable(modifiedText);
@@ -5651,23 +5952,24 @@ function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser
5651
5952
  }
5652
5953
  const importedTable = workingDoc.importNode(newTableElement, true);
5653
5954
  if (generateRedlines) {
5654
- const date = getRevisionTimestamp();
5655
5955
  paragraphs.forEach((p) => {
5956
+ markParagraphMarkDeleted(workingDoc, p, author);
5656
5957
  const runs = getElementsByTagNS(p, NS_W, "r");
5657
5958
  runs.forEach((run) => {
5658
5959
  const textNodes = getElementsByTagNS(run, NS_W, "t");
5659
5960
  textNodes.forEach((t) => {
5660
5961
  const text = t.textContent || "";
5661
5962
  if (text.trim()) {
5662
- const delText = workingDoc.createElementNS(NS_W, "w:delText");
5963
+ const delText = createWordElement(workingDoc, "w:delText");
5663
5964
  delText.textContent = text;
5664
5965
  t.parentNode.replaceChild(delText, t);
5665
5966
  }
5666
5967
  });
5667
- const del = workingDoc.createElementNS(NS_W, "w:del");
5668
- del.setAttribute("w:id", String(getNextRevisionId()));
5669
- del.setAttribute("w:author", author);
5670
- del.setAttribute("w:date", date);
5968
+ const del = createWordElement(workingDoc, "w:del");
5969
+ const metadata = createRevisionMetadata(author);
5970
+ del.setAttribute("w:id", String(metadata.id));
5971
+ del.setAttribute("w:author", metadata.author);
5972
+ del.setAttribute("w:date", metadata.date);
5671
5973
  run.parentNode.insertBefore(del, run);
5672
5974
  del.appendChild(run);
5673
5975
  });
@@ -5680,7 +5982,405 @@ function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser
5680
5982
  parent.removeChild(firstParagraph);
5681
5983
  }
5682
5984
  log("[OxmlEngine] Text-to-table transformation complete");
5683
- return { oxml: serializer.serializeToString(workingDoc), hasChanges: true };
5985
+ return withOoxmlSourceType({ oxml: serializer.serializeToString(workingDoc), hasChanges: true });
5986
+ }
5987
+
5988
+ // services/revision-comment-management.js
5989
+ function getAttributeByLocalName(node, localName) {
5990
+ if (!node || !node.attributes) return "";
5991
+ for (const attr of Array.from(node.attributes)) {
5992
+ if ((attr.localName || "").toLowerCase() === localName.toLowerCase()) {
5993
+ return String(attr.value || "");
5994
+ }
5995
+ }
5996
+ return String(
5997
+ node.getAttribute?.(`w:${localName}`) || node.getAttribute?.(localName) || ""
5998
+ );
5999
+ }
6000
+ function normalizeAuthor(author) {
6001
+ return typeof author === "string" ? author.trim().toLowerCase() : "";
6002
+ }
6003
+ function isElement(node) {
6004
+ return !!node && node.nodeType === 1;
6005
+ }
6006
+ function isWordElement5(node, localName) {
6007
+ return isElement(node) && node.namespaceURI === NS_W && String(node.localName || "").toLowerCase() === localName.toLowerCase();
6008
+ }
6009
+ function getWordElementsByLocalName(xmlDoc, localName) {
6010
+ return Array.from(xmlDoc.getElementsByTagNameNS(NS_W, localName));
6011
+ }
6012
+ function resolveAuthorFilter(options = {}) {
6013
+ if (options?.allAuthors === true) {
6014
+ return { valid: true, allAuthors: true, normalizedAuthor: "" };
6015
+ }
6016
+ const normalizedAuthor = normalizeAuthor(options?.author);
6017
+ if (!normalizedAuthor) {
6018
+ return {
6019
+ valid: false,
6020
+ allAuthors: false,
6021
+ normalizedAuthor: "",
6022
+ warning: "No author provided. Pass { author } or set { allAuthors: true }."
6023
+ };
6024
+ }
6025
+ return { valid: true, allAuthors: false, normalizedAuthor };
6026
+ }
6027
+ function authorMatchesNode(node, filter) {
6028
+ if (filter.allAuthors) return true;
6029
+ const nodeAuthor = normalizeAuthor(getAttributeByLocalName(node, "author"));
6030
+ return !!nodeAuthor && nodeAuthor === filter.normalizedAuthor;
6031
+ }
6032
+ function parseXmlWithWarnings(oxml, parseFailurePrefix) {
6033
+ const parser = createParser();
6034
+ const xmlDoc = parser.parseFromString(oxml, "application/xml");
6035
+ const parseError = getXmlParseError(xmlDoc);
6036
+ if (parseError) {
6037
+ return {
6038
+ xmlDoc: null,
6039
+ serializer: null,
6040
+ warning: `${parseFailurePrefix}: ${parseError.textContent || "parse error"}`
6041
+ };
6042
+ }
6043
+ return { xmlDoc, serializer: createSerializer(), warning: null };
6044
+ }
6045
+ function removeNode(node) {
6046
+ if (node?.parentNode) {
6047
+ node.parentNode.removeChild(node);
6048
+ return true;
6049
+ }
6050
+ return false;
6051
+ }
6052
+ function unwrapNode(node) {
6053
+ const parent = node?.parentNode;
6054
+ if (!parent) return false;
6055
+ while (node.firstChild) {
6056
+ parent.insertBefore(node.firstChild, node);
6057
+ }
6058
+ parent.removeChild(node);
6059
+ return true;
6060
+ }
6061
+ function isTableRowRevisionMarker(node) {
6062
+ const parent = node?.parentNode;
6063
+ return isWordElement5(parent, "trPr") && isWordElement5(parent?.parentNode, "tr");
6064
+ }
6065
+ function isParagraphMarkRevisionMarker(node) {
6066
+ const rPr = node?.parentNode;
6067
+ const pPr = rPr?.parentNode;
6068
+ const paragraph = pPr?.parentNode;
6069
+ return isWordElement5(rPr, "rPr") && isWordElement5(pPr, "pPr") && isWordElement5(paragraph, "p");
6070
+ }
6071
+ function getContainingParagraphMarkRevision(node) {
6072
+ return isParagraphMarkRevisionMarker(node) ? node.parentNode.parentNode.parentNode : null;
6073
+ }
6074
+ function getNextWordParagraph(paragraph) {
6075
+ let cursor = paragraph?.nextSibling || null;
6076
+ while (cursor) {
6077
+ if (isWordElement5(cursor, "p")) return cursor;
6078
+ cursor = cursor.nextSibling;
6079
+ }
6080
+ return null;
6081
+ }
6082
+ function mergeParagraphIntoNextAndRemove(paragraph) {
6083
+ if (!paragraph?.parentNode) return false;
6084
+ const nextParagraph = getNextWordParagraph(paragraph);
6085
+ if (!nextParagraph) {
6086
+ return removeNode(paragraph);
6087
+ }
6088
+ const childrenToMove = Array.from(paragraph.childNodes || []).filter((child) => !isWordElement5(child, "pPr"));
6089
+ const insertionPoint = nextParagraph.firstChild || null;
6090
+ for (const child of childrenToMove) {
6091
+ nextParagraph.insertBefore(child, insertionPoint);
6092
+ }
6093
+ return removeNode(paragraph);
6094
+ }
6095
+ function acceptTrackedChangesInOoxml(oxml, options = {}) {
6096
+ const warnings = [];
6097
+ const filter = resolveAuthorFilter(options);
6098
+ if (!filter.valid) {
6099
+ return { oxml, hasChanges: false, acceptedCount: 0, warnings: [filter.warning] };
6100
+ }
6101
+ const parseResult = parseXmlWithWarnings(oxml, "Failed to parse OOXML");
6102
+ if (!parseResult.xmlDoc) {
6103
+ return { oxml, hasChanges: false, acceptedCount: 0, warnings: [parseResult.warning] };
6104
+ }
6105
+ const { xmlDoc, serializer } = parseResult;
6106
+ let acceptedCount = 0;
6107
+ for (const insNode of getWordElementsByLocalName(xmlDoc, "ins")) {
6108
+ if (!insNode.parentNode || !authorMatchesNode(insNode, filter)) continue;
6109
+ if (isParagraphMarkRevisionMarker(insNode)) {
6110
+ if (removeNode(insNode)) acceptedCount += 1;
6111
+ continue;
6112
+ }
6113
+ if (isTableRowRevisionMarker(insNode)) {
6114
+ if (removeNode(insNode)) acceptedCount += 1;
6115
+ continue;
6116
+ }
6117
+ if (unwrapNode(insNode)) acceptedCount += 1;
6118
+ }
6119
+ for (const delNode of getWordElementsByLocalName(xmlDoc, "del")) {
6120
+ if (!delNode.parentNode || !authorMatchesNode(delNode, filter)) continue;
6121
+ const paragraphMark = getContainingParagraphMarkRevision(delNode);
6122
+ if (paragraphMark) {
6123
+ if (mergeParagraphIntoNextAndRemove(paragraphMark)) acceptedCount += 1;
6124
+ continue;
6125
+ }
6126
+ if (isTableRowRevisionMarker(delNode)) {
6127
+ const rowNode = delNode.parentNode?.parentNode;
6128
+ if (removeNode(rowNode)) acceptedCount += 1;
6129
+ continue;
6130
+ }
6131
+ if (removeNode(delNode)) acceptedCount += 1;
6132
+ }
6133
+ for (const moveFromNode of getWordElementsByLocalName(xmlDoc, "moveFrom")) {
6134
+ if (!moveFromNode.parentNode || !authorMatchesNode(moveFromNode, filter)) continue;
6135
+ if (removeNode(moveFromNode)) acceptedCount += 1;
6136
+ }
6137
+ for (const moveToNode of getWordElementsByLocalName(xmlDoc, "moveTo")) {
6138
+ if (!moveToNode.parentNode || !authorMatchesNode(moveToNode, filter)) continue;
6139
+ if (unwrapNode(moveToNode)) acceptedCount += 1;
6140
+ }
6141
+ acceptedCount += removeMoveRangeMarkers(xmlDoc, filter);
6142
+ const changeTags = ["rPrChange", "pPrChange", "tblPrChange", "trPrChange", "tcPrChange"];
6143
+ for (const localName of changeTags) {
6144
+ for (const changeNode of getWordElementsByLocalName(xmlDoc, localName)) {
6145
+ if (!changeNode.parentNode || !authorMatchesNode(changeNode, filter)) continue;
6146
+ if (removeNode(changeNode)) acceptedCount += 1;
6147
+ }
6148
+ }
6149
+ return {
6150
+ oxml: serializer.serializeToString(xmlDoc),
6151
+ hasChanges: acceptedCount > 0,
6152
+ acceptedCount,
6153
+ warnings
6154
+ };
6155
+ }
6156
+ function convertDeletionTextNodes(xmlDoc, delNode) {
6157
+ for (const delTextNode of Array.from(delNode.getElementsByTagNameNS(NS_W, "delText"))) {
6158
+ const normalText = createWordElement(xmlDoc, "w:t");
6159
+ const spaceValue = delTextNode.getAttribute("xml:space");
6160
+ if (spaceValue) {
6161
+ normalText.setAttribute("xml:space", spaceValue);
6162
+ }
6163
+ while (delTextNode.firstChild) {
6164
+ normalText.appendChild(delTextNode.firstChild);
6165
+ }
6166
+ delTextNode.parentNode?.replaceChild(normalText, delTextNode);
6167
+ }
6168
+ }
6169
+ function rejectPropertyChangeNode(changeNode, localName) {
6170
+ const parent = changeNode?.parentNode;
6171
+ if (!parent) return false;
6172
+ const baseLocalName = localName.endsWith("Change") ? localName.slice(0, -"Change".length) : "";
6173
+ if (!baseLocalName || String(parent.localName || "").toLowerCase() !== baseLocalName.toLowerCase() || parent.namespaceURI !== NS_W) {
6174
+ return removeNode(changeNode);
6175
+ }
6176
+ const historicalNode = Array.from(changeNode.childNodes || []).find(
6177
+ (child) => child.nodeType === 1 && child.namespaceURI === NS_W && String(child.localName || "").toLowerCase() === baseLocalName.toLowerCase()
6178
+ );
6179
+ if (!historicalNode) {
6180
+ return removeNode(changeNode);
6181
+ }
6182
+ const toAppend = Array.from(historicalNode.childNodes || []);
6183
+ while (parent.firstChild) {
6184
+ parent.removeChild(parent.firstChild);
6185
+ }
6186
+ for (const node of toAppend) {
6187
+ const clone = xmlDocImportNode(parent.ownerDocument, node);
6188
+ parent.appendChild(clone);
6189
+ }
6190
+ return true;
6191
+ }
6192
+ function xmlDocImportNode(xmlDoc, node) {
6193
+ if (xmlDoc && typeof xmlDoc.importNode === "function") {
6194
+ return xmlDoc.importNode(node, true);
6195
+ }
6196
+ return node.cloneNode(true);
6197
+ }
6198
+ function collectMoveRangeStartIds(xmlDoc, localName, filter) {
6199
+ const ids = /* @__PURE__ */ new Set();
6200
+ for (const node of getWordElementsByLocalName(xmlDoc, localName)) {
6201
+ if (!authorMatchesNode(node, filter)) continue;
6202
+ const id = getAttributeByLocalName(node, "id");
6203
+ if (id) ids.add(id);
6204
+ }
6205
+ return ids;
6206
+ }
6207
+ function removeMoveRangeMarkers(xmlDoc, filter) {
6208
+ let removed = 0;
6209
+ const moveFromIds = collectMoveRangeStartIds(xmlDoc, "moveFromRangeStart", filter);
6210
+ const moveToIds = collectMoveRangeStartIds(xmlDoc, "moveToRangeStart", filter);
6211
+ const markerSpecs = [
6212
+ ["moveFromRangeStart", moveFromIds, true],
6213
+ ["moveFromRangeEnd", moveFromIds, false],
6214
+ ["moveToRangeStart", moveToIds, true],
6215
+ ["moveToRangeEnd", moveToIds, false]
6216
+ ];
6217
+ for (const [localName, ids, isStart] of markerSpecs) {
6218
+ for (const node of getWordElementsByLocalName(xmlDoc, localName)) {
6219
+ if (!node.parentNode) continue;
6220
+ const id = getAttributeByLocalName(node, "id");
6221
+ if (!id) continue;
6222
+ if (filter.allAuthors || ids.has(id) || isStart && authorMatchesNode(node, filter)) {
6223
+ if (removeNode(node)) removed += 1;
6224
+ }
6225
+ }
6226
+ }
6227
+ return removed;
6228
+ }
6229
+ function rejectTrackedChangesInOoxml(oxml, options = {}) {
6230
+ const warnings = [];
6231
+ const filter = resolveAuthorFilter(options);
6232
+ if (!filter.valid) {
6233
+ return { oxml, hasChanges: false, rejectedCount: 0, warnings: [filter.warning] };
6234
+ }
6235
+ const parseResult = parseXmlWithWarnings(oxml, "Failed to parse OOXML");
6236
+ if (!parseResult.xmlDoc) {
6237
+ return { oxml, hasChanges: false, rejectedCount: 0, warnings: [parseResult.warning] };
6238
+ }
6239
+ const { xmlDoc, serializer } = parseResult;
6240
+ let rejectedCount = 0;
6241
+ for (const insNode of getWordElementsByLocalName(xmlDoc, "ins")) {
6242
+ if (!insNode.parentNode || !authorMatchesNode(insNode, filter)) continue;
6243
+ const paragraphMark = getContainingParagraphMarkRevision(insNode);
6244
+ if (paragraphMark) {
6245
+ if (mergeParagraphIntoNextAndRemove(paragraphMark)) rejectedCount += 1;
6246
+ continue;
6247
+ }
6248
+ if (isTableRowRevisionMarker(insNode)) {
6249
+ const rowNode = insNode.parentNode?.parentNode;
6250
+ if (removeNode(rowNode)) rejectedCount += 1;
6251
+ continue;
6252
+ }
6253
+ if (removeNode(insNode)) rejectedCount += 1;
6254
+ }
6255
+ for (const delNode of getWordElementsByLocalName(xmlDoc, "del")) {
6256
+ if (!delNode.parentNode || !authorMatchesNode(delNode, filter)) continue;
6257
+ if (isParagraphMarkRevisionMarker(delNode)) {
6258
+ if (removeNode(delNode)) rejectedCount += 1;
6259
+ continue;
6260
+ }
6261
+ if (isTableRowRevisionMarker(delNode)) {
6262
+ if (removeNode(delNode)) rejectedCount += 1;
6263
+ continue;
6264
+ }
6265
+ convertDeletionTextNodes(xmlDoc, delNode);
6266
+ if (unwrapNode(delNode)) rejectedCount += 1;
6267
+ }
6268
+ for (const moveFromNode of getWordElementsByLocalName(xmlDoc, "moveFrom")) {
6269
+ if (!moveFromNode.parentNode || !authorMatchesNode(moveFromNode, filter)) continue;
6270
+ convertDeletionTextNodes(xmlDoc, moveFromNode);
6271
+ if (unwrapNode(moveFromNode)) rejectedCount += 1;
6272
+ }
6273
+ for (const moveToNode of getWordElementsByLocalName(xmlDoc, "moveTo")) {
6274
+ if (!moveToNode.parentNode || !authorMatchesNode(moveToNode, filter)) continue;
6275
+ if (removeNode(moveToNode)) rejectedCount += 1;
6276
+ }
6277
+ rejectedCount += removeMoveRangeMarkers(xmlDoc, filter);
6278
+ const changeTags = ["rPrChange", "pPrChange", "tblPrChange", "trPrChange", "tcPrChange"];
6279
+ for (const localName of changeTags) {
6280
+ for (const changeNode of getWordElementsByLocalName(xmlDoc, localName)) {
6281
+ if (!changeNode.parentNode || !authorMatchesNode(changeNode, filter)) continue;
6282
+ if (rejectPropertyChangeNode(changeNode, localName)) rejectedCount += 1;
6283
+ }
6284
+ }
6285
+ return {
6286
+ oxml: serializer.serializeToString(xmlDoc),
6287
+ hasChanges: rejectedCount > 0,
6288
+ rejectedCount,
6289
+ warnings
6290
+ };
6291
+ }
6292
+ function collectCommentTargetIds(xmlDoc, filter) {
6293
+ const targetIds = /* @__PURE__ */ new Set();
6294
+ const commentNodes = getWordElementsByLocalName(xmlDoc, "comment");
6295
+ for (const commentNode of commentNodes) {
6296
+ if (!authorMatchesNode(commentNode, filter)) continue;
6297
+ const id = getAttributeByLocalName(commentNode, "id");
6298
+ if (id) targetIds.add(id);
6299
+ }
6300
+ return { targetIds, commentNodes };
6301
+ }
6302
+ function removeCommentNodesById(commentNodes, targetIds) {
6303
+ let removed = 0;
6304
+ for (const commentNode of commentNodes) {
6305
+ const id = getAttributeByLocalName(commentNode, "id");
6306
+ if (!id || !targetIds.has(id)) continue;
6307
+ if (removeNode(commentNode)) removed += 1;
6308
+ }
6309
+ return removed;
6310
+ }
6311
+ function runIsOnlyCommentReference(runNode) {
6312
+ if (!isWordElement5(runNode, "r")) return false;
6313
+ const meaningfulChildren = Array.from(runNode.childNodes || []).filter((child) => {
6314
+ if (child.nodeType === 3) return String(child.nodeValue || "").trim().length > 0;
6315
+ if (child.nodeType !== 1) return false;
6316
+ if (child.namespaceURI !== NS_W) return true;
6317
+ const local = String(child.localName || "").toLowerCase();
6318
+ return local !== "rpr" && local !== "commentreference";
6319
+ });
6320
+ return meaningfulChildren.length === 0;
6321
+ }
6322
+ function removeCommentAnchors(xmlDoc, targetIds) {
6323
+ let removed = 0;
6324
+ const anchorTags = ["commentRangeStart", "commentRangeEnd", "commentReference"];
6325
+ for (const localName of anchorTags) {
6326
+ for (const node of getWordElementsByLocalName(xmlDoc, localName)) {
6327
+ if (!node.parentNode) continue;
6328
+ const id = getAttributeByLocalName(node, "id");
6329
+ if (!id || !targetIds.has(id)) continue;
6330
+ if (localName === "commentReference" && runIsOnlyCommentReference(node.parentNode)) {
6331
+ if (removeNode(node.parentNode)) {
6332
+ removed += 1;
6333
+ }
6334
+ continue;
6335
+ }
6336
+ if (removeNode(node)) {
6337
+ removed += 1;
6338
+ }
6339
+ }
6340
+ }
6341
+ return removed;
6342
+ }
6343
+ function deleteCommentsByAuthorInOoxml(oxml, options = {}) {
6344
+ const warnings = [];
6345
+ const filter = resolveAuthorFilter(options);
6346
+ if (!filter.valid) {
6347
+ return {
6348
+ oxml,
6349
+ hasChanges: false,
6350
+ commentsRemoved: 0,
6351
+ referencesRemoved: 0,
6352
+ warnings: [filter.warning]
6353
+ };
6354
+ }
6355
+ const parseResult = parseXmlWithWarnings(oxml, "Failed to parse OOXML");
6356
+ if (!parseResult.xmlDoc) {
6357
+ return {
6358
+ oxml,
6359
+ hasChanges: false,
6360
+ commentsRemoved: 0,
6361
+ referencesRemoved: 0,
6362
+ warnings: [parseResult.warning]
6363
+ };
6364
+ }
6365
+ const { xmlDoc, serializer } = parseResult;
6366
+ const { targetIds, commentNodes } = collectCommentTargetIds(xmlDoc, filter);
6367
+ if (filter.allAuthors) {
6368
+ for (const localName of ["commentRangeStart", "commentRangeEnd", "commentReference"]) {
6369
+ for (const node of getWordElementsByLocalName(xmlDoc, localName)) {
6370
+ const id = getAttributeByLocalName(node, "id");
6371
+ if (id) targetIds.add(id);
6372
+ }
6373
+ }
6374
+ }
6375
+ const commentsRemoved = removeCommentNodesById(commentNodes, targetIds);
6376
+ const referencesRemoved = removeCommentAnchors(xmlDoc, targetIds);
6377
+ return {
6378
+ oxml: serializer.serializeToString(xmlDoc),
6379
+ hasChanges: commentsRemoved > 0 || referencesRemoved > 0,
6380
+ commentsRemoved,
6381
+ referencesRemoved,
6382
+ warnings
6383
+ };
5684
6384
  }
5685
6385
 
5686
6386
  // engine/oxml-engine.js
@@ -5689,18 +6389,70 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
5689
6389
  const author = options.author || getDefaultAuthor();
5690
6390
  const parser = createParser();
5691
6391
  const serializer = createSerializer();
5692
- const noChanges2 = () => ({ oxml, hasChanges: false });
6392
+ const finalize = (result) => {
6393
+ const withStatus = { ...result };
6394
+ if (!withStatus.status) {
6395
+ withStatus.status = withStatus.hasChanges ? "ok" : "no-op";
6396
+ }
6397
+ return withOoxmlSourceType(withStatus);
6398
+ };
6399
+ const noChanges2 = () => finalize({ oxml, hasChanges: false });
5693
6400
  let xmlDoc;
5694
6401
  try {
5695
6402
  xmlDoc = parser.parseFromString(oxml, "text/xml");
5696
6403
  } catch (e) {
5697
6404
  error("[OxmlEngine] Failed to parse OXML:", e);
5698
- return noChanges2();
6405
+ return finalize({
6406
+ oxml,
6407
+ hasChanges: false,
6408
+ status: "error",
6409
+ error: { code: "PARSE_ERROR", message: "Could not parse OOXML input." }
6410
+ });
5699
6411
  }
5700
6412
  const parseError = getXmlParseError(xmlDoc);
5701
6413
  if (parseError) {
5702
6414
  error("[OxmlEngine] XML parse error:", parseError.textContent);
5703
- return noChanges2();
6415
+ return finalize({
6416
+ oxml,
6417
+ hasChanges: false,
6418
+ status: "error",
6419
+ error: { code: "PARSE_ERROR", message: parseError.textContent || "Could not parse OOXML input." }
6420
+ });
6421
+ }
6422
+ seedRevisionIdsFromDocument(xmlDoc);
6423
+ if (containsTrackedChanges(xmlDoc)) {
6424
+ const existingRevisionsPolicy = options.existingRevisions || "reject-input";
6425
+ if (existingRevisionsPolicy === "accept-all-first") {
6426
+ log("[OxmlEngine] Existing revisions detected; accepting all input revisions before redlining");
6427
+ const accepted = acceptTrackedChangesInOoxml(oxml, { allAuthors: true });
6428
+ oxml = accepted.oxml;
6429
+ xmlDoc = parser.parseFromString(oxml, "text/xml");
6430
+ const acceptedParseError = getXmlParseError(xmlDoc);
6431
+ if (acceptedParseError) {
6432
+ error("[OxmlEngine] XML parse error after accepting existing revisions:", acceptedParseError.textContent);
6433
+ return finalize({
6434
+ oxml,
6435
+ hasChanges: false,
6436
+ status: "error",
6437
+ error: {
6438
+ code: "PARSE_ERROR",
6439
+ message: "Could not parse OOXML after accepting existing revisions."
6440
+ }
6441
+ });
6442
+ }
6443
+ seedRevisionIdsFromDocument(xmlDoc);
6444
+ } else {
6445
+ log("[OxmlEngine] Existing revisions detected; rejecting input per existingRevisions policy");
6446
+ return finalize({
6447
+ oxml,
6448
+ hasChanges: false,
6449
+ status: "error",
6450
+ error: {
6451
+ code: "EXISTING_REVISIONS",
6452
+ message: 'Input OOXML contains existing tracked changes. Pass existingRevisions: "accept-all-first" to normalize before redlining.'
6453
+ }
6454
+ });
6455
+ }
5704
6456
  }
5705
6457
  const initialTableCellContext = detectTableCellContext(xmlDoc, originalText, options);
5706
6458
  if (initialTableCellContext.hasTableWrapper && initialTableCellContext.targetParagraph && !options._isolatedTableCell) {
@@ -5717,6 +6469,19 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
5717
6469
  const hasFormatHints = formatHints.length > 0;
5718
6470
  const { existingFormatHints, textSpans, paragraphs } = extractFormattingFromOoxml(xmlDoc);
5719
6471
  const hasExistingFormatting = existingFormatHints.length > 0;
6472
+ const visibleText = textSpans.map((span) => textSpanVisibleText(span)).join("");
6473
+ if (hasTextChanges && typeof originalText === "string" && originalText.trim() && !originalText.includes("\n") && !visibleText.includes(originalText.trim()) && !visibleText.replace(/[\t\n\u2011]/g, "").includes(originalText.trim().replace(/[\t\n\u2011]/g, "")) && !normalizeTargetText(visibleText).includes(normalizeTargetText(originalText))) {
6474
+ log("[OxmlEngine] Target text not found in OOXML");
6475
+ return finalize({
6476
+ oxml,
6477
+ hasChanges: false,
6478
+ status: "error",
6479
+ error: {
6480
+ code: "TARGET_NOT_FOUND",
6481
+ message: "Original text was not found in the supplied OOXML."
6482
+ }
6483
+ });
6484
+ }
5720
6485
  let paragraphInfos = null;
5721
6486
  const getParagraphInfos = () => {
5722
6487
  if (!paragraphInfos) {
@@ -5780,12 +6545,12 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
5780
6545
  generateRedlines
5781
6546
  );
5782
6547
  if (tableCellCtx.hasTableWrapper && targetParagraph) {
5783
- return {
6548
+ return finalize({
5784
6549
  oxml: serializeParagraphOnly(xmlDoc, targetParagraph, serializer),
5785
6550
  hasChanges: removalResult.hasChanges
5786
- };
6551
+ });
5787
6552
  }
5788
- return removalResult;
6553
+ return finalize(removalResult);
5789
6554
  }
5790
6555
  if (!hasTextChanges && hasFormatHints) {
5791
6556
  log(`[OxmlEngine] Format-only change detected: ${formatHints.length} format hints`);
@@ -5799,14 +6564,14 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
5799
6564
  log("[OxmlEngine] Table cell context: applying formatting to target paragraph only");
5800
6565
  const formatResult = applyFormatOnlyWithOoxmlFallback(precomputedFormatContext);
5801
6566
  log("[OxmlEngine] Stripping table wrapper for table cell paragraph (format-only)");
5802
- return {
6567
+ return finalize({
5803
6568
  oxml: serializeParagraphOnly(xmlDoc, tableCellCtx.targetParagraph, serializer),
5804
6569
  hasChanges: formatResult.hasChanges
5805
- };
6570
+ });
5806
6571
  }
5807
- return applyFormatOnlyWithOoxmlFallback(precomputedFormatContext);
6572
+ return finalize(applyFormatOnlyWithOoxmlFallback(precomputedFormatContext));
5808
6573
  }
5809
- const tables = getElementsByTag(xmlDoc, "w:tbl");
6574
+ const tables = getElementsByTagNSOrTag(xmlDoc, NS_W, "tbl");
5810
6575
  const hasTables = tables.length > 0;
5811
6576
  const isMarkdownTable = /^\|.+\|/.test(cleanModifiedText.trim()) && cleanModifiedText.includes("\n");
5812
6577
  const isTargetList = isListTargetLoose(cleanModifiedText);
@@ -5814,10 +6579,10 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
5814
6579
  log(`[OxmlEngine] Mode: ${hasTables ? "SURGICAL" : "RECONSTRUCTION"}, formatHints: ${formatHints.length}, isMarkdownTable: ${isMarkdownTable}, isTargetList: ${isTargetList}, isTableCellParagraph: ${tableCellContext.isTableCellParagraph}`);
5815
6580
  if (isMarkdownTable && !hasTables) {
5816
6581
  log("[OxmlEngine] Text-to-table transformation: generating new table from Markdown");
5817
- return applyTextToTableTransformation(xmlDoc, cleanModifiedText, serializer, parser, author, generateRedlines);
6582
+ return finalize(applyTextToTableTransformation(xmlDoc, cleanModifiedText, serializer, parser, author, generateRedlines));
5818
6583
  }
5819
6584
  if (hasTables && isMarkdownTable) {
5820
- return applyTableReconciliation(xmlDoc, cleanModifiedText, serializer, parser, author, generateRedlines);
6585
+ return finalize(applyTableReconciliation(xmlDoc, cleanModifiedText, serializer, parser, author, generateRedlines));
5821
6586
  }
5822
6587
  if (hasTables) {
5823
6588
  const surgicalTarget = tableCellContext.hasTableWrapper && tableCellContext.targetParagraph ? tableCellContext.targetParagraph : null;
@@ -5836,9 +6601,9 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
5836
6601
  );
5837
6602
  if (tableCellContext.hasTableWrapper && result.hasChanges && tableCellContext.targetParagraph) {
5838
6603
  log("[OxmlEngine] Stripping table wrapper for table cell paragraph (surgical mode)");
5839
- return { oxml: serializeParagraphOnly(xmlDoc, tableCellContext.targetParagraph, serializer), hasChanges: true };
6604
+ return finalize({ oxml: serializeParagraphOnly(xmlDoc, tableCellContext.targetParagraph, serializer), hasChanges: true });
5840
6605
  }
5841
- return result;
6606
+ return finalize(result);
5842
6607
  }
5843
6608
  if (isTargetList) {
5844
6609
  log("[OxmlEngine] \u{1F3AF} Using reconciliation pipeline for list generation");
@@ -5851,11 +6616,22 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
5851
6616
  numberingXml: result.numberingXml
5852
6617
  });
5853
6618
  log(`[OxmlEngine] \u2705 Wrapped OOXML length: ${wrapped.length}`);
5854
- return { oxml: wrapped, hasChanges: true };
6619
+ return finalize({ oxml: wrapped, hasChanges: true });
5855
6620
  }
5856
6621
  return noChanges2();
5857
6622
  }
5858
- return applyReconstructionMode(xmlDoc, originalText, cleanModifiedText, serializer, author, formatHints, generateRedlines);
6623
+ return finalize(applyReconstructionMode(xmlDoc, originalText, cleanModifiedText, serializer, author, formatHints, generateRedlines));
6624
+ }
6625
+ function normalizeTargetText(text) {
6626
+ return String(text || "").replace(/[\t\n\u2011]/g, " ").replace(/\s+/g, " ").trim();
6627
+ }
6628
+ function textSpanVisibleText(span) {
6629
+ const node = span?.textElement;
6630
+ const localName = String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
6631
+ if (localName === "tab") return " ";
6632
+ if (localName === "br" || localName === "cr") return "\n";
6633
+ if (localName === "noBreakHyphen") return "\u2011";
6634
+ return node?.textContent || "";
5859
6635
  }
5860
6636
  function sanitizeAiResponse(text) {
5861
6637
  let cleaned = text;
@@ -6684,7 +7460,7 @@ function clearSingleLineListFallbackExplicitSequence(sequenceState, numberingKey
6684
7460
  if (!numberingKey) return;
6685
7461
  sequenceState.explicitByNumberingKey.delete(String(numberingKey));
6686
7462
  }
6687
- function getDirectWordChild(element, localName) {
7463
+ function getDirectWordChild2(element, localName) {
6688
7464
  if (!element) return null;
6689
7465
  return Array.from(element.childNodes || []).find(
6690
7466
  (node) => node && node.nodeType === 1 && node.namespaceURI === "http://schemas.openxmlformats.org/wordprocessingml/2006/main" && node.localName === localName
@@ -6701,7 +7477,7 @@ function enforceListBindingOnParagraphNodes(nodes, options = {}) {
6701
7477
  for (const paragraph of paragraphs) {
6702
7478
  const ownerDoc = paragraph.ownerDocument;
6703
7479
  if (!ownerDoc) continue;
6704
- let pPr = getDirectWordChild(paragraph, "pPr");
7480
+ let pPr = getDirectWordChild2(paragraph, "pPr");
6705
7481
  if (!pPr) {
6706
7482
  pPr = ownerDoc.createElementNS(
6707
7483
  "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
@@ -6710,14 +7486,14 @@ function enforceListBindingOnParagraphNodes(nodes, options = {}) {
6710
7486
  paragraph.insertBefore(pPr, paragraph.firstChild);
6711
7487
  }
6712
7488
  if (clearParagraphPropertyChanges) {
6713
- const pPrChange = getDirectWordChild(pPr, "pPrChange");
7489
+ const pPrChange = getDirectWordChild2(pPr, "pPrChange");
6714
7490
  if (pPrChange) pPr.removeChild(pPrChange);
6715
7491
  }
6716
7492
  if (removeListPropertyNode) {
6717
- const listPr = getDirectWordChild(pPr, "listPr");
7493
+ const listPr = getDirectWordChild2(pPr, "listPr");
6718
7494
  if (listPr) pPr.removeChild(listPr);
6719
7495
  }
6720
- let numPr = getDirectWordChild(pPr, "numPr");
7496
+ let numPr = getDirectWordChild2(pPr, "numPr");
6721
7497
  if (!numPr) {
6722
7498
  numPr = ownerDoc.createElementNS(
6723
7499
  "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
@@ -6725,7 +7501,7 @@ function enforceListBindingOnParagraphNodes(nodes, options = {}) {
6725
7501
  );
6726
7502
  pPr.appendChild(numPr);
6727
7503
  }
6728
- let ilvlEl = getDirectWordChild(numPr, "ilvl");
7504
+ let ilvlEl = getDirectWordChild2(numPr, "ilvl");
6729
7505
  if (!ilvlEl) {
6730
7506
  ilvlEl = ownerDoc.createElementNS(
6731
7507
  "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
@@ -6734,7 +7510,7 @@ function enforceListBindingOnParagraphNodes(nodes, options = {}) {
6734
7510
  numPr.appendChild(ilvlEl);
6735
7511
  }
6736
7512
  ilvlEl.setAttribute("w:val", String(ilvl));
6737
- let numIdEl = getDirectWordChild(numPr, "numId");
7513
+ let numIdEl = getDirectWordChild2(numPr, "numId");
6738
7514
  if (!numIdEl) {
6739
7515
  numIdEl = ownerDoc.createElementNS(
6740
7516
  "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
@@ -7392,7 +8168,7 @@ function getWordParagraphs(doc) {
7392
8168
  if (namespaced.length > 0) return namespaced;
7393
8169
  return Array.from(doc.getElementsByTagNameNS("*", "p")).filter((node) => node?.localName === "p");
7394
8170
  }
7395
- function getDirectWordChild2(node, localName) {
8171
+ function getDirectWordChild3(node, localName) {
7396
8172
  const children = Array.from(node?.childNodes || []);
7397
8173
  for (const child of children) {
7398
8174
  if (child?.nodeType !== 1) continue;
@@ -7439,7 +8215,7 @@ function readRunText(run) {
7439
8215
  return text;
7440
8216
  }
7441
8217
  function getRunFormatting(run) {
7442
- const rPr = getDirectWordChild2(run, "rPr");
8218
+ const rPr = getDirectWordChild3(run, "rPr");
7443
8219
  if (!rPr) return { bold: false, italic: false };
7444
8220
  const rStyle = getWordDescendants(rPr, "rStyle")[0] || null;
7445
8221
  const rStyleValue = getWordAttribute(rStyle, ["w:val", "val"]).toLowerCase();
@@ -7455,6 +8231,7 @@ function collectParagraphSegments(paragraph) {
7455
8231
  const runs = Array.from(paragraph?.getElementsByTagNameNS?.(NS_W, "r") || []);
7456
8232
  for (const run of runs) {
7457
8233
  if (hasWordAncestorWithin(run, "del", paragraph)) continue;
8234
+ if (hasWordAncestorWithin(run, "moveFrom", paragraph)) continue;
7458
8235
  const text = readRunText(run);
7459
8236
  if (!text) continue;
7460
8237
  segments.push({
@@ -7485,7 +8262,7 @@ function wrapRunMarkdown(text, format) {
7485
8262
  return `${leading}${wrapped}${trailing}`;
7486
8263
  }
7487
8264
  function parseHeadingLevel(paragraph) {
7488
- const pPr = getDirectWordChild2(paragraph, "pPr");
8265
+ const pPr = getDirectWordChild3(paragraph, "pPr");
7489
8266
  if (!pPr) return null;
7490
8267
  const pStyle = getWordDescendants(pPr, "pStyle")[0] || null;
7491
8268
  const styleVal = getWordAttribute(pStyle, ["w:val", "val"]);
@@ -7505,7 +8282,7 @@ function parseHeadingLevel(paragraph) {
7505
8282
  return null;
7506
8283
  }
7507
8284
  function parseListInfo(paragraph) {
7508
- const pPr = getDirectWordChild2(paragraph, "pPr");
8285
+ const pPr = getDirectWordChild3(paragraph, "pPr");
7509
8286
  if (!pPr) return null;
7510
8287
  const numPr = getWordDescendants(pPr, "numPr")[0] || null;
7511
8288
  if (!numPr) return null;
@@ -7626,11 +8403,11 @@ function findTextInParagraphIndex(paragraphIndex, searchText) {
7626
8403
  };
7627
8404
  }
7628
8405
  function cloneRunWithText(xmlDoc, rPr, newText) {
7629
- const newRun = xmlDoc.createElementNS(NS_W, "w:r");
8406
+ const newRun = createWordElement(xmlDoc, "w:r");
7630
8407
  if (rPr) {
7631
8408
  newRun.appendChild(rPr.cloneNode(true));
7632
8409
  }
7633
- const newTextNode = xmlDoc.createElementNS(NS_W, "w:t");
8410
+ const newTextNode = createWordElement(xmlDoc, "w:t");
7634
8411
  newTextNode.setAttribute("xml:space", "preserve");
7635
8412
  newTextNode.textContent = newText;
7636
8413
  newRun.appendChild(newTextNode);
@@ -7642,12 +8419,12 @@ function injectMarkersIntoParagraph(xmlDoc, paragraph, textToFind, commentId, pa
7642
8419
  if (!location.found || !location.startRun) {
7643
8420
  return false;
7644
8421
  }
7645
- const startMarker = xmlDoc.createElementNS(NS_W, "w:commentRangeStart");
8422
+ const startMarker = createWordElement(xmlDoc, "w:commentRangeStart");
7646
8423
  startMarker.setAttribute("w:id", String(commentId));
7647
- const endMarker = xmlDoc.createElementNS(NS_W, "w:commentRangeEnd");
8424
+ const endMarker = createWordElement(xmlDoc, "w:commentRangeEnd");
7648
8425
  endMarker.setAttribute("w:id", String(commentId));
7649
- const referenceRun = xmlDoc.createElementNS(NS_W, "w:r");
7650
- const reference = xmlDoc.createElementNS(NS_W, "w:commentReference");
8426
+ const referenceRun = createWordElement(xmlDoc, "w:r");
8427
+ const reference = createWordElement(xmlDoc, "w:commentReference");
7651
8428
  reference.setAttribute("w:id", String(commentId));
7652
8429
  referenceRun.appendChild(reference);
7653
8430
  if (location.startRun === location.endRun) {
@@ -8009,13 +8786,13 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
8009
8786
  const author = options?.author || getDefaultAuthor();
8010
8787
  let rPrElement = rPr;
8011
8788
  if (!rPrElement) {
8012
- rPrElement = doc.createElementNS(NS_W7, "w:rPr");
8789
+ rPrElement = createWordElement(doc, "w:rPr");
8013
8790
  } else {
8014
8791
  rPrElement = rPr.cloneNode(true);
8015
8792
  }
8016
8793
  let previousRPrState = null;
8017
8794
  if (generateRedlines) {
8018
- previousRPrState = doc.createElementNS(NS_W7, "w:rPr");
8795
+ previousRPrState = createWordElement(doc, "w:rPr");
8019
8796
  Array.from(rPrElement.childNodes).forEach((child) => {
8020
8797
  if (child.nodeName !== "w:rPrChange") {
8021
8798
  previousRPrState.appendChild(child.cloneNode(true));
@@ -8024,14 +8801,15 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
8024
8801
  }
8025
8802
  const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W7, "highlight");
8026
8803
  Array.from(existingHighlight).forEach((el) => el.remove());
8027
- const highlightEl = doc.createElementNS(NS_W7, "w:highlight");
8804
+ const highlightEl = createWordElement(doc, "w:highlight");
8028
8805
  highlightEl.setAttributeNS(NS_W7, "w:val", ooxmlColor);
8029
8806
  rPrElement.appendChild(highlightEl);
8030
8807
  if (generateRedlines && previousRPrState) {
8031
- const rPrChange = doc.createElementNS(NS_W7, "w:rPrChange");
8032
- rPrChange.setAttributeNS(NS_W7, "w:id", Math.floor(Math.random() * 9999999).toString());
8033
- rPrChange.setAttributeNS(NS_W7, "w:author", author);
8034
- rPrChange.setAttributeNS(NS_W7, "w:date", (/* @__PURE__ */ new Date()).toISOString());
8808
+ const rPrChange = createWordElement(doc, "w:rPrChange");
8809
+ const metadata = createRevisionMetadata(author);
8810
+ rPrChange.setAttribute("w:id", String(metadata.id));
8811
+ rPrChange.setAttribute("w:author", metadata.author);
8812
+ rPrChange.setAttribute("w:date", metadata.date);
8035
8813
  rPrChange.appendChild(previousRPrState);
8036
8814
  const existingChange = rPrElement.getElementsByTagNameNS(NS_W7, "rPrChange");
8037
8815
  Array.from(existingChange).forEach((el) => el.remove());
@@ -8067,7 +8845,7 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
8067
8845
  const prefixRun = run.cloneNode(true);
8068
8846
  const tNodes = prefixRun.getElementsByTagNameNS(NS_W7, "t");
8069
8847
  Array.from(tNodes).forEach((t) => t.remove());
8070
- const newT = doc.createElementNS(NS_W7, "w:t");
8848
+ const newT = createWordElement(doc, "w:t");
8071
8849
  newT.setAttribute("xml:space", "preserve");
8072
8850
  newT.textContent = prefixText;
8073
8851
  prefixRun.appendChild(newT);
@@ -8077,7 +8855,7 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
8077
8855
  const matchRun = run.cloneNode(true);
8078
8856
  const tNodes = matchRun.getElementsByTagNameNS(NS_W7, "t");
8079
8857
  Array.from(tNodes).forEach((t) => t.remove());
8080
- const newT = doc.createElementNS(NS_W7, "w:t");
8858
+ const newT = createWordElement(doc, "w:t");
8081
8859
  newT.setAttribute("xml:space", "preserve");
8082
8860
  newT.textContent = matchText;
8083
8861
  matchRun.appendChild(newT);
@@ -8095,7 +8873,7 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
8095
8873
  const suffixRun = run.cloneNode(true);
8096
8874
  const tNodes = suffixRun.getElementsByTagNameNS(NS_W7, "t");
8097
8875
  Array.from(tNodes).forEach((t) => t.remove());
8098
- const newT = doc.createElementNS(NS_W7, "w:t");
8876
+ const newT = createWordElement(doc, "w:t");
8099
8877
  newT.setAttribute("xml:space", "preserve");
8100
8878
  newT.textContent = suffixText;
8101
8879
  suffixRun.appendChild(newT);
@@ -8572,7 +9350,7 @@ async function applyRedlineToOxml2(oxml, originalText, modifiedText, options = {
8572
9350
  const result = await applyRedlineToOxml(oxml, originalText, modifiedText, options);
8573
9351
  if (result?.useNativeApi && typeof result?.oxml !== "string") {
8574
9352
  const existingWarnings = Array.isArray(result?.warnings) ? result.warnings : [];
8575
- return {
9353
+ return withOoxmlSourceType({
8576
9354
  ...result,
8577
9355
  oxml,
8578
9356
  hasChanges: false,
@@ -8580,7 +9358,7 @@ async function applyRedlineToOxml2(oxml, originalText, modifiedText, options = {
8580
9358
  ...existingWarnings,
8581
9359
  "Standalone mode cannot execute native Word API fallback for this operation."
8582
9360
  ]
8583
- };
9361
+ });
8584
9362
  }
8585
9363
  return result;
8586
9364
  }
@@ -8636,14 +9414,14 @@ async function applyRedlineToOxmlWithListFallback(oxml, originalText, modifiedTe
8636
9414
  numberingXml: fallbackResult2.numberingXml
8637
9415
  });
8638
9416
  const fallbackWarnings2 = Array.isArray(fallbackResult2?.warnings) ? fallbackResult2.warnings : [];
8639
- return {
9417
+ return withOoxmlSourceType({
8640
9418
  oxml: wrappedOxml2,
8641
9419
  hasChanges: true,
8642
9420
  warnings: fallbackWarnings2,
8643
9421
  listStructuralFallbackApplied: true,
8644
9422
  listStructuralFallbackKey: fallbackResult2.listStructuralFallbackKey || null,
8645
9423
  listStructuralFallbackNumberingXml: fallbackResult2.numberingXml || null
8646
- };
9424
+ });
8647
9425
  }
8648
9426
  preflightFallbackWarnings = Array.isArray(fallbackResult2?.warnings) ? fallbackResult2.warnings : [];
8649
9427
  }
@@ -8694,7 +9472,7 @@ async function applyRedlineToOxmlWithListFallback(oxml, originalText, modifiedTe
8694
9472
  });
8695
9473
  const existingWarnings = Array.isArray(baseResult?.warnings) ? baseResult.warnings : [];
8696
9474
  const fallbackWarnings = Array.isArray(fallbackResult?.warnings) ? fallbackResult.warnings : [];
8697
- return {
9475
+ return withOoxmlSourceType({
8698
9476
  ...baseResult,
8699
9477
  oxml: wrappedOxml,
8700
9478
  hasChanges: true,
@@ -8702,7 +9480,7 @@ async function applyRedlineToOxmlWithListFallback(oxml, originalText, modifiedTe
8702
9480
  listStructuralFallbackApplied: true,
8703
9481
  listStructuralFallbackKey: fallbackResult.listStructuralFallbackKey || null,
8704
9482
  listStructuralFallbackNumberingXml: fallbackResult.numberingXml || null
8705
- };
9483
+ });
8706
9484
  }
8707
9485
  export {
8708
9486
  ContainerKind,
@@ -8714,6 +9492,7 @@ export {
8714
9492
  RoutePlanKind,
8715
9493
  RunKind,
8716
9494
  WORD_MAIN_NS,
9495
+ acceptTrackedChangesInOoxml,
8717
9496
  applyFormattingRemovalToOoxml,
8718
9497
  applyHighlightToOoxml,
8719
9498
  applyRedlineToOxml2 as applyRedlineToOxml,
@@ -8729,7 +9508,9 @@ export {
8729
9508
  collectContiguousListParagraphBlock,
8730
9509
  configureLogger,
8731
9510
  configureXmlProvider,
9511
+ containsTrackedChanges,
8732
9512
  createDynamicNumberingIdState,
9513
+ deleteCommentsByAuthorInOoxml,
8733
9514
  enforceListBindingOnParagraphNodes,
8734
9515
  ensureCommentsArtifactsInZip,
8735
9516
  ensureNumberingArtifactsInZip,
@@ -8775,6 +9556,7 @@ export {
8775
9556
  preprocessMarkdown,
8776
9557
  reconcileMarkdownTableOoxml,
8777
9558
  recordSingleLineListFallbackExplicitSequence,
9559
+ rejectTrackedChangesInOoxml,
8778
9560
  remapNumberingPayloadForDocument,
8779
9561
  removeFormattingFromRPr,
8780
9562
  reserveNextNumberingId,