bun-docx 0.6.1 → 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 +19 -11
  2. package/dist/index.js +1200 -198
  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;
13595
13604
  continue;
13596
13605
  }
13597
- sliced.children.push(child.clone());
13606
+ if (SINGLE_CHAR_TAGS.has(child.tag)) {
13607
+ if (offset >= start && offset < end) {
13608
+ sliced.children.push(child.clone());
13609
+ }
13610
+ offset += 1;
13611
+ continue;
13612
+ }
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}` };
13992
- }
13993
- }
13994
- let combinedText = "";
13995
- for (const child of node.children) {
13996
- if (child.tag === "w:t" || child.tag === "w:delText") {
13997
- combinedText += child.collectText();
14404
+ out.push({ type: "footnoteRef", kind: "endnote", id: `en${id}` });
13998
14405
  }
13999
14406
  }
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
 
@@ -14658,7 +15095,7 @@ function convertTextToDelText(node) {
14658
15095
  function computeMaxRevisionId(view) {
14659
15096
  let max = -1;
14660
15097
  walkXml(view.documentTree, (node) => {
14661
- 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")
14662
15099
  return;
14663
15100
  const idAttr = node.getAttribute("w:id");
14664
15101
  if (!idAttr)
@@ -14946,6 +15383,7 @@ function exitCodeFor(code) {
14946
15383
  case "COMMENT_NOT_FOUND":
14947
15384
  case "IMAGE_NOT_FOUND":
14948
15385
  case "HYPERLINK_NOT_FOUND":
15386
+ case "TRACKED_CHANGE_NOT_FOUND":
14949
15387
  case "MATCH_NOT_FOUND":
14950
15388
  return EXIT.NOT_FOUND;
14951
15389
  case "NOT_A_ZIP":
@@ -15065,18 +15503,7 @@ function ensureCommentsExtPart(view) {
15065
15503
  return root;
15066
15504
  }
15067
15505
  function paragraphTextLength(paragraph) {
15068
- return sumTextLength(paragraph.children);
15069
- }
15070
- function sumTextLength(children) {
15071
- let total = 0;
15072
- for (const child of children) {
15073
- if (child.tag === "w:r")
15074
- total += runTextLength(child);
15075
- else if (child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:hyperlink") {
15076
- total += sumTextLength(child.children);
15077
- }
15078
- }
15079
- return total;
15506
+ return sumRunBearingTextLength(paragraph.children);
15080
15507
  }
15081
15508
  function addCommentMarkersToParagraph(paragraph, commentId, span) {
15082
15509
  const total = paragraphTextLength(paragraph);
@@ -15121,7 +15548,7 @@ function addCommentMarkersAroundRun(paragraph, target, commentId) {
15121
15548
  children.splice(index, 1, commentRangeStartMarker(commentId), target, commentRangeEndMarker(commentId), commentReferenceRun(commentId));
15122
15549
  return true;
15123
15550
  }
15124
- if (child && (child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:hyperlink")) {
15551
+ if (child && isRunBearingWrapper(child.tag)) {
15125
15552
  if (walk(child))
15126
15553
  return true;
15127
15554
  }
@@ -15137,7 +15564,7 @@ function findElementOffsetsInParagraph(paragraph, target) {
15137
15564
  for (const child of children) {
15138
15565
  if (child === target) {
15139
15566
  const start = cursor;
15140
- cursor += sumTextLength([child]);
15567
+ cursor += sumRunBearingTextLength([child]);
15141
15568
  result = { start, end: cursor };
15142
15569
  return true;
15143
15570
  }
@@ -15145,7 +15572,7 @@ function findElementOffsetsInParagraph(paragraph, target) {
15145
15572
  cursor += runTextLength(child);
15146
15573
  continue;
15147
15574
  }
15148
- if (child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:hyperlink") {
15575
+ if (isRunBearingWrapper(child.tag)) {
15149
15576
  if (walk(child.children))
15150
15577
  return true;
15151
15578
  }
@@ -15286,7 +15713,7 @@ function walkAndPlace(children, pending, state) {
15286
15713
  state.offset = runEnd;
15287
15714
  continue;
15288
15715
  }
15289
- if (child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:hyperlink") {
15716
+ if (isRunBearingWrapper(child.tag)) {
15290
15717
  flushAtCurrentOffset(result, pending, state);
15291
15718
  const innerChildren = walkAndPlace(child.children, pending, state);
15292
15719
  const wrapper = new XmlNode2(child.tag, { ...child.attributes });
@@ -16794,10 +17221,9 @@ function trackedChangesOverlapping(paragraph, start, end) {
16794
17221
  const change = run10.trackedChange;
16795
17222
  if (!change)
16796
17223
  continue;
16797
- const key = `${change.kind}:${change.revisionId}:${change.author}`;
16798
- if (seen.has(key))
17224
+ if (seen.has(change.id))
16799
17225
  continue;
16800
- seen.add(key);
17226
+ seen.add(change.id);
16801
17227
  out.push(change);
16802
17228
  }
16803
17229
  return out;
@@ -16955,20 +17381,8 @@ function wrapSpanInHyperlink(paragraph, span, relationshipId) {
16955
17381
  }
16956
17382
  continue;
16957
17383
  }
16958
- if (child.tag === "w:hyperlink") {
16959
- const hyperlinkLength = sumInnerRunLengths(child);
16960
- const hyperlinkStart = offset;
16961
- const hyperlinkEnd = offset + hyperlinkLength;
16962
- offset = hyperlinkEnd;
16963
- if (hyperlinkEnd <= span.start || hyperlinkStart >= span.end) {
16964
- placeWrapper();
16965
- newChildren.push(child);
16966
- continue;
16967
- }
16968
- throw new HyperlinkWrapError(`Span ${span.start}-${span.end} overlaps an existing hyperlink at ${hyperlinkStart}-${hyperlinkEnd}; nested hyperlinks are not allowed`);
16969
- }
16970
- if (child.tag === "w:ins" || child.tag === "w:del") {
16971
- const innerLength = sumInnerRunLengths(child);
17384
+ if (isRunBearingWrapper(child.tag)) {
17385
+ const innerLength = sumRunBearingTextLength(child.children);
16972
17386
  const wrapperStart = offset;
16973
17387
  const wrapperEnd = offset + innerLength;
16974
17388
  offset = wrapperEnd;
@@ -16977,27 +17391,28 @@ function wrapSpanInHyperlink(paragraph, span, relationshipId) {
16977
17391
  newChildren.push(child);
16978
17392
  continue;
16979
17393
  }
16980
- 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));
16981
17395
  }
16982
17396
  newChildren.push(child);
16983
17397
  }
16984
17398
  placeWrapper();
16985
17399
  paragraph.children = newChildren;
16986
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
+ }
16987
17410
  function hyperlinkWrapper(relationshipId, runs) {
16988
17411
  return /* @__PURE__ */ jsxDEV(w.hyperlink, {
16989
17412
  "r:id": relationshipId,
16990
17413
  children: runs
16991
17414
  }, undefined, false, undefined, this);
16992
17415
  }
16993
- function sumInnerRunLengths(wrapper) {
16994
- let total = 0;
16995
- for (const inner of wrapper.children) {
16996
- if (inner.tag === "w:r")
16997
- total += runTextLength(inner);
16998
- }
16999
- return total;
17000
- }
17001
17416
  var HyperlinkWrapError;
17002
17417
  var init_wrap = __esm(() => {
17003
17418
  init_jsx();
@@ -17131,7 +17546,9 @@ Optional:
17131
17546
  -h, --help Show this help
17132
17547
 
17133
17548
  The span must lie inside a single paragraph and must not overlap an existing
17134
- 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.
17135
17552
 
17136
17553
  When track-changes is on, an audit comment is anchored to the wrapped span
17137
17554
  since OOXML has no native tracked-change form for hyperlink edits.
@@ -18168,12 +18585,22 @@ export type Footnote = {
18168
18585
  };
18169
18586
 
18170
18587
  export type TrackedChange = {
18171
- kind: "ins" | "del";
18588
+ id: string;
18589
+ kind: TrackedChangeKind;
18172
18590
  author: string;
18173
18591
  date: string;
18174
18592
  revisionId: string;
18175
18593
  };
18176
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
+
18177
18604
  export type Comment = {
18178
18605
  id: string;
18179
18606
  author: string;
@@ -18330,9 +18757,10 @@ var init_schema = __esm(() => {
18330
18757
  comments: { type: "array", items: { type: "string" } },
18331
18758
  trackedChange: {
18332
18759
  type: "object",
18333
- required: ["kind", "author", "date", "revisionId"],
18760
+ required: ["id", "kind", "author", "date", "revisionId"],
18334
18761
  properties: {
18335
- kind: { enum: ["ins", "del"] },
18762
+ id: { type: "string" },
18763
+ kind: { enum: ["ins", "del", "moveFrom", "moveTo"] },
18336
18764
  author: { type: "string" },
18337
18765
  date: { type: "string" },
18338
18766
  revisionId: { type: "string" }
@@ -18538,6 +18966,8 @@ Entity locators:
18538
18966
  cN Comment id (e.g., c0)
18539
18967
  imgN Image id (e.g., img2)
18540
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
18541
18971
  tN:rRcC Cell at row R, column C of table tN
18542
18972
 
18543
18973
  Examples:
@@ -18547,6 +18977,7 @@ Examples:
18547
18977
  c1 -> comment c1
18548
18978
  img0 -> image img0
18549
18979
  link0 -> hyperlink link0
18980
+ tc0 -> first tracked change in the document
18550
18981
  t0:r1c2 -> cell at row 1, col 2 of table t0
18551
18982
  t0:r1c2:p0 -> first paragraph of that cell
18552
18983
  t0:r1c2:p0:5-10 -> chars 5..10 of that paragraph
@@ -18580,6 +19011,7 @@ var init_locators2 = __esm(() => {
18580
19011
  comment: { syntax: "cN", example: "c1" },
18581
19012
  image: { syntax: "imgN", example: "img0" },
18582
19013
  hyperlink: { syntax: "linkN", example: "link0" },
19014
+ trackedChange: { syntax: "tcN", example: "tc0" },
18583
19015
  cell: { syntax: "tN:rRcC", example: "t0:r1c2" },
18584
19016
  nestedCell: { syntax: "tN:rRcC:pK", example: "t0:r1c2:p0" }
18585
19017
  },
@@ -19001,7 +19433,8 @@ function renderMarkdown(doc, options = {}) {
19001
19433
  options,
19002
19434
  commentIndex,
19003
19435
  referencedFootnoteIds: new Set,
19004
- referencedEndnoteIds: new Set
19436
+ referencedEndnoteIds: new Set,
19437
+ referencedTrackedChanges: new Map
19005
19438
  };
19006
19439
  const parts = [];
19007
19440
  for (const block of blocks) {
@@ -19021,6 +19454,9 @@ function renderMarkdown(doc, options = {}) {
19021
19454
  const endnoteDefs = renderNoteDefinitions(doc.endnotes, ctx.referencedEndnoteIds);
19022
19455
  if (endnoteDefs.length > 0)
19023
19456
  definitions.push(endnoteDefs);
19457
+ const trackedChangeDefs = renderTrackedChangeFootnotes(ctx.referencedTrackedChanges);
19458
+ if (trackedChangeDefs.length > 0)
19459
+ definitions.push(trackedChangeDefs);
19024
19460
  if (definitions.length > 0)
19025
19461
  parts.push(definitions.join(`
19026
19462
  `));
@@ -19038,8 +19474,18 @@ function emptyCommentIndex() {
19038
19474
  orderedIds: []
19039
19475
  };
19040
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
+ }
19041
19487
  function buildCommentIndex(blocks, options) {
19042
- const showChanges = options.showChanges ?? false;
19488
+ const view = options.view ?? "current";
19043
19489
  const lastSlot = new Map;
19044
19490
  const spanText = new Map;
19045
19491
  const orderedIds = [];
@@ -19047,7 +19493,7 @@ function buildCommentIndex(blocks, options) {
19047
19493
  paragraph.runs.forEach((run25, index) => {
19048
19494
  if (run25.type !== "text")
19049
19495
  return;
19050
- if (!showChanges && run25.trackedChange?.kind === "del")
19496
+ if (!isRunVisible(run25, view))
19051
19497
  return;
19052
19498
  for (const commentId of run25.comments ?? []) {
19053
19499
  if (!spanText.has(commentId))
@@ -19111,12 +19557,11 @@ function headingLevelFor(style) {
19111
19557
  return parsed;
19112
19558
  }
19113
19559
  function renderRuns(paragraphId, runs, ctx) {
19114
- const showChanges = ctx.options.showChanges ?? false;
19560
+ const view = ctx.options.view ?? "current";
19115
19561
  const visibleEntries = [];
19116
19562
  runs.forEach((run25, index) => {
19117
- if (!showChanges && run25.type === "text" && run25.trackedChange?.kind === "del") {
19563
+ if (run25.type === "text" && !isRunVisible(run25, view))
19118
19564
  return;
19119
- }
19120
19565
  visibleEntries.push({ run: run25, originalIndex: index });
19121
19566
  });
19122
19567
  let out = "";
@@ -19139,7 +19584,15 @@ function renderRuns(paragraphId, runs, ctx) {
19139
19584
  lookahead++;
19140
19585
  }
19141
19586
  const segment = visibleEntries.slice(cursor, lookahead);
19142
- 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
+ }
19143
19596
  out += commentEndingsFor(paragraphId, segment, ctx.commentIndex);
19144
19597
  cursor = lookahead;
19145
19598
  continue;
@@ -19182,7 +19635,7 @@ function commentEndingsFor(paragraphId, segment, commentIndex) {
19182
19635
  return out;
19183
19636
  }
19184
19637
  function sameDecoration(a2, b) {
19185
- 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);
19186
19639
  }
19187
19640
  function sameCommentSet(left, right) {
19188
19641
  const a2 = left ?? [];
@@ -19195,7 +19648,7 @@ function sameCommentSet(left, right) {
19195
19648
  }
19196
19649
  return true;
19197
19650
  }
19198
- function renderTextSegment(runs, showChanges) {
19651
+ function renderTextSegment(runs, view) {
19199
19652
  const text = runs.map((run25) => run25.text).join("");
19200
19653
  if (text.length === 0)
19201
19654
  return "";
@@ -19221,11 +19674,17 @@ function renderTextSegment(runs, showChanges) {
19221
19674
  const target = first.hyperlink.url ?? `#${first.hyperlink.anchor ?? ""}`;
19222
19675
  out = `[${out}](${target})`;
19223
19676
  }
19224
- if (showChanges && first.trackedChange) {
19225
- 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}]`;
19226
19680
  }
19227
19681
  return out;
19228
19682
  }
19683
+ function criticMarkerFor(kind) {
19684
+ if (kind === "ins" || kind === "moveTo")
19685
+ return "++";
19686
+ return "--";
19687
+ }
19229
19688
  function colorAttrFor(value) {
19230
19689
  if (!value)
19231
19690
  return null;
@@ -19330,6 +19789,32 @@ function renderNoteDefinitions(notes, referenced) {
19330
19789
  return lines.join(`
19331
19790
  `);
19332
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
+ }
19333
19818
  function noteIdCompare(left, right) {
19334
19819
  return numericIdCompare(left, right, /(\d+)$/);
19335
19820
  }
@@ -19395,6 +19880,7 @@ function blockIdForLocator(input, position) {
19395
19880
  case "comment":
19396
19881
  case "image":
19397
19882
  case "hyperlink":
19883
+ case "trackedChange":
19398
19884
  throw new MarkdownLocatorError(input, `--${position} does not accept a ${parsed.kind} locator \u2014 use a paragraph or table locator`);
19399
19885
  }
19400
19886
  }
@@ -19427,7 +19913,8 @@ async function run25(args) {
19427
19913
  markdown: { type: "boolean" },
19428
19914
  from: { type: "string" },
19429
19915
  to: { type: "string" },
19430
- changes: { type: "boolean" },
19916
+ accepted: { type: "boolean" },
19917
+ baseline: { type: "boolean" },
19431
19918
  comments: { type: "boolean" },
19432
19919
  help: { type: "boolean", short: "h" }
19433
19920
  }
@@ -19445,20 +19932,25 @@ async function run25(args) {
19445
19932
  const markdown = Boolean(parsed.values.markdown);
19446
19933
  const from = parsed.values.from;
19447
19934
  const to = parsed.values.to;
19448
- const showChanges = Boolean(parsed.values.changes);
19935
+ const accepted = Boolean(parsed.values.accepted);
19936
+ const baseline = Boolean(parsed.values.baseline);
19449
19937
  const showComments = Boolean(parsed.values.comments);
19450
- if (!markdown && (from || to || showChanges || showComments)) {
19451
- 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);
19452
19940
  }
19453
- const view = await openOrFail(path);
19454
- if (typeof view === "number")
19455
- 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;
19456
19948
  if (markdown) {
19457
19949
  try {
19458
- const rendered = renderMarkdown(view.doc, {
19950
+ const rendered = renderMarkdown(docView.doc, {
19459
19951
  from,
19460
19952
  to,
19461
- showChanges,
19953
+ view,
19462
19954
  showComments
19463
19955
  });
19464
19956
  await writeStdout(rendered);
@@ -19470,8 +19962,8 @@ async function run25(args) {
19470
19962
  throw err;
19471
19963
  }
19472
19964
  }
19473
- await enrichImageHashes(view);
19474
- await respond(view.doc);
19965
+ await enrichImageHashes(docView);
19966
+ await respond(docView.doc);
19475
19967
  return EXIT.OK;
19476
19968
  }
19477
19969
  var HELP25 = `docx read \u2014 print AST as JSON, or render document body as Markdown
@@ -19490,8 +19982,18 @@ Options:
19490
19982
  Accepts pN, tN, tN:rRcC[:pK[:S-E]], pN:S-E, pN:S-pM:E.
19491
19983
  Cell/span/range locators collapse to their enclosing
19492
19984
  top-level block (the table or paragraph).
19493
- --changes With --markdown, render tracked insertions/deletions as
19494
- <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).
19495
19997
  --comments With --markdown, append [^cN] after each commented span
19496
19998
  and emit a footnote definition for each comment at the
19497
19999
  end of the output (author, date, body).
@@ -19501,7 +20003,8 @@ Examples:
19501
20003
  docx read input.docx
19502
20004
  docx read input.docx --markdown
19503
20005
  docx read input.docx --markdown --from p3 --to p20
19504
- docx read input.docx --markdown --changes --comments
20006
+ docx read input.docx --markdown --accepted --comments
20007
+ docx read input.docx --markdown --baseline
19505
20008
  docx read input.docx | jq '.blocks[] | select(.type == "paragraph")'
19506
20009
  `;
19507
20010
  var init_read2 = __esm(() => {
@@ -19538,33 +20041,25 @@ function replaceSpanInParagraph(paragraph, span, replacement, tracked) {
19538
20041
  function collectRunSlots(paragraph) {
19539
20042
  const slots = [];
19540
20043
  let offset = 0;
19541
- for (const child of paragraph.children) {
19542
- if (child.tag === "w:r") {
19543
- const length = runTextLength(child);
19544
- slots.push({
19545
- parent: paragraph,
19546
- run: child,
19547
- offsetBefore: offset,
19548
- length
19549
- });
19550
- offset += length;
19551
- continue;
19552
- }
19553
- if (child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:hyperlink") {
19554
- for (const inner of child.children) {
19555
- if (inner.tag !== "w:r")
19556
- continue;
19557
- 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);
19558
20048
  slots.push({
19559
- parent: child,
19560
- run: inner,
20049
+ parent,
20050
+ run: child,
19561
20051
  offsetBefore: offset,
19562
20052
  length
19563
20053
  });
19564
20054
  offset += length;
20055
+ continue;
20056
+ }
20057
+ if (isRunBearingWrapper(child.tag)) {
20058
+ walk(child, child.children);
19565
20059
  }
19566
20060
  }
19567
20061
  }
20062
+ walk(paragraph, paragraph.children);
19568
20063
  return slots;
19569
20064
  }
19570
20065
  function rebuildContainer(container, baseOffset, span, replacement, runProperties, isParagraph, tracked) {
@@ -19618,13 +20113,8 @@ function rebuildContainer(container, baseOffset, span, replacement, runPropertie
19618
20113
  }
19619
20114
  continue;
19620
20115
  }
19621
- if (isParagraph && (child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:hyperlink")) {
19622
- let innerLength = 0;
19623
- for (const inner of child.children) {
19624
- if (inner.tag === "w:r")
19625
- innerLength += runTextLength(inner);
19626
- }
19627
- offset += innerLength;
20116
+ if (isParagraph && isRunBearingWrapper(child.tag)) {
20117
+ offset += sumRunBearingTextLength(child.children);
19628
20118
  newChildren.push(child);
19629
20119
  continue;
19630
20120
  }
@@ -19685,8 +20175,8 @@ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tr
19685
20175
  }
19686
20176
  continue;
19687
20177
  }
19688
- if (child.tag === "w:ins" || child.tag === "w:del") {
19689
- 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);
19690
20180
  const wrapperStart = offset;
19691
20181
  const wrapperEnd = offset + innerLength;
19692
20182
  offset = wrapperEnd;
@@ -19703,7 +20193,7 @@ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tr
19703
20193
  continue;
19704
20194
  }
19705
20195
  if (child.tag === "w:hyperlink") {
19706
- const innerLength = sumInnerRunLengths2(child);
20196
+ const innerLength = sumRunBearingTextLength(child.children);
19707
20197
  const wrapperStart = offset;
19708
20198
  const wrapperEnd = offset + innerLength;
19709
20199
  offset = wrapperEnd;
@@ -19721,6 +20211,23 @@ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tr
19721
20211
  });
19722
20212
  continue;
19723
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
+ }
19724
20231
  newChildren.push(child);
19725
20232
  }
19726
20233
  if (!placed)
@@ -19728,7 +20235,7 @@ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tr
19728
20235
  paragraph.children = newChildren;
19729
20236
  }
19730
20237
  function splitWrapperAcrossSpan(wrapper, wrapperStart, span, tracked, out, placeReplacement) {
19731
- const isDel = wrapper.tag === "w:del";
20238
+ const isSubtractive = isSubtractiveTrackedChangeWrapper(wrapper.tag);
19732
20239
  const preInner = [];
19733
20240
  const cutInner = [];
19734
20241
  const postInner = [];
@@ -19759,7 +20266,7 @@ function splitWrapperAcrossSpan(wrapper, wrapperStart, span, tracked, out, place
19759
20266
  postInner.push(sliceRun(inner, sliceEndInRun, length));
19760
20267
  }
19761
20268
  const preChildren = preInner.slice();
19762
- if (isDel) {
20269
+ if (isSubtractive) {
19763
20270
  preChildren.push(...cutInner);
19764
20271
  } else if (tracked && cutInner.length > 0) {
19765
20272
  for (const cutRun of cutInner)
@@ -19781,8 +20288,48 @@ function splitWrapperAcrossSpan(wrapper, wrapperStart, span, tracked, out, place
19781
20288
  out.push(postWrapper);
19782
20289
  }
19783
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
+ }
19784
20331
  function splitHyperlinkAcrossSpan(wrapper, wrapperStart, span, runProperties, replacement, tracked, out, placeReplacement, markReplacementPlaced) {
19785
- const wrapperEnd = wrapperStart + wrapper.children.reduce((total, inner) => inner.tag === "w:r" ? total + runTextLength(inner) : total, 0);
20332
+ const wrapperEnd = wrapperStart + sumRunBearingTextLength(wrapper.children);
19786
20333
  const startsInside = span.start > wrapperStart && span.start < wrapperEnd;
19787
20334
  const preInner = [];
19788
20335
  const postInner = [];
@@ -19836,14 +20383,6 @@ function splitHyperlinkAcrossSpan(wrapper, wrapperStart, span, runProperties, re
19836
20383
  out.push(postWrapper);
19837
20384
  }
19838
20385
  }
19839
- function sumInnerRunLengths2(wrapper) {
19840
- let total = 0;
19841
- for (const inner of wrapper.children) {
19842
- if (inner.tag === "w:r")
19843
- total += runTextLength(inner);
19844
- }
19845
- return total;
19846
- }
19847
20386
  function mintMeta(tracked) {
19848
20387
  return { ...tracked.meta, revisionId: tracked.allocator.next() };
19849
20388
  }
@@ -20057,9 +20596,9 @@ var init_replace3 = __esm(() => {
20057
20596
  init_replace_span();
20058
20597
  });
20059
20598
 
20060
- // src/cli/track-changes/index.tsx
20061
- var exports_track_changes = {};
20062
- __export(exports_track_changes, {
20599
+ // src/cli/track-changes/list.ts
20600
+ var exports_list4 = {};
20601
+ __export(exports_list4, {
20063
20602
  run: () => run27
20064
20603
  });
20065
20604
  import { parseArgs as parseArgs23 } from "util";
@@ -20070,8 +20609,6 @@ async function run27(args) {
20070
20609
  args,
20071
20610
  allowPositionals: true,
20072
20611
  options: {
20073
- output: { type: "string", short: "o" },
20074
- "dry-run": { type: "boolean" },
20075
20612
  help: { type: "boolean", short: "h" }
20076
20613
  }
20077
20614
  });
@@ -20086,9 +20623,364 @@ async function run27(args) {
20086
20623
  const path = parsed.positionals[0];
20087
20624
  if (!path)
20088
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);
20089
20981
  const mode = parsed.positionals[1];
20090
20982
  if (mode !== "on" && mode !== "off") {
20091
- return fail("USAGE", `Expected "on" or "off", got: ${mode ?? "<nothing>"}`, HELP27);
20983
+ return fail("USAGE", `Expected "on" or "off", got: ${mode ?? "<nothing>"}`, HELP30);
20092
20984
  }
20093
20985
  const view = await openOrFail(path);
20094
20986
  if (typeof view === "number")
@@ -20140,7 +21032,7 @@ async function run27(args) {
20140
21032
  });
20141
21033
  return EXIT.OK;
20142
21034
  }
20143
- 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
20144
21036
 
20145
21037
  Usage:
20146
21038
  docx track-changes FILE on|off [options]
@@ -20159,7 +21051,7 @@ Examples:
20159
21051
  docx track-changes doc.docx on
20160
21052
  docx track-changes doc.docx off
20161
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";
20162
- var init_track_changes2 = __esm(() => {
21054
+ var init_toggle = __esm(() => {
20163
21055
  init_core();
20164
21056
  init_jsx();
20165
21057
  init_package();
@@ -20168,58 +21060,126 @@ var init_track_changes2 = __esm(() => {
20168
21060
  init_jsx_dev_runtime();
20169
21061
  });
20170
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
+
20171
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
+ }
20172
21130
  function countWords(text) {
20173
21131
  const matches = text.match(/\S+/g);
20174
21132
  return matches?.length ?? 0;
20175
21133
  }
20176
- function countWordsInBlocks(blocks) {
21134
+ function countWordsInBlocks(blocks, options = {}) {
21135
+ const text = textFor(options);
20177
21136
  let total = 0;
20178
21137
  for (const block of blocks) {
20179
21138
  if (block.type === "paragraph") {
20180
- total += countWords(paragraphText(block));
21139
+ total += countWords(text(block));
20181
21140
  } else if (block.type === "table") {
20182
21141
  for (const row of block.rows) {
20183
21142
  for (const cell of row.cells) {
20184
- total += countWordsInBlocks(cell.blocks);
21143
+ total += countWordsInBlocks(cell.blocks, options);
20185
21144
  }
20186
21145
  }
20187
21146
  }
20188
21147
  }
20189
21148
  return total;
20190
21149
  }
20191
- function countWordsInParagraphSpan(paragraph, start, end) {
20192
- const text = paragraphText(paragraph);
21150
+ function countWordsInParagraphSpan(paragraph, start, end, options = {}) {
21151
+ const text = textFor(options)(paragraph);
20193
21152
  const clampedEnd = Math.min(Math.max(end, 0), text.length);
20194
21153
  const clampedStart = Math.min(Math.max(start, 0), clampedEnd);
20195
21154
  return countWords(text.slice(clampedStart, clampedEnd));
20196
21155
  }
20197
- function countWordsInRange(paragraphsInOrder, startBlockId, startOffset, endBlockId, endOffset) {
21156
+ function countWordsInRange(paragraphsInOrder, startBlockId, startOffset, endBlockId, endOffset, options = {}) {
20198
21157
  const startIndex = paragraphsInOrder.findIndex((p) => p.id === startBlockId);
20199
21158
  const endIndex = paragraphsInOrder.findIndex((p) => p.id === endBlockId);
20200
21159
  if (startIndex === -1 || endIndex === -1)
20201
21160
  return 0;
20202
21161
  if (endIndex < startIndex)
20203
21162
  return 0;
21163
+ const text = textFor(options);
20204
21164
  if (startIndex === endIndex) {
20205
21165
  const paragraph = paragraphsInOrder[startIndex];
20206
21166
  if (!paragraph)
20207
21167
  return 0;
20208
- return countWordsInParagraphSpan(paragraph, startOffset, endOffset);
21168
+ return countWordsInParagraphSpan(paragraph, startOffset, endOffset, options);
20209
21169
  }
20210
21170
  let total = 0;
20211
21171
  const first = paragraphsInOrder[startIndex];
20212
21172
  if (first) {
20213
- total += countWordsInParagraphSpan(first, startOffset, paragraphText(first).length);
21173
+ total += countWordsInParagraphSpan(first, startOffset, text(first).length, options);
20214
21174
  }
20215
21175
  for (let index = startIndex + 1;index < endIndex; index++) {
20216
21176
  const middle = paragraphsInOrder[index];
20217
21177
  if (middle)
20218
- total += countWords(paragraphText(middle));
21178
+ total += countWords(text(middle));
20219
21179
  }
20220
21180
  const last = paragraphsInOrder[endIndex];
20221
21181
  if (last)
20222
- total += countWordsInParagraphSpan(last, 0, endOffset);
21182
+ total += countWordsInParagraphSpan(last, 0, endOffset, options);
20223
21183
  return total;
20224
21184
  }
20225
21185
  var init_count = __esm(() => {
@@ -20229,41 +21189,51 @@ var init_count = __esm(() => {
20229
21189
  // src/cli/wc/index.ts
20230
21190
  var exports_wc = {};
20231
21191
  __export(exports_wc, {
20232
- run: () => run28
21192
+ run: () => run32
20233
21193
  });
20234
- import { parseArgs as parseArgs24 } from "util";
20235
- async function run28(args) {
21194
+ import { parseArgs as parseArgs26 } from "util";
21195
+ async function run32(args) {
20236
21196
  let parsed;
20237
21197
  try {
20238
- parsed = parseArgs24({
21198
+ parsed = parseArgs26({
20239
21199
  args,
20240
21200
  allowPositionals: true,
20241
21201
  options: {
21202
+ accepted: { type: "boolean" },
21203
+ baseline: { type: "boolean" },
20242
21204
  help: { type: "boolean", short: "h" }
20243
21205
  }
20244
21206
  });
20245
21207
  } catch (parseError) {
20246
21208
  const message = parseError instanceof Error ? parseError.message : String(parseError);
20247
- return fail("USAGE", message, HELP28);
21209
+ return fail("USAGE", message, HELP32);
20248
21210
  }
20249
21211
  if (parsed.values.help) {
20250
- await writeStdout(HELP28);
21212
+ await writeStdout(HELP32);
20251
21213
  return EXIT.OK;
20252
21214
  }
20253
21215
  const path = parsed.positionals[0];
20254
21216
  const locatorInput = parsed.positionals[1];
20255
21217
  if (!path)
20256
- return fail("USAGE", "Missing FILE argument", HELP28);
20257
- const view = await openOrFail(path);
20258
- if (typeof view === "number")
20259
- 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;
20260
21229
  if (!locatorInput) {
20261
21230
  await respond({
20262
21231
  ok: true,
20263
21232
  operation: "wc",
20264
21233
  path,
20265
21234
  scope: "document",
20266
- words: countWordsInBlocks(view.doc.blocks)
21235
+ view,
21236
+ words: countWordsInBlocks(docView.doc.blocks, { view })
20267
21237
  });
20268
21238
  return EXIT.OK;
20269
21239
  }
@@ -20276,22 +21246,23 @@ async function run28(args) {
20276
21246
  }
20277
21247
  throw error;
20278
21248
  }
20279
- if (locator.kind === "comment" || locator.kind === "image") {
21249
+ if (locator.kind === "comment" || locator.kind === "image" || locator.kind === "hyperlink" || locator.kind === "trackedChange") {
20280
21250
  return fail("USAGE", `Locator ${locatorInput} addresses a ${locator.kind}, not text`, "wc accepts paragraph, span, range, table, and cell locators.");
20281
21251
  }
20282
- const blocks = view.doc.blocks;
21252
+ const blocks = docView.doc.blocks;
20283
21253
  if (locator.kind === "block") {
20284
21254
  const block = findBlockById(blocks, locator.blockId);
20285
21255
  if (!block) {
20286
21256
  return fail("BLOCK_NOT_FOUND", `Block not found: ${locator.blockId}`);
20287
21257
  }
20288
- 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;
20289
21259
  await respond({
20290
21260
  ok: true,
20291
21261
  operation: "wc",
20292
21262
  path,
20293
21263
  locator: locatorInput,
20294
21264
  scope: block.type,
21265
+ view,
20295
21266
  words
20296
21267
  });
20297
21268
  return EXIT.OK;
@@ -20307,7 +21278,10 @@ async function run28(args) {
20307
21278
  path,
20308
21279
  locator: locatorInput,
20309
21280
  scope: "paragraphSpan",
20310
- words: countWordsInParagraphSpan(block, locator.start, locator.end)
21281
+ view,
21282
+ words: countWordsInParagraphSpan(block, locator.start, locator.end, {
21283
+ view
21284
+ })
20311
21285
  });
20312
21286
  return EXIT.OK;
20313
21287
  }
@@ -20327,7 +21301,8 @@ async function run28(args) {
20327
21301
  path,
20328
21302
  locator: locatorInput,
20329
21303
  scope: "range",
20330
- 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 })
20331
21306
  });
20332
21307
  return EXIT.OK;
20333
21308
  }
@@ -20344,7 +21319,8 @@ async function run28(args) {
20344
21319
  path,
20345
21320
  locator: locatorInput,
20346
21321
  scope: "cell",
20347
- words: countWordsInBlocks(cellBlocks)
21322
+ view,
21323
+ words: countWordsInBlocks(cellBlocks, { view })
20348
21324
  });
20349
21325
  return EXIT.OK;
20350
21326
  }
@@ -20357,13 +21333,14 @@ async function run28(args) {
20357
21333
  if (!block || block.type !== "paragraph") {
20358
21334
  return fail("BLOCK_NOT_FOUND", `Paragraph not found: ${composedId}`);
20359
21335
  }
20360
- 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));
20361
21337
  await respond({
20362
21338
  ok: true,
20363
21339
  operation: "wc",
20364
21340
  path,
20365
21341
  locator: locatorInput,
20366
21342
  scope: locator.inner.kind === "blockSpan" ? "paragraphSpan" : "paragraph",
21343
+ view,
20367
21344
  words
20368
21345
  });
20369
21346
  return EXIT.OK;
@@ -20382,7 +21359,14 @@ function findCellBlocks(blocks, locator) {
20382
21359
  return null;
20383
21360
  return cell.blocks;
20384
21361
  }
20385
- 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
20386
21370
 
20387
21371
  Usage:
20388
21372
  docx wc FILE [LOCATOR] [options]
@@ -20397,13 +21381,20 @@ Locators (optional; default: whole document):
20397
21381
  tN:rRcC:pK:S-E span within a cell paragraph
20398
21382
 
20399
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.
20400
21392
  -h, --help show this help
20401
21393
 
20402
21394
  Counting is whitespace-segmented (\\S+) over the joined paragraph text. Hidden
20403
- content like images/breaks/tabs contributes no words. Tracked-change deletions
20404
- are counted alongside insertions because their characters are still part of the
20405
- on-disk text; accept or reject changes first if you need a delta on the
20406
- "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.
20407
21398
 
20408
21399
  Examples:
20409
21400
  docx wc doc.docx
@@ -20420,7 +21411,7 @@ var init_wc = __esm(() => {
20420
21411
  // package.json
20421
21412
  var package_default = {
20422
21413
  name: "bun-docx",
20423
- version: "0.6.1",
21414
+ version: "0.7.0",
20424
21415
  description: "CLI for AI agents (Claude, Codex) to read, edit, and comment on .docx files with full format fidelity.",
20425
21416
  keywords: [
20426
21417
  "docx",
@@ -20500,9 +21491,10 @@ Commands:
20500
21491
  comments \u2026 Add, reply, resolve, delete, list comments
20501
21492
  images \u2026 Extract, replace, list images
20502
21493
  hyperlinks \u2026 Add, list, replace, delete hyperlinks
20503
- track-changes FILE on|off Toggle tracked-changes mode
21494
+ track-changes \u2026 Toggle (FILE on|off), list / accept / reject changes
20504
21495
  info \u2026 Reference material (schema, locators)
20505
21496
 
21497
+ It is highly recommended to agents to run "docx info locators" to understand their capabilities.
20506
21498
  Run "docx <command> --help" for command-specific help.
20507
21499
 
20508
21500
  Environment:
@@ -20510,9 +21502,19 @@ Environment:
20510
21502
  DOCX_CLI_NOW Override the timestamp used for tracked changes (test only)
20511
21503
 
20512
21504
  Tracked changes:
20513
- When <w:trackChanges/> is set in the doc (toggle via "docx track-changes
20514
- FILE on"), insert/edit/delete/replace automatically emit <w:ins>/<w:del>
20515
- 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.
20516
21518
  `;
20517
21519
  async function printTopHelp() {
20518
21520
  await writeStdout(TOP_HELP);