bun-docx 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +27 -10
  2. package/dist/index.js +1419 -217
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13570,36 +13570,81 @@ var init_xml_node = __esm(() => {
13570
13570
  });
13571
13571
 
13572
13572
  // src/core/parser/run-ops.ts
13573
+ function isSubtractiveTrackedChangeWrapper(tag) {
13574
+ return tag === "w:del" || tag === "w:moveFrom";
13575
+ }
13573
13576
  function runTextLength(run) {
13574
13577
  let total = 0;
13575
13578
  for (const child of run.children) {
13576
13579
  if (child.tag === "w:t")
13577
13580
  total += child.collectText().length;
13581
+ else if (SINGLE_CHAR_TAGS.has(child.tag))
13582
+ total += 1;
13578
13583
  }
13579
13584
  return total;
13580
13585
  }
13581
13586
  function sliceRun(run, start, end) {
13582
13587
  const sliced = new XmlNode2("w:r", { ...run.attributes });
13583
- let consumed = 0;
13588
+ let offset = 0;
13584
13589
  for (const child of run.children) {
13585
- if (child.tag === "w:t") {
13590
+ if (child.tag === "w:rPr") {
13591
+ sliced.children.push(child.clone());
13592
+ continue;
13593
+ }
13594
+ if (child.tag === "w:t" || child.tag === "w:delText") {
13586
13595
  const text = child.collectText();
13587
- const localStart = Math.max(0, start - consumed);
13588
- const localEnd = Math.min(text.length, end - consumed);
13596
+ const localStart = Math.max(0, start - offset);
13597
+ const localEnd = Math.min(text.length, end - offset);
13589
13598
  if (localStart < localEnd) {
13590
- const slicedText = new XmlNode2("w:t", { "xml:space": "preserve" });
13599
+ const slicedText = new XmlNode2(child.tag, { "xml:space": "preserve" });
13591
13600
  slicedText.children.push(XmlNode2.textNode(text.slice(localStart, localEnd)));
13592
13601
  sliced.children.push(slicedText);
13593
13602
  }
13594
- consumed += text.length;
13603
+ offset += text.length;
13604
+ continue;
13605
+ }
13606
+ if (SINGLE_CHAR_TAGS.has(child.tag)) {
13607
+ if (offset >= start && offset < end) {
13608
+ sliced.children.push(child.clone());
13609
+ }
13610
+ offset += 1;
13595
13611
  continue;
13596
13612
  }
13597
- sliced.children.push(child.clone());
13613
+ if (offset >= start && offset < end) {
13614
+ sliced.children.push(child.clone());
13615
+ }
13598
13616
  }
13599
13617
  return sliced;
13600
13618
  }
13619
+ function isRunBearingWrapper(tag) {
13620
+ return RUN_BEARING_WRAPPER_TAGS.has(tag);
13621
+ }
13622
+ function sumRunBearingTextLength(children) {
13623
+ let total = 0;
13624
+ for (const child of children) {
13625
+ if (child.tag === "w:r") {
13626
+ total += runTextLength(child);
13627
+ continue;
13628
+ }
13629
+ if (isRunBearingWrapper(child.tag)) {
13630
+ total += sumRunBearingTextLength(child.children);
13631
+ }
13632
+ }
13633
+ return total;
13634
+ }
13635
+ var SINGLE_CHAR_TAGS, RUN_BEARING_WRAPPER_TAGS;
13601
13636
  var init_run_ops = __esm(() => {
13602
13637
  init_xml_node();
13638
+ SINGLE_CHAR_TAGS = new Set(["w:noBreakHyphen", "w:softHyphen", "w:sym"]);
13639
+ RUN_BEARING_WRAPPER_TAGS = new Set([
13640
+ "w:ins",
13641
+ "w:del",
13642
+ "w:moveFrom",
13643
+ "w:moveTo",
13644
+ "w:hyperlink",
13645
+ "w:fldSimple",
13646
+ "w:smartTag"
13647
+ ]);
13603
13648
  });
13604
13649
 
13605
13650
  // src/core/parser/index.ts
@@ -13760,6 +13805,311 @@ var init_package = __esm(() => {
13760
13805
  };
13761
13806
  });
13762
13807
 
13808
+ // src/core/ast/sym.ts
13809
+ function decodeSym(font, charHex) {
13810
+ const codepoint = Number.parseInt(charHex, 16);
13811
+ if (!Number.isFinite(codepoint))
13812
+ return "";
13813
+ const fontKey = font.toLowerCase().trim();
13814
+ const table = TABLES.get(fontKey);
13815
+ if (table) {
13816
+ const mapped = table.get(codepoint);
13817
+ if (mapped !== undefined)
13818
+ return mapped;
13819
+ }
13820
+ if (table && codepoint >= 61440 && codepoint <= 61695) {
13821
+ const aliased = table.get(codepoint - 61440);
13822
+ if (aliased !== undefined)
13823
+ return aliased;
13824
+ }
13825
+ if (table && codepoint >= 32 && codepoint <= 255) {
13826
+ const aliased = table.get(codepoint + 61440);
13827
+ if (aliased !== undefined)
13828
+ return aliased;
13829
+ }
13830
+ try {
13831
+ return String.fromCodePoint(codepoint);
13832
+ } catch {
13833
+ return "";
13834
+ }
13835
+ }
13836
+ var SYMBOL, WINGDINGS, WINGDINGS_2, WEBDINGS, ZAPF_DINGBATS, TABLES;
13837
+ var init_sym = __esm(() => {
13838
+ SYMBOL = [
13839
+ [33, "!"],
13840
+ [34, "\u2200"],
13841
+ [35, "#"],
13842
+ [36, "\u2203"],
13843
+ [37, "%"],
13844
+ [38, "&"],
13845
+ [39, "\u220B"],
13846
+ [40, "("],
13847
+ [41, ")"],
13848
+ [42, "\u2217"],
13849
+ [43, "+"],
13850
+ [44, ","],
13851
+ [45, "\u2212"],
13852
+ [46, "."],
13853
+ [47, "/"],
13854
+ [48, "0"],
13855
+ [49, "1"],
13856
+ [50, "2"],
13857
+ [51, "3"],
13858
+ [52, "4"],
13859
+ [53, "5"],
13860
+ [54, "6"],
13861
+ [55, "7"],
13862
+ [56, "8"],
13863
+ [57, "9"],
13864
+ [58, ":"],
13865
+ [59, ";"],
13866
+ [60, "<"],
13867
+ [61, "="],
13868
+ [62, ">"],
13869
+ [63, "?"],
13870
+ [64, "\u2245"],
13871
+ [65, "\u0391"],
13872
+ [66, "\u0392"],
13873
+ [67, "\u03A7"],
13874
+ [68, "\u0394"],
13875
+ [69, "\u0395"],
13876
+ [70, "\u03A6"],
13877
+ [71, "\u0393"],
13878
+ [72, "\u0397"],
13879
+ [73, "\u0399"],
13880
+ [74, "\u03D1"],
13881
+ [75, "\u039A"],
13882
+ [76, "\u039B"],
13883
+ [77, "\u039C"],
13884
+ [78, "\u039D"],
13885
+ [79, "\u039F"],
13886
+ [80, "\u03A0"],
13887
+ [81, "\u0398"],
13888
+ [82, "\u03A1"],
13889
+ [83, "\u03A3"],
13890
+ [84, "\u03A4"],
13891
+ [85, "\u03A5"],
13892
+ [86, "\u03C2"],
13893
+ [87, "\u03A9"],
13894
+ [88, "\u039E"],
13895
+ [89, "\u03A8"],
13896
+ [90, "\u0396"],
13897
+ [91, "["],
13898
+ [92, "\u2234"],
13899
+ [93, "]"],
13900
+ [94, "\u22A5"],
13901
+ [95, "_"],
13902
+ [96, "\u203E"],
13903
+ [97, "\u03B1"],
13904
+ [98, "\u03B2"],
13905
+ [99, "\u03C7"],
13906
+ [100, "\u03B4"],
13907
+ [101, "\u03B5"],
13908
+ [102, "\u03C6"],
13909
+ [103, "\u03B3"],
13910
+ [104, "\u03B7"],
13911
+ [105, "\u03B9"],
13912
+ [106, "\u03D5"],
13913
+ [107, "\u03BA"],
13914
+ [108, "\u03BB"],
13915
+ [109, "\u03BC"],
13916
+ [110, "\u03BD"],
13917
+ [111, "\u03BF"],
13918
+ [112, "\u03C0"],
13919
+ [113, "\u03B8"],
13920
+ [114, "\u03C1"],
13921
+ [115, "\u03C3"],
13922
+ [116, "\u03C4"],
13923
+ [117, "\u03C5"],
13924
+ [118, "\u03D6"],
13925
+ [119, "\u03C9"],
13926
+ [120, "\u03BE"],
13927
+ [121, "\u03C8"],
13928
+ [122, "\u03B6"],
13929
+ [123, "{"],
13930
+ [124, "|"],
13931
+ [125, "}"],
13932
+ [126, "\u223C"],
13933
+ [160, "\u20AC"],
13934
+ [161, "\u03D2"],
13935
+ [162, "\u2032"],
13936
+ [163, "\u2264"],
13937
+ [164, "\u2044"],
13938
+ [165, "\u221E"],
13939
+ [166, "\u0192"],
13940
+ [167, "\u2663"],
13941
+ [168, "\u2666"],
13942
+ [169, "\u2665"],
13943
+ [170, "\u2660"],
13944
+ [171, "\u2194"],
13945
+ [172, "\u2190"],
13946
+ [173, "\u2191"],
13947
+ [174, "\u2192"],
13948
+ [175, "\u2193"],
13949
+ [176, "\xB0"],
13950
+ [177, "\xB1"],
13951
+ [178, "\u2033"],
13952
+ [179, "\u2265"],
13953
+ [180, "\xD7"],
13954
+ [181, "\u221D"],
13955
+ [182, "\u2202"],
13956
+ [183, "\u2022"],
13957
+ [184, "\xF7"],
13958
+ [185, "\u2260"],
13959
+ [186, "\u2261"],
13960
+ [187, "\u2248"],
13961
+ [188, "\u2026"],
13962
+ [189, "|"],
13963
+ [190, "\u2014"],
13964
+ [191, "\u21B5"],
13965
+ [192, "\u2135"],
13966
+ [193, "\u2111"],
13967
+ [194, "\u211C"],
13968
+ [195, "\u2118"],
13969
+ [196, "\u2297"],
13970
+ [197, "\u2295"],
13971
+ [198, "\u2205"],
13972
+ [199, "\u2229"],
13973
+ [200, "\u222A"],
13974
+ [201, "\u2283"],
13975
+ [202, "\u2287"],
13976
+ [203, "\u2284"],
13977
+ [204, "\u2282"],
13978
+ [205, "\u2286"],
13979
+ [206, "\u2208"],
13980
+ [207, "\u2209"],
13981
+ [208, "\u2220"],
13982
+ [209, "\u2207"],
13983
+ [210, "\xAE"],
13984
+ [211, "\xA9"],
13985
+ [212, "\u2122"],
13986
+ [213, "\u220F"],
13987
+ [214, "\u221A"],
13988
+ [215, "\xB7"],
13989
+ [216, "\xAC"],
13990
+ [217, "\u2227"],
13991
+ [218, "\u2228"],
13992
+ [219, "\u21D4"],
13993
+ [220, "\u21D0"],
13994
+ [221, "\u21D1"],
13995
+ [222, "\u21D2"],
13996
+ [223, "\u21D3"],
13997
+ [224, "\u25CA"],
13998
+ [225, "\u27E8"],
13999
+ [229, "\u2211"],
14000
+ [241, "\u27E9"],
14001
+ [242, "\u222B"]
14002
+ ];
14003
+ WINGDINGS = [
14004
+ [61474, "\u2702"],
14005
+ [61479, "\u260E"],
14006
+ [61480, "\u2706"],
14007
+ [61481, "\u2709"],
14008
+ [61501, "\u231B"],
14009
+ [61514, "\u263A"],
14010
+ [61515, "\uD83D\uDE10"],
14011
+ [61516, "\u2639"],
14012
+ [61519, "\u2620"],
14013
+ [61520, "\uD83C\uDFF3"],
14014
+ [61521, "\uD83C\uDFF4"],
14015
+ [61528, "\u261C"],
14016
+ [61529, "\u261E"],
14017
+ [61530, "\u261D"],
14018
+ [61531, "\u261F"],
14019
+ [61548, "\u2751"],
14020
+ [61549, "\u2752"],
14021
+ [61550, "\u25AA"],
14022
+ [61551, "\u25A1"],
14023
+ [61553, "\u25C6"],
14024
+ [61555, "\u25CF"],
14025
+ [61556, "\u25A0"],
14026
+ [61557, "\u25CF"],
14027
+ [61558, "\u25C6"],
14028
+ [61559, "\u2756"],
14029
+ [61607, "\u25AA"],
14030
+ [61608, "\u25AB"],
14031
+ [61664, "\u2190"],
14032
+ [61665, "\u2192"],
14033
+ [61666, "\u2191"],
14034
+ [61667, "\u2193"],
14035
+ [61668, "\u2196"],
14036
+ [61669, "\u2197"],
14037
+ [61670, "\u2198"],
14038
+ [61671, "\u2199"],
14039
+ [61672, "\u2194"],
14040
+ [61673, "\u2195"],
14041
+ [61690, "\u2713"],
14042
+ [61691, "\u2713"],
14043
+ [61692, "\u2713"],
14044
+ [61693, "\u2717"],
14045
+ [61694, "\u2717"]
14046
+ ];
14047
+ WINGDINGS_2 = [
14048
+ [61520, "\u25B6"],
14049
+ [61521, "\u25C0"],
14050
+ [61522, "\u25B2"],
14051
+ [61523, "\u25BC"],
14052
+ [61656, "\u2752"],
14053
+ [61657, "\u2610"],
14054
+ [61691, "\u2611"],
14055
+ [61692, "\u2612"],
14056
+ [61693, "\u2713"],
14057
+ [61694, "\u2717"],
14058
+ [61695, "\u2718"]
14059
+ ];
14060
+ WEBDINGS = [
14061
+ [61473, "\uD83D\uDD77"],
14062
+ [61476, "\uD83D\uDC41"],
14063
+ [61477, "\uD83D\uDC42"],
14064
+ [61490, "\uD83C\uDF10"],
14065
+ [61497, "\u2709"],
14066
+ [61500, "\uD83D\uDCC5"],
14067
+ [61504, "\uD83C\uDFE0"],
14068
+ [61514, "\uD83D\uDCDE"],
14069
+ [61528, "\uD83D\uDD0D"],
14070
+ [61543, "\uD83D\uDD12"],
14071
+ [61544, "\uD83D\uDD13"],
14072
+ [61560, "\u270F"],
14073
+ [61604, "\u2605"]
14074
+ ];
14075
+ ZAPF_DINGBATS = [
14076
+ [33, "\u2701"],
14077
+ [34, "\u2702"],
14078
+ [35, "\u2703"],
14079
+ [36, "\u2704"],
14080
+ [43, "\u271A"],
14081
+ [51, "\u2713"],
14082
+ [52, "\u2714"],
14083
+ [53, "\u2715"],
14084
+ [54, "\u2716"],
14085
+ [55, "\u2717"],
14086
+ [56, "\u2718"],
14087
+ [57, "\u2719"],
14088
+ [58, "\u271A"],
14089
+ [59, "\u271B"],
14090
+ [60, "\u271C"],
14091
+ [74, "\u2740"],
14092
+ [75, "\u2741"],
14093
+ [76, "\u2742"],
14094
+ [77, "\u2743"],
14095
+ [78, "\u2744"],
14096
+ [79, "\u2745"],
14097
+ [108, "\u25CF"],
14098
+ [110, "\u25A0"],
14099
+ [113, "\u25C6"]
14100
+ ];
14101
+ TABLES = new Map([
14102
+ ["symbol", new Map(SYMBOL)],
14103
+ ["wingdings", new Map(WINGDINGS)],
14104
+ ["wingdings 2", new Map(WINGDINGS_2)],
14105
+ ["wingdings 3", new Map(WINGDINGS_2)],
14106
+ ["webdings", new Map(WEBDINGS)],
14107
+ ["zapfdingbats", new Map(ZAPF_DINGBATS)],
14108
+ ["zapf dingbats", new Map(ZAPF_DINGBATS)],
14109
+ ["itc zapf dingbats", new Map(ZAPF_DINGBATS)]
14110
+ ]);
14111
+ });
14112
+
13763
14113
  // src/core/ast/read.ts
13764
14114
  function buildDoc(view, path) {
13765
14115
  readRelationships(view);
@@ -13775,6 +14125,7 @@ function buildDoc(view, path) {
13775
14125
  const state = {
13776
14126
  imageIndex: 0,
13777
14127
  hyperlinkIndex: 0,
14128
+ trackedChangeIndex: 0,
13778
14129
  commentAnchors: new Map,
13779
14130
  openComments: new Map
13780
14131
  };
@@ -13870,8 +14221,8 @@ function walkRunContainer(context, container, trackedChange, hyperlink) {
13870
14221
  continue;
13871
14222
  }
13872
14223
  if (child.tag === "w:r") {
13873
- const run = readRun(context.view, child, context.activeComments, trackedChange, hyperlink, context.state);
13874
- if (run) {
14224
+ const runs = readRun(context.view, child, context.activeComments, trackedChange, hyperlink, context.state);
14225
+ for (const run of runs) {
13875
14226
  if (run.type === "text")
13876
14227
  context.offsetRef.value += run.text.length;
13877
14228
  context.paragraph.runs.push(run);
@@ -13900,19 +14251,30 @@ function walkRunContainer(context, container, trackedChange, hyperlink) {
13900
14251
  }
13901
14252
  continue;
13902
14253
  }
13903
- if (child.tag === "w:ins" || child.tag === "w:del") {
14254
+ if (child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:moveFrom" || child.tag === "w:moveTo") {
14255
+ const trackedChangeId = `tc${context.state.trackedChangeIndex++}`;
13904
14256
  const change = {
13905
- kind: child.tag === "w:ins" ? "ins" : "del",
14257
+ id: trackedChangeId,
14258
+ kind: TRACKED_CHANGE_KIND_BY_TAG[child.tag],
13906
14259
  author: child.getAttribute("w:author") ?? "",
13907
14260
  date: child.getAttribute("w:date") ?? "",
13908
14261
  revisionId: child.getAttribute("w:id") ?? ""
13909
14262
  };
14263
+ context.view.trackedChangeReferences.set(trackedChangeId, {
14264
+ node: child,
14265
+ parent: container.children,
14266
+ blockId: context.blockId
14267
+ });
13910
14268
  walkRunContainer(context, child, change, hyperlink);
13911
14269
  continue;
13912
14270
  }
13913
14271
  if (child.tag === "w:hyperlink") {
13914
14272
  const link = readHyperlinkProperties(context.view, child, container.children, context.state);
13915
14273
  walkRunContainer(context, child, trackedChange, link);
14274
+ continue;
14275
+ }
14276
+ if (child.tag === "w:fldSimple" || child.tag === "w:smartTag") {
14277
+ walkRunContainer(context, child, trackedChange, hyperlink);
13916
14278
  }
13917
14279
  }
13918
14280
  }
@@ -13967,48 +14329,83 @@ function applyParagraphProperties(paragraph, paragraphProperties) {
13967
14329
  }
13968
14330
  function readRun(view, node, activeComments, trackedChange, hyperlink, state) {
13969
14331
  const runProperties = node.findChild("w:rPr");
14332
+ const out = [];
14333
+ let pendingText = "";
14334
+ function flushText() {
14335
+ if (pendingText.length === 0)
14336
+ return;
14337
+ const run = { type: "text", text: pendingText };
14338
+ if (runProperties)
14339
+ applyRunProperties(run, runProperties);
14340
+ if (activeComments.size > 0)
14341
+ run.comments = [...activeComments];
14342
+ if (trackedChange)
14343
+ run.trackedChange = trackedChange;
14344
+ if (hyperlink)
14345
+ run.hyperlink = hyperlink;
14346
+ out.push(run);
14347
+ pendingText = "";
14348
+ }
13970
14349
  for (const child of node.children) {
14350
+ if (child.tag === "w:rPr")
14351
+ continue;
14352
+ if (child.tag === "w:t" || child.tag === "w:delText") {
14353
+ pendingText += child.collectText();
14354
+ continue;
14355
+ }
14356
+ if (child.tag === "w:noBreakHyphen") {
14357
+ pendingText += "\u2011";
14358
+ continue;
14359
+ }
14360
+ if (child.tag === "w:softHyphen") {
14361
+ pendingText += "\xAD";
14362
+ continue;
14363
+ }
14364
+ if (child.tag === "w:sym") {
14365
+ const font = child.getAttribute("w:font") ?? "";
14366
+ const charHex = child.getAttribute("w:char") ?? "";
14367
+ pendingText += decodeSym(font, charHex);
14368
+ continue;
14369
+ }
13971
14370
  if (child.tag === "w:drawing") {
14371
+ flushText();
13972
14372
  const drawing = readDrawing(view, child, state);
13973
14373
  if (drawing)
13974
- return drawing;
14374
+ out.push(drawing);
14375
+ continue;
13975
14376
  }
13976
- if (child.tag === "w:br") {
13977
- const kind = child.getAttribute("w:type") ?? "line";
13978
- return { type: "break", kind };
14377
+ if (child.tag === "w:pict" || child.tag === "w:object") {
14378
+ flushText();
14379
+ out.push({ type: "chart", kind: "drawing" });
14380
+ continue;
13979
14381
  }
13980
- if (child.tag === "w:tab") {
13981
- return { type: "tab" };
14382
+ if (child.tag === "w:br" || child.tag === "w:cr") {
14383
+ flushText();
14384
+ const kind = child.tag === "w:cr" ? "line" : child.getAttribute("w:type") ?? "line";
14385
+ out.push({ type: "break", kind });
14386
+ continue;
14387
+ }
14388
+ if (child.tag === "w:tab" || child.tag === "w:ptab") {
14389
+ flushText();
14390
+ out.push({ type: "tab" });
14391
+ continue;
13982
14392
  }
13983
14393
  if (child.tag === "w:footnoteReference") {
14394
+ flushText();
13984
14395
  const id = child.getAttribute("w:id");
13985
14396
  if (id)
13986
- return { type: "footnoteRef", kind: "footnote", id: `fn${id}` };
14397
+ out.push({ type: "footnoteRef", kind: "footnote", id: `fn${id}` });
14398
+ continue;
13987
14399
  }
13988
14400
  if (child.tag === "w:endnoteReference") {
14401
+ flushText();
13989
14402
  const id = child.getAttribute("w:id");
13990
14403
  if (id)
13991
- return { type: "footnoteRef", kind: "endnote", id: `en${id}` };
14404
+ out.push({ type: "footnoteRef", kind: "endnote", id: `en${id}` });
13992
14405
  }
13993
14406
  }
13994
- let combinedText = "";
13995
- for (const child of node.children) {
13996
- if (child.tag === "w:t" || child.tag === "w:delText") {
13997
- combinedText += child.collectText();
13998
- }
13999
- }
14000
- if (combinedText.length === 0)
14001
- return null;
14002
- const run = { type: "text", text: combinedText };
14003
- if (runProperties)
14004
- applyRunProperties(run, runProperties);
14005
- if (activeComments.size > 0)
14006
- run.comments = [...activeComments];
14007
- if (trackedChange)
14008
- run.trackedChange = trackedChange;
14009
- if (hyperlink)
14010
- run.hyperlink = hyperlink;
14011
- return run;
14407
+ flushText();
14408
+ return out;
14012
14409
  }
14013
14410
  function readDrawing(view, drawing, state) {
14014
14411
  const image = readImageFromDrawing(view, drawing, state);
@@ -14307,9 +14704,16 @@ function readNotes(tree, rootTag, itemTag, idPrefix) {
14307
14704
  }
14308
14705
  return out;
14309
14706
  }
14310
- var RELATIONSHIP_NAMESPACE_IMAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", RELATIONSHIP_NAMESPACE_HYPERLINK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
14707
+ var RELATIONSHIP_NAMESPACE_IMAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", RELATIONSHIP_NAMESPACE_HYPERLINK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", TRACKED_CHANGE_KIND_BY_TAG;
14311
14708
  var init_read = __esm(() => {
14312
14709
  init_parser();
14710
+ init_sym();
14711
+ TRACKED_CHANGE_KIND_BY_TAG = {
14712
+ "w:ins": "ins",
14713
+ "w:del": "del",
14714
+ "w:moveFrom": "moveFrom",
14715
+ "w:moveTo": "moveTo"
14716
+ };
14313
14717
  });
14314
14718
 
14315
14719
  // src/core/ast/doc-view.ts
@@ -14341,7 +14745,8 @@ async function openDocView(path) {
14341
14745
  imagesByRelationshipId: new Map,
14342
14746
  imageById: new Map,
14343
14747
  hyperlinksByRelationshipId: new Map,
14344
- hyperlinkById: new Map
14748
+ hyperlinkById: new Map,
14749
+ trackedChangeReferences: new Map
14345
14750
  };
14346
14751
  view.doc = buildDoc(view, pkg.path);
14347
14752
  return view;
@@ -14420,6 +14825,30 @@ function paragraphText(paragraph) {
14420
14825
  }
14421
14826
  return out;
14422
14827
  }
14828
+ function paragraphTextAccepted(paragraph) {
14829
+ let out = "";
14830
+ for (const run of paragraph.runs) {
14831
+ if (run.type !== "text")
14832
+ continue;
14833
+ const kind = run.trackedChange?.kind;
14834
+ if (kind === "del" || kind === "moveFrom")
14835
+ continue;
14836
+ out += run.text;
14837
+ }
14838
+ return out;
14839
+ }
14840
+ function paragraphTextBaseline(paragraph) {
14841
+ let out = "";
14842
+ for (const run of paragraph.runs) {
14843
+ if (run.type !== "text")
14844
+ continue;
14845
+ const kind = run.trackedChange?.kind;
14846
+ if (kind === "ins" || kind === "moveTo")
14847
+ continue;
14848
+ out += run.text;
14849
+ }
14850
+ return out;
14851
+ }
14423
14852
  function flattenParagraphs(blocks) {
14424
14853
  const out = [];
14425
14854
  for (const block of blocks) {
@@ -14475,6 +14904,13 @@ function parseLocator(input) {
14475
14904
  if (linkMatch) {
14476
14905
  return { kind: "hyperlink", hyperlinkId: `link${linkMatch[1]}` };
14477
14906
  }
14907
+ const trackedChangeMatch = trimmed.match(TRACKED_CHANGE_RE);
14908
+ if (trackedChangeMatch) {
14909
+ return {
14910
+ kind: "trackedChange",
14911
+ trackedChangeId: `tc${trackedChangeMatch[1]}`
14912
+ };
14913
+ }
14478
14914
  const cellMatch = trimmed.match(CELL_RE);
14479
14915
  if (cellMatch) {
14480
14916
  const [, tableIndex, rowIndex, columnIndex, rest] = cellMatch;
@@ -14523,7 +14959,7 @@ function validateOffsets(input, start, end, crossBlock) {
14523
14959
  throw new LocatorParseError(input, "end offset precedes start");
14524
14960
  }
14525
14961
  }
14526
- var LocatorParseError, BLOCK_RE, SPAN_RE, RANGE_RE, COMMENT_RE, IMAGE_RE, LINK_RE, CELL_RE;
14962
+ var LocatorParseError, BLOCK_RE, SPAN_RE, RANGE_RE, COMMENT_RE, IMAGE_RE, LINK_RE, TRACKED_CHANGE_RE, CELL_RE;
14527
14963
  var init_parse = __esm(() => {
14528
14964
  LocatorParseError = class LocatorParseError extends Error {
14529
14965
  input;
@@ -14539,6 +14975,7 @@ var init_parse = __esm(() => {
14539
14975
  COMMENT_RE = /^c(\d+)$/;
14540
14976
  IMAGE_RE = /^img(\d+)$/;
14541
14977
  LINK_RE = /^link(\d+)$/;
14978
+ TRACKED_CHANGE_RE = /^tc(\d+)$/;
14542
14979
  CELL_RE = /^t(\d+):r(\d+)c(\d+)(?::(.+))?$/;
14543
14980
  });
14544
14981
 
@@ -14645,8 +15082,6 @@ function resolveAuthor(authorFlag) {
14645
15082
  return authorFlag;
14646
15083
  if (Bun.env.DOCX_AUTHOR)
14647
15084
  return Bun.env.DOCX_AUTHOR;
14648
- if (Bun.env.DOCX_CLI_AUTHOR)
14649
- return Bun.env.DOCX_CLI_AUTHOR;
14650
15085
  return "docx-cli";
14651
15086
  }
14652
15087
  function resolveDate() {
@@ -14660,7 +15095,7 @@ function convertTextToDelText(node) {
14660
15095
  function computeMaxRevisionId(view) {
14661
15096
  let max = -1;
14662
15097
  walkXml(view.documentTree, (node) => {
14663
- if (node.tag !== "w:ins" && node.tag !== "w:del")
15098
+ if (node.tag !== "w:ins" && node.tag !== "w:del" && node.tag !== "w:moveFrom" && node.tag !== "w:moveTo")
14664
15099
  return;
14665
15100
  const idAttr = node.getAttribute("w:id");
14666
15101
  if (!idAttr)
@@ -14948,6 +15383,7 @@ function exitCodeFor(code) {
14948
15383
  case "COMMENT_NOT_FOUND":
14949
15384
  case "IMAGE_NOT_FOUND":
14950
15385
  case "HYPERLINK_NOT_FOUND":
15386
+ case "TRACKED_CHANGE_NOT_FOUND":
14951
15387
  case "MATCH_NOT_FOUND":
14952
15388
  return EXIT.NOT_FOUND;
14953
15389
  case "NOT_A_ZIP":
@@ -15067,18 +15503,7 @@ function ensureCommentsExtPart(view) {
15067
15503
  return root;
15068
15504
  }
15069
15505
  function paragraphTextLength(paragraph) {
15070
- let total = 0;
15071
- for (const child of paragraph.children) {
15072
- if (child.tag === "w:r")
15073
- total += runTextLength(child);
15074
- else if (child.tag === "w:ins" || child.tag === "w:del") {
15075
- for (const inner of child.children) {
15076
- if (inner.tag === "w:r")
15077
- total += runTextLength(inner);
15078
- }
15079
- }
15080
- }
15081
- return total;
15506
+ return sumRunBearingTextLength(paragraph.children);
15082
15507
  }
15083
15508
  function addCommentMarkersToParagraph(paragraph, commentId, span) {
15084
15509
  const total = paragraphTextLength(paragraph);
@@ -15114,6 +15539,97 @@ function addCommentRangeMarkers(startParagraph, startOffset, endParagraph, endOf
15114
15539
  }
15115
15540
  ]);
15116
15541
  }
15542
+ function addCommentMarkersAroundRun(paragraph, target, commentId) {
15543
+ function walk(parent) {
15544
+ const children = parent.children;
15545
+ for (let index = 0;index < children.length; index++) {
15546
+ const child = children[index];
15547
+ if (child === target) {
15548
+ children.splice(index, 1, commentRangeStartMarker(commentId), target, commentRangeEndMarker(commentId), commentReferenceRun(commentId));
15549
+ return true;
15550
+ }
15551
+ if (child && isRunBearingWrapper(child.tag)) {
15552
+ if (walk(child))
15553
+ return true;
15554
+ }
15555
+ }
15556
+ return false;
15557
+ }
15558
+ return walk(paragraph);
15559
+ }
15560
+ function findElementOffsetsInParagraph(paragraph, target) {
15561
+ let cursor = 0;
15562
+ let result = null;
15563
+ function walk(children) {
15564
+ for (const child of children) {
15565
+ if (child === target) {
15566
+ const start = cursor;
15567
+ cursor += sumRunBearingTextLength([child]);
15568
+ result = { start, end: cursor };
15569
+ return true;
15570
+ }
15571
+ if (child.tag === "w:r") {
15572
+ cursor += runTextLength(child);
15573
+ continue;
15574
+ }
15575
+ if (isRunBearingWrapper(child.tag)) {
15576
+ if (walk(child.children))
15577
+ return true;
15578
+ }
15579
+ }
15580
+ return false;
15581
+ }
15582
+ walk(paragraph.children);
15583
+ return result;
15584
+ }
15585
+ function findContainingParagraph(documentTree, target) {
15586
+ function walk(node) {
15587
+ if (node.tag === "w:p" && containsNode(node, target))
15588
+ return node;
15589
+ for (const child of node.children) {
15590
+ const found = walk(child);
15591
+ if (found)
15592
+ return found;
15593
+ }
15594
+ return null;
15595
+ }
15596
+ for (const root of documentTree) {
15597
+ const found = walk(root);
15598
+ if (found)
15599
+ return found;
15600
+ }
15601
+ return null;
15602
+ }
15603
+ function containsNode(haystack, needle) {
15604
+ if (haystack === needle)
15605
+ return true;
15606
+ for (const child of haystack.children) {
15607
+ if (containsNode(child, needle))
15608
+ return true;
15609
+ }
15610
+ return false;
15611
+ }
15612
+ function emitAuditComment(view, anchor, options) {
15613
+ const numericId = nextCommentId(view);
15614
+ const paraId = generateParaId();
15615
+ if (anchor.kind === "span") {
15616
+ addCommentMarkersToParagraph(anchor.paragraph, numericId, anchor.span);
15617
+ } else {
15618
+ addCommentMarkersAroundRun(anchor.paragraph, anchor.run, numericId);
15619
+ }
15620
+ const commentsRoot = ensureCommentsPart(view);
15621
+ commentsRoot.children.push(/* @__PURE__ */ jsxDEV(CommentBody, {
15622
+ options: {
15623
+ id: numericId,
15624
+ author: options.author,
15625
+ date: options.date,
15626
+ initials: authorInitials(options.author),
15627
+ paraId,
15628
+ text: options.body
15629
+ }
15630
+ }, undefined, false, undefined, this));
15631
+ return numericId;
15632
+ }
15117
15633
  function commentRangeStartMarker(commentId) {
15118
15634
  return /* @__PURE__ */ jsxDEV(w.commentRangeStart, {
15119
15635
  "w-id": commentId
@@ -15149,13 +15665,13 @@ function placeMarkersInParagraph(paragraph, markers) {
15149
15665
  }
15150
15666
  const pending = markers.slice();
15151
15667
  const state = { offset: 0, placedCount: 0 };
15152
- paragraph.children = walkAndPlace(paragraph.children, pending, true, state);
15668
+ paragraph.children = walkAndPlace(paragraph.children, pending, state);
15153
15669
  flushAtCurrentOffset(paragraph.children, pending, state);
15154
15670
  if (state.placedCount !== markers.length) {
15155
15671
  throw new SpanOutOfRangeError(`Could not place comment markers (placed ${state.placedCount} of ${markers.length})`);
15156
15672
  }
15157
15673
  }
15158
- function walkAndPlace(children, pending, isParagraphLevel, state) {
15674
+ function walkAndPlace(children, pending, state) {
15159
15675
  const result = [];
15160
15676
  for (const child of children) {
15161
15677
  if (child.tag === "w:r") {
@@ -15197,9 +15713,9 @@ function walkAndPlace(children, pending, isParagraphLevel, state) {
15197
15713
  state.offset = runEnd;
15198
15714
  continue;
15199
15715
  }
15200
- if (isParagraphLevel && (child.tag === "w:ins" || child.tag === "w:del")) {
15716
+ if (isRunBearingWrapper(child.tag)) {
15201
15717
  flushAtCurrentOffset(result, pending, state);
15202
- const innerChildren = walkAndPlace(child.children, pending, false, state);
15718
+ const innerChildren = walkAndPlace(child.children, pending, state);
15203
15719
  const wrapper = new XmlNode2(child.tag, { ...child.attributes });
15204
15720
  wrapper.children = innerChildren;
15205
15721
  result.push(wrapper);
@@ -16174,6 +16690,7 @@ async function run8(args) {
16174
16690
  allowPositionals: true,
16175
16691
  options: {
16176
16692
  at: { type: "string" },
16693
+ author: { type: "string" },
16177
16694
  output: { type: "string", short: "o" },
16178
16695
  "dry-run": { type: "boolean" },
16179
16696
  help: { type: "boolean", short: "h" }
@@ -16219,7 +16736,7 @@ async function run8(args) {
16219
16736
  if (blockRef.node.tag !== "w:p") {
16220
16737
  return fail("TRACKED_CHANGE_CONFLICT", "Tracked deletion of non-paragraph blocks (e.g., tables) is not supported", "Use `docx track-changes off` first, or delete table contents row-by-row.");
16221
16738
  }
16222
- applyTrackedDeletion(view, blockRef.node);
16739
+ applyTrackedDeletion(view, blockRef.node, parsed.values.author);
16223
16740
  } else {
16224
16741
  blockRef.parent.splice(targetIndex, 1);
16225
16742
  }
@@ -16232,9 +16749,9 @@ async function run8(args) {
16232
16749
  });
16233
16750
  return EXIT.OK;
16234
16751
  }
16235
- function applyTrackedDeletion(view, paragraph) {
16752
+ function applyTrackedDeletion(view, paragraph, authorFlag) {
16236
16753
  const allocator = createRevisionAllocator(view);
16237
- const baseMeta = { author: resolveAuthor(), date: resolveDate() };
16754
+ const baseMeta = { author: resolveAuthor(authorFlag), date: resolveDate() };
16238
16755
  const mintMeta = () => ({
16239
16756
  ...baseMeta,
16240
16757
  revisionId: allocator.next()
@@ -16271,6 +16788,7 @@ Usage:
16271
16788
  Locator (required):
16272
16789
  --at LOCATOR Block to remove (e.g., p3, t0)
16273
16790
 
16791
+ --author NAME Author for tracked changes (default: $DOCX_AUTHOR)
16274
16792
  -o, --output PATH Write to PATH instead of overwriting FILE
16275
16793
  --dry-run Print what would be removed; do not write the file
16276
16794
  -h, --help Show this help
@@ -16423,6 +16941,7 @@ async function run9(args) {
16423
16941
  color: { type: "string" },
16424
16942
  bold: { type: "boolean" },
16425
16943
  italic: { type: "boolean" },
16944
+ author: { type: "string" },
16426
16945
  output: { type: "string", short: "o" },
16427
16946
  "dry-run": { type: "boolean" },
16428
16947
  help: { type: "boolean", short: "h" }
@@ -16510,7 +17029,7 @@ async function run9(args) {
16510
17029
  return EXIT.OK;
16511
17030
  }
16512
17031
  if (isTrackChangesEnabled(view)) {
16513
- applyTrackedEdit(view, blockRef.node, paragraphNode);
17032
+ applyTrackedEdit(view, blockRef.node, paragraphNode, parsed.values.author);
16514
17033
  } else {
16515
17034
  blockRef.parent.splice(targetIndex, 1, paragraphNode);
16516
17035
  }
@@ -16523,9 +17042,9 @@ async function run9(args) {
16523
17042
  });
16524
17043
  return EXIT.OK;
16525
17044
  }
16526
- function applyTrackedEdit(view, existingParagraph, newParagraph) {
17045
+ function applyTrackedEdit(view, existingParagraph, newParagraph, authorFlag) {
16527
17046
  const allocator = createRevisionAllocator(view);
16528
- const baseMeta = { author: resolveAuthor(), date: resolveDate() };
17047
+ const baseMeta = { author: resolveAuthor(authorFlag), date: resolveDate() };
16529
17048
  const mintMeta = () => ({
16530
17049
  ...baseMeta,
16531
17050
  revisionId: allocator.next()
@@ -16592,6 +17111,7 @@ Run options (only with --text):
16592
17111
  --bold Bold
16593
17112
  --italic Italic
16594
17113
 
17114
+ --author NAME Author for tracked changes (default: $DOCX_AUTHOR)
16595
17115
  -o, --output PATH Write to PATH instead of overwriting FILE
16596
17116
  --dry-run Print what would change; do not write the file
16597
17117
  -h, --help Show this help
@@ -16701,10 +17221,9 @@ function trackedChangesOverlapping(paragraph, start, end) {
16701
17221
  const change = run10.trackedChange;
16702
17222
  if (!change)
16703
17223
  continue;
16704
- const key = `${change.kind}:${change.revisionId}:${change.author}`;
16705
- if (seen.has(key))
17224
+ if (seen.has(change.id))
16706
17225
  continue;
16707
- seen.add(key);
17226
+ seen.add(change.id);
16708
17227
  out.push(change);
16709
17228
  }
16710
17229
  return out;
@@ -16862,20 +17381,8 @@ function wrapSpanInHyperlink(paragraph, span, relationshipId) {
16862
17381
  }
16863
17382
  continue;
16864
17383
  }
16865
- if (child.tag === "w:hyperlink") {
16866
- const hyperlinkLength = sumInnerRunLengths(child);
16867
- const hyperlinkStart = offset;
16868
- const hyperlinkEnd = offset + hyperlinkLength;
16869
- offset = hyperlinkEnd;
16870
- if (hyperlinkEnd <= span.start || hyperlinkStart >= span.end) {
16871
- placeWrapper();
16872
- newChildren.push(child);
16873
- continue;
16874
- }
16875
- throw new HyperlinkWrapError(`Span ${span.start}-${span.end} overlaps an existing hyperlink at ${hyperlinkStart}-${hyperlinkEnd}; nested hyperlinks are not allowed`);
16876
- }
16877
- if (child.tag === "w:ins" || child.tag === "w:del") {
16878
- const innerLength = sumInnerRunLengths(child);
17384
+ if (isRunBearingWrapper(child.tag)) {
17385
+ const innerLength = sumRunBearingTextLength(child.children);
16879
17386
  const wrapperStart = offset;
16880
17387
  const wrapperEnd = offset + innerLength;
16881
17388
  offset = wrapperEnd;
@@ -16884,27 +17391,28 @@ function wrapSpanInHyperlink(paragraph, span, relationshipId) {
16884
17391
  newChildren.push(child);
16885
17392
  continue;
16886
17393
  }
16887
- throw new HyperlinkWrapError(`Span ${span.start}-${span.end} crosses a tracked-change wrapper at ${wrapperStart}-${wrapperEnd}; resolve or accept the change first`);
17394
+ throw new HyperlinkWrapError(messageForOverlap(child.tag, span));
16888
17395
  }
16889
17396
  newChildren.push(child);
16890
17397
  }
16891
17398
  placeWrapper();
16892
17399
  paragraph.children = newChildren;
16893
17400
  }
17401
+ function messageForOverlap(tag, span) {
17402
+ if (tag === "w:hyperlink") {
17403
+ return `Span ${span.start}-${span.end} overlaps an existing hyperlink; nested hyperlinks are not allowed`;
17404
+ }
17405
+ if (tag === "w:ins" || tag === "w:del" || tag === "w:moveFrom" || tag === "w:moveTo") {
17406
+ return `Span ${span.start}-${span.end} crosses a tracked-change wrapper (${tag}); resolve or accept the change first`;
17407
+ }
17408
+ return `Span ${span.start}-${span.end} crosses a ${tag} wrapper; cannot wrap a hyperlink across it`;
17409
+ }
16894
17410
  function hyperlinkWrapper(relationshipId, runs) {
16895
17411
  return /* @__PURE__ */ jsxDEV(w.hyperlink, {
16896
17412
  "r:id": relationshipId,
16897
17413
  children: runs
16898
17414
  }, undefined, false, undefined, this);
16899
17415
  }
16900
- function sumInnerRunLengths(wrapper) {
16901
- let total = 0;
16902
- for (const inner of wrapper.children) {
16903
- if (inner.tag === "w:r")
16904
- total += runTextLength(inner);
16905
- }
16906
- return total;
16907
- }
16908
17416
  var HyperlinkWrapError;
16909
17417
  var init_wrap = __esm(() => {
16910
17418
  init_jsx();
@@ -16933,6 +17441,7 @@ async function run11(args) {
16933
17441
  options: {
16934
17442
  at: { type: "string" },
16935
17443
  url: { type: "string" },
17444
+ author: { type: "string" },
16936
17445
  output: { type: "string", short: "o" },
16937
17446
  "dry-run": { type: "boolean" },
16938
17447
  help: { type: "boolean", short: "h" }
@@ -17001,6 +17510,13 @@ async function run11(args) {
17001
17510
  throw error;
17002
17511
  }
17003
17512
  view.hyperlinksByRelationshipId.set(relationshipId, { url });
17513
+ if (isTrackChangesEnabled(view)) {
17514
+ emitAuditComment(view, { kind: "span", paragraph: paragraphRef.node, span: target.span }, {
17515
+ body: `[docx-cli] hyperlink added \u2192 ${url}`,
17516
+ author: resolveAuthor(parsed.values.author),
17517
+ date: resolveDate()
17518
+ });
17519
+ }
17004
17520
  await saveDocView(view, outputPath);
17005
17521
  await respond({
17006
17522
  ok: true,
@@ -17023,12 +17539,19 @@ Required:
17023
17539
  --url URL Target URL
17024
17540
 
17025
17541
  Optional:
17542
+ --author NAME Author for the audit comment when track-changes is on
17543
+ (default: $DOCX_AUTHOR)
17026
17544
  -o, --output PATH Write to PATH instead of overwriting FILE
17027
17545
  --dry-run Print what would change; do not write the file
17028
17546
  -h, --help Show this help
17029
17547
 
17030
17548
  The span must lie inside a single paragraph and must not overlap an existing
17031
- hyperlink or a tracked-change wrapper.
17549
+ hyperlink, a tracked-change wrapper (<w:ins>/<w:del>/<w:moveFrom>/<w:moveTo>),
17550
+ or any other run-bearing wrapper that we model. Resolve or accept the
17551
+ wrapper first, then add the hyperlink.
17552
+
17553
+ When track-changes is on, an audit comment is anchored to the wrapped span
17554
+ since OOXML has no native tracked-change form for hyperlink edits.
17032
17555
 
17033
17556
  Examples:
17034
17557
  docx hyperlinks add doc.docx --at p3:5-20 --url https://example.com
@@ -17036,6 +17559,7 @@ Examples:
17036
17559
  var init_add2 = __esm(() => {
17037
17560
  init_core();
17038
17561
  init_parser();
17562
+ init_helpers();
17039
17563
  init_respond();
17040
17564
  init_wrap();
17041
17565
  });
@@ -17054,6 +17578,7 @@ async function run12(args) {
17054
17578
  allowPositionals: true,
17055
17579
  options: {
17056
17580
  at: { type: "string" },
17581
+ author: { type: "string" },
17057
17582
  output: { type: "string", short: "o" },
17058
17583
  "dry-run": { type: "boolean" },
17059
17584
  help: { type: "boolean", short: "h" }
@@ -17098,6 +17623,9 @@ async function run12(args) {
17098
17623
  if (index === -1) {
17099
17624
  return fail("HYPERLINK_NOT_FOUND", `Hyperlink reference is stale (parent does not contain it): ${targetId}`);
17100
17625
  }
17626
+ const trackingOn = isTrackChangesEnabled(view);
17627
+ const paragraph = trackingOn ? findContainingParagraph(view.documentTree, reference.node) : null;
17628
+ const offsets = trackingOn && paragraph ? findElementOffsetsInParagraph(paragraph, reference.node) : null;
17101
17629
  reference.parent.splice(index, 1, ...reference.node.children);
17102
17630
  view.hyperlinkById.delete(targetId);
17103
17631
  if (reference.relationshipId) {
@@ -17107,6 +17635,13 @@ async function run12(args) {
17107
17635
  view.hyperlinksByRelationshipId.delete(reference.relationshipId);
17108
17636
  }
17109
17637
  }
17638
+ if (trackingOn && paragraph && offsets) {
17639
+ emitAuditComment(view, { kind: "span", paragraph, span: offsets }, {
17640
+ body: `[docx-cli] hyperlink removed (was: ${oldUrl ?? "(none)"})`,
17641
+ author: resolveAuthor(parsed.values.author),
17642
+ date: resolveDate()
17643
+ });
17644
+ }
17110
17645
  await saveDocView(view, outputPath);
17111
17646
  await respond({
17112
17647
  ok: true,
@@ -17149,6 +17684,8 @@ Required:
17149
17684
  --at LINK_ID Existing hyperlink to remove (e.g., link0)
17150
17685
 
17151
17686
  Optional:
17687
+ --author NAME Author for the audit comment when track-changes is on
17688
+ (default: $DOCX_AUTHOR)
17152
17689
  -o, --output PATH Write to PATH instead of overwriting FILE
17153
17690
  --dry-run Print what would change; do not write the file
17154
17691
  -h, --help Show this help
@@ -17157,12 +17694,16 @@ The display text stays in place; only the <w:hyperlink> wrapper is removed.
17157
17694
  If the underlying relationship is no longer referenced, it is pruned from the
17158
17695
  rels file too.
17159
17696
 
17697
+ When track-changes is on, an audit comment is anchored to the surviving text
17698
+ since OOXML has no native tracked-change form for hyperlink removal.
17699
+
17160
17700
  Examples:
17161
17701
  docx hyperlinks delete doc.docx --at link0
17162
17702
  `;
17163
17703
  var init_delete3 = __esm(() => {
17164
17704
  init_core();
17165
17705
  init_parser();
17706
+ init_helpers();
17166
17707
  init_respond();
17167
17708
  });
17168
17709
 
@@ -17272,6 +17813,7 @@ async function run14(args) {
17272
17813
  options: {
17273
17814
  at: { type: "string" },
17274
17815
  with: { type: "string" },
17816
+ author: { type: "string" },
17275
17817
  output: { type: "string", short: "o" },
17276
17818
  "dry-run": { type: "boolean" },
17277
17819
  help: { type: "boolean", short: "h" }
@@ -17333,6 +17875,17 @@ async function run14(args) {
17333
17875
  updateRelationshipTarget(relationships, existingId, newUrl);
17334
17876
  view.hyperlinksByRelationshipId.set(existingId, { url: newUrl });
17335
17877
  }
17878
+ if (isTrackChangesEnabled(view)) {
17879
+ const paragraph = findContainingParagraph(view.documentTree, reference.node);
17880
+ const offsets = paragraph ? findElementOffsetsInParagraph(paragraph, reference.node) : null;
17881
+ if (paragraph && offsets) {
17882
+ emitAuditComment(view, { kind: "span", paragraph, span: offsets }, {
17883
+ body: `[docx-cli] hyperlink target changed: ${oldUrl ?? "(none)"} \u2192 ${newUrl}`,
17884
+ author: resolveAuthor(parsed.values.author),
17885
+ date: resolveDate()
17886
+ });
17887
+ }
17888
+ }
17336
17889
  await saveDocView(view, outputPath);
17337
17890
  await respond({
17338
17891
  ok: true,
@@ -17381,6 +17934,8 @@ Required:
17381
17934
  --with URL New target URL
17382
17935
 
17383
17936
  Optional:
17937
+ --author NAME Author for the audit comment when track-changes is on
17938
+ (default: $DOCX_AUTHOR)
17384
17939
  -o, --output PATH Write to PATH instead of overwriting FILE
17385
17940
  --dry-run Print what would change; do not write the file
17386
17941
  -h, --help Show this help
@@ -17389,12 +17944,17 @@ Replaces only the targeted hyperlink. If multiple hyperlinks shared the same
17389
17944
  underlying relationship, a new relationship is allocated so the others are
17390
17945
  unaffected.
17391
17946
 
17947
+ When track-changes is on, an audit comment is anchored to the affected
17948
+ hyperlink span since OOXML has no native tracked-change form for hyperlink
17949
+ target edits.
17950
+
17392
17951
  Examples:
17393
17952
  docx hyperlinks replace doc.docx --at link0 --with https://example.com
17394
17953
  `;
17395
17954
  var init_replace = __esm(() => {
17396
17955
  init_core();
17397
17956
  init_parser();
17957
+ init_helpers();
17398
17958
  init_respond();
17399
17959
  });
17400
17960
 
@@ -17654,6 +18214,7 @@ async function run18(args) {
17654
18214
  options: {
17655
18215
  at: { type: "string" },
17656
18216
  with: { type: "string" },
18217
+ author: { type: "string" },
17657
18218
  output: { type: "string", short: "o" },
17658
18219
  "dry-run": { type: "boolean" },
17659
18220
  help: { type: "boolean", short: "h" }
@@ -17709,6 +18270,7 @@ async function run18(args) {
17709
18270
  return EXIT.OK;
17710
18271
  }
17711
18272
  const bytes = new Uint8Array(await sourceFile.arrayBuffer());
18273
+ const originalMimeType = reference.contentType;
17712
18274
  if (newPartName === originalPartName) {
17713
18275
  view.pkg.writeBytes(originalPartName, bytes);
17714
18276
  } else {
@@ -17719,6 +18281,18 @@ async function run18(args) {
17719
18281
  reference.partName = newPartName;
17720
18282
  reference.contentType = newMimeType;
17721
18283
  }
18284
+ if (isTrackChangesEnabled(view)) {
18285
+ const author = resolveAuthor(parsed.values.author);
18286
+ const date = resolveDate();
18287
+ const body = `[docx-cli] image replaced: ${originalPartName} (${originalMimeType}) \u2192 ${newPartName} (${newMimeType}, ${bytes.length} bytes)`;
18288
+ const drawingRuns = findDrawingRunsForRelationship(view.documentTree, reference.relationshipId);
18289
+ for (const drawingRun of drawingRuns) {
18290
+ const paragraph = findContainingParagraph(view.documentTree, drawingRun);
18291
+ if (!paragraph)
18292
+ continue;
18293
+ emitAuditComment(view, { kind: "run", paragraph, run: drawingRun }, { body, author, date });
18294
+ }
18295
+ }
17722
18296
  await saveDocView(view, outputPath);
17723
18297
  await respond({
17724
18298
  ok: true,
@@ -17731,6 +18305,33 @@ async function run18(args) {
17731
18305
  });
17732
18306
  return EXIT.OK;
17733
18307
  }
18308
+ function findDrawingRunsForRelationship(documentTree, relationshipId) {
18309
+ const matches = [];
18310
+ function walk(node) {
18311
+ if (node.tag === "w:r" && runReferencesImage(node, relationshipId)) {
18312
+ matches.push(node);
18313
+ return;
18314
+ }
18315
+ for (const child of node.children)
18316
+ walk(child);
18317
+ }
18318
+ for (const root of documentTree)
18319
+ walk(root);
18320
+ return matches;
18321
+ }
18322
+ function runReferencesImage(run19, relationshipId) {
18323
+ for (const child of run19.children) {
18324
+ if (child.tag !== "w:drawing")
18325
+ continue;
18326
+ const blip = child.findDescendant("a:blip");
18327
+ if (!blip)
18328
+ continue;
18329
+ const embed = blip.getAttribute("r:embed") ?? blip.getAttribute("r:link");
18330
+ if (embed === relationshipId)
18331
+ return true;
18332
+ }
18333
+ return false;
18334
+ }
17734
18335
  function renameExtension(partName, newExtension) {
17735
18336
  const dotIndex = partName.lastIndexOf(".");
17736
18337
  const slashIndex = partName.lastIndexOf("/");
@@ -17777,6 +18378,8 @@ Required:
17777
18378
  --with PATH New image file (any image MIME type)
17778
18379
 
17779
18380
  Optional:
18381
+ --author NAME Author for the audit comment when track-changes is on
18382
+ (default: $DOCX_AUTHOR)
17780
18383
  -o, --output PATH Write to PATH instead of overwriting FILE
17781
18384
  --dry-run Print what would change; do not write the file
17782
18385
  -h, --help Show this help
@@ -17785,6 +18388,10 @@ If the replacement uses a different format from the original, the part is
17785
18388
  renamed (extension changes), the relationship Target is rewritten, and
17786
18389
  [Content_Types].xml gets a Default entry for the new extension if needed.
17787
18390
 
18391
+ When track-changes is on, an audit comment is anchored to each drawing that
18392
+ referenced the swapped image since OOXML has no native tracked-change form
18393
+ for image replacement.
18394
+
17788
18395
  Examples:
17789
18396
  docx images replace doc.docx --at img2 --with ./new-photo.png
17790
18397
  docx images replace doc.docx --at img0 --with ./diagram.svg
@@ -17792,6 +18399,7 @@ Examples:
17792
18399
  var init_replace2 = __esm(() => {
17793
18400
  init_core();
17794
18401
  init_parser();
18402
+ init_helpers();
17795
18403
  init_respond();
17796
18404
  EXTENSION_BY_MIME = {
17797
18405
  "image/png": "png",
@@ -17977,12 +18585,22 @@ export type Footnote = {
17977
18585
  };
17978
18586
 
17979
18587
  export type TrackedChange = {
17980
- kind: "ins" | "del";
18588
+ id: string;
18589
+ kind: TrackedChangeKind;
17981
18590
  author: string;
17982
18591
  date: string;
17983
18592
  revisionId: string;
17984
18593
  };
17985
18594
 
18595
+ /** OOXML revision-tracking wrappers we surface in the AST.
18596
+ * - \`ins\` / \`del\`: <w:ins> / <w:del> \u2014 inserted / deleted runs.
18597
+ * - \`moveFrom\` / \`moveTo\`: <w:moveFrom> / <w:moveTo> \u2014 origin / destination
18598
+ * of a tracked move. moveFrom behaves like a delete (text leaves this
18599
+ * location, stored as <w:delText> internally); moveTo behaves like an
18600
+ * insert (text arrives at this location).
18601
+ */
18602
+ export type TrackedChangeKind = "ins" | "del" | "moveFrom" | "moveTo";
18603
+
17986
18604
  export type Comment = {
17987
18605
  id: string;
17988
18606
  author: string;
@@ -18036,10 +18654,10 @@ async function run20(args) {
18036
18654
  await respond(JSON_SCHEMA);
18037
18655
  return EXIT.OK;
18038
18656
  }
18039
- var HELP20 = `docx schema \u2014 print the AST type definitions
18657
+ var HELP20 = `docx info schema \u2014 print the AST type definitions
18040
18658
 
18041
18659
  Usage:
18042
- docx schema [options]
18660
+ docx info schema [options]
18043
18661
 
18044
18662
  Options:
18045
18663
  --json Print as a JSON Schema document (default)
@@ -18047,8 +18665,8 @@ Options:
18047
18665
  -h, --help Show this help
18048
18666
 
18049
18667
  Examples:
18050
- docx schema | jq '.definitions.Run'
18051
- docx schema --ts > ast.d.ts
18668
+ docx info schema | jq '.$defs.Run'
18669
+ docx info schema --ts > ast.d.ts
18052
18670
  `, JSON_SCHEMA;
18053
18671
  var init_schema = __esm(() => {
18054
18672
  init_types();
@@ -18139,9 +18757,10 @@ var init_schema = __esm(() => {
18139
18757
  comments: { type: "array", items: { type: "string" } },
18140
18758
  trackedChange: {
18141
18759
  type: "object",
18142
- required: ["kind", "author", "date", "revisionId"],
18760
+ required: ["id", "kind", "author", "date", "revisionId"],
18143
18761
  properties: {
18144
- kind: { enum: ["ins", "del"] },
18762
+ id: { type: "string" },
18763
+ kind: { enum: ["ins", "del", "moveFrom", "moveTo"] },
18145
18764
  author: { type: "string" },
18146
18765
  date: { type: "string" },
18147
18766
  revisionId: { type: "string" }
@@ -18316,14 +18935,18 @@ async function run21(args) {
18316
18935
  await writeStdout(REFERENCE);
18317
18936
  return EXIT.OK;
18318
18937
  }
18319
- var HELP21 = `docx locators \u2014 print the locator grammar reference
18938
+ var HELP21 = `docx info locators \u2014 print the locator grammar reference
18320
18939
 
18321
18940
  Usage:
18322
- docx locators [options]
18941
+ docx info locators [options]
18323
18942
 
18324
18943
  Options:
18325
18944
  --json Print as JSON
18326
18945
  -h, --help Show this help
18946
+
18947
+ Examples:
18948
+ docx info locators
18949
+ docx info locators --json | jq '.entityLocators'
18327
18950
  `, REFERENCE = `LOCATOR GRAMMAR
18328
18951
 
18329
18952
  Block locators:
@@ -18343,6 +18966,8 @@ Entity locators:
18343
18966
  cN Comment id (e.g., c0)
18344
18967
  imgN Image id (e.g., img2)
18345
18968
  linkN Hyperlink id (e.g., link0)
18969
+ tcN Tracked change id (e.g., tc0) \u2014 the Nth revision wrapper
18970
+ (<w:ins>/<w:del>/<w:moveFrom>/<w:moveTo>) in document order
18346
18971
  tN:rRcC Cell at row R, column C of table tN
18347
18972
 
18348
18973
  Examples:
@@ -18352,6 +18977,7 @@ Examples:
18352
18977
  c1 -> comment c1
18353
18978
  img0 -> image img0
18354
18979
  link0 -> hyperlink link0
18980
+ tc0 -> first tracked change in the document
18355
18981
  t0:r1c2 -> cell at row 1, col 2 of table t0
18356
18982
  t0:r1c2:p0 -> first paragraph of that cell
18357
18983
  t0:r1c2:p0:5-10 -> chars 5..10 of that paragraph
@@ -18385,6 +19011,7 @@ var init_locators2 = __esm(() => {
18385
19011
  comment: { syntax: "cN", example: "c1" },
18386
19012
  image: { syntax: "imgN", example: "img0" },
18387
19013
  hyperlink: { syntax: "linkN", example: "link0" },
19014
+ trackedChange: { syntax: "tcN", example: "tc0" },
18388
19015
  cell: { syntax: "tN:rRcC", example: "t0:r1c2" },
18389
19016
  nestedCell: { syntax: "tN:rRcC:pK", example: "t0:r1c2:p0" }
18390
19017
  },
@@ -18456,6 +19083,7 @@ async function run23(args) {
18456
19083
  bold: { type: "boolean" },
18457
19084
  italic: { type: "boolean" },
18458
19085
  url: { type: "string" },
19086
+ author: { type: "string" },
18459
19087
  output: { type: "string", short: "o" },
18460
19088
  "dry-run": { type: "boolean" },
18461
19089
  help: { type: "boolean", short: "h" }
@@ -18544,7 +19172,7 @@ async function run23(args) {
18544
19172
  }
18545
19173
  const insertIndex = after !== undefined ? targetIndex + 1 : targetIndex;
18546
19174
  if (isTrackChangesEnabled(view)) {
18547
- applyTrackedInsertion(paragraphNode, view);
19175
+ applyTrackedInsertion(paragraphNode, view, parsed.values.author);
18548
19176
  }
18549
19177
  const outputPath = parsed.values.output;
18550
19178
  if (parsed.values["dry-run"]) {
@@ -18593,9 +19221,9 @@ function wrapFirstRunInHyperlink(view, paragraph, url) {
18593
19221
  }
18594
19222
  paragraph.children = newChildren;
18595
19223
  }
18596
- function applyTrackedInsertion(paragraph, view) {
19224
+ function applyTrackedInsertion(paragraph, view, authorFlag) {
18597
19225
  const allocator = createRevisionAllocator(view);
18598
- const baseMeta = { author: resolveAuthor(), date: resolveDate() };
19226
+ const baseMeta = { author: resolveAuthor(authorFlag), date: resolveDate() };
18599
19227
  const mintMeta = () => ({
18600
19228
  ...baseMeta,
18601
19229
  revisionId: allocator.next()
@@ -18646,6 +19274,7 @@ Run options (only with --text):
18646
19274
  --italic Italic
18647
19275
  --url URL Wrap the inserted text in a hyperlink to URL
18648
19276
 
19277
+ --author NAME Author for tracked changes (default: $DOCX_AUTHOR)
18649
19278
  -o, --output PATH Write to PATH instead of overwriting FILE
18650
19279
  --dry-run Print what would be inserted; do not write the file
18651
19280
  -h, --help Show this help
@@ -18804,7 +19433,8 @@ function renderMarkdown(doc, options = {}) {
18804
19433
  options,
18805
19434
  commentIndex,
18806
19435
  referencedFootnoteIds: new Set,
18807
- referencedEndnoteIds: new Set
19436
+ referencedEndnoteIds: new Set,
19437
+ referencedTrackedChanges: new Map
18808
19438
  };
18809
19439
  const parts = [];
18810
19440
  for (const block of blocks) {
@@ -18824,6 +19454,9 @@ function renderMarkdown(doc, options = {}) {
18824
19454
  const endnoteDefs = renderNoteDefinitions(doc.endnotes, ctx.referencedEndnoteIds);
18825
19455
  if (endnoteDefs.length > 0)
18826
19456
  definitions.push(endnoteDefs);
19457
+ const trackedChangeDefs = renderTrackedChangeFootnotes(ctx.referencedTrackedChanges);
19458
+ if (trackedChangeDefs.length > 0)
19459
+ definitions.push(trackedChangeDefs);
18827
19460
  if (definitions.length > 0)
18828
19461
  parts.push(definitions.join(`
18829
19462
  `));
@@ -18841,8 +19474,18 @@ function emptyCommentIndex() {
18841
19474
  orderedIds: []
18842
19475
  };
18843
19476
  }
19477
+ function isRunVisible(run25, view) {
19478
+ const kind = run25.trackedChange?.kind;
19479
+ if (!kind)
19480
+ return true;
19481
+ if (view === "accepted" && (kind === "del" || kind === "moveFrom"))
19482
+ return false;
19483
+ if (view === "baseline" && (kind === "ins" || kind === "moveTo"))
19484
+ return false;
19485
+ return true;
19486
+ }
18844
19487
  function buildCommentIndex(blocks, options) {
18845
- const showChanges = options.showChanges ?? false;
19488
+ const view = options.view ?? "current";
18846
19489
  const lastSlot = new Map;
18847
19490
  const spanText = new Map;
18848
19491
  const orderedIds = [];
@@ -18850,7 +19493,7 @@ function buildCommentIndex(blocks, options) {
18850
19493
  paragraph.runs.forEach((run25, index) => {
18851
19494
  if (run25.type !== "text")
18852
19495
  return;
18853
- if (!showChanges && run25.trackedChange?.kind === "del")
19496
+ if (!isRunVisible(run25, view))
18854
19497
  return;
18855
19498
  for (const commentId of run25.comments ?? []) {
18856
19499
  if (!spanText.has(commentId))
@@ -18914,12 +19557,11 @@ function headingLevelFor(style) {
18914
19557
  return parsed;
18915
19558
  }
18916
19559
  function renderRuns(paragraphId, runs, ctx) {
18917
- const showChanges = ctx.options.showChanges ?? false;
19560
+ const view = ctx.options.view ?? "current";
18918
19561
  const visibleEntries = [];
18919
19562
  runs.forEach((run25, index) => {
18920
- if (!showChanges && run25.type === "text" && run25.trackedChange?.kind === "del") {
19563
+ if (run25.type === "text" && !isRunVisible(run25, view))
18921
19564
  return;
18922
- }
18923
19565
  visibleEntries.push({ run: run25, originalIndex: index });
18924
19566
  });
18925
19567
  let out = "";
@@ -18942,7 +19584,15 @@ function renderRuns(paragraphId, runs, ctx) {
18942
19584
  lookahead++;
18943
19585
  }
18944
19586
  const segment = visibleEntries.slice(cursor, lookahead);
18945
- out += renderTextSegment(segment.map((entry2) => entry2.run), showChanges);
19587
+ const segmentRuns = segment.map((entry2) => entry2.run);
19588
+ out += renderTextSegment(segmentRuns, view);
19589
+ if (view === "current") {
19590
+ for (const segmentRun of segmentRuns) {
19591
+ if (segmentRun.trackedChange) {
19592
+ ctx.referencedTrackedChanges.set(segmentRun.trackedChange.id, segmentRun.trackedChange);
19593
+ }
19594
+ }
19595
+ }
18946
19596
  out += commentEndingsFor(paragraphId, segment, ctx.commentIndex);
18947
19597
  cursor = lookahead;
18948
19598
  continue;
@@ -18985,7 +19635,7 @@ function commentEndingsFor(paragraphId, segment, commentIndex) {
18985
19635
  return out;
18986
19636
  }
18987
19637
  function sameDecoration(a2, b) {
18988
- return (a2.bold ?? false) === (b.bold ?? false) && (a2.italic ?? false) === (b.italic ?? false) && (a2.strike ?? false) === (b.strike ?? false) && (a2.underline ?? "") === (b.underline ?? "") && (a2.color ?? "") === (b.color ?? "") && (a2.highlight ?? "") === (b.highlight ?? "") && a2.hyperlink?.id === b.hyperlink?.id && a2.trackedChange?.kind === b.trackedChange?.kind && sameCommentSet(a2.comments, b.comments);
19638
+ return (a2.bold ?? false) === (b.bold ?? false) && (a2.italic ?? false) === (b.italic ?? false) && (a2.strike ?? false) === (b.strike ?? false) && (a2.underline ?? "") === (b.underline ?? "") && (a2.color ?? "") === (b.color ?? "") && (a2.highlight ?? "") === (b.highlight ?? "") && a2.hyperlink?.id === b.hyperlink?.id && a2.trackedChange?.id === b.trackedChange?.id && sameCommentSet(a2.comments, b.comments);
18989
19639
  }
18990
19640
  function sameCommentSet(left, right) {
18991
19641
  const a2 = left ?? [];
@@ -18998,7 +19648,7 @@ function sameCommentSet(left, right) {
18998
19648
  }
18999
19649
  return true;
19000
19650
  }
19001
- function renderTextSegment(runs, showChanges) {
19651
+ function renderTextSegment(runs, view) {
19002
19652
  const text = runs.map((run25) => run25.text).join("");
19003
19653
  if (text.length === 0)
19004
19654
  return "";
@@ -19024,11 +19674,17 @@ function renderTextSegment(runs, showChanges) {
19024
19674
  const target = first.hyperlink.url ?? `#${first.hyperlink.anchor ?? ""}`;
19025
19675
  out = `[${out}](${target})`;
19026
19676
  }
19027
- if (showChanges && first.trackedChange) {
19028
- out = `<${first.trackedChange.kind}>${out}</${first.trackedChange.kind}>`;
19677
+ if (view === "current" && first.trackedChange) {
19678
+ const marker = criticMarkerFor(first.trackedChange.kind);
19679
+ out = `{${marker}${out}${marker}}[^${first.trackedChange.id}]`;
19029
19680
  }
19030
19681
  return out;
19031
19682
  }
19683
+ function criticMarkerFor(kind) {
19684
+ if (kind === "ins" || kind === "moveTo")
19685
+ return "++";
19686
+ return "--";
19687
+ }
19032
19688
  function colorAttrFor(value) {
19033
19689
  if (!value)
19034
19690
  return null;
@@ -19133,6 +19789,32 @@ function renderNoteDefinitions(notes, referenced) {
19133
19789
  return lines.join(`
19134
19790
  `);
19135
19791
  }
19792
+ function renderTrackedChangeFootnotes(referenced) {
19793
+ if (referenced.size === 0)
19794
+ return "";
19795
+ const sorted = [...referenced.values()].sort((a2, b) => trackedChangeIdCompare(a2.id, b.id));
19796
+ const lines = [];
19797
+ for (const change of sorted) {
19798
+ const kind = trackedChangeLabelFor(change.kind);
19799
+ const author = change.author || "unknown";
19800
+ const meta = change.date ? `${author} (${change.date})` : author;
19801
+ lines.push(`[^${change.id}]: ${kind} by ${meta}`);
19802
+ }
19803
+ return lines.join(`
19804
+ `);
19805
+ }
19806
+ function trackedChangeLabelFor(kind) {
19807
+ if (kind === "ins")
19808
+ return "insertion";
19809
+ if (kind === "del")
19810
+ return "deletion";
19811
+ if (kind === "moveTo")
19812
+ return "moveTo";
19813
+ return "moveFrom";
19814
+ }
19815
+ function trackedChangeIdCompare(left, right) {
19816
+ return numericIdCompare(left, right, /^tc(\d+)$/);
19817
+ }
19136
19818
  function noteIdCompare(left, right) {
19137
19819
  return numericIdCompare(left, right, /(\d+)$/);
19138
19820
  }
@@ -19198,6 +19880,7 @@ function blockIdForLocator(input, position) {
19198
19880
  case "comment":
19199
19881
  case "image":
19200
19882
  case "hyperlink":
19883
+ case "trackedChange":
19201
19884
  throw new MarkdownLocatorError(input, `--${position} does not accept a ${parsed.kind} locator \u2014 use a paragraph or table locator`);
19202
19885
  }
19203
19886
  }
@@ -19230,7 +19913,8 @@ async function run25(args) {
19230
19913
  markdown: { type: "boolean" },
19231
19914
  from: { type: "string" },
19232
19915
  to: { type: "string" },
19233
- changes: { type: "boolean" },
19916
+ accepted: { type: "boolean" },
19917
+ baseline: { type: "boolean" },
19234
19918
  comments: { type: "boolean" },
19235
19919
  help: { type: "boolean", short: "h" }
19236
19920
  }
@@ -19248,20 +19932,25 @@ async function run25(args) {
19248
19932
  const markdown = Boolean(parsed.values.markdown);
19249
19933
  const from = parsed.values.from;
19250
19934
  const to = parsed.values.to;
19251
- const showChanges = Boolean(parsed.values.changes);
19935
+ const accepted = Boolean(parsed.values.accepted);
19936
+ const baseline = Boolean(parsed.values.baseline);
19252
19937
  const showComments = Boolean(parsed.values.comments);
19253
- if (!markdown && (from || to || showChanges || showComments)) {
19254
- return fail("USAGE", "--from, --to, --changes, and --comments require --markdown", HELP25);
19938
+ if (!markdown && (from || to || accepted || baseline || showComments)) {
19939
+ return fail("USAGE", "--from, --to, --accepted, --baseline, and --comments require --markdown", HELP25);
19255
19940
  }
19256
- const view = await openOrFail(path);
19257
- if (typeof view === "number")
19258
- return view;
19941
+ if (accepted && baseline) {
19942
+ return fail("USAGE", "--accepted and --baseline are mutually exclusive", HELP25);
19943
+ }
19944
+ const view = accepted ? "accepted" : baseline ? "baseline" : "current";
19945
+ const docView = await openOrFail(path);
19946
+ if (typeof docView === "number")
19947
+ return docView;
19259
19948
  if (markdown) {
19260
19949
  try {
19261
- const rendered = renderMarkdown(view.doc, {
19950
+ const rendered = renderMarkdown(docView.doc, {
19262
19951
  from,
19263
19952
  to,
19264
- showChanges,
19953
+ view,
19265
19954
  showComments
19266
19955
  });
19267
19956
  await writeStdout(rendered);
@@ -19273,8 +19962,8 @@ async function run25(args) {
19273
19962
  throw err;
19274
19963
  }
19275
19964
  }
19276
- await enrichImageHashes(view);
19277
- await respond(view.doc);
19965
+ await enrichImageHashes(docView);
19966
+ await respond(docView.doc);
19278
19967
  return EXIT.OK;
19279
19968
  }
19280
19969
  var HELP25 = `docx read \u2014 print AST as JSON, or render document body as Markdown
@@ -19293,8 +19982,18 @@ Options:
19293
19982
  Accepts pN, tN, tN:rRcC[:pK[:S-E]], pN:S-E, pN:S-pM:E.
19294
19983
  Cell/span/range locators collapse to their enclosing
19295
19984
  top-level block (the table or paragraph).
19296
- --changes With --markdown, render tracked insertions/deletions as
19297
- <ins>/<del> instead of producing the accepted view.
19985
+ --accepted With --markdown, render the post-accept view: drop
19986
+ subtractive wrappers (<w:del>, <w:moveFrom>), inline
19987
+ additive wrappers (<w:ins>, <w:moveTo>) as plain text,
19988
+ no markers/refs.
19989
+ --baseline With --markdown, render the pre-change view: drop
19990
+ additive wrappers (<w:ins>, <w:moveTo>), inline
19991
+ subtractive wrappers (<w:del>, <w:moveFrom>) as plain
19992
+ text, no markers/refs. Mutually exclusive with --accepted.
19993
+ Default --markdown shows all four: additive wrappers as
19994
+ {++text++}[^tcN] and subtractive as {--text--}[^tcN]
19995
+ (CriticMarkup); the [^tcN] footnote spells out the kind
19996
+ (insertion / deletion / moveTo / moveFrom).
19298
19997
  --comments With --markdown, append [^cN] after each commented span
19299
19998
  and emit a footnote definition for each comment at the
19300
19999
  end of the output (author, date, body).
@@ -19304,7 +20003,8 @@ Examples:
19304
20003
  docx read input.docx
19305
20004
  docx read input.docx --markdown
19306
20005
  docx read input.docx --markdown --from p3 --to p20
19307
- docx read input.docx --markdown --changes --comments
20006
+ docx read input.docx --markdown --accepted --comments
20007
+ docx read input.docx --markdown --baseline
19308
20008
  docx read input.docx | jq '.blocks[] | select(.type == "paragraph")'
19309
20009
  `;
19310
20010
  var init_read2 = __esm(() => {
@@ -19341,33 +20041,25 @@ function replaceSpanInParagraph(paragraph, span, replacement, tracked) {
19341
20041
  function collectRunSlots(paragraph) {
19342
20042
  const slots = [];
19343
20043
  let offset = 0;
19344
- for (const child of paragraph.children) {
19345
- if (child.tag === "w:r") {
19346
- const length = runTextLength(child);
19347
- slots.push({
19348
- parent: paragraph,
19349
- run: child,
19350
- offsetBefore: offset,
19351
- length
19352
- });
19353
- offset += length;
19354
- continue;
19355
- }
19356
- if (child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:hyperlink") {
19357
- for (const inner of child.children) {
19358
- if (inner.tag !== "w:r")
19359
- continue;
19360
- const length = runTextLength(inner);
20044
+ function walk(parent, children) {
20045
+ for (const child of children) {
20046
+ if (child.tag === "w:r") {
20047
+ const length = runTextLength(child);
19361
20048
  slots.push({
19362
- parent: child,
19363
- run: inner,
20049
+ parent,
20050
+ run: child,
19364
20051
  offsetBefore: offset,
19365
20052
  length
19366
20053
  });
19367
20054
  offset += length;
20055
+ continue;
20056
+ }
20057
+ if (isRunBearingWrapper(child.tag)) {
20058
+ walk(child, child.children);
19368
20059
  }
19369
20060
  }
19370
20061
  }
20062
+ walk(paragraph, paragraph.children);
19371
20063
  return slots;
19372
20064
  }
19373
20065
  function rebuildContainer(container, baseOffset, span, replacement, runProperties, isParagraph, tracked) {
@@ -19421,13 +20113,8 @@ function rebuildContainer(container, baseOffset, span, replacement, runPropertie
19421
20113
  }
19422
20114
  continue;
19423
20115
  }
19424
- if (isParagraph && (child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:hyperlink")) {
19425
- let innerLength = 0;
19426
- for (const inner of child.children) {
19427
- if (inner.tag === "w:r")
19428
- innerLength += runTextLength(inner);
19429
- }
19430
- offset += innerLength;
20116
+ if (isParagraph && isRunBearingWrapper(child.tag)) {
20117
+ offset += sumRunBearingTextLength(child.children);
19431
20118
  newChildren.push(child);
19432
20119
  continue;
19433
20120
  }
@@ -19488,8 +20175,8 @@ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tr
19488
20175
  }
19489
20176
  continue;
19490
20177
  }
19491
- if (child.tag === "w:ins" || child.tag === "w:del") {
19492
- const innerLength = sumInnerRunLengths2(child);
20178
+ if (child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:moveFrom" || child.tag === "w:moveTo") {
20179
+ const innerLength = sumRunBearingTextLength(child.children);
19493
20180
  const wrapperStart = offset;
19494
20181
  const wrapperEnd = offset + innerLength;
19495
20182
  offset = wrapperEnd;
@@ -19506,7 +20193,7 @@ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tr
19506
20193
  continue;
19507
20194
  }
19508
20195
  if (child.tag === "w:hyperlink") {
19509
- const innerLength = sumInnerRunLengths2(child);
20196
+ const innerLength = sumRunBearingTextLength(child.children);
19510
20197
  const wrapperStart = offset;
19511
20198
  const wrapperEnd = offset + innerLength;
19512
20199
  offset = wrapperEnd;
@@ -19524,6 +20211,23 @@ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tr
19524
20211
  });
19525
20212
  continue;
19526
20213
  }
20214
+ if (isRunBearingWrapper(child.tag)) {
20215
+ const innerLength = sumRunBearingTextLength(child.children);
20216
+ const wrapperStart = offset;
20217
+ const wrapperEnd = offset + innerLength;
20218
+ offset = wrapperEnd;
20219
+ if (wrapperEnd <= span.start) {
20220
+ newChildren.push(child);
20221
+ continue;
20222
+ }
20223
+ if (wrapperStart >= span.end) {
20224
+ placeReplacement();
20225
+ newChildren.push(child);
20226
+ continue;
20227
+ }
20228
+ splitTransparentWrapperAcrossSpan(child, wrapperStart, span, newChildren, placeReplacement);
20229
+ continue;
20230
+ }
19527
20231
  newChildren.push(child);
19528
20232
  }
19529
20233
  if (!placed)
@@ -19531,7 +20235,7 @@ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tr
19531
20235
  paragraph.children = newChildren;
19532
20236
  }
19533
20237
  function splitWrapperAcrossSpan(wrapper, wrapperStart, span, tracked, out, placeReplacement) {
19534
- const isDel = wrapper.tag === "w:del";
20238
+ const isSubtractive = isSubtractiveTrackedChangeWrapper(wrapper.tag);
19535
20239
  const preInner = [];
19536
20240
  const cutInner = [];
19537
20241
  const postInner = [];
@@ -19562,7 +20266,7 @@ function splitWrapperAcrossSpan(wrapper, wrapperStart, span, tracked, out, place
19562
20266
  postInner.push(sliceRun(inner, sliceEndInRun, length));
19563
20267
  }
19564
20268
  const preChildren = preInner.slice();
19565
- if (isDel) {
20269
+ if (isSubtractive) {
19566
20270
  preChildren.push(...cutInner);
19567
20271
  } else if (tracked && cutInner.length > 0) {
19568
20272
  for (const cutRun of cutInner)
@@ -19584,8 +20288,48 @@ function splitWrapperAcrossSpan(wrapper, wrapperStart, span, tracked, out, place
19584
20288
  out.push(postWrapper);
19585
20289
  }
19586
20290
  }
20291
+ function splitTransparentWrapperAcrossSpan(wrapper, wrapperStart, span, out, placeReplacement) {
20292
+ const preInner = [];
20293
+ const postInner = [];
20294
+ let innerOffset = wrapperStart;
20295
+ for (const inner of wrapper.children) {
20296
+ if (inner.tag !== "w:r") {
20297
+ preInner.push(inner);
20298
+ continue;
20299
+ }
20300
+ const length = runTextLength(inner);
20301
+ const runStart = innerOffset;
20302
+ const runEnd = innerOffset + length;
20303
+ innerOffset = runEnd;
20304
+ if (runEnd <= span.start) {
20305
+ preInner.push(inner);
20306
+ continue;
20307
+ }
20308
+ if (runStart >= span.end) {
20309
+ postInner.push(inner);
20310
+ continue;
20311
+ }
20312
+ const sliceStartInRun = Math.max(0, span.start - runStart);
20313
+ const sliceEndInRun = Math.min(length, span.end - runStart);
20314
+ if (sliceStartInRun > 0)
20315
+ preInner.push(sliceRun(inner, 0, sliceStartInRun));
20316
+ if (sliceEndInRun < length)
20317
+ postInner.push(sliceRun(inner, sliceEndInRun, length));
20318
+ }
20319
+ if (preInner.length > 0) {
20320
+ const preWrapper = new XmlNode2(wrapper.tag, { ...wrapper.attributes });
20321
+ preWrapper.children = preInner;
20322
+ out.push(preWrapper);
20323
+ }
20324
+ placeReplacement();
20325
+ if (postInner.length > 0) {
20326
+ const postWrapper = new XmlNode2(wrapper.tag, { ...wrapper.attributes });
20327
+ postWrapper.children = postInner;
20328
+ out.push(postWrapper);
20329
+ }
20330
+ }
19587
20331
  function splitHyperlinkAcrossSpan(wrapper, wrapperStart, span, runProperties, replacement, tracked, out, placeReplacement, markReplacementPlaced) {
19588
- const wrapperEnd = wrapperStart + wrapper.children.reduce((total, inner) => inner.tag === "w:r" ? total + runTextLength(inner) : total, 0);
20332
+ const wrapperEnd = wrapperStart + sumRunBearingTextLength(wrapper.children);
19589
20333
  const startsInside = span.start > wrapperStart && span.start < wrapperEnd;
19590
20334
  const preInner = [];
19591
20335
  const postInner = [];
@@ -19639,14 +20383,6 @@ function splitHyperlinkAcrossSpan(wrapper, wrapperStart, span, runProperties, re
19639
20383
  out.push(postWrapper);
19640
20384
  }
19641
20385
  }
19642
- function sumInnerRunLengths2(wrapper) {
19643
- let total = 0;
19644
- for (const inner of wrapper.children) {
19645
- if (inner.tag === "w:r")
19646
- total += runTextLength(inner);
19647
- }
19648
- return total;
19649
- }
19650
20386
  function mintMeta(tracked) {
19651
20387
  return { ...tracked.meta, revisionId: tracked.allocator.next() };
19652
20388
  }
@@ -19694,6 +20430,7 @@ async function run26(args) {
19694
20430
  "ignore-case": { type: "boolean" },
19695
20431
  all: { type: "boolean" },
19696
20432
  limit: { type: "string" },
20433
+ author: { type: "string" },
19697
20434
  output: { type: "string", short: "o" },
19698
20435
  "dry-run": { type: "boolean" },
19699
20436
  help: { type: "boolean", short: "h" }
@@ -19792,8 +20529,9 @@ async function run26(args) {
19792
20529
  }
19793
20530
  return rightMatch.start - leftMatch.start;
19794
20531
  });
20532
+ const authorFlag = parsed.values.author;
19795
20533
  const tracked = isTrackChangesEnabled(view) ? {
19796
- meta: { author: resolveAuthor(), date: resolveDate() },
20534
+ meta: { author: resolveAuthor(authorFlag), date: resolveDate() },
19797
20535
  allocator: createRevisionAllocator(view)
19798
20536
  } : undefined;
19799
20537
  const regexFlags = ignoreCase ? "i" : "";
@@ -19827,6 +20565,7 @@ Options:
19827
20565
  --ignore-case case-insensitive match
19828
20566
  --all replace every match (default: just the first)
19829
20567
  --limit N replace at most N matches (in document order)
20568
+ --author NAME author for tracked changes (default: $DOCX_AUTHOR)
19830
20569
  -o, --output PATH write to PATH instead of overwriting FILE
19831
20570
  --dry-run report what would change without writing the file
19832
20571
  -h, --help show this help
@@ -19857,9 +20596,9 @@ var init_replace3 = __esm(() => {
19857
20596
  init_replace_span();
19858
20597
  });
19859
20598
 
19860
- // src/cli/track-changes/index.tsx
19861
- var exports_track_changes = {};
19862
- __export(exports_track_changes, {
20599
+ // src/cli/track-changes/list.ts
20600
+ var exports_list4 = {};
20601
+ __export(exports_list4, {
19863
20602
  run: () => run27
19864
20603
  });
19865
20604
  import { parseArgs as parseArgs23 } from "util";
@@ -19870,8 +20609,6 @@ async function run27(args) {
19870
20609
  args,
19871
20610
  allowPositionals: true,
19872
20611
  options: {
19873
- output: { type: "string", short: "o" },
19874
- "dry-run": { type: "boolean" },
19875
20612
  help: { type: "boolean", short: "h" }
19876
20613
  }
19877
20614
  });
@@ -19886,9 +20623,364 @@ async function run27(args) {
19886
20623
  const path = parsed.positionals[0];
19887
20624
  if (!path)
19888
20625
  return fail("USAGE", "Missing FILE argument", HELP27);
20626
+ const view = await openOrFail(path);
20627
+ if (typeof view === "number")
20628
+ return view;
20629
+ const byId = new Map;
20630
+ for (const paragraph of flattenParagraphs(view.doc.blocks)) {
20631
+ for (const run28 of paragraph.runs) {
20632
+ if (run28.type !== "text" || !run28.trackedChange)
20633
+ continue;
20634
+ const change = run28.trackedChange;
20635
+ const existing = byId.get(change.id);
20636
+ if (existing) {
20637
+ existing.text += run28.text;
20638
+ continue;
20639
+ }
20640
+ byId.set(change.id, {
20641
+ ...change,
20642
+ blockId: paragraph.id,
20643
+ text: run28.text
20644
+ });
20645
+ }
20646
+ }
20647
+ for (const [id, reference] of view.trackedChangeReferences) {
20648
+ if (byId.has(id))
20649
+ continue;
20650
+ const kind = trackedChangeKindForTag(reference.node.tag);
20651
+ if (!kind)
20652
+ continue;
20653
+ byId.set(id, {
20654
+ id,
20655
+ kind,
20656
+ author: reference.node.getAttribute("w:author") ?? "",
20657
+ date: reference.node.getAttribute("w:date") ?? "",
20658
+ revisionId: reference.node.getAttribute("w:id") ?? "",
20659
+ blockId: reference.blockId,
20660
+ text: ""
20661
+ });
20662
+ }
20663
+ const sorted = [...byId.values()].sort((a2, b) => trackedChangeIndex(a2.id) - trackedChangeIndex(b.id));
20664
+ await respond(sorted);
20665
+ return EXIT.OK;
20666
+ }
20667
+ function trackedChangeIndex(id) {
20668
+ const match = id.match(/^tc(\d+)$/);
20669
+ return match?.[1] ? Number(match[1]) : 0;
20670
+ }
20671
+ function trackedChangeKindForTag(tag) {
20672
+ if (tag === "w:ins")
20673
+ return "ins";
20674
+ if (tag === "w:del")
20675
+ return "del";
20676
+ if (tag === "w:moveFrom")
20677
+ return "moveFrom";
20678
+ if (tag === "w:moveTo")
20679
+ return "moveTo";
20680
+ return null;
20681
+ }
20682
+ var HELP27 = `docx track-changes list \u2014 inventory every revision wrapper
20683
+
20684
+ Usage:
20685
+ docx track-changes list FILE [options]
20686
+
20687
+ Options:
20688
+ -h, --help Show this help
20689
+
20690
+ Lists every <w:ins>, <w:del>, <w:moveFrom>, and <w:moveTo> wrapper with
20691
+ stable tcN ids. moveFrom/moveTo halves of the same logical move appear
20692
+ as separate entries (one for each side); their kind tells them apart.
20693
+
20694
+ Output: JSON array of { id, kind, author, date, revisionId, blockId, text }
20695
+ sorted by id (document order). kind is one of: "ins", "del", "moveFrom",
20696
+ "moveTo".
20697
+
20698
+ Examples:
20699
+ docx track-changes list doc.docx
20700
+ docx track-changes list doc.docx | jq '.[] | select(.kind == "del")'
20701
+ docx track-changes list doc.docx | jq '.[] | select(.kind | test("move"))'
20702
+ `;
20703
+ var init_list4 = __esm(() => {
20704
+ init_core();
20705
+ init_respond();
20706
+ });
20707
+
20708
+ // src/cli/track-changes/apply.ts
20709
+ import { parseArgs as parseArgs24 } from "util";
20710
+ async function runApply(args, verb, help) {
20711
+ let parsed;
20712
+ try {
20713
+ parsed = parseArgs24({
20714
+ args,
20715
+ allowPositionals: true,
20716
+ options: {
20717
+ at: { type: "string" },
20718
+ all: { type: "boolean" },
20719
+ output: { type: "string", short: "o" },
20720
+ "dry-run": { type: "boolean" },
20721
+ help: { type: "boolean", short: "h" }
20722
+ }
20723
+ });
20724
+ } catch (parseError) {
20725
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
20726
+ return fail("USAGE", message, help);
20727
+ }
20728
+ if (parsed.values.help) {
20729
+ await writeStdout(help);
20730
+ return EXIT.OK;
20731
+ }
20732
+ const path = parsed.positionals[0];
20733
+ if (!path)
20734
+ return fail("USAGE", "Missing FILE argument", help);
20735
+ const at = parsed.values.at;
20736
+ const all = Boolean(parsed.values.all);
20737
+ if (at && all) {
20738
+ return fail("USAGE", "--at and --all are mutually exclusive", help);
20739
+ }
20740
+ if (!at && !all) {
20741
+ return fail("USAGE", "Specify --at tcN or --all", help);
20742
+ }
20743
+ const view = await openOrFail(path);
20744
+ if (typeof view === "number")
20745
+ return view;
20746
+ const allChanges = collectTrackedChanges(view.documentTree);
20747
+ let targets;
20748
+ if (all) {
20749
+ targets = allChanges;
20750
+ } else {
20751
+ const found = allChanges.find((change) => change.id === at);
20752
+ if (!found) {
20753
+ return fail("TRACKED_CHANGE_NOT_FOUND", `Tracked change not found: ${at}`);
20754
+ }
20755
+ targets = [found];
20756
+ }
20757
+ const records = targets.map((target) => ({
20758
+ id: target.id,
20759
+ kind: target.kind,
20760
+ action: actionFor(target.kind, verb),
20761
+ author: target.author,
20762
+ date: target.date
20763
+ }));
20764
+ const outputPath = parsed.values.output;
20765
+ if (parsed.values["dry-run"]) {
20766
+ await respond({
20767
+ ok: true,
20768
+ operation: `track-changes.${verb}`,
20769
+ dryRun: true,
20770
+ path,
20771
+ ...outputPath ? { output: outputPath } : {},
20772
+ applied: records
20773
+ });
20774
+ return EXIT.OK;
20775
+ }
20776
+ for (const target of [...targets].reverse()) {
20777
+ if (verb === "accept")
20778
+ applyAccept(target.node, target.parent);
20779
+ else
20780
+ applyReject(target.node, target.parent);
20781
+ }
20782
+ await saveDocView(view, outputPath);
20783
+ await respond({
20784
+ ok: true,
20785
+ operation: `track-changes.${verb}`,
20786
+ path: outputPath ?? path,
20787
+ applied: records
20788
+ });
20789
+ return EXIT.OK;
20790
+ }
20791
+ function collectTrackedChanges(tree) {
20792
+ const out = [];
20793
+ let counter = 0;
20794
+ function walk(children) {
20795
+ for (const node of children) {
20796
+ const kind = trackedChangeKindForTag2(node.tag);
20797
+ if (kind) {
20798
+ out.push({
20799
+ node,
20800
+ parent: children,
20801
+ kind,
20802
+ id: `tc${counter++}`,
20803
+ author: node.getAttribute("w:author") ?? "",
20804
+ date: node.getAttribute("w:date") ?? ""
20805
+ });
20806
+ }
20807
+ if (node.children.length > 0)
20808
+ walk(node.children);
20809
+ }
20810
+ }
20811
+ walk(tree);
20812
+ return out;
20813
+ }
20814
+ function trackedChangeKindForTag2(tag) {
20815
+ if (tag === "w:ins")
20816
+ return "ins";
20817
+ if (tag === "w:del")
20818
+ return "del";
20819
+ if (tag === "w:moveFrom")
20820
+ return "moveFrom";
20821
+ if (tag === "w:moveTo")
20822
+ return "moveTo";
20823
+ return null;
20824
+ }
20825
+ function actionFor(kind, verb) {
20826
+ const isAdditive = kind === "ins" || kind === "moveTo";
20827
+ if (verb === "accept")
20828
+ return isAdditive ? "unwrap" : "delete";
20829
+ return isAdditive ? "delete" : "unwrap";
20830
+ }
20831
+ function applyAccept(node, parent) {
20832
+ if (node.tag === "w:ins" || node.tag === "w:moveTo") {
20833
+ unwrapNode(node, parent);
20834
+ return;
20835
+ }
20836
+ deleteNode(node, parent);
20837
+ }
20838
+ function applyReject(node, parent) {
20839
+ if (node.tag === "w:ins" || node.tag === "w:moveTo") {
20840
+ deleteNode(node, parent);
20841
+ return;
20842
+ }
20843
+ renameDelTextToText(node);
20844
+ unwrapNode(node, parent);
20845
+ }
20846
+ function unwrapNode(node, parent) {
20847
+ const index = parent.indexOf(node);
20848
+ if (index === -1)
20849
+ return;
20850
+ parent.splice(index, 1, ...node.children);
20851
+ }
20852
+ function deleteNode(node, parent) {
20853
+ const index = parent.indexOf(node);
20854
+ if (index === -1)
20855
+ return;
20856
+ parent.splice(index, 1);
20857
+ }
20858
+ function renameDelTextToText(node) {
20859
+ if (node.tag === "w:delText")
20860
+ node.tag = "w:t";
20861
+ for (const child of node.children)
20862
+ renameDelTextToText(child);
20863
+ }
20864
+ var init_apply = __esm(() => {
20865
+ init_core();
20866
+ init_respond();
20867
+ });
20868
+
20869
+ // src/cli/track-changes/accept.ts
20870
+ var exports_accept = {};
20871
+ __export(exports_accept, {
20872
+ run: () => run28
20873
+ });
20874
+ async function run28(args) {
20875
+ return runApply(args, "accept", HELP28);
20876
+ }
20877
+ var HELP28 = `docx track-changes accept \u2014 accept tracked changes (incorporate into the doc)
20878
+
20879
+ Usage:
20880
+ docx track-changes accept FILE --at tcN [options]
20881
+ docx track-changes accept FILE --all [options]
20882
+
20883
+ Accepting an insertion (<w:ins>) or move destination (<w:moveTo>) unwraps the
20884
+ wrapper \u2014 the content becomes plain runs at this location. Accepting a
20885
+ deletion (<w:del>) or move source (<w:moveFrom>) removes the element and its
20886
+ <w:delText> entirely (the text disappears for real).
20887
+
20888
+ Out of scope: tracked paragraph marks (<w:rPr><w:ins/></w:rPr> inside <w:pPr>)
20889
+ and formatting changes (<w:rPrChange>/<w:pPrChange>). These aren't modeled in
20890
+ the AST today and --all silently skips them.
20891
+
20892
+ Options:
20893
+ --at tcN Accept a single tracked change by id
20894
+ --all Accept every tracked change (mutually exclusive with --at)
20895
+ -o, --output PATH Write to PATH instead of overwriting FILE
20896
+ --dry-run Print what would change; do not write the file
20897
+ -h, --help Show this help
20898
+
20899
+ Examples:
20900
+ docx track-changes accept doc.docx --at tc0
20901
+ docx track-changes accept doc.docx --all
20902
+ docx track-changes accept doc.docx --all --dry-run
20903
+ `;
20904
+ var init_accept = __esm(() => {
20905
+ init_apply();
20906
+ });
20907
+
20908
+ // src/cli/track-changes/reject.ts
20909
+ var exports_reject = {};
20910
+ __export(exports_reject, {
20911
+ run: () => run29
20912
+ });
20913
+ async function run29(args) {
20914
+ return runApply(args, "reject", HELP29);
20915
+ }
20916
+ var HELP29 = `docx track-changes reject \u2014 reject tracked changes (revert to pre-change state)
20917
+
20918
+ Usage:
20919
+ docx track-changes reject FILE --at tcN [options]
20920
+ docx track-changes reject FILE --all [options]
20921
+
20922
+ Rejecting an insertion (<w:ins>) or move destination (<w:moveTo>) removes the
20923
+ element and its content (it shouldn't have arrived at this location).
20924
+ Rejecting a deletion (<w:del>) or move source (<w:moveFrom>) unwraps the
20925
+ wrapper and converts <w:delText> back to <w:t>, so the text reappears as
20926
+ plain runs.
20927
+
20928
+ moveFrom and moveTo are processed independently. To fully undo a move, target
20929
+ both halves (or use --all). The runtime treats them as paired only by their
20930
+ shared revision id, not by atomic accept/reject.
20931
+
20932
+ Out of scope: tracked paragraph marks (<w:rPr><w:del/></w:rPr> inside <w:pPr>)
20933
+ and formatting changes (<w:rPrChange>/<w:pPrChange>). These aren't modeled in
20934
+ the AST today and --all silently skips them.
20935
+
20936
+ Options:
20937
+ --at tcN Reject a single tracked change by id
20938
+ --all Reject every tracked change (mutually exclusive with --at)
20939
+ -o, --output PATH Write to PATH instead of overwriting FILE
20940
+ --dry-run Print what would change; do not write the file
20941
+ -h, --help Show this help
20942
+
20943
+ Examples:
20944
+ docx track-changes reject doc.docx --at tc0
20945
+ docx track-changes reject doc.docx --all
20946
+ docx track-changes reject doc.docx --all --dry-run
20947
+ `;
20948
+ var init_reject = __esm(() => {
20949
+ init_apply();
20950
+ });
20951
+
20952
+ // src/cli/track-changes/toggle.tsx
20953
+ var exports_toggle = {};
20954
+ __export(exports_toggle, {
20955
+ run: () => run30
20956
+ });
20957
+ import { parseArgs as parseArgs25 } from "util";
20958
+ async function run30(args) {
20959
+ let parsed;
20960
+ try {
20961
+ parsed = parseArgs25({
20962
+ args,
20963
+ allowPositionals: true,
20964
+ options: {
20965
+ output: { type: "string", short: "o" },
20966
+ "dry-run": { type: "boolean" },
20967
+ help: { type: "boolean", short: "h" }
20968
+ }
20969
+ });
20970
+ } catch (parseError) {
20971
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
20972
+ return fail("USAGE", message, HELP30);
20973
+ }
20974
+ if (parsed.values.help) {
20975
+ await writeStdout(HELP30);
20976
+ return EXIT.OK;
20977
+ }
20978
+ const path = parsed.positionals[0];
20979
+ if (!path)
20980
+ return fail("USAGE", "Missing FILE argument", HELP30);
19889
20981
  const mode = parsed.positionals[1];
19890
20982
  if (mode !== "on" && mode !== "off") {
19891
- return fail("USAGE", `Expected "on" or "off", got: ${mode ?? "<nothing>"}`, HELP27);
20983
+ return fail("USAGE", `Expected "on" or "off", got: ${mode ?? "<nothing>"}`, HELP30);
19892
20984
  }
19893
20985
  const view = await openOrFail(path);
19894
20986
  if (typeof view === "number")
@@ -19940,7 +21032,7 @@ async function run27(args) {
19940
21032
  });
19941
21033
  return EXIT.OK;
19942
21034
  }
19943
- var HELP27 = `docx track-changes \u2014 toggle the document's tracked-changes mode
21035
+ var HELP30 = `docx track-changes \u2014 toggle the document's tracked-changes mode
19944
21036
 
19945
21037
  Usage:
19946
21038
  docx track-changes FILE on|off [options]
@@ -19959,7 +21051,7 @@ Examples:
19959
21051
  docx track-changes doc.docx on
19960
21052
  docx track-changes doc.docx off
19961
21053
  `, SETTINGS_PART = "word/settings.xml", SETTINGS_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings", SETTINGS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml", NS_W2 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19962
- var init_track_changes2 = __esm(() => {
21054
+ var init_toggle = __esm(() => {
19963
21055
  init_core();
19964
21056
  init_jsx();
19965
21057
  init_package();
@@ -19968,58 +21060,126 @@ var init_track_changes2 = __esm(() => {
19968
21060
  init_jsx_dev_runtime();
19969
21061
  });
19970
21062
 
21063
+ // src/cli/track-changes/index.ts
21064
+ var exports_track_changes = {};
21065
+ __export(exports_track_changes, {
21066
+ run: () => run31
21067
+ });
21068
+ async function run31(args) {
21069
+ const first = args[0];
21070
+ if (first === "--help" || first === "-h" || first === "help") {
21071
+ await writeStdout(HELP31);
21072
+ return 0;
21073
+ }
21074
+ if (!first) {
21075
+ return fail("USAGE", "Missing arguments", HELP31);
21076
+ }
21077
+ if (first === "list") {
21078
+ const module_2 = await Promise.resolve().then(() => (init_list4(), exports_list4));
21079
+ return module_2.run(args.slice(1));
21080
+ }
21081
+ if (first === "accept") {
21082
+ const module_2 = await Promise.resolve().then(() => (init_accept(), exports_accept));
21083
+ return module_2.run(args.slice(1));
21084
+ }
21085
+ if (first === "reject") {
21086
+ const module_2 = await Promise.resolve().then(() => (init_reject(), exports_reject));
21087
+ return module_2.run(args.slice(1));
21088
+ }
21089
+ const module_ = await Promise.resolve().then(() => (init_toggle(), exports_toggle));
21090
+ return module_.run(args);
21091
+ }
21092
+ var HELP31 = `docx track-changes \u2014 manage tracked-changes
21093
+
21094
+ Usage:
21095
+ docx track-changes FILE on|off [options]
21096
+ docx track-changes list FILE [options]
21097
+ docx track-changes accept FILE (--at tcN | --all) [options]
21098
+ docx track-changes reject FILE (--at tcN | --all) [options]
21099
+
21100
+ Verbs:
21101
+ on Set <w:trackChanges/> in word/settings.xml
21102
+ off Remove <w:trackChanges/>
21103
+ list Inventory every <w:ins>/<w:del>/<w:moveFrom>/<w:moveTo> with
21104
+ id, kind, author, date, location
21105
+ accept Accept tracked changes \u2014 additive wrappers (<w:ins>, <w:moveTo>)
21106
+ unwrap; subtractive wrappers (<w:del>, <w:moveFrom>) are removed
21107
+ reject Reject tracked changes \u2014 additive wrappers are removed;
21108
+ subtractive wrappers unwrap (with <w:delText> \u2192 <w:t> rename)
21109
+
21110
+ When tracking is on, this CLI's insert/edit/delete/replace commands
21111
+ emit <w:ins>/<w:del> markers (attributed via --author or $DOCX_AUTHOR).
21112
+ moveFrom/moveTo are read, listed, and accept/reject independently \u2014 we
21113
+ don't emit them ourselves (Word does that interactively).
21114
+ Accept/reject themselves bypass tracking \u2014 they're doc surgery, not edits.
21115
+
21116
+ Run "docx track-changes <verb> --help" for verb-specific help.
21117
+ `;
21118
+ var init_track_changes2 = __esm(() => {
21119
+ init_respond();
21120
+ });
21121
+
19971
21122
  // src/cli/wc/count.ts
21123
+ function textFor(options) {
21124
+ if (options.view === "accepted")
21125
+ return paragraphTextAccepted;
21126
+ if (options.view === "baseline")
21127
+ return paragraphTextBaseline;
21128
+ return paragraphText;
21129
+ }
19972
21130
  function countWords(text) {
19973
21131
  const matches = text.match(/\S+/g);
19974
21132
  return matches?.length ?? 0;
19975
21133
  }
19976
- function countWordsInBlocks(blocks) {
21134
+ function countWordsInBlocks(blocks, options = {}) {
21135
+ const text = textFor(options);
19977
21136
  let total = 0;
19978
21137
  for (const block of blocks) {
19979
21138
  if (block.type === "paragraph") {
19980
- total += countWords(paragraphText(block));
21139
+ total += countWords(text(block));
19981
21140
  } else if (block.type === "table") {
19982
21141
  for (const row of block.rows) {
19983
21142
  for (const cell of row.cells) {
19984
- total += countWordsInBlocks(cell.blocks);
21143
+ total += countWordsInBlocks(cell.blocks, options);
19985
21144
  }
19986
21145
  }
19987
21146
  }
19988
21147
  }
19989
21148
  return total;
19990
21149
  }
19991
- function countWordsInParagraphSpan(paragraph, start, end) {
19992
- const text = paragraphText(paragraph);
21150
+ function countWordsInParagraphSpan(paragraph, start, end, options = {}) {
21151
+ const text = textFor(options)(paragraph);
19993
21152
  const clampedEnd = Math.min(Math.max(end, 0), text.length);
19994
21153
  const clampedStart = Math.min(Math.max(start, 0), clampedEnd);
19995
21154
  return countWords(text.slice(clampedStart, clampedEnd));
19996
21155
  }
19997
- function countWordsInRange(paragraphsInOrder, startBlockId, startOffset, endBlockId, endOffset) {
21156
+ function countWordsInRange(paragraphsInOrder, startBlockId, startOffset, endBlockId, endOffset, options = {}) {
19998
21157
  const startIndex = paragraphsInOrder.findIndex((p) => p.id === startBlockId);
19999
21158
  const endIndex = paragraphsInOrder.findIndex((p) => p.id === endBlockId);
20000
21159
  if (startIndex === -1 || endIndex === -1)
20001
21160
  return 0;
20002
21161
  if (endIndex < startIndex)
20003
21162
  return 0;
21163
+ const text = textFor(options);
20004
21164
  if (startIndex === endIndex) {
20005
21165
  const paragraph = paragraphsInOrder[startIndex];
20006
21166
  if (!paragraph)
20007
21167
  return 0;
20008
- return countWordsInParagraphSpan(paragraph, startOffset, endOffset);
21168
+ return countWordsInParagraphSpan(paragraph, startOffset, endOffset, options);
20009
21169
  }
20010
21170
  let total = 0;
20011
21171
  const first = paragraphsInOrder[startIndex];
20012
21172
  if (first) {
20013
- total += countWordsInParagraphSpan(first, startOffset, paragraphText(first).length);
21173
+ total += countWordsInParagraphSpan(first, startOffset, text(first).length, options);
20014
21174
  }
20015
21175
  for (let index = startIndex + 1;index < endIndex; index++) {
20016
21176
  const middle = paragraphsInOrder[index];
20017
21177
  if (middle)
20018
- total += countWords(paragraphText(middle));
21178
+ total += countWords(text(middle));
20019
21179
  }
20020
21180
  const last = paragraphsInOrder[endIndex];
20021
21181
  if (last)
20022
- total += countWordsInParagraphSpan(last, 0, endOffset);
21182
+ total += countWordsInParagraphSpan(last, 0, endOffset, options);
20023
21183
  return total;
20024
21184
  }
20025
21185
  var init_count = __esm(() => {
@@ -20029,41 +21189,51 @@ var init_count = __esm(() => {
20029
21189
  // src/cli/wc/index.ts
20030
21190
  var exports_wc = {};
20031
21191
  __export(exports_wc, {
20032
- run: () => run28
21192
+ run: () => run32
20033
21193
  });
20034
- import { parseArgs as parseArgs24 } from "util";
20035
- async function run28(args) {
21194
+ import { parseArgs as parseArgs26 } from "util";
21195
+ async function run32(args) {
20036
21196
  let parsed;
20037
21197
  try {
20038
- parsed = parseArgs24({
21198
+ parsed = parseArgs26({
20039
21199
  args,
20040
21200
  allowPositionals: true,
20041
21201
  options: {
21202
+ accepted: { type: "boolean" },
21203
+ baseline: { type: "boolean" },
20042
21204
  help: { type: "boolean", short: "h" }
20043
21205
  }
20044
21206
  });
20045
21207
  } catch (parseError) {
20046
21208
  const message = parseError instanceof Error ? parseError.message : String(parseError);
20047
- return fail("USAGE", message, HELP28);
21209
+ return fail("USAGE", message, HELP32);
20048
21210
  }
20049
21211
  if (parsed.values.help) {
20050
- await writeStdout(HELP28);
21212
+ await writeStdout(HELP32);
20051
21213
  return EXIT.OK;
20052
21214
  }
20053
21215
  const path = parsed.positionals[0];
20054
21216
  const locatorInput = parsed.positionals[1];
20055
21217
  if (!path)
20056
- return fail("USAGE", "Missing FILE argument", HELP28);
20057
- const view = await openOrFail(path);
20058
- if (typeof view === "number")
20059
- return view;
21218
+ return fail("USAGE", "Missing FILE argument", HELP32);
21219
+ const wantAccepted = Boolean(parsed.values.accepted);
21220
+ const wantBaseline = Boolean(parsed.values.baseline);
21221
+ if (wantAccepted && wantBaseline) {
21222
+ return fail("USAGE", "--accepted and --baseline are mutually exclusive", HELP32);
21223
+ }
21224
+ const view = wantAccepted ? "accepted" : wantBaseline ? "baseline" : "current";
21225
+ const pickText = paragraphTextFor(view);
21226
+ const docView = await openOrFail(path);
21227
+ if (typeof docView === "number")
21228
+ return docView;
20060
21229
  if (!locatorInput) {
20061
21230
  await respond({
20062
21231
  ok: true,
20063
21232
  operation: "wc",
20064
21233
  path,
20065
21234
  scope: "document",
20066
- words: countWordsInBlocks(view.doc.blocks)
21235
+ view,
21236
+ words: countWordsInBlocks(docView.doc.blocks, { view })
20067
21237
  });
20068
21238
  return EXIT.OK;
20069
21239
  }
@@ -20076,22 +21246,23 @@ async function run28(args) {
20076
21246
  }
20077
21247
  throw error;
20078
21248
  }
20079
- if (locator.kind === "comment" || locator.kind === "image") {
21249
+ if (locator.kind === "comment" || locator.kind === "image" || locator.kind === "hyperlink" || locator.kind === "trackedChange") {
20080
21250
  return fail("USAGE", `Locator ${locatorInput} addresses a ${locator.kind}, not text`, "wc accepts paragraph, span, range, table, and cell locators.");
20081
21251
  }
20082
- const blocks = view.doc.blocks;
21252
+ const blocks = docView.doc.blocks;
20083
21253
  if (locator.kind === "block") {
20084
21254
  const block = findBlockById(blocks, locator.blockId);
20085
21255
  if (!block) {
20086
21256
  return fail("BLOCK_NOT_FOUND", `Block not found: ${locator.blockId}`);
20087
21257
  }
20088
- const words = block.type === "paragraph" ? countWords(paragraphText(block)) : block.type === "table" ? countWordsInBlocks([block]) : 0;
21258
+ const words = block.type === "paragraph" ? countWords(pickText(block)) : block.type === "table" ? countWordsInBlocks([block], { view }) : 0;
20089
21259
  await respond({
20090
21260
  ok: true,
20091
21261
  operation: "wc",
20092
21262
  path,
20093
21263
  locator: locatorInput,
20094
21264
  scope: block.type,
21265
+ view,
20095
21266
  words
20096
21267
  });
20097
21268
  return EXIT.OK;
@@ -20107,7 +21278,10 @@ async function run28(args) {
20107
21278
  path,
20108
21279
  locator: locatorInput,
20109
21280
  scope: "paragraphSpan",
20110
- words: countWordsInParagraphSpan(block, locator.start, locator.end)
21281
+ view,
21282
+ words: countWordsInParagraphSpan(block, locator.start, locator.end, {
21283
+ view
21284
+ })
20111
21285
  });
20112
21286
  return EXIT.OK;
20113
21287
  }
@@ -20127,7 +21301,8 @@ async function run28(args) {
20127
21301
  path,
20128
21302
  locator: locatorInput,
20129
21303
  scope: "range",
20130
- words: countWordsInRange(paragraphs, locator.start.blockId, locator.start.offset, locator.end.blockId, locator.end.offset)
21304
+ view,
21305
+ words: countWordsInRange(paragraphs, locator.start.blockId, locator.start.offset, locator.end.blockId, locator.end.offset, { view })
20131
21306
  });
20132
21307
  return EXIT.OK;
20133
21308
  }
@@ -20144,7 +21319,8 @@ async function run28(args) {
20144
21319
  path,
20145
21320
  locator: locatorInput,
20146
21321
  scope: "cell",
20147
- words: countWordsInBlocks(cellBlocks)
21322
+ view,
21323
+ words: countWordsInBlocks(cellBlocks, { view })
20148
21324
  });
20149
21325
  return EXIT.OK;
20150
21326
  }
@@ -20157,13 +21333,14 @@ async function run28(args) {
20157
21333
  if (!block || block.type !== "paragraph") {
20158
21334
  return fail("BLOCK_NOT_FOUND", `Paragraph not found: ${composedId}`);
20159
21335
  }
20160
- const words = locator.inner.kind === "blockSpan" ? countWordsInParagraphSpan(block, locator.inner.start, locator.inner.end) : countWords(paragraphText(block));
21336
+ const words = locator.inner.kind === "blockSpan" ? countWordsInParagraphSpan(block, locator.inner.start, locator.inner.end, { view }) : countWords(pickText(block));
20161
21337
  await respond({
20162
21338
  ok: true,
20163
21339
  operation: "wc",
20164
21340
  path,
20165
21341
  locator: locatorInput,
20166
21342
  scope: locator.inner.kind === "blockSpan" ? "paragraphSpan" : "paragraph",
21343
+ view,
20167
21344
  words
20168
21345
  });
20169
21346
  return EXIT.OK;
@@ -20182,7 +21359,14 @@ function findCellBlocks(blocks, locator) {
20182
21359
  return null;
20183
21360
  return cell.blocks;
20184
21361
  }
20185
- var HELP28 = `docx wc \u2014 count words in a document or a locator-addressed slice
21362
+ function paragraphTextFor(view) {
21363
+ if (view === "accepted")
21364
+ return paragraphTextAccepted;
21365
+ if (view === "baseline")
21366
+ return paragraphTextBaseline;
21367
+ return paragraphText;
21368
+ }
21369
+ var HELP32 = `docx wc \u2014 count words in a document or a locator-addressed slice
20186
21370
 
20187
21371
  Usage:
20188
21372
  docx wc FILE [LOCATOR] [options]
@@ -20197,13 +21381,20 @@ Locators (optional; default: whole document):
20197
21381
  tN:rRcC:pK:S-E span within a cell paragraph
20198
21382
 
20199
21383
  Options:
21384
+ --accepted Count the accepted view: skip subtractive wrappers
21385
+ (<w:del>, <w:moveFrom>); keep additive wrappers
21386
+ (<w:ins>, <w:moveTo>) as plain text. Mirrors
21387
+ "docx read --markdown --accepted".
21388
+ --baseline Count the baseline view: skip additive wrappers
21389
+ (<w:ins>, <w:moveTo>); keep subtractive wrappers
21390
+ (<w:del>, <w:moveFrom>) as plain text \u2014 i.e., the doc as
21391
+ it was before any tracked changes were made.
20200
21392
  -h, --help show this help
20201
21393
 
20202
21394
  Counting is whitespace-segmented (\\S+) over the joined paragraph text. Hidden
20203
- content like images/breaks/tabs contributes no words. Tracked-change deletions
20204
- are counted alongside insertions because their characters are still part of the
20205
- on-disk text; accept or reject changes first if you need a delta on the
20206
- "accepted" view.
21395
+ content like images/breaks/tabs contributes no words. By default, every
21396
+ tracked-change wrapper's text counts (they're all on disk); pass --accepted
21397
+ or --baseline (mutually exclusive) to count a tracked-change-aware slice.
20207
21398
 
20208
21399
  Examples:
20209
21400
  docx wc doc.docx
@@ -20220,7 +21411,7 @@ var init_wc = __esm(() => {
20220
21411
  // package.json
20221
21412
  var package_default = {
20222
21413
  name: "bun-docx",
20223
- version: "0.6.0",
21414
+ version: "0.7.0",
20224
21415
  description: "CLI for AI agents (Claude, Codex) to read, edit, and comment on .docx files with full format fidelity.",
20225
21416
  keywords: [
20226
21417
  "docx",
@@ -20299,10 +21490,11 @@ Commands:
20299
21490
  outline FILE List headings as a hierarchical tree
20300
21491
  comments \u2026 Add, reply, resolve, delete, list comments
20301
21492
  images \u2026 Extract, replace, list images
20302
- hyperlinks \u2026 List, replace hyperlinks
20303
- track-changes FILE on|off Toggle tracked-changes mode
21493
+ hyperlinks \u2026 Add, list, replace, delete hyperlinks
21494
+ track-changes \u2026 Toggle (FILE on|off), list / accept / reject changes
20304
21495
  info \u2026 Reference material (schema, locators)
20305
21496
 
21497
+ It is highly recommended to agents to run "docx info locators" to understand their capabilities.
20306
21498
  Run "docx <command> --help" for command-specific help.
20307
21499
 
20308
21500
  Environment:
@@ -20310,9 +21502,19 @@ Environment:
20310
21502
  DOCX_CLI_NOW Override the timestamp used for tracked changes (test only)
20311
21503
 
20312
21504
  Tracked changes:
20313
- When <w:trackChanges/> is set in the doc (toggle via "docx track-changes
20314
- FILE on"), insert/edit/delete/replace automatically emit <w:ins>/<w:del>
20315
- markers attributed to $DOCX_AUTHOR.
21505
+ Toggle: docx track-changes FILE on|off
21506
+ Inventory: docx track-changes list FILE (JSON: id, kind, author, date, blockId, text)
21507
+ Accept: docx track-changes accept FILE (--at tcN | --all)
21508
+ Reject: docx track-changes reject FILE (--at tcN | --all)
21509
+
21510
+ When <w:trackChanges/> is set, insert/edit/delete/replace automatically emit
21511
+ <w:ins>/<w:del> markers attributed via --author (or $DOCX_AUTHOR, or "docx-cli").
21512
+
21513
+ "docx read --markdown" surfaces them as CriticMarkup:
21514
+ {++inserted++}[^tcN] {--deleted--}[^tcN]
21515
+ with a [^tcN]: definition appendix. Switch view with --accepted (post-accept:
21516
+ drop del, ins as plain) or --baseline (pre-change: drop ins, del as plain).
21517
+ "docx wc" mirrors the same --accepted / --baseline flags.
20316
21518
  `;
20317
21519
  async function printTopHelp() {
20318
21520
  await writeStdout(TOP_HELP);