bun-docx 0.7.0 → 0.8.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 +95 -106
  2. package/dist/index.js +2815 -931
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13576,10 +13576,11 @@ function isSubtractiveTrackedChangeWrapper(tag) {
13576
13576
  function runTextLength(run) {
13577
13577
  let total = 0;
13578
13578
  for (const child of run.children) {
13579
- if (child.tag === "w:t")
13579
+ if (child.tag === "w:t" || child.tag === "w:delText") {
13580
13580
  total += child.collectText().length;
13581
- else if (SINGLE_CHAR_TAGS.has(child.tag))
13581
+ } else if (SINGLE_CHAR_TAGS.has(child.tag)) {
13582
13582
  total += 1;
13583
+ }
13583
13584
  }
13584
13585
  return total;
13585
13586
  }
@@ -13805,6 +13806,322 @@ var init_package = __esm(() => {
13805
13806
  };
13806
13807
  });
13807
13808
 
13809
+ // src/core/jsx/index.ts
13810
+ function normalizeChildren(children) {
13811
+ if (children === undefined || children === null)
13812
+ return [];
13813
+ if (Array.isArray(children))
13814
+ return children;
13815
+ return [children];
13816
+ }
13817
+ function namespace(prefix, tags) {
13818
+ const result = {};
13819
+ for (const tagName of tags) {
13820
+ result[tagName] = makeTag(`${prefix}:${tagName}`);
13821
+ }
13822
+ return result;
13823
+ }
13824
+ function makeTag(qualifiedName) {
13825
+ return (props) => {
13826
+ const attributes = {};
13827
+ let childrenProp;
13828
+ if (props) {
13829
+ for (const [key, value] of Object.entries(props)) {
13830
+ if (key === "children") {
13831
+ childrenProp = value;
13832
+ continue;
13833
+ }
13834
+ if (value === false || value == null)
13835
+ continue;
13836
+ attributes[mapAttributeName(key)] = String(value);
13837
+ }
13838
+ }
13839
+ const childNodes = [];
13840
+ flatten(normalizeChildren(childrenProp), childNodes);
13841
+ return new XmlNode2(qualifiedName, attributes, childNodes);
13842
+ };
13843
+ }
13844
+ function flatten(items, out) {
13845
+ for (const item of items) {
13846
+ if (item == null || item === false)
13847
+ continue;
13848
+ if (Array.isArray(item)) {
13849
+ flatten(item, out);
13850
+ continue;
13851
+ }
13852
+ if (item instanceof XmlNode2) {
13853
+ if (item.tag === FRAGMENT_TAG) {
13854
+ for (const child of item.children)
13855
+ out.push(child);
13856
+ continue;
13857
+ }
13858
+ out.push(item);
13859
+ continue;
13860
+ }
13861
+ if (typeof item === "string" || typeof item === "number") {
13862
+ out.push(XmlNode2.textNode(String(item)));
13863
+ }
13864
+ }
13865
+ }
13866
+ function mapAttributeName(name) {
13867
+ const dashIndex = name.indexOf("-");
13868
+ if (dashIndex === -1)
13869
+ return name;
13870
+ return `${name.slice(0, dashIndex)}:${name.slice(dashIndex + 1)}`;
13871
+ }
13872
+ var FRAGMENT_TAG = "#fragment", W_TAGS, R_TAGS, A_TAGS, WP_TAGS, PIC_TAGS, CP_TAGS, DC_TAGS, DCTERMS_TAGS, W14_TAGS, W15_TAGS, w, r, a, wp, pic, cp, dc, dcterms, w14, w15;
13873
+ var init_jsx = __esm(() => {
13874
+ init_parser();
13875
+ W_TAGS = [
13876
+ "document",
13877
+ "body",
13878
+ "p",
13879
+ "pPr",
13880
+ "pStyle",
13881
+ "jc",
13882
+ "numPr",
13883
+ "ilvl",
13884
+ "numId",
13885
+ "r",
13886
+ "rPr",
13887
+ "rStyle",
13888
+ "t",
13889
+ "b",
13890
+ "i",
13891
+ "u",
13892
+ "strike",
13893
+ "color",
13894
+ "highlight",
13895
+ "sz",
13896
+ "rFonts",
13897
+ "br",
13898
+ "tab",
13899
+ "drawing",
13900
+ "sectPr",
13901
+ "sectPrChange",
13902
+ "cols",
13903
+ "type",
13904
+ "tbl",
13905
+ "tblPr",
13906
+ "tblGrid",
13907
+ "gridCol",
13908
+ "tr",
13909
+ "tc",
13910
+ "tcPr",
13911
+ "ins",
13912
+ "del",
13913
+ "delText",
13914
+ "commentRangeStart",
13915
+ "commentRangeEnd",
13916
+ "commentReference",
13917
+ "comments",
13918
+ "comment",
13919
+ "settings",
13920
+ "trackChanges",
13921
+ "hyperlink"
13922
+ ];
13923
+ R_TAGS = ["embed", "link", "id"];
13924
+ A_TAGS = [
13925
+ "graphic",
13926
+ "graphicData",
13927
+ "blip",
13928
+ "xfrm",
13929
+ "off",
13930
+ "ext",
13931
+ "prstGeom",
13932
+ "avLst"
13933
+ ];
13934
+ WP_TAGS = [
13935
+ "inline",
13936
+ "anchor",
13937
+ "extent",
13938
+ "effectExtent",
13939
+ "docPr",
13940
+ "cNvGraphicFramePr"
13941
+ ];
13942
+ PIC_TAGS = [
13943
+ "pic",
13944
+ "nvPicPr",
13945
+ "cNvPr",
13946
+ "cNvPicPr",
13947
+ "blipFill",
13948
+ "spPr"
13949
+ ];
13950
+ CP_TAGS = ["coreProperties", "lastModifiedBy"];
13951
+ DC_TAGS = ["title", "creator", "subject", "description"];
13952
+ DCTERMS_TAGS = ["created", "modified"];
13953
+ W14_TAGS = ["paraId", "textId"];
13954
+ W15_TAGS = ["commentsEx", "commentEx"];
13955
+ w = namespace("w", W_TAGS);
13956
+ r = namespace("r", R_TAGS);
13957
+ a = namespace("a", A_TAGS);
13958
+ wp = namespace("wp", WP_TAGS);
13959
+ pic = namespace("pic", PIC_TAGS);
13960
+ cp = namespace("cp", CP_TAGS);
13961
+ dc = namespace("dc", DC_TAGS);
13962
+ dcterms = namespace("dcterms", DCTERMS_TAGS);
13963
+ w14 = namespace("w14", W14_TAGS);
13964
+ w15 = namespace("w15", W15_TAGS);
13965
+ });
13966
+
13967
+ // src/core/jsx/jsx-runtime.ts
13968
+ function jsx(type, props, _key) {
13969
+ const result = type(props);
13970
+ return result ?? new XmlNode2("#fragment");
13971
+ }
13972
+ var jsxDEV;
13973
+ var init_jsx_runtime = __esm(() => {
13974
+ init_parser();
13975
+ init_jsx();
13976
+ jsxDEV = jsx;
13977
+ });
13978
+
13979
+ // src/core/jsx/jsx-dev-runtime.ts
13980
+ var init_jsx_dev_runtime = __esm(() => {
13981
+ init_jsx_runtime();
13982
+ });
13983
+
13984
+ // src/core/sections.tsx
13985
+ function readSectionProperties(children) {
13986
+ const props = {};
13987
+ for (const child of children) {
13988
+ if (child.tag === "w:cols") {
13989
+ const num = child.getAttribute("w:num");
13990
+ if (num) {
13991
+ const parsed = Number.parseInt(num, 10);
13992
+ if (Number.isFinite(parsed) && parsed > 0)
13993
+ props.columns = parsed;
13994
+ }
13995
+ continue;
13996
+ }
13997
+ if (child.tag === "w:type") {
13998
+ const value = child.getAttribute("w:val");
13999
+ if (value && isSectionType(value))
14000
+ props.sectionType = value;
14001
+ }
14002
+ }
14003
+ return props;
14004
+ }
14005
+ function isSectionType(value) {
14006
+ return SECTION_TYPE_ORDER.includes(value);
14007
+ }
14008
+ function SentinelSectionParagraph({
14009
+ columns,
14010
+ sectionType
14011
+ }) {
14012
+ return /* @__PURE__ */ jsxDEV(w.p, {
14013
+ children: /* @__PURE__ */ jsxDEV(w.pPr, {
14014
+ children: /* @__PURE__ */ jsxDEV(w.sectPr, {
14015
+ children: [
14016
+ sectionType && /* @__PURE__ */ jsxDEV(w.type, {
14017
+ "w-val": sectionType
14018
+ }, undefined, false, undefined, this),
14019
+ columns !== undefined && /* @__PURE__ */ jsxDEV(w.cols, {
14020
+ "w-num": String(columns)
14021
+ }, undefined, false, undefined, this)
14022
+ ]
14023
+ }, undefined, true, undefined, this)
14024
+ }, undefined, false, undefined, this)
14025
+ }, undefined, false, undefined, this);
14026
+ }
14027
+ function applyColumns(sectPr, columns) {
14028
+ if (columns === undefined)
14029
+ return;
14030
+ const existing = sectPr.findChild("w:cols");
14031
+ if (columns === null) {
14032
+ if (existing) {
14033
+ const index = sectPr.children.indexOf(existing);
14034
+ if (index !== -1)
14035
+ sectPr.children.splice(index, 1);
14036
+ }
14037
+ return;
14038
+ }
14039
+ if (existing) {
14040
+ existing.setAttribute("w:num", String(columns));
14041
+ return;
14042
+ }
14043
+ insertBeforeSectPrChange(sectPr, /* @__PURE__ */ jsxDEV(w.cols, {
14044
+ "w-num": String(columns)
14045
+ }, undefined, false, undefined, this));
14046
+ }
14047
+ function applySectionType(sectPr, sectionType) {
14048
+ if (sectionType === undefined)
14049
+ return;
14050
+ const existing = sectPr.findChild("w:type");
14051
+ if (sectionType === null) {
14052
+ if (existing) {
14053
+ const index = sectPr.children.indexOf(existing);
14054
+ if (index !== -1)
14055
+ sectPr.children.splice(index, 1);
14056
+ }
14057
+ return;
14058
+ }
14059
+ if (existing) {
14060
+ existing.setAttribute("w:val", sectionType);
14061
+ return;
14062
+ }
14063
+ const node = /* @__PURE__ */ jsxDEV(w.type, {
14064
+ "w-val": sectionType
14065
+ }, undefined, false, undefined, this);
14066
+ const colsIndex = sectPr.children.findIndex((child) => child.tag === "w:cols");
14067
+ if (colsIndex !== -1) {
14068
+ sectPr.children.splice(colsIndex, 0, node);
14069
+ return;
14070
+ }
14071
+ insertBeforeSectPrChange(sectPr, node);
14072
+ }
14073
+ function insertBeforeSectPrChange(sectPr, node) {
14074
+ const changeIndex = sectPr.children.findIndex((child) => child.tag === "w:sectPrChange");
14075
+ if (changeIndex === -1)
14076
+ sectPr.children.push(node);
14077
+ else
14078
+ sectPr.children.splice(changeIndex, 0, node);
14079
+ }
14080
+ function removeInlineSectPr(sectPr, parentChildren) {
14081
+ const index = parentChildren.indexOf(sectPr);
14082
+ if (index === -1)
14083
+ return false;
14084
+ parentChildren.splice(index, 1);
14085
+ return true;
14086
+ }
14087
+ function isTrailingSectPr(bodyChildren, parentChildren) {
14088
+ return parentChildren === bodyChildren;
14089
+ }
14090
+ function wrapSectPrChange(sectPr, meta) {
14091
+ const existingChangeIndex = sectPr.children.findIndex((child) => child.tag === "w:sectPrChange");
14092
+ if (existingChangeIndex !== -1) {
14093
+ sectPr.children.splice(existingChangeIndex, 1);
14094
+ }
14095
+ const snapshotChildren = [];
14096
+ for (const child of sectPr.children) {
14097
+ if (child.tag === "w:sectPrChange")
14098
+ continue;
14099
+ snapshotChildren.push(child.clone());
14100
+ }
14101
+ const change = /* @__PURE__ */ jsxDEV(w.sectPrChange, {
14102
+ "w-id": meta.revisionId,
14103
+ "w-author": meta.author,
14104
+ "w-date": meta.date,
14105
+ children: /* @__PURE__ */ jsxDEV(w.sectPr, {
14106
+ children: snapshotChildren
14107
+ }, undefined, false, undefined, this)
14108
+ }, undefined, false, undefined, this);
14109
+ sectPr.children.push(change);
14110
+ return change;
14111
+ }
14112
+ var SECTION_TYPE_ORDER;
14113
+ var init_sections = __esm(() => {
14114
+ init_jsx();
14115
+ init_jsx_dev_runtime();
14116
+ SECTION_TYPE_ORDER = [
14117
+ "continuous",
14118
+ "nextPage",
14119
+ "evenPage",
14120
+ "oddPage",
14121
+ "nextColumn"
14122
+ ];
14123
+ });
14124
+
13808
14125
  // src/core/ast/sym.ts
13809
14126
  function decodeSym(font, charHex) {
13810
14127
  const codepoint = Number.parseInt(charHex, 16);
@@ -14153,6 +14470,14 @@ function readBlocks(view, body, state) {
14153
14470
  const id = `p${paragraphIndex++}`;
14154
14471
  blocks.push(readParagraph(view, child, id, state));
14155
14472
  view.blockReferences.set(id, { node: child, parent: body.children });
14473
+ const inlineSectPr = findInlineSectPr(child);
14474
+ if (inlineSectPr) {
14475
+ const sectionId = `s${sectionIndex++}`;
14476
+ blocks.push(buildSectionBreak(sectionId, inlineSectPr.node));
14477
+ view.blockReferences.set(sectionId, inlineSectPr);
14478
+ registerSectPrChange(view, inlineSectPr.node, state, sectionId);
14479
+ }
14480
+ registerParagraphMarkChanges(view, child, state, id);
14156
14481
  continue;
14157
14482
  }
14158
14483
  if (child.tag === "w:tbl") {
@@ -14163,12 +14488,60 @@ function readBlocks(view, body, state) {
14163
14488
  }
14164
14489
  if (child.tag === "w:sectPr") {
14165
14490
  const id = `s${sectionIndex++}`;
14166
- blocks.push({ id, type: "sectionBreak" });
14491
+ blocks.push(buildSectionBreak(id, child));
14167
14492
  view.blockReferences.set(id, { node: child, parent: body.children });
14493
+ registerSectPrChange(view, child, state, id);
14168
14494
  }
14169
14495
  }
14170
14496
  return blocks;
14171
14497
  }
14498
+ function registerParagraphMarkChanges(view, paragraph, state, blockId) {
14499
+ const pPr = paragraph.findChild("w:pPr");
14500
+ if (!pPr)
14501
+ return;
14502
+ const rPr = pPr.findChild("w:rPr");
14503
+ if (!rPr)
14504
+ return;
14505
+ for (const child of rPr.children) {
14506
+ if (child.tag !== "w:ins" && child.tag !== "w:del")
14507
+ continue;
14508
+ const id = `tc${state.trackedChangeIndex++}`;
14509
+ view.trackedChangeReferences.set(id, {
14510
+ node: child,
14511
+ parent: rPr.children,
14512
+ blockId
14513
+ });
14514
+ }
14515
+ }
14516
+ function registerSectPrChange(view, sectPr, state, blockId) {
14517
+ const change = sectPr.findChild("w:sectPrChange");
14518
+ if (!change)
14519
+ return;
14520
+ const id = `tc${state.trackedChangeIndex++}`;
14521
+ view.trackedChangeReferences.set(id, {
14522
+ node: change,
14523
+ parent: sectPr.children,
14524
+ blockId
14525
+ });
14526
+ }
14527
+ function findInlineSectPr(paragraph) {
14528
+ const pPr = paragraph.findChild("w:pPr");
14529
+ if (!pPr)
14530
+ return;
14531
+ const sectPr = pPr.findChild("w:sectPr");
14532
+ if (!sectPr)
14533
+ return;
14534
+ return { node: sectPr, parent: pPr.children };
14535
+ }
14536
+ function buildSectionBreak(id, sectPr) {
14537
+ const props = readSectionProperties(sectPr.children);
14538
+ const block = { id, type: "sectionBreak" };
14539
+ if (props.columns !== undefined)
14540
+ block.columns = props.columns;
14541
+ if (props.sectionType !== undefined)
14542
+ block.sectionType = props.sectionType;
14543
+ return block;
14544
+ }
14172
14545
  function readParagraph(view, node, id, state) {
14173
14546
  const paragraph = { id, type: "paragraph", runs: [] };
14174
14547
  const paragraphProperties = node.findChild("w:pPr");
@@ -14707,6 +15080,7 @@ function readNotes(tree, rootTag, itemTag, idPrefix) {
14707
15080
  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;
14708
15081
  var init_read = __esm(() => {
14709
15082
  init_parser();
15083
+ init_sections();
14710
15084
  init_sym();
14711
15085
  TRACKED_CHANGE_KIND_BY_TAG = {
14712
15086
  "w:ins": "ins",
@@ -15124,178 +15498,6 @@ var init_track_changes = __esm(() => {
15124
15498
  init_parser();
15125
15499
  });
15126
15500
 
15127
- // src/core/jsx/index.ts
15128
- function normalizeChildren(children) {
15129
- if (children === undefined || children === null)
15130
- return [];
15131
- if (Array.isArray(children))
15132
- return children;
15133
- return [children];
15134
- }
15135
- function namespace(prefix, tags) {
15136
- const result = {};
15137
- for (const tagName of tags) {
15138
- result[tagName] = makeTag(`${prefix}:${tagName}`);
15139
- }
15140
- return result;
15141
- }
15142
- function makeTag(qualifiedName) {
15143
- return (props) => {
15144
- const attributes = {};
15145
- let childrenProp;
15146
- if (props) {
15147
- for (const [key, value] of Object.entries(props)) {
15148
- if (key === "children") {
15149
- childrenProp = value;
15150
- continue;
15151
- }
15152
- if (value === false || value == null)
15153
- continue;
15154
- attributes[mapAttributeName(key)] = String(value);
15155
- }
15156
- }
15157
- const childNodes = [];
15158
- flatten(normalizeChildren(childrenProp), childNodes);
15159
- return new XmlNode2(qualifiedName, attributes, childNodes);
15160
- };
15161
- }
15162
- function flatten(items, out) {
15163
- for (const item of items) {
15164
- if (item == null || item === false)
15165
- continue;
15166
- if (Array.isArray(item)) {
15167
- flatten(item, out);
15168
- continue;
15169
- }
15170
- if (item instanceof XmlNode2) {
15171
- if (item.tag === FRAGMENT_TAG) {
15172
- for (const child of item.children)
15173
- out.push(child);
15174
- continue;
15175
- }
15176
- out.push(item);
15177
- continue;
15178
- }
15179
- if (typeof item === "string" || typeof item === "number") {
15180
- out.push(XmlNode2.textNode(String(item)));
15181
- }
15182
- }
15183
- }
15184
- function mapAttributeName(name) {
15185
- const dashIndex = name.indexOf("-");
15186
- if (dashIndex === -1)
15187
- return name;
15188
- return `${name.slice(0, dashIndex)}:${name.slice(dashIndex + 1)}`;
15189
- }
15190
- var FRAGMENT_TAG = "#fragment", W_TAGS, R_TAGS, A_TAGS, WP_TAGS, PIC_TAGS, CP_TAGS, DC_TAGS, DCTERMS_TAGS, W14_TAGS, W15_TAGS, w, r, a, wp, pic, cp, dc, dcterms, w14, w15;
15191
- var init_jsx = __esm(() => {
15192
- init_parser();
15193
- W_TAGS = [
15194
- "document",
15195
- "body",
15196
- "p",
15197
- "pPr",
15198
- "pStyle",
15199
- "jc",
15200
- "numPr",
15201
- "ilvl",
15202
- "numId",
15203
- "r",
15204
- "rPr",
15205
- "rStyle",
15206
- "t",
15207
- "b",
15208
- "i",
15209
- "u",
15210
- "strike",
15211
- "color",
15212
- "highlight",
15213
- "sz",
15214
- "rFonts",
15215
- "br",
15216
- "tab",
15217
- "drawing",
15218
- "sectPr",
15219
- "tbl",
15220
- "tblPr",
15221
- "tblGrid",
15222
- "gridCol",
15223
- "tr",
15224
- "tc",
15225
- "tcPr",
15226
- "ins",
15227
- "del",
15228
- "delText",
15229
- "commentRangeStart",
15230
- "commentRangeEnd",
15231
- "commentReference",
15232
- "comments",
15233
- "comment",
15234
- "settings",
15235
- "trackChanges",
15236
- "hyperlink"
15237
- ];
15238
- R_TAGS = ["embed", "link", "id"];
15239
- A_TAGS = [
15240
- "graphic",
15241
- "graphicData",
15242
- "blip",
15243
- "xfrm",
15244
- "off",
15245
- "ext",
15246
- "prstGeom",
15247
- "avLst"
15248
- ];
15249
- WP_TAGS = [
15250
- "inline",
15251
- "anchor",
15252
- "extent",
15253
- "effectExtent",
15254
- "docPr",
15255
- "cNvGraphicFramePr"
15256
- ];
15257
- PIC_TAGS = [
15258
- "pic",
15259
- "nvPicPr",
15260
- "cNvPr",
15261
- "cNvPicPr",
15262
- "blipFill",
15263
- "spPr"
15264
- ];
15265
- CP_TAGS = ["coreProperties", "lastModifiedBy"];
15266
- DC_TAGS = ["title", "creator", "subject", "description"];
15267
- DCTERMS_TAGS = ["created", "modified"];
15268
- W14_TAGS = ["paraId", "textId"];
15269
- W15_TAGS = ["commentsEx", "commentEx"];
15270
- w = namespace("w", W_TAGS);
15271
- r = namespace("r", R_TAGS);
15272
- a = namespace("a", A_TAGS);
15273
- wp = namespace("wp", WP_TAGS);
15274
- pic = namespace("pic", PIC_TAGS);
15275
- cp = namespace("cp", CP_TAGS);
15276
- dc = namespace("dc", DC_TAGS);
15277
- dcterms = namespace("dcterms", DCTERMS_TAGS);
15278
- w14 = namespace("w14", W14_TAGS);
15279
- w15 = namespace("w15", W15_TAGS);
15280
- });
15281
-
15282
- // src/core/jsx/jsx-runtime.ts
15283
- function jsx(type, props, _key) {
15284
- const result = type(props);
15285
- return result ?? new XmlNode2("#fragment");
15286
- }
15287
- var jsxDEV;
15288
- var init_jsx_runtime = __esm(() => {
15289
- init_parser();
15290
- init_jsx();
15291
- jsxDEV = jsx;
15292
- });
15293
-
15294
- // src/core/jsx/jsx-dev-runtime.ts
15295
- var init_jsx_dev_runtime = __esm(() => {
15296
- init_jsx_runtime();
15297
- });
15298
-
15299
15501
  // src/core/track-changes/emit.tsx
15300
15502
  function Ins({
15301
15503
  meta,
@@ -15349,6 +15551,7 @@ var init_core = __esm(() => {
15349
15551
  init_package();
15350
15552
  init_parser();
15351
15553
  init_relationships();
15554
+ init_sections();
15352
15555
  init_track_changes();
15353
15556
  init_emit();
15354
15557
  });
@@ -15358,6 +15561,14 @@ async function respond(payload) {
15358
15561
  await Bun.write(Bun.stdout, `${JSON.stringify(payload)}
15359
15562
  `);
15360
15563
  }
15564
+ function setVerboseAck(verbose) {
15565
+ verboseAck = verbose;
15566
+ }
15567
+ async function respondAck(payload) {
15568
+ if (!verboseAck)
15569
+ return;
15570
+ await respond(payload);
15571
+ }
15361
15572
  async function writeStdout(text) {
15362
15573
  await Bun.write(Bun.stdout, text);
15363
15574
  }
@@ -15416,7 +15627,7 @@ async function resolveBlockOrFail(view, locator) {
15416
15627
  throw err;
15417
15628
  }
15418
15629
  }
15419
- var EXIT;
15630
+ var EXIT, verboseAck = false;
15420
15631
  var init_respond = __esm(() => {
15421
15632
  init_core();
15422
15633
  EXIT = {
@@ -15427,86 +15638,291 @@ var init_respond = __esm(() => {
15427
15638
  };
15428
15639
  });
15429
15640
 
15430
- // src/cli/comments/helpers.tsx
15431
- function nextCommentId(view) {
15432
- if (!view.commentsTree)
15433
- return "0";
15434
- const root = XmlNode2.findRoot(view.commentsTree, "w:comments");
15435
- if (!root)
15436
- return "0";
15437
- let highest = -1;
15438
- for (const child of root.children) {
15439
- if (child.tag !== "w:comment")
15440
- continue;
15441
- const idAttribute = child.getAttribute("w:id");
15442
- if (idAttribute == null)
15443
- continue;
15444
- const numeric = Number(idAttribute);
15445
- if (Number.isFinite(numeric) && numeric > highest)
15446
- highest = numeric;
15447
- }
15448
- return String(highest + 1);
15449
- }
15450
- function generateParaId() {
15451
- const bytes = new Uint8Array(4);
15452
- crypto.getRandomValues(bytes);
15453
- let hex = "";
15454
- for (const byte of bytes)
15455
- hex += byte.toString(16).padStart(2, "0");
15456
- return hex.toUpperCase();
15457
- }
15458
- function authorInitials(author) {
15459
- const parts = author.trim().split(/\s+/).filter(Boolean);
15460
- if (parts.length === 0)
15461
- return "?";
15462
- const first = parts[0]?.charAt(0) ?? "";
15463
- const last = parts.length > 1 ? parts[parts.length - 1]?.charAt(0) ?? "" : "";
15464
- return (first + last).toUpperCase() || "?";
15465
- }
15466
- function ensureCommentsPart(view) {
15467
- if (view.commentsTree) {
15468
- const existing = XmlNode2.findRoot(view.commentsTree, "w:comments");
15469
- if (existing)
15470
- return existing;
15641
+ // src/core/find/index.ts
15642
+ function findTextSpans(doc, query, options = {}) {
15643
+ const view = options.view ?? "accepted";
15644
+ const exact = options.exact ?? false;
15645
+ const useRegex = options.regex ?? false;
15646
+ let effectiveQuery = query;
15647
+ const applied = [];
15648
+ if (!useRegex && !exact) {
15649
+ const norm = normalizeQuery(query);
15650
+ effectiveQuery = norm.normalized;
15651
+ applied.push(...norm.applied);
15652
+ }
15653
+ const matcher = useRegex ? regexMatcher(query, options.ignoreCase ?? false) : literalMatcher(effectiveQuery, options.ignoreCase ?? false, !exact);
15654
+ const out = [];
15655
+ collectMatches(doc.blocks, matcher, view, out);
15656
+ const result = { matches: out };
15657
+ if (applied.length > 0) {
15658
+ result.normalizedQuery = effectiveQuery;
15659
+ result.normalizationApplied = applied;
15471
15660
  }
15472
- const root = /* @__PURE__ */ jsxDEV(w.comments, {
15473
- "xmlns:w": NS_W,
15474
- "xmlns:w14": NS_W14
15475
- }, undefined, false, undefined, this);
15476
- view.commentsTree = [root];
15477
- registerPart(view.relationshipsTree, view.contentTypesTree, {
15478
- partName: "word/comments.xml",
15479
- contentType: COMMENTS_CONTENT_TYPE,
15480
- relationshipType: COMMENTS_REL_TYPE,
15481
- target: "comments.xml"
15482
- });
15483
- return root;
15661
+ return result;
15484
15662
  }
15485
- function ensureCommentsExtPart(view) {
15486
- if (view.commentsExtTree) {
15487
- const existing = XmlNode2.findRoot(view.commentsExtTree, "w15:commentsEx");
15488
- if (existing)
15489
- return existing;
15663
+ function literalMatcher(query, ignoreCase, normalize) {
15664
+ if (query.length === 0) {
15665
+ throw new Error("query cannot be empty");
15490
15666
  }
15491
- const root = /* @__PURE__ */ jsxDEV(w15.commentsEx, {
15492
- "xmlns:w15": NS_W15,
15493
- "xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006",
15494
- "mc:Ignorable": "w15"
15495
- }, undefined, false, undefined, this);
15496
- view.commentsExtTree = [root];
15497
- registerPart(view.relationshipsTree, view.contentTypesTree, {
15498
- partName: "word/commentsExtended.xml",
15499
- contentType: COMMENTS_EXT_CONTENT_TYPE,
15500
- relationshipType: COMMENTS_EXT_REL_TYPE,
15501
- target: "commentsExtended.xml"
15502
- });
15503
- return root;
15667
+ const needle = ignoreCase ? query.toLowerCase() : query;
15668
+ return (paragraphText2) => {
15669
+ const canonical = normalize ? normalizeHaystack(paragraphText2) : paragraphText2;
15670
+ const haystack = ignoreCase ? canonical.toLowerCase() : canonical;
15671
+ const matches = [];
15672
+ let cursor = haystack.indexOf(needle);
15673
+ while (cursor !== -1) {
15674
+ matches.push({
15675
+ start: cursor,
15676
+ end: cursor + needle.length,
15677
+ text: paragraphText2.slice(cursor, cursor + needle.length)
15678
+ });
15679
+ cursor = haystack.indexOf(needle, cursor + needle.length);
15680
+ }
15681
+ return matches;
15682
+ };
15504
15683
  }
15505
- function paragraphTextLength(paragraph) {
15506
- return sumRunBearingTextLength(paragraph.children);
15684
+ function regexMatcher(pattern, ignoreCase) {
15685
+ const flags = `g${ignoreCase ? "i" : ""}`;
15686
+ const regex = new RegExp(pattern, flags);
15687
+ return (paragraphText2) => {
15688
+ const matches = [];
15689
+ regex.lastIndex = 0;
15690
+ let result = regex.exec(paragraphText2);
15691
+ while (result !== null) {
15692
+ const matched = result[0];
15693
+ if (matched.length === 0) {
15694
+ regex.lastIndex += 1;
15695
+ result = regex.exec(paragraphText2);
15696
+ continue;
15697
+ }
15698
+ matches.push({
15699
+ start: result.index,
15700
+ end: result.index + matched.length,
15701
+ text: matched
15702
+ });
15703
+ result = regex.exec(paragraphText2);
15704
+ }
15705
+ return matches;
15706
+ };
15707
+ }
15708
+ function collectMatches(blocks, matcher, view, out) {
15709
+ for (const block of blocks) {
15710
+ if (block.type === "paragraph") {
15711
+ const paragraphText2 = paragraphTextForView(block, view);
15712
+ for (const span of matcher(paragraphText2)) {
15713
+ const match = {
15714
+ blockId: block.id,
15715
+ start: span.start,
15716
+ end: span.end,
15717
+ text: span.text
15718
+ };
15719
+ const overlaps = trackedChangesOverlapping(block, span.start, span.end, view);
15720
+ if (overlaps.length > 0)
15721
+ match.trackedChanges = overlaps;
15722
+ out.push(match);
15723
+ }
15724
+ continue;
15725
+ }
15726
+ if (block.type === "table") {
15727
+ for (const row of block.rows) {
15728
+ for (const cell of row.cells) {
15729
+ collectMatches(cell.blocks, matcher, view, out);
15730
+ }
15731
+ }
15732
+ }
15733
+ }
15734
+ }
15735
+ function paragraphTextForView(paragraph, view) {
15736
+ let out = "";
15737
+ for (const run of paragraph.runs) {
15738
+ if (run.type !== "text")
15739
+ continue;
15740
+ if (!isRunVisibleInView(run.trackedChange?.kind, view))
15741
+ continue;
15742
+ out += run.text;
15743
+ }
15744
+ return out;
15745
+ }
15746
+ function isRunVisibleInView(kind, view) {
15747
+ if (view === "current")
15748
+ return true;
15749
+ if (view === "accepted")
15750
+ return kind !== "del" && kind !== "moveFrom";
15751
+ return kind !== "ins" && kind !== "moveTo";
15752
+ }
15753
+ function normalizeQuery(query) {
15754
+ const applied = [];
15755
+ let result = query;
15756
+ const stripped = stripBalancedMarkdownEmphasis(result);
15757
+ if (stripped !== result) {
15758
+ applied.push("strip-md-emphasis");
15759
+ result = stripped;
15760
+ }
15761
+ const quoteNormalized = normalizeQuotes(result);
15762
+ if (quoteNormalized !== result) {
15763
+ applied.push("smart-quotes");
15764
+ result = quoteNormalized;
15765
+ }
15766
+ const dashNormalized = normalizeDashes(result);
15767
+ if (dashNormalized !== result) {
15768
+ applied.push("dashes");
15769
+ result = dashNormalized;
15770
+ }
15771
+ return { normalized: result, applied };
15772
+ }
15773
+ function normalizeHaystack(text) {
15774
+ return normalizeDashes(normalizeQuotes(text));
15775
+ }
15776
+ function normalizeQuotes(text) {
15777
+ return text.replace(/[\u2018\u2019]/g, "'").replace(/[\u201C\u201D]/g, '"');
15778
+ }
15779
+ function normalizeDashes(text) {
15780
+ return text.replace(/[\u2013\u2014]/g, "-");
15781
+ }
15782
+ function stripBalancedMarkdownEmphasis(text) {
15783
+ const patterns = [
15784
+ /\*\*(\S(?:.*?\S)?)\*\*/g,
15785
+ /__(\S(?:.*?\S)?)__/g,
15786
+ /`(\S(?:.*?\S)?)`/g,
15787
+ /(?<![A-Za-z0-9_])\*(\S(?:.*?\S)?)\*(?![A-Za-z0-9_])/g,
15788
+ /(?<![A-Za-z0-9_])_(\S(?:.*?\S)?)_(?![A-Za-z0-9_])/g
15789
+ ];
15790
+ let result = text;
15791
+ for (const pattern of patterns) {
15792
+ result = result.replace(pattern, "$1");
15793
+ }
15794
+ return result;
15795
+ }
15796
+ function trackedChangesOverlapping(paragraph, start, end, view) {
15797
+ const seen = new Set;
15798
+ const out = [];
15799
+ let offset = 0;
15800
+ for (const run of paragraph.runs) {
15801
+ if (run.type !== "text")
15802
+ continue;
15803
+ if (!isRunVisibleInView(run.trackedChange?.kind, view))
15804
+ continue;
15805
+ const length = run.text.length;
15806
+ const runStart = offset;
15807
+ const runEnd = offset + length;
15808
+ offset = runEnd;
15809
+ if (runEnd <= start || runStart >= end)
15810
+ continue;
15811
+ const change = run.trackedChange;
15812
+ if (!change)
15813
+ continue;
15814
+ if (seen.has(change.id))
15815
+ continue;
15816
+ seen.add(change.id);
15817
+ out.push(change);
15818
+ }
15819
+ return out;
15820
+ }
15821
+
15822
+ // src/cli/comments/helpers.tsx
15823
+ function isWrapperVisibleInView(tag, view) {
15824
+ if (!isRunBearingWrapper(tag))
15825
+ return false;
15826
+ if (view === "current")
15827
+ return true;
15828
+ if (view === "accepted")
15829
+ return tag !== "w:del" && tag !== "w:moveFrom";
15830
+ return tag !== "w:ins" && tag !== "w:moveTo";
15831
+ }
15832
+ function sumVisibleTextLength(children, view) {
15833
+ let total = 0;
15834
+ for (const child of children) {
15835
+ if (child.tag === "w:r") {
15836
+ total += runTextLength(child);
15837
+ continue;
15838
+ }
15839
+ if (isWrapperVisibleInView(child.tag, view)) {
15840
+ total += sumVisibleTextLength(child.children, view);
15841
+ }
15842
+ }
15843
+ return total;
15844
+ }
15845
+ function nextCommentId(view) {
15846
+ if (!view.commentsTree)
15847
+ return "0";
15848
+ const root = XmlNode2.findRoot(view.commentsTree, "w:comments");
15849
+ if (!root)
15850
+ return "0";
15851
+ let highest = -1;
15852
+ for (const child of root.children) {
15853
+ if (child.tag !== "w:comment")
15854
+ continue;
15855
+ const idAttribute = child.getAttribute("w:id");
15856
+ if (idAttribute == null)
15857
+ continue;
15858
+ const numeric = Number(idAttribute);
15859
+ if (Number.isFinite(numeric) && numeric > highest)
15860
+ highest = numeric;
15861
+ }
15862
+ return String(highest + 1);
15863
+ }
15864
+ function generateParaId() {
15865
+ const bytes = new Uint8Array(4);
15866
+ crypto.getRandomValues(bytes);
15867
+ let hex = "";
15868
+ for (const byte of bytes)
15869
+ hex += byte.toString(16).padStart(2, "0");
15870
+ return hex.toUpperCase();
15871
+ }
15872
+ function authorInitials(author) {
15873
+ const parts = author.trim().split(/\s+/).filter(Boolean);
15874
+ if (parts.length === 0)
15875
+ return "?";
15876
+ const first = parts[0]?.charAt(0) ?? "";
15877
+ const last = parts.length > 1 ? parts[parts.length - 1]?.charAt(0) ?? "" : "";
15878
+ return (first + last).toUpperCase() || "?";
15879
+ }
15880
+ function ensureCommentsPart(view) {
15881
+ if (view.commentsTree) {
15882
+ const existing = XmlNode2.findRoot(view.commentsTree, "w:comments");
15883
+ if (existing)
15884
+ return existing;
15885
+ }
15886
+ const root = /* @__PURE__ */ jsxDEV(w.comments, {
15887
+ "xmlns:w": NS_W,
15888
+ "xmlns:w14": NS_W14
15889
+ }, undefined, false, undefined, this);
15890
+ view.commentsTree = [root];
15891
+ registerPart(view.relationshipsTree, view.contentTypesTree, {
15892
+ partName: "word/comments.xml",
15893
+ contentType: COMMENTS_CONTENT_TYPE,
15894
+ relationshipType: COMMENTS_REL_TYPE,
15895
+ target: "comments.xml"
15896
+ });
15897
+ return root;
15898
+ }
15899
+ function ensureCommentsExtPart(view) {
15900
+ if (view.commentsExtTree) {
15901
+ const existing = XmlNode2.findRoot(view.commentsExtTree, "w15:commentsEx");
15902
+ if (existing)
15903
+ return existing;
15904
+ }
15905
+ const root = /* @__PURE__ */ jsxDEV(w15.commentsEx, {
15906
+ "xmlns:w15": NS_W15,
15907
+ "xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006",
15908
+ "mc:Ignorable": "w15"
15909
+ }, undefined, false, undefined, this);
15910
+ view.commentsExtTree = [root];
15911
+ registerPart(view.relationshipsTree, view.contentTypesTree, {
15912
+ partName: "word/commentsExtended.xml",
15913
+ contentType: COMMENTS_EXT_CONTENT_TYPE,
15914
+ relationshipType: COMMENTS_EXT_REL_TYPE,
15915
+ target: "commentsExtended.xml"
15916
+ });
15917
+ return root;
15507
15918
  }
15508
- function addCommentMarkersToParagraph(paragraph, commentId, span) {
15509
- const total = paragraphTextLength(paragraph);
15919
+ function paragraphTextLength(paragraph, view = "current") {
15920
+ if (view === "current")
15921
+ return sumRunBearingTextLength(paragraph.children);
15922
+ return sumVisibleTextLength(paragraph.children, view);
15923
+ }
15924
+ function addCommentMarkersToParagraph(paragraph, commentId, span, view = "current") {
15925
+ const total = paragraphTextLength(paragraph, view);
15510
15926
  const range = span ?? { start: 0, end: total };
15511
15927
  if (range.start < 0 || range.end > total || range.start > range.end) {
15512
15928
  throw new SpanOutOfRangeError(`Span ${range.start}-${range.end} out of paragraph length ${total}`);
@@ -15518,26 +15934,24 @@ function addCommentMarkersToParagraph(paragraph, commentId, span) {
15518
15934
  node: commentRangeEndMarker(commentId),
15519
15935
  follower: commentReferenceRun(commentId)
15520
15936
  }
15521
- ]);
15937
+ ], view);
15522
15938
  }
15523
- function addCommentRangeMarkers(startParagraph, startOffset, endParagraph, endOffset, commentId) {
15939
+ function addCommentRangeMarkers(startParagraph, startOffset, endParagraph, endOffset, commentId, view = "current") {
15524
15940
  if (startParagraph === endParagraph) {
15525
15941
  addCommentMarkersToParagraph(startParagraph, commentId, {
15526
15942
  start: startOffset,
15527
15943
  end: endOffset
15528
- });
15944
+ }, view);
15529
15945
  return;
15530
15946
  }
15531
- placeMarkersInParagraph(startParagraph, [
15532
- { offset: startOffset, node: commentRangeStartMarker(commentId) }
15533
- ]);
15947
+ placeMarkersInParagraph(startParagraph, [{ offset: startOffset, node: commentRangeStartMarker(commentId) }], view);
15534
15948
  placeMarkersInParagraph(endParagraph, [
15535
15949
  {
15536
15950
  offset: endOffset,
15537
15951
  node: commentRangeEndMarker(commentId),
15538
15952
  follower: commentReferenceRun(commentId)
15539
15953
  }
15540
- ]);
15954
+ ], view);
15541
15955
  }
15542
15956
  function addCommentMarkersAroundRun(paragraph, target, commentId) {
15543
15957
  function walk(parent) {
@@ -15654,10 +16068,10 @@ function commentReferenceRun(commentId) {
15654
16068
  ]
15655
16069
  }, undefined, true, undefined, this);
15656
16070
  }
15657
- function placeMarkersInParagraph(paragraph, markers) {
16071
+ function placeMarkersInParagraph(paragraph, markers, view = "current") {
15658
16072
  if (markers.length === 0)
15659
16073
  return;
15660
- const total = paragraphTextLength(paragraph);
16074
+ const total = paragraphTextLength(paragraph, view);
15661
16075
  for (const marker of markers) {
15662
16076
  if (marker.offset < 0 || marker.offset > total) {
15663
16077
  throw new SpanOutOfRangeError(`Marker offset ${marker.offset} out of paragraph length ${total}`);
@@ -15665,13 +16079,13 @@ function placeMarkersInParagraph(paragraph, markers) {
15665
16079
  }
15666
16080
  const pending = markers.slice();
15667
16081
  const state = { offset: 0, placedCount: 0 };
15668
- paragraph.children = walkAndPlace(paragraph.children, pending, state);
16082
+ paragraph.children = walkAndPlace(paragraph.children, pending, state, view);
15669
16083
  flushAtCurrentOffset(paragraph.children, pending, state);
15670
16084
  if (state.placedCount !== markers.length) {
15671
16085
  throw new SpanOutOfRangeError(`Could not place comment markers (placed ${state.placedCount} of ${markers.length})`);
15672
16086
  }
15673
16087
  }
15674
- function walkAndPlace(children, pending, state) {
16088
+ function walkAndPlace(children, pending, state, view) {
15675
16089
  const result = [];
15676
16090
  for (const child of children) {
15677
16091
  if (child.tag === "w:r") {
@@ -15714,8 +16128,12 @@ function walkAndPlace(children, pending, state) {
15714
16128
  continue;
15715
16129
  }
15716
16130
  if (isRunBearingWrapper(child.tag)) {
16131
+ if (!isWrapperVisibleInView(child.tag, view)) {
16132
+ result.push(child);
16133
+ continue;
16134
+ }
15717
16135
  flushAtCurrentOffset(result, pending, state);
15718
- const innerChildren = walkAndPlace(child.children, pending, state);
16136
+ const innerChildren = walkAndPlace(child.children, pending, state, view);
15719
16137
  const wrapper = new XmlNode2(child.tag, { ...child.attributes });
15720
16138
  wrapper.children = innerChildren;
15721
16139
  result.push(wrapper);
@@ -15880,10 +16298,16 @@ async function run(args) {
15880
16298
  allowPositionals: true,
15881
16299
  options: {
15882
16300
  range: { type: "string" },
16301
+ anchor: { type: "string" },
16302
+ occurrence: { type: "string" },
15883
16303
  text: { type: "string" },
16304
+ batch: { type: "string" },
15884
16305
  author: { type: "string" },
16306
+ current: { type: "boolean" },
16307
+ baseline: { type: "boolean" },
15885
16308
  output: { type: "string", short: "o" },
15886
16309
  "dry-run": { type: "boolean" },
16310
+ verbose: { type: "boolean", short: "v" },
15887
16311
  help: { type: "boolean", short: "h" }
15888
16312
  }
15889
16313
  });
@@ -15895,171 +16319,338 @@ async function run(args) {
15895
16319
  await writeStdout(HELP);
15896
16320
  return EXIT.OK;
15897
16321
  }
16322
+ setVerboseAck(Boolean(parsed.values.verbose));
15898
16323
  const path = parsed.positionals[0];
15899
16324
  if (!path)
15900
16325
  return fail("USAGE", "Missing FILE argument", HELP);
15901
16326
  const rangeInput = parsed.values.range;
16327
+ const anchorInput = parsed.values.anchor;
16328
+ const batchInput = parsed.values.batch;
15902
16329
  const text = parsed.values.text;
15903
- if (!rangeInput)
15904
- return fail("USAGE", "Missing --range LOCATOR", HELP);
15905
- if (!text)
15906
- return fail("USAGE", "Missing --text TEXT", HELP);
15907
- let locator;
15908
- try {
15909
- locator = parseLocator(rangeInput);
15910
- } catch (error) {
15911
- if (error instanceof LocatorParseError) {
15912
- return fail("INVALID_LOCATOR", error.message);
15913
- }
15914
- throw error;
16330
+ const occurrenceRaw = parsed.values.occurrence;
16331
+ const defaultAuthor = parsed.values.author ?? Bun.env.DOCX_AUTHOR ?? "";
16332
+ const outputPath = parsed.values.output;
16333
+ const dryRun = Boolean(parsed.values["dry-run"]);
16334
+ const wantCurrent = Boolean(parsed.values.current);
16335
+ const wantBaseline = Boolean(parsed.values.baseline);
16336
+ if (wantCurrent && wantBaseline) {
16337
+ return fail("USAGE", "--current and --baseline are mutually exclusive");
15915
16338
  }
15916
- if (locator.kind === "range") {
15917
- return runCrossBlock(parsed, path, rangeInput, locator, text);
16339
+ const findView = wantCurrent ? "current" : wantBaseline ? "baseline" : "accepted";
16340
+ const anchorCount = (rangeInput ? 1 : 0) + (anchorInput ? 1 : 0) + (batchInput ? 1 : 0);
16341
+ if (anchorCount === 0) {
16342
+ return fail("USAGE", "Specify exactly one of --range, --anchor, or --batch", HELP);
15918
16343
  }
15919
- const target = locatorToBlockTarget(locator);
15920
- if (!target) {
15921
- return fail("INVALID_LOCATOR", "comments add supports paragraph locators only (pN, pN:start-end, pN:S-pM:E, or tT:rRcC:pK[:start-end])", "Comments and images are not valid anchors.");
16344
+ if (anchorCount > 1) {
16345
+ return fail("USAGE", "--range, --anchor, and --batch are mutually exclusive", HELP);
15922
16346
  }
15923
- const blockId = target.blockId;
15924
16347
  const view = await openOrFail(path);
15925
16348
  if (typeof view === "number")
15926
16349
  return view;
15927
- const paragraphRef = await resolveBlockOrFail(view, blockId);
15928
- if (typeof paragraphRef === "number")
15929
- return paragraphRef;
15930
- const span = target.span;
15931
- const author = parsed.values.author ?? Bun.env.DOCX_AUTHOR ?? "";
15932
- const date = new Date().toISOString();
15933
- const numericId = nextCommentId(view);
15934
- const paraId = generateParaId();
15935
- const outputPath = parsed.values.output;
15936
- if (parsed.values["dry-run"]) {
15937
- await respond({
15938
- ok: true,
15939
- operation: "comments.add",
15940
- dryRun: true,
15941
- path,
15942
- commentId: `c${numericId}`,
15943
- locator: rangeInput,
15944
- ...outputPath ? { output: outputPath } : {}
15945
- });
15946
- return EXIT.OK;
15947
- }
16350
+ let rawEntries;
16351
+ if (batchInput) {
16352
+ if (text !== undefined || rangeInput || anchorInput || occurrenceRaw) {
16353
+ return fail("USAGE", "--batch reads each entry's range/anchor/text/author from JSONL \u2014 do not pass them on the CLI", HELP);
16354
+ }
16355
+ try {
16356
+ rawEntries = await readJsonlEntries(batchInput);
16357
+ } catch (error) {
16358
+ const message = error instanceof Error ? error.message : String(error);
16359
+ return fail("USAGE", `Failed to read batch: ${message}`);
16360
+ }
16361
+ if (rawEntries.length === 0) {
16362
+ return fail("USAGE", "Batch file is empty");
16363
+ }
16364
+ } else {
16365
+ if (!text)
16366
+ return fail("USAGE", "Missing --text TEXT", HELP);
16367
+ const occurrence = occurrenceRaw === undefined ? undefined : Number(occurrenceRaw);
16368
+ if (occurrence !== undefined && (!Number.isInteger(occurrence) || occurrence < 1)) {
16369
+ return fail("USAGE", `--occurrence must be a positive integer (1-indexed), got "${occurrenceRaw}"`);
16370
+ }
16371
+ rawEntries = [
16372
+ {
16373
+ range: rangeInput,
16374
+ anchor: anchorInput,
16375
+ occurrence,
16376
+ text
16377
+ }
16378
+ ];
16379
+ }
16380
+ let resolved;
15948
16381
  try {
15949
- addCommentMarkersToParagraph(paragraphRef.node, numericId, span);
16382
+ resolved = rawEntries.map((entry, index) => resolveEntry(view, entry, defaultAuthor, index, batchInput !== undefined, findView));
15950
16383
  } catch (error) {
15951
- if (error instanceof SpanOutOfRangeError) {
15952
- return fail("INVALID_LOCATOR", error.message);
16384
+ if (error instanceof EntryError) {
16385
+ return fail(error.code, error.message, error.hint);
15953
16386
  }
15954
16387
  throw error;
15955
16388
  }
15956
- const commentsRoot = ensureCommentsPart(view);
15957
- commentsRoot.children.push(/* @__PURE__ */ jsxDEV(CommentBody, {
15958
- options: {
15959
- id: numericId,
15960
- author,
15961
- date,
15962
- initials: authorInitials(author),
15963
- paraId,
15964
- text
15965
- }
15966
- }, undefined, false, undefined, this));
15967
- await saveDocView(view, outputPath);
15968
- await respond({
15969
- ok: true,
15970
- operation: "comments.add",
15971
- path: outputPath ?? path,
15972
- commentId: `c${numericId}`,
15973
- locator: rangeInput
15974
- });
15975
- return EXIT.OK;
15976
- }
15977
- async function runCrossBlock(parsed, path, rangeInput, locator, text) {
15978
- const view = await openOrFail(path);
15979
- if (typeof view === "number")
15980
- return view;
15981
- const startRef = await resolveBlockOrFail(view, locator.start.blockId);
15982
- if (typeof startRef === "number")
15983
- return startRef;
15984
- const endRef = await resolveBlockOrFail(view, locator.end.blockId);
15985
- if (typeof endRef === "number")
15986
- return endRef;
15987
- const author = parsed.values.author ?? Bun.env.DOCX_AUTHOR ?? "";
15988
- const date = new Date().toISOString();
15989
- const numericId = nextCommentId(view);
15990
- const paraId = generateParaId();
15991
- const outputPath = parsed.values.output;
15992
- if (parsed.values["dry-run"]) {
16389
+ if (dryRun) {
15993
16390
  await respond({
15994
16391
  ok: true,
15995
16392
  operation: "comments.add",
15996
16393
  dryRun: true,
15997
16394
  path,
15998
- commentId: `c${numericId}`,
15999
- locator: rangeInput,
16000
- ...outputPath ? { output: outputPath } : {}
16395
+ ...outputPath ? { output: outputPath } : {},
16396
+ batch: resolved.map((entry) => ({
16397
+ locator: entry.locatorString
16398
+ }))
16001
16399
  });
16002
16400
  return EXIT.OK;
16003
16401
  }
16004
- try {
16005
- addCommentRangeMarkers(startRef.node, locator.start.offset, endRef.node, locator.end.offset, numericId);
16006
- } catch (error) {
16007
- if (error instanceof SpanOutOfRangeError) {
16008
- return fail("INVALID_LOCATOR", error.message);
16009
- }
16010
- throw error;
16011
- }
16012
- const commentsRoot = ensureCommentsPart(view);
16013
- commentsRoot.children.push(/* @__PURE__ */ jsxDEV(CommentBody, {
16014
- options: {
16015
- id: numericId,
16016
- author,
16017
- date,
16018
- initials: authorInitials(author),
16019
- paraId,
16020
- text
16021
- }
16022
- }, undefined, false, undefined, this));
16402
+ const date = new Date().toISOString();
16403
+ const minted = [];
16404
+ for (const entry of resolved) {
16405
+ const numericId = nextCommentId(view);
16406
+ const paraId = generateParaId();
16407
+ try {
16408
+ if (entry.kind === "single") {
16409
+ const paragraphRef = await resolveBlockOrFail(view, entry.blockId);
16410
+ if (typeof paragraphRef === "number") {
16411
+ return paragraphRef;
16412
+ }
16413
+ addCommentMarkersToParagraph(paragraphRef.node, numericId, entry.span, findView);
16414
+ } else {
16415
+ const startRef = await resolveBlockOrFail(view, entry.startBlockId);
16416
+ if (typeof startRef === "number")
16417
+ return startRef;
16418
+ const endRef = await resolveBlockOrFail(view, entry.endBlockId);
16419
+ if (typeof endRef === "number")
16420
+ return endRef;
16421
+ addCommentRangeMarkers(startRef.node, entry.startOffset, endRef.node, entry.endOffset, numericId, findView);
16422
+ }
16423
+ } catch (error) {
16424
+ if (error instanceof SpanOutOfRangeError) {
16425
+ return fail("INVALID_LOCATOR", error.message);
16426
+ }
16427
+ throw error;
16428
+ }
16429
+ const commentsRoot = ensureCommentsPart(view);
16430
+ commentsRoot.children.push(/* @__PURE__ */ jsxDEV(CommentBody, {
16431
+ options: {
16432
+ id: numericId,
16433
+ author: entry.author,
16434
+ date,
16435
+ initials: authorInitials(entry.author),
16436
+ paraId,
16437
+ text: entry.text
16438
+ }
16439
+ }, undefined, false, undefined, this));
16440
+ minted.push({
16441
+ commentId: `c${numericId}`,
16442
+ locator: entry.locatorString
16443
+ });
16444
+ }
16023
16445
  await saveDocView(view, outputPath);
16024
- await respond({
16025
- ok: true,
16026
- operation: "comments.add",
16027
- path: outputPath ?? path,
16028
- commentId: `c${numericId}`,
16029
- locator: rangeInput
16030
- });
16446
+ if (batchInput) {
16447
+ await respond({
16448
+ ok: true,
16449
+ operation: "comments.add",
16450
+ path: outputPath ?? path,
16451
+ batch: minted
16452
+ });
16453
+ } else {
16454
+ const single = minted[0];
16455
+ if (!single) {
16456
+ throw new Error("internal: single-shot path produced no minted entry");
16457
+ }
16458
+ await respondAck({
16459
+ ok: true,
16460
+ operation: "comments.add",
16461
+ path: outputPath ?? path,
16462
+ commentId: single.commentId,
16463
+ locator: single.locator
16464
+ });
16465
+ }
16031
16466
  return EXIT.OK;
16032
16467
  }
16033
- var HELP = `docx comments add \u2014 anchor a new comment to a locator
16468
+ function resolveEntry(view, raw, defaultAuthor, entryIndex, isBatch, findView) {
16469
+ const labelPrefix = isBatch ? `entry ${entryIndex}: ` : "";
16470
+ const author = raw.author ?? defaultAuthor;
16471
+ const anchorCount = (raw.range ? 1 : 0) + (raw.anchor ? 1 : 0);
16472
+ if (anchorCount === 0) {
16473
+ throw new EntryError("USAGE", `${labelPrefix}must specify either "range" or "anchor"`);
16474
+ }
16475
+ if (anchorCount > 1) {
16476
+ throw new EntryError("USAGE", `${labelPrefix}"range" and "anchor" are mutually exclusive`);
16477
+ }
16478
+ if (typeof raw.text !== "string" || raw.text.length === 0) {
16479
+ throw new EntryError("USAGE", `${labelPrefix}"text" is required`);
16480
+ }
16481
+ if (raw.range) {
16482
+ return resolveLocatorEntry(raw.range, raw.text, author, labelPrefix);
16483
+ }
16484
+ const anchor = raw.anchor;
16485
+ if (!anchor) {
16486
+ throw new EntryError("USAGE", `${labelPrefix}missing anchor`);
16487
+ }
16488
+ const occurrenceExplicit = raw.occurrence !== undefined;
16489
+ const occurrence = raw.occurrence ?? 1;
16490
+ if (!Number.isInteger(occurrence) || occurrence < 1) {
16491
+ throw new EntryError("USAGE", `${labelPrefix}"occurrence" must be a positive integer, got ${JSON.stringify(raw.occurrence)}`);
16492
+ }
16493
+ return resolveAnchorEntry(view, anchor, occurrence, occurrenceExplicit, raw.text, author, labelPrefix, findView);
16494
+ }
16495
+ function resolveLocatorEntry(rangeInput, text, author, labelPrefix) {
16496
+ let locator;
16497
+ try {
16498
+ locator = parseLocator(rangeInput);
16499
+ } catch (error) {
16500
+ const message = error instanceof Error ? error.message : String(error);
16501
+ throw new EntryError("INVALID_LOCATOR", `${labelPrefix}${message}`);
16502
+ }
16503
+ if (locator.kind === "range") {
16504
+ return {
16505
+ kind: "range",
16506
+ startBlockId: locator.start.blockId,
16507
+ startOffset: locator.start.offset,
16508
+ endBlockId: locator.end.blockId,
16509
+ endOffset: locator.end.offset,
16510
+ text,
16511
+ author,
16512
+ locatorString: rangeInput
16513
+ };
16514
+ }
16515
+ const target = locatorToBlockTarget(locator);
16516
+ if (!target) {
16517
+ throw new EntryError("INVALID_LOCATOR", `${labelPrefix}comments add supports paragraph locators only (pN, pN:start-end, pN:S-pM:E, or tT:rRcC:pK[:start-end])`, "Comments and images are not valid anchors.");
16518
+ }
16519
+ return {
16520
+ kind: "single",
16521
+ blockId: target.blockId,
16522
+ span: target.span,
16523
+ text,
16524
+ author,
16525
+ locatorString: rangeInput
16526
+ };
16527
+ }
16528
+ function resolveAnchorEntry(view, anchor, occurrence, occurrenceExplicit, text, author, labelPrefix, findView) {
16529
+ const result = findTextSpans(view.doc, anchor, { view: findView });
16530
+ const matches = result.matches;
16531
+ if (matches.length === 0) {
16532
+ throw new EntryError("MATCH_NOT_FOUND", `${labelPrefix}anchor not found: ${JSON.stringify(anchor)}`, result.normalizedQuery ? `Searched as "${result.normalizedQuery}" after normalization (${(result.normalizationApplied ?? []).join(", ")}).` : undefined);
16533
+ }
16534
+ if (matches.length > 1 && !occurrenceExplicit) {
16535
+ throw new EntryError("USAGE", `${labelPrefix}anchor matches ${matches.length} times; pass --occurrence N (1..${matches.length}) to disambiguate`);
16536
+ }
16537
+ if (occurrence > matches.length) {
16538
+ throw new EntryError("MATCH_NOT_FOUND", `${labelPrefix}only ${matches.length} match(es) for anchor; --occurrence ${occurrence} is out of range`);
16539
+ }
16540
+ const match = matches[occurrence - 1];
16541
+ if (!match) {
16542
+ throw new EntryError("MATCH_NOT_FOUND", `${labelPrefix}match index ${occurrence} not found`);
16543
+ }
16544
+ const locatorString = `${match.blockId}:${match.start}-${match.end}`;
16545
+ return {
16546
+ kind: "single",
16547
+ blockId: match.blockId,
16548
+ span: { start: match.start, end: match.end },
16549
+ text,
16550
+ author,
16551
+ locatorString
16552
+ };
16553
+ }
16554
+ async function readJsonlEntries(source) {
16555
+ const raw = source === "-" ? await new Response(Bun.stdin.stream()).text() : await Bun.file(source).text();
16556
+ const entries = [];
16557
+ const lines = raw.split(`
16558
+ `);
16559
+ for (let index = 0;index < lines.length; index++) {
16560
+ const lineRaw = lines[index];
16561
+ if (lineRaw === undefined)
16562
+ continue;
16563
+ const line = lineRaw.trim();
16564
+ if (line.length === 0)
16565
+ continue;
16566
+ let parsed;
16567
+ try {
16568
+ parsed = JSON.parse(line);
16569
+ } catch (error) {
16570
+ const message = error instanceof Error ? error.message : String(error);
16571
+ throw new Error(`line ${index + 1}: invalid JSON (${message})`);
16572
+ }
16573
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
16574
+ throw new Error(`line ${index + 1}: expected a JSON object`);
16575
+ }
16576
+ entries.push(parsed);
16577
+ }
16578
+ return entries;
16579
+ }
16580
+ var HELP = `docx comments add \u2014 anchor a new comment to a locator or phrase
16034
16581
 
16035
16582
  Usage:
16036
- docx comments add FILE [options]
16037
-
16038
- Required:
16039
- --range LOCATOR Where to anchor. Supports:
16040
- pN whole paragraph
16041
- pN:S-E chars S..E of pN
16042
- pN:S-pM:E chars S of pN through char E of pM (cross-paragraph)
16043
- tT:rRcC:pK whole cell paragraph
16044
- tT:rRcC:pK:S-E chars S..E of cell paragraph
16045
- --text TEXT Comment body
16046
-
16047
- Optional:
16048
- --author NAME Author name (default: $DOCX_AUTHOR)
16049
- -o, --output PATH Write to PATH instead of overwriting FILE
16050
- --dry-run Print what would be added; do not write the file
16051
- -h, --help Show this help
16583
+ docx comments add FILE --range LOCATOR --text TEXT [options]
16584
+ docx comments add FILE --anchor PHRASE --text TEXT [options]
16585
+ docx comments add FILE --batch FILE.jsonl [options]
16586
+ docx comments add FILE --batch - [options] # read JSONL from stdin
16587
+
16588
+ Anchor (one required, mutually exclusive):
16589
+ --range LOCATOR Where to anchor. Supports:
16590
+ pN whole paragraph
16591
+ pN:S-E chars S..E of pN
16592
+ pN:S-pM:E chars S of pN through char E of pM
16593
+ tT:rRcC:pK whole cell paragraph
16594
+ tT:rRcC:pK:S-E chars S..E of cell paragraph
16595
+ --anchor PHRASE Find the phrase via the same matcher as \`docx find\`
16596
+ (default: accepted view, normalization on). The match
16597
+ is converted to a pN:S-E locator and used as the
16598
+ anchor. Errors if the phrase matches more than once
16599
+ without --occurrence.
16600
+ --batch PATH Read a JSONL file (one entry per line). Each entry is
16601
+ a JSON object with the same shape as a single-shot
16602
+ call: { range | anchor (+ optional occurrence), text,
16603
+ optional author }. Use - for stdin.
16604
+
16605
+ Per-entry fields:
16606
+ --text TEXT Comment body. Required for single-shot; required as
16607
+ a "text" field on every JSONL entry.
16608
+ --occurrence N For --anchor: pick the Nth match (1-indexed,
16609
+ default 1). Errors if N is out of range.
16610
+ --author NAME Author name (default: $DOCX_AUTHOR)
16611
+
16612
+ View (for resolving --range offsets and --anchor matches):
16613
+ --current Resolve offsets in the raw concatenation (with both
16614
+ ins and del text). Use when offsets came from
16615
+ \`docx find --current\` or hand-counted bytes.
16616
+ --baseline Resolve offsets in the pre-change view (skip ins/
16617
+ moveTo).
16618
+ Default: accepted view (skip del/moveFrom) \u2014 matches
16619
+ \`find\`'s default. Mutually exclusive.
16620
+
16621
+ General options:
16622
+ -o, --output PATH Write to PATH instead of overwriting FILE
16623
+ --dry-run Print what would be added; do not write the file
16624
+ -v, --verbose Print the success ack JSON (default: silent on success
16625
+ for single; batch always prints the minted ids)
16626
+ -h, --help Show this help
16052
16627
 
16053
16628
  Examples:
16054
16629
  docx comments add doc.docx --range p3 --text "Reconsider this paragraph"
16055
- docx comments add doc.docx --range p3:5-20 --text "Sharper wording?" --author "Jane"
16056
- docx comments add doc.docx --range p3:5-p5:10 --text "Whole section?" --author "Reviewer"
16057
- `;
16630
+ docx comments add doc.docx --anchor "fatally flawed" --text "Cite source?"
16631
+ docx comments add doc.docx --anchor "TODO" --occurrence 2 --text "Pick up here"
16632
+ docx comments add doc.docx --batch reviews.jsonl
16633
+ cat reviews.jsonl | docx comments add doc.docx --batch -
16634
+
16635
+ Batch JSONL example:
16636
+ {"range": "p3:5-20", "text": "Sharper wording?"}
16637
+ {"anchor": "fatally flawed", "text": "Cite Bianco here.", "author": "Reviewer"}
16638
+ `, EntryError;
16058
16639
  var init_add = __esm(() => {
16059
16640
  init_core();
16060
16641
  init_respond();
16061
16642
  init_helpers();
16062
16643
  init_jsx_dev_runtime();
16644
+ EntryError = class EntryError extends Error {
16645
+ code;
16646
+ hint;
16647
+ constructor(code, message, hint) {
16648
+ super(message);
16649
+ this.code = code;
16650
+ this.hint = hint;
16651
+ this.name = "EntryError";
16652
+ }
16653
+ };
16063
16654
  });
16064
16655
 
16065
16656
  // src/cli/comments/delete.ts
@@ -16075,9 +16666,11 @@ async function run2(args) {
16075
16666
  args,
16076
16667
  allowPositionals: true,
16077
16668
  options: {
16078
- id: { type: "string" },
16669
+ id: { type: "string", multiple: true },
16670
+ batch: { type: "string" },
16079
16671
  output: { type: "string", short: "o" },
16080
16672
  "dry-run": { type: "boolean" },
16673
+ verbose: { type: "boolean", short: "v" },
16081
16674
  help: { type: "boolean", short: "h" }
16082
16675
  }
16083
16676
  });
@@ -16089,20 +16682,49 @@ async function run2(args) {
16089
16682
  await writeStdout(HELP2);
16090
16683
  return EXIT.OK;
16091
16684
  }
16685
+ setVerboseAck(Boolean(parsed.values.verbose));
16092
16686
  const path = parsed.positionals[0];
16093
16687
  if (!path)
16094
16688
  return fail("USAGE", "Missing FILE argument", HELP2);
16095
- const idInput = parsed.values.id;
16096
- if (!idInput)
16097
- return fail("USAGE", "Missing --id COMMENT_ID", HELP2);
16098
- const numericId = idInput.startsWith("c") ? idInput.slice(1) : idInput;
16099
- const commentId = `c${numericId}`;
16689
+ const idsRaw = parsed.values.id ?? [];
16690
+ const batchInput = parsed.values.batch;
16691
+ if (idsRaw.length > 0 && batchInput) {
16692
+ return fail("USAGE", "--id and --batch are mutually exclusive", HELP2);
16693
+ }
16694
+ if (idsRaw.length === 0 && !batchInput) {
16695
+ return fail("USAGE", "Specify --id cN (repeatable) or --batch FILE", HELP2);
16696
+ }
16697
+ let rawIds;
16698
+ if (batchInput) {
16699
+ try {
16700
+ rawIds = await readJsonlIds(batchInput);
16701
+ } catch (error) {
16702
+ const message = error instanceof Error ? error.message : String(error);
16703
+ return fail("USAGE", `Failed to read batch: ${message}`);
16704
+ }
16705
+ if (rawIds.length === 0) {
16706
+ return fail("USAGE", "Batch file is empty");
16707
+ }
16708
+ } else {
16709
+ rawIds = idsRaw;
16710
+ }
16711
+ const seen = new Set;
16712
+ const ordered = [];
16713
+ for (const raw of rawIds) {
16714
+ const normalized = raw.startsWith("c") ? raw : `c${raw}`;
16715
+ if (seen.has(normalized))
16716
+ continue;
16717
+ seen.add(normalized);
16718
+ ordered.push(normalized);
16719
+ }
16100
16720
  const view = await openOrFail(path);
16101
16721
  if (typeof view === "number")
16102
16722
  return view;
16103
- const commentReference = findCommentByNumericId(view, numericId);
16104
- if (!commentReference) {
16105
- return fail("COMMENT_NOT_FOUND", `Comment not found: ${commentId}`);
16723
+ for (const commentId of ordered) {
16724
+ const numericId = commentId.slice(1);
16725
+ if (!findCommentByNumericId(view, numericId)) {
16726
+ return fail("COMMENT_NOT_FOUND", `Comment not found: ${commentId}`);
16727
+ }
16106
16728
  }
16107
16729
  const outputPath = parsed.values.output;
16108
16730
  if (parsed.values["dry-run"]) {
@@ -16111,46 +16733,104 @@ async function run2(args) {
16111
16733
  operation: "comments.delete",
16112
16734
  dryRun: true,
16113
16735
  path,
16114
- commentId,
16115
- ...outputPath ? { output: outputPath } : {}
16736
+ ...outputPath ? { output: outputPath } : {},
16737
+ batch: ordered.map((commentId) => ({ commentId }))
16116
16738
  });
16117
16739
  return EXIT.OK;
16118
16740
  }
16119
- const paraId = findCommentParaId(view, commentId);
16120
- const commentIndex = commentReference.parent.indexOf(commentReference.node);
16121
- if (commentIndex !== -1)
16122
- commentReference.parent.splice(commentIndex, 1);
16123
- if (paraId && view.commentsExtTree) {
16124
- const extRoot = XmlNode2.findRoot(view.commentsExtTree, "w15:commentsEx");
16125
- if (extRoot) {
16126
- extRoot.children = extRoot.children.filter((child) => !(child.tag === "w15:commentEx" && child.getAttribute("w15:paraId") === paraId));
16741
+ for (const commentId of ordered) {
16742
+ const numericId = commentId.slice(1);
16743
+ const commentReference = findCommentByNumericId(view, numericId);
16744
+ if (!commentReference)
16745
+ continue;
16746
+ const paraId = findCommentParaId(view, commentId);
16747
+ const commentIndex = commentReference.parent.indexOf(commentReference.node);
16748
+ if (commentIndex !== -1)
16749
+ commentReference.parent.splice(commentIndex, 1);
16750
+ if (paraId && view.commentsExtTree) {
16751
+ const extRoot = XmlNode2.findRoot(view.commentsExtTree, "w15:commentsEx");
16752
+ if (extRoot) {
16753
+ extRoot.children = extRoot.children.filter((child) => !(child.tag === "w15:commentEx" && child.getAttribute("w15:paraId") === paraId));
16754
+ }
16127
16755
  }
16756
+ removeCommentMarkers(view.documentTree, numericId);
16128
16757
  }
16129
- removeCommentMarkers(view.documentTree, numericId);
16130
16758
  await saveDocView(view, outputPath);
16131
- await respond({
16132
- ok: true,
16133
- operation: "comments.delete",
16134
- path: outputPath ?? path,
16135
- commentId
16136
- });
16759
+ if (batchInput || ordered.length > 1) {
16760
+ await respond({
16761
+ ok: true,
16762
+ operation: "comments.delete",
16763
+ path: outputPath ?? path,
16764
+ batch: ordered.map((commentId) => ({ commentId }))
16765
+ });
16766
+ } else {
16767
+ const single = ordered[0];
16768
+ if (!single) {
16769
+ throw new Error("internal: empty single-shot id list");
16770
+ }
16771
+ await respondAck({
16772
+ ok: true,
16773
+ operation: "comments.delete",
16774
+ path: outputPath ?? path,
16775
+ commentId: single
16776
+ });
16777
+ }
16137
16778
  return EXIT.OK;
16138
16779
  }
16139
- var HELP2 = `docx comments delete \u2014 remove a comment
16780
+ async function readJsonlIds(source) {
16781
+ const raw = source === "-" ? await new Response(Bun.stdin.stream()).text() : await Bun.file(source).text();
16782
+ const ids = [];
16783
+ const lines = raw.split(`
16784
+ `);
16785
+ for (let index = 0;index < lines.length; index++) {
16786
+ const lineRaw = lines[index];
16787
+ if (lineRaw === undefined)
16788
+ continue;
16789
+ const line = lineRaw.trim();
16790
+ if (line.length === 0)
16791
+ continue;
16792
+ let parsed;
16793
+ try {
16794
+ parsed = JSON.parse(line);
16795
+ } catch (error) {
16796
+ const message = error instanceof Error ? error.message : String(error);
16797
+ throw new Error(`line ${index + 1}: invalid JSON (${message})`);
16798
+ }
16799
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
16800
+ throw new Error(`line ${index + 1}: expected a JSON object`);
16801
+ }
16802
+ const entry = parsed;
16803
+ if (typeof entry.id !== "string" || entry.id.length === 0) {
16804
+ throw new Error(`line ${index + 1}: missing "id"`);
16805
+ }
16806
+ ids.push(entry.id);
16807
+ }
16808
+ return ids;
16809
+ }
16810
+ var HELP2 = `docx comments delete \u2014 remove one or more comments
16140
16811
 
16141
16812
  Usage:
16142
- docx comments delete FILE --id cN [options]
16813
+ docx comments delete FILE --id cN [--id cM ...] [options]
16814
+ docx comments delete FILE --batch FILE.jsonl [options]
16815
+ docx comments delete FILE --batch - [options] # JSONL from stdin
16143
16816
 
16144
- Required:
16145
- --id ID Comment id (e.g., c0)
16817
+ Anchor (one required, mutually exclusive):
16818
+ --id ID Comment id (e.g., c0). Repeat for multiple ids:
16819
+ --id c1 --id c3 --id c5. All ids are validated against
16820
+ the pre-mutation tree, so the batch is atomic.
16821
+ --batch PATH JSONL with one {"id": "cN"} per line. Use - for stdin.
16146
16822
 
16147
16823
  Optional:
16148
- -o, --output PATH Write to PATH instead of overwriting FILE
16149
- --dry-run Print what would be removed; do not write the file
16150
- -h, --help Show this help
16824
+ -o, --output PATH Write to PATH instead of overwriting FILE
16825
+ --dry-run Print what would be removed; do not write the file
16826
+ -v, --verbose Print the success ack JSON (default: silent on success
16827
+ for single; batch always prints the removed ids)
16828
+ -h, --help Show this help
16151
16829
 
16152
16830
  Examples:
16153
16831
  docx comments delete doc.docx --id c2
16832
+ docx comments delete doc.docx --id c1 --id c3 --id c7
16833
+ docx comments delete doc.docx --batch removals.jsonl
16154
16834
  `;
16155
16835
  var init_delete = __esm(() => {
16156
16836
  init_core();
@@ -16255,6 +16935,7 @@ async function run4(args) {
16255
16935
  author: { type: "string" },
16256
16936
  output: { type: "string", short: "o" },
16257
16937
  "dry-run": { type: "boolean" },
16938
+ verbose: { type: "boolean", short: "v" },
16258
16939
  help: { type: "boolean", short: "h" }
16259
16940
  }
16260
16941
  });
@@ -16266,6 +16947,7 @@ async function run4(args) {
16266
16947
  await writeStdout(HELP4);
16267
16948
  return EXIT.OK;
16268
16949
  }
16950
+ setVerboseAck(Boolean(parsed.values.verbose));
16269
16951
  const path = parsed.positionals[0];
16270
16952
  if (!path)
16271
16953
  return fail("USAGE", "Missing FILE argument", HELP4);
@@ -16322,7 +17004,7 @@ async function run4(args) {
16322
17004
  "w15:done": "0"
16323
17005
  }));
16324
17006
  await saveDocView(view, outputPath);
16325
- await respond({
17007
+ await respondAck({
16326
17008
  ok: true,
16327
17009
  operation: "comments.reply",
16328
17010
  path: outputPath ?? path,
@@ -16344,6 +17026,7 @@ Optional:
16344
17026
  --author NAME Author name (default: $DOCX_AUTHOR)
16345
17027
  -o, --output PATH Write to PATH instead of overwriting FILE
16346
17028
  --dry-run Print what would be added; do not write the file
17029
+ -v, --verbose Print the success ack JSON (default: silent on success)
16347
17030
  -h, --help Show this help
16348
17031
 
16349
17032
  Examples:
@@ -16370,10 +17053,12 @@ async function run5(args) {
16370
17053
  args,
16371
17054
  allowPositionals: true,
16372
17055
  options: {
16373
- id: { type: "string" },
17056
+ id: { type: "string", multiple: true },
17057
+ batch: { type: "string" },
16374
17058
  unset: { type: "boolean" },
16375
17059
  output: { type: "string", short: "o" },
16376
17060
  "dry-run": { type: "boolean" },
17061
+ verbose: { type: "boolean", short: "v" },
16377
17062
  help: { type: "boolean", short: "h" }
16378
17063
  }
16379
17064
  });
@@ -16385,24 +17070,56 @@ async function run5(args) {
16385
17070
  await writeStdout(HELP5);
16386
17071
  return EXIT.OK;
16387
17072
  }
17073
+ setVerboseAck(Boolean(parsed.values.verbose));
16388
17074
  const path = parsed.positionals[0];
16389
17075
  if (!path)
16390
17076
  return fail("USAGE", "Missing FILE argument", HELP5);
16391
- const idInput = parsed.values.id;
16392
- if (!idInput)
16393
- return fail("USAGE", "Missing --id COMMENT_ID", HELP5);
16394
- const numericId = idInput.startsWith("c") ? idInput.slice(1) : idInput;
17077
+ const idsRaw = parsed.values.id ?? [];
17078
+ const batchInput = parsed.values.batch;
16395
17079
  const resolved = !parsed.values.unset;
17080
+ if (idsRaw.length > 0 && batchInput) {
17081
+ return fail("USAGE", "--id and --batch are mutually exclusive", HELP5);
17082
+ }
17083
+ if (idsRaw.length === 0 && !batchInput) {
17084
+ return fail("USAGE", "Specify --id cN (repeatable) or --batch FILE", HELP5);
17085
+ }
17086
+ let rawIds;
17087
+ if (batchInput) {
17088
+ try {
17089
+ rawIds = await readJsonlIds2(batchInput);
17090
+ } catch (error) {
17091
+ const message = error instanceof Error ? error.message : String(error);
17092
+ return fail("USAGE", `Failed to read batch: ${message}`);
17093
+ }
17094
+ if (rawIds.length === 0) {
17095
+ return fail("USAGE", "Batch file is empty");
17096
+ }
17097
+ } else {
17098
+ rawIds = idsRaw;
17099
+ }
17100
+ const seen = new Set;
17101
+ const ordered = [];
17102
+ for (const raw of rawIds) {
17103
+ const normalized = raw.startsWith("c") ? raw : `c${raw}`;
17104
+ if (seen.has(normalized))
17105
+ continue;
17106
+ seen.add(normalized);
17107
+ ordered.push(normalized);
17108
+ }
16396
17109
  const view = await openOrFail(path);
16397
17110
  if (typeof view === "number")
16398
17111
  return view;
16399
- const commentReference = findCommentByNumericId(view, numericId);
16400
- if (!commentReference) {
16401
- return fail("COMMENT_NOT_FOUND", `Comment not found: c${numericId}`);
16402
- }
16403
- const paraId = ensureCommentParaId(view, idInput);
16404
- if (!paraId) {
16405
- return fail("COMMENT_NOT_FOUND", `Comment c${numericId} could not be assigned a w14:paraId.`);
17112
+ const paraIdByCommentId = new Map;
17113
+ for (const commentId of ordered) {
17114
+ const numericId = commentId.slice(1);
17115
+ if (!findCommentByNumericId(view, numericId)) {
17116
+ return fail("COMMENT_NOT_FOUND", `Comment not found: ${commentId}`);
17117
+ }
17118
+ const paraId = ensureCommentParaId(view, commentId);
17119
+ if (!paraId) {
17120
+ return fail("COMMENT_NOT_FOUND", `Comment ${commentId} could not be assigned a w14:paraId.`);
17121
+ }
17122
+ paraIdByCommentId.set(commentId, paraId);
16406
17123
  }
16407
17124
  const outputPath = parsed.values.output;
16408
17125
  if (parsed.values["dry-run"]) {
@@ -16411,49 +17128,108 @@ async function run5(args) {
16411
17128
  operation: "comments.resolve",
16412
17129
  dryRun: true,
16413
17130
  path,
16414
- commentId: `c${numericId}`,
16415
17131
  resolved,
16416
- ...outputPath ? { output: outputPath } : {}
17132
+ ...outputPath ? { output: outputPath } : {},
17133
+ batch: ordered.map((commentId) => ({ commentId, resolved }))
16417
17134
  });
16418
17135
  return EXIT.OK;
16419
17136
  }
16420
17137
  const extRoot = ensureCommentsExtPart(view);
16421
- let entry = extRoot.children.find((child) => child.tag === "w15:commentEx" && child.getAttribute("w15:paraId") === paraId);
16422
- if (!entry) {
16423
- entry = new XmlNode2("w15:commentEx", { "w15:paraId": paraId });
16424
- extRoot.children.push(entry);
17138
+ for (const commentId of ordered) {
17139
+ const paraId = paraIdByCommentId.get(commentId);
17140
+ if (!paraId) {
17141
+ throw new Error(`internal: missing paraId for ${commentId}`);
17142
+ }
17143
+ let entry = extRoot.children.find((child) => child.tag === "w15:commentEx" && child.getAttribute("w15:paraId") === paraId);
17144
+ if (!entry) {
17145
+ entry = new XmlNode2("w15:commentEx", { "w15:paraId": paraId });
17146
+ extRoot.children.push(entry);
17147
+ }
17148
+ if (resolved)
17149
+ entry.setAttribute("w15:done", "1");
17150
+ else
17151
+ delete entry.attributes["w15:done"];
16425
17152
  }
16426
- if (resolved)
16427
- entry.setAttribute("w15:done", "1");
16428
- else
16429
- delete entry.attributes["w15:done"];
16430
17153
  await saveDocView(view, outputPath);
16431
- await respond({
16432
- ok: true,
16433
- operation: "comments.resolve",
16434
- path: outputPath ?? path,
16435
- commentId: `c${numericId}`,
16436
- resolved
16437
- });
17154
+ if (batchInput || ordered.length > 1) {
17155
+ await respond({
17156
+ ok: true,
17157
+ operation: "comments.resolve",
17158
+ path: outputPath ?? path,
17159
+ resolved,
17160
+ batch: ordered.map((commentId) => ({ commentId, resolved }))
17161
+ });
17162
+ } else {
17163
+ const single = ordered[0];
17164
+ if (!single) {
17165
+ throw new Error("internal: empty single-shot id list");
17166
+ }
17167
+ await respondAck({
17168
+ ok: true,
17169
+ operation: "comments.resolve",
17170
+ path: outputPath ?? path,
17171
+ commentId: single,
17172
+ resolved
17173
+ });
17174
+ }
16438
17175
  return EXIT.OK;
16439
17176
  }
16440
- var HELP5 = `docx comments resolve \u2014 mark a comment resolved
17177
+ async function readJsonlIds2(source) {
17178
+ const raw = source === "-" ? await new Response(Bun.stdin.stream()).text() : await Bun.file(source).text();
17179
+ const ids = [];
17180
+ const lines = raw.split(`
17181
+ `);
17182
+ for (let index = 0;index < lines.length; index++) {
17183
+ const lineRaw = lines[index];
17184
+ if (lineRaw === undefined)
17185
+ continue;
17186
+ const line = lineRaw.trim();
17187
+ if (line.length === 0)
17188
+ continue;
17189
+ let parsed;
17190
+ try {
17191
+ parsed = JSON.parse(line);
17192
+ } catch (error) {
17193
+ const message = error instanceof Error ? error.message : String(error);
17194
+ throw new Error(`line ${index + 1}: invalid JSON (${message})`);
17195
+ }
17196
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
17197
+ throw new Error(`line ${index + 1}: expected a JSON object`);
17198
+ }
17199
+ const entry = parsed;
17200
+ if (typeof entry.id !== "string" || entry.id.length === 0) {
17201
+ throw new Error(`line ${index + 1}: missing "id"`);
17202
+ }
17203
+ ids.push(entry.id);
17204
+ }
17205
+ return ids;
17206
+ }
17207
+ var HELP5 = `docx comments resolve \u2014 mark one or more comments resolved
16441
17208
 
16442
17209
  Usage:
16443
- docx comments resolve FILE --id cN [options]
17210
+ docx comments resolve FILE --id cN [--id cM ...] [options]
17211
+ docx comments resolve FILE --batch FILE.jsonl [options]
17212
+ docx comments resolve FILE --batch - [options] # JSONL from stdin
16444
17213
 
16445
- Required:
16446
- --id ID Comment id (e.g., c0)
17214
+ Anchor (one required, mutually exclusive):
17215
+ --id ID Comment id (e.g., c0). Repeat for multiple ids:
17216
+ --id c1 --id c3 --id c5. All ids are validated against
17217
+ the pre-mutation tree, so the batch is atomic.
17218
+ --batch PATH JSONL with one {"id": "cN"} per line. Use - for stdin.
16447
17219
 
16448
17220
  Optional:
16449
- --unset Mark unresolved instead of resolved
16450
- -o, --output PATH Write to PATH instead of overwriting FILE
16451
- --dry-run Print what would change; do not write the file
16452
- -h, --help Show this help
17221
+ --unset Mark unresolved instead of resolved (applies to all
17222
+ ids in the batch)
17223
+ -o, --output PATH Write to PATH instead of overwriting FILE
17224
+ --dry-run Print what would change; do not write the file
17225
+ -v, --verbose Print the success ack JSON (default: silent on success
17226
+ for single; batch always prints the affected ids)
17227
+ -h, --help Show this help
16453
17228
 
16454
17229
  Examples:
16455
17230
  docx comments resolve doc.docx --id c2
16456
- docx comments resolve doc.docx --id c2 --unset
17231
+ docx comments resolve doc.docx --id c1 --id c3 --unset
17232
+ docx comments resolve doc.docx --batch resolutions.jsonl
16457
17233
  `;
16458
17234
  var init_resolve2 = __esm(() => {
16459
17235
  init_core();
@@ -16611,6 +17387,7 @@ async function run7(args) {
16611
17387
  author: { type: "string" },
16612
17388
  text: { type: "string" },
16613
17389
  force: { type: "boolean" },
17390
+ verbose: { type: "boolean", short: "v" },
16614
17391
  help: { type: "boolean", short: "h" }
16615
17392
  }
16616
17393
  });
@@ -16621,6 +17398,7 @@ async function run7(args) {
16621
17398
  await writeStdout(HELP7);
16622
17399
  return EXIT.OK;
16623
17400
  }
17401
+ setVerboseAck(Boolean(parsed.values.verbose));
16624
17402
  const path = parsed.positionals[0];
16625
17403
  if (!path) {
16626
17404
  return fail("USAGE", "Missing FILE argument", HELP7);
@@ -16644,7 +17422,7 @@ async function run7(args) {
16644
17422
  compressionOptions: { level: 6 }
16645
17423
  });
16646
17424
  await writeAtomic(path, buf);
16647
- await respond({
17425
+ await respondAck({
16648
17426
  ok: true,
16649
17427
  operation: "create",
16650
17428
  path,
@@ -16663,6 +17441,7 @@ Options:
16663
17441
  --author TEXT Document author (default: $DOCX_AUTHOR)
16664
17442
  --text TEXT Seed first paragraph with this text
16665
17443
  --force Overwrite if file exists
17444
+ -v, --verbose Print the success ack JSON (default: silent on success)
16666
17445
  -h, --help Show this help
16667
17446
 
16668
17447
  Examples:
@@ -16683,18 +17462,27 @@ __export(exports_delete2, {
16683
17462
  });
16684
17463
  import { parseArgs as parseArgs7 } from "util";
16685
17464
  async function run8(args) {
17465
+ const opts = await parseAndValidateOptions(args);
17466
+ if (typeof opts === "number")
17467
+ return opts;
17468
+ const view = await openOrFail(opts.filePath);
17469
+ if (typeof view === "number")
17470
+ return view;
17471
+ const blockRef = await resolveBlockOrFail(view, opts.locator);
17472
+ if (typeof blockRef === "number")
17473
+ return blockRef;
17474
+ if (blockRef.node.tag === "w:sectPr") {
17475
+ return commitSectionDelete(view, blockRef, opts);
17476
+ }
17477
+ return commitBlockDelete(view, blockRef, opts);
17478
+ }
17479
+ async function parseAndValidateOptions(args) {
16686
17480
  let parsed;
16687
17481
  try {
16688
17482
  parsed = parseArgs7({
16689
17483
  args,
16690
17484
  allowPositionals: true,
16691
- options: {
16692
- at: { type: "string" },
16693
- author: { type: "string" },
16694
- output: { type: "string", short: "o" },
16695
- "dry-run": { type: "boolean" },
16696
- help: { type: "boolean", short: "h" }
16697
- }
17485
+ options: OPTION_SPEC
16698
17486
  });
16699
17487
  } catch (parseError) {
16700
17488
  const message = parseError instanceof Error ? parseError.message : String(parseError);
@@ -16704,51 +17492,89 @@ async function run8(args) {
16704
17492
  await writeStdout(HELP8);
16705
17493
  return EXIT.OK;
16706
17494
  }
16707
- const path = parsed.positionals[0];
16708
- if (!path)
17495
+ setVerboseAck(Boolean(parsed.values.verbose));
17496
+ const filePath = parsed.positionals[0];
17497
+ if (!filePath)
16709
17498
  return fail("USAGE", "Missing FILE argument", HELP8);
16710
- const at = parsed.values.at;
16711
- if (!at)
17499
+ const locator = parsed.values.at;
17500
+ if (!locator)
16712
17501
  return fail("USAGE", "Missing --at LOCATOR", HELP8);
16713
- const view = await openOrFail(path);
16714
- if (typeof view === "number")
16715
- return view;
16716
- const blockRef = await resolveBlockOrFail(view, at);
16717
- if (typeof blockRef === "number")
16718
- return blockRef;
17502
+ return {
17503
+ filePath,
17504
+ locator,
17505
+ authorFlag: parsed.values.author,
17506
+ outputPath: parsed.values.output,
17507
+ dryRun: Boolean(parsed.values["dry-run"])
17508
+ };
17509
+ }
17510
+ async function commitSectionDelete(view, blockRef, opts) {
17511
+ const bodyChildren = findBodyChildren(view);
17512
+ if (bodyChildren && isTrailingSectPr(bodyChildren, blockRef.parent)) {
17513
+ return fail("USAGE", "Cannot delete the trailing section break (mandatory in OOXML)", "Use `docx edit --at sN --columns 1` to reset its properties instead.");
17514
+ }
17515
+ if (opts.dryRun)
17516
+ return respondDryRun(opts);
17517
+ const trackingOn = isTrackChangesEnabled(view);
17518
+ const owningParagraph = trackingOn ? findContainingParagraph(view.documentTree, blockRef.node) : null;
17519
+ const anchorRun = owningParagraph?.children.find((child) => child.tag === "w:r") ?? null;
17520
+ removeInlineSectPr(blockRef.node, blockRef.parent);
17521
+ if (trackingOn && owningParagraph && anchorRun) {
17522
+ emitAuditComment(view, { kind: "run", paragraph: owningParagraph, run: anchorRun }, {
17523
+ body: `[docx-cli] section break removed (${opts.locator})`,
17524
+ author: resolveAuthor(opts.authorFlag),
17525
+ date: resolveDate()
17526
+ });
17527
+ }
17528
+ await saveDocView(view, opts.outputPath);
17529
+ return emitDeleteAck(opts);
17530
+ }
17531
+ async function commitBlockDelete(view, blockRef, opts) {
16719
17532
  const targetIndex = blockRef.parent.indexOf(blockRef.node);
16720
17533
  if (targetIndex === -1) {
16721
17534
  return fail("BLOCK_NOT_FOUND", "Block reference is stale (parent does not contain it)");
16722
17535
  }
16723
- const outputPath = parsed.values.output;
16724
- if (parsed.values["dry-run"]) {
16725
- await respond({
16726
- ok: true,
16727
- operation: "delete",
16728
- dryRun: true,
16729
- path,
16730
- locator: at,
16731
- ...outputPath ? { output: outputPath } : {}
16732
- });
16733
- return EXIT.OK;
16734
- }
17536
+ if (opts.dryRun)
17537
+ return respondDryRun(opts);
16735
17538
  if (isTrackChangesEnabled(view)) {
16736
17539
  if (blockRef.node.tag !== "w:p") {
16737
17540
  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.");
16738
17541
  }
16739
- applyTrackedDeletion(view, blockRef.node, parsed.values.author);
17542
+ applyTrackedDeletion(view, blockRef.node, opts.authorFlag);
16740
17543
  } else {
16741
17544
  blockRef.parent.splice(targetIndex, 1);
16742
17545
  }
16743
- await saveDocView(view, outputPath);
17546
+ await saveDocView(view, opts.outputPath);
17547
+ return emitDeleteAck(opts);
17548
+ }
17549
+ async function respondDryRun(opts) {
16744
17550
  await respond({
16745
17551
  ok: true,
16746
17552
  operation: "delete",
16747
- path: outputPath ?? path,
16748
- locator: at
17553
+ dryRun: true,
17554
+ path: opts.filePath,
17555
+ locator: opts.locator,
17556
+ ...opts.outputPath ? { output: opts.outputPath } : {}
17557
+ });
17558
+ return EXIT.OK;
17559
+ }
17560
+ async function emitDeleteAck(opts) {
17561
+ await respondAck({
17562
+ ok: true,
17563
+ operation: "delete",
17564
+ path: opts.outputPath ?? opts.filePath,
17565
+ locator: opts.locator
16749
17566
  });
16750
17567
  return EXIT.OK;
16751
17568
  }
17569
+ function findBodyChildren(view) {
17570
+ const root = XmlNode2.findRoot(view.documentTree, "w:document");
17571
+ if (!root)
17572
+ return null;
17573
+ const body = root.findChild("w:body");
17574
+ if (!body)
17575
+ return null;
17576
+ return body.children;
17577
+ }
16752
17578
  function applyTrackedDeletion(view, paragraph, authorFlag) {
16753
17579
  const allocator = createRevisionAllocator(view);
16754
17580
  const baseMeta = { author: resolveAuthor(authorFlag), date: resolveDate() };
@@ -16786,21 +17612,47 @@ Usage:
16786
17612
  docx delete FILE [options]
16787
17613
 
16788
17614
  Locator (required):
16789
- --at LOCATOR Block to remove (e.g., p3, t0)
17615
+ --at LOCATOR Block to remove
17616
+ pN paragraph (whole block, with all its runs)
17617
+ tN table (entire table)
17618
+ sN inline section break \u2014 strips the <w:sectPr> from
17619
+ its owning paragraph (the paragraph itself stays);
17620
+ rejects the trailing section break (mandatory in OOXML)
16790
17621
 
16791
17622
  --author NAME Author for tracked changes (default: $DOCX_AUTHOR)
16792
17623
  -o, --output PATH Write to PATH instead of overwriting FILE
16793
17624
  --dry-run Print what would be removed; do not write the file
17625
+ -v, --verbose Print the success ack JSON (default: silent on success)
16794
17626
  -h, --help Show this help
16795
17627
 
17628
+ Tracked behavior:
17629
+ When tracking is on, paragraph deletion wraps runs in <w:del> and marks
17630
+ the paragraph mark as deleted (accept removes the paragraph by merging it
17631
+ forward). Section deletion under tracking emits a [docx-cli] audit comment
17632
+ on the owning paragraph if it has runs to anchor on; otherwise (sentinel
17633
+ paragraphs from "insert --section" have no runs) the mutation is silent.
17634
+ delete --at tN under tracking is rejected (tracked table-row deletion is
17635
+ not supported).
17636
+
16796
17637
  Examples:
16797
17638
  docx delete doc.docx --at p3
16798
17639
  docx delete doc.docx --at t0
16799
- `;
17640
+ docx delete doc.docx --at s2
17641
+ `, OPTION_SPEC;
16800
17642
  var init_delete2 = __esm(() => {
16801
17643
  init_core();
17644
+ init_parser();
17645
+ init_helpers();
16802
17646
  init_respond();
16803
17647
  init_jsx_dev_runtime();
17648
+ OPTION_SPEC = {
17649
+ at: { type: "string" },
17650
+ author: { type: "string" },
17651
+ output: { type: "string", short: "o" },
17652
+ "dry-run": { type: "boolean" },
17653
+ verbose: { type: "boolean", short: "v" },
17654
+ help: { type: "boolean", short: "h" }
17655
+ };
16804
17656
  });
16805
17657
 
16806
17658
  // src/cli/insert/emit.tsx
@@ -16920,32 +17772,399 @@ var init_emit2 = __esm(() => {
16920
17772
  ];
16921
17773
  });
16922
17774
 
16923
- // src/cli/edit/index.tsx
16924
- var exports_edit = {};
16925
- __export(exports_edit, {
16926
- run: () => run9
16927
- });
16928
- import { parseArgs as parseArgs8 } from "util";
16929
- async function run9(args) {
17775
+ // src/cli/edit/preserve-formatting.tsx
17776
+ function extractOldTokens(paragraph) {
17777
+ let fullText = "";
17778
+ const charToRpr = [];
17779
+ function walk(node) {
17780
+ for (const child of node.children) {
17781
+ if (child.tag === "w:r") {
17782
+ const segText = collectRunText(child);
17783
+ if (segText.length === 0)
17784
+ continue;
17785
+ const rPr = child.findChild("w:rPr") ?? null;
17786
+ fullText += segText;
17787
+ for (let index = 0;index < segText.length; index++) {
17788
+ charToRpr.push(rPr);
17789
+ }
17790
+ continue;
17791
+ }
17792
+ if (isAcceptedViewVisibleWrapper(child.tag)) {
17793
+ walk(child);
17794
+ }
17795
+ }
17796
+ }
17797
+ walk(paragraph);
17798
+ const tokenTexts = tokenize(fullText);
17799
+ const tokens = [];
17800
+ let offset = 0;
17801
+ for (const text of tokenTexts) {
17802
+ const rPrSource = charToRpr[offset] ?? null;
17803
+ tokens.push({
17804
+ text,
17805
+ rPr: rPrSource ? rPrSource.clone() : null
17806
+ });
17807
+ offset += text.length;
17808
+ }
17809
+ return tokens;
17810
+ }
17811
+ function isAcceptedViewVisibleWrapper(tag) {
17812
+ if (!isRunBearingWrapper(tag))
17813
+ return false;
17814
+ return tag !== "w:del" && tag !== "w:moveFrom";
17815
+ }
17816
+ function collectRunText(run9) {
17817
+ let out = "";
17818
+ for (const child of run9.children) {
17819
+ if (child.tag === "w:t")
17820
+ out += child.collectText();
17821
+ }
17822
+ return out;
17823
+ }
17824
+ function tokenize(text) {
17825
+ if (text.length === 0)
17826
+ return [];
17827
+ return text.match(/\S+|\s+/g) ?? [];
17828
+ }
17829
+ function diffTokens(oldTokens, newTokens) {
17830
+ const m = oldTokens.length;
17831
+ const n = newTokens.length;
17832
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
17833
+ for (let i2 = 1;i2 <= m; i2++) {
17834
+ for (let j2 = 1;j2 <= n; j2++) {
17835
+ const oldText = oldTokens[i2 - 1]?.text;
17836
+ const newText = newTokens[j2 - 1];
17837
+ const left = dp[i2 - 1] ?? [];
17838
+ const here = dp[i2] ?? [];
17839
+ if (oldText === newText) {
17840
+ here[j2] = (left[j2 - 1] ?? 0) + 1;
17841
+ } else {
17842
+ here[j2] = Math.max(left[j2] ?? 0, here[j2 - 1] ?? 0);
17843
+ }
17844
+ }
17845
+ }
17846
+ const ops = [];
17847
+ let i = m;
17848
+ let j = n;
17849
+ while (i > 0 || j > 0) {
17850
+ const oldToken = i > 0 ? oldTokens[i - 1] : undefined;
17851
+ const newText = j > 0 ? newTokens[j - 1] : undefined;
17852
+ if (i > 0 && j > 0 && oldToken !== undefined && oldToken.text === newText) {
17853
+ ops.unshift({ kind: "keep", old: oldToken });
17854
+ i--;
17855
+ j--;
17856
+ continue;
17857
+ }
17858
+ const upScore = i > 0 ? dp[i - 1]?.[j] ?? 0 : -1;
17859
+ const leftScore = j > 0 ? dp[i]?.[j - 1] ?? 0 : -1;
17860
+ if (j > 0 && newText !== undefined && (i === 0 || leftScore >= upScore)) {
17861
+ ops.unshift({ kind: "insert", text: newText });
17862
+ j--;
17863
+ continue;
17864
+ }
17865
+ if (i > 0 && oldToken !== undefined) {
17866
+ ops.unshift({ kind: "delete", old: oldToken });
17867
+ i--;
17868
+ continue;
17869
+ }
17870
+ break;
17871
+ }
17872
+ return consolidateEditGroups(demoteOrphanWhitespaceKeeps(ops));
17873
+ }
17874
+ function demoteOrphanWhitespaceKeeps(ops) {
17875
+ const out = [];
17876
+ for (let index = 0;index < ops.length; index++) {
17877
+ const op = ops[index];
17878
+ if (!op)
17879
+ continue;
17880
+ if (op.kind !== "keep") {
17881
+ out.push(op);
17882
+ continue;
17883
+ }
17884
+ if (!isWhitespaceOnly(op.old.text)) {
17885
+ out.push(op);
17886
+ continue;
17887
+ }
17888
+ const prev = ops[index - 1];
17889
+ const next = ops[index + 1];
17890
+ const prevIsEdit = prev !== undefined && prev.kind !== "keep";
17891
+ const nextIsEdit = next !== undefined && next.kind !== "keep";
17892
+ if (!prevIsEdit && !nextIsEdit) {
17893
+ out.push(op);
17894
+ continue;
17895
+ }
17896
+ out.push({ kind: "delete", old: op.old });
17897
+ out.push({ kind: "insert", text: op.old.text });
17898
+ }
17899
+ return out;
17900
+ }
17901
+ function consolidateEditGroups(ops) {
17902
+ const out = [];
17903
+ let cursor = 0;
17904
+ while (cursor < ops.length) {
17905
+ const op = ops[cursor];
17906
+ if (!op) {
17907
+ cursor++;
17908
+ continue;
17909
+ }
17910
+ if (op.kind === "keep") {
17911
+ out.push(op);
17912
+ cursor++;
17913
+ continue;
17914
+ }
17915
+ const groupEnd = (() => {
17916
+ let end = cursor;
17917
+ while (end < ops.length) {
17918
+ const candidate = ops[end];
17919
+ if (!candidate || candidate.kind === "keep")
17920
+ break;
17921
+ end++;
17922
+ }
17923
+ return end;
17924
+ })();
17925
+ const deletes = [];
17926
+ const inserts = [];
17927
+ for (let index = cursor;index < groupEnd; index++) {
17928
+ const groupOp = ops[index];
17929
+ if (!groupOp)
17930
+ continue;
17931
+ if (groupOp.kind === "delete")
17932
+ deletes.push(groupOp);
17933
+ else if (groupOp.kind === "insert")
17934
+ inserts.push(groupOp);
17935
+ }
17936
+ out.push(...deletes, ...inserts);
17937
+ cursor = groupEnd;
17938
+ }
17939
+ return out;
17940
+ }
17941
+ function isWhitespaceOnly(text) {
17942
+ return text.length > 0 && /^\s+$/.test(text);
17943
+ }
17944
+ function buildUntrackedRuns(ops) {
17945
+ const flow = [];
17946
+ for (let index = 0;index < ops.length; index++) {
17947
+ const op = ops[index];
17948
+ if (!op)
17949
+ continue;
17950
+ if (op.kind === "delete")
17951
+ continue;
17952
+ if (op.kind === "keep") {
17953
+ flow.push({ text: op.old.text, rPr: cloneOrNull(op.old.rPr) });
17954
+ continue;
17955
+ }
17956
+ const inherited = inheritedRprForInsert(ops, index);
17957
+ flow.push({ text: op.text, rPr: cloneOrNull(inherited) });
17958
+ }
17959
+ return groupAndEmitPlainRuns(flow);
17960
+ }
17961
+ function inheritedRprForInsert(ops, index) {
17962
+ let groupStart = index;
17963
+ while (groupStart > 0) {
17964
+ const prev = ops[groupStart - 1];
17965
+ if (!prev || prev.kind === "keep")
17966
+ break;
17967
+ groupStart--;
17968
+ }
17969
+ let groupEnd = index;
17970
+ while (groupEnd < ops.length - 1) {
17971
+ const next = ops[groupEnd + 1];
17972
+ if (!next || next.kind === "keep")
17973
+ break;
17974
+ groupEnd++;
17975
+ }
17976
+ const deletes = [];
17977
+ let insertOrdinal = 0;
17978
+ let myInsertOrdinal = -1;
17979
+ for (let cursor = groupStart;cursor <= groupEnd; cursor++) {
17980
+ const op = ops[cursor];
17981
+ if (!op)
17982
+ continue;
17983
+ if (op.kind === "delete") {
17984
+ deletes.push(op.old.rPr);
17985
+ continue;
17986
+ }
17987
+ if (op.kind === "insert") {
17988
+ if (cursor === index)
17989
+ myInsertOrdinal = insertOrdinal;
17990
+ insertOrdinal++;
17991
+ }
17992
+ }
17993
+ if (deletes.length > 0 && myInsertOrdinal >= 0) {
17994
+ const paired = myInsertOrdinal < deletes.length ? deletes[myInsertOrdinal] : deletes[deletes.length - 1];
17995
+ if (paired !== undefined)
17996
+ return paired;
17997
+ }
17998
+ for (let cursor = groupStart - 1;cursor >= 0; cursor--) {
17999
+ const op = ops[cursor];
18000
+ if (op?.kind === "keep")
18001
+ return op.old.rPr;
18002
+ }
18003
+ for (let cursor = groupEnd + 1;cursor < ops.length; cursor++) {
18004
+ const op = ops[cursor];
18005
+ if (op?.kind === "keep")
18006
+ return op.old.rPr;
18007
+ }
18008
+ return null;
18009
+ }
18010
+ function buildTrackedRuns(ops, mintMeta) {
18011
+ const grouping = [];
18012
+ for (let index = 0;index < ops.length; index++) {
18013
+ const op = ops[index];
18014
+ if (!op)
18015
+ continue;
18016
+ if (op.kind === "keep") {
18017
+ pushOpTo(grouping, "keep", {
18018
+ text: op.old.text,
18019
+ rPr: cloneOrNull(op.old.rPr)
18020
+ });
18021
+ continue;
18022
+ }
18023
+ if (op.kind === "insert") {
18024
+ const inherited = inheritedRprForInsert(ops, index);
18025
+ pushOpTo(grouping, "insert", {
18026
+ text: op.text,
18027
+ rPr: cloneOrNull(inherited)
18028
+ });
18029
+ continue;
18030
+ }
18031
+ pushOpTo(grouping, "delete", {
18032
+ text: op.old.text,
18033
+ rPr: cloneOrNull(op.old.rPr)
18034
+ });
18035
+ }
18036
+ const out = [];
18037
+ for (const group of grouping) {
18038
+ const runs = group.kind === "delete" ? groupAndEmitDeletedRuns(group.flow) : groupAndEmitPlainRuns(group.flow);
18039
+ if (runs.length === 0)
18040
+ continue;
18041
+ if (group.kind === "keep") {
18042
+ out.push(...runs);
18043
+ } else if (group.kind === "insert") {
18044
+ out.push(/* @__PURE__ */ jsxDEV(Ins, {
18045
+ meta: mintMeta(),
18046
+ children: runs
18047
+ }, undefined, false, undefined, this));
18048
+ } else {
18049
+ out.push(/* @__PURE__ */ jsxDEV(Del, {
18050
+ meta: mintMeta(),
18051
+ children: runs
18052
+ }, undefined, false, undefined, this));
18053
+ }
18054
+ }
18055
+ return out;
18056
+ }
18057
+ function pushOpTo(grouping, kind, entry) {
18058
+ const last = grouping[grouping.length - 1];
18059
+ if (last && last.kind === kind) {
18060
+ last.flow.push(entry);
18061
+ return;
18062
+ }
18063
+ grouping.push({ kind, flow: [entry] });
18064
+ }
18065
+ function groupAndEmitPlainRuns(flow) {
18066
+ const out = [];
18067
+ let current = null;
18068
+ for (const entry of flow) {
18069
+ if (entry.text.length === 0)
18070
+ continue;
18071
+ if (current && rPrEqual(current.rPr, entry.rPr)) {
18072
+ current.text += entry.text;
18073
+ continue;
18074
+ }
18075
+ if (current)
18076
+ out.push(emitPlainRun(current.text, current.rPr));
18077
+ current = { text: entry.text, rPr: entry.rPr };
18078
+ }
18079
+ if (current)
18080
+ out.push(emitPlainRun(current.text, current.rPr));
18081
+ return out;
18082
+ }
18083
+ function groupAndEmitDeletedRuns(flow) {
18084
+ const out = [];
18085
+ let current = null;
18086
+ for (const entry of flow) {
18087
+ if (entry.text.length === 0)
18088
+ continue;
18089
+ if (current && rPrEqual(current.rPr, entry.rPr)) {
18090
+ current.text += entry.text;
18091
+ continue;
18092
+ }
18093
+ if (current)
18094
+ out.push(emitDeletedRun(current.text, current.rPr));
18095
+ current = { text: entry.text, rPr: entry.rPr };
18096
+ }
18097
+ if (current)
18098
+ out.push(emitDeletedRun(current.text, current.rPr));
18099
+ return out;
18100
+ }
18101
+ function emitPlainRun(text, rPr) {
18102
+ return /* @__PURE__ */ jsxDEV(w.r, {
18103
+ children: [
18104
+ rPr,
18105
+ /* @__PURE__ */ jsxDEV(w.t, {
18106
+ "xml:space": "preserve",
18107
+ children: text
18108
+ }, undefined, false, undefined, this)
18109
+ ]
18110
+ }, undefined, true, undefined, this);
18111
+ }
18112
+ function emitDeletedRun(text, rPr) {
18113
+ return /* @__PURE__ */ jsxDEV(w.r, {
18114
+ children: [
18115
+ rPr,
18116
+ /* @__PURE__ */ jsxDEV(w.delText, {
18117
+ "xml:space": "preserve",
18118
+ children: text
18119
+ }, undefined, false, undefined, this)
18120
+ ]
18121
+ }, undefined, true, undefined, this);
18122
+ }
18123
+ function cloneOrNull(node) {
18124
+ return node ? node.clone() : null;
18125
+ }
18126
+ function rPrEqual(a2, b) {
18127
+ if (a2 === null && b === null)
18128
+ return true;
18129
+ if (a2 === null || b === null)
18130
+ return false;
18131
+ return XmlNode2.serialize([a2]) === XmlNode2.serialize([b]);
18132
+ }
18133
+ var init_preserve_formatting = __esm(() => {
18134
+ init_core();
18135
+ init_jsx();
18136
+ init_parser();
18137
+ init_jsx_dev_runtime();
18138
+ });
18139
+
18140
+ // src/cli/edit/index.tsx
18141
+ var exports_edit = {};
18142
+ __export(exports_edit, {
18143
+ run: () => run9
18144
+ });
18145
+ import { parseArgs as parseArgs8 } from "util";
18146
+ async function run9(args) {
18147
+ const opts = await parseAndValidateOptions2(args);
18148
+ if (typeof opts === "number")
18149
+ return opts;
18150
+ const view = await openOrFail(opts.filePath);
18151
+ if (typeof view === "number")
18152
+ return view;
18153
+ const blockRef = await resolveBlockOrFail(view, opts.locator);
18154
+ if (typeof blockRef === "number")
18155
+ return blockRef;
18156
+ if (opts.spec.kind === "section") {
18157
+ return commitSectionPropertyEdit(view, blockRef, opts.spec, opts);
18158
+ }
18159
+ return commitParagraphReplacement(view, blockRef, opts.spec, opts);
18160
+ }
18161
+ async function parseAndValidateOptions2(args) {
16930
18162
  let parsed;
16931
18163
  try {
16932
18164
  parsed = parseArgs8({
16933
18165
  args,
16934
18166
  allowPositionals: true,
16935
- options: {
16936
- at: { type: "string" },
16937
- text: { type: "string" },
16938
- runs: { type: "string" },
16939
- style: { type: "string" },
16940
- alignment: { type: "string" },
16941
- color: { type: "string" },
16942
- bold: { type: "boolean" },
16943
- italic: { type: "boolean" },
16944
- author: { type: "string" },
16945
- output: { type: "string", short: "o" },
16946
- "dry-run": { type: "boolean" },
16947
- help: { type: "boolean", short: "h" }
16948
- }
18167
+ options: OPTION_SPEC2
16949
18168
  });
16950
18169
  } catch (parseError) {
16951
18170
  const message = parseError instanceof Error ? parseError.message : String(parseError);
@@ -16955,90 +18174,255 @@ async function run9(args) {
16955
18174
  await writeStdout(HELP9);
16956
18175
  return EXIT.OK;
16957
18176
  }
16958
- const path = parsed.positionals[0];
16959
- if (!path)
18177
+ setVerboseAck(Boolean(parsed.values.verbose));
18178
+ const filePath = parsed.positionals[0];
18179
+ if (!filePath)
16960
18180
  return fail("USAGE", "Missing FILE argument", HELP9);
16961
- const at = parsed.values.at;
16962
- if (!at)
18181
+ const locator = parsed.values.at;
18182
+ if (!locator)
16963
18183
  return fail("USAGE", "Missing --at LOCATOR", HELP9);
16964
- const text = parsed.values.text;
16965
- const runsJson = parsed.values.runs;
18184
+ const paragraphOptions = await parseParagraphOptions(parsed.values);
18185
+ if (typeof paragraphOptions === "number")
18186
+ return paragraphOptions;
18187
+ const isSectionLocator = /^s\d+$/.test(locator);
18188
+ const spec = isSectionLocator ? await validateSectionEdit(parsed.values) : await validateParagraphEdit(parsed.values, paragraphOptions);
18189
+ if (typeof spec === "number")
18190
+ return spec;
18191
+ return {
18192
+ filePath,
18193
+ locator,
18194
+ spec,
18195
+ authorFlag: parsed.values.author,
18196
+ outputPath: parsed.values.output,
18197
+ dryRun: Boolean(parsed.values["dry-run"]),
18198
+ noFormatting: Boolean(parsed.values["no-formatting"])
18199
+ };
18200
+ }
18201
+ async function validateSectionEdit(values) {
18202
+ if (values.text !== undefined || values.runs !== undefined) {
18203
+ return fail("USAGE", "Section locators (sN) take --columns and --type, not --text/--runs", HELP9);
18204
+ }
18205
+ if (values.columns === undefined && values.type === undefined) {
18206
+ return fail("USAGE", "Section edit requires --columns and/or --type", HELP9);
18207
+ }
18208
+ const sectionFlags = await parseSectionFlags(values);
18209
+ if (typeof sectionFlags === "number")
18210
+ return sectionFlags;
18211
+ return { kind: "section", ...sectionFlags };
18212
+ }
18213
+ async function validateParagraphEdit(values, paragraphOptions) {
18214
+ if (values.columns !== undefined || values.type !== undefined) {
18215
+ return fail("USAGE", "--columns and --type require a section locator (sN)", HELP9);
18216
+ }
18217
+ const text = values.text;
18218
+ const runsJson = values.runs;
16966
18219
  if (!text && !runsJson) {
16967
18220
  return fail("USAGE", "Missing content: pass --text or --runs", HELP9);
16968
18221
  }
16969
18222
  if (text && runsJson) {
16970
18223
  return fail("USAGE", "Pass either --text or --runs, not both", HELP9);
16971
18224
  }
16972
- const paragraphOptions = {};
16973
- const styleValue = parsed.values.style;
18225
+ if (text !== undefined) {
18226
+ return {
18227
+ kind: "text",
18228
+ text,
18229
+ format: {
18230
+ color: values.color,
18231
+ bold: values.bold,
18232
+ italic: values.italic
18233
+ },
18234
+ paragraphOptions
18235
+ };
18236
+ }
18237
+ const runs = await parseRunsArg(runsJson);
18238
+ if (typeof runs === "number")
18239
+ return runs;
18240
+ return { kind: "runs", runs, paragraphOptions };
18241
+ }
18242
+ async function parseSectionFlags(values) {
18243
+ const out = {};
18244
+ const columnsRaw = values.columns;
18245
+ if (columnsRaw !== undefined) {
18246
+ const columns = Number.parseInt(columnsRaw, 10);
18247
+ if (!Number.isFinite(columns) || columns <= 0) {
18248
+ return fail("USAGE", `--columns must be a positive integer, got "${columnsRaw}"`);
18249
+ }
18250
+ out.columns = columns;
18251
+ }
18252
+ const sectionTypeRaw = values.type;
18253
+ if (sectionTypeRaw !== undefined) {
18254
+ if (!isSectionType(sectionTypeRaw)) {
18255
+ return fail("USAGE", `Invalid --type: ${sectionTypeRaw}`, "Valid values: continuous, nextPage, evenPage, oddPage, nextColumn");
18256
+ }
18257
+ out.sectionType = sectionTypeRaw;
18258
+ }
18259
+ return out;
18260
+ }
18261
+ async function parseRunsArg(json) {
18262
+ let parsed;
18263
+ try {
18264
+ parsed = JSON.parse(json);
18265
+ } catch (jsonError) {
18266
+ const message = jsonError instanceof Error ? jsonError.message : String(jsonError);
18267
+ return fail("USAGE", `Invalid --runs JSON: ${message}`);
18268
+ }
18269
+ if (!Array.isArray(parsed)) {
18270
+ return fail("USAGE", "--runs must be a JSON array of Run objects");
18271
+ }
18272
+ return parsed;
18273
+ }
18274
+ async function parseParagraphOptions(values) {
18275
+ const out = {};
18276
+ const styleValue = values.style;
16974
18277
  if (styleValue)
16975
- paragraphOptions.style = styleValue;
16976
- const alignmentValue = parsed.values.alignment;
18278
+ out.style = styleValue;
18279
+ const alignmentValue = values.alignment;
16977
18280
  if (alignmentValue) {
16978
18281
  if (alignmentValue !== "left" && alignmentValue !== "center" && alignmentValue !== "right" && alignmentValue !== "justify") {
16979
18282
  return fail("USAGE", `Invalid --alignment: ${alignmentValue}`, "Valid values: left, center, right, justify");
16980
18283
  }
16981
- paragraphOptions.alignment = alignmentValue;
18284
+ out.alignment = alignmentValue;
16982
18285
  }
16983
- const view = await openOrFail(path);
16984
- if (typeof view === "number")
16985
- return view;
16986
- const blockRef = await resolveBlockOrFail(view, at);
16987
- if (typeof blockRef === "number")
16988
- return blockRef;
16989
- let paragraphNode;
16990
- if (text !== undefined) {
16991
- const color = parsed.values.color;
16992
- paragraphNode = /* @__PURE__ */ jsxDEV(Paragraph, {
16993
- text,
16994
- ...paragraphOptions,
16995
- ...color ? { color } : {},
16996
- ...parsed.values.bold ? { bold: true } : {},
16997
- ...parsed.values.italic ? { italic: true } : {}
16998
- }, undefined, false, undefined, this);
16999
- } else {
17000
- let runsValue;
17001
- try {
17002
- runsValue = JSON.parse(runsJson);
17003
- } catch (jsonError) {
17004
- const message = jsonError instanceof Error ? jsonError.message : String(jsonError);
17005
- return fail("USAGE", `Invalid --runs JSON: ${message}`);
17006
- }
17007
- if (!Array.isArray(runsValue)) {
17008
- return fail("USAGE", "--runs must be a JSON array of Run objects");
17009
- }
17010
- paragraphNode = /* @__PURE__ */ jsxDEV(Paragraph, {
17011
- runs: runsValue,
17012
- ...paragraphOptions
17013
- }, undefined, false, undefined, this);
18286
+ return out;
18287
+ }
18288
+ async function commitSectionPropertyEdit(view, blockRef, spec, opts) {
18289
+ if (blockRef.node.tag !== "w:sectPr") {
18290
+ return fail("BLOCK_NOT_FOUND", `Locator ${opts.locator} did not resolve to a section break`);
17014
18291
  }
18292
+ if (opts.dryRun)
18293
+ return respondDryRun2(opts);
18294
+ if (isTrackChangesEnabled(view)) {
18295
+ const allocator = createRevisionAllocator(view);
18296
+ const meta = {
18297
+ author: resolveAuthor(opts.authorFlag),
18298
+ date: resolveDate(),
18299
+ revisionId: allocator.next()
18300
+ };
18301
+ wrapSectPrChange(blockRef.node, meta);
18302
+ }
18303
+ applyColumns(blockRef.node, spec.columns);
18304
+ applySectionType(blockRef.node, spec.sectionType);
18305
+ await saveDocView(view, opts.outputPath);
18306
+ return emitEditAck(opts);
18307
+ }
18308
+ async function commitParagraphReplacement(view, blockRef, spec, opts) {
17015
18309
  const targetIndex = blockRef.parent.indexOf(blockRef.node);
17016
18310
  if (targetIndex === -1) {
17017
18311
  return fail("BLOCK_NOT_FOUND", "Block reference is stale (parent does not contain it)");
17018
18312
  }
17019
- const outputPath = parsed.values.output;
17020
- if (parsed.values["dry-run"]) {
17021
- await respond({
17022
- ok: true,
17023
- operation: "edit",
17024
- dryRun: true,
17025
- path,
17026
- locator: at,
17027
- ...outputPath ? { output: outputPath } : {}
17028
- });
17029
- return EXIT.OK;
18313
+ if (opts.dryRun)
18314
+ return respondDryRun2(opts);
18315
+ const tracked = isTrackChangesEnabled(view);
18316
+ if (canPreserveFormatting(spec, opts)) {
18317
+ applyFormattingPreservingEdit(view, blockRef.node, spec, opts.authorFlag, tracked);
18318
+ await saveDocView(view, opts.outputPath);
18319
+ return emitEditAck(opts);
18320
+ }
18321
+ const newParagraph = buildReplacementParagraph(spec);
18322
+ if (tracked) {
18323
+ applyTrackedEdit(view, blockRef.node, newParagraph, opts.authorFlag);
18324
+ } else {
18325
+ blockRef.parent.splice(targetIndex, 1, newParagraph);
17030
18326
  }
17031
- if (isTrackChangesEnabled(view)) {
17032
- applyTrackedEdit(view, blockRef.node, paragraphNode, parsed.values.author);
18327
+ await saveDocView(view, opts.outputPath);
18328
+ return emitEditAck(opts);
18329
+ }
18330
+ function canPreserveFormatting(spec, opts) {
18331
+ if (opts.noFormatting)
18332
+ return false;
18333
+ if (spec.kind !== "text")
18334
+ return false;
18335
+ const format = spec.format;
18336
+ if (format.color || format.bold || format.italic)
18337
+ return false;
18338
+ return true;
18339
+ }
18340
+ function applyFormattingPreservingEdit(view, existingParagraph, spec, authorFlag, tracked) {
18341
+ const oldTokens = extractOldTokens(existingParagraph);
18342
+ const newTokens = tokenize(spec.text);
18343
+ const ops = diffTokens(oldTokens, newTokens);
18344
+ let runChildren;
18345
+ if (tracked) {
18346
+ const allocator = createRevisionAllocator(view);
18347
+ const baseMeta = { author: resolveAuthor(authorFlag), date: resolveDate() };
18348
+ const mintMeta = () => ({
18349
+ ...baseMeta,
18350
+ revisionId: allocator.next()
18351
+ });
18352
+ runChildren = buildTrackedRuns(ops, mintMeta);
17033
18353
  } else {
17034
- blockRef.parent.splice(targetIndex, 1, paragraphNode);
18354
+ runChildren = buildUntrackedRuns(ops);
17035
18355
  }
17036
- await saveDocView(view, outputPath);
18356
+ const rebuilt = [];
18357
+ for (const child of existingParagraph.children) {
18358
+ if (child.tag === "w:r")
18359
+ continue;
18360
+ if (isRunBearingWrapper(child.tag))
18361
+ continue;
18362
+ rebuilt.push(child);
18363
+ }
18364
+ applyParagraphOptionsInPlace(rebuilt, spec.paragraphOptions);
18365
+ rebuilt.push(...runChildren);
18366
+ existingParagraph.children = rebuilt;
18367
+ }
18368
+ function applyParagraphOptionsInPlace(rebuilt, options) {
18369
+ if (!options.style && !options.alignment)
18370
+ return;
18371
+ let pPr = rebuilt.find((child) => child.tag === "w:pPr");
18372
+ if (!pPr) {
18373
+ pPr = new XmlNode2("w:pPr");
18374
+ rebuilt.unshift(pPr);
18375
+ }
18376
+ if (options.style) {
18377
+ const existingStyle = pPr.findChild("w:pStyle");
18378
+ if (existingStyle) {
18379
+ existingStyle.setAttribute("w:val", options.style);
18380
+ } else {
18381
+ const styleNode = new XmlNode2("w:pStyle", { "w:val": options.style });
18382
+ pPr.children.unshift(styleNode);
18383
+ }
18384
+ }
18385
+ if (options.alignment) {
18386
+ const existingJc = pPr.findChild("w:jc");
18387
+ if (existingJc) {
18388
+ existingJc.setAttribute("w:val", options.alignment);
18389
+ } else {
18390
+ pPr.children.push(new XmlNode2("w:jc", { "w:val": options.alignment }));
18391
+ }
18392
+ }
18393
+ }
18394
+ function buildReplacementParagraph(spec) {
18395
+ if (spec.kind === "text") {
18396
+ return /* @__PURE__ */ jsxDEV(Paragraph, {
18397
+ text: spec.text,
18398
+ ...spec.paragraphOptions,
18399
+ ...spec.format.color ? { color: spec.format.color } : {},
18400
+ ...spec.format.bold ? { bold: true } : {},
18401
+ ...spec.format.italic ? { italic: true } : {}
18402
+ }, undefined, false, undefined, this);
18403
+ }
18404
+ return /* @__PURE__ */ jsxDEV(Paragraph, {
18405
+ runs: spec.runs,
18406
+ ...spec.paragraphOptions
18407
+ }, undefined, false, undefined, this);
18408
+ }
18409
+ async function respondDryRun2(opts) {
17037
18410
  await respond({
17038
18411
  ok: true,
17039
18412
  operation: "edit",
17040
- path: outputPath ?? path,
17041
- locator: at
18413
+ dryRun: true,
18414
+ path: opts.filePath,
18415
+ locator: opts.locator,
18416
+ ...opts.outputPath ? { output: opts.outputPath } : {}
18417
+ });
18418
+ return EXIT.OK;
18419
+ }
18420
+ async function emitEditAck(opts) {
18421
+ await respondAck({
18422
+ ok: true,
18423
+ operation: "edit",
18424
+ path: opts.outputPath ?? opts.filePath,
18425
+ locator: opts.locator
17042
18426
  });
17043
18427
  return EXIT.OK;
17044
18428
  }
@@ -17052,10 +18436,15 @@ function applyTrackedEdit(view, existingParagraph, newParagraph, authorFlag) {
17052
18436
  const oldRuns = [];
17053
18437
  const oldNonRuns = [];
17054
18438
  for (const child of existingParagraph.children) {
17055
- if (child.tag === "w:r")
18439
+ if (child.tag === "w:r") {
17056
18440
  oldRuns.push(child);
17057
- else
17058
- oldNonRuns.push(child);
18441
+ continue;
18442
+ }
18443
+ if (isRunBearingWrapper(child.tag)) {
18444
+ collectRunsFromWrapper(child, oldRuns);
18445
+ continue;
18446
+ }
18447
+ oldNonRuns.push(child);
17059
18448
  }
17060
18449
  let newPPr = null;
17061
18450
  const newRuns = [];
@@ -17090,145 +18479,91 @@ function applyTrackedEdit(view, existingParagraph, newParagraph, authorFlag) {
17090
18479
  }
17091
18480
  existingParagraph.children = rebuilt;
17092
18481
  }
17093
- var HELP9 = `docx edit \u2014 replace a paragraph at a locator
18482
+ function collectRunsFromWrapper(wrapper, out) {
18483
+ for (const child of wrapper.children) {
18484
+ if (child.tag === "w:r") {
18485
+ out.push(child);
18486
+ continue;
18487
+ }
18488
+ if (isRunBearingWrapper(child.tag)) {
18489
+ collectRunsFromWrapper(child, out);
18490
+ }
18491
+ }
18492
+ }
18493
+ var HELP9 = `docx edit \u2014 replace a paragraph or modify a section at a locator
17094
18494
 
17095
18495
  Usage:
17096
18496
  docx edit FILE [options]
17097
18497
 
17098
18498
  Locator (required):
17099
- --at LOCATOR Block to replace (e.g., p3)
18499
+ --at LOCATOR Block to edit (paragraph pN or section sN)
17100
18500
 
17101
- Content (one required):
18501
+ Paragraph content (one required for paragraph locators):
17102
18502
  --text TEXT Replace with a single-run paragraph
17103
18503
  --runs JSON Replace with custom runs (Run[] JSON)
17104
18504
 
17105
- Paragraph options:
17106
- --style NAME Paragraph style (e.g., Heading1)
17107
- --alignment ALIGN left | center | right | justify
17108
-
17109
- Run options (only with --text):
17110
- --color HEX Run color, hex (e.g., 800080 for purple)
17111
- --bold Bold
17112
- --italic Italic
17113
-
17114
- --author NAME Author for tracked changes (default: $DOCX_AUTHOR)
17115
- -o, --output PATH Write to PATH instead of overwriting FILE
17116
- --dry-run Print what would change; do not write the file
17117
- -h, --help Show this help
17118
-
17119
- Examples:
17120
- docx edit doc.docx --at p3 --text "Replaced." --style Heading2
17121
- docx edit doc.docx --at p0 --runs '[{"type":"text","text":"X","bold":true}]'
17122
- `;
17123
- var init_edit = __esm(() => {
17124
- init_core();
17125
- init_emit2();
17126
- init_respond();
17127
- init_jsx_dev_runtime();
17128
- });
17129
-
17130
- // src/core/find/index.ts
17131
- function findTextSpans(doc, query, options = {}) {
17132
- const matcher = options.regex ? regexMatcher(query, options.ignoreCase ?? false) : literalMatcher(query, options.ignoreCase ?? false);
17133
- const out = [];
17134
- collectMatches(doc.blocks, matcher, out);
17135
- return out;
17136
- }
17137
- function literalMatcher(query, ignoreCase) {
17138
- if (query.length === 0) {
17139
- throw new Error("query cannot be empty");
17140
- }
17141
- const needle = ignoreCase ? query.toLowerCase() : query;
17142
- return (paragraphText2) => {
17143
- const haystack = ignoreCase ? paragraphText2.toLowerCase() : paragraphText2;
17144
- const matches = [];
17145
- let cursor = haystack.indexOf(needle);
17146
- while (cursor !== -1) {
17147
- matches.push({
17148
- start: cursor,
17149
- end: cursor + needle.length,
17150
- text: paragraphText2.slice(cursor, cursor + needle.length)
17151
- });
17152
- cursor = haystack.indexOf(needle, cursor + needle.length);
17153
- }
17154
- return matches;
17155
- };
17156
- }
17157
- function regexMatcher(pattern, ignoreCase) {
17158
- const flags = `g${ignoreCase ? "i" : ""}`;
17159
- const regex = new RegExp(pattern, flags);
17160
- return (paragraphText2) => {
17161
- const matches = [];
17162
- regex.lastIndex = 0;
17163
- let result = regex.exec(paragraphText2);
17164
- while (result !== null) {
17165
- const matched = result[0];
17166
- if (matched.length === 0) {
17167
- regex.lastIndex += 1;
17168
- result = regex.exec(paragraphText2);
17169
- continue;
17170
- }
17171
- matches.push({
17172
- start: result.index,
17173
- end: result.index + matched.length,
17174
- text: matched
17175
- });
17176
- result = regex.exec(paragraphText2);
17177
- }
17178
- return matches;
17179
- };
17180
- }
17181
- function collectMatches(blocks, matcher, out) {
17182
- for (const block of blocks) {
17183
- if (block.type === "paragraph") {
17184
- const paragraphText2 = block.runs.map((run10) => run10.type === "text" ? run10.text : "").join("");
17185
- for (const span of matcher(paragraphText2)) {
17186
- const match = {
17187
- blockId: block.id,
17188
- start: span.start,
17189
- end: span.end,
17190
- text: span.text
17191
- };
17192
- const overlaps = trackedChangesOverlapping(block, span.start, span.end);
17193
- if (overlaps.length > 0)
17194
- match.trackedChanges = overlaps;
17195
- out.push(match);
17196
- }
17197
- continue;
17198
- }
17199
- if (block.type === "table") {
17200
- for (const row of block.rows) {
17201
- for (const cell of row.cells) {
17202
- collectMatches(cell.blocks, matcher, out);
17203
- }
17204
- }
17205
- }
17206
- }
17207
- }
17208
- function trackedChangesOverlapping(paragraph, start, end) {
17209
- const seen = new Set;
17210
- const out = [];
17211
- let offset = 0;
17212
- for (const run10 of paragraph.runs) {
17213
- const length = run10.type === "text" ? run10.text.length : 0;
17214
- const runStart = offset;
17215
- const runEnd = offset + length;
17216
- offset = runEnd;
17217
- if (run10.type !== "text")
17218
- continue;
17219
- if (runEnd <= start || runStart >= end)
17220
- continue;
17221
- const change = run10.trackedChange;
17222
- if (!change)
17223
- continue;
17224
- if (seen.has(change.id))
17225
- continue;
17226
- seen.add(change.id);
17227
- out.push(change);
17228
- }
17229
- return out;
17230
- }
17231
-
18505
+ Paragraph options:
18506
+ --style NAME Paragraph style (e.g., Heading1)
18507
+ --alignment ALIGN left | center | right | justify
18508
+
18509
+ Run options (only with --text):
18510
+ --color HEX Run color, hex (e.g., 800080 for purple)
18511
+ --bold Bold
18512
+ --italic Italic
18513
+
18514
+ Section options (for section locators sN):
18515
+ --columns N Number of columns for the targeted section
18516
+ --type T continuous | nextPage | evenPage | oddPage | nextColumn
18517
+
18518
+ Formatting (for --text):
18519
+ By default --text preserves run-level formatting (bold/italic/color/etc.)
18520
+ on words shared between the old and new text via a word-level diff. New
18521
+ words inherit formatting from the nearest unchanged neighbor. Pass
18522
+ --no-formatting to fall back to a single fresh run with no formatting.
18523
+ Passing --color/--bold/--italic also bypasses preservation \u2014 those flags
18524
+ apply uniformly to the new paragraph.
18525
+
18526
+ General options:
18527
+ --author NAME Author for tracked changes (default: $DOCX_AUTHOR)
18528
+ --no-formatting Replace with a single fresh run; do not preserve rPr
18529
+ on unchanged words
18530
+ -o, --output PATH Write to PATH instead of overwriting FILE
18531
+ --dry-run Print what would change; do not write the file
18532
+ -v, --verbose Print the success ack JSON (default: silent on success)
18533
+ -h, --help Show this help
18534
+
18535
+ Examples:
18536
+ docx edit doc.docx --at p3 --text "Replaced." --style Heading2
18537
+ docx edit doc.docx --at p0 --runs '[{"type":"text","text":"X","bold":true}]'
18538
+ docx edit doc.docx --at s0 --columns 2 --type continuous
18539
+ `, OPTION_SPEC2;
18540
+ var init_edit = __esm(() => {
18541
+ init_core();
18542
+ init_parser();
18543
+ init_emit2();
18544
+ init_respond();
18545
+ init_preserve_formatting();
18546
+ init_jsx_dev_runtime();
18547
+ OPTION_SPEC2 = {
18548
+ at: { type: "string" },
18549
+ text: { type: "string" },
18550
+ runs: { type: "string" },
18551
+ columns: { type: "string" },
18552
+ type: { type: "string" },
18553
+ style: { type: "string" },
18554
+ alignment: { type: "string" },
18555
+ color: { type: "string" },
18556
+ bold: { type: "boolean" },
18557
+ italic: { type: "boolean" },
18558
+ author: { type: "string" },
18559
+ "no-formatting": { type: "boolean" },
18560
+ output: { type: "string", short: "o" },
18561
+ "dry-run": { type: "boolean" },
18562
+ verbose: { type: "boolean", short: "v" },
18563
+ help: { type: "boolean", short: "h" }
18564
+ };
18565
+ });
18566
+
17232
18567
  // src/cli/find/index.ts
17233
18568
  var exports_find = {};
17234
18569
  __export(exports_find, {
@@ -17246,6 +18581,9 @@ async function run10(args) {
17246
18581
  "ignore-case": { type: "boolean" },
17247
18582
  all: { type: "boolean" },
17248
18583
  nth: { type: "string" },
18584
+ current: { type: "boolean" },
18585
+ baseline: { type: "boolean" },
18586
+ exact: { type: "boolean" },
17249
18587
  help: { type: "boolean", short: "h" }
17250
18588
  }
17251
18589
  });
@@ -17266,6 +18604,13 @@ async function run10(args) {
17266
18604
  const ignoreCase = Boolean(parsed.values["ignore-case"]);
17267
18605
  const useRegex = Boolean(parsed.values.regex);
17268
18606
  const wantAll = Boolean(parsed.values.all);
18607
+ const wantCurrent = Boolean(parsed.values.current);
18608
+ const wantBaseline = Boolean(parsed.values.baseline);
18609
+ const exact = Boolean(parsed.values.exact);
18610
+ if (wantCurrent && wantBaseline) {
18611
+ return fail("USAGE", "--current and --baseline are mutually exclusive");
18612
+ }
18613
+ const findView = wantCurrent ? "current" : wantBaseline ? "baseline" : "accepted";
17269
18614
  const nthRaw = parsed.values.nth;
17270
18615
  const nth = nthRaw === undefined ? undefined : Number(nthRaw);
17271
18616
  if (nth !== undefined && (!Number.isInteger(nth) || nth < 0)) {
@@ -17274,16 +18619,19 @@ async function run10(args) {
17274
18619
  const view = await openOrFail(path);
17275
18620
  if (typeof view === "number")
17276
18621
  return view;
17277
- let allMatches;
18622
+ let result;
17278
18623
  try {
17279
- allMatches = findTextSpans(view.doc, query, {
18624
+ result = findTextSpans(view.doc, query, {
17280
18625
  regex: useRegex,
17281
- ignoreCase
18626
+ ignoreCase,
18627
+ view: findView,
18628
+ exact
17282
18629
  });
17283
18630
  } catch (matcherError) {
17284
18631
  const message = matcherError instanceof Error ? matcherError.message : String(matcherError);
17285
18632
  return fail("USAGE", `Invalid query: ${message}`);
17286
18633
  }
18634
+ const allMatches = result.matches;
17287
18635
  let selected;
17288
18636
  if (nth !== undefined) {
17289
18637
  const single = allMatches[nth];
@@ -17303,11 +18651,16 @@ async function run10(args) {
17303
18651
  query,
17304
18652
  regex: useRegex,
17305
18653
  ignoreCase,
18654
+ view: findView,
17306
18655
  totalMatches: allMatches.length,
17307
18656
  matches: selected.map((match) => ({
17308
18657
  locator: `${match.blockId}:${match.start}-${match.end}`,
17309
18658
  ...match
17310
- }))
18659
+ })),
18660
+ ...result.normalizedQuery !== undefined ? {
18661
+ normalizedQuery: result.normalizedQuery,
18662
+ normalizationApplied: result.normalizationApplied
18663
+ } : {}
17311
18664
  });
17312
18665
  return EXIT.OK;
17313
18666
  }
@@ -17324,6 +18677,12 @@ Options:
17324
18677
  --ignore-case case-insensitive match
17325
18678
  --all return every match (default: just the first)
17326
18679
  --nth N return only the Nth match (0-indexed)
18680
+ --current search the raw concatenation (both ins and del text)
18681
+ --baseline search the pre-change text (skip ins/moveTo)
18682
+ default: accepted view (skip del/moveFrom) \u2014 matches
18683
+ "docx read --markdown" / "docx wc" / "docx comments add"
18684
+ --exact disable query normalization (no markdown-emphasis stripping,
18685
+ no smart/straight quote or em/en-dash equivalence)
17327
18686
  -h, --help show this help
17328
18687
 
17329
18688
  Within-paragraph matches only \u2014 cross-paragraph ranges aren't supported
@@ -17331,6 +18690,11 @@ yet. Searches the concatenated text of each paragraph in document order,
17331
18690
  including paragraphs nested in table cells (locators look like
17332
18691
  tT:rRcC:pK:S-E for those).
17333
18692
 
18693
+ By default the query is normalized: balanced markdown emphasis around
18694
+ non-whitespace (**X**, __X__, *X*, \`X\`) is stripped; smart quotes match
18695
+ straight quotes; em-dash and en-dash match the hyphen. Pass --exact to
18696
+ match the raw query verbatim. --regex is always verbatim.
18697
+
17334
18698
  Examples:
17335
18699
  docx find doc.docx "fox"
17336
18700
  docx find doc.docx "Action Item:" --all
@@ -17444,6 +18808,7 @@ async function run11(args) {
17444
18808
  author: { type: "string" },
17445
18809
  output: { type: "string", short: "o" },
17446
18810
  "dry-run": { type: "boolean" },
18811
+ verbose: { type: "boolean", short: "v" },
17447
18812
  help: { type: "boolean", short: "h" }
17448
18813
  }
17449
18814
  });
@@ -17455,6 +18820,7 @@ async function run11(args) {
17455
18820
  await writeStdout(HELP11);
17456
18821
  return EXIT.OK;
17457
18822
  }
18823
+ setVerboseAck(Boolean(parsed.values.verbose));
17458
18824
  const path = parsed.positionals[0];
17459
18825
  if (!path)
17460
18826
  return fail("USAGE", "Missing FILE argument", HELP11);
@@ -17518,7 +18884,7 @@ async function run11(args) {
17518
18884
  });
17519
18885
  }
17520
18886
  await saveDocView(view, outputPath);
17521
- await respond({
18887
+ await respondAck({
17522
18888
  ok: true,
17523
18889
  operation: "hyperlinks.add",
17524
18890
  path: outputPath ?? path,
@@ -17543,6 +18909,7 @@ Optional:
17543
18909
  (default: $DOCX_AUTHOR)
17544
18910
  -o, --output PATH Write to PATH instead of overwriting FILE
17545
18911
  --dry-run Print what would change; do not write the file
18912
+ -v, --verbose Print the success ack JSON (default: silent on success)
17546
18913
  -h, --help Show this help
17547
18914
 
17548
18915
  The span must lie inside a single paragraph and must not overlap an existing
@@ -17581,6 +18948,7 @@ async function run12(args) {
17581
18948
  author: { type: "string" },
17582
18949
  output: { type: "string", short: "o" },
17583
18950
  "dry-run": { type: "boolean" },
18951
+ verbose: { type: "boolean", short: "v" },
17584
18952
  help: { type: "boolean", short: "h" }
17585
18953
  }
17586
18954
  });
@@ -17592,6 +18960,7 @@ async function run12(args) {
17592
18960
  await writeStdout(HELP12);
17593
18961
  return EXIT.OK;
17594
18962
  }
18963
+ setVerboseAck(Boolean(parsed.values.verbose));
17595
18964
  const path = parsed.positionals[0];
17596
18965
  if (!path)
17597
18966
  return fail("USAGE", "Missing FILE argument", HELP12);
@@ -17643,7 +19012,7 @@ async function run12(args) {
17643
19012
  });
17644
19013
  }
17645
19014
  await saveDocView(view, outputPath);
17646
- await respond({
19015
+ await respondAck({
17647
19016
  ok: true,
17648
19017
  operation: "hyperlinks.delete",
17649
19018
  path: outputPath ?? path,
@@ -17688,6 +19057,7 @@ Optional:
17688
19057
  (default: $DOCX_AUTHOR)
17689
19058
  -o, --output PATH Write to PATH instead of overwriting FILE
17690
19059
  --dry-run Print what would change; do not write the file
19060
+ -v, --verbose Print the success ack JSON (default: silent on success)
17691
19061
  -h, --help Show this help
17692
19062
 
17693
19063
  The display text stays in place; only the <w:hyperlink> wrapper is removed.
@@ -17816,6 +19186,7 @@ async function run14(args) {
17816
19186
  author: { type: "string" },
17817
19187
  output: { type: "string", short: "o" },
17818
19188
  "dry-run": { type: "boolean" },
19189
+ verbose: { type: "boolean", short: "v" },
17819
19190
  help: { type: "boolean", short: "h" }
17820
19191
  }
17821
19192
  });
@@ -17827,6 +19198,7 @@ async function run14(args) {
17827
19198
  await writeStdout(HELP14);
17828
19199
  return EXIT.OK;
17829
19200
  }
19201
+ setVerboseAck(Boolean(parsed.values.verbose));
17830
19202
  const path = parsed.positionals[0];
17831
19203
  if (!path)
17832
19204
  return fail("USAGE", "Missing FILE argument", HELP14);
@@ -17887,7 +19259,7 @@ async function run14(args) {
17887
19259
  }
17888
19260
  }
17889
19261
  await saveDocView(view, outputPath);
17890
- await respond({
19262
+ await respondAck({
17891
19263
  ok: true,
17892
19264
  operation: "hyperlinks.replace",
17893
19265
  path: outputPath ?? path,
@@ -17938,6 +19310,7 @@ Optional:
17938
19310
  (default: $DOCX_AUTHOR)
17939
19311
  -o, --output PATH Write to PATH instead of overwriting FILE
17940
19312
  --dry-run Print what would change; do not write the file
19313
+ -v, --verbose Print the success ack JSON (default: silent on success)
17941
19314
  -h, --help Show this help
17942
19315
 
17943
19316
  Replaces only the targeted hyperlink. If multiple hyperlinks shared the same
@@ -18217,6 +19590,7 @@ async function run18(args) {
18217
19590
  author: { type: "string" },
18218
19591
  output: { type: "string", short: "o" },
18219
19592
  "dry-run": { type: "boolean" },
19593
+ verbose: { type: "boolean", short: "v" },
18220
19594
  help: { type: "boolean", short: "h" }
18221
19595
  }
18222
19596
  });
@@ -18228,6 +19602,7 @@ async function run18(args) {
18228
19602
  await writeStdout(HELP18);
18229
19603
  return EXIT.OK;
18230
19604
  }
19605
+ setVerboseAck(Boolean(parsed.values.verbose));
18231
19606
  const path = parsed.positionals[0];
18232
19607
  if (!path)
18233
19608
  return fail("USAGE", "Missing FILE argument", HELP18);
@@ -18294,7 +19669,7 @@ async function run18(args) {
18294
19669
  }
18295
19670
  }
18296
19671
  await saveDocView(view, outputPath);
18297
- await respond({
19672
+ await respondAck({
18298
19673
  ok: true,
18299
19674
  operation: "images.replace",
18300
19675
  path: outputPath ?? path,
@@ -18382,6 +19757,7 @@ Optional:
18382
19757
  (default: $DOCX_AUTHOR)
18383
19758
  -o, --output PATH Write to PATH instead of overwriting FILE
18384
19759
  --dry-run Print what would change; do not write the file
19760
+ -v, --verbose Print the success ack JSON (default: silent on success)
18385
19761
  -h, --help Show this help
18386
19762
 
18387
19763
  If the replacement uses a different format from the original, the part is
@@ -18500,8 +19876,17 @@ export type TableCell = {
18500
19876
  export type SectionBreak = {
18501
19877
  id: string;
18502
19878
  type: "sectionBreak";
19879
+ columns?: number;
19880
+ sectionType?: SectionType;
18503
19881
  };
18504
19882
 
19883
+ export type SectionType =
19884
+ | "continuous"
19885
+ | "nextPage"
19886
+ | "evenPage"
19887
+ | "oddPage"
19888
+ | "nextColumn";
19889
+
18505
19890
  export type Run =
18506
19891
  | TextRun
18507
19892
  | ImageRun
@@ -18598,8 +19983,17 @@ export type TrackedChange = {
18598
19983
  * of a tracked move. moveFrom behaves like a delete (text leaves this
18599
19984
  * location, stored as <w:delText> internally); moveTo behaves like an
18600
19985
  * insert (text arrives at this location).
19986
+ * - \`sectPrChange\`: <w:sectPrChange> \u2014 section-property revision marker
19987
+ * embedded inside a <w:sectPr>. Carries a snapshot of the prior section
19988
+ * properties (e.g. columns / type) so accept/reject can drop or restore
19989
+ * them. Has no run text.
18601
19990
  */
18602
- export type TrackedChangeKind = "ins" | "del" | "moveFrom" | "moveTo";
19991
+ export type TrackedChangeKind =
19992
+ | "ins"
19993
+ | "del"
19994
+ | "moveFrom"
19995
+ | "moveTo"
19996
+ | "sectPrChange";
18603
19997
 
18604
19998
  export type Comment = {
18605
19999
  id: string;
@@ -18760,7 +20154,9 @@ var init_schema = __esm(() => {
18760
20154
  required: ["id", "kind", "author", "date", "revisionId"],
18761
20155
  properties: {
18762
20156
  id: { type: "string" },
18763
- kind: { enum: ["ins", "del", "moveFrom", "moveTo"] },
20157
+ kind: {
20158
+ enum: ["ins", "del", "moveFrom", "moveTo"]
20159
+ },
18764
20160
  author: { type: "string" },
18765
20161
  date: { type: "string" },
18766
20162
  revisionId: { type: "string" }
@@ -18872,7 +20268,11 @@ var init_schema = __esm(() => {
18872
20268
  required: ["id", "type"],
18873
20269
  properties: {
18874
20270
  id: { type: "string" },
18875
- type: { const: "sectionBreak" }
20271
+ type: { const: "sectionBreak" },
20272
+ columns: { type: "number" },
20273
+ sectionType: {
20274
+ enum: ["continuous", "nextPage", "evenPage", "oddPage", "nextColumn"]
20275
+ }
18876
20276
  }
18877
20277
  },
18878
20278
  Comment: {
@@ -18967,13 +20367,17 @@ Entity locators:
18967
20367
  imgN Image id (e.g., img2)
18968
20368
  linkN Hyperlink id (e.g., link0)
18969
20369
  tcN Tracked change id (e.g., tc0) \u2014 the Nth revision wrapper
18970
- (<w:ins>/<w:del>/<w:moveFrom>/<w:moveTo>) in document order
20370
+ in document order. Includes run-level <w:ins>/<w:del>/
20371
+ <w:moveFrom>/<w:moveTo>, section-property revisions
20372
+ <w:sectPrChange>, and paragraph-mark <w:ins>/<w:del>
20373
+ (a self-closing element inside <w:pPr><w:rPr>).
18971
20374
  tN:rRcC Cell at row R, column C of table tN
18972
20375
 
18973
20376
  Examples:
18974
20377
  p3 -> the entire paragraph p3
18975
20378
  p3:5-20 -> characters 5..20 of p3
18976
20379
  p3:5-p5:10 -> from char 5 of p3 to char 10 of p5
20380
+ s0 -> first section break (typically the front-matter section)
18977
20381
  c1 -> comment c1
18978
20382
  img0 -> image img0
18979
20383
  link0 -> hyperlink link0
@@ -19011,7 +20415,11 @@ var init_locators2 = __esm(() => {
19011
20415
  comment: { syntax: "cN", example: "c1" },
19012
20416
  image: { syntax: "imgN", example: "img0" },
19013
20417
  hyperlink: { syntax: "linkN", example: "link0" },
19014
- trackedChange: { syntax: "tcN", example: "tc0" },
20418
+ trackedChange: {
20419
+ syntax: "tcN",
20420
+ example: "tc0",
20421
+ notes: "Covers run-level <w:ins>/<w:del>/<w:moveFrom>/<w:moveTo>, <w:sectPrChange>, and paragraph-mark <w:ins>/<w:del>."
20422
+ },
19015
20423
  cell: { syntax: "tN:rRcC", example: "t0:r1c2" },
19016
20424
  nestedCell: { syntax: "tN:rRcC:pK", example: "t0:r1c2:p0" }
19017
20425
  },
@@ -19067,27 +20475,25 @@ __export(exports_insert, {
19067
20475
  });
19068
20476
  import { parseArgs as parseArgs19 } from "util";
19069
20477
  async function run23(args) {
20478
+ const opts = await parseAndValidateOptions3(args);
20479
+ if (typeof opts === "number")
20480
+ return opts;
20481
+ const view = await openOrFail(opts.filePath);
20482
+ if (typeof view === "number")
20483
+ return view;
20484
+ const blockRef = await resolveBlockOrFail(view, opts.placement.locator);
20485
+ if (typeof blockRef === "number")
20486
+ return blockRef;
20487
+ const paragraph = buildInsertedParagraph(view, opts.spec, opts.paragraphOptions);
20488
+ return commitInsertedParagraph(view, blockRef, paragraph, opts);
20489
+ }
20490
+ async function parseAndValidateOptions3(args) {
19070
20491
  let parsed;
19071
20492
  try {
19072
20493
  parsed = parseArgs19({
19073
20494
  args,
19074
20495
  allowPositionals: true,
19075
- options: {
19076
- after: { type: "string" },
19077
- before: { type: "string" },
19078
- text: { type: "string" },
19079
- runs: { type: "string" },
19080
- style: { type: "string" },
19081
- alignment: { type: "string" },
19082
- color: { type: "string" },
19083
- bold: { type: "boolean" },
19084
- italic: { type: "boolean" },
19085
- url: { type: "string" },
19086
- author: { type: "string" },
19087
- output: { type: "string", short: "o" },
19088
- "dry-run": { type: "boolean" },
19089
- help: { type: "boolean", short: "h" }
19090
- }
20496
+ options: OPTION_SPEC3
19091
20497
  });
19092
20498
  } catch (parseError) {
19093
20499
  const message = parseError instanceof Error ? parseError.message : String(parseError);
@@ -19097,104 +20503,200 @@ async function run23(args) {
19097
20503
  await writeStdout(HELP23);
19098
20504
  return EXIT.OK;
19099
20505
  }
19100
- const path = parsed.positionals[0];
19101
- if (!path)
20506
+ setVerboseAck(Boolean(parsed.values.verbose));
20507
+ const filePath = parsed.positionals[0];
20508
+ if (!filePath)
19102
20509
  return fail("USAGE", "Missing FILE argument", HELP23);
19103
- const after = parsed.values.after;
19104
- const before = parsed.values.before;
20510
+ const placement = await parseTargetPlacement(parsed.values);
20511
+ if (typeof placement === "number")
20512
+ return placement;
20513
+ const spec = await chooseContentSpec(parsed.values);
20514
+ if (typeof spec === "number")
20515
+ return spec;
20516
+ const paragraphOptions = await parseParagraphOptions2(parsed.values);
20517
+ if (typeof paragraphOptions === "number")
20518
+ return paragraphOptions;
20519
+ return {
20520
+ filePath,
20521
+ placement,
20522
+ spec,
20523
+ paragraphOptions,
20524
+ authorFlag: parsed.values.author,
20525
+ outputPath: parsed.values.output,
20526
+ dryRun: Boolean(parsed.values["dry-run"])
20527
+ };
20528
+ }
20529
+ async function parseTargetPlacement(values) {
20530
+ const after = values.after;
20531
+ const before = values.before;
19105
20532
  if (!after && !before) {
19106
20533
  return fail("USAGE", "Missing locator: pass --after or --before", HELP23);
19107
20534
  }
19108
20535
  if (after && before) {
19109
20536
  return fail("USAGE", "Pass either --after or --before, not both", HELP23);
19110
20537
  }
19111
- const text = parsed.values.text;
19112
- const runsJson = parsed.values.runs;
19113
- const url = parsed.values.url;
19114
- if (!text && !runsJson) {
19115
- return fail("USAGE", "Missing content: pass --text or --runs", HELP23);
20538
+ if (after !== undefined)
20539
+ return { mode: "after", locator: after };
20540
+ return { mode: "before", locator: before };
20541
+ }
20542
+ async function chooseContentSpec(values) {
20543
+ const text = values.text;
20544
+ const runsJson = values.runs;
20545
+ const url = values.url;
20546
+ const pageBreak = values["page-break"] ?? false;
20547
+ const columnBreak = values["column-break"] ?? false;
20548
+ const sectionFlag = values.section ?? false;
20549
+ const columnsRaw = values.columns;
20550
+ const sectionTypeRaw = values.type;
20551
+ const contentFlagCount = (text !== undefined ? 1 : 0) + (runsJson !== undefined ? 1 : 0) + (pageBreak ? 1 : 0) + (columnBreak ? 1 : 0) + (sectionFlag ? 1 : 0);
20552
+ if (contentFlagCount === 0) {
20553
+ return fail("USAGE", "Missing content: pass --text, --runs, --page-break, --column-break, or --section", HELP23);
20554
+ }
20555
+ if (contentFlagCount > 1) {
20556
+ return fail("USAGE", "Pass only one of --text, --runs, --page-break, --column-break, --section", HELP23);
20557
+ }
20558
+ if (url !== undefined && text === undefined) {
20559
+ return fail("USAGE", "--url requires --text", HELP23);
19116
20560
  }
19117
- if (text && runsJson) {
19118
- return fail("USAGE", "Pass either --text or --runs, not both", HELP23);
20561
+ if ((columnsRaw !== undefined || sectionTypeRaw !== undefined) && !sectionFlag) {
20562
+ return fail("USAGE", "--columns and --type require --section", HELP23);
19119
20563
  }
19120
- if (url && !text) {
19121
- return fail("USAGE", "--url requires --text", HELP23);
20564
+ if (text !== undefined) {
20565
+ return {
20566
+ kind: "text",
20567
+ text,
20568
+ format: {
20569
+ color: values.color,
20570
+ bold: values.bold,
20571
+ italic: values.italic
20572
+ },
20573
+ ...url ? { hyperlinkUrl: url } : {}
20574
+ };
20575
+ }
20576
+ if (runsJson !== undefined) {
20577
+ const runs = await parseRunsArg2(runsJson);
20578
+ if (typeof runs === "number")
20579
+ return runs;
20580
+ return { kind: "runs", runs };
20581
+ }
20582
+ if (pageBreak)
20583
+ return { kind: "break", breakKind: "page" };
20584
+ if (columnBreak)
20585
+ return { kind: "break", breakKind: "column" };
20586
+ const sectionFlags = await parseSectionFlags2(values);
20587
+ if (typeof sectionFlags === "number")
20588
+ return sectionFlags;
20589
+ return { kind: "section", ...sectionFlags };
20590
+ }
20591
+ async function parseRunsArg2(json) {
20592
+ let parsed;
20593
+ try {
20594
+ parsed = JSON.parse(json);
20595
+ } catch (jsonError) {
20596
+ const message = jsonError instanceof Error ? jsonError.message : String(jsonError);
20597
+ return fail("USAGE", `Invalid --runs JSON: ${message}`);
20598
+ }
20599
+ if (!Array.isArray(parsed)) {
20600
+ return fail("USAGE", "--runs must be a JSON array of Run objects");
20601
+ }
20602
+ return parsed;
20603
+ }
20604
+ async function parseSectionFlags2(values) {
20605
+ const out = {};
20606
+ const columnsRaw = values.columns;
20607
+ if (columnsRaw !== undefined) {
20608
+ const columns = Number.parseInt(columnsRaw, 10);
20609
+ if (!Number.isFinite(columns) || columns <= 0) {
20610
+ return fail("USAGE", `--columns must be a positive integer, got "${columnsRaw}"`);
20611
+ }
20612
+ out.columns = columns;
20613
+ }
20614
+ const sectionTypeRaw = values.type;
20615
+ if (sectionTypeRaw !== undefined) {
20616
+ if (!isSectionType(sectionTypeRaw)) {
20617
+ return fail("USAGE", `Invalid --type: ${sectionTypeRaw}`, "Valid values: continuous, nextPage, evenPage, oddPage, nextColumn");
20618
+ }
20619
+ out.sectionType = sectionTypeRaw;
19122
20620
  }
19123
- const paragraphOptions = {};
19124
- const styleValue = parsed.values.style;
20621
+ return out;
20622
+ }
20623
+ async function parseParagraphOptions2(values) {
20624
+ const out = {};
20625
+ const styleValue = values.style;
19125
20626
  if (styleValue)
19126
- paragraphOptions.style = styleValue;
19127
- const alignmentValue = parsed.values.alignment;
20627
+ out.style = styleValue;
20628
+ const alignmentValue = values.alignment;
19128
20629
  if (alignmentValue) {
19129
20630
  if (alignmentValue !== "left" && alignmentValue !== "center" && alignmentValue !== "right" && alignmentValue !== "justify") {
19130
20631
  return fail("USAGE", `Invalid --alignment: ${alignmentValue}`, "Valid values: left, center, right, justify");
19131
20632
  }
19132
- paragraphOptions.alignment = alignmentValue;
20633
+ out.alignment = alignmentValue;
19133
20634
  }
19134
- const view = await openOrFail(path);
19135
- if (typeof view === "number")
19136
- return view;
19137
- const targetLocator = after ?? before;
19138
- const blockRef = await resolveBlockOrFail(view, targetLocator);
19139
- if (typeof blockRef === "number")
19140
- return blockRef;
19141
- let paragraphNode;
19142
- if (text !== undefined) {
19143
- const color = parsed.values.color;
19144
- paragraphNode = /* @__PURE__ */ jsxDEV(Paragraph, {
19145
- text,
19146
- ...paragraphOptions,
19147
- ...color ? { color } : {},
19148
- ...parsed.values.bold ? { bold: true } : {},
19149
- ...parsed.values.italic ? { italic: true } : {}
19150
- }, undefined, false, undefined, this);
19151
- if (url)
19152
- wrapFirstRunInHyperlink(view, paragraphNode, url);
19153
- } else {
19154
- let runsValue;
19155
- try {
19156
- runsValue = JSON.parse(runsJson);
19157
- } catch (jsonError) {
19158
- const message = jsonError instanceof Error ? jsonError.message : String(jsonError);
19159
- return fail("USAGE", `Invalid --runs JSON: ${message}`);
19160
- }
19161
- if (!Array.isArray(runsValue)) {
19162
- return fail("USAGE", "--runs must be a JSON array of Run objects");
19163
- }
19164
- paragraphNode = /* @__PURE__ */ jsxDEV(Paragraph, {
19165
- runs: runsValue,
19166
- ...paragraphOptions
19167
- }, undefined, false, undefined, this);
20635
+ return out;
20636
+ }
20637
+ function buildInsertedParagraph(view, spec, paragraphOptions) {
20638
+ switch (spec.kind) {
20639
+ case "text":
20640
+ return buildTextParagraph(view, spec, paragraphOptions);
20641
+ case "runs":
20642
+ return /* @__PURE__ */ jsxDEV(Paragraph, {
20643
+ runs: spec.runs,
20644
+ ...paragraphOptions
20645
+ }, undefined, false, undefined, this);
20646
+ case "break":
20647
+ return /* @__PURE__ */ jsxDEV(Paragraph, {
20648
+ runs: [{ type: "break", kind: spec.breakKind }],
20649
+ ...paragraphOptions
20650
+ }, undefined, false, undefined, this);
20651
+ case "section":
20652
+ return /* @__PURE__ */ jsxDEV(SentinelSectionParagraph, {
20653
+ ...spec.columns !== undefined ? { columns: spec.columns } : {},
20654
+ ...spec.sectionType ? { sectionType: spec.sectionType } : {}
20655
+ }, undefined, false, undefined, this);
20656
+ }
20657
+ }
20658
+ function buildTextParagraph(view, spec, paragraphOptions) {
20659
+ const paragraphNode = /* @__PURE__ */ jsxDEV(Paragraph, {
20660
+ text: spec.text,
20661
+ ...paragraphOptions,
20662
+ ...spec.format.color ? { color: spec.format.color } : {},
20663
+ ...spec.format.bold ? { bold: true } : {},
20664
+ ...spec.format.italic ? { italic: true } : {}
20665
+ }, undefined, false, undefined, this);
20666
+ if (spec.hyperlinkUrl) {
20667
+ wrapFirstRunInHyperlink(view, paragraphNode, spec.hyperlinkUrl);
19168
20668
  }
20669
+ return paragraphNode;
20670
+ }
20671
+ async function commitInsertedParagraph(view, blockRef, paragraph, opts) {
19169
20672
  const targetIndex = blockRef.parent.indexOf(blockRef.node);
19170
20673
  if (targetIndex === -1) {
19171
20674
  return fail("BLOCK_NOT_FOUND", "Block reference is stale (parent does not contain it)");
19172
20675
  }
19173
- const insertIndex = after !== undefined ? targetIndex + 1 : targetIndex;
20676
+ const insertIndex = opts.placement.mode === "after" ? targetIndex + 1 : targetIndex;
19174
20677
  if (isTrackChangesEnabled(view)) {
19175
- applyTrackedInsertion(paragraphNode, view, parsed.values.author);
20678
+ applyTrackedInsertion(paragraph, view, opts.authorFlag);
19176
20679
  }
19177
- const outputPath = parsed.values.output;
19178
- if (parsed.values["dry-run"]) {
20680
+ if (opts.dryRun) {
19179
20681
  await respond({
19180
20682
  ok: true,
19181
20683
  operation: "insert",
19182
20684
  dryRun: true,
19183
- path,
19184
- locator: targetLocator,
19185
- placement: after !== undefined ? "after" : "before",
19186
- ...outputPath ? { output: outputPath } : {}
20685
+ path: opts.filePath,
20686
+ locator: opts.placement.locator,
20687
+ placement: opts.placement.mode,
20688
+ ...opts.outputPath ? { output: opts.outputPath } : {}
19187
20689
  });
19188
20690
  return EXIT.OK;
19189
20691
  }
19190
- blockRef.parent.splice(insertIndex, 0, paragraphNode);
19191
- await saveDocView(view, outputPath);
19192
- await respond({
20692
+ blockRef.parent.splice(insertIndex, 0, paragraph);
20693
+ await saveDocView(view, opts.outputPath);
20694
+ await respondAck({
19193
20695
  ok: true,
19194
20696
  operation: "insert",
19195
- path: outputPath ?? path,
19196
- locator: targetLocator,
19197
- placement: after !== undefined ? "after" : "before"
20697
+ path: opts.outputPath ?? opts.filePath,
20698
+ locator: opts.placement.locator,
20699
+ placement: opts.placement.mode
19198
20700
  });
19199
20701
  return EXIT.OK;
19200
20702
  }
@@ -19263,6 +20765,9 @@ Locator (one required):
19263
20765
  Content (one required):
19264
20766
  --text TEXT Insert a paragraph with this text
19265
20767
  --runs JSON Insert a paragraph with custom runs (Run[] JSON)
20768
+ --page-break Insert an empty paragraph containing a page break
20769
+ --column-break Insert an empty paragraph containing a column break
20770
+ --section Insert a section boundary (sentinel paragraph w/ inline sectPr)
19266
20771
 
19267
20772
  Paragraph options:
19268
20773
  --style NAME Apply paragraph style (e.g., Heading1)
@@ -19274,9 +20779,15 @@ Run options (only with --text):
19274
20779
  --italic Italic
19275
20780
  --url URL Wrap the inserted text in a hyperlink to URL
19276
20781
 
20782
+ Section options (only with --section):
20783
+ --columns N Number of columns for the section ending at this boundary
20784
+ --type T continuous | nextPage | evenPage | oddPage | nextColumn
20785
+
20786
+ General options:
19277
20787
  --author NAME Author for tracked changes (default: $DOCX_AUTHOR)
19278
20788
  -o, --output PATH Write to PATH instead of overwriting FILE
19279
20789
  --dry-run Print what would be inserted; do not write the file
20790
+ -v, --verbose Print the success ack JSON (default: silent on success)
19280
20791
  -h, --help Show this help
19281
20792
 
19282
20793
  Examples:
@@ -19284,7 +20795,9 @@ Examples:
19284
20795
  docx insert doc.docx --before p0 --text "ALERT" --color CC0000 --bold
19285
20796
  docx insert doc.docx --after p2 --runs '[{"type":"text","text":"X","bold":true}]'
19286
20797
  docx insert doc.docx --after p3 --text "click here" --url https://example.com
19287
- `;
20798
+ docx insert doc.docx --after p3 --page-break
20799
+ docx insert doc.docx --after p9 --section --columns 2 --type continuous
20800
+ `, OPTION_SPEC3;
19288
20801
  var init_insert = __esm(() => {
19289
20802
  init_core();
19290
20803
  init_jsx();
@@ -19292,6 +20805,28 @@ var init_insert = __esm(() => {
19292
20805
  init_respond();
19293
20806
  init_emit2();
19294
20807
  init_jsx_dev_runtime();
20808
+ OPTION_SPEC3 = {
20809
+ after: { type: "string" },
20810
+ before: { type: "string" },
20811
+ text: { type: "string" },
20812
+ runs: { type: "string" },
20813
+ "page-break": { type: "boolean" },
20814
+ "column-break": { type: "boolean" },
20815
+ section: { type: "boolean" },
20816
+ columns: { type: "string" },
20817
+ type: { type: "string" },
20818
+ style: { type: "string" },
20819
+ alignment: { type: "string" },
20820
+ color: { type: "string" },
20821
+ bold: { type: "boolean" },
20822
+ italic: { type: "boolean" },
20823
+ url: { type: "string" },
20824
+ author: { type: "string" },
20825
+ output: { type: "string", short: "o" },
20826
+ "dry-run": { type: "boolean" },
20827
+ verbose: { type: "boolean", short: "v" },
20828
+ help: { type: "boolean", short: "h" }
20829
+ };
19295
20830
  });
19296
20831
 
19297
20832
  // src/cli/outline/build.ts
@@ -19485,7 +21020,7 @@ function isRunVisible(run25, view) {
19485
21020
  return true;
19486
21021
  }
19487
21022
  function buildCommentIndex(blocks, options) {
19488
- const view = options.view ?? "current";
21023
+ const view = options.view ?? "accepted";
19489
21024
  const lastSlot = new Map;
19490
21025
  const spanText = new Map;
19491
21026
  const orderedIds = [];
@@ -19557,7 +21092,7 @@ function headingLevelFor(style) {
19557
21092
  return parsed;
19558
21093
  }
19559
21094
  function renderRuns(paragraphId, runs, ctx) {
19560
- const view = ctx.options.view ?? "current";
21095
+ const view = ctx.options.view ?? "accepted";
19561
21096
  const visibleEntries = [];
19562
21097
  runs.forEach((run25, index) => {
19563
21098
  if (run25.type === "text" && !isRunVisible(run25, view))
@@ -19915,6 +21450,7 @@ async function run25(args) {
19915
21450
  to: { type: "string" },
19916
21451
  accepted: { type: "boolean" },
19917
21452
  baseline: { type: "boolean" },
21453
+ current: { type: "boolean" },
19918
21454
  comments: { type: "boolean" },
19919
21455
  help: { type: "boolean", short: "h" }
19920
21456
  }
@@ -19934,14 +21470,16 @@ async function run25(args) {
19934
21470
  const to = parsed.values.to;
19935
21471
  const accepted = Boolean(parsed.values.accepted);
19936
21472
  const baseline = Boolean(parsed.values.baseline);
21473
+ const current = Boolean(parsed.values.current);
19937
21474
  const showComments = Boolean(parsed.values.comments);
19938
- if (!markdown && (from || to || accepted || baseline || showComments)) {
19939
- return fail("USAGE", "--from, --to, --accepted, --baseline, and --comments require --markdown", HELP25);
21475
+ if (!markdown && (from || to || accepted || baseline || current || showComments)) {
21476
+ return fail("USAGE", "--from, --to, --accepted, --baseline, --current, and --comments require --markdown", HELP25);
19940
21477
  }
19941
- if (accepted && baseline) {
19942
- return fail("USAGE", "--accepted and --baseline are mutually exclusive", HELP25);
21478
+ const viewFlagCount = (accepted ? 1 : 0) + (baseline ? 1 : 0) + (current ? 1 : 0);
21479
+ if (viewFlagCount > 1) {
21480
+ return fail("USAGE", "--accepted, --baseline, and --current are mutually exclusive", HELP25);
19943
21481
  }
19944
- const view = accepted ? "accepted" : baseline ? "baseline" : "current";
21482
+ const view = current ? "current" : baseline ? "baseline" : "accepted";
19945
21483
  const docView = await openOrFail(path);
19946
21484
  if (typeof docView === "number")
19947
21485
  return docView;
@@ -19982,18 +21520,19 @@ Options:
19982
21520
  Accepts pN, tN, tN:rRcC[:pK[:S-E]], pN:S-E, pN:S-pM:E.
19983
21521
  Cell/span/range locators collapse to their enclosing
19984
21522
  top-level block (the table or paragraph).
19985
- --accepted With --markdown, render the post-accept view: drop
19986
- subtractive wrappers (<w:del>, <w:moveFrom>), inline
21523
+ --accepted Default with --markdown: render the post-accept view \u2014
21524
+ drop subtractive wrappers (<w:del>, <w:moveFrom>), inline
19987
21525
  additive wrappers (<w:ins>, <w:moveTo>) as plain text,
19988
- no markers/refs.
21526
+ no markers/refs. Kept as an explicit alias for clarity.
19989
21527
  --baseline With --markdown, render the pre-change view: drop
19990
21528
  additive wrappers (<w:ins>, <w:moveTo>), inline
19991
21529
  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).
21530
+ text, no markers/refs.
21531
+ --current With --markdown, render the raw concatenation: additive
21532
+ wrappers as {++text++}[^tcN] and subtractive as
21533
+ {--text--}[^tcN] (CriticMarkup); the [^tcN] footnote
21534
+ spells out the kind (insertion / deletion / moveTo /
21535
+ moveFrom). Mutually exclusive with --accepted/--baseline.
19997
21536
  --comments With --markdown, append [^cN] after each commented span
19998
21537
  and emit a footnote definition for each comment at the
19999
21538
  end of the output (author, date, body).
@@ -20014,11 +21553,33 @@ var init_read2 = __esm(() => {
20014
21553
  });
20015
21554
 
20016
21555
  // src/cli/replace/replace-span.tsx
20017
- function replaceSpanInParagraph(paragraph, span, replacement, tracked) {
21556
+ function isWrapperVisibleInView2(tag, view) {
21557
+ if (!isRunBearingWrapper(tag))
21558
+ return false;
21559
+ if (view === "current")
21560
+ return true;
21561
+ if (view === "accepted")
21562
+ return tag !== "w:del" && tag !== "w:moveFrom";
21563
+ return tag !== "w:ins" && tag !== "w:moveTo";
21564
+ }
21565
+ function sumVisibleTextLength2(children, view) {
21566
+ let total = 0;
21567
+ for (const child of children) {
21568
+ if (child.tag === "w:r") {
21569
+ total += runTextLength(child);
21570
+ continue;
21571
+ }
21572
+ if (isWrapperVisibleInView2(child.tag, view)) {
21573
+ total += sumVisibleTextLength2(child.children, view);
21574
+ }
21575
+ }
21576
+ return total;
21577
+ }
21578
+ function replaceSpanInParagraph(paragraph, span, replacement, tracked, view = "accepted") {
20018
21579
  if (span.start > span.end) {
20019
21580
  throw new Error(`replaceSpanInParagraph: invalid span ${span.start}-${span.end}`);
20020
21581
  }
20021
- const slots = collectRunSlots(paragraph);
21582
+ const slots = collectRunSlots(paragraph, view);
20022
21583
  const overlapping = slots.filter((slot) => slot.offsetBefore + slot.length > span.start && slot.offsetBefore < span.end);
20023
21584
  const firstSlot = overlapping[0];
20024
21585
  if (!firstSlot) {
@@ -20033,12 +21594,12 @@ function replaceSpanInParagraph(paragraph, span, replacement, tracked) {
20033
21594
  const allSameParent = overlapping.every((slot) => slot.parent === firstParent);
20034
21595
  if (allSameParent) {
20035
21596
  const containerStart = firstParent === paragraph ? 0 : firstSlot.offsetBefore;
20036
- rebuildContainer(firstParent, containerStart, span, replacement, inheritedProperties, firstParent === paragraph, tracked ?? null);
21597
+ rebuildContainer(firstParent, containerStart, span, replacement, inheritedProperties, firstParent === paragraph, tracked ?? null, view);
20037
21598
  return;
20038
21599
  }
20039
- rebuildAcrossBoundaries(paragraph, span, replacement, inheritedProperties, tracked ?? null);
21600
+ rebuildAcrossBoundaries(paragraph, span, replacement, inheritedProperties, tracked ?? null, view);
20040
21601
  }
20041
- function collectRunSlots(paragraph) {
21602
+ function collectRunSlots(paragraph, view) {
20042
21603
  const slots = [];
20043
21604
  let offset = 0;
20044
21605
  function walk(parent, children) {
@@ -20054,7 +21615,7 @@ function collectRunSlots(paragraph) {
20054
21615
  offset += length;
20055
21616
  continue;
20056
21617
  }
20057
- if (isRunBearingWrapper(child.tag)) {
21618
+ if (isWrapperVisibleInView2(child.tag, view)) {
20058
21619
  walk(child, child.children);
20059
21620
  }
20060
21621
  }
@@ -20062,7 +21623,7 @@ function collectRunSlots(paragraph) {
20062
21623
  walk(paragraph, paragraph.children);
20063
21624
  return slots;
20064
21625
  }
20065
- function rebuildContainer(container, baseOffset, span, replacement, runProperties, isParagraph, tracked) {
21626
+ function rebuildContainer(container, baseOffset, span, replacement, runProperties, isParagraph, tracked, view) {
20066
21627
  const newChildren = [];
20067
21628
  let offset = baseOffset;
20068
21629
  let placed = false;
@@ -20113,8 +21674,8 @@ function rebuildContainer(container, baseOffset, span, replacement, runPropertie
20113
21674
  }
20114
21675
  continue;
20115
21676
  }
20116
- if (isParagraph && isRunBearingWrapper(child.tag)) {
20117
- offset += sumRunBearingTextLength(child.children);
21677
+ if (isParagraph && isWrapperVisibleInView2(child.tag, view)) {
21678
+ offset += sumVisibleTextLength2(child.children, view);
20118
21679
  newChildren.push(child);
20119
21680
  continue;
20120
21681
  }
@@ -20124,7 +21685,7 @@ function rebuildContainer(container, baseOffset, span, replacement, runPropertie
20124
21685
  placeReplacement();
20125
21686
  container.children = newChildren;
20126
21687
  }
20127
- function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tracked) {
21688
+ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tracked, view) {
20128
21689
  const newChildren = [];
20129
21690
  let offset = 0;
20130
21691
  let placed = false;
@@ -20175,8 +21736,12 @@ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tr
20175
21736
  }
20176
21737
  continue;
20177
21738
  }
21739
+ if ((child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:moveFrom" || child.tag === "w:moveTo") && !isWrapperVisibleInView2(child.tag, view)) {
21740
+ newChildren.push(child);
21741
+ continue;
21742
+ }
20178
21743
  if (child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:moveFrom" || child.tag === "w:moveTo") {
20179
- const innerLength = sumRunBearingTextLength(child.children);
21744
+ const innerLength = sumVisibleTextLength2(child.children, view);
20180
21745
  const wrapperStart = offset;
20181
21746
  const wrapperEnd = offset + innerLength;
20182
21747
  offset = wrapperEnd;
@@ -20193,7 +21758,7 @@ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tr
20193
21758
  continue;
20194
21759
  }
20195
21760
  if (child.tag === "w:hyperlink") {
20196
- const innerLength = sumRunBearingTextLength(child.children);
21761
+ const innerLength = sumVisibleTextLength2(child.children, view);
20197
21762
  const wrapperStart = offset;
20198
21763
  const wrapperEnd = offset + innerLength;
20199
21764
  offset = wrapperEnd;
@@ -20206,13 +21771,13 @@ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tr
20206
21771
  newChildren.push(child);
20207
21772
  continue;
20208
21773
  }
20209
- splitHyperlinkAcrossSpan(child, wrapperStart, span, runProperties, replacement, tracked, newChildren, placeReplacement, () => {
21774
+ splitHyperlinkAcrossSpan(child, wrapperStart, span, runProperties, replacement, tracked, view, newChildren, placeReplacement, () => {
20210
21775
  placed = true;
20211
21776
  });
20212
21777
  continue;
20213
21778
  }
20214
21779
  if (isRunBearingWrapper(child.tag)) {
20215
- const innerLength = sumRunBearingTextLength(child.children);
21780
+ const innerLength = sumVisibleTextLength2(child.children, view);
20216
21781
  const wrapperStart = offset;
20217
21782
  const wrapperEnd = offset + innerLength;
20218
21783
  offset = wrapperEnd;
@@ -20328,8 +21893,8 @@ function splitTransparentWrapperAcrossSpan(wrapper, wrapperStart, span, out, pla
20328
21893
  out.push(postWrapper);
20329
21894
  }
20330
21895
  }
20331
- function splitHyperlinkAcrossSpan(wrapper, wrapperStart, span, runProperties, replacement, tracked, out, placeReplacement, markReplacementPlaced) {
20332
- const wrapperEnd = wrapperStart + sumRunBearingTextLength(wrapper.children);
21896
+ function splitHyperlinkAcrossSpan(wrapper, wrapperStart, span, runProperties, replacement, tracked, view, out, placeReplacement, markReplacementPlaced) {
21897
+ const wrapperEnd = wrapperStart + sumVisibleTextLength2(wrapper.children, view);
20333
21898
  const startsInside = span.start > wrapperStart && span.start < wrapperEnd;
20334
21899
  const preInner = [];
20335
21900
  const postInner = [];
@@ -20431,8 +21996,12 @@ async function run26(args) {
20431
21996
  all: { type: "boolean" },
20432
21997
  limit: { type: "string" },
20433
21998
  author: { type: "string" },
21999
+ current: { type: "boolean" },
22000
+ baseline: { type: "boolean" },
22001
+ exact: { type: "boolean" },
20434
22002
  output: { type: "string", short: "o" },
20435
22003
  "dry-run": { type: "boolean" },
22004
+ verbose: { type: "boolean", short: "v" },
20436
22005
  help: { type: "boolean", short: "h" }
20437
22006
  }
20438
22007
  });
@@ -20444,6 +22013,7 @@ async function run26(args) {
20444
22013
  await writeStdout(HELP26);
20445
22014
  return EXIT.OK;
20446
22015
  }
22016
+ setVerboseAck(Boolean(parsed.values.verbose));
20447
22017
  const path = parsed.positionals[0];
20448
22018
  const pattern = parsed.positionals[1];
20449
22019
  const replacement = parsed.positionals[2];
@@ -20457,6 +22027,13 @@ async function run26(args) {
20457
22027
  const ignoreCase = Boolean(parsed.values["ignore-case"]);
20458
22028
  const useRegex = Boolean(parsed.values.regex);
20459
22029
  const wantAll = Boolean(parsed.values.all);
22030
+ const wantCurrent = Boolean(parsed.values.current);
22031
+ const wantBaseline = Boolean(parsed.values.baseline);
22032
+ const exact = Boolean(parsed.values.exact);
22033
+ if (wantCurrent && wantBaseline) {
22034
+ return fail("USAGE", "--current and --baseline are mutually exclusive");
22035
+ }
22036
+ const findView = wantCurrent ? "current" : wantBaseline ? "baseline" : "accepted";
20460
22037
  const limitRaw = parsed.values.limit;
20461
22038
  const limit = limitRaw === undefined ? undefined : Number(limitRaw);
20462
22039
  if (limit !== undefined && (!Number.isInteger(limit) || limit <= 0)) {
@@ -20465,16 +22042,23 @@ async function run26(args) {
20465
22042
  const view = await openOrFail(path);
20466
22043
  if (typeof view === "number")
20467
22044
  return view;
20468
- let allMatches;
22045
+ let findResult;
20469
22046
  try {
20470
- allMatches = findTextSpans(view.doc, pattern, {
22047
+ findResult = findTextSpans(view.doc, pattern, {
20471
22048
  regex: useRegex,
20472
- ignoreCase
22049
+ ignoreCase,
22050
+ view: findView,
22051
+ exact
20473
22052
  });
20474
22053
  } catch (matcherError) {
20475
22054
  const message = matcherError instanceof Error ? matcherError.message : String(matcherError);
20476
22055
  return fail("USAGE", `Invalid pattern: ${message}`);
20477
22056
  }
22057
+ const allMatches = findResult.matches;
22058
+ const normalizationFields = findResult.normalizedQuery !== undefined ? {
22059
+ normalizedPattern: findResult.normalizedQuery,
22060
+ normalizationApplied: findResult.normalizationApplied
22061
+ } : {};
20478
22062
  let selected;
20479
22063
  if (limit !== undefined) {
20480
22064
  selected = allMatches.slice(0, limit);
@@ -20501,15 +22085,17 @@ async function run26(args) {
20501
22085
  replacement,
20502
22086
  regex: useRegex,
20503
22087
  ignoreCase,
22088
+ view: findView,
20504
22089
  totalMatches: allMatches.length,
20505
22090
  replaced: selected.length,
20506
22091
  matches: matchesPayload,
22092
+ ...normalizationFields,
20507
22093
  ...outputPath ? { output: outputPath } : {}
20508
22094
  });
20509
22095
  return EXIT.OK;
20510
22096
  }
20511
22097
  if (selected.length === 0) {
20512
- await respond({
22098
+ await respondAck({
20513
22099
  ok: true,
20514
22100
  operation: "replace",
20515
22101
  path,
@@ -20517,9 +22103,11 @@ async function run26(args) {
20517
22103
  replacement,
20518
22104
  regex: useRegex,
20519
22105
  ignoreCase,
22106
+ view: findView,
20520
22107
  totalMatches: 0,
20521
22108
  replaced: 0,
20522
- matches: []
22109
+ matches: [],
22110
+ ...normalizationFields
20523
22111
  });
20524
22112
  return EXIT.OK;
20525
22113
  }
@@ -20538,10 +22126,10 @@ async function run26(args) {
20538
22126
  for (const match of reversed) {
20539
22127
  const concreteReplacement = useRegex ? match.text.replace(new RegExp(pattern, regexFlags), replacement) : replacement;
20540
22128
  const blockRef = resolveBlock(view, match.blockId);
20541
- replaceSpanInParagraph(blockRef.node, { start: match.start, end: match.end }, concreteReplacement, tracked);
22129
+ replaceSpanInParagraph(blockRef.node, { start: match.start, end: match.end }, concreteReplacement, tracked, findView);
20542
22130
  }
20543
22131
  await saveDocView(view, outputPath);
20544
- await respond({
22132
+ await respondAck({
20545
22133
  ok: true,
20546
22134
  operation: "replace",
20547
22135
  path: outputPath ?? path,
@@ -20549,9 +22137,11 @@ async function run26(args) {
20549
22137
  replacement,
20550
22138
  regex: useRegex,
20551
22139
  ignoreCase,
22140
+ view: findView,
20552
22141
  totalMatches: allMatches.length,
20553
22142
  replaced: selected.length,
20554
- matches: matchesPayload
22143
+ matches: matchesPayload,
22144
+ ...normalizationFields
20555
22145
  });
20556
22146
  return EXIT.OK;
20557
22147
  }
@@ -20566,8 +22156,15 @@ Options:
20566
22156
  --all replace every match (default: just the first)
20567
22157
  --limit N replace at most N matches (in document order)
20568
22158
  --author NAME author for tracked changes (default: $DOCX_AUTHOR)
22159
+ --current operate on the raw concatenation (both ins and del text)
22160
+ --baseline operate on the pre-change text (skip ins/moveTo)
22161
+ default: accepted view (skip del/moveFrom) \u2014 matches
22162
+ "docx find" / "docx read --markdown"
22163
+ --exact disable pattern normalization (no markdown-emphasis stripping,
22164
+ no smart/straight quote or em/en-dash equivalence)
20569
22165
  -o, --output PATH write to PATH instead of overwriting FILE
20570
22166
  --dry-run report what would change without writing the file
22167
+ -v, --verbose print the success ack JSON (default: silent on success)
20571
22168
  -h, --help show this help
20572
22169
 
20573
22170
  Within-paragraph matches only. Run formatting (rPr) on the surrounding text
@@ -20576,6 +22173,12 @@ overlaps the matched span. When a single invocation produces multiple
20576
22173
  replacements in the same paragraph, they're applied in reverse offset order
20577
22174
  so earlier offsets don't shift before being applied.
20578
22175
 
22176
+ By default the PATTERN is normalized: balanced markdown emphasis around
22177
+ non-whitespace (**X**, __X__, *X*, \`X\`) is stripped; smart quotes match
22178
+ straight quotes; em-dash and en-dash match the hyphen. The REPLACEMENT
22179
+ is always literal \u2014 whatever bytes you pass go in as-is. Pass --exact
22180
+ to match the raw pattern verbatim. --regex is always verbatim.
22181
+
20579
22182
  With --regex, REPLACEMENT supports JS String.replace substitution syntax:
20580
22183
  $1, $2, ... numbered capture groups
20581
22184
  $& the matched substring
@@ -20650,7 +22253,7 @@ async function run27(args) {
20650
22253
  const kind = trackedChangeKindForTag(reference.node.tag);
20651
22254
  if (!kind)
20652
22255
  continue;
20653
- byId.set(id, {
22256
+ const record = {
20654
22257
  id,
20655
22258
  kind,
20656
22259
  author: reference.node.getAttribute("w:author") ?? "",
@@ -20658,7 +22261,14 @@ async function run27(args) {
20658
22261
  revisionId: reference.node.getAttribute("w:id") ?? "",
20659
22262
  blockId: reference.blockId,
20660
22263
  text: ""
20661
- });
22264
+ };
22265
+ if (kind === "sectPrChange") {
22266
+ const liveSiblings = reference.parent.filter((child) => child !== reference.node);
22267
+ record.current = readSectionProperties(liveSiblings);
22268
+ const snapshot = reference.node.findChild("w:sectPr");
22269
+ record.prior = snapshot ? readSectionProperties(snapshot.children) : {};
22270
+ }
22271
+ byId.set(id, record);
20662
22272
  }
20663
22273
  const sorted = [...byId.values()].sort((a2, b) => trackedChangeIndex(a2.id) - trackedChangeIndex(b.id));
20664
22274
  await respond(sorted);
@@ -20677,6 +22287,8 @@ function trackedChangeKindForTag(tag) {
20677
22287
  return "moveFrom";
20678
22288
  if (tag === "w:moveTo")
20679
22289
  return "moveTo";
22290
+ if (tag === "w:sectPrChange")
22291
+ return "sectPrChange";
20680
22292
  return null;
20681
22293
  }
20682
22294
  var HELP27 = `docx track-changes list \u2014 inventory every revision wrapper
@@ -20687,18 +22299,28 @@ Usage:
20687
22299
  Options:
20688
22300
  -h, --help Show this help
20689
22301
 
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.
22302
+ Lists every revision wrapper with stable tcN ids \u2014 run-level <w:ins>, <w:del>,
22303
+ <w:moveFrom>, <w:moveTo>; section-property revisions <w:sectPrChange>; and
22304
+ paragraph-mark <w:ins>/<w:del> markers (a self-closing element inside
22305
+ <w:pPr><w:rPr> that tracks a paragraph break itself). moveFrom/moveTo halves
22306
+ of the same logical move appear as separate entries; their kind tells them
22307
+ apart.
20693
22308
 
20694
22309
  Output: JSON array of { id, kind, author, date, revisionId, blockId, text }
20695
22310
  sorted by id (document order). kind is one of: "ins", "del", "moveFrom",
20696
- "moveTo".
22311
+ "moveTo", "sectPrChange". Paragraph-mark entries have kind "ins"/"del" with
22312
+ text "" \u2014 their blockId is the owning paragraph's pN.
22313
+
22314
+ Section-property revisions (kind="sectPrChange") additionally include
22315
+ { prior, current } objects with the section's columns/sectionType from
22316
+ before and after the tracked edit, so agents can see the diff without
22317
+ re-reading XML.
20697
22318
 
20698
22319
  Examples:
20699
22320
  docx track-changes list doc.docx
20700
22321
  docx track-changes list doc.docx | jq '.[] | select(.kind == "del")'
20701
22322
  docx track-changes list doc.docx | jq '.[] | select(.kind | test("move"))'
22323
+ docx track-changes list doc.docx | jq '.[] | select(.kind == "sectPrChange") | .prior, .current'
20702
22324
  `;
20703
22325
  var init_list4 = __esm(() => {
20704
22326
  init_core();
@@ -20714,10 +22336,11 @@ async function runApply(args, verb, help) {
20714
22336
  args,
20715
22337
  allowPositionals: true,
20716
22338
  options: {
20717
- at: { type: "string" },
22339
+ at: { type: "string", multiple: true },
20718
22340
  all: { type: "boolean" },
20719
22341
  output: { type: "string", short: "o" },
20720
22342
  "dry-run": { type: "boolean" },
22343
+ verbose: { type: "boolean", short: "v" },
20721
22344
  help: { type: "boolean", short: "h" }
20722
22345
  }
20723
22346
  });
@@ -20729,16 +22352,17 @@ async function runApply(args, verb, help) {
20729
22352
  await writeStdout(help);
20730
22353
  return EXIT.OK;
20731
22354
  }
22355
+ setVerboseAck(Boolean(parsed.values.verbose));
20732
22356
  const path = parsed.positionals[0];
20733
22357
  if (!path)
20734
22358
  return fail("USAGE", "Missing FILE argument", help);
20735
- const at = parsed.values.at;
22359
+ const atRaw = parsed.values.at;
20736
22360
  const all = Boolean(parsed.values.all);
20737
- if (at && all) {
22361
+ if (atRaw && atRaw.length > 0 && all) {
20738
22362
  return fail("USAGE", "--at and --all are mutually exclusive", help);
20739
22363
  }
20740
- if (!at && !all) {
20741
- return fail("USAGE", "Specify --at tcN or --all", help);
22364
+ if ((!atRaw || atRaw.length === 0) && !all) {
22365
+ return fail("USAGE", "Specify --at tcN (repeatable) or --all", help);
20742
22366
  }
20743
22367
  const view = await openOrFail(path);
20744
22368
  if (typeof view === "number")
@@ -20748,16 +22372,33 @@ async function runApply(args, verb, help) {
20748
22372
  if (all) {
20749
22373
  targets = allChanges;
20750
22374
  } 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];
22375
+ const seen = new Set;
22376
+ const ordered = [];
22377
+ for (const id of atRaw ?? []) {
22378
+ if (seen.has(id))
22379
+ continue;
22380
+ seen.add(id);
22381
+ ordered.push(id);
22382
+ }
22383
+ const byId = new Map(allChanges.map((change) => [change.id, change]));
22384
+ targets = [];
22385
+ for (const id of ordered) {
22386
+ const found = byId.get(id);
22387
+ if (!found) {
22388
+ return fail("TRACKED_CHANGE_NOT_FOUND", `Tracked change not found: ${id}`);
22389
+ }
22390
+ targets.push(found);
22391
+ }
22392
+ targets.sort((left, right) => {
22393
+ const leftIndex = allChanges.indexOf(left);
22394
+ const rightIndex = allChanges.indexOf(right);
22395
+ return leftIndex - rightIndex;
22396
+ });
20756
22397
  }
20757
22398
  const records = targets.map((target) => ({
20758
22399
  id: target.id,
20759
22400
  kind: target.kind,
20760
- action: actionFor(target.kind, verb),
22401
+ action: actionFor(target, verb),
20761
22402
  author: target.author,
20762
22403
  date: target.date
20763
22404
  }));
@@ -20775,12 +22416,12 @@ async function runApply(args, verb, help) {
20775
22416
  }
20776
22417
  for (const target of [...targets].reverse()) {
20777
22418
  if (verb === "accept")
20778
- applyAccept(target.node, target.parent);
22419
+ applyAccept(target);
20779
22420
  else
20780
- applyReject(target.node, target.parent);
22421
+ applyReject(target);
20781
22422
  }
20782
22423
  await saveDocView(view, outputPath);
20783
- await respond({
22424
+ await respondAck({
20784
22425
  ok: true,
20785
22426
  operation: `track-changes.${verb}`,
20786
22427
  path: outputPath ?? path,
@@ -20790,26 +22431,95 @@ async function runApply(args, verb, help) {
20790
22431
  }
20791
22432
  function collectTrackedChanges(tree) {
20792
22433
  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
- });
22434
+ const counter = { value: 0 };
22435
+ const documentNode = XmlNode2.findRoot(tree, "w:document");
22436
+ if (!documentNode)
22437
+ return out;
22438
+ const body = documentNode.findChild("w:body");
22439
+ if (!body)
22440
+ return out;
22441
+ visitBlocks(body.children, out, counter);
22442
+ return out;
22443
+ }
22444
+ function visitBlocks(blocks, out, counter) {
22445
+ for (const block of blocks) {
22446
+ if (block.tag === "w:p") {
22447
+ visitParagraph(block, blocks, out, counter);
22448
+ continue;
22449
+ }
22450
+ if (block.tag === "w:tbl") {
22451
+ for (const row of block.findChildren("w:tr")) {
22452
+ for (const cell of row.findChildren("w:tc")) {
22453
+ visitBlocks(cell.children, out, counter);
22454
+ }
20806
22455
  }
20807
- if (node.children.length > 0)
20808
- walk(node.children);
22456
+ continue;
22457
+ }
22458
+ if (block.tag === "w:sectPr") {
22459
+ visitSectPrChange(block, out, counter);
20809
22460
  }
20810
22461
  }
20811
- walk(tree);
20812
- return out;
22462
+ }
22463
+ function visitParagraph(paragraph, paragraphParent, out, counter) {
22464
+ visitRunContainer(paragraph, out, counter);
22465
+ const pPr = paragraph.findChild("w:pPr");
22466
+ if (!pPr)
22467
+ return;
22468
+ const sectPr = pPr.findChild("w:sectPr");
22469
+ if (sectPr)
22470
+ visitSectPrChange(sectPr, out, counter);
22471
+ const rPr = pPr.findChild("w:rPr");
22472
+ if (!rPr)
22473
+ return;
22474
+ for (const child of rPr.children) {
22475
+ const kind = trackedChangeKindForTag2(child.tag);
22476
+ if (kind !== "ins" && kind !== "del")
22477
+ continue;
22478
+ out.push({
22479
+ node: child,
22480
+ parent: rPr.children,
22481
+ kind,
22482
+ id: `tc${counter.value++}`,
22483
+ author: child.getAttribute("w:author") ?? "",
22484
+ date: child.getAttribute("w:date") ?? "",
22485
+ paragraph,
22486
+ paragraphParent
22487
+ });
22488
+ }
22489
+ }
22490
+ function visitRunContainer(container, out, counter) {
22491
+ for (const child of container.children) {
22492
+ if (child.tag === "w:pPr")
22493
+ continue;
22494
+ const kind = trackedChangeKindForTag2(child.tag);
22495
+ if (kind === "ins" || kind === "del" || kind === "moveFrom" || kind === "moveTo") {
22496
+ out.push({
22497
+ node: child,
22498
+ parent: container.children,
22499
+ kind,
22500
+ id: `tc${counter.value++}`,
22501
+ author: child.getAttribute("w:author") ?? "",
22502
+ date: child.getAttribute("w:date") ?? ""
22503
+ });
22504
+ visitRunContainer(child, out, counter);
22505
+ continue;
22506
+ }
22507
+ if (child.children.length > 0)
22508
+ visitRunContainer(child, out, counter);
22509
+ }
22510
+ }
22511
+ function visitSectPrChange(sectPr, out, counter) {
22512
+ const change = sectPr.findChild("w:sectPrChange");
22513
+ if (!change)
22514
+ return;
22515
+ out.push({
22516
+ node: change,
22517
+ parent: sectPr.children,
22518
+ kind: "sectPrChange",
22519
+ id: `tc${counter.value++}`,
22520
+ author: change.getAttribute("w:author") ?? "",
22521
+ date: change.getAttribute("w:date") ?? ""
22522
+ });
20813
22523
  }
20814
22524
  function trackedChangeKindForTag2(tag) {
20815
22525
  if (tag === "w:ins")
@@ -20820,28 +22530,96 @@ function trackedChangeKindForTag2(tag) {
20820
22530
  return "moveFrom";
20821
22531
  if (tag === "w:moveTo")
20822
22532
  return "moveTo";
22533
+ if (tag === "w:sectPrChange")
22534
+ return "sectPrChange";
20823
22535
  return null;
20824
22536
  }
20825
- function actionFor(kind, verb) {
20826
- const isAdditive = kind === "ins" || kind === "moveTo";
22537
+ function actionFor(target, verb) {
22538
+ if (target.paragraph) {
22539
+ if (target.kind === "ins") {
22540
+ return verb === "accept" ? "delete" : "deleteParagraph";
22541
+ }
22542
+ if (target.kind === "del") {
22543
+ return verb === "accept" ? "merge" : "delete";
22544
+ }
22545
+ }
22546
+ if (target.kind === "sectPrChange") {
22547
+ return verb === "accept" ? "delete" : "restore";
22548
+ }
22549
+ const isAdditive = target.kind === "ins" || target.kind === "moveTo";
20827
22550
  if (verb === "accept")
20828
22551
  return isAdditive ? "unwrap" : "delete";
20829
22552
  return isAdditive ? "delete" : "unwrap";
20830
22553
  }
20831
- function applyAccept(node, parent) {
20832
- if (node.tag === "w:ins" || node.tag === "w:moveTo") {
20833
- unwrapNode(node, parent);
22554
+ function applyAccept(target) {
22555
+ if (target.paragraph && target.paragraphParent) {
22556
+ if (target.kind === "ins") {
22557
+ deleteNode(target.node, target.parent);
22558
+ return;
22559
+ }
22560
+ if (target.kind === "del") {
22561
+ deleteNode(target.node, target.parent);
22562
+ mergeParagraphWithNext(target.paragraph, target.paragraphParent);
22563
+ return;
22564
+ }
22565
+ }
22566
+ if (target.node.tag === "w:ins" || target.node.tag === "w:moveTo") {
22567
+ unwrapNode(target.node, target.parent);
22568
+ return;
22569
+ }
22570
+ if (target.node.tag === "w:sectPrChange") {
22571
+ deleteNode(target.node, target.parent);
22572
+ return;
22573
+ }
22574
+ deleteNode(target.node, target.parent);
22575
+ }
22576
+ function applyReject(target) {
22577
+ if (target.paragraph && target.paragraphParent) {
22578
+ if (target.kind === "ins") {
22579
+ const idx = target.paragraphParent.indexOf(target.paragraph);
22580
+ if (idx !== -1)
22581
+ target.paragraphParent.splice(idx, 1);
22582
+ return;
22583
+ }
22584
+ if (target.kind === "del") {
22585
+ deleteNode(target.node, target.parent);
22586
+ return;
22587
+ }
22588
+ }
22589
+ if (target.node.tag === "w:ins" || target.node.tag === "w:moveTo") {
22590
+ deleteNode(target.node, target.parent);
22591
+ return;
22592
+ }
22593
+ if (target.node.tag === "w:sectPrChange") {
22594
+ restoreSectPrSnapshot(target.node, target.parent);
20834
22595
  return;
20835
22596
  }
20836
- deleteNode(node, parent);
22597
+ renameDelTextToText(target.node);
22598
+ unwrapNode(target.node, target.parent);
20837
22599
  }
20838
- function applyReject(node, parent) {
20839
- if (node.tag === "w:ins" || node.tag === "w:moveTo") {
20840
- deleteNode(node, parent);
22600
+ function mergeParagraphWithNext(paragraph, paragraphParent) {
22601
+ const idx = paragraphParent.indexOf(paragraph);
22602
+ if (idx === -1)
20841
22603
  return;
22604
+ const next = paragraphParent[idx + 1];
22605
+ if (!next || next.tag !== "w:p")
22606
+ return;
22607
+ const toMove = [];
22608
+ for (const child of next.children) {
22609
+ if (child.tag === "w:pPr")
22610
+ continue;
22611
+ toMove.push(child);
22612
+ }
22613
+ paragraph.children.push(...toMove);
22614
+ paragraphParent.splice(idx + 1, 1);
22615
+ }
22616
+ function restoreSectPrSnapshot(node, parent) {
22617
+ const snapshot = node.findChild("w:sectPr");
22618
+ parent.length = 0;
22619
+ if (snapshot) {
22620
+ for (const child of snapshot.children)
22621
+ parent.push(child);
20842
22622
  }
20843
- renameDelTextToText(node);
20844
- unwrapNode(node, parent);
20845
22623
  }
20846
22624
  function unwrapNode(node, parent) {
20847
22625
  const index = parent.indexOf(node);
@@ -20863,6 +22641,7 @@ function renameDelTextToText(node) {
20863
22641
  }
20864
22642
  var init_apply = __esm(() => {
20865
22643
  init_core();
22644
+ init_parser();
20866
22645
  init_respond();
20867
22646
  });
20868
22647
 
@@ -20885,19 +22664,33 @@ wrapper \u2014 the content becomes plain runs at this location. Accepting a
20885
22664
  deletion (<w:del>) or move source (<w:moveFrom>) removes the element and its
20886
22665
  <w:delText> entirely (the text disappears for real).
20887
22666
 
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.
22667
+ Section-property revisions (<w:sectPrChange>): accept drops the prior-state
22668
+ snapshot, leaving the live section properties in place.
22669
+
22670
+ Paragraph-mark trackings (<w:ins>/<w:del> inside <w:pPr><w:rPr>): accepting
22671
+ a paragraph-mark insertion just removes the marker (the inserted paragraph
22672
+ stays as a regular paragraph). Accepting a paragraph-mark deletion merges
22673
+ the owning paragraph with the next paragraph \u2014 the next paragraph's runs
22674
+ are appended to this one and the next paragraph is removed (per ECMA-376
22675
+ \xA717.13.5.4).
22676
+
22677
+ Out of scope: formatting changes (<w:rPrChange>/<w:pPrChange>). These aren't
22678
+ modeled in the AST today and --all silently skips them.
20891
22679
 
20892
22680
  Options:
20893
- --at tcN Accept a single tracked change by id
22681
+ --at tcN Accept a tracked change by id. Repeat for multiple ids
22682
+ (--at tc1 --at tc2 --at tc3) \u2014 all targets are resolved
22683
+ against the pre-mutation tree, so renumbering during the
22684
+ batch is not a concern.
20894
22685
  --all Accept every tracked change (mutually exclusive with --at)
20895
22686
  -o, --output PATH Write to PATH instead of overwriting FILE
20896
22687
  --dry-run Print what would change; do not write the file
22688
+ -v, --verbose Print the success ack JSON (default: silent on success)
20897
22689
  -h, --help Show this help
20898
22690
 
20899
22691
  Examples:
20900
22692
  docx track-changes accept doc.docx --at tc0
22693
+ docx track-changes accept doc.docx --at tc1 --at tc3 --at tc5
20901
22694
  docx track-changes accept doc.docx --all
20902
22695
  docx track-changes accept doc.docx --all --dry-run
20903
22696
  `;
@@ -20925,23 +22718,37 @@ Rejecting a deletion (<w:del>) or move source (<w:moveFrom>) unwraps the
20925
22718
  wrapper and converts <w:delText> back to <w:t>, so the text reappears as
20926
22719
  plain runs.
20927
22720
 
22721
+ Section-property revisions (<w:sectPrChange>): reject restores the prior-state
22722
+ snapshot \u2014 the live section's columns/type are replaced with the values that
22723
+ were in effect before the tracked edit.
22724
+
22725
+ Paragraph-mark trackings (<w:ins>/<w:del> inside <w:pPr><w:rPr>): rejecting
22726
+ a paragraph-mark insertion removes the entire owning paragraph (the inserted
22727
+ break disappears \u2014 for sentinels created by "insert --section" this also
22728
+ removes the section break the sentinel was carrying). Rejecting a
22729
+ paragraph-mark deletion just removes the marker (the paragraph stays).
22730
+
20928
22731
  moveFrom and moveTo are processed independently. To fully undo a move, target
20929
22732
  both halves (or use --all). The runtime treats them as paired only by their
20930
22733
  shared revision id, not by atomic accept/reject.
20931
22734
 
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.
22735
+ Out of scope: formatting changes (<w:rPrChange>/<w:pPrChange>). These aren't
22736
+ modeled in the AST today and --all silently skips them.
20935
22737
 
20936
22738
  Options:
20937
- --at tcN Reject a single tracked change by id
22739
+ --at tcN Reject a tracked change by id. Repeat for multiple ids
22740
+ (--at tc1 --at tc2 --at tc3) \u2014 all targets are resolved
22741
+ against the pre-mutation tree, so renumbering during the
22742
+ batch is not a concern.
20938
22743
  --all Reject every tracked change (mutually exclusive with --at)
20939
22744
  -o, --output PATH Write to PATH instead of overwriting FILE
20940
22745
  --dry-run Print what would change; do not write the file
22746
+ -v, --verbose Print the success ack JSON (default: silent on success)
20941
22747
  -h, --help Show this help
20942
22748
 
20943
22749
  Examples:
20944
22750
  docx track-changes reject doc.docx --at tc0
22751
+ docx track-changes reject doc.docx --at tc1 --at tc3 --at tc5
20945
22752
  docx track-changes reject doc.docx --all
20946
22753
  docx track-changes reject doc.docx --all --dry-run
20947
22754
  `;
@@ -20964,6 +22771,7 @@ async function run30(args) {
20964
22771
  options: {
20965
22772
  output: { type: "string", short: "o" },
20966
22773
  "dry-run": { type: "boolean" },
22774
+ verbose: { type: "boolean", short: "v" },
20967
22775
  help: { type: "boolean", short: "h" }
20968
22776
  }
20969
22777
  });
@@ -20975,6 +22783,7 @@ async function run30(args) {
20975
22783
  await writeStdout(HELP30);
20976
22784
  return EXIT.OK;
20977
22785
  }
22786
+ setVerboseAck(Boolean(parsed.values.verbose));
20978
22787
  const path = parsed.positionals[0];
20979
22788
  if (!path)
20980
22789
  return fail("USAGE", "Missing FILE argument", HELP30);
@@ -21023,7 +22832,7 @@ async function run30(args) {
21023
22832
  });
21024
22833
  }
21025
22834
  await saveDocView(view, outputPath);
21026
- await respond({
22835
+ await respondAck({
21027
22836
  ok: true,
21028
22837
  operation: "track-changes",
21029
22838
  path: outputPath ?? path,
@@ -21045,6 +22854,7 @@ insert/edit/delete/replace commands also emit <w:ins>/<w:del> markers
21045
22854
  Options:
21046
22855
  -o, --output PATH Write to PATH instead of overwriting FILE
21047
22856
  --dry-run Print what would change; do not write the file
22857
+ -v, --verbose Print the success ack JSON (default: silent on success)
21048
22858
  -h, --help Show this help
21049
22859
 
21050
22860
  Examples:
@@ -21100,18 +22910,23 @@ Usage:
21100
22910
  Verbs:
21101
22911
  on Set <w:trackChanges/> in word/settings.xml
21102
22912
  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)
22913
+ list Inventory every revision wrapper (run-level ins/del/move,
22914
+ <w:sectPrChange>, paragraph-mark <w:ins>/<w:del>) with stable
22915
+ tcN ids
22916
+ accept Accept tracked changes \u2014 ins/moveTo unwrap; del/moveFrom are
22917
+ removed; sectPrChange drops its snapshot (live state stays);
22918
+ paragraph-mark del merges with the next paragraph
22919
+ reject Reject tracked changes \u2014 ins/moveTo are removed (and for a
22920
+ paragraph-mark ins the entire paragraph is removed); del/moveFrom
22921
+ unwrap (with <w:delText> \u2192 <w:t> rename); sectPrChange restores
22922
+ its snapshot
21109
22923
 
21110
22924
  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.
22925
+ emit <w:ins>/<w:del> markers (attributed via --author or $DOCX_AUTHOR);
22926
+ edit --at sN under tracking emits <w:sectPrChange>. moveFrom/moveTo are
22927
+ read, listed, and accept/reject independently \u2014 we don't emit them
22928
+ ourselves (Word does that interactively). Accept/reject themselves
22929
+ bypass tracking \u2014 they're doc surgery, not edits.
21115
22930
 
21116
22931
  Run "docx track-changes <verb> --help" for verb-specific help.
21117
22932
  `;
@@ -21121,16 +22936,48 @@ var init_track_changes2 = __esm(() => {
21121
22936
 
21122
22937
  // src/cli/wc/count.ts
21123
22938
  function textFor(options) {
21124
- if (options.view === "accepted")
21125
- return paragraphTextAccepted;
21126
- if (options.view === "baseline")
22939
+ const view = options.view ?? "accepted";
22940
+ if (view === "current")
22941
+ return paragraphText;
22942
+ if (view === "baseline")
21127
22943
  return paragraphTextBaseline;
21128
- return paragraphText;
22944
+ return paragraphTextAccepted;
21129
22945
  }
21130
22946
  function countWords(text) {
21131
22947
  const matches = text.match(/\S+/g);
21132
22948
  return matches?.length ?? 0;
21133
22949
  }
22950
+ function countWordsInSection(blocks, targetId, options = {}) {
22951
+ let bucket = [];
22952
+ for (const block of blocks) {
22953
+ if (block.type === "sectionBreak") {
22954
+ if (block.id === targetId) {
22955
+ return countWordsInBlocks(bucket, options);
22956
+ }
22957
+ bucket = [];
22958
+ continue;
22959
+ }
22960
+ bucket.push(block);
22961
+ }
22962
+ return null;
22963
+ }
22964
+ function countSectionsInBlocks(blocks) {
22965
+ let total = 0;
22966
+ for (const block of blocks) {
22967
+ if (block.type === "sectionBreak") {
22968
+ total += 1;
22969
+ continue;
22970
+ }
22971
+ if (block.type === "table") {
22972
+ for (const row of block.rows) {
22973
+ for (const cell of row.cells) {
22974
+ total += countSectionsInBlocks(cell.blocks);
22975
+ }
22976
+ }
22977
+ }
22978
+ }
22979
+ return total;
22980
+ }
21134
22981
  function countWordsInBlocks(blocks, options = {}) {
21135
22982
  const text = textFor(options);
21136
22983
  let total = 0;
@@ -21201,6 +23048,7 @@ async function run32(args) {
21201
23048
  options: {
21202
23049
  accepted: { type: "boolean" },
21203
23050
  baseline: { type: "boolean" },
23051
+ current: { type: "boolean" },
21204
23052
  help: { type: "boolean", short: "h" }
21205
23053
  }
21206
23054
  });
@@ -21218,10 +23066,12 @@ async function run32(args) {
21218
23066
  return fail("USAGE", "Missing FILE argument", HELP32);
21219
23067
  const wantAccepted = Boolean(parsed.values.accepted);
21220
23068
  const wantBaseline = Boolean(parsed.values.baseline);
21221
- if (wantAccepted && wantBaseline) {
21222
- return fail("USAGE", "--accepted and --baseline are mutually exclusive", HELP32);
23069
+ const wantCurrent = Boolean(parsed.values.current);
23070
+ const viewFlagCount = (wantAccepted ? 1 : 0) + (wantBaseline ? 1 : 0) + (wantCurrent ? 1 : 0);
23071
+ if (viewFlagCount > 1) {
23072
+ return fail("USAGE", "--accepted, --baseline, and --current are mutually exclusive", HELP32);
21223
23073
  }
21224
- const view = wantAccepted ? "accepted" : wantBaseline ? "baseline" : "current";
23074
+ const view = wantCurrent ? "current" : wantBaseline ? "baseline" : "accepted";
21225
23075
  const pickText = paragraphTextFor(view);
21226
23076
  const docView = await openOrFail(path);
21227
23077
  if (typeof docView === "number")
@@ -21233,7 +23083,8 @@ async function run32(args) {
21233
23083
  path,
21234
23084
  scope: "document",
21235
23085
  view,
21236
- words: countWordsInBlocks(docView.doc.blocks, { view })
23086
+ words: countWordsInBlocks(docView.doc.blocks, { view }),
23087
+ sections: countSectionsInBlocks(docView.doc.blocks)
21237
23088
  });
21238
23089
  return EXIT.OK;
21239
23090
  }
@@ -21255,6 +23106,24 @@ async function run32(args) {
21255
23106
  if (!block) {
21256
23107
  return fail("BLOCK_NOT_FOUND", `Block not found: ${locator.blockId}`);
21257
23108
  }
23109
+ if (block.type === "sectionBreak") {
23110
+ const sectionWords = countWordsInSection(blocks, locator.blockId, {
23111
+ view
23112
+ });
23113
+ if (sectionWords === null) {
23114
+ return fail("BLOCK_NOT_FOUND", `Section not found: ${locator.blockId}`);
23115
+ }
23116
+ await respond({
23117
+ ok: true,
23118
+ operation: "wc",
23119
+ path,
23120
+ locator: locatorInput,
23121
+ scope: "section",
23122
+ view,
23123
+ words: sectionWords
23124
+ });
23125
+ return EXIT.OK;
23126
+ }
21258
23127
  const words = block.type === "paragraph" ? countWords(pickText(block)) : block.type === "table" ? countWordsInBlocks([block], { view }) : 0;
21259
23128
  await respond({
21260
23129
  ok: true,
@@ -21379,22 +23248,27 @@ Locators (optional; default: whole document):
21379
23248
  tN:rRcC whole cell
21380
23249
  tN:rRcC:pK paragraph K inside that cell
21381
23250
  tN:rRcC:pK:S-E span within a cell paragraph
23251
+ sN section N \u2014 every paragraph and table from the prior section
23252
+ boundary up to and including the paragraph that holds sN's
23253
+ inline sectPr (or to end of body for the trailing section)
21382
23254
 
21383
23255
  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".
23256
+ --accepted Default. Count the accepted view: skip subtractive
23257
+ wrappers (<w:del>, <w:moveFrom>); keep additive
23258
+ wrappers (<w:ins>, <w:moveTo>) as plain text. Mirrors
23259
+ "docx read --markdown" / "docx find" defaults.
21388
23260
  --baseline Count the baseline view: skip additive wrappers
21389
23261
  (<w:ins>, <w:moveTo>); keep subtractive wrappers
21390
23262
  (<w:del>, <w:moveFrom>) as plain text \u2014 i.e., the doc as
21391
23263
  it was before any tracked changes were made.
23264
+ --current Count the raw concatenation: every tracked-change
23265
+ wrapper's text counts (everything on disk).
21392
23266
  -h, --help show this help
21393
23267
 
21394
23268
  Counting is whitespace-segmented (\\S+) over the joined paragraph text. Hidden
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.
23269
+ content like images/breaks/tabs contributes no words. The three view flags
23270
+ are mutually exclusive; default (no flag) is --accepted, matching what a
23271
+ reader would see if every tracked change were accepted.
21398
23272
 
21399
23273
  Examples:
21400
23274
  docx wc doc.docx
@@ -21402,6 +23276,7 @@ Examples:
21402
23276
  docx wc doc.docx p3:0-120
21403
23277
  docx wc doc.docx p5:10-p9:42
21404
23278
  docx wc doc.docx t0:r1c0
23279
+ docx wc doc.docx s2
21405
23280
  `;
21406
23281
  var init_wc = __esm(() => {
21407
23282
  init_core();
@@ -21411,7 +23286,7 @@ var init_wc = __esm(() => {
21411
23286
  // package.json
21412
23287
  var package_default = {
21413
23288
  name: "bun-docx",
21414
- version: "0.7.0",
23289
+ version: "0.8.0",
21415
23290
  description: "CLI for AI agents (Claude, Codex) to read, edit, and comment on .docx files with full format fidelity.",
21416
23291
  keywords: [
21417
23292
  "docx",
@@ -21503,12 +23378,21 @@ Environment:
21503
23378
 
21504
23379
  Tracked changes:
21505
23380
  Toggle: docx track-changes FILE on|off
21506
- Inventory: docx track-changes list FILE (JSON: id, kind, author, date, blockId, text)
23381
+ Inventory: docx track-changes list FILE (JSON array of { id, kind, author,
23382
+ date, revisionId, blockId, text }; sectPrChange entries also
23383
+ include { prior, current } with the section properties on each
23384
+ side of the edit)
21507
23385
  Accept: docx track-changes accept FILE (--at tcN | --all)
21508
23386
  Reject: docx track-changes reject FILE (--at tcN | --all)
21509
23387
 
21510
23388
  When <w:trackChanges/> is set, insert/edit/delete/replace automatically emit
21511
23389
  <w:ins>/<w:del> markers attributed via --author (or $DOCX_AUTHOR, or "docx-cli").
23390
+ edit --at sN under tracking emits <w:sectPrChange> snapshots on the section.
23391
+
23392
+ Accept/reject handle: run-level ins/del/moveFrom/moveTo, sectPrChange
23393
+ (snapshot drop or restore), and paragraph-mark ins/del (accept-del merges
23394
+ with the next paragraph; reject-ins removes the entire owning paragraph).
23395
+ Out of scope: rPrChange / pPrChange formatting revisions.
21512
23396
 
21513
23397
  "docx read --markdown" surfaces them as CriticMarkup:
21514
23398
  {++inserted++}[^tcN] {--deleted--}[^tcN]