bun-docx 0.13.0 → 0.14.1

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 +13 -5
  2. package/dist/index.js +1453 -718
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -16632,6 +16632,7 @@ var init_jsx = __esm(() => {
16632
16632
  "rFonts",
16633
16633
  "br",
16634
16634
  "tab",
16635
+ "tabs",
16635
16636
  "drawing",
16636
16637
  "sectPr",
16637
16638
  "sectPrChange",
@@ -21616,6 +21617,15 @@ function applyParagraphProperties(document2, paragraph, paragraphProperties) {
21616
21617
  paragraph.alignment = value;
21617
21618
  }
21618
21619
  }
21620
+ const tabsNode = paragraphProperties.findChild("w:tabs");
21621
+ if (tabsNode) {
21622
+ const stops = tabsNode.findChildren("w:tab").map((tab) => ({
21623
+ align: tab.getAttribute("w:val") ?? "left",
21624
+ pos: Number(tab.getAttribute("w:pos") ?? "0")
21625
+ })).filter((stop) => Number.isFinite(stop.pos));
21626
+ if (stops.length > 0)
21627
+ paragraph.tabStops = stops;
21628
+ }
21619
21629
  const numberingProperties = paragraphProperties.findChild("w:numPr");
21620
21630
  if (numberingProperties) {
21621
21631
  const indentLevel = numberingProperties.findChild("w:ilvl");
@@ -22188,15 +22198,12 @@ class CommentsView {
22188
22198
  }
22189
22199
  paraIdFor(commentId) {
22190
22200
  const numericId = commentId.startsWith("c") ? commentId.slice(1) : commentId;
22191
- const comment = this.findById(numericId);
22192
- const paragraph = comment?.findChild("w:p");
22193
- return paragraph?.getAttribute("w14:paraId");
22201
+ return lastParagraph(this.findById(numericId))?.getAttribute("w14:paraId");
22194
22202
  }
22195
22203
  ensureParaId(commentId) {
22196
22204
  const numericId = commentId.startsWith("c") ? commentId.slice(1) : commentId;
22197
22205
  const root = XmlNode2.findRoot(this.tree, "w:comments");
22198
- const comment = this.findById(numericId);
22199
- const paragraph = comment?.findChild("w:p");
22206
+ const paragraph = lastParagraph(this.findById(numericId));
22200
22207
  if (!root || !paragraph)
22201
22208
  return;
22202
22209
  const existing = paragraph.getAttribute("w14:paraId");
@@ -22226,6 +22233,96 @@ class CommentsView {
22226
22233
  }
22227
22234
  return String(highest + 1);
22228
22235
  }
22236
+ threadRootId(commentId) {
22237
+ const root = XmlNode2.findRoot(this.tree, "w:comments");
22238
+ const numericId = commentId.startsWith("c") ? commentId.slice(1) : commentId;
22239
+ if (!root)
22240
+ return numericId;
22241
+ const extended = this.#readExtended();
22242
+ const idByParaId = new Map;
22243
+ for (const child2 of root.children) {
22244
+ if (child2.tag !== "w:comment")
22245
+ continue;
22246
+ const childId = child2.getAttribute("w:id");
22247
+ if (childId == null)
22248
+ continue;
22249
+ const paraId = lastParagraph(child2)?.getAttribute("w14:paraId");
22250
+ if (paraId)
22251
+ idByParaId.set(paraId, childId);
22252
+ }
22253
+ let currentId = numericId;
22254
+ const seen = new Set([currentId]);
22255
+ for (;; ) {
22256
+ const paraId = lastParagraph(this.findById(currentId))?.getAttribute("w14:paraId");
22257
+ const parentParaId = paraId ? extended.get(paraId)?.parentParaId : undefined;
22258
+ const parentId = parentParaId ? idByParaId.get(parentParaId) : undefined;
22259
+ if (!parentId || seen.has(parentId))
22260
+ return currentId;
22261
+ seen.add(parentId);
22262
+ currentId = parentId;
22263
+ }
22264
+ }
22265
+ descendantReplyIds(commentId) {
22266
+ const root = XmlNode2.findRoot(this.tree, "w:comments");
22267
+ if (!root)
22268
+ return [];
22269
+ const numericId = commentId.startsWith("c") ? commentId.slice(1) : commentId;
22270
+ const startParaId = lastParagraph(this.findById(numericId))?.getAttribute("w14:paraId");
22271
+ if (!startParaId)
22272
+ return [];
22273
+ const extended = this.#readExtended();
22274
+ const idByParaId = new Map;
22275
+ const childParaIdsByParent = new Map;
22276
+ for (const child2 of root.children) {
22277
+ if (child2.tag !== "w:comment")
22278
+ continue;
22279
+ const childId = child2.getAttribute("w:id");
22280
+ if (childId == null)
22281
+ continue;
22282
+ const paraId = lastParagraph(child2)?.getAttribute("w14:paraId");
22283
+ if (!paraId)
22284
+ continue;
22285
+ idByParaId.set(paraId, childId);
22286
+ const parentParaId = extended.get(paraId)?.parentParaId;
22287
+ if (!parentParaId)
22288
+ continue;
22289
+ const siblings = childParaIdsByParent.get(parentParaId) ?? [];
22290
+ siblings.push(paraId);
22291
+ childParaIdsByParent.set(parentParaId, siblings);
22292
+ }
22293
+ const descendants = [];
22294
+ const queue = [startParaId];
22295
+ while (queue.length > 0) {
22296
+ const paraId = queue.pop();
22297
+ if (!paraId)
22298
+ break;
22299
+ for (const childParaId of childParaIdsByParent.get(paraId) ?? []) {
22300
+ const childId = idByParaId.get(childParaId);
22301
+ if (childId)
22302
+ descendants.push(childId);
22303
+ queue.push(childParaId);
22304
+ }
22305
+ }
22306
+ return descendants;
22307
+ }
22308
+ insertReplyAfter(parentNumericId, comment) {
22309
+ const root = XmlNode2.findRoot(this.tree, "w:comments");
22310
+ if (!root)
22311
+ throw new Error("expected <w:comments> root");
22312
+ const threadIds = new Set([
22313
+ parentNumericId,
22314
+ ...this.descendantReplyIds(parentNumericId)
22315
+ ]);
22316
+ let insertIndex = root.children.length;
22317
+ for (const [index, child2] of root.children.entries()) {
22318
+ if (child2.tag !== "w:comment")
22319
+ continue;
22320
+ const id = child2.getAttribute("w:id");
22321
+ if (id != null && threadIds.has(id))
22322
+ insertIndex = index + 1;
22323
+ }
22324
+ root.children.splice(insertIndex, 0, comment);
22325
+ }
22229
22326
  toComments(anchors) {
22230
22327
  const root = XmlNode2.findRoot(this.tree, "w:comments");
22231
22328
  if (!root)
@@ -22237,8 +22334,7 @@ class CommentsView {
22237
22334
  const numericId = child2.getAttribute("w:id");
22238
22335
  if (numericId == null)
22239
22336
  continue;
22240
- const paragraph = child2.findChild("w:p");
22241
- const paraId = paragraph?.getAttribute("w14:paraId");
22337
+ const paraId = lastParagraph(child2)?.getAttribute("w14:paraId");
22242
22338
  if (paraId)
22243
22339
  commentIdByParaId.set(paraId, `c${numericId}`);
22244
22340
  }
@@ -22261,8 +22357,7 @@ class CommentsView {
22261
22357
  endBlockId: "",
22262
22358
  endOffset: 0
22263
22359
  };
22264
- const paragraph = child2.findChild("w:p");
22265
- const paraId = paragraph?.getAttribute("w14:paraId");
22360
+ const paraId = lastParagraph(child2)?.getAttribute("w14:paraId");
22266
22361
  const meta = paraId ? extendedByParaId.get(paraId) ?? {} : {};
22267
22362
  const parentCommentId = meta.parentParaId ? commentIdByParaId.get(meta.parentParaId) : undefined;
22268
22363
  comments.push({
@@ -22321,12 +22416,21 @@ function CommentsRoot() {
22321
22416
  "xmlns:w14": NS_W14
22322
22417
  }, undefined, false, undefined, this);
22323
22418
  }
22419
+ function lastParagraph(comment) {
22420
+ return comment?.findChildren("w:p").at(-1);
22421
+ }
22324
22422
  function generateParaId() {
22423
+ const seed = Bun.env.DOCX_CLI_PARA_ID_SEED;
22424
+ if (seed) {
22425
+ const minted = (Number.parseInt(seed, 16) + seededParaIds++) % 2147483648;
22426
+ return (minted || 1).toString(16).padStart(8, "0").toUpperCase();
22427
+ }
22325
22428
  const bytes = new Uint8Array(4);
22326
22429
  crypto.getRandomValues(bytes);
22327
- let hex = "";
22328
- for (const byte of bytes)
22329
- hex += byte.toString(16).padStart(2, "0");
22430
+ bytes[0] = (bytes[0] ?? 0) & 127;
22431
+ const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
22432
+ if (hex === "00000000")
22433
+ return "00000001";
22330
22434
  return hex.toUpperCase();
22331
22435
  }
22332
22436
  function CommentsExRoot() {
@@ -22336,7 +22440,7 @@ function CommentsExRoot() {
22336
22440
  "mc:Ignorable": "w15"
22337
22441
  }, undefined, false, undefined, this);
22338
22442
  }
22339
- var COMMENTS_PART_NAME = "word/comments.xml", COMMENTS_EXT_PART_NAME = "word/commentsExtended.xml", COMMENTS_RELATIONSHIP_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments", COMMENTS_EXT_RELATIONSHIP_TYPE = "http://schemas.microsoft.com/office/2011/relationships/commentsExtended", COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml", COMMENTS_EXT_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml", NS_W2 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main", NS_W14 = "http://schemas.microsoft.com/office/word/2010/wordml", NS_W15 = "http://schemas.microsoft.com/office/word/2012/wordml";
22443
+ var COMMENTS_PART_NAME = "word/comments.xml", COMMENTS_EXT_PART_NAME = "word/commentsExtended.xml", COMMENTS_RELATIONSHIP_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments", COMMENTS_EXT_RELATIONSHIP_TYPE = "http://schemas.microsoft.com/office/2011/relationships/commentsExtended", COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml", COMMENTS_EXT_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml", NS_W2 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main", NS_W14 = "http://schemas.microsoft.com/office/word/2010/wordml", NS_W15 = "http://schemas.microsoft.com/office/word/2012/wordml", seededParaIds = 0;
22340
22444
  var init_comments = __esm(() => {
22341
22445
  init_jsx();
22342
22446
  init_parser();
@@ -33816,6 +33920,38 @@ function CommentBody({
33816
33920
  }, undefined, false, undefined, this)
33817
33921
  }, undefined, false, undefined, this);
33818
33922
  }
33923
+ function addReplyCommentMarkers(documentTree, threadNumericIds, replyNumericId) {
33924
+ const document2 = XmlNode2.findRoot(documentTree, "w:document");
33925
+ if (!document2)
33926
+ return false;
33927
+ const threadIds = new Set(threadNumericIds);
33928
+ const referenceRun = findLastDescendant(document2, (node) => node.tag === "w:r" && node.children.some((child2) => child2.tag === "w:commentReference" && threadIds.has(child2.getAttribute("w:id") ?? "")));
33929
+ if (!referenceRun)
33930
+ return false;
33931
+ const rangeStart = findLastDescendant(document2, (node) => node.tag === "w:commentRangeStart" && threadIds.has(node.getAttribute("w:id") ?? ""));
33932
+ if (!rangeStart) {
33933
+ spliceAfter(referenceRun, commentReferenceRun(replyNumericId));
33934
+ return true;
33935
+ }
33936
+ spliceAfter(rangeStart, commentRangeStartMarker(replyNumericId));
33937
+ spliceAfter(referenceRun, commentRangeEndMarker(replyNumericId), commentReferenceRun(replyNumericId));
33938
+ return true;
33939
+ }
33940
+ function findLastDescendant(root, matches) {
33941
+ let last;
33942
+ for (const child2 of root.children) {
33943
+ if (matches(child2))
33944
+ last = { node: child2, siblings: root.children };
33945
+ const found = findLastDescendant(child2, matches);
33946
+ if (found)
33947
+ last = found;
33948
+ }
33949
+ return last;
33950
+ }
33951
+ function spliceAfter(target, ...nodes) {
33952
+ const index = target.siblings.indexOf(target.node);
33953
+ target.siblings.splice(index + 1, 0, ...nodes);
33954
+ }
33819
33955
  function removeCommentMarkers(documentTree, numericId) {
33820
33956
  const document2 = XmlNode2.findRoot(documentTree, "w:document");
33821
33957
  if (!document2)
@@ -33876,7 +34012,7 @@ class Comments {
33876
34012
  return comments;
33877
34013
  }
33878
34014
  add(anchor, options) {
33879
- const date = options.date ?? new Date().toISOString();
34015
+ const date = options.date ?? resolveDate();
33880
34016
  const numericId = this.document.comments?.nextId() ?? "0";
33881
34017
  const paraId = generateParaId();
33882
34018
  try {
@@ -33903,21 +34039,47 @@ class Comments {
33903
34039
  if (!view?.findById(parentNumericId)) {
33904
34040
  throw new CommentsError("COMMENT_NOT_FOUND", `Parent comment not found: c${parentNumericId}`);
33905
34041
  }
33906
- const parentParaId = view.ensureParaId(parentNumericId);
33907
- if (!parentParaId) {
33908
- throw new CommentsError("COMMENT_NOT_FOUND", `Parent comment c${parentNumericId} could not be assigned a w14:paraId.`);
34042
+ const rootNumericId = view.threadRootId(parentNumericId);
34043
+ const rootParaId = view.ensureParaId(rootNumericId);
34044
+ if (!rootParaId) {
34045
+ throw new CommentsError("COMMENT_NOT_FOUND", `Parent comment c${rootNumericId} could not be assigned a w14:paraId.`);
33909
34046
  }
33910
- const date = options.date ?? new Date().toISOString();
34047
+ const date = options.date ?? resolveDate();
33911
34048
  const numericId = view.nextId();
33912
34049
  const replyParaId = generateParaId();
33913
- this.#appendCommentBody(numericId, replyParaId, body, options.author, date);
34050
+ const anchored = addReplyCommentMarkers(this.document.documentTree, [rootNumericId, ...view.descendantReplyIds(rootNumericId)], numericId);
34051
+ if (!anchored) {
34052
+ throw new CommentsError("COMMENT_NOT_FOUND", `Parent comment c${parentNumericId} has no anchor in the document body; cannot anchor a reply to it.`);
34053
+ }
34054
+ this.#appendCommentBody(numericId, replyParaId, body, options.author, date, {
34055
+ threadParentNumericId: rootNumericId
34056
+ });
33914
34057
  const extView = this.document.ensureCommentsExtended();
33915
34058
  const extRoot = extView.extendedTree ? XmlNode2.findRoot(extView.extendedTree, "w15:commentsEx") : undefined;
33916
34059
  if (!extRoot)
33917
34060
  throw new Error("expected <w15:commentsEx> root");
33918
- extRoot.children.push(new XmlNode2("w15:commentEx", {
34061
+ const threadParaIds = new Set([
34062
+ rootParaId,
34063
+ ...view.descendantReplyIds(rootNumericId).map((replyId) => view.paraIdFor(replyId))
34064
+ ]);
34065
+ const hasRootEntry = extRoot.children.some((child2) => child2.tag === "w15:commentEx" && child2.getAttribute("w15:paraId") === rootParaId);
34066
+ if (!hasRootEntry) {
34067
+ extRoot.children.push(new XmlNode2("w15:commentEx", {
34068
+ "w15:paraId": rootParaId,
34069
+ "w15:done": "0"
34070
+ }));
34071
+ }
34072
+ let insertIndex = extRoot.children.length;
34073
+ for (const [index, child2] of extRoot.children.entries()) {
34074
+ if (child2.tag !== "w15:commentEx")
34075
+ continue;
34076
+ const paraId = child2.getAttribute("w15:paraId");
34077
+ if (paraId && threadParaIds.has(paraId))
34078
+ insertIndex = index + 1;
34079
+ }
34080
+ extRoot.children.splice(insertIndex, 0, new XmlNode2("w15:commentEx", {
33919
34081
  "w15:paraId": replyParaId,
33920
- "w15:paraIdParent": parentParaId,
34082
+ "w15:paraIdParent": rootParaId,
33921
34083
  "w15:done": "0"
33922
34084
  }));
33923
34085
  return numericId;
@@ -33967,10 +34129,16 @@ class Comments {
33967
34129
  }
33968
34130
  if (!view)
33969
34131
  return;
34132
+ const cascaded = new Set(normalized);
34133
+ for (const commentId of normalized) {
34134
+ for (const replyId of view.descendantReplyIds(commentId)) {
34135
+ cascaded.add(`c${replyId}`);
34136
+ }
34137
+ }
33970
34138
  const root = XmlNode2.findRoot(view.tree, "w:comments");
33971
34139
  if (!root)
33972
34140
  return;
33973
- for (const commentId of normalized) {
34141
+ for (const commentId of cascaded) {
33974
34142
  const numericId = commentId.slice(1);
33975
34143
  const node = view.findById(numericId);
33976
34144
  if (!node)
@@ -33999,12 +34167,12 @@ class Comments {
33999
34167
  this.#appendCommentBody(numericId, paraId, options.body, options.author, options.date);
34000
34168
  return numericId;
34001
34169
  }
34002
- #appendCommentBody(numericId, paraId, text2, author, date) {
34170
+ #appendCommentBody(numericId, paraId, text2, author, date, options) {
34003
34171
  const commentsView = this.document.ensureComments();
34004
34172
  const commentsRoot = XmlNode2.findRoot(commentsView.tree, "w:comments");
34005
34173
  if (!commentsRoot)
34006
34174
  throw new Error("expected <w:comments> root");
34007
- commentsRoot.children.push(/* @__PURE__ */ jsxDEV(CommentBody, {
34175
+ const comment = /* @__PURE__ */ jsxDEV(CommentBody, {
34008
34176
  options: {
34009
34177
  id: numericId,
34010
34178
  author,
@@ -34013,7 +34181,12 @@ class Comments {
34013
34181
  paraId,
34014
34182
  text: text2
34015
34183
  }
34016
- }, undefined, false, undefined, this));
34184
+ }, undefined, false, undefined, this);
34185
+ if (options?.threadParentNumericId) {
34186
+ commentsView.insertReplyAfter(options.threadParentNumericId, comment);
34187
+ return;
34188
+ }
34189
+ commentsRoot.children.push(comment);
34017
34190
  }
34018
34191
  #resolveBlock(blockId) {
34019
34192
  try {
@@ -34048,6 +34221,7 @@ var init_comments2 = __esm(() => {
34048
34221
  init_parser();
34049
34222
  init_markers();
34050
34223
  init_markers();
34224
+ init_track_changes();
34051
34225
  init_jsx_dev_runtime();
34052
34226
  CommentsError = class CommentsError extends Error {
34053
34227
  code;
@@ -34067,6 +34241,7 @@ function Paragraph({
34067
34241
  alignment,
34068
34242
  list,
34069
34243
  taskState,
34244
+ tabs,
34070
34245
  text: text2,
34071
34246
  runs,
34072
34247
  ...formatting
@@ -34075,7 +34250,7 @@ function Paragraph({
34075
34250
  return /* @__PURE__ */ jsxDEV(w.p, {
34076
34251
  children: [
34077
34252
  /* @__PURE__ */ jsxDEV(ParagraphProperties, {
34078
- options: { style, alignment, list }
34253
+ options: { style, alignment, list, tabs }
34079
34254
  }, undefined, false, undefined, this),
34080
34255
  taskState && /* @__PURE__ */ jsxDEV(TaskCheckbox, {
34081
34256
  checked: taskState === "checked"
@@ -34131,13 +34306,26 @@ function RunElement({ run }) {
34131
34306
  return null;
34132
34307
  }
34133
34308
  function applyParagraphOptionsInPlace(rebuilt, options) {
34134
- if (!options.style && !options.alignment)
34309
+ if (!options.style && !options.alignment && !options.tabs)
34135
34310
  return;
34136
34311
  let pPr = rebuilt.find((child2) => child2.tag === "w:pPr");
34137
34312
  if (!pPr) {
34138
34313
  pPr = new XmlNode2("w:pPr");
34139
34314
  rebuilt.unshift(pPr);
34140
34315
  }
34316
+ if (options.tabs) {
34317
+ pPr.children = pPr.children.filter((child2) => child2.tag !== "w:tabs");
34318
+ if (options.tabs.length > 0) {
34319
+ const tabsNode = new XmlNode2("w:tabs");
34320
+ for (const stop of options.tabs) {
34321
+ tabsNode.children.push(new XmlNode2("w:tab", {
34322
+ "w:val": stop.align,
34323
+ "w:pos": String(Math.round(stop.pos))
34324
+ }));
34325
+ }
34326
+ insertPprChildInOrder(pPr, tabsNode);
34327
+ }
34328
+ }
34141
34329
  if (options.style) {
34142
34330
  const existingStyle = pPr.findChild("w:pStyle");
34143
34331
  if (existingStyle) {
@@ -34244,7 +34432,8 @@ function RunProperties({ run }) {
34244
34432
  function ParagraphProperties({
34245
34433
  options
34246
34434
  }) {
34247
- if (!options.style && !options.alignment && !options.list)
34435
+ const hasTabs = options.tabs !== undefined && options.tabs.length > 0;
34436
+ if (!options.style && !options.alignment && !options.list && !hasTabs)
34248
34437
  return null;
34249
34438
  return /* @__PURE__ */ jsxDEV(w.pPr, {
34250
34439
  children: [
@@ -34261,6 +34450,12 @@ function ParagraphProperties({
34261
34450
  }, undefined, false, undefined, this)
34262
34451
  ]
34263
34452
  }, undefined, true, undefined, this),
34453
+ hasTabs && /* @__PURE__ */ jsxDEV(w.tabs, {
34454
+ children: options.tabs?.map((stop) => /* @__PURE__ */ jsxDEV(w.tab, {
34455
+ "w-val": stop.align,
34456
+ "w-pos": String(Math.round(stop.pos))
34457
+ }, undefined, false, undefined, this))
34458
+ }, undefined, false, undefined, this),
34264
34459
  options.alignment && /* @__PURE__ */ jsxDEV(w.jc, {
34265
34460
  "w-val": options.alignment
34266
34461
  }, undefined, false, undefined, this)
@@ -49400,7 +49595,7 @@ function collectRunText(run) {
49400
49595
  function tokenize(text2) {
49401
49596
  if (text2.length === 0)
49402
49597
  return [];
49403
- return text2.match(/\S+|\s+/g) ?? [];
49598
+ return text2.match(/\S+|\t+|[^\S\t]+/g) ?? [];
49404
49599
  }
49405
49600
  function diffTokens(oldTokens, newTokens) {
49406
49601
  const m2 = oldTokens.length;
@@ -49461,6 +49656,10 @@ function demoteOrphanWhitespaceKeeps(ops) {
49461
49656
  out.push(op2);
49462
49657
  continue;
49463
49658
  }
49659
+ if (op2.old.text.includes("\t")) {
49660
+ out.push(op2);
49661
+ continue;
49662
+ }
49464
49663
  const prev = ops[index - 1];
49465
49664
  const next = ops[index + 1];
49466
49665
  const prevIsEdit = prev !== undefined && prev.kind !== "keep";
@@ -49549,25 +49748,45 @@ function inheritedRprForInsert(ops, index, fallbackRpr) {
49549
49748
  break;
49550
49749
  groupEnd++;
49551
49750
  }
49552
- const deletes = [];
49553
- let insertOrdinal = 0;
49554
- let myInsertOrdinal = -1;
49751
+ const targetText = (() => {
49752
+ const op2 = ops[index];
49753
+ return op2 && op2.kind === "insert" ? op2.text : "";
49754
+ })();
49755
+ const targetIsWord = !isWhitespaceOnly(targetText);
49756
+ const allDeletes = [];
49757
+ const wordDeletes = [];
49758
+ let allInsertOrdinal = 0;
49759
+ let wordInsertOrdinal = 0;
49760
+ let myAllOrdinal = -1;
49761
+ let myWordOrdinal = -1;
49555
49762
  for (let cursor = groupStart;cursor <= groupEnd; cursor++) {
49556
49763
  const op2 = ops[cursor];
49557
49764
  if (!op2)
49558
49765
  continue;
49559
49766
  if (op2.kind === "delete") {
49560
- deletes.push(op2.old.rPr);
49767
+ allDeletes.push(op2.old.rPr);
49768
+ if (!isWhitespaceOnly(op2.old.text))
49769
+ wordDeletes.push(op2.old.rPr);
49561
49770
  continue;
49562
49771
  }
49563
49772
  if (op2.kind === "insert") {
49564
- if (cursor === index)
49565
- myInsertOrdinal = insertOrdinal;
49566
- insertOrdinal++;
49773
+ const isWord = !isWhitespaceOnly(op2.text);
49774
+ if (cursor === index) {
49775
+ myAllOrdinal = allInsertOrdinal;
49776
+ myWordOrdinal = isWord ? wordInsertOrdinal : -1;
49777
+ }
49778
+ allInsertOrdinal++;
49779
+ if (isWord)
49780
+ wordInsertOrdinal++;
49567
49781
  }
49568
49782
  }
49569
- if (deletes.length > 0 && myInsertOrdinal >= 0) {
49570
- const paired = myInsertOrdinal < deletes.length ? deletes[myInsertOrdinal] : deletes[deletes.length - 1];
49783
+ if (targetIsWord && wordDeletes.length > 0 && myWordOrdinal >= 0) {
49784
+ const paired = myWordOrdinal < wordDeletes.length ? wordDeletes[myWordOrdinal] : wordDeletes[wordDeletes.length - 1];
49785
+ if (paired !== undefined)
49786
+ return paired;
49787
+ }
49788
+ if (!targetIsWord && allDeletes.length > 0 && myAllOrdinal >= 0) {
49789
+ const paired = myAllOrdinal < allDeletes.length ? allDeletes[myAllOrdinal] : allDeletes[allDeletes.length - 1];
49571
49790
  if (paired !== undefined)
49572
49791
  return paired;
49573
49792
  }
@@ -49964,6 +50183,14 @@ class Edit {
49964
50183
  }
49965
50184
  return anchorTarget ?? blockRef.node;
49966
50185
  }
50186
+ paragraphProperties(blockRef, options) {
50187
+ if (blockRef.node.tag !== "w:p") {
50188
+ throw new EditError("USAGE", "--style/--alignment alone restyle a paragraph; this locator is not a paragraph.");
50189
+ }
50190
+ this.document.ensureStyles().ensureReferencedStyle(options.style);
50191
+ applyParagraphOptionsInPlace(blockRef.node.children, options);
50192
+ return blockRef.node;
50193
+ }
49967
50194
  span(blockRef, span, replacement, opts = {}) {
49968
50195
  if (blockRef.node.tag !== "w:p") {
49969
50196
  throw new EditError("USAGE", "A character-span locator (pN:S-E) edits text inside a paragraph; this locator does not resolve to a paragraph.");
@@ -80368,16 +80595,20 @@ async function respond(payload) {
80368
80595
  function setVerboseAck(verbose) {
80369
80596
  verboseAck = verbose;
80370
80597
  }
80371
- async function respondAck(payload) {
80598
+ async function respondAck(payload, layoutHint) {
80372
80599
  if (verboseAck) {
80373
80600
  await respond(payload);
80374
80601
  return;
80375
80602
  }
80376
- const line = summarizeAck(payload);
80377
- if (line)
80378
- await writeStdout(`${line}
80603
+ const parts = [summarizeAck(payload), layoutHint].filter((part) => Boolean(part));
80604
+ if (parts.length > 0)
80605
+ await writeStdout(`${parts.join(`
80606
+ `)}
80379
80607
  `);
80380
80608
  }
80609
+ function renderVerifyHint(path2) {
80610
+ return `\u21B3 layout changed \u2014 verify it renders right: docx render ${path2} --out pages/ (read shows text/structure, NOT page layout: columns, wraps, image sizing)`;
80611
+ }
80381
80612
  function summarizeAck(payload) {
80382
80613
  if (!payload || typeof payload !== "object")
80383
80614
  return null;
@@ -80425,13 +80656,16 @@ function ackTarget(ack) {
80425
80656
  return str(ack.path);
80426
80657
  return null;
80427
80658
  }
80428
- async function respondMinted(locators, verbosePayload) {
80659
+ async function respondMinted(locators, verbosePayload, layoutHint) {
80429
80660
  if (verboseAck) {
80430
80661
  await respond(verbosePayload);
80431
80662
  return;
80432
80663
  }
80433
- if (locators.length > 0)
80434
- await writeStdout(`${locators.join(`
80664
+ const parts = [...locators];
80665
+ if (layoutHint)
80666
+ parts.push(layoutHint);
80667
+ if (parts.length > 0)
80668
+ await writeStdout(`${parts.join(`
80435
80669
  `)}
80436
80670
  `);
80437
80671
  }
@@ -80571,254 +80805,36 @@ var init_respond = __esm(() => {
80571
80805
  };
80572
80806
  });
80573
80807
 
80574
- // src/cli/columns/index.tsx
80575
- var exports_columns = {};
80576
- __export(exports_columns, {
80577
- run: () => run
80578
- });
80579
- async function run(args) {
80580
- const parsed = await tryParseArgs(args, OPTION_SPEC, HELP);
80581
- if (typeof parsed === "number")
80582
- return parsed;
80583
- if (parsed.values.help) {
80584
- await writeStdout(HELP);
80585
- return EXIT2.OK;
80586
- }
80587
- setVerboseAck(Boolean(parsed.values.verbose));
80588
- const filePath = parsed.positionals[0];
80589
- if (!filePath)
80590
- return fail("USAGE", "Missing FILE argument", HELP);
80591
- const at = parsed.values.at;
80592
- if (!at) {
80593
- return fail("USAGE", "Missing --at LOCATOR (a range pN-pM or a section sN)", HELP);
80594
- }
80595
- const count = parseCount(parsed.values.count);
80596
- if (typeof count === "number" && Number.isNaN(count)) {
80597
- return fail("USAGE", `--count must be a positive integer`, HELP);
80598
- }
80599
- if (count === undefined)
80600
- return fail("USAGE", "Missing --count N", HELP);
80601
- const typeRaw = parsed.values.type;
80602
- if (typeRaw !== undefined && !isSectionType(typeRaw)) {
80603
- return fail("USAGE", `Invalid --type: ${typeRaw}`, "Valid values: continuous, nextPage, evenPage, oddPage, nextColumn");
80604
- }
80605
- let locator;
80606
- try {
80607
- locator = parseLocator(at);
80608
- } catch (error) {
80609
- if (error instanceof LocatorParseError) {
80610
- return fail("INVALID_LOCATOR", error.message, HELP);
80611
- }
80612
- throw error;
80613
- }
80614
- const document4 = await openOrFail(filePath);
80615
- if (typeof document4 === "number")
80616
- return document4;
80617
- const opts = {
80618
- filePath,
80619
- count,
80620
- sectionType: typeRaw,
80621
- authorFlag: parsed.values.author,
80622
- trackFlag: Boolean(parsed.values.track),
80623
- outputPath: parsed.values.output,
80624
- dryRun: Boolean(parsed.values["dry-run"])
80625
- };
80626
- if (locator.kind === "blockRange") {
80627
- const range = await resolveBlockRangeOrFail(document4, at);
80628
- if (typeof range === "number")
80629
- return range;
80630
- return wrapRange(document4, range.parent, range.startIndex, range.endIndex, at, opts);
80631
- }
80632
- const blockRef = await resolveBlockOrFail(document4, at);
80633
- if (typeof blockRef === "number")
80634
- return blockRef;
80635
- if (blockRef.node.tag === "w:sectPr") {
80636
- return editSection(document4, blockRef, at, opts);
80637
- }
80638
- if (blockRef.node.tag === "w:p") {
80639
- const index2 = blockRef.parent.indexOf(blockRef.node);
80640
- return wrapRange(document4, blockRef.parent, index2, index2, at, opts);
80641
- }
80642
- return fail("INVALID_LOCATOR", `columns needs a section (sN) or a paragraph range (pN-pM); ${at} is neither`, HELP);
80643
- }
80644
- async function editSection(document4, blockRef, at, opts) {
80645
- try {
80646
- new Edit(document4).section(blockRef, { columns: opts.count, sectionType: opts.sectionType }, { authorFlag: opts.authorFlag, track: opts.trackFlag });
80647
- } catch (error) {
80648
- if (error instanceof EditError)
80649
- return fail(error.code, error.message);
80650
- throw error;
80651
- }
80652
- return commit(document4, opts, { at, mode: "section" });
80653
- }
80654
- async function wrapRange(document4, parent, startIndex, endIndex, at, opts) {
80655
- if (parent !== document4.body.findBodyChildren()) {
80656
- return fail("USAGE", `columns can only wrap body-level paragraphs; ${at} is inside a table cell`, "Section breaks carry column layout and cannot live in a table cell.");
80657
- }
80658
- for (let index2 = startIndex;index2 <= endIndex; index2++) {
80659
- if (containsSectionBreak(parent[index2])) {
80660
- return fail("USAGE", `The range ${at} already contains a section break`, "Target the existing section directly with `--at sN`, or pick a range without one.");
80661
- }
80662
- }
80663
- const governing = governingColumns(parent, endIndex + 1);
80664
- const track = resolveTracked(document4, opts.trackFlag);
80665
- const insert = new Insert(document4);
80666
- const allocator = track ? new TrackChanges(document4).createAllocator() : undefined;
80667
- const endNode = parent[endIndex];
80668
- if (!endNode) {
80669
- return fail("BLOCK_NOT_FOUND", "Range end is stale (parent does not contain it)");
80670
- }
80671
- const startNode = startIndex > 0 ? parent[startIndex] : undefined;
80672
- if (startIndex > 0 && !startNode) {
80673
- return fail("BLOCK_NOT_FOUND", "Range start is stale (parent does not contain it)");
80674
- }
80675
- let afterBlocks;
80676
- let beforeBlocks = [];
80677
- try {
80678
- afterBlocks = await insert.paragraph({ node: endNode, parent }, {
80679
- kind: "section",
80680
- columns: opts.count,
80681
- sectionType: opts.sectionType ?? "continuous"
80682
- }, {}, { placement: "after", authorFlag: opts.authorFlag, track, allocator });
80683
- if (startNode) {
80684
- beforeBlocks = await insert.paragraph({ node: startNode, parent }, {
80685
- kind: "section",
80686
- ...governing > 1 ? { columns: governing } : {},
80687
- sectionType: "continuous"
80688
- }, {}, { placement: "before", authorFlag: opts.authorFlag, track, allocator });
80689
- }
80690
- } catch (error) {
80691
- if (error instanceof InsertError)
80692
- return fail(error.code, error.message, error.hint);
80693
- throw error;
80694
- }
80695
- if (opts.dryRun) {
80696
- return commit(document4, opts, { at, mode: "range", dryRunOnly: true });
80697
- }
80698
- parent.splice(endIndex + 1, 0, ...afterBlocks);
80699
- parent.splice(startIndex, 0, ...beforeBlocks);
80700
- return commit(document4, opts, { at, mode: "range" });
80701
- }
80702
- function containsSectionBreak(node2) {
80703
- if (!node2)
80704
- return false;
80705
- if (node2.tag === "w:sectPr")
80706
- return true;
80707
- if (node2.tag !== "w:p")
80708
- return false;
80709
- return Boolean(node2.findChild("w:pPr")?.findChild("w:sectPr"));
80710
- }
80711
- function governingColumns(parent, fromIndex) {
80712
- for (let index2 = fromIndex;index2 < parent.length; index2++) {
80713
- const node2 = parent[index2];
80714
- if (!node2)
80715
- continue;
80716
- if (node2.tag === "w:sectPr")
80717
- return columnsOf(node2);
80718
- if (node2.tag === "w:p") {
80719
- const inline = node2.findChild("w:pPr")?.findChild("w:sectPr");
80720
- if (inline)
80721
- return columnsOf(inline);
80722
- }
80723
- }
80724
- return 1;
80808
+ // src/cli/parse-helpers.ts
80809
+ function detectMarkdownInPlainText(text6) {
80810
+ if (/\*\*[^*\n]+\*\*/.test(text6))
80811
+ return "bold (**\u2026**)";
80812
+ if (/__[^_\n]+__/.test(text6))
80813
+ return "bold (__\u2026__)";
80814
+ if (/(^|\n) {0,3}#{1,6}\s/.test(text6))
80815
+ return "a heading (#\u2026)";
80816
+ if (/\[[^\]\n]+\]\([^)\n]+\)/.test(text6))
80817
+ return "a link ([text](url))";
80818
+ return null;
80725
80819
  }
80726
- function columnsOf(sectPr) {
80727
- const raw = sectPr.findChild("w:cols")?.getAttribute("w:num");
80728
- const parsed = raw ? Number.parseInt(raw, 10) : 1;
80729
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
80820
+ async function rejectMarkdownInText(text6, help) {
80821
+ const found = detectMarkdownInPlainText(text6);
80822
+ if (!found)
80823
+ return null;
80824
+ return await fail("USAGE", `--text writes literal characters, but this value looks like markdown: ${found}. It would be baked in verbatim (e.g. literal ** around the word), not rendered.`, `Use --markdown to parse it (handles ${found}, headings, lists, links), --bold/--italic for a uniformly-emphasized run, or --runs for literal text that really contains these characters. Help:
80825
+ ${help}`);
80730
80826
  }
80731
- function parseCount(raw) {
80732
- if (raw === undefined)
80733
- return;
80734
- if (!/^\d+$/.test(raw.trim()))
80735
- return Number.NaN;
80736
- const parsed = Number.parseInt(raw, 10);
80737
- if (!Number.isFinite(parsed) || parsed < 1)
80738
- return Number.NaN;
80739
- return parsed;
80827
+ function detectShellMangledCurrency(text6) {
80828
+ const match = text6.match(/(?<![\d$])([.,]\d{2,})/);
80829
+ return match?.[1] ?? null;
80740
80830
  }
80741
- async function commit(document4, opts, meta) {
80742
- if (opts.dryRun || meta.dryRunOnly) {
80743
- await respond({
80744
- operation: "columns",
80745
- dryRun: true,
80746
- path: opts.filePath,
80747
- locator: meta.at,
80748
- columnCount: opts.count,
80749
- mode: meta.mode,
80750
- ...opts.outputPath ? { output: opts.outputPath } : {}
80751
- });
80752
- return EXIT2.OK;
80753
- }
80754
- await document4.save(opts.outputPath);
80755
- await respondAck({
80756
- ok: true,
80757
- operation: "columns",
80758
- path: opts.outputPath ?? opts.filePath,
80759
- locator: meta.at,
80760
- columnCount: opts.count,
80761
- mode: meta.mode
80762
- });
80763
- return EXIT2.OK;
80831
+ async function rejectShellMangledValue(text6, help, label = "this value") {
80832
+ const fragment = detectShellMangledCurrency(text6);
80833
+ if (!fragment)
80834
+ return null;
80835
+ return await fail("USAGE", `${label} contains "${fragment}" \u2014 a number with no integer part, the signature of a "$" amount gutted by the shell (bash turns double-quoted "$300.00" into ".00" and "$10,000" into ",000"). docx would write the corrupted value verbatim.`, `Wrap any "$"-bearing value in SINGLE quotes ('$300.00') so bash leaves it alone, or supply it via --batch FILE (JSONL never touches the shell). If you really mean "${fragment}", write its integer part (0${fragment}) or use --batch. Help:
80836
+ ${help}`);
80764
80837
  }
80765
- var HELP = `docx columns \u2014 lay text out in multiple columns
80766
-
80767
- Usage:
80768
- docx columns FILE --at LOCATOR --count N [options]
80769
-
80770
- The intuitive verb for column layout, so you don't have to hand-build section
80771
- breaks. Two addressing modes:
80772
-
80773
- --at pN-pM Wrap the paragraph range pN\u2026pM in its own N-column section
80774
- (inserts the bounding continuous section breaks for you). Also
80775
- accepts a single paragraph (--at pN).
80776
- --at sN Set the column count on an EXISTING section break sN (the section
80777
- whose content ENDS at sN). Equivalent to \`edit --at sN --columns N\`.
80778
-
80779
- Options:
80780
- --at LOCATOR Paragraph range (pN-pM), single paragraph (pN), or section (sN)
80781
- --count N Number of columns (>= 1; use 1 to collapse back to single column)
80782
- --type T Section type for the wrapping break: continuous (default),
80783
- nextPage, evenPage, oddPage, nextColumn. Only meaningful with
80784
- a pN-pM/pN range; with sN it overrides the section's type.
80785
- --author NAME Author for tracked changes (default: $DOCX_AUTHOR)
80786
- --track Record as a tracked change even when the doc toggle is off
80787
- -o, --output PATH Write to PATH instead of overwriting FILE
80788
- --dry-run Print what would change; do not write the file
80789
- -v, --verbose Print the full success ack JSON
80790
- -h, --help Show this help
80791
-
80792
- Agent tip: VERIFY LAYOUT VISUALLY. \`docx read\` shows the section as a
80793
- \`<!-- docx:section \u2026 cols="N" -->\` annotation but NOT how the columns actually
80794
- flow on the page (balance, overflow). After setting columns, render and look:
80795
- docx render FILE --out pages/
80796
- Adjust the range and re-render until the columns read the way you intended.
80797
-
80798
- Output:
80799
- Prints a one-line confirmation on success (exit 0); --verbose prints {ok:true, \u2026}. Positional ids shift
80800
- after a range wrap (two section breaks are inserted), so re-read before further
80801
- edits. Errors print {code, error, hint?} + nonzero exit.
80802
-
80803
- Examples:
80804
- docx columns doc.docx --at p4-p9 --count 2
80805
- docx columns doc.docx --at p4-p9 --count 3 --type continuous
80806
- docx columns doc.docx --at s2 --count 1 # collapse section s2 to one column
80807
- `, OPTION_SPEC;
80808
- var init_columns = __esm(() => {
80809
- init_core2();
80810
- init_respond();
80811
- OPTION_SPEC = {
80812
- at: { type: "string" },
80813
- count: { type: "string" },
80814
- type: { type: "string" },
80815
- author: { type: "string" },
80816
- track: { type: "boolean" },
80817
- ...SAVE_FLAGS
80818
- };
80819
- });
80820
-
80821
- // src/cli/parse-helpers.ts
80822
80838
  function parseTaskFlag(value) {
80823
80839
  const normalized = value.toLowerCase();
80824
80840
  if (normalized === "checked" || normalized === "true" || normalized === "1")
@@ -80849,9 +80865,9 @@ async function parseRunsArg(json2) {
80849
80865
  return fail("USAGE", "--runs must be a JSON array of Run objects");
80850
80866
  }
80851
80867
  const runs = parsed;
80852
- for (const run2 of runs) {
80853
- if (run2 !== null && typeof run2 === "object" && run2.type === "text") {
80854
- const invalid = firstInvalidRunFormat(run2);
80868
+ for (const run of runs) {
80869
+ if (run !== null && typeof run === "object" && run.type === "text") {
80870
+ const invalid = firstInvalidRunFormat(run);
80855
80871
  if (invalid) {
80856
80872
  return fail("USAGE", `Invalid ${invalid.field} "${invalid.value}" in a --runs text run`, `Use ${invalid.valid}.`);
80857
80873
  }
@@ -80937,9 +80953,9 @@ var init_parse_helpers = __esm(() => {
80937
80953
  // src/cli/comments/add.tsx
80938
80954
  var exports_add = {};
80939
80955
  __export(exports_add, {
80940
- run: () => run2
80956
+ run: () => run
80941
80957
  });
80942
- async function run2(args) {
80958
+ async function run(args) {
80943
80959
  const parsed = await tryParseArgs(args, {
80944
80960
  at: { type: "string" },
80945
80961
  anchor: { type: "string" },
@@ -80950,17 +80966,17 @@ async function run2(args) {
80950
80966
  current: { type: "boolean" },
80951
80967
  baseline: { type: "boolean" },
80952
80968
  ...SAVE_FLAGS
80953
- }, HELP2);
80969
+ }, HELP);
80954
80970
  if (typeof parsed === "number")
80955
80971
  return parsed;
80956
80972
  if (parsed.values.help) {
80957
- await writeStdout(HELP2);
80973
+ await writeStdout(HELP);
80958
80974
  return EXIT2.OK;
80959
80975
  }
80960
80976
  setVerboseAck(Boolean(parsed.values.verbose));
80961
80977
  const path2 = parsed.positionals[0];
80962
80978
  if (!path2)
80963
- return fail("USAGE", "Missing FILE argument", HELP2);
80979
+ return fail("USAGE", "Missing FILE argument", HELP);
80964
80980
  const atInput = parsed.values.at;
80965
80981
  const anchorInput = parsed.values.anchor;
80966
80982
  const batchInput = parsed.values.batch;
@@ -80975,10 +80991,10 @@ async function run2(args) {
80975
80991
  }
80976
80992
  const anchorCount = (atInput ? 1 : 0) + (anchorInput ? 1 : 0) + (batchInput ? 1 : 0);
80977
80993
  if (anchorCount === 0) {
80978
- return fail("USAGE", "Specify exactly one of --at, --anchor, or --batch", HELP2);
80994
+ return fail("USAGE", "Specify exactly one of --at, --anchor, or --batch", HELP);
80979
80995
  }
80980
80996
  if (anchorCount > 1) {
80981
- return fail("USAGE", "--at, --anchor, and --batch are mutually exclusive", HELP2);
80997
+ return fail("USAGE", "--at, --anchor, and --batch are mutually exclusive", HELP);
80982
80998
  }
80983
80999
  const document4 = await openOrFail(path2);
80984
81000
  if (typeof document4 === "number")
@@ -80986,7 +81002,7 @@ async function run2(args) {
80986
81002
  let rawEntries;
80987
81003
  if (batchInput) {
80988
81004
  if (text6 !== undefined || atInput || anchorInput || occurrenceRaw) {
80989
- return fail("USAGE", "--batch reads each entry's at/anchor/text/author from JSONL \u2014 do not pass them on the CLI", HELP2);
81005
+ return fail("USAGE", "--batch reads each entry's at/anchor/text/author from JSONL \u2014 do not pass them on the CLI", HELP);
80990
81006
  }
80991
81007
  try {
80992
81008
  rawEntries = await readJsonlObjects(batchInput);
@@ -80999,7 +81015,7 @@ async function run2(args) {
80999
81015
  }
81000
81016
  } else {
81001
81017
  if (!text6)
81002
- return fail("USAGE", "Missing --text TEXT", HELP2);
81018
+ return fail("USAGE", "Missing --text TEXT", HELP);
81003
81019
  const occurrence = occurrenceRaw === undefined ? undefined : Number(occurrenceRaw);
81004
81020
  if (occurrence !== undefined && (!Number.isInteger(occurrence) || occurrence < 1)) {
81005
81021
  return fail("USAGE", `--occurrence must be a positive integer (1-indexed), got "${occurrenceRaw}"`);
@@ -81173,14 +81189,14 @@ function resolveAnchorEntry(document4, anchor, occurrence, occurrenceExplicit, t
81173
81189
  locatorString
81174
81190
  };
81175
81191
  }
81176
- var AT_FORMS, HELP2, EntryError;
81192
+ var AT_FORMS, HELP, EntryError;
81177
81193
  var init_add = __esm(() => {
81178
81194
  init_core2();
81179
81195
  init_find();
81180
81196
  init_parse_helpers();
81181
81197
  init_respond();
81182
81198
  AT_FORMS = describeForms(["paragraph", "span", "crossSpan", "cellParagraph", "cellSpan"], " ");
81183
- HELP2 = `docx comments add \u2014 anchor a new comment to a locator or phrase
81199
+ HELP = `docx comments add \u2014 anchor a new comment to a locator or phrase
81184
81200
 
81185
81201
  Usage:
81186
81202
  docx comments add FILE --at LOCATOR --text TEXT [options]
@@ -81254,31 +81270,31 @@ Batch JSONL example:
81254
81270
  // src/cli/comments/delete.ts
81255
81271
  var exports_delete = {};
81256
81272
  __export(exports_delete, {
81257
- run: () => run3
81273
+ run: () => run2
81258
81274
  });
81259
- async function run3(args) {
81275
+ async function run2(args) {
81260
81276
  const parsed = await tryParseArgs(args, {
81261
81277
  at: { type: "string", multiple: true },
81262
81278
  batch: { type: "string" },
81263
81279
  ...SAVE_FLAGS
81264
- }, HELP3);
81280
+ }, HELP2);
81265
81281
  if (typeof parsed === "number")
81266
81282
  return parsed;
81267
81283
  if (parsed.values.help) {
81268
- await writeStdout(HELP3);
81284
+ await writeStdout(HELP2);
81269
81285
  return EXIT2.OK;
81270
81286
  }
81271
81287
  setVerboseAck(Boolean(parsed.values.verbose));
81272
81288
  const path2 = parsed.positionals[0];
81273
81289
  if (!path2)
81274
- return fail("USAGE", "Missing FILE argument", HELP3);
81290
+ return fail("USAGE", "Missing FILE argument", HELP2);
81275
81291
  const atValues = parsed.values.at ?? [];
81276
81292
  const batchInput = parsed.values.batch;
81277
81293
  if (atValues.length > 0 && batchInput) {
81278
- return fail("USAGE", "--at and --batch are mutually exclusive", HELP3);
81294
+ return fail("USAGE", "--at and --batch are mutually exclusive", HELP2);
81279
81295
  }
81280
81296
  if (atValues.length === 0 && !batchInput) {
81281
- return fail("USAGE", "Specify --at cN (repeatable) or --batch FILE", HELP3);
81297
+ return fail("USAGE", "Specify --at cN (repeatable) or --batch FILE", HELP2);
81282
81298
  }
81283
81299
  let rawIds;
81284
81300
  if (batchInput) {
@@ -81333,7 +81349,7 @@ async function run3(args) {
81333
81349
  });
81334
81350
  return EXIT2.OK;
81335
81351
  }
81336
- var HELP3 = `docx comments delete \u2014 remove one or more comments
81352
+ var HELP2 = `docx comments delete \u2014 remove one or more comments
81337
81353
 
81338
81354
  Usage:
81339
81355
  docx comments delete FILE --at cN [--at cM ...] [options]
@@ -81344,7 +81360,8 @@ Target (one required, mutually exclusive):
81344
81360
  --at cN Comment id (e.g., c0). Repeat for multiple ids:
81345
81361
  --at c1 --at c3 --at c5. All ids are validated against
81346
81362
  the pre-mutation tree, so the batch is atomic. The "c"
81347
- prefix is optional.
81363
+ prefix is optional. Deleting a thread parent also
81364
+ deletes its replies.
81348
81365
  --batch PATH JSONL with one {"id": "cN"} per line. Use - for stdin.
81349
81366
 
81350
81367
  Optional:
@@ -81372,23 +81389,23 @@ var init_delete = __esm(() => {
81372
81389
  // src/cli/comments/list.ts
81373
81390
  var exports_list = {};
81374
81391
  __export(exports_list, {
81375
- run: () => run4
81392
+ run: () => run3
81376
81393
  });
81377
- async function run4(args) {
81394
+ async function run3(args) {
81378
81395
  const parsed = await tryParseArgs(args, {
81379
81396
  "include-resolved": { type: "boolean" },
81380
81397
  thread: { type: "string" },
81381
81398
  help: { type: "boolean", short: "h" }
81382
- }, HELP4);
81399
+ }, HELP3);
81383
81400
  if (typeof parsed === "number")
81384
81401
  return parsed;
81385
81402
  if (parsed.values.help) {
81386
- await writeStdout(HELP4);
81403
+ await writeStdout(HELP3);
81387
81404
  return EXIT2.OK;
81388
81405
  }
81389
81406
  const path2 = parsed.positionals[0];
81390
81407
  if (!path2)
81391
- return fail("USAGE", "Missing FILE argument", HELP4);
81408
+ return fail("USAGE", "Missing FILE argument", HELP3);
81392
81409
  const document4 = await openOrFail(path2);
81393
81410
  if (typeof document4 === "number")
81394
81411
  return document4;
@@ -81399,7 +81416,7 @@ async function run4(args) {
81399
81416
  await respond(comments);
81400
81417
  return EXIT2.OK;
81401
81418
  }
81402
- var HELP4 = `docx comments list \u2014 print existing comments as JSON
81419
+ var HELP3 = `docx comments list \u2014 print existing comments as JSON
81403
81420
 
81404
81421
  Usage:
81405
81422
  docx comments list FILE [options]
@@ -81427,31 +81444,31 @@ var init_list3 = __esm(() => {
81427
81444
  // src/cli/comments/reply.tsx
81428
81445
  var exports_reply = {};
81429
81446
  __export(exports_reply, {
81430
- run: () => run5
81447
+ run: () => run4
81431
81448
  });
81432
- async function run5(args) {
81449
+ async function run4(args) {
81433
81450
  const parsed = await tryParseArgs(args, {
81434
81451
  at: { type: "string" },
81435
81452
  text: { type: "string" },
81436
81453
  author: { type: "string" },
81437
81454
  ...SAVE_FLAGS
81438
- }, HELP5);
81455
+ }, HELP4);
81439
81456
  if (typeof parsed === "number")
81440
81457
  return parsed;
81441
81458
  if (parsed.values.help) {
81442
- await writeStdout(HELP5);
81459
+ await writeStdout(HELP4);
81443
81460
  return EXIT2.OK;
81444
81461
  }
81445
81462
  setVerboseAck(Boolean(parsed.values.verbose));
81446
81463
  const path2 = parsed.positionals[0];
81447
81464
  if (!path2)
81448
- return fail("USAGE", "Missing FILE argument", HELP5);
81465
+ return fail("USAGE", "Missing FILE argument", HELP4);
81449
81466
  const parentInput = parsed.values.at;
81450
81467
  const text6 = parsed.values.text;
81451
81468
  if (!parentInput)
81452
- return fail("USAGE", "Missing --at cN", HELP5);
81469
+ return fail("USAGE", "Missing --at cN", HELP4);
81453
81470
  if (!text6)
81454
- return fail("USAGE", "Missing --text TEXT", HELP5);
81471
+ return fail("USAGE", "Missing --text TEXT", HELP4);
81455
81472
  const document4 = await openOrFail(path2);
81456
81473
  if (typeof document4 === "number")
81457
81474
  return document4;
@@ -81468,7 +81485,7 @@ async function run5(args) {
81468
81485
  dryRun: true,
81469
81486
  path: path2,
81470
81487
  commentId: `c${nextId}`,
81471
- parentId: `c${parentNumericId}`,
81488
+ parentId: `c${document4.comments.threadRootId(parentNumericId)}`,
81472
81489
  ...outputPath ? { output: outputPath } : {}
81473
81490
  });
81474
81491
  return EXIT2.OK;
@@ -81488,17 +81505,19 @@ async function run5(args) {
81488
81505
  operation: "comments.reply",
81489
81506
  path: outputPath ?? path2,
81490
81507
  commentId: `c${numericId}`,
81491
- parentId: `c${parentNumericId}`
81508
+ parentId: `c${document4.comments?.threadRootId(parentNumericId) ?? parentNumericId}`
81492
81509
  });
81493
81510
  return EXIT2.OK;
81494
81511
  }
81495
- var HELP5 = `docx comments reply \u2014 reply to an existing comment
81512
+ var HELP4 = `docx comments reply \u2014 reply to an existing comment
81496
81513
 
81497
81514
  Usage:
81498
81515
  docx comments reply FILE --at cN --text TEXT [options]
81499
81516
 
81500
81517
  Required:
81501
81518
  --at cN Parent comment id (e.g., c0). The "c" prefix is optional.
81519
+ Replying to a reply attaches to the thread root (Word
81520
+ threads are single-level); the ack's parentId reports it.
81502
81521
  --text TEXT Reply body
81503
81522
 
81504
81523
  Optional:
@@ -81525,33 +81544,33 @@ var init_reply = __esm(() => {
81525
81544
  // src/cli/comments/resolve.ts
81526
81545
  var exports_resolve = {};
81527
81546
  __export(exports_resolve, {
81528
- run: () => run6
81547
+ run: () => run5
81529
81548
  });
81530
- async function run6(args) {
81549
+ async function run5(args) {
81531
81550
  const parsed = await tryParseArgs(args, {
81532
81551
  at: { type: "string", multiple: true },
81533
81552
  batch: { type: "string" },
81534
81553
  unset: { type: "boolean" },
81535
81554
  ...SAVE_FLAGS
81536
- }, HELP6);
81555
+ }, HELP5);
81537
81556
  if (typeof parsed === "number")
81538
81557
  return parsed;
81539
81558
  if (parsed.values.help) {
81540
- await writeStdout(HELP6);
81559
+ await writeStdout(HELP5);
81541
81560
  return EXIT2.OK;
81542
81561
  }
81543
81562
  setVerboseAck(Boolean(parsed.values.verbose));
81544
81563
  const path2 = parsed.positionals[0];
81545
81564
  if (!path2)
81546
- return fail("USAGE", "Missing FILE argument", HELP6);
81565
+ return fail("USAGE", "Missing FILE argument", HELP5);
81547
81566
  const atValues = parsed.values.at ?? [];
81548
81567
  const batchInput = parsed.values.batch;
81549
81568
  const resolved = !parsed.values.unset;
81550
81569
  if (atValues.length > 0 && batchInput) {
81551
- return fail("USAGE", "--at and --batch are mutually exclusive", HELP6);
81570
+ return fail("USAGE", "--at and --batch are mutually exclusive", HELP5);
81552
81571
  }
81553
81572
  if (atValues.length === 0 && !batchInput) {
81554
- return fail("USAGE", "Specify --at cN (repeatable) or --batch FILE", HELP6);
81573
+ return fail("USAGE", "Specify --at cN (repeatable) or --batch FILE", HELP5);
81555
81574
  }
81556
81575
  let rawIds;
81557
81576
  if (batchInput) {
@@ -81608,7 +81627,7 @@ async function run6(args) {
81608
81627
  });
81609
81628
  return EXIT2.OK;
81610
81629
  }
81611
- var HELP6 = `docx comments resolve \u2014 mark one or more comments resolved
81630
+ var HELP5 = `docx comments resolve \u2014 mark one or more comments resolved
81612
81631
 
81613
81632
  Usage:
81614
81633
  docx comments resolve FILE --at cN [--at cM ...] [options]
@@ -81649,12 +81668,12 @@ var init_resolve2 = __esm(() => {
81649
81668
  // src/cli/comments/index.ts
81650
81669
  var exports_comments = {};
81651
81670
  __export(exports_comments, {
81652
- run: () => run7
81671
+ run: () => run6
81653
81672
  });
81654
- async function run7(args) {
81673
+ async function run6(args) {
81655
81674
  const verb = args[0];
81656
81675
  if (!verb || verb === "--help" || verb === "-h" || verb === "help") {
81657
- await writeStdout(HELP7);
81676
+ await writeStdout(HELP6);
81658
81677
  return verb ? 0 : 2;
81659
81678
  }
81660
81679
  const loader = SUBCOMMANDS[verb];
@@ -81664,7 +81683,7 @@ async function run7(args) {
81664
81683
  const module_ = await loader();
81665
81684
  return module_.run(args.slice(1));
81666
81685
  }
81667
- var SUBCOMMANDS, HELP7 = `docx comments \u2014 manage Word comments
81686
+ var SUBCOMMANDS, HELP6 = `docx comments \u2014 manage Word comments
81668
81687
 
81669
81688
  Usage:
81670
81689
  docx comments <verb> FILE [options]
@@ -81984,9 +82003,9 @@ var init_create = __esm(() => {
81984
82003
  // src/cli/create/index.tsx
81985
82004
  var exports_create = {};
81986
82005
  __export(exports_create, {
81987
- run: () => run8
82006
+ run: () => run7
81988
82007
  });
81989
- async function run8(args) {
82008
+ async function run7(args) {
81990
82009
  const parsed = await tryParseArgs(args, {
81991
82010
  title: { type: "string" },
81992
82011
  author: { type: "string" },
@@ -81996,17 +82015,17 @@ async function run8(args) {
81996
82015
  "dry-run": { type: "boolean" },
81997
82016
  verbose: { type: "boolean", short: "v" },
81998
82017
  help: { type: "boolean", short: "h" }
81999
- }, HELP8);
82018
+ }, HELP7);
82000
82019
  if (typeof parsed === "number")
82001
82020
  return parsed;
82002
82021
  if (parsed.values.help) {
82003
- await writeStdout(HELP8);
82022
+ await writeStdout(HELP7);
82004
82023
  return EXIT2.OK;
82005
82024
  }
82006
82025
  setVerboseAck(Boolean(parsed.values.verbose));
82007
82026
  const path2 = parsed.positionals[0];
82008
82027
  if (!path2) {
82009
- return fail("USAGE", "Missing FILE argument", HELP8);
82028
+ return fail("USAGE", "Missing FILE argument", HELP7);
82010
82029
  }
82011
82030
  const text6 = parsed.values.text;
82012
82031
  const fromPath = parsed.values.from;
@@ -82078,7 +82097,7 @@ async function applyMarkdownToBody(docxPath, markdownPath) {
82078
82097
  await document4.save();
82079
82098
  return { blockCount: blocks.length };
82080
82099
  }
82081
- var HELP8 = `docx create \u2014 create a new minimal .docx
82100
+ var HELP7 = `docx create \u2014 create a new minimal .docx
82082
82101
 
82083
82102
  Usage:
82084
82103
  docx create FILE [options]
@@ -82220,27 +82239,27 @@ var init_batch = __esm(() => {
82220
82239
  // src/cli/delete/index.tsx
82221
82240
  var exports_delete2 = {};
82222
82241
  __export(exports_delete2, {
82223
- run: () => run9
82242
+ run: () => run8
82224
82243
  });
82225
- async function run9(args) {
82226
- const parsed = await tryParseArgs(args, OPTION_SPEC2, HELP9);
82244
+ async function run8(args) {
82245
+ const parsed = await tryParseArgs(args, OPTION_SPEC, HELP8);
82227
82246
  if (typeof parsed === "number")
82228
82247
  return parsed;
82229
82248
  if (parsed.values.help) {
82230
- await writeStdout(HELP9);
82249
+ await writeStdout(HELP8);
82231
82250
  return EXIT2.OK;
82232
82251
  }
82233
82252
  setVerboseAck(Boolean(parsed.values.verbose));
82234
82253
  const filePath = parsed.positionals[0];
82235
82254
  if (!filePath)
82236
- return fail("USAGE", "Missing FILE argument", HELP9);
82255
+ return fail("USAGE", "Missing FILE argument", HELP8);
82237
82256
  const batchSource = parsed.values.batch;
82238
82257
  if (batchSource !== undefined) {
82239
82258
  return runDeleteBatch(filePath, batchSource, parsed.values);
82240
82259
  }
82241
82260
  const locator = parsed.values.at;
82242
82261
  if (!locator)
82243
- return fail("USAGE", "Missing --at LOCATOR (or --batch FILE)", HELP9);
82262
+ return fail("USAGE", "Missing --at LOCATOR (or --batch FILE)", HELP8);
82244
82263
  const opts = {
82245
82264
  filePath,
82246
82265
  locator,
@@ -82358,7 +82377,7 @@ function findBodyChildren(document4) {
82358
82377
  return null;
82359
82378
  return body.children;
82360
82379
  }
82361
- var HELP9 = `docx delete \u2014 remove a block at a locator
82380
+ var HELP8 = `docx delete \u2014 remove a block at a locator
82362
82381
 
82363
82382
  Usage:
82364
82383
  docx delete FILE --at LOCATOR [options]
@@ -82397,7 +82416,7 @@ Tracked behavior:
82397
82416
  the paragraph mark as deleted (accept removes the paragraph by merging it
82398
82417
  forward). Section deletion under tracking emits a [docx-cli] audit comment
82399
82418
  on the owning paragraph if it has runs to anchor on; otherwise (sentinel
82400
- paragraphs from "insert --section" have no runs) the mutation is silent.
82419
+ section sentinel paragraphs have no runs) the mutation is silent.
82401
82420
  delete --at tN under tracking is rejected (tracked table-row deletion is
82402
82421
  not supported).
82403
82422
 
@@ -82410,7 +82429,7 @@ Examples:
82410
82429
  docx delete doc.docx --at t0
82411
82430
  docx delete doc.docx --at s2
82412
82431
  docx delete doc.docx --batch drop.jsonl # {"at":"p26"} per line
82413
- `, OPTION_SPEC2;
82432
+ `, OPTION_SPEC;
82414
82433
  var init_delete2 = __esm(() => {
82415
82434
  init_core2();
82416
82435
  init_comments2();
@@ -82418,7 +82437,7 @@ var init_delete2 = __esm(() => {
82418
82437
  init_replace();
82419
82438
  init_respond();
82420
82439
  init_batch();
82421
- OPTION_SPEC2 = {
82440
+ OPTION_SPEC = {
82422
82441
  at: { type: "string" },
82423
82442
  batch: { type: "string" },
82424
82443
  author: { type: "string" },
@@ -82427,6 +82446,42 @@ var init_delete2 = __esm(() => {
82427
82446
  };
82428
82447
  });
82429
82448
 
82449
+ // src/cli/edit/tabs.ts
82450
+ function parseTabsValue(value) {
82451
+ const normalized = value.trim().toLowerCase();
82452
+ if (normalized === "right" || normalized === "right-margin")
82453
+ return { kind: "right-margin" };
82454
+ if (normalized === "clear" || normalized === "none" || normalized === "off")
82455
+ return { kind: "clear" };
82456
+ const stops = [];
82457
+ for (const token of value.split(",")) {
82458
+ const match = token.trim().match(/^(left|right|center)@([\d.]+)in$/i);
82459
+ const align = (match?.[1] ?? "").toLowerCase();
82460
+ const inches = Number(match?.[2] ?? Number.NaN);
82461
+ if (!match || !TAB_ALIGNS.has(align) || !Number.isFinite(inches)) {
82462
+ return {
82463
+ error: `Invalid --tabs value: "${token.trim()}"`,
82464
+ hint: TABS_HINT
82465
+ };
82466
+ }
82467
+ stops.push({ align, pos: Math.round(inches * 1440) });
82468
+ }
82469
+ return { kind: "explicit", stops };
82470
+ }
82471
+ function resolveTabsDirective(directive, document4) {
82472
+ if (directive.kind === "clear")
82473
+ return [];
82474
+ if (directive.kind === "explicit")
82475
+ return directive.stops;
82476
+ const marginTwips = Math.round(getPageContentWidthEmu(document4) / 635);
82477
+ return [{ align: "right", pos: marginTwips }];
82478
+ }
82479
+ var TAB_ALIGNS, TABS_HINT = "Use `right` (a right tab at the margin \u2014 cures the wrapping LEFT-tab `docx:layout` warning from `read`), `clear`, or explicit stops like `right@7.5in` / `left@1in,right@7.5in`.";
82480
+ var init_tabs = __esm(() => {
82481
+ init_core2();
82482
+ TAB_ALIGNS = new Set(["left", "right", "center"]);
82483
+ });
82484
+
82430
82485
  // src/cli/edit/batch.ts
82431
82486
  async function runEditBatch(filePath, batchSource, values2) {
82432
82487
  const conflicting = SINGLE_SHOT_FLAGS.find((flag) => values2[flag] !== undefined && values2[flag] !== false);
@@ -82512,13 +82567,14 @@ async function runEditBatch(filePath, batchSource, values2) {
82512
82567
  async function resolveEntry2(document4, raw, index2, opts) {
82513
82568
  const present = CONTENT_KEYS.filter((key) => raw[key] !== undefined);
82514
82569
  const hasClear = raw.clear !== undefined;
82515
- if (present.length === 0 && !hasClear) {
82516
- throw new EntryError2("USAGE", `entry ${index2}: no content \u2014 provide one of ${CONTENT_KEYS.join(", ")}, or "clear"`);
82570
+ const hasProps = raw.style !== undefined || raw.alignment !== undefined || raw.tabs !== undefined;
82571
+ if (present.length === 0 && !hasClear && !hasProps) {
82572
+ throw new EntryError2("USAGE", `entry ${index2}: no content \u2014 provide one of ${CONTENT_KEYS.join(", ")}, "clear", or "style"/"alignment"/"tabs" to adjust in place`);
82517
82573
  }
82518
82574
  if (present.length > 1) {
82519
82575
  throw new EntryError2("USAGE", `entry ${index2}: provide exactly one content field, got ${present.join(", ")}`);
82520
82576
  }
82521
- const kind = present[0] ?? "clear";
82577
+ const kind = present[0] ?? (hasClear ? "clear" : "props");
82522
82578
  const at = raw.at;
82523
82579
  if (typeof at !== "string" || at.length === 0) {
82524
82580
  throw new EntryError2("USAGE", `entry ${index2}: "at" is required`);
@@ -82557,6 +82613,13 @@ async function resolveEntry2(document4, raw, index2, opts) {
82557
82613
  async function buildApply(document4, raw, index2, kind, blockRef, span, opts) {
82558
82614
  const author = typeof raw.author === "string" ? raw.author : opts.authorFlag;
82559
82615
  const clearTags = raw.clear !== undefined ? resolveClearOrThrow(raw.clear, index2) : null;
82616
+ if (kind === "props") {
82617
+ if (span) {
82618
+ throw new EntryError2("USAGE", `entry ${index2}: a character span (${raw.at}) can't take "style"/"alignment" \u2014 restyle the whole paragraph (pN).`);
82619
+ }
82620
+ const paragraphOptions = readParagraphOptions(document4, raw, index2);
82621
+ return () => void new Edit(document4).paragraphProperties(blockRef, paragraphOptions);
82622
+ }
82560
82623
  if (kind === "clear") {
82561
82624
  const tags = clearTags ?? new Set;
82562
82625
  return () => new Edit(document4).clearFormatting(blockRef, span, tags);
@@ -82591,9 +82654,12 @@ async function buildApply(document4, raw, index2, kind, blockRef, span, opts) {
82591
82654
  };
82592
82655
  }
82593
82656
  async function buildWholeParagraphContent(document4, raw, index2, kind, blockRef, author, opts) {
82594
- const paragraphOptions = readParagraphOptions(raw, index2);
82657
+ const paragraphOptions = readParagraphOptions(document4, raw, index2);
82595
82658
  if (kind === "text") {
82596
82659
  const text6 = requireString(raw.text, index2, "text");
82660
+ if (text6 === "") {
82661
+ throw new EntryError2("USAGE", `entry ${index2}: empty "text" leaves a blank paragraph in place, it doesn't remove the line (${raw.at}).`, 'Remove lines with `docx delete --batch` ({ "at": "pN" } per line). To keep an empty spacer, use "runs": [] instead.');
82662
+ }
82597
82663
  const format = readTextFormat(raw, index2);
82598
82664
  return () => new Edit(document4).paragraph(blockRef, { kind: "text", text: text6, format, paragraphOptions }, {
82599
82665
  authorFlag: author,
@@ -82691,8 +82757,8 @@ function rejectSpanFormatFlags(raw, index2) {
82691
82757
  if (raw.color !== undefined || raw.bold !== undefined || raw.italic !== undefined) {
82692
82758
  throw new EntryError2("USAGE", `entry ${index2}: --color/--bold/--italic aren't supported on a character span \u2014 the replacement inherits the run's formatting`);
82693
82759
  }
82694
- if (raw.style !== undefined || raw.alignment !== undefined) {
82695
- throw new EntryError2("USAGE", `entry ${index2}: style/alignment apply to a whole paragraph, not a character span`);
82760
+ if (raw.style !== undefined || raw.alignment !== undefined || raw.tabs !== undefined) {
82761
+ throw new EntryError2("USAGE", `entry ${index2}: style/alignment/tabs apply to a whole paragraph, not a character span`);
82696
82762
  }
82697
82763
  }
82698
82764
  function readTextFormat(raw, index2) {
@@ -82709,7 +82775,7 @@ function readTextFormat(raw, index2) {
82709
82775
  out.italic = Boolean(raw.italic);
82710
82776
  return out;
82711
82777
  }
82712
- function readParagraphOptions(raw, index2) {
82778
+ function readParagraphOptions(document4, raw, index2) {
82713
82779
  const out = {};
82714
82780
  if (raw.style !== undefined) {
82715
82781
  if (typeof raw.style !== "string") {
@@ -82724,15 +82790,25 @@ function readParagraphOptions(raw, index2) {
82724
82790
  }
82725
82791
  out.alignment = alignment;
82726
82792
  }
82793
+ if (raw.tabs !== undefined) {
82794
+ if (typeof raw.tabs !== "string") {
82795
+ throw new EntryError2("USAGE", `entry ${index2}: "tabs" must be a string`);
82796
+ }
82797
+ const parsed = parseTabsValue(raw.tabs);
82798
+ if ("error" in parsed) {
82799
+ throw new EntryError2("USAGE", `entry ${index2}: ${parsed.error}`, parsed.hint);
82800
+ }
82801
+ out.tabs = resolveTabsDirective(parsed, document4);
82802
+ }
82727
82803
  return out;
82728
82804
  }
82729
82805
  function readRuns(value, index2) {
82730
82806
  if (!Array.isArray(value)) {
82731
82807
  throw new EntryError2("USAGE", `entry ${index2}: "runs" must be a JSON array of Run objects`);
82732
82808
  }
82733
- for (const run10 of value) {
82734
- if (run10 !== null && typeof run10 === "object" && run10.type === "text") {
82735
- const invalid = firstInvalidRunFormat(run10);
82809
+ for (const run9 of value) {
82810
+ if (run9 !== null && typeof run9 === "object" && run9.type === "text") {
82811
+ const invalid = firstInvalidRunFormat(run9);
82736
82812
  if (invalid) {
82737
82813
  throw new EntryError2("USAGE", `entry ${index2}: invalid ${invalid.field} "${invalid.value}" in a run`, `Use ${invalid.valid}.`);
82738
82814
  }
@@ -82761,6 +82837,7 @@ var init_batch2 = __esm(() => {
82761
82837
  init_run_formatting();
82762
82838
  init_parse_helpers();
82763
82839
  init_respond();
82840
+ init_tabs();
82764
82841
  SINGLE_SHOT_FLAGS = [
82765
82842
  "at",
82766
82843
  "text",
@@ -82778,6 +82855,7 @@ var init_batch2 = __esm(() => {
82778
82855
  "type",
82779
82856
  "style",
82780
82857
  "alignment",
82858
+ "tabs",
82781
82859
  "color",
82782
82860
  "bold",
82783
82861
  "italic"
@@ -82798,20 +82876,20 @@ var init_batch2 = __esm(() => {
82798
82876
  // src/cli/edit/index.tsx
82799
82877
  var exports_edit = {};
82800
82878
  __export(exports_edit, {
82801
- run: () => run10
82879
+ run: () => run9
82802
82880
  });
82803
- async function run10(args) {
82804
- const parsed = await tryParseArgs(args, OPTION_SPEC3, HELP10);
82881
+ async function run9(args) {
82882
+ const parsed = await tryParseArgs(args, OPTION_SPEC2, HELP9);
82805
82883
  if (typeof parsed === "number")
82806
82884
  return parsed;
82807
82885
  if (parsed.values.help) {
82808
- await writeStdout(HELP10);
82886
+ await writeStdout(HELP9);
82809
82887
  return EXIT2.OK;
82810
82888
  }
82811
82889
  setVerboseAck(Boolean(parsed.values.verbose));
82812
82890
  const filePath = parsed.positionals[0];
82813
82891
  if (!filePath)
82814
- return fail("USAGE", "Missing FILE argument", HELP10);
82892
+ return fail("USAGE", "Missing FILE argument", HELP9);
82815
82893
  const batchInput = parsed.values.batch;
82816
82894
  if (batchInput !== undefined) {
82817
82895
  return runEditBatch(filePath, batchInput, parsed.values);
@@ -82822,9 +82900,12 @@ async function run10(args) {
82822
82900
  const document4 = await openOrFail(opts.filePath);
82823
82901
  if (typeof document4 === "number")
82824
82902
  return document4;
82903
+ if (opts.tabsDirective) {
82904
+ injectTabsIntoSpec(opts.spec, resolveTabsDirective(opts.tabsDirective, document4));
82905
+ }
82825
82906
  if (isBlockRangeLocator2(opts.locator)) {
82826
82907
  if (opts.spec.kind === "section") {
82827
- return fail("USAGE", "Range locators (pN-pM) don't accept --columns/--type \u2014 use sN for section edits", HELP10);
82908
+ return fail("USAGE", "Range locators (pN-pM) don't accept --columns/--type \u2014 use sN for section edits", HELP9);
82828
82909
  }
82829
82910
  return commitRangeEdit(document4, opts);
82830
82911
  }
@@ -82857,14 +82938,14 @@ function spanLocatorTarget(locator) {
82857
82938
  async function commitSpanEdit(document4, spanTarget, opts) {
82858
82939
  const spec = opts.spec;
82859
82940
  if (spec.kind !== "text" && spec.kind !== "clear") {
82860
- return fail("USAGE", "A character-span locator (pN:S-E) supports --text or --clear. Use a whole-paragraph locator (pN) for --markdown/--runs/--code.", HELP10);
82941
+ return fail("USAGE", "A character-span locator (pN:S-E) supports --text or --clear. Use a whole-paragraph locator (pN) for --markdown/--runs/--code.", HELP9);
82861
82942
  }
82862
82943
  if (spec.kind === "text") {
82863
82944
  if (spec.format.color || spec.format.bold || spec.format.italic) {
82864
- return fail("USAGE", "--color/--bold/--italic aren't supported on a character span \u2014 the replacement inherits the existing run's formatting. Edit the whole paragraph (pN) to set uniform run formatting.", HELP10);
82945
+ return fail("USAGE", "--color/--bold/--italic aren't supported on a character span \u2014 the replacement inherits the existing run's formatting. Edit the whole paragraph (pN) to set uniform run formatting.", HELP9);
82865
82946
  }
82866
82947
  if (spec.paragraphOptions.style || spec.paragraphOptions.alignment) {
82867
- return fail("USAGE", "--style/--alignment apply to a whole paragraph, not a character span (pN:S-E).", HELP10);
82948
+ return fail("USAGE", "--style/--alignment apply to a whole paragraph, not a character span (pN:S-E).", HELP9);
82868
82949
  }
82869
82950
  }
82870
82951
  const blockRef = await resolveBlockOrFail(document4, spanTarget.blockId);
@@ -82928,6 +83009,8 @@ async function commitBlockEdit(document4, blockRef, opts) {
82928
83009
  });
82929
83010
  } else if (opts.spec.kind === "clear") {
82930
83011
  edit.clearFormatting(blockRef, null, opts.spec.tags);
83012
+ } else if (opts.spec.kind === "paragraphProps") {
83013
+ resultNode = edit.paragraphProperties(blockRef, opts.spec.paragraphOptions);
82931
83014
  } else {
82932
83015
  return fail("USAGE", "Unsupported edit spec for single-block locator");
82933
83016
  }
@@ -82945,13 +83028,16 @@ async function commitBlockEdit(document4, blockRef, opts) {
82945
83028
  }
82946
83029
  async function commitRangeEdit(document4, opts) {
82947
83030
  if (opts.spec.kind === "section") {
82948
- return fail("USAGE", "Section edits don't support range locators", HELP10);
83031
+ return fail("USAGE", "Section edits don't support range locators", HELP9);
82949
83032
  }
82950
83033
  if (opts.spec.kind === "task") {
82951
- return fail("USAGE", "--task takes a single paragraph locator (pN), not a range", HELP10);
83034
+ return fail("USAGE", "--task takes a single paragraph locator (pN), not a range", HELP9);
82952
83035
  }
82953
83036
  if (opts.spec.kind === "equation") {
82954
- return fail("USAGE", "--equation takes a single equation locator (eqN), not a paragraph range", HELP10);
83037
+ return fail("USAGE", "--equation takes a single equation locator (eqN), not a paragraph range", HELP9);
83038
+ }
83039
+ if (opts.spec.kind === "paragraphProps") {
83040
+ return commitRangeProps(document4, opts, opts.spec.paragraphOptions);
82955
83041
  }
82956
83042
  const rangeRef = await resolveBlockRangeOrFail(document4, opts.locator);
82957
83043
  if (typeof rangeRef === "number")
@@ -82983,6 +83069,53 @@ async function commitRangeEdit(document4, opts) {
82983
83069
  await document4.save(opts.outputPath);
82984
83070
  return emitEditAck(opts);
82985
83071
  }
83072
+ async function commitRangeProps(document4, opts, options) {
83073
+ const rangeRef = await resolveBlockRangeOrFail(document4, opts.locator);
83074
+ if (typeof rangeRef === "number")
83075
+ return rangeRef;
83076
+ if (opts.dryRun)
83077
+ return respondDryRun2(opts);
83078
+ let applied = 0;
83079
+ try {
83080
+ const edit = new Edit(document4);
83081
+ for (let index2 = rangeRef.startIndex;index2 <= rangeRef.endIndex; index2++) {
83082
+ const node2 = rangeRef.parent[index2];
83083
+ if (!node2 || node2.tag !== "w:p")
83084
+ continue;
83085
+ const perParagraph = scopeRangeProps(node2, options);
83086
+ if (!perParagraph)
83087
+ continue;
83088
+ edit.paragraphProperties({ node: node2, parent: rangeRef.parent }, perParagraph);
83089
+ applied++;
83090
+ }
83091
+ } catch (error) {
83092
+ if (error instanceof EditError) {
83093
+ return fail(error.code, error.message, error.hint);
83094
+ }
83095
+ throw error;
83096
+ }
83097
+ if (applied === 0) {
83098
+ return fail("BLOCK_NOT_FOUND", options.tabs !== undefined ? `No paragraphs with tab stops in ${opts.locator} \u2014 --tabs only adjusts tab-using lines (the ones \`read\` flags with docx:layout).` : `No paragraphs in ${opts.locator} to restyle.`);
83099
+ }
83100
+ await document4.save(opts.outputPath);
83101
+ return emitEditAck(opts);
83102
+ }
83103
+ function scopeRangeProps(node2, options) {
83104
+ const out = {};
83105
+ if (options.style !== undefined)
83106
+ out.style = options.style;
83107
+ if (options.alignment !== undefined)
83108
+ out.alignment = options.alignment;
83109
+ if (options.tabs !== undefined && paragraphHasTabStops(node2)) {
83110
+ out.tabs = options.tabs;
83111
+ }
83112
+ if (out.style === undefined && out.alignment === undefined && out.tabs === undefined)
83113
+ return null;
83114
+ return out;
83115
+ }
83116
+ function paragraphHasTabStops(node2) {
83117
+ return node2.findChild("w:pPr")?.findChild("w:tabs") !== undefined;
83118
+ }
82986
83119
  async function commitEquationEdit(document4, spec, opts) {
82987
83120
  if (spec.latex === undefined && spec.display === undefined) {
82988
83121
  return fail("USAGE", "--equation requires --equation NEW_LATEX, --display, or --inline");
@@ -83013,10 +83146,17 @@ async function commitEquationEdit(document4, spec, opts) {
83013
83146
  async function validateSingleShotOptions(filePath, values2) {
83014
83147
  const locator = values2.at;
83015
83148
  if (!locator)
83016
- return fail("USAGE", "Missing --at LOCATOR", HELP10);
83149
+ return fail("USAGE", "Missing --at LOCATOR", HELP9);
83017
83150
  const paragraphOptions = await parseParagraphOptions(values2);
83018
83151
  if (typeof paragraphOptions === "number")
83019
83152
  return paragraphOptions;
83153
+ let tabsDirective;
83154
+ if (values2.tabs !== undefined) {
83155
+ const parsed = parseTabsValue(values2.tabs);
83156
+ if ("error" in parsed)
83157
+ return fail("USAGE", parsed.error, parsed.hint);
83158
+ tabsDirective = parsed;
83159
+ }
83020
83160
  const isSectionLocator = /^s\d+$/.test(locator);
83021
83161
  const spec = isSectionLocator ? await validateSectionEdit(values2) : await validateParagraphEdit(values2, paragraphOptions);
83022
83162
  if (typeof spec === "number")
@@ -83037,15 +83177,16 @@ async function validateSingleShotOptions(filePath, values2) {
83037
83177
  outputPath: values2.output,
83038
83178
  dryRun: Boolean(values2["dry-run"]),
83039
83179
  noFormatting: Boolean(values2["no-formatting"]),
83040
- ...clearTags ? { clearTags } : {}
83180
+ ...clearTags ? { clearTags } : {},
83181
+ ...tabsDirective ? { tabsDirective } : {}
83041
83182
  };
83042
83183
  }
83043
83184
  async function validateSectionEdit(values2) {
83044
83185
  if (values2.text !== undefined || values2.runs !== undefined) {
83045
- return fail("USAGE", "Section locators (sN) take --columns and --type, not --text/--runs", HELP10);
83186
+ return fail("USAGE", "Section locators (sN) take --columns and --type, not --text/--runs", HELP9);
83046
83187
  }
83047
83188
  if (values2.columns === undefined && values2.type === undefined) {
83048
- return fail("USAGE", "Section edit requires --columns and/or --type", HELP10);
83189
+ return fail("USAGE", "Section edit requires --columns and/or --type", HELP9);
83049
83190
  }
83050
83191
  const sectionFlags = await parseSectionFlags(values2);
83051
83192
  if (typeof sectionFlags === "number")
@@ -83056,22 +83197,22 @@ async function parseClearTagsOrFail(clearFlag) {
83056
83197
  const raw = Array.isArray(clearFlag) ? clearFlag : [clearFlag];
83057
83198
  const names = raw.flatMap((entry) => entry.split(",")).map((name) => name.trim().toLowerCase()).filter(Boolean);
83058
83199
  if (names.length === 0) {
83059
- return fail("USAGE", "--clear needs an attribute name, or 'all'", HELP10);
83200
+ return fail("USAGE", "--clear needs an attribute name, or 'all'", HELP9);
83060
83201
  }
83061
83202
  const tags = resolveClearTags(names);
83062
83203
  if (!tags) {
83063
- return fail("USAGE", `--clear: unknown attribute in "${raw.join(",")}". Valid: ${CLEARABLE_ATTRS.join(", ")}, all`, HELP10);
83204
+ return fail("USAGE", `--clear: unknown attribute in "${raw.join(",")}". Valid: ${CLEARABLE_ATTRS.join(", ")}, all`, HELP9);
83064
83205
  }
83065
83206
  return tags;
83066
83207
  }
83067
83208
  async function validateParagraphEdit(values2, paragraphOptions) {
83068
83209
  if (values2.columns !== undefined || values2.type !== undefined) {
83069
- return fail("USAGE", "--columns and --type require a section locator (sN)", HELP10);
83210
+ return fail("USAGE", "--columns and --type require a section locator (sN)", HELP9);
83070
83211
  }
83071
83212
  const clearFlag = values2.clear;
83072
83213
  if (clearFlag !== undefined) {
83073
83214
  if (values2.equation !== undefined || values2.display === true || values2.inline === true) {
83074
- return fail("USAGE", "--clear can't be combined with --equation/--display/--inline", HELP10);
83215
+ return fail("USAGE", "--clear can't be combined with --equation/--display/--inline", HELP9);
83075
83216
  }
83076
83217
  const hasContent = [
83077
83218
  "text",
@@ -83108,10 +83249,10 @@ async function validateParagraphEdit(values2, paragraphOptions) {
83108
83249
  taskFlag !== undefined
83109
83250
  ].filter(Boolean).length;
83110
83251
  if (otherContent > 0) {
83111
- return fail("USAGE", "--equation / --display / --inline cannot be combined with --text/--runs/--code/--code-file/--task", HELP10);
83252
+ return fail("USAGE", "--equation / --display / --inline cannot be combined with --text/--runs/--code/--code-file/--task", HELP9);
83112
83253
  }
83113
83254
  if (displayFlag && inlineFlag) {
83114
- return fail("USAGE", "--display and --inline are mutually exclusive", HELP10);
83255
+ return fail("USAGE", "--display and --inline are mutually exclusive", HELP9);
83115
83256
  }
83116
83257
  const displayMode = displayFlag ? true : inlineFlag ? false : undefined;
83117
83258
  return {
@@ -83129,11 +83270,11 @@ async function validateParagraphEdit(values2, paragraphOptions) {
83129
83270
  codeFile !== undefined
83130
83271
  ].filter(Boolean).length;
83131
83272
  if (otherFlags > 0) {
83132
- return fail("USAGE", "--task cannot be combined with --text, --runs, --code, or --code-file", HELP10);
83273
+ return fail("USAGE", "--task cannot be combined with --text, --runs, --code, or --code-file", HELP9);
83133
83274
  }
83134
83275
  const checked = parseTaskFlag(taskFlag);
83135
83276
  if (checked === null) {
83136
- return fail("USAGE", `--task must be "checked" or "unchecked", got "${taskFlag}"`, HELP10);
83277
+ return fail("USAGE", `--task must be "checked" or "unchecked", got "${taskFlag}"`, HELP9);
83137
83278
  }
83138
83279
  return { kind: "task", checked };
83139
83280
  }
@@ -83148,15 +83289,29 @@ async function validateParagraphEdit(values2, paragraphOptions) {
83148
83289
  markdownFile !== undefined
83149
83290
  ].filter(Boolean).length;
83150
83291
  if (contentFlags === 0) {
83151
- return fail("USAGE", "Missing content: pass --text, --runs, --code, --code-file, --markdown, --markdown-file, --task, or --equation", HELP10);
83292
+ if (paragraphOptions.style || paragraphOptions.alignment || values2.tabs !== undefined) {
83293
+ return { kind: "paragraphProps", paragraphOptions };
83294
+ }
83295
+ return fail("USAGE", "Missing content: pass --text, --runs, --code, --code-file, --markdown, --markdown-file, --task, or --equation \u2014 or --style/--alignment/--tabs to adjust the paragraph in place", HELP9);
83152
83296
  }
83153
83297
  if (contentFlags > 1) {
83154
- return fail("USAGE", "Pass only one of --text, --runs, --code, --code-file, --markdown, --markdown-file", HELP10);
83298
+ return fail("USAGE", "Pass only one of --text, --runs, --code, --code-file, --markdown, --markdown-file", HELP9);
83155
83299
  }
83156
83300
  if (language !== undefined && codeInline === undefined && codeFile === undefined) {
83157
- return fail("USAGE", "--language requires --code or --code-file", HELP10);
83301
+ return fail("USAGE", "--language requires --code or --code-file", HELP9);
83158
83302
  }
83159
83303
  if (text6 !== undefined) {
83304
+ const at = values2.at;
83305
+ if (text6 === "" && !(at && spanLocatorTarget(at))) {
83306
+ return fail("USAGE", `Empty --text leaves a blank paragraph in place, it doesn't remove the line.`, `To DELETE the paragraph: \`docx delete --at ${at ?? "pN"}\` (or \`docx delete --batch\` for many). To delete just SOME characters, use a span locator (\`--at pN:S-E --text ""\`). To blank the paragraph but keep an empty spacer, pass --runs '[]'. Help:
83307
+ ${HELP9}`);
83308
+ }
83309
+ const rejected = await rejectMarkdownInText(text6, HELP9);
83310
+ if (typeof rejected === "number")
83311
+ return rejected;
83312
+ const mangled = await rejectShellMangledValue(text6, HELP9, "--text");
83313
+ if (typeof mangled === "number")
83314
+ return mangled;
83160
83315
  return {
83161
83316
  kind: "text",
83162
83317
  text: text6,
@@ -83182,7 +83337,7 @@ async function validateParagraphEdit(values2, paragraphOptions) {
83182
83337
  if (markdownInline !== undefined || markdownFile !== undefined) {
83183
83338
  const conflict = MARKDOWN_INCOMPATIBLE_FLAGS.find((flag) => values2[flag] !== undefined);
83184
83339
  if (conflict) {
83185
- return fail("USAGE", `--${conflict} can't be combined with --markdown / --markdown-file (the markdown source controls block-level styling)`, HELP10);
83340
+ return fail("USAGE", `--${conflict} can't be combined with --markdown / --markdown-file (the markdown source controls block-level styling)`, HELP9);
83186
83341
  }
83187
83342
  const source2 = markdownInline !== undefined ? markdownInline : await loadMarkdownFile(markdownFile);
83188
83343
  if (typeof source2 === "number")
@@ -83239,6 +83394,11 @@ async function parseParagraphOptions(values2) {
83239
83394
  }
83240
83395
  return out;
83241
83396
  }
83397
+ function injectTabsIntoSpec(spec, tabs) {
83398
+ if (spec.kind === "text" || spec.kind === "runs" || spec.kind === "code" || spec.kind === "markdown" || spec.kind === "paragraphProps") {
83399
+ spec.paragraphOptions.tabs = tabs;
83400
+ }
83401
+ }
83242
83402
  async function respondDryRun2(opts) {
83243
83403
  await respond({
83244
83404
  operation: "edit",
@@ -83258,13 +83418,14 @@ async function emitEditAck(opts) {
83258
83418
  });
83259
83419
  return EXIT2.OK;
83260
83420
  }
83261
- var AT_FORMS2, HELP10, MARKDOWN_INCOMPATIBLE_FLAGS, OPTION_SPEC3;
83421
+ var AT_FORMS2, HELP9, MARKDOWN_INCOMPATIBLE_FLAGS, OPTION_SPEC2;
83262
83422
  var init_edit2 = __esm(() => {
83263
83423
  init_core2();
83264
83424
  init_equation();
83265
83425
  init_parse_helpers();
83266
83426
  init_respond();
83267
83427
  init_batch2();
83428
+ init_tabs();
83268
83429
  AT_FORMS2 = describeForms([
83269
83430
  "paragraph",
83270
83431
  "span",
@@ -83274,7 +83435,7 @@ var init_edit2 = __esm(() => {
83274
83435
  "section",
83275
83436
  "equation"
83276
83437
  ], " ");
83277
- HELP10 = `docx edit \u2014 replace a paragraph (or paragraph range), a section, or an equation
83438
+ HELP9 = `docx edit \u2014 replace a paragraph (or paragraph range), a section, or an equation
83278
83439
 
83279
83440
  Usage:
83280
83441
  docx edit FILE --at LOCATOR <content> [options]
@@ -83291,7 +83452,8 @@ ${AT_FORMS2}
83291
83452
  inheriting the existing run's formatting \u2014 paste a locator
83292
83453
  straight from \`docx find\`. See \`docx info locators\`.
83293
83454
 
83294
- Paragraph content (one required for paragraph / range locators):
83455
+ Paragraph content (one required for paragraph / range locators \u2014 UNLESS you pass
83456
+ only --style/--alignment below, which restyle the paragraph in place):
83295
83457
  --text TEXT Replace with a single-run paragraph
83296
83458
  --runs JSON Replace with custom runs (Run[] JSON)
83297
83459
  --code TEXT Replace with a code block \u2014 newlines split into one
@@ -83334,6 +83496,25 @@ Equation editing (requires --at eqN):
83334
83496
  Paragraph options:
83335
83497
  --style NAME Paragraph style (e.g., Heading1)
83336
83498
  --alignment ALIGN left | center | right | justify
83499
+ --tabs SPEC Replace the paragraph's tab stops. SPEC is:
83500
+ right \u2014 a single RIGHT tab flush at the text margin.
83501
+ This is the CURE for the \`docx:layout \u2026 warn=\u2026\`
83502
+ hint \`read\` prints on a line whose trailing
83503
+ content sits on a fixed LEFT tab: a long value
83504
+ (e.g. "San Francisco, CA") overflows the margin
83505
+ and WRAPS to a second line. A right tab at the
83506
+ margin right-aligns it instead, so it never wraps.
83507
+ clear \u2014 remove the paragraph's tab stops.
83508
+ <list> \u2014 explicit stops, e.g. \`right@7.5in\` or
83509
+ \`left@1in,right@7.5in\` (ALIGN@POSin, comma list).
83510
+ Pass any of --style/--alignment/--tabs ALONE (no content flag) to adjust the
83511
+ paragraph in place, keeping its text/runs: \`docx edit doc.docx --at p4 --style
83512
+ Heading1\`, \`docx edit doc.docx --at p9 --tabs right\`. --tabs also rides along
83513
+ with --text (fill the line AND fix its tab in one call), and works per-entry in
83514
+ --batch ({"at":"p9","text":"Harvard University\\tCambridge, MA","tabs":"right"}).
83515
+ ONE-CALL cure: a RANGE locator fixes every wrapping tab line at once \u2014
83516
+ \`docx edit doc.docx --at p9-p38 --tabs right\` (the exact "fix-all" command
83517
+ \`read\` prints at the top when lines wrap; it skips paragraphs with no tab stops).
83337
83518
 
83338
83519
  Run options (only with --text):
83339
83520
  --color HEX Run color, hex (e.g., 800080 for purple)
@@ -83413,7 +83594,7 @@ Batch JSONL example (one edit per line):
83413
83594
  {"at": "p1", "markdown": "## Revised heading"}
83414
83595
  `;
83415
83596
  MARKDOWN_INCOMPATIBLE_FLAGS = ["style", "alignment"];
83416
- OPTION_SPEC3 = {
83597
+ OPTION_SPEC2 = {
83417
83598
  at: { type: "string" },
83418
83599
  batch: { type: "string" },
83419
83600
  text: { type: "string" },
@@ -83432,6 +83613,7 @@ Batch JSONL example (one edit per line):
83432
83613
  type: { type: "string" },
83433
83614
  style: { type: "string" },
83434
83615
  alignment: { type: "string" },
83616
+ tabs: { type: "string" },
83435
83617
  color: { type: "string" },
83436
83618
  bold: { type: "boolean" },
83437
83619
  italic: { type: "boolean" },
@@ -83446,7 +83628,7 @@ Batch JSONL example (one edit per line):
83446
83628
  var exports_add2 = {};
83447
83629
  __export(exports_add2, {
83448
83630
  runAddNote: () => runAddNote,
83449
- run: () => run11
83631
+ run: () => run10
83450
83632
  });
83451
83633
  function helpFor(kind) {
83452
83634
  const verb = kind === "footnote" ? "footnotes" : "endnotes";
@@ -83712,8 +83894,8 @@ function splitMarkdownBlocks(blocks) {
83712
83894
  }
83713
83895
  function runsToXml(runs) {
83714
83896
  const out = [];
83715
- for (const run11 of runs) {
83716
- const element = RunElement({ run: run11 });
83897
+ for (const run10 of runs) {
83898
+ const element = RunElement({ run: run10 });
83717
83899
  if (element)
83718
83900
  out.push(element);
83719
83901
  }
@@ -83751,7 +83933,7 @@ function parseAnchor(input) {
83751
83933
  }
83752
83934
  return null;
83753
83935
  }
83754
- async function run11(args) {
83936
+ async function run10(args) {
83755
83937
  return runAddNote(args, "footnote");
83756
83938
  }
83757
83939
  var ANCHOR_FORMS;
@@ -83773,7 +83955,7 @@ var init_add2 = __esm(() => {
83773
83955
  var exports_delete3 = {};
83774
83956
  __export(exports_delete3, {
83775
83957
  runDeleteNote: () => runDeleteNote,
83776
- run: () => run12
83958
+ run: () => run11
83777
83959
  });
83778
83960
  function helpFor2(kind) {
83779
83961
  const verb = kind === "footnote" ? "footnotes" : "endnotes";
@@ -83882,7 +84064,7 @@ async function runDeleteNote(args, kind) {
83882
84064
  });
83883
84065
  return EXIT2.OK;
83884
84066
  }
83885
- async function run12(args) {
84067
+ async function run11(args) {
83886
84068
  return runDeleteNote(args, "footnote");
83887
84069
  }
83888
84070
  var init_delete3 = __esm(() => {
@@ -83895,7 +84077,7 @@ var init_delete3 = __esm(() => {
83895
84077
  var exports_edit2 = {};
83896
84078
  __export(exports_edit2, {
83897
84079
  runEditNote: () => runEditNote,
83898
- run: () => run13
84080
+ run: () => run12
83899
84081
  });
83900
84082
  function helpFor3(kind) {
83901
84083
  const verb = kind === "footnote" ? "footnotes" : "endnotes";
@@ -84086,8 +84268,8 @@ function splitMarkdownBlocks2(blocks) {
84086
84268
  }
84087
84269
  function runsToXml2(runs) {
84088
84270
  const out = [];
84089
- for (const run13 of runs) {
84090
- const element = RunElement({ run: run13 });
84271
+ for (const run12 of runs) {
84272
+ const element = RunElement({ run: run12 });
84091
84273
  if (element)
84092
84274
  out.push(element);
84093
84275
  }
@@ -84136,7 +84318,7 @@ function NoteParagraph({
84136
84318
  ]
84137
84319
  }, undefined, true, undefined, this);
84138
84320
  }
84139
- async function run13(args) {
84321
+ async function run12(args) {
84140
84322
  return runEditNote(args, "footnote");
84141
84323
  }
84142
84324
  var init_edit3 = __esm(() => {
@@ -84154,7 +84336,7 @@ var init_edit3 = __esm(() => {
84154
84336
  var exports_list2 = {};
84155
84337
  __export(exports_list2, {
84156
84338
  runListNotes: () => runListNotes,
84157
- run: () => run14
84339
+ run: () => run13
84158
84340
  });
84159
84341
  function helpFor4(kind) {
84160
84342
  const verb = kind === "footnote" ? "footnotes" : "endnotes";
@@ -84196,7 +84378,7 @@ async function runListNotes(args, kind) {
84196
84378
  await respond(notes);
84197
84379
  return EXIT2.OK;
84198
84380
  }
84199
- async function run14(args) {
84381
+ async function run13(args) {
84200
84382
  return runListNotes(args, "footnote");
84201
84383
  }
84202
84384
  var init_list4 = __esm(() => {
@@ -84206,12 +84388,12 @@ var init_list4 = __esm(() => {
84206
84388
  // src/cli/endnotes/index.ts
84207
84389
  var exports_endnotes = {};
84208
84390
  __export(exports_endnotes, {
84209
- run: () => run15
84391
+ run: () => run14
84210
84392
  });
84211
- async function run15(args) {
84393
+ async function run14(args) {
84212
84394
  const verb = args[0];
84213
84395
  if (!verb || verb === "--help" || verb === "-h" || verb === "help") {
84214
- await writeStdout(HELP11);
84396
+ await writeStdout(HELP10);
84215
84397
  return verb ? 0 : 2;
84216
84398
  }
84217
84399
  const handler = SUBCOMMANDS2[verb];
@@ -84220,7 +84402,7 @@ async function run15(args) {
84220
84402
  }
84221
84403
  return handler(args.slice(1));
84222
84404
  }
84223
- var SUBCOMMANDS2, HELP11 = `docx endnotes \u2014 author endnotes
84405
+ var SUBCOMMANDS2, HELP10 = `docx endnotes \u2014 author endnotes
84224
84406
 
84225
84407
  Usage:
84226
84408
  docx endnotes <verb> FILE [options]
@@ -84250,7 +84432,7 @@ var init_endnotes = __esm(() => {
84250
84432
  // src/cli/find/index.ts
84251
84433
  var exports_find = {};
84252
84434
  __export(exports_find, {
84253
- run: () => run16
84435
+ run: () => run15
84254
84436
  });
84255
84437
  function withBareHighlightAsAny(args) {
84256
84438
  const out = [];
@@ -84272,7 +84454,7 @@ function withBareHighlightAsAny(args) {
84272
84454
  }
84273
84455
  return out;
84274
84456
  }
84275
- async function run16(args) {
84457
+ async function run15(args) {
84276
84458
  const parsed = await tryParseArgs(withBareHighlightAsAny(args), {
84277
84459
  regex: { type: "boolean" },
84278
84460
  "ignore-case": { type: "boolean" },
@@ -84288,17 +84470,17 @@ async function run16(args) {
84288
84470
  underline: { type: "boolean" },
84289
84471
  json: { type: "boolean" },
84290
84472
  help: { type: "boolean", short: "h" }
84291
- }, HELP12);
84473
+ }, HELP11);
84292
84474
  if (typeof parsed === "number")
84293
84475
  return parsed;
84294
84476
  if (parsed.values.help) {
84295
- await writeStdout(HELP12);
84477
+ await writeStdout(HELP11);
84296
84478
  return EXIT2.OK;
84297
84479
  }
84298
84480
  const path2 = parsed.positionals[0];
84299
84481
  const query = parsed.positionals[1];
84300
84482
  if (!path2)
84301
- return fail("USAGE", "Missing FILE argument", HELP12);
84483
+ return fail("USAGE", "Missing FILE argument", HELP11);
84302
84484
  const formatFilter = {};
84303
84485
  if (parsed.values.highlight !== undefined)
84304
84486
  formatFilter.highlight = parsed.values.highlight;
@@ -84312,10 +84494,10 @@ async function run16(args) {
84312
84494
  formatFilter.underline = true;
84313
84495
  const hasFormatFilter = Object.keys(formatFilter).length > 0;
84314
84496
  if (query == null && !hasFormatFilter) {
84315
- return fail("USAGE", "Missing QUERY (or a --highlight/--color/--bold/--italic/--underline filter)", HELP12);
84497
+ return fail("USAGE", "Missing QUERY (or a --highlight/--color/--bold/--italic/--underline filter)", HELP11);
84316
84498
  }
84317
84499
  if (query != null && hasFormatFilter) {
84318
- return fail("USAGE", "Pass a text QUERY or formatting filters (--highlight/--color/...), not both", HELP12);
84500
+ return fail("USAGE", "Pass a text QUERY or formatting filters (--highlight/--color/...), not both", HELP11);
84319
84501
  }
84320
84502
  const ignoreCase = Boolean(parsed.values["ignore-case"]);
84321
84503
  const useRegex = Boolean(parsed.values.regex);
@@ -84390,7 +84572,7 @@ async function run16(args) {
84390
84572
  }
84391
84573
  return EXIT2.OK;
84392
84574
  }
84393
- var HELP12 = `docx find \u2014 locate text spans and return their locators
84575
+ var HELP11 = `docx find \u2014 locate text spans and return their locators
84394
84576
 
84395
84577
  Usage:
84396
84578
  docx find FILE QUERY [options]
@@ -84463,12 +84645,12 @@ var init_find2 = __esm(() => {
84463
84645
  // src/cli/footnotes/index.ts
84464
84646
  var exports_footnotes = {};
84465
84647
  __export(exports_footnotes, {
84466
- run: () => run17
84648
+ run: () => run16
84467
84649
  });
84468
- async function run17(args) {
84650
+ async function run16(args) {
84469
84651
  const verb = args[0];
84470
84652
  if (!verb || verb === "--help" || verb === "-h" || verb === "help") {
84471
- await writeStdout(HELP13);
84653
+ await writeStdout(HELP12);
84472
84654
  return verb ? 0 : 2;
84473
84655
  }
84474
84656
  const loader = SUBCOMMANDS3[verb];
@@ -84478,7 +84660,7 @@ async function run17(args) {
84478
84660
  const module_ = await loader();
84479
84661
  return module_.run(args.slice(1));
84480
84662
  }
84481
- var SUBCOMMANDS3, HELP13 = `docx footnotes \u2014 author footnotes
84663
+ var SUBCOMMANDS3, HELP12 = `docx footnotes \u2014 author footnotes
84482
84664
 
84483
84665
  Usage:
84484
84666
  docx footnotes <verb> FILE [options]
@@ -84502,7 +84684,7 @@ var init_footnotes = __esm(() => {
84502
84684
  });
84503
84685
 
84504
84686
  // src/core/hyperlinks/wrap.tsx
84505
- function wrapSpanInHyperlink(paragraph2, span, relationshipId) {
84687
+ function wrapSpanInHyperlink(paragraph2, span, relationshipId, applyStyle = true) {
84506
84688
  if (span.start >= span.end) {
84507
84689
  throw new HyperlinkWrapError(`Empty or inverted span ${span.start}-${span.end}`);
84508
84690
  }
@@ -84513,6 +84695,9 @@ function wrapSpanInHyperlink(paragraph2, span, relationshipId) {
84513
84695
  const placeWrapper = () => {
84514
84696
  if (placed || wrappedRuns.length === 0)
84515
84697
  return;
84698
+ if (applyStyle)
84699
+ for (const run17 of wrappedRuns)
84700
+ applyHyperlinkRunStyle(run17);
84516
84701
  newChildren.push(hyperlinkWrapper(relationshipId, wrappedRuns));
84517
84702
  wrappedRuns.length = 0;
84518
84703
  placed = true;
@@ -84573,6 +84758,20 @@ function hyperlinkWrapper(relationshipId, runs) {
84573
84758
  children: runs
84574
84759
  }, undefined, false, undefined, this);
84575
84760
  }
84761
+ function applyHyperlinkRunStyle(run17) {
84762
+ if (run17.tag !== "w:r")
84763
+ return;
84764
+ let rPr = run17.findChild("w:rPr");
84765
+ if (!rPr) {
84766
+ rPr = /* @__PURE__ */ jsxDEV(w.rPr, {}, undefined, false, undefined, this);
84767
+ run17.children.unshift(rPr);
84768
+ }
84769
+ if (rPr.findChild("w:rStyle"))
84770
+ return;
84771
+ rPr.children.unshift(/* @__PURE__ */ jsxDEV(w.rStyle, {
84772
+ "w-val": "Hyperlink"
84773
+ }, undefined, false, undefined, this));
84774
+ }
84576
84775
  var HyperlinkWrapError;
84577
84776
  var init_wrap = __esm(() => {
84578
84777
  init_jsx();
@@ -84594,7 +84793,10 @@ class Hyperlinks {
84594
84793
  }
84595
84794
  add(paragraph2, span, url, options = {}) {
84596
84795
  const relationshipId = this.document.relationships.addHyperlink(url);
84597
- wrapSpanInHyperlink(paragraph2, span, relationshipId);
84796
+ const applyStyle = options.style ?? true;
84797
+ if (applyStyle)
84798
+ this.document.ensureStyles().ensureStyle("Hyperlink");
84799
+ wrapSpanInHyperlink(paragraph2, span, relationshipId, applyStyle);
84598
84800
  this.document.relationships.hyperlinksByRelationshipId.set(relationshipId, {
84599
84801
  url
84600
84802
  });
@@ -84697,31 +84899,32 @@ var init_hyperlinks = __esm(() => {
84697
84899
  // src/cli/hyperlinks/add.tsx
84698
84900
  var exports_add3 = {};
84699
84901
  __export(exports_add3, {
84700
- run: () => run18
84902
+ run: () => run17
84701
84903
  });
84702
- async function run18(args) {
84904
+ async function run17(args) {
84703
84905
  const parsed = await tryParseArgs(args, {
84704
84906
  at: { type: "string" },
84705
84907
  url: { type: "string" },
84706
84908
  author: { type: "string" },
84909
+ "no-style": { type: "boolean" },
84707
84910
  ...SAVE_FLAGS
84708
- }, HELP14);
84911
+ }, HELP13);
84709
84912
  if (typeof parsed === "number")
84710
84913
  return parsed;
84711
84914
  if (parsed.values.help) {
84712
- await writeStdout(HELP14);
84915
+ await writeStdout(HELP13);
84713
84916
  return EXIT2.OK;
84714
84917
  }
84715
84918
  setVerboseAck(Boolean(parsed.values.verbose));
84716
84919
  const path2 = parsed.positionals[0];
84717
84920
  if (!path2)
84718
- return fail("USAGE", "Missing FILE argument", HELP14);
84921
+ return fail("USAGE", "Missing FILE argument", HELP13);
84719
84922
  const atInput = parsed.values.at;
84720
84923
  const url = parsed.values.url;
84721
84924
  if (!atInput)
84722
- return fail("USAGE", "Missing --at LOCATOR", HELP14);
84925
+ return fail("USAGE", "Missing --at LOCATOR", HELP13);
84723
84926
  if (!url)
84724
- return fail("USAGE", "Missing --url URL", HELP14);
84927
+ return fail("USAGE", "Missing --url URL", HELP13);
84725
84928
  let locator;
84726
84929
  try {
84727
84930
  locator = parseLocator(atInput);
@@ -84755,7 +84958,8 @@ async function run18(args) {
84755
84958
  }
84756
84959
  try {
84757
84960
  new Hyperlinks(document4).add(paragraphRef.node, target.span, url, {
84758
- author: parsed.values.author
84961
+ author: parsed.values.author,
84962
+ style: !parsed.values["no-style"]
84759
84963
  });
84760
84964
  } catch (error) {
84761
84965
  if (error instanceof HyperlinkWrapError) {
@@ -84784,27 +84988,27 @@ async function findMintedHyperlink(savedPath, blockId, spanStart) {
84784
84988
  let offset2 = 0;
84785
84989
  let foundId;
84786
84990
  let text6 = "";
84787
- for (const run19 of block.runs) {
84788
- if (run19.type !== "text")
84991
+ for (const run18 of block.runs) {
84992
+ if (run18.type !== "text")
84789
84993
  continue;
84790
- const runEnd = offset2 + run19.text.length;
84791
- if (foundId === undefined && run19.hyperlink && spanStart < runEnd) {
84792
- foundId = run19.hyperlink.id;
84994
+ const runEnd = offset2 + run18.text.length;
84995
+ if (foundId === undefined && run18.hyperlink && spanStart < runEnd) {
84996
+ foundId = run18.hyperlink.id;
84793
84997
  }
84794
- if (foundId !== undefined && run19.hyperlink?.id === foundId) {
84795
- text6 += run19.text;
84998
+ if (foundId !== undefined && run18.hyperlink?.id === foundId) {
84999
+ text6 += run18.text;
84796
85000
  }
84797
85001
  offset2 = runEnd;
84798
85002
  }
84799
85003
  return foundId === undefined ? undefined : { id: foundId, text: text6 };
84800
85004
  }
84801
- var AT_FORMS3, HELP14;
85005
+ var AT_FORMS3, HELP13;
84802
85006
  var init_add3 = __esm(() => {
84803
85007
  init_core2();
84804
85008
  init_hyperlinks();
84805
85009
  init_respond();
84806
85010
  AT_FORMS3 = describeForms(["span", "cellSpan"], " ");
84807
- HELP14 = `docx hyperlinks add \u2014 wrap an existing span in a hyperlink
85011
+ HELP13 = `docx hyperlinks add \u2014 wrap an existing span in a hyperlink
84808
85012
 
84809
85013
  Usage:
84810
85014
  docx hyperlinks add FILE --at LOCATOR --url URL [options]
@@ -84818,6 +85022,9 @@ ${AT_FORMS3}
84818
85022
  --url URL Target URL
84819
85023
 
84820
85024
  Optional:
85025
+ --no-style Don't apply Word's "Hyperlink" character style (blue +
85026
+ underline). By default the wrapped text is styled so the link
85027
+ is visibly a link; pass this to keep the original run look.
84821
85028
  --author NAME Author for the audit comment when track-changes is on
84822
85029
  (default: $DOCX_AUTHOR)
84823
85030
  -o, --output PATH Write to PATH instead of overwriting FILE
@@ -84851,27 +85058,27 @@ Examples:
84851
85058
  // src/cli/hyperlinks/delete.ts
84852
85059
  var exports_delete4 = {};
84853
85060
  __export(exports_delete4, {
84854
- run: () => run19
85061
+ run: () => run18
84855
85062
  });
84856
- async function run19(args) {
85063
+ async function run18(args) {
84857
85064
  const parsed = await tryParseArgs(args, {
84858
85065
  at: { type: "string" },
84859
85066
  author: { type: "string" },
84860
85067
  ...SAVE_FLAGS
84861
- }, HELP15);
85068
+ }, HELP14);
84862
85069
  if (typeof parsed === "number")
84863
85070
  return parsed;
84864
85071
  if (parsed.values.help) {
84865
- await writeStdout(HELP15);
85072
+ await writeStdout(HELP14);
84866
85073
  return EXIT2.OK;
84867
85074
  }
84868
85075
  setVerboseAck(Boolean(parsed.values.verbose));
84869
85076
  const path2 = parsed.positionals[0];
84870
85077
  if (!path2)
84871
- return fail("USAGE", "Missing FILE argument", HELP15);
85078
+ return fail("USAGE", "Missing FILE argument", HELP14);
84872
85079
  const targetId = parsed.values.at;
84873
85080
  if (!targetId)
84874
- return fail("USAGE", "Missing --at linkN", HELP15);
85081
+ return fail("USAGE", "Missing --at linkN", HELP14);
84875
85082
  const document4 = await openOrFail(path2);
84876
85083
  if (typeof document4 === "number")
84877
85084
  return document4;
@@ -84912,13 +85119,13 @@ async function run19(args) {
84912
85119
  });
84913
85120
  return EXIT2.OK;
84914
85121
  }
84915
- var AT_FORMS4, HELP15;
85122
+ var AT_FORMS4, HELP14;
84916
85123
  var init_delete4 = __esm(() => {
84917
85124
  init_core2();
84918
85125
  init_hyperlinks();
84919
85126
  init_respond();
84920
85127
  AT_FORMS4 = describeForms(["hyperlink"], " ");
84921
- HELP15 = `docx hyperlinks delete \u2014 unwrap a hyperlink (keep the text)
85128
+ HELP14 = `docx hyperlinks delete \u2014 unwrap a hyperlink (keep the text)
84922
85129
 
84923
85130
  Usage:
84924
85131
  docx hyperlinks delete FILE --at linkN [options]
@@ -84957,21 +85164,21 @@ Examples:
84957
85164
  // src/cli/hyperlinks/list.ts
84958
85165
  var exports_list3 = {};
84959
85166
  __export(exports_list3, {
84960
- run: () => run20
85167
+ run: () => run19
84961
85168
  });
84962
- async function run20(args) {
85169
+ async function run19(args) {
84963
85170
  const parsed = await tryParseArgs(args, {
84964
85171
  help: { type: "boolean", short: "h" }
84965
- }, HELP16);
85172
+ }, HELP15);
84966
85173
  if (typeof parsed === "number")
84967
85174
  return parsed;
84968
85175
  if (parsed.values.help) {
84969
- await writeStdout(HELP16);
85176
+ await writeStdout(HELP15);
84970
85177
  return EXIT2.OK;
84971
85178
  }
84972
85179
  const path2 = parsed.positionals[0];
84973
85180
  if (!path2)
84974
- return fail("USAGE", "Missing FILE argument", HELP16);
85181
+ return fail("USAGE", "Missing FILE argument", HELP15);
84975
85182
  const document4 = await openOrFail(path2);
84976
85183
  if (typeof document4 === "number")
84977
85184
  return document4;
@@ -84984,10 +85191,10 @@ function collectHyperlinks(blocks, entries) {
84984
85191
  for (const block of iterateBlocks(blocks)) {
84985
85192
  if (block.type !== "paragraph")
84986
85193
  continue;
84987
- for (const run21 of block.runs) {
84988
- if (run21.type !== "text" || !run21.hyperlink)
85194
+ for (const run20 of block.runs) {
85195
+ if (run20.type !== "text" || !run20.hyperlink)
84989
85196
  continue;
84990
- addToEntry(entries, run21.hyperlink, block.id, run21.text);
85197
+ addToEntry(entries, run20.hyperlink, block.id, run20.text);
84991
85198
  }
84992
85199
  }
84993
85200
  }
@@ -85010,7 +85217,7 @@ function addToEntry(entries, hyperlink, blockId, text6) {
85010
85217
  entry.tooltip = hyperlink.tooltip;
85011
85218
  entries.set(hyperlink.id, entry);
85012
85219
  }
85013
- var HELP16 = `docx hyperlinks list \u2014 print hyperlink manifest as JSON
85220
+ var HELP15 = `docx hyperlinks list \u2014 print hyperlink manifest as JSON
85014
85221
 
85015
85222
  Usage:
85016
85223
  docx hyperlinks list FILE [options]
@@ -85036,31 +85243,31 @@ var init_list5 = __esm(() => {
85036
85243
  // src/cli/hyperlinks/replace.ts
85037
85244
  var exports_replace = {};
85038
85245
  __export(exports_replace, {
85039
- run: () => run21
85246
+ run: () => run20
85040
85247
  });
85041
- async function run21(args) {
85248
+ async function run20(args) {
85042
85249
  const parsed = await tryParseArgs(args, {
85043
85250
  at: { type: "string" },
85044
85251
  with: { type: "string" },
85045
85252
  author: { type: "string" },
85046
85253
  ...SAVE_FLAGS
85047
- }, HELP17);
85254
+ }, HELP16);
85048
85255
  if (typeof parsed === "number")
85049
85256
  return parsed;
85050
85257
  if (parsed.values.help) {
85051
- await writeStdout(HELP17);
85258
+ await writeStdout(HELP16);
85052
85259
  return EXIT2.OK;
85053
85260
  }
85054
85261
  setVerboseAck(Boolean(parsed.values.verbose));
85055
85262
  const path2 = parsed.positionals[0];
85056
85263
  if (!path2)
85057
- return fail("USAGE", "Missing FILE argument", HELP17);
85264
+ return fail("USAGE", "Missing FILE argument", HELP16);
85058
85265
  const targetId = parsed.values.at;
85059
85266
  if (!targetId)
85060
- return fail("USAGE", "Missing --at linkN", HELP17);
85267
+ return fail("USAGE", "Missing --at linkN", HELP16);
85061
85268
  const newUrl = parsed.values.with;
85062
85269
  if (!newUrl)
85063
- return fail("USAGE", "Missing --with URL", HELP17);
85270
+ return fail("USAGE", "Missing --with URL", HELP16);
85064
85271
  const document4 = await openOrFail(path2);
85065
85272
  if (typeof document4 === "number")
85066
85273
  return document4;
@@ -85099,13 +85306,13 @@ async function run21(args) {
85099
85306
  });
85100
85307
  return EXIT2.OK;
85101
85308
  }
85102
- var AT_FORMS5, HELP17;
85309
+ var AT_FORMS5, HELP16;
85103
85310
  var init_replace2 = __esm(() => {
85104
85311
  init_core2();
85105
85312
  init_hyperlinks();
85106
85313
  init_respond();
85107
85314
  AT_FORMS5 = describeForms(["hyperlink"], " ");
85108
- HELP17 = `docx hyperlinks replace \u2014 change a hyperlink's URL
85315
+ HELP16 = `docx hyperlinks replace \u2014 change a hyperlink's URL
85109
85316
 
85110
85317
  Usage:
85111
85318
  docx hyperlinks replace FILE --at linkN --with URL [options]
@@ -85146,12 +85353,12 @@ Examples:
85146
85353
  // src/cli/hyperlinks/index.ts
85147
85354
  var exports_hyperlinks = {};
85148
85355
  __export(exports_hyperlinks, {
85149
- run: () => run22
85356
+ run: () => run21
85150
85357
  });
85151
- async function run22(args) {
85358
+ async function run21(args) {
85152
85359
  const verb = args[0];
85153
85360
  if (!verb || verb === "--help" || verb === "-h" || verb === "help") {
85154
- await writeStdout(HELP18);
85361
+ await writeStdout(HELP17);
85155
85362
  return verb ? 0 : 2;
85156
85363
  }
85157
85364
  const loader = SUBCOMMANDS4[verb];
@@ -85161,7 +85368,7 @@ async function run22(args) {
85161
85368
  const module_ = await loader();
85162
85369
  return module_.run(args.slice(1));
85163
85370
  }
85164
- var SUBCOMMANDS4, HELP18 = `docx hyperlinks \u2014 manage hyperlinks
85371
+ var SUBCOMMANDS4, HELP17 = `docx hyperlinks \u2014 manage hyperlinks
85165
85372
 
85166
85373
  Usage:
85167
85374
  docx hyperlinks <verb> FILE [options]
@@ -85380,24 +85587,24 @@ var init_batch3 = __esm(() => {
85380
85587
  // src/cli/insert/index.tsx
85381
85588
  var exports_insert = {};
85382
85589
  __export(exports_insert, {
85383
- run: () => run23,
85590
+ run: () => run22,
85384
85591
  parseTargetPlacement: () => parseTargetPlacement,
85385
85592
  parseParagraphOptions: () => parseParagraphOptions2,
85386
85593
  chooseContentSpec: () => chooseContentSpec,
85387
85594
  MARKDOWN_INCOMPATIBLE_FLAGS: () => MARKDOWN_INCOMPATIBLE_FLAGS2
85388
85595
  });
85389
- async function run23(args) {
85390
- const parsed = await tryParseArgs(args, OPTION_SPEC4, HELP19);
85596
+ async function run22(args) {
85597
+ const parsed = await tryParseArgs(args, OPTION_SPEC3, HELP18);
85391
85598
  if (typeof parsed === "number")
85392
85599
  return parsed;
85393
85600
  if (parsed.values.help) {
85394
- await writeStdout(HELP19);
85601
+ await writeStdout(HELP18);
85395
85602
  return EXIT2.OK;
85396
85603
  }
85397
85604
  setVerboseAck(Boolean(parsed.values.verbose));
85398
85605
  const filePath = parsed.positionals[0];
85399
85606
  if (!filePath)
85400
- return fail("USAGE", "Missing FILE argument", HELP19);
85607
+ return fail("USAGE", "Missing FILE argument", HELP18);
85401
85608
  const batchInput = parsed.values.batch;
85402
85609
  if (batchInput !== undefined) {
85403
85610
  return runInsertBatch(filePath, batchInput, parsed.values);
@@ -85452,16 +85659,20 @@ async function commitInsert(document4, blockRef, blocks, opts) {
85452
85659
  if (insertedNodes.has(reference.node))
85453
85660
  locators.push(blockId);
85454
85661
  }
85662
+ const destination = opts.outputPath ?? opts.filePath;
85455
85663
  await respondMinted(locators, {
85456
85664
  ok: true,
85457
85665
  operation: "insert",
85458
- path: opts.outputPath ?? opts.filePath,
85666
+ path: destination,
85459
85667
  locators,
85460
85668
  anchor: opts.placement.locator,
85461
85669
  placement: opts.placement.mode
85462
- });
85670
+ }, isLayoutAffecting(opts.spec) ? renderVerifyHint(destination) : undefined);
85463
85671
  return EXIT2.OK;
85464
85672
  }
85673
+ function isLayoutAffecting(spec) {
85674
+ return spec.kind === "section" || spec.kind === "image" || spec.kind === "table" || spec.kind === "break";
85675
+ }
85465
85676
  async function buildSingleShotOptions(filePath, values2) {
85466
85677
  const placement = await parseTargetPlacement(values2);
85467
85678
  if (typeof placement === "number")
@@ -85469,10 +85680,18 @@ async function buildSingleShotOptions(filePath, values2) {
85469
85680
  const spec = await chooseContentSpec(values2);
85470
85681
  if (typeof spec === "number")
85471
85682
  return spec;
85683
+ if (spec.kind === "text") {
85684
+ const rejected = await rejectMarkdownInText(values2.text, HELP18);
85685
+ if (typeof rejected === "number")
85686
+ return rejected;
85687
+ const mangled = await rejectShellMangledValue(values2.text, HELP18, "--text");
85688
+ if (typeof mangled === "number")
85689
+ return mangled;
85690
+ }
85472
85691
  if (spec.kind === "markdown") {
85473
85692
  const conflict = MARKDOWN_INCOMPATIBLE_FLAGS2.find((flag) => values2[flag] !== undefined);
85474
85693
  if (conflict) {
85475
- return fail("USAGE", `--${conflict} can't be combined with --markdown / --markdown-file (the markdown source controls block-level styling)`, HELP19);
85694
+ return fail("USAGE", `--${conflict} can't be combined with --markdown / --markdown-file (the markdown source controls block-level styling)`, HELP18);
85476
85695
  }
85477
85696
  }
85478
85697
  const paragraphOptions = await parseParagraphOptions2(values2);
@@ -85493,23 +85712,26 @@ async function parseTargetPlacement(values2) {
85493
85712
  const after = values2.after;
85494
85713
  const before = values2.before;
85495
85714
  if (!after && !before) {
85496
- return fail("USAGE", "Missing locator: pass --after or --before", HELP19);
85715
+ return fail("USAGE", "Missing locator: pass --after or --before", HELP18);
85497
85716
  }
85498
85717
  if (after && before) {
85499
- return fail("USAGE", "Pass either --after or --before, not both", HELP19);
85718
+ return fail("USAGE", "Pass either --after or --before, not both", HELP18);
85500
85719
  }
85501
85720
  if (after !== undefined)
85502
85721
  return { mode: "after", locator: after };
85503
85722
  return { mode: "before", locator: before };
85504
85723
  }
85505
85724
  async function chooseContentSpec(values2) {
85725
+ if (values2.section !== undefined || values2.columns !== undefined || values2.type !== undefined) {
85726
+ return fail("USAGE", "insert no longer creates section/column layout \u2014 use `docx sections`", "To put paragraphs pN\u2026pM in N columns: `docx sections --at pN-pM --columns N`. To recount an existing section: `docx sections --at sN --columns N`.");
85727
+ }
85506
85728
  const present = CONTENT_KINDS.filter((kind) => values2[kind.flag] !== undefined);
85507
85729
  if (present.length > 1) {
85508
- return fail("USAGE", `Pass only one of ${CONTENT_FLAG_LIST}`, HELP19);
85730
+ return fail("USAGE", `Pass only one of ${CONTENT_FLAG_LIST}`, HELP18);
85509
85731
  }
85510
85732
  const chosen = present[0];
85511
85733
  if (!chosen) {
85512
- return fail("USAGE", `Missing content: pass ${CONTENT_FLAG_LIST}`, HELP19);
85734
+ return fail("USAGE", `Missing content: pass ${CONTENT_FLAG_LIST}`, HELP18);
85513
85735
  }
85514
85736
  const chosenSubFlags = new Set(chosen.subFlags);
85515
85737
  for (const kind of CONTENT_KINDS) {
@@ -85517,7 +85739,7 @@ async function chooseContentSpec(values2) {
85517
85739
  continue;
85518
85740
  const orphan = kind.subFlags.find((flag) => values2[flag] !== undefined && !chosenSubFlags.has(flag));
85519
85741
  if (orphan) {
85520
- return fail("USAGE", `--${orphan} requires --${kind.flag}`, HELP19);
85742
+ return fail("USAGE", `--${orphan} requires --${kind.flag}`, HELP18);
85521
85743
  }
85522
85744
  }
85523
85745
  switch (chosen.flag) {
@@ -85531,10 +85753,6 @@ async function chooseContentSpec(values2) {
85531
85753
  return { kind: "break", breakKind: "page" };
85532
85754
  case "column-break":
85533
85755
  return { kind: "break", breakKind: "column" };
85534
- case "section": {
85535
- const flags = await parseSectionFlags(values2);
85536
- return typeof flags === "number" ? flags : { kind: "section", ...flags };
85537
- }
85538
85756
  case "table": {
85539
85757
  const flags = await parseTableFlags(values2);
85540
85758
  return typeof flags === "number" ? flags : { kind: "table", ...flags };
@@ -85589,7 +85807,7 @@ async function resolveCodeSpec(values2, flag) {
85589
85807
  async function parseImageFlags(values2) {
85590
85808
  const src = values2.image;
85591
85809
  if (!src)
85592
- return fail("USAGE", "--image requires a SRC argument", HELP19);
85810
+ return fail("USAGE", "--image requires a SRC argument", HELP18);
85593
85811
  const out = { src };
85594
85812
  const alt = values2.alt;
85595
85813
  if (alt !== undefined)
@@ -85632,7 +85850,7 @@ async function parseTableFlags(values2) {
85632
85850
  const rowsRaw = values2.rows;
85633
85851
  const colsRaw = values2.cols;
85634
85852
  if (rowsRaw === undefined || colsRaw === undefined) {
85635
- return fail("USAGE", "--table requires --rows and --cols", HELP19);
85853
+ return fail("USAGE", "--table requires --rows and --cols", HELP18);
85636
85854
  }
85637
85855
  const rows = Number.parseInt(rowsRaw, 10);
85638
85856
  const cols = Number.parseInt(colsRaw, 10);
@@ -85709,18 +85927,18 @@ async function parseParagraphOptions2(values2) {
85709
85927
  const listValue = values2.list;
85710
85928
  const listLevelValue = values2["list-level"];
85711
85929
  if (taskValue !== undefined && listValue !== undefined) {
85712
- return fail("USAGE", "--task and --list are mutually exclusive (--task already implies a bullet list)", HELP19);
85930
+ return fail("USAGE", "--task and --list are mutually exclusive (--task already implies a bullet list)", HELP18);
85713
85931
  }
85714
85932
  if (taskValue !== undefined) {
85715
85933
  const checked = parseTaskFlag(taskValue);
85716
85934
  if (checked === null) {
85717
- return fail("USAGE", `--task must be "checked" or "unchecked", got "${taskValue}"`, HELP19);
85935
+ return fail("USAGE", `--task must be "checked" or "unchecked", got "${taskValue}"`, HELP18);
85718
85936
  }
85719
85937
  out.taskState = checked ? "checked" : "unchecked";
85720
85938
  }
85721
85939
  if (listValue !== undefined) {
85722
85940
  if (listValue !== "bullet" && listValue !== "ordered") {
85723
- return fail("USAGE", `--list must be "bullet" or "ordered", got "${listValue}"`, HELP19);
85941
+ return fail("USAGE", `--list must be "bullet" or "ordered", got "${listValue}"`, HELP18);
85724
85942
  }
85725
85943
  out.list = { level: 0, numId: -1 };
85726
85944
  out.listKind = listValue;
@@ -85728,7 +85946,7 @@ async function parseParagraphOptions2(values2) {
85728
85946
  if (listLevelValue !== undefined) {
85729
85947
  const level = Number(listLevelValue);
85730
85948
  if (!Number.isInteger(level) || level < 0 || level > 8) {
85731
- return fail("USAGE", `--list-level must be an integer 0-8, got "${listLevelValue}"`, HELP19);
85949
+ return fail("USAGE", `--list-level must be an integer 0-8, got "${listLevelValue}"`, HELP18);
85732
85950
  }
85733
85951
  if (out.list)
85734
85952
  out.list.level = level;
@@ -85736,14 +85954,14 @@ async function parseParagraphOptions2(values2) {
85736
85954
  }
85737
85955
  return out;
85738
85956
  }
85739
- var ANCHOR_FORMS2, HELP19, OPTION_SPEC4, MARKDOWN_INCOMPATIBLE_FLAGS2, CONTENT_KINDS, CONTENT_FLAG_LIST;
85957
+ var ANCHOR_FORMS2, HELP18, OPTION_SPEC3, MARKDOWN_INCOMPATIBLE_FLAGS2, CONTENT_KINDS, CONTENT_FLAG_LIST;
85740
85958
  var init_insert2 = __esm(() => {
85741
85959
  init_core2();
85742
85960
  init_parse_helpers();
85743
85961
  init_respond();
85744
85962
  init_batch3();
85745
85963
  ANCHOR_FORMS2 = describeForms(["paragraph", "table", "section", "cellParagraph"], " ");
85746
- HELP19 = `docx insert \u2014 insert a block (paragraph, table, image, \u2026) at a locator
85964
+ HELP18 = `docx insert \u2014 insert a block (paragraph, table, image, \u2026) at a locator
85747
85965
 
85748
85966
  Usage:
85749
85967
  docx insert FILE (--after | --before) LOCATOR <content> [options]
@@ -85762,7 +85980,6 @@ Content (one required):
85762
85980
  --runs JSON Insert a paragraph with custom runs (Run[] JSON)
85763
85981
  --page-break Insert an empty paragraph containing a page break
85764
85982
  --column-break Insert an empty paragraph containing a column break
85765
- --section Insert a section boundary (sentinel paragraph w/ inline sectPr)
85766
85983
  --table Insert an empty rows\xD7cols table (requires --rows and --cols)
85767
85984
  --image SRC Insert an image (SRC is a file path, data: URI, or http(s) URL)
85768
85985
  --code TEXT Insert a multi-line code block. Newlines split into one
@@ -85808,19 +86025,20 @@ Run options (only with --text):
85808
86025
  --italic Italic
85809
86026
  --url URL Wrap the inserted text in a hyperlink to URL
85810
86027
 
85811
- Section options (only with --section):
85812
- --columns N Number of columns for the section ending at this boundary
85813
- --type T continuous | nextPage | evenPage | oddPage | nextColumn
86028
+ Column / section layout: NOT here \u2014 use \`docx sections\`. Name the range and it
86029
+ inserts the bounding breaks correctly: \`docx sections --at p6-p16 --columns 2\`.
86030
+ (A raw section break formats the content ABOVE it, the off-by-one that made
86031
+ \`insert --section\` a footgun, so it was removed.)
85814
86032
 
85815
86033
  Agent tip: VERIFY LAYOUT VISUALLY: \`docx read\` shows text and structure as
85816
- Markdown, but NOT how the page actually looks \u2014 multi-column sections, page
85817
- breaks, image sizing, and where content lands on the page do not appear there.
85818
- After inserting layout-affecting content (--section/--columns, --page-break,
85819
- --image, --table), render the document to images and look at them:
86034
+ Markdown, but NOT how the page actually looks \u2014 page breaks, image sizing, and
86035
+ where content lands on the page do not appear there. After inserting
86036
+ layout-affecting content (--page-break, --image, --table), render the document to
86037
+ images and look at them:
85820
86038
  docx render FILE --out pages/ # writes page-001.png, page-002.png, \u2026
85821
- Read the PNGs, check the layout reads the way you intended (columns balanced, no
85822
- stray blank page, figure sized sensibly), and adjust placement + re-render until
85823
- it looks right. Don't assume a section/column insert looks good without seeing it.
86039
+ Read the PNGs, check the layout reads the way you intended (no stray blank page,
86040
+ figure sized sensibly), and adjust placement + re-render until it looks right.
86041
+ Don't assume a layout-affecting insert looks good without seeing it.
85824
86042
 
85825
86043
  Table options (only with --table):
85826
86044
  --rows N Number of rows (required, >= 1)
@@ -85869,7 +86087,6 @@ Examples:
85869
86087
  docx insert doc.docx --after p2 --runs '[{"type":"text","text":"X","bold":true}]'
85870
86088
  docx insert doc.docx --after p3 --text "click here" --url https://example.com
85871
86089
  docx insert doc.docx --after p3 --page-break
85872
- docx insert doc.docx --after p9 --section --columns 2 --type continuous
85873
86090
  docx insert doc.docx --after p3 --table --rows 3 --cols 2
85874
86091
  docx insert doc.docx --after p3 --table --rows 2 --cols 3 --widths 1440,2880,4320
85875
86092
  docx insert doc.docx --after p3 --image ./diagram.png --alt "System diagram"
@@ -85887,7 +86104,7 @@ Batch JSONL example (keys mirror the flags; one insert per line):
85887
86104
  {"before": "p0", "text": "ALERT", "color": "CC0000", "bold": true}
85888
86105
  {"after": "p5", "markdown": "## Summary\\n\\n- point a\\n- point b"}
85889
86106
  `;
85890
- OPTION_SPEC4 = {
86107
+ OPTION_SPEC3 = {
85891
86108
  after: { type: "string" },
85892
86109
  before: { type: "string" },
85893
86110
  batch: { type: "string" },
@@ -85942,7 +86159,6 @@ Batch JSONL example (keys mirror the flags; one insert per line):
85942
86159
  { flag: "runs", subFlags: [] },
85943
86160
  { flag: "page-break", subFlags: [] },
85944
86161
  { flag: "column-break", subFlags: [] },
85945
- { flag: "section", subFlags: ["columns", "type"] },
85946
86162
  {
85947
86163
  flag: "table",
85948
86164
  subFlags: ["rows", "cols", "widths", "table-width", "borders", "layout"]
@@ -85960,16 +86176,16 @@ Batch JSONL example (keys mirror the flags; one insert per line):
85960
86176
  // src/cli/images/add.ts
85961
86177
  var exports_add4 = {};
85962
86178
  __export(exports_add4, {
85963
- run: () => run24
86179
+ run: () => run23
85964
86180
  });
85965
- async function run24(args) {
86181
+ async function run23(args) {
85966
86182
  if (args[0] === "-h" || args[0] === "--help") {
85967
- await writeStdout(HELP20);
86183
+ await writeStdout(HELP19);
85968
86184
  return 0;
85969
86185
  }
85970
- return run23(args);
86186
+ return run22(args);
85971
86187
  }
85972
- var HELP20 = `docx images add \u2014 insert an image (an alias for \`docx insert --image\`)
86188
+ var HELP19 = `docx images add \u2014 insert an image (an alias for \`docx insert --image\`)
85973
86189
 
85974
86190
  Usage:
85975
86191
  docx images add FILE --image PATH --after pN [options]
@@ -85993,28 +86209,28 @@ var init_add4 = __esm(() => {
85993
86209
  // src/cli/images/delete.tsx
85994
86210
  var exports_delete5 = {};
85995
86211
  __export(exports_delete5, {
85996
- run: () => run25
86212
+ run: () => run24
85997
86213
  });
85998
- async function run25(args) {
86214
+ async function run24(args) {
85999
86215
  const parsed = await tryParseArgs(args, {
86000
86216
  at: { type: "string" },
86001
86217
  author: { type: "string" },
86002
86218
  track: { type: "boolean" },
86003
86219
  ...SAVE_FLAGS
86004
- }, HELP21);
86220
+ }, HELP20);
86005
86221
  if (typeof parsed === "number")
86006
86222
  return parsed;
86007
86223
  if (parsed.values.help) {
86008
- await writeStdout(HELP21);
86224
+ await writeStdout(HELP20);
86009
86225
  return EXIT2.OK;
86010
86226
  }
86011
86227
  setVerboseAck(Boolean(parsed.values.verbose));
86012
86228
  const path2 = parsed.positionals[0];
86013
86229
  if (!path2)
86014
- return fail("USAGE", "Missing FILE argument", HELP21);
86230
+ return fail("USAGE", "Missing FILE argument", HELP20);
86015
86231
  const targetId = parsed.values.at;
86016
86232
  if (!targetId)
86017
- return fail("USAGE", "Missing --at imgN", HELP21);
86233
+ return fail("USAGE", "Missing --at imgN", HELP20);
86018
86234
  const document4 = await openOrFail(path2);
86019
86235
  if (typeof document4 === "number")
86020
86236
  return document4;
@@ -86084,7 +86300,7 @@ function pruneIfUnreferenced(document4, relationshipId) {
86084
86300
  }
86085
86301
  return true;
86086
86302
  }
86087
- var HELP21 = `docx images delete \u2014 remove an embedded image
86303
+ var HELP20 = `docx images delete \u2014 remove an embedded image
86088
86304
 
86089
86305
  Usage:
86090
86306
  docx images delete FILE --at imgN [options]
@@ -86129,27 +86345,27 @@ var init_delete5 = __esm(() => {
86129
86345
  // src/cli/images/extract.ts
86130
86346
  var exports_extract = {};
86131
86347
  __export(exports_extract, {
86132
- run: () => run26
86348
+ run: () => run25
86133
86349
  });
86134
86350
  import { join as join6 } from "path";
86135
- async function run26(args) {
86351
+ async function run25(args) {
86136
86352
  const parsed = await tryParseArgs(args, {
86137
86353
  to: { type: "string" },
86138
86354
  at: { type: "string" },
86139
86355
  help: { type: "boolean", short: "h" }
86140
- }, HELP22);
86356
+ }, HELP21);
86141
86357
  if (typeof parsed === "number")
86142
86358
  return parsed;
86143
86359
  if (parsed.values.help) {
86144
- await writeStdout(HELP22);
86360
+ await writeStdout(HELP21);
86145
86361
  return EXIT2.OK;
86146
86362
  }
86147
86363
  const path2 = parsed.positionals[0];
86148
86364
  if (!path2)
86149
- return fail("USAGE", "Missing FILE argument", HELP22);
86365
+ return fail("USAGE", "Missing FILE argument", HELP21);
86150
86366
  const outputDir = parsed.values.to;
86151
86367
  if (!outputDir)
86152
- return fail("USAGE", "Missing --to DIR", HELP22);
86368
+ return fail("USAGE", "Missing --to DIR", HELP21);
86153
86369
  const targetId = parsed.values.at;
86154
86370
  const document4 = await openOrFail(path2);
86155
86371
  if (typeof document4 === "number")
@@ -86190,7 +86406,7 @@ function extensionFor(contentType, partName) {
86190
86406
  const fromName = partName.split(".").pop()?.toLowerCase();
86191
86407
  return fromName ?? "bin";
86192
86408
  }
86193
- var HELP22 = `docx images extract \u2014 dump image bytes to a directory
86409
+ var HELP21 = `docx images extract \u2014 dump image bytes to a directory
86194
86410
 
86195
86411
  Usage:
86196
86412
  docx images extract FILE --to DIR [options]
@@ -86223,21 +86439,21 @@ var init_extract = __esm(() => {
86223
86439
  // src/cli/images/list.ts
86224
86440
  var exports_list4 = {};
86225
86441
  __export(exports_list4, {
86226
- run: () => run27
86442
+ run: () => run26
86227
86443
  });
86228
- async function run27(args) {
86444
+ async function run26(args) {
86229
86445
  const parsed = await tryParseArgs(args, {
86230
86446
  help: { type: "boolean", short: "h" }
86231
- }, HELP23);
86447
+ }, HELP22);
86232
86448
  if (typeof parsed === "number")
86233
86449
  return parsed;
86234
86450
  if (parsed.values.help) {
86235
- await writeStdout(HELP23);
86451
+ await writeStdout(HELP22);
86236
86452
  return EXIT2.OK;
86237
86453
  }
86238
86454
  const path2 = parsed.positionals[0];
86239
86455
  if (!path2)
86240
- return fail("USAGE", "Missing FILE argument", HELP23);
86456
+ return fail("USAGE", "Missing FILE argument", HELP22);
86241
86457
  const document4 = await openOrFail(path2);
86242
86458
  if (typeof document4 === "number")
86243
86459
  return document4;
@@ -86245,7 +86461,7 @@ async function run27(args) {
86245
86461
  await respond(flattenImageRuns(document4.body.blocks));
86246
86462
  return EXIT2.OK;
86247
86463
  }
86248
- var HELP23 = `docx images list \u2014 print image manifest as JSON
86464
+ var HELP22 = `docx images list \u2014 print image manifest as JSON
86249
86465
 
86250
86466
  Usage:
86251
86467
  docx images list FILE [options]
@@ -86271,31 +86487,31 @@ var init_list6 = __esm(() => {
86271
86487
  // src/cli/images/replace.ts
86272
86488
  var exports_replace2 = {};
86273
86489
  __export(exports_replace2, {
86274
- run: () => run28
86490
+ run: () => run27
86275
86491
  });
86276
- async function run28(args) {
86492
+ async function run27(args) {
86277
86493
  const parsed = await tryParseArgs(args, {
86278
86494
  at: { type: "string" },
86279
86495
  with: { type: "string" },
86280
86496
  author: { type: "string" },
86281
86497
  ...SAVE_FLAGS
86282
- }, HELP24);
86498
+ }, HELP23);
86283
86499
  if (typeof parsed === "number")
86284
86500
  return parsed;
86285
86501
  if (parsed.values.help) {
86286
- await writeStdout(HELP24);
86502
+ await writeStdout(HELP23);
86287
86503
  return EXIT2.OK;
86288
86504
  }
86289
86505
  setVerboseAck(Boolean(parsed.values.verbose));
86290
86506
  const path2 = parsed.positionals[0];
86291
86507
  if (!path2)
86292
- return fail("USAGE", "Missing FILE argument", HELP24);
86508
+ return fail("USAGE", "Missing FILE argument", HELP23);
86293
86509
  const targetId = parsed.values.at;
86294
86510
  if (!targetId)
86295
- return fail("USAGE", "Missing --at imgN", HELP24);
86511
+ return fail("USAGE", "Missing --at imgN", HELP23);
86296
86512
  const sourcePath = parsed.values.with;
86297
86513
  if (!sourcePath)
86298
- return fail("USAGE", "Missing --with PATH", HELP24);
86514
+ return fail("USAGE", "Missing --with PATH", HELP23);
86299
86515
  const sourceFile = Bun.file(sourcePath);
86300
86516
  if (!await sourceFile.exists()) {
86301
86517
  return fail("FILE_NOT_FOUND", `Replacement file not found: ${sourcePath}`);
@@ -86386,7 +86602,7 @@ function renameExtension(partName, newExtension) {
86386
86602
  function relativeTargetFor(partName) {
86387
86603
  return partName.startsWith("word/") ? partName.slice("word/".length) : partName;
86388
86604
  }
86389
- var HELP24 = `docx images replace \u2014 swap an image's bytes
86605
+ var HELP23 = `docx images replace \u2014 swap an image's bytes
86390
86606
 
86391
86607
  Usage:
86392
86608
  docx images replace FILE --at imgN --with PATH [options]
@@ -86431,12 +86647,12 @@ var init_replace3 = __esm(() => {
86431
86647
  // src/cli/images/index.ts
86432
86648
  var exports_images = {};
86433
86649
  __export(exports_images, {
86434
- run: () => run29
86650
+ run: () => run28
86435
86651
  });
86436
- async function run29(args) {
86652
+ async function run28(args) {
86437
86653
  const verb = args[0];
86438
86654
  if (!verb || verb === "--help" || verb === "-h" || verb === "help") {
86439
- await writeStdout(HELP25);
86655
+ await writeStdout(HELP24);
86440
86656
  return verb ? 0 : 2;
86441
86657
  }
86442
86658
  const loader = SUBCOMMANDS5[verb];
@@ -86446,7 +86662,7 @@ async function run29(args) {
86446
86662
  const module_ = await loader();
86447
86663
  return module_.run(args.slice(1));
86448
86664
  }
86449
- var SUBCOMMANDS5, HELP25 = `docx images \u2014 manage embedded images
86665
+ var SUBCOMMANDS5, HELP24 = `docx images \u2014 manage embedded images
86450
86666
 
86451
86667
  Usage:
86452
86668
  docx images <verb> FILE [options]
@@ -86501,6 +86717,13 @@ export type Paragraph = {
86501
86717
  * them at top level, breaking the quote at that point. See
86502
86718
  * [src/core/markdown/CLAUDE.md](../markdown/CLAUDE.md). */
86503
86719
  quoteDepth?: number;
86720
+ /** Explicit tab stops from \`<w:pPr><w:tabs>\`, each \`{ align, pos }\` (pos in
86721
+ * twips). Surfaced so \`read\` can flag a fragile right-alignment: a LEFT (or
86722
+ * center) tab stop near the right margin pushes content rightward but \u2014 unlike
86723
+ * a RIGHT tab \u2014 wraps anything wider than the gap to the margin (the r\xE9sum\xE9
86724
+ * "San / Francisco, CA" break). Present only when the paragraph declares
86725
+ * explicit tab stops. */
86726
+ tabStops?: { align: string; pos: number }[];
86504
86727
  runs: Run[];
86505
86728
  };
86506
86729
 
@@ -86806,18 +87029,18 @@ var init_types3 = () => {};
86806
87029
  // src/cli/info/schema.ts
86807
87030
  var exports_schema = {};
86808
87031
  __export(exports_schema, {
86809
- run: () => run30
87032
+ run: () => run29
86810
87033
  });
86811
- async function run30(args) {
87034
+ async function run29(args) {
86812
87035
  const parsed = await tryParseArgs(args, {
86813
87036
  json: { type: "boolean" },
86814
87037
  ts: { type: "boolean" },
86815
87038
  help: { type: "boolean", short: "h" }
86816
- }, HELP26);
87039
+ }, HELP25);
86817
87040
  if (typeof parsed === "number")
86818
87041
  return parsed;
86819
87042
  if (parsed.values.help) {
86820
- await writeStdout(HELP26);
87043
+ await writeStdout(HELP25);
86821
87044
  return EXIT2.OK;
86822
87045
  }
86823
87046
  if (parsed.values.ts) {
@@ -86827,7 +87050,7 @@ async function run30(args) {
86827
87050
  await respond(JSON_SCHEMA);
86828
87051
  return EXIT2.OK;
86829
87052
  }
86830
- var HELP26 = `docx info schema \u2014 print the AST type definitions
87053
+ var HELP25 = `docx info schema \u2014 print the AST type definitions
86831
87054
 
86832
87055
  Usage:
86833
87056
  docx info schema [options]
@@ -86901,6 +87124,17 @@ var init_schema = __esm(() => {
86901
87124
  }
86902
87125
  },
86903
87126
  taskState: { enum: ["checked", "unchecked"] },
87127
+ tabStops: {
87128
+ type: "array",
87129
+ items: {
87130
+ type: "object",
87131
+ required: ["align", "pos"],
87132
+ properties: {
87133
+ align: { type: "string" },
87134
+ pos: { type: "number" }
87135
+ }
87136
+ }
87137
+ },
86904
87138
  runs: { type: "array", items: { $ref: "#/$defs/Run" } }
86905
87139
  }
86906
87140
  },
@@ -87155,7 +87389,7 @@ var init_schema = __esm(() => {
87155
87389
  // src/cli/info/locators.ts
87156
87390
  var exports_locators = {};
87157
87391
  __export(exports_locators, {
87158
- run: () => run31
87392
+ run: () => run30
87159
87393
  });
87160
87394
  function renderReference() {
87161
87395
  const sections = GROUPS.map((group) => {
@@ -87286,15 +87520,15 @@ function renderJson() {
87286
87520
  ]
87287
87521
  };
87288
87522
  }
87289
- async function run31(args) {
87523
+ async function run30(args) {
87290
87524
  const parsed = await tryParseArgs(args, {
87291
87525
  json: { type: "boolean" },
87292
87526
  help: { type: "boolean", short: "h" }
87293
- }, HELP27);
87527
+ }, HELP26);
87294
87528
  if (typeof parsed === "number")
87295
87529
  return parsed;
87296
87530
  if (parsed.values.help) {
87297
- await writeStdout(HELP27);
87531
+ await writeStdout(HELP26);
87298
87532
  return EXIT2.OK;
87299
87533
  }
87300
87534
  if (parsed.values.json) {
@@ -87305,7 +87539,7 @@ async function run31(args) {
87305
87539
  await writeStdout(renderReference());
87306
87540
  return EXIT2.OK;
87307
87541
  }
87308
- var HELP27 = `docx info locators \u2014 print the locator grammar reference
87542
+ var HELP26 = `docx info locators \u2014 print the locator grammar reference
87309
87543
 
87310
87544
  Usage:
87311
87545
  docx info locators [options]
@@ -87357,12 +87591,12 @@ var init_locators2 = __esm(() => {
87357
87591
  // src/cli/info/index.ts
87358
87592
  var exports_info = {};
87359
87593
  __export(exports_info, {
87360
- run: () => run32
87594
+ run: () => run31
87361
87595
  });
87362
- async function run32(args) {
87596
+ async function run31(args) {
87363
87597
  const topic = args[0];
87364
87598
  if (!topic || topic === "--help" || topic === "-h" || topic === "help") {
87365
- await writeStdout(HELP28);
87599
+ await writeStdout(HELP27);
87366
87600
  return topic ? 0 : 2;
87367
87601
  }
87368
87602
  const loader = SUBCOMMANDS6[topic];
@@ -87372,7 +87606,7 @@ async function run32(args) {
87372
87606
  const module_ = await loader();
87373
87607
  return module_.run(args.slice(1));
87374
87608
  }
87375
- var SUBCOMMANDS6, HELP28 = `docx info \u2014 print reference material about the CLI
87609
+ var SUBCOMMANDS6, HELP27 = `docx info \u2014 print reference material about the CLI
87376
87610
 
87377
87611
  Usage:
87378
87612
  docx info <topic> [options]
@@ -87452,23 +87686,23 @@ var init_build = __esm(() => {
87452
87686
  // src/cli/outline/index.ts
87453
87687
  var exports_outline = {};
87454
87688
  __export(exports_outline, {
87455
- run: () => run33
87689
+ run: () => run32
87456
87690
  });
87457
- async function run33(args) {
87691
+ async function run32(args) {
87458
87692
  const parsed = await tryParseArgs(args, {
87459
87693
  "style-prefix": { type: "string" },
87460
87694
  json: { type: "boolean" },
87461
87695
  help: { type: "boolean", short: "h" }
87462
- }, HELP29);
87696
+ }, HELP28);
87463
87697
  if (typeof parsed === "number")
87464
87698
  return parsed;
87465
87699
  if (parsed.values.help) {
87466
- await writeStdout(HELP29);
87700
+ await writeStdout(HELP28);
87467
87701
  return EXIT2.OK;
87468
87702
  }
87469
87703
  const path2 = parsed.positionals[0];
87470
87704
  if (!path2)
87471
- return fail("USAGE", "Missing FILE argument", HELP29);
87705
+ return fail("USAGE", "Missing FILE argument", HELP28);
87472
87706
  const stylePrefix = parsed.values["style-prefix"] ?? "Heading";
87473
87707
  if (stylePrefix.length === 0) {
87474
87708
  return fail("USAGE", "--style-prefix cannot be empty");
@@ -87496,7 +87730,7 @@ function renderOutlineText(entries, depth = 0) {
87496
87730
  }
87497
87731
  return lines;
87498
87732
  }
87499
- var HELP29 = `docx outline \u2014 list headings as a hierarchical tree
87733
+ var HELP28 = `docx outline \u2014 list headings as a hierarchical tree
87500
87734
 
87501
87735
  Usage:
87502
87736
  docx outline FILE [options]
@@ -87564,7 +87798,9 @@ function renderMarkdown(doc, options = {}) {
87564
87798
  referencedEndnoteIds: new Set,
87565
87799
  referencedTrackedChanges: new Map,
87566
87800
  orderedCounters: new Map,
87567
- contentWidthEmu: contentWidthEmu(documentGeometry(blocks))
87801
+ contentWidthEmu: contentWidthEmu(documentGeometry(blocks)),
87802
+ governingColumns: computeGoverningColumns(blocks),
87803
+ wrappingTabLines: []
87568
87804
  };
87569
87805
  const parts = [];
87570
87806
  let cursor = 0;
@@ -87591,7 +87827,7 @@ function renderMarkdown(doc, options = {}) {
87591
87827
  cursor = lookahead2;
87592
87828
  continue;
87593
87829
  }
87594
- const rendered = renderBlock(block, ctx);
87830
+ const rendered = renderBlock(block, ctx, blocks, cursor);
87595
87831
  if (rendered !== null)
87596
87832
  parts.push(rendered);
87597
87833
  cursor++;
@@ -87618,7 +87854,9 @@ function renderMarkdown(doc, options = {}) {
87618
87854
  return "";
87619
87855
  const baseNote = formatBaseNote(dominant);
87620
87856
  const pageNote = formatPageNote(documentGeometry(blocks));
87621
- const headLines = [baseNote, pageNote].filter((line) => line.length > 0);
87857
+ const trackNote = options.trackChangesOn ? formatNote("track-changes", [], ["on"]) : "";
87858
+ const wrapSummary = formatWrappingTabSummary(ctx.wrappingTabLines);
87859
+ const headLines = [baseNote, pageNote, trackNote, wrapSummary].filter((line) => line.length > 0);
87622
87860
  const head = headLines.length > 0 ? `${headLines.join(`
87623
87861
  `)}
87624
87862
 
@@ -87633,17 +87871,17 @@ function detectFormatBaseline(blocks) {
87633
87871
  const sizeChars = new Map;
87634
87872
  let total = 0;
87635
87873
  for (const paragraph2 of flattenParagraphs(blocks)) {
87636
- for (const run34 of paragraph2.runs) {
87637
- if (run34.type !== "text")
87874
+ for (const run33 of paragraph2.runs) {
87875
+ if (run33.type !== "text")
87638
87876
  continue;
87639
- const length = run34.text.length;
87877
+ const length = run33.text.length;
87640
87878
  if (length === 0)
87641
87879
  continue;
87642
87880
  total += length;
87643
- if (run34.font)
87644
- fontChars.set(run34.font, (fontChars.get(run34.font) ?? 0) + length);
87645
- if (run34.sizeHalfPoints !== undefined) {
87646
- sizeChars.set(run34.sizeHalfPoints, (sizeChars.get(run34.sizeHalfPoints) ?? 0) + length);
87881
+ if (run33.font)
87882
+ fontChars.set(run33.font, (fontChars.get(run33.font) ?? 0) + length);
87883
+ if (run33.sizeHalfPoints !== undefined) {
87884
+ sizeChars.set(run33.sizeHalfPoints, (sizeChars.get(run33.sizeHalfPoints) ?? 0) + length);
87647
87885
  }
87648
87886
  }
87649
87887
  }
@@ -87677,8 +87915,8 @@ function emptyCommentIndex() {
87677
87915
  orderedIds: []
87678
87916
  };
87679
87917
  }
87680
- function isRunVisible(run34, view) {
87681
- const kind = run34.trackedChange?.kind;
87918
+ function isRunVisible(run33, view) {
87919
+ const kind = run33.trackedChange?.kind;
87682
87920
  if (!kind)
87683
87921
  return true;
87684
87922
  if (view === "accepted" && (kind === "del" || kind === "moveFrom"))
@@ -87693,13 +87931,13 @@ function buildCommentIndex(blocks, options) {
87693
87931
  const spanText = new Map;
87694
87932
  const orderedIds = [];
87695
87933
  for (const paragraph2 of flattenParagraphs(blocks)) {
87696
- paragraph2.runs.forEach((run34, index2) => {
87697
- const comments = runComments(run34);
87934
+ paragraph2.runs.forEach((run33, index2) => {
87935
+ const comments = runComments(run33);
87698
87936
  if (!comments)
87699
87937
  return;
87700
- if (run34.type === "text" && !isRunVisible(run34, view))
87938
+ if (run33.type === "text" && !isRunVisible(run33, view))
87701
87939
  return;
87702
- const spanContribution = run34.type === "text" ? run34.text : run34.type === "equation" ? run34.text : "";
87940
+ const spanContribution = run33.type === "text" ? run33.text : run33.type === "equation" ? run33.text : "";
87703
87941
  for (const commentId of comments) {
87704
87942
  if (!spanText.has(commentId))
87705
87943
  orderedIds.push(commentId);
@@ -87719,32 +87957,121 @@ function buildCommentIndex(blocks, options) {
87719
87957
  }
87720
87958
  return { endingsByRun, spanText, orderedIds };
87721
87959
  }
87722
- function runComments(run34) {
87723
- if (run34.type === "text")
87724
- return run34.comments;
87725
- if (run34.type === "equation")
87726
- return run34.comments;
87960
+ function runComments(run33) {
87961
+ if (run33.type === "text")
87962
+ return run33.comments;
87963
+ if (run33.type === "equation")
87964
+ return run33.comments;
87727
87965
  return;
87728
87966
  }
87729
87967
  function slotKey(paragraphId, runIndex) {
87730
87968
  return `${paragraphId}#${runIndex}`;
87731
87969
  }
87732
- function renderBlock(block, ctx) {
87970
+ function renderBlock(block, ctx, blocks, index2) {
87733
87971
  if (block.type === "paragraph")
87734
87972
  return renderParagraph(block, ctx);
87735
87973
  if (block.type === "table")
87736
87974
  return renderTable(block, ctx);
87737
- if (block.type === "sectionBreak")
87738
- return renderSectionBreak(block);
87975
+ if (block.type === "sectionBreak") {
87976
+ return renderSectionBreak(block, governedRange(blocks, index2));
87977
+ }
87739
87978
  return null;
87740
87979
  }
87741
- function renderSectionBreak(block) {
87980
+ function computeGoverningColumns(blocks) {
87981
+ const map3 = new Map;
87982
+ let cols = 1;
87983
+ for (let index2 = blocks.length - 1;index2 >= 0; index2--) {
87984
+ const block = blocks[index2];
87985
+ if (!block)
87986
+ continue;
87987
+ if (block.type === "sectionBreak") {
87988
+ cols = block.columns ?? 1;
87989
+ continue;
87990
+ }
87991
+ map3.set(block.id, cols);
87992
+ }
87993
+ return map3;
87994
+ }
87995
+ function formatWrappingTabSummary(ids) {
87996
+ if (ids.length === 0)
87997
+ return "";
87998
+ const count = `${ids.length} line${ids.length > 1 ? "s" : ""}`;
87999
+ const range = tabCureRange(ids);
88000
+ const fix = range ? `edit FILE --at ${range} --tabs right` : ids.map((id) => `edit FILE --at ${id} --tabs right`).join(" ; ");
88001
+ return formatNote("layout", [
88002
+ ["wrap", count],
88003
+ [
88004
+ "warn",
88005
+ "tab-aligned content (e.g. dates/locations) overflows the right margin and WRAPS in render until cured"
88006
+ ],
88007
+ ["fix-all", fix]
88008
+ ], ids);
88009
+ }
88010
+ function tabCureRange(ids) {
88011
+ const nums = ids.map((id) => /^p(\d+)$/.exec(id)?.[1]).filter((value) => value !== undefined).map(Number);
88012
+ if (nums.length === 0)
88013
+ return null;
88014
+ const min = Math.min(...nums);
88015
+ const max = Math.max(...nums);
88016
+ return min === max ? `p${min}` : `p${min}-p${max}`;
88017
+ }
88018
+ function layoutHazardNote(paragraph2, ctx) {
88019
+ if (!paragraph2.runs.some((run33) => run33.type === "tab"))
88020
+ return "";
88021
+ const cols = ctx.governingColumns.get(paragraph2.id) ?? 1;
88022
+ if (cols > 1) {
88023
+ return ` ${formatNote("layout", [
88024
+ ["cols", cols],
88025
+ [
88026
+ "warn",
88027
+ `tab alignment can wrap mid-line in this ${cols}-column section; render to verify`
88028
+ ]
88029
+ ], [paragraph2.id])}`;
88030
+ }
88031
+ const tabs = paragraph2.tabStops ?? [];
88032
+ if (tabs.some((tab) => tab.align === "right"))
88033
+ return "";
88034
+ const textWidthTwips = Math.round(ctx.contentWidthEmu / EMU_PER_TWIP2);
88035
+ const fragile = tabs.find((tab) => (tab.align === "left" || tab.align === "center" || tab.align === "") && tab.pos > textWidthTwips * 0.7);
88036
+ if (!fragile)
88037
+ return "";
88038
+ ctx.wrappingTabLines.push(paragraph2.id);
88039
+ const gapIn = twipsToInches(Math.max(0, textWidthTwips - fragile.pos));
88040
+ return ` ${formatNote("layout", [
88041
+ ["tab", `${fragile.align || "left"}@${twipsToInches(fragile.pos)}in`],
88042
+ [
88043
+ "warn",
88044
+ `right content on a LEFT tab ~${gapIn}in from the margin \u2014 wraps when longer (cure: --tabs right; see the docx:layout fix-all at top)`
88045
+ ]
88046
+ ], [paragraph2.id])}`;
88047
+ }
88048
+ function governedRange(blocks, index2) {
88049
+ let first;
88050
+ let last;
88051
+ for (let cursor = index2 - 1;cursor >= 0; cursor--) {
88052
+ const block = blocks[cursor];
88053
+ if (!block)
88054
+ continue;
88055
+ if (block.type === "sectionBreak")
88056
+ break;
88057
+ first = block.id;
88058
+ if (last === undefined)
88059
+ last = block.id;
88060
+ }
88061
+ if (!first || !last)
88062
+ return;
88063
+ return first === last ? first : `${first}..${last}`;
88064
+ }
88065
+ function renderSectionBreak(block, governs) {
87742
88066
  const pairs = [];
87743
88067
  if (block.columns !== undefined && block.columns > 1) {
87744
88068
  pairs.push(["cols", block.columns]);
87745
88069
  }
87746
88070
  if (block.sectionType !== undefined)
87747
88071
  pairs.push(["type", block.sectionType]);
88072
+ if (governs !== undefined && pairs.length > 0) {
88073
+ pairs.push(["applies-to", `${governs} (above)`]);
88074
+ }
87748
88075
  return formatNote("section", pairs, [block.id]);
87749
88076
  }
87750
88077
  function contentWidthEmu(geometry) {
@@ -87755,25 +88082,25 @@ function contentWidthEmu(geometry) {
87755
88082
  const fallback = (DEFAULT_PAGE.width - 2 * DEFAULT_PAGE.margin) * EMU_PER_TWIP2;
87756
88083
  return contentTwips > 0 ? contentTwips * EMU_PER_TWIP2 : fallback;
87757
88084
  }
87758
- function formatImageNote(run34, contentEmu) {
88085
+ function formatImageNote(run33, contentEmu) {
87759
88086
  const pairs = [];
87760
- if (run34.widthEmu && run34.heightEmu) {
88087
+ if (run33.widthEmu && run33.heightEmu) {
87761
88088
  pairs.push([
87762
88089
  "size",
87763
- `${emuToInches(run34.widthEmu)}x${emuToInches(run34.heightEmu)}in`
88090
+ `${emuToInches(run33.widthEmu)}x${emuToInches(run33.heightEmu)}in`
87764
88091
  ]);
87765
88092
  }
87766
- if (run34.floating)
88093
+ if (run33.floating)
87767
88094
  pairs.push(["float", "yes"]);
87768
- if (run34.wrap)
87769
- pairs.push(["wrap", run34.wrap]);
87770
- if (run34.align)
87771
- pairs.push(["align", run34.align]);
87772
- if (run34.widthEmu && run34.widthEmu > contentEmu)
88095
+ if (run33.wrap)
88096
+ pairs.push(["wrap", run33.wrap]);
88097
+ if (run33.align)
88098
+ pairs.push(["align", run33.align]);
88099
+ if (run33.widthEmu && run33.widthEmu > contentEmu)
87773
88100
  pairs.push(["overflow", "yes"]);
87774
88101
  if (pairs.length === 0)
87775
88102
  return "";
87776
- return ` ${formatNote("image", pairs, [run34.id])}`;
88103
+ return ` ${formatNote("image", pairs, [run33.id])}`;
87777
88104
  }
87778
88105
  function documentGeometry(blocks) {
87779
88106
  for (let index2 = blocks.length - 1;index2 >= 0; index2--) {
@@ -87825,14 +88152,14 @@ function isCodeBlockParagraph(block) {
87825
88152
  function renderCodeBlockGroup(paragraphs, ctx) {
87826
88153
  if (ctx.options.view !== "baseline" && ctx.options.view !== "accepted") {
87827
88154
  for (const paragraph2 of paragraphs) {
87828
- for (const run34 of paragraph2.runs) {
87829
- if (run34.type === "text" && run34.trackedChange) {
87830
- ctx.referencedTrackedChanges.set(run34.trackedChange.id, run34.trackedChange);
88155
+ for (const run33 of paragraph2.runs) {
88156
+ if (run33.type === "text" && run33.trackedChange) {
88157
+ ctx.referencedTrackedChanges.set(run33.trackedChange.id, run33.trackedChange);
87831
88158
  }
87832
88159
  }
87833
88160
  }
87834
88161
  }
87835
- const lines = paragraphs.map((paragraph2) => paragraph2.runs.filter((run34) => run34.type === "text" && typeof run34.text === "string").map((run34) => run34.text).join(""));
88162
+ const lines = paragraphs.map((paragraph2) => paragraph2.runs.filter((run33) => run33.type === "text" && typeof run33.text === "string").map((run33) => run33.text).join(""));
87836
88163
  const firstId = paragraphs[0]?.id ?? "";
87837
88164
  const lastId = paragraphs[paragraphs.length - 1]?.id ?? firstId;
87838
88165
  const language = codeBlockLanguageFromStyleId(paragraphs[0]?.style) ?? "";
@@ -87852,7 +88179,7 @@ function renderParagraph(paragraph2, ctx) {
87852
88179
  const body = rendered.replace(/[ \t]+$/, "");
87853
88180
  const separator = isDisplayEquationOnly(body) ? `
87854
88181
  ` : " ";
87855
- return `${prefix}${body}${separator}<!-- ${paragraph2.id} -->${formatParagraphNote(paragraph2)}`;
88182
+ return `${prefix}${body}${separator}<!-- ${paragraph2.id} -->${formatParagraphNote(paragraph2)}${layoutHazardNote(paragraph2, ctx)}`;
87856
88183
  }
87857
88184
  function formatParagraphNote(paragraph2) {
87858
88185
  const pairs = [];
@@ -87925,10 +88252,10 @@ function headingLevelFor(style) {
87925
88252
  function renderRuns(paragraphId, runs, ctx, mask, baseOffset) {
87926
88253
  const view = ctx.options.view ?? "accepted";
87927
88254
  const visibleEntries = [];
87928
- runs.forEach((run34, index2) => {
87929
- if (run34.type === "text" && !isRunVisible(run34, view))
88255
+ runs.forEach((run33, index2) => {
88256
+ if (run33.type === "text" && !isRunVisible(run33, view))
87930
88257
  return;
87931
- visibleEntries.push({ run: run34, originalIndex: index2 });
88258
+ visibleEntries.push({ run: run33, originalIndex: index2 });
87932
88259
  });
87933
88260
  let out = "";
87934
88261
  let cursor = 0;
@@ -87939,14 +88266,14 @@ function renderRuns(paragraphId, runs, ctx, mask, baseOffset) {
87939
88266
  cursor++;
87940
88267
  continue;
87941
88268
  }
87942
- const { run: run34 } = entry;
87943
- if (run34.type === "text") {
88269
+ const { run: run33 } = entry;
88270
+ if (run33.type === "text") {
87944
88271
  let lookahead2 = cursor + 1;
87945
88272
  while (lookahead2 < visibleEntries.length) {
87946
88273
  const next = visibleEntries[lookahead2];
87947
88274
  if (!next || next.run.type !== "text")
87948
88275
  break;
87949
- if (!sameDecoration(run34, next.run))
88276
+ if (!sameDecoration(run33, next.run))
87950
88277
  break;
87951
88278
  lookahead2++;
87952
88279
  }
@@ -87967,29 +88294,29 @@ function renderRuns(paragraphId, runs, ctx, mask, baseOffset) {
87967
88294
  cursor = lookahead2;
87968
88295
  continue;
87969
88296
  }
87970
- if (run34.type === "image") {
87971
- const alt = sanitizeAltText(run34.alt ?? run34.id);
87972
- const extension2 = extensionForImageMime(run34.contentType) ?? "bin";
87973
- out += `![${alt}](${run34.hash}.${extension2})`;
87974
- out += formatImageNote(run34, ctx.contentWidthEmu);
87975
- } else if (run34.type === "break") {
87976
- if (run34.kind === "line")
88297
+ if (run33.type === "image") {
88298
+ const alt = sanitizeAltText(run33.alt ?? run33.id);
88299
+ const extension2 = extensionForImageMime(run33.contentType) ?? "bin";
88300
+ out += `![${alt}](${run33.hash}.${extension2})`;
88301
+ out += formatImageNote(run33, ctx.contentWidthEmu);
88302
+ } else if (run33.type === "break") {
88303
+ if (run33.kind === "line")
87977
88304
  out += `
87978
88305
  `;
87979
- } else if (run34.type === "tab") {
88306
+ } else if (run33.type === "tab") {
87980
88307
  out += "\t";
87981
- } else if (run34.type === "equation") {
87982
- const body = run34.latex.length > 0 ? run34.latex : run34.text;
87983
- out += run34.display ? `$$${body}$$` : `$${body}$`;
88308
+ } else if (run33.type === "equation") {
88309
+ const body = run33.latex.length > 0 ? run33.latex : run33.text;
88310
+ out += run33.display ? `$$${body}$$` : `$${body}$`;
87984
88311
  out += commentEndingsFor(paragraphId, [{ originalIndex: cursor }], ctx.commentIndex);
87985
- } else if (run34.type === "noteRef") {
87986
- if (run34.kind === "footnote")
87987
- ctx.referencedFootnoteIds.add(run34.id);
88312
+ } else if (run33.type === "noteRef") {
88313
+ if (run33.kind === "footnote")
88314
+ ctx.referencedFootnoteIds.add(run33.id);
87988
88315
  else
87989
- ctx.referencedEndnoteIds.add(run34.id);
87990
- out += `[^${run34.id}]`;
87991
- } else if (run34.type === "chart") {
87992
- out += `\`[${run34.kind}]\``;
88316
+ ctx.referencedEndnoteIds.add(run33.id);
88317
+ out += `[^${run33.id}]`;
88318
+ } else if (run33.type === "chart") {
88319
+ out += `\`[${run33.kind}]\``;
87993
88320
  }
87994
88321
  cursor++;
87995
88322
  }
@@ -88023,7 +88350,7 @@ function sameCommentSet(left, right) {
88023
88350
  return true;
88024
88351
  }
88025
88352
  function renderTextSegment(runs, view, baseline, mask, offset2) {
88026
- const text6 = runs.map((run34) => run34.text).join("");
88353
+ const text6 = runs.map((run33) => run33.text).join("");
88027
88354
  if (text6.length === 0)
88028
88355
  return "";
88029
88356
  const first = runs[0];
@@ -88067,33 +88394,33 @@ function criticMarkerFor(kind) {
88067
88394
  return "++";
88068
88395
  return "--";
88069
88396
  }
88070
- function needsHtmlWrap(run34, baseline) {
88071
- return Boolean(run34.color && !isDefaultColor(run34.color) || run34.colorTheme && !isDefaultThemeColor(run34) || run34.shade || run34.font && run34.font !== baseline.font || run34.sizeHalfPoints !== undefined && run34.sizeHalfPoints !== baseline.sizeHalfPoints || run34.smallCaps || run34.allCaps || run34.underline || run34.vertAlign === "superscript" || run34.vertAlign === "subscript" || run34.highlight);
88397
+ function needsHtmlWrap(run33, baseline) {
88398
+ return Boolean(run33.color && !isDefaultColor(run33.color) || run33.colorTheme && !isDefaultThemeColor(run33) || run33.shade || run33.font && run33.font !== baseline.font || run33.sizeHalfPoints !== undefined && run33.sizeHalfPoints !== baseline.sizeHalfPoints || run33.smallCaps || run33.allCaps || run33.underline || run33.vertAlign === "superscript" || run33.vertAlign === "subscript" || run33.highlight);
88072
88399
  }
88073
- function wrapRunFormatting(body, run34, baseline) {
88400
+ function wrapRunFormatting(body, run33, baseline) {
88074
88401
  const styles = [];
88075
88402
  const attrs = [];
88076
- if (run34.color && !isDefaultColor(run34.color))
88077
- styles.push(`color:#${run34.color}`);
88078
- if (run34.shade)
88079
- styles.push(`background-color:#${run34.shade}`);
88080
- if (run34.font && run34.font !== baseline.font) {
88081
- styles.push(`font-family:${cssFontFamily(run34.font)}`);
88403
+ if (run33.color && !isDefaultColor(run33.color))
88404
+ styles.push(`color:#${run33.color}`);
88405
+ if (run33.shade)
88406
+ styles.push(`background-color:#${run33.shade}`);
88407
+ if (run33.font && run33.font !== baseline.font) {
88408
+ styles.push(`font-family:${cssFontFamily(run33.font)}`);
88082
88409
  }
88083
- if (run34.sizeHalfPoints !== undefined && run34.sizeHalfPoints !== baseline.sizeHalfPoints) {
88084
- styles.push(`font-size:${run34.sizeHalfPoints / 2}pt`);
88410
+ if (run33.sizeHalfPoints !== undefined && run33.sizeHalfPoints !== baseline.sizeHalfPoints) {
88411
+ styles.push(`font-size:${run33.sizeHalfPoints / 2}pt`);
88085
88412
  }
88086
- if (run34.smallCaps)
88413
+ if (run33.smallCaps)
88087
88414
  styles.push("font-variant:small-caps");
88088
- if (run34.allCaps)
88415
+ if (run33.allCaps)
88089
88416
  styles.push("text-transform:uppercase");
88090
- if (run34.colorTheme && !isDefaultThemeColor(run34)) {
88091
- attrs.push(htmlAttr("data-color-theme", run34.colorTheme));
88092
- if (run34.colorThemeTint) {
88093
- attrs.push(htmlAttr("data-color-theme-tint", run34.colorThemeTint));
88417
+ if (run33.colorTheme && !isDefaultThemeColor(run33)) {
88418
+ attrs.push(htmlAttr("data-color-theme", run33.colorTheme));
88419
+ if (run33.colorThemeTint) {
88420
+ attrs.push(htmlAttr("data-color-theme-tint", run33.colorThemeTint));
88094
88421
  }
88095
- if (run34.colorThemeShade) {
88096
- attrs.push(htmlAttr("data-color-theme-shade", run34.colorThemeShade));
88422
+ if (run33.colorThemeShade) {
88423
+ attrs.push(htmlAttr("data-color-theme-shade", run33.colorThemeShade));
88097
88424
  }
88098
88425
  }
88099
88426
  let out = body;
@@ -88102,18 +88429,18 @@ function wrapRunFormatting(body, run34, baseline) {
88102
88429
  const attrPart = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
88103
88430
  out = `<span${stylePart}${attrPart}>${out}</span>`;
88104
88431
  }
88105
- if (run34.underline === "single") {
88432
+ if (run33.underline === "single") {
88106
88433
  out = `<u>${out}</u>`;
88107
- } else if (run34.underline) {
88108
- const color2 = run34.underlineColor ? ` ${htmlAttr("data-underline-color", run34.underlineColor)}` : "";
88109
- out = `<u ${htmlAttr("data-underline", run34.underline)}${color2}>${out}</u>`;
88434
+ } else if (run33.underline) {
88435
+ const color2 = run33.underlineColor ? ` ${htmlAttr("data-underline-color", run33.underlineColor)}` : "";
88436
+ out = `<u ${htmlAttr("data-underline", run33.underline)}${color2}>${out}</u>`;
88110
88437
  }
88111
- if (run34.vertAlign === "superscript")
88438
+ if (run33.vertAlign === "superscript")
88112
88439
  out = `<sup>${out}</sup>`;
88113
- else if (run34.vertAlign === "subscript")
88440
+ else if (run33.vertAlign === "subscript")
88114
88441
  out = `<sub>${out}</sub>`;
88115
- if (run34.highlight) {
88116
- const named = run34.highlight === "yellow" ? "" : ` ${htmlAttr("data-highlight", run34.highlight)}`;
88442
+ if (run33.highlight) {
88443
+ const named = run33.highlight === "yellow" ? "" : ` ${htmlAttr("data-highlight", run33.highlight)}`;
88117
88444
  out = `<mark${named}>${out}</mark>`;
88118
88445
  }
88119
88446
  return out;
@@ -88122,8 +88449,8 @@ function isDefaultColor(color2) {
88122
88449
  const normalized = color2.toLowerCase();
88123
88450
  return normalized === "000000" || normalized === "auto";
88124
88451
  }
88125
- function isDefaultThemeColor(run34) {
88126
- return (run34.colorTheme === "text1" || run34.colorTheme === "dark1") && !run34.colorThemeTint && !run34.colorThemeShade;
88452
+ function isDefaultThemeColor(run33) {
88453
+ return (run33.colorTheme === "text1" || run33.colorTheme === "dark1") && !run33.colorThemeTint && !run33.colorThemeShade;
88127
88454
  }
88128
88455
  function cssFontFamily(font) {
88129
88456
  return /\s/.test(font) ? `'${font}'` : font;
@@ -88140,19 +88467,19 @@ function applyEscapeMask(text6, mask, offset2) {
88140
88467
  }
88141
88468
  function paragraphContent(runs, view) {
88142
88469
  let content3 = "";
88143
- for (const run34 of runs) {
88144
- if (run34.type !== "text")
88470
+ for (const run33 of runs) {
88471
+ if (run33.type !== "text")
88145
88472
  continue;
88146
- if (run34.runStyle === "Code")
88473
+ if (run33.runStyle === "Code")
88147
88474
  continue;
88148
- if (!isRunVisible(run34, view))
88475
+ if (!isRunVisible(run33, view))
88149
88476
  continue;
88150
- content3 += run34.text;
88477
+ content3 += run33.text;
88151
88478
  }
88152
88479
  return content3;
88153
88480
  }
88154
88481
  function hasEquationRun(runs) {
88155
- return runs.some((run34) => run34.type === "equation");
88482
+ return runs.some((run33) => run33.type === "equation");
88156
88483
  }
88157
88484
  function renderTable(table, ctx) {
88158
88485
  const view = ctx.options.view ?? "accepted";
@@ -88438,9 +88765,9 @@ var init_markdown3 = __esm(() => {
88438
88765
  // src/cli/read/index.ts
88439
88766
  var exports_read = {};
88440
88767
  __export(exports_read, {
88441
- run: () => run34
88768
+ run: () => run33
88442
88769
  });
88443
- async function run34(args) {
88770
+ async function run33(args) {
88444
88771
  const parsed = await tryParseArgs(args, {
88445
88772
  ast: { type: "boolean" },
88446
88773
  from: { type: "string" },
@@ -88450,16 +88777,16 @@ async function run34(args) {
88450
88777
  current: { type: "boolean" },
88451
88778
  comments: { type: "boolean" },
88452
88779
  help: { type: "boolean", short: "h" }
88453
- }, HELP30);
88780
+ }, HELP29);
88454
88781
  if (typeof parsed === "number")
88455
88782
  return parsed;
88456
88783
  if (parsed.values.help) {
88457
- await writeStdout(HELP30);
88784
+ await writeStdout(HELP29);
88458
88785
  return EXIT2.OK;
88459
88786
  }
88460
88787
  const path2 = parsed.positionals[0];
88461
88788
  if (!path2)
88462
- return fail("USAGE", "Missing FILE argument", HELP30);
88789
+ return fail("USAGE", "Missing FILE argument", HELP29);
88463
88790
  const ast = Boolean(parsed.values.ast);
88464
88791
  const from = parsed.values.from;
88465
88792
  const to = parsed.values.to;
@@ -88468,11 +88795,11 @@ async function run34(args) {
88468
88795
  const current = Boolean(parsed.values.current);
88469
88796
  const showComments = Boolean(parsed.values.comments);
88470
88797
  if (ast && (from || to || accepted || baseline || current || showComments)) {
88471
- return fail("USAGE", "--from, --to, --accepted, --baseline, --current, and --comments are Markdown-only and cannot be combined with --ast", HELP30);
88798
+ return fail("USAGE", "--from, --to, --accepted, --baseline, --current, and --comments are Markdown-only and cannot be combined with --ast", HELP29);
88472
88799
  }
88473
88800
  const view = resolveView({ accepted, baseline, current });
88474
88801
  if (!view) {
88475
- return fail("USAGE", "--accepted, --baseline, and --current are mutually exclusive", HELP30);
88802
+ return fail("USAGE", "--accepted, --baseline, and --current are mutually exclusive", HELP29);
88476
88803
  }
88477
88804
  const docView = await openOrFail(path2);
88478
88805
  if (typeof docView === "number")
@@ -88488,7 +88815,8 @@ async function run34(args) {
88488
88815
  to,
88489
88816
  view,
88490
88817
  showComments,
88491
- defaultSizeHalfPoints: docView.styles?.defaultSizeHalfPoints()
88818
+ defaultSizeHalfPoints: docView.styles?.defaultSizeHalfPoints(),
88819
+ trackChangesOn: docView.isTrackChangesEnabled()
88492
88820
  });
88493
88821
  await writeStdout(rendered);
88494
88822
  return EXIT2.OK;
@@ -88499,7 +88827,7 @@ async function run34(args) {
88499
88827
  throw err;
88500
88828
  }
88501
88829
  }
88502
- var HELP30 = `docx read \u2014 render document body as Markdown, or print AST as JSON
88830
+ var HELP29 = `docx read \u2014 render document body as Markdown, or print AST as JSON
88503
88831
 
88504
88832
  Usage:
88505
88833
  docx read FILE [options]
@@ -88589,20 +88917,20 @@ function parsePagesSpec(spec) {
88589
88917
  // src/cli/render/index.ts
88590
88918
  var exports_render = {};
88591
88919
  __export(exports_render, {
88592
- run: () => run35
88920
+ run: () => run34
88593
88921
  });
88594
88922
  import { basename as basename2, extname as extname2, resolve as resolve2 } from "path";
88595
- async function run35(args) {
88596
- const parsed = await tryParseArgs(args, OPTION_SPEC5, HELP31);
88923
+ async function run34(args) {
88924
+ const parsed = await tryParseArgs(args, OPTION_SPEC4, HELP30);
88597
88925
  if (typeof parsed === "number")
88598
88926
  return parsed;
88599
88927
  if (parsed.values.help) {
88600
- await writeStdout(HELP31);
88928
+ await writeStdout(HELP30);
88601
88929
  return EXIT2.OK;
88602
88930
  }
88603
88931
  const filePath = parsed.positionals[0];
88604
88932
  if (!filePath)
88605
- return fail("USAGE", "Missing FILE argument", HELP31);
88933
+ return fail("USAGE", "Missing FILE argument", HELP30);
88606
88934
  if (!await Bun.file(filePath).exists()) {
88607
88935
  return fail("FILE_NOT_FOUND", `File not found: ${filePath}`);
88608
88936
  }
@@ -88682,7 +89010,7 @@ function availabilityHint(installed) {
88682
89010
  }
88683
89011
  return `Detected engines: ${installed.join(", ")} \u2014 but none chose auto-select; pass --engine explicitly.`;
88684
89012
  }
88685
- var HELP31 = `docx render \u2014 render each page of a .docx as a PNG/JPG image
89013
+ var HELP30 = `docx render \u2014 render each page of a .docx as a PNG/JPG image
88686
89014
 
88687
89015
  Usage:
88688
89016
  docx render FILE [options]
@@ -88734,11 +89062,11 @@ Runtime dependencies:
88734
89062
  \u2014 no extra system tools (poppler / pdftoppm / ImageMagick) required.
88735
89063
 
88736
89064
  Run "docx render --help" or "docx --help" for the full command list.
88737
- `, OPTION_SPEC5;
89065
+ `, OPTION_SPEC4;
88738
89066
  var init_render2 = __esm(() => {
88739
89067
  init_core2();
88740
89068
  init_respond();
88741
- OPTION_SPEC5 = {
89069
+ OPTION_SPEC4 = {
88742
89070
  out: { type: "string" },
88743
89071
  engine: { type: "string" },
88744
89072
  dpi: { type: "string" },
@@ -88899,9 +89227,9 @@ var init_batch4 = __esm(() => {
88899
89227
  // src/cli/replace/index.ts
88900
89228
  var exports_replace3 = {};
88901
89229
  __export(exports_replace3, {
88902
- run: () => run36
89230
+ run: () => run35
88903
89231
  });
88904
- async function run36(args) {
89232
+ async function run35(args) {
88905
89233
  const parsed = await tryParseArgs(args, {
88906
89234
  batch: { type: "string" },
88907
89235
  regex: { type: "boolean" },
@@ -88914,30 +89242,30 @@ async function run36(args) {
88914
89242
  baseline: { type: "boolean" },
88915
89243
  exact: { type: "boolean" },
88916
89244
  ...SAVE_FLAGS
88917
- }, HELP32);
89245
+ }, HELP31);
88918
89246
  if (typeof parsed === "number")
88919
89247
  return parsed;
88920
89248
  if (parsed.values.help) {
88921
- await writeStdout(HELP32);
89249
+ await writeStdout(HELP31);
88922
89250
  return EXIT2.OK;
88923
89251
  }
88924
89252
  setVerboseAck(Boolean(parsed.values.verbose));
88925
89253
  const path2 = parsed.positionals[0];
88926
89254
  if (!path2)
88927
- return fail("USAGE", "Missing FILE argument", HELP32);
89255
+ return fail("USAGE", "Missing FILE argument", HELP31);
88928
89256
  const batchInput = parsed.values.batch;
88929
89257
  if (batchInput !== undefined) {
88930
89258
  if (parsed.positionals.length > 1) {
88931
- return fail("USAGE", "--batch reads pattern/replacement from the JSONL file; don't also pass them as positionals", HELP32);
89259
+ return fail("USAGE", "--batch reads pattern/replacement from the JSONL file; don't also pass them as positionals", HELP31);
88932
89260
  }
88933
89261
  return runReplaceBatch(path2, batchInput, parsed.values);
88934
89262
  }
88935
89263
  const pattern = parsed.positionals[1];
88936
89264
  const replacement = parsed.positionals[2];
88937
89265
  if (pattern == null)
88938
- return fail("USAGE", "Missing PATTERN argument", HELP32);
89266
+ return fail("USAGE", "Missing PATTERN argument", HELP31);
88939
89267
  if (replacement == null) {
88940
- return fail("USAGE", "Missing REPLACEMENT argument", HELP32);
89268
+ return fail("USAGE", "Missing REPLACEMENT argument", HELP31);
88941
89269
  }
88942
89270
  const ignoreCase = Boolean(parsed.values["ignore-case"]);
88943
89271
  const useRegex = Boolean(parsed.values.regex);
@@ -89041,6 +89369,8 @@ async function run36(args) {
89041
89369
  replaceSpanInParagraph(blockRef.node, { start: match.start, end: match.end }, concreteReplacement, tracked, findView);
89042
89370
  }
89043
89371
  await document4.save(outputPath);
89372
+ const remaining = allMatches.length - selected.length;
89373
+ const partialHint = remaining > 0 ? `\u21B3 ${remaining} more match${remaining === 1 ? "" : "es"} left unreplaced (${selected.length} of ${allMatches.length} done) \u2014 pass --all to replace every match, or --limit N for a specific count.` : undefined;
89044
89374
  await respondAck({
89045
89375
  ok: true,
89046
89376
  operation: "replace",
@@ -89054,10 +89384,10 @@ async function run36(args) {
89054
89384
  replaced: selected.length,
89055
89385
  matches: matchesPayload,
89056
89386
  ...normalizationFields
89057
- });
89387
+ }, partialHint);
89058
89388
  return EXIT2.OK;
89059
89389
  }
89060
- var HELP32 = `docx replace \u2014 substitute text spans (sed for docx)
89390
+ var HELP31 = `docx replace \u2014 substitute text spans (sed for docx)
89061
89391
 
89062
89392
  Usage:
89063
89393
  docx replace FILE PATTERN REPLACEMENT [options]
@@ -89135,6 +89465,258 @@ var init_replace4 = __esm(() => {
89135
89465
  init_batch4();
89136
89466
  });
89137
89467
 
89468
+ // src/cli/sections/index.tsx
89469
+ var exports_sections = {};
89470
+ __export(exports_sections, {
89471
+ run: () => run36
89472
+ });
89473
+ async function run36(args) {
89474
+ const parsed = await tryParseArgs(args, OPTION_SPEC5, HELP32);
89475
+ if (typeof parsed === "number")
89476
+ return parsed;
89477
+ if (parsed.values.help) {
89478
+ await writeStdout(HELP32);
89479
+ return EXIT2.OK;
89480
+ }
89481
+ setVerboseAck(Boolean(parsed.values.verbose));
89482
+ const filePath = parsed.positionals[0];
89483
+ if (!filePath)
89484
+ return fail("USAGE", "Missing FILE argument", HELP32);
89485
+ const at = parsed.values.at;
89486
+ if (!at) {
89487
+ return fail("USAGE", "Missing --at LOCATOR (a range pN-pM or a section sN)", HELP32);
89488
+ }
89489
+ const count = parseCount(parsed.values.columns);
89490
+ if (typeof count === "number" && Number.isNaN(count)) {
89491
+ return fail("USAGE", `--columns must be a positive integer`, HELP32);
89492
+ }
89493
+ if (count === undefined)
89494
+ return fail("USAGE", "Missing --columns N", HELP32);
89495
+ const typeRaw = parsed.values.type;
89496
+ if (typeRaw !== undefined && !isSectionType(typeRaw)) {
89497
+ return fail("USAGE", `Invalid --type: ${typeRaw}`, "Valid values: continuous, nextPage, evenPage, oddPage, nextColumn");
89498
+ }
89499
+ let locator;
89500
+ try {
89501
+ locator = parseLocator(at);
89502
+ } catch (error) {
89503
+ if (error instanceof LocatorParseError) {
89504
+ return fail("INVALID_LOCATOR", error.message, HELP32);
89505
+ }
89506
+ throw error;
89507
+ }
89508
+ const document4 = await openOrFail(filePath);
89509
+ if (typeof document4 === "number")
89510
+ return document4;
89511
+ const opts = {
89512
+ filePath,
89513
+ count,
89514
+ sectionType: typeRaw,
89515
+ authorFlag: parsed.values.author,
89516
+ trackFlag: Boolean(parsed.values.track),
89517
+ outputPath: parsed.values.output,
89518
+ dryRun: Boolean(parsed.values["dry-run"])
89519
+ };
89520
+ if (locator.kind === "blockRange") {
89521
+ const range = await resolveBlockRangeOrFail(document4, at);
89522
+ if (typeof range === "number")
89523
+ return range;
89524
+ return wrapRange(document4, range.parent, range.startIndex, range.endIndex, at, opts);
89525
+ }
89526
+ const blockRef = await resolveBlockOrFail(document4, at);
89527
+ if (typeof blockRef === "number")
89528
+ return blockRef;
89529
+ if (blockRef.node.tag === "w:sectPr") {
89530
+ return editSection(document4, blockRef, at, opts);
89531
+ }
89532
+ if (blockRef.node.tag === "w:p") {
89533
+ const index2 = blockRef.parent.indexOf(blockRef.node);
89534
+ return wrapRange(document4, blockRef.parent, index2, index2, at, opts);
89535
+ }
89536
+ return fail("INVALID_LOCATOR", `columns needs a section (sN) or a paragraph range (pN-pM); ${at} is neither`, HELP32);
89537
+ }
89538
+ async function editSection(document4, blockRef, at, opts) {
89539
+ try {
89540
+ new Edit(document4).section(blockRef, { columns: opts.count, sectionType: opts.sectionType }, { authorFlag: opts.authorFlag, track: opts.trackFlag });
89541
+ } catch (error) {
89542
+ if (error instanceof EditError)
89543
+ return fail(error.code, error.message);
89544
+ throw error;
89545
+ }
89546
+ return commit(document4, opts, { at, mode: "section" });
89547
+ }
89548
+ async function wrapRange(document4, parent, startIndex, endIndex, at, opts) {
89549
+ if (parent !== document4.body.findBodyChildren()) {
89550
+ return fail("USAGE", `columns can only wrap body-level paragraphs; ${at} is inside a table cell`, "Section breaks carry column layout and cannot live in a table cell.");
89551
+ }
89552
+ for (let index2 = startIndex;index2 <= endIndex; index2++) {
89553
+ if (containsSectionBreak(parent[index2])) {
89554
+ return fail("USAGE", `The range ${at} already contains a section break`, "Target the existing section directly with `--at sN`, or pick a range without one.");
89555
+ }
89556
+ }
89557
+ const governing = governingColumns(parent, endIndex + 1);
89558
+ const track = resolveTracked(document4, opts.trackFlag);
89559
+ const insert = new Insert(document4);
89560
+ const allocator = track ? new TrackChanges(document4).createAllocator() : undefined;
89561
+ const endNode = parent[endIndex];
89562
+ if (!endNode) {
89563
+ return fail("BLOCK_NOT_FOUND", "Range end is stale (parent does not contain it)");
89564
+ }
89565
+ const startNode = startIndex > 0 ? parent[startIndex] : undefined;
89566
+ if (startIndex > 0 && !startNode) {
89567
+ return fail("BLOCK_NOT_FOUND", "Range start is stale (parent does not contain it)");
89568
+ }
89569
+ let afterBlocks;
89570
+ let beforeBlocks = [];
89571
+ try {
89572
+ afterBlocks = await insert.paragraph({ node: endNode, parent }, {
89573
+ kind: "section",
89574
+ columns: opts.count,
89575
+ sectionType: opts.sectionType ?? "continuous"
89576
+ }, {}, { placement: "after", authorFlag: opts.authorFlag, track, allocator });
89577
+ if (startNode) {
89578
+ beforeBlocks = await insert.paragraph({ node: startNode, parent }, {
89579
+ kind: "section",
89580
+ ...governing > 1 ? { columns: governing } : {},
89581
+ sectionType: "continuous"
89582
+ }, {}, { placement: "before", authorFlag: opts.authorFlag, track, allocator });
89583
+ }
89584
+ } catch (error) {
89585
+ if (error instanceof InsertError)
89586
+ return fail(error.code, error.message, error.hint);
89587
+ throw error;
89588
+ }
89589
+ if (opts.dryRun) {
89590
+ return commit(document4, opts, { at, mode: "range", dryRunOnly: true });
89591
+ }
89592
+ parent.splice(endIndex + 1, 0, ...afterBlocks);
89593
+ parent.splice(startIndex, 0, ...beforeBlocks);
89594
+ return commit(document4, opts, { at, mode: "range" });
89595
+ }
89596
+ function containsSectionBreak(node2) {
89597
+ if (!node2)
89598
+ return false;
89599
+ if (node2.tag === "w:sectPr")
89600
+ return true;
89601
+ if (node2.tag !== "w:p")
89602
+ return false;
89603
+ return Boolean(node2.findChild("w:pPr")?.findChild("w:sectPr"));
89604
+ }
89605
+ function governingColumns(parent, fromIndex) {
89606
+ for (let index2 = fromIndex;index2 < parent.length; index2++) {
89607
+ const node2 = parent[index2];
89608
+ if (!node2)
89609
+ continue;
89610
+ if (node2.tag === "w:sectPr")
89611
+ return columnsOf(node2);
89612
+ if (node2.tag === "w:p") {
89613
+ const inline = node2.findChild("w:pPr")?.findChild("w:sectPr");
89614
+ if (inline)
89615
+ return columnsOf(inline);
89616
+ }
89617
+ }
89618
+ return 1;
89619
+ }
89620
+ function columnsOf(sectPr) {
89621
+ const raw = sectPr.findChild("w:cols")?.getAttribute("w:num");
89622
+ const parsed = raw ? Number.parseInt(raw, 10) : 1;
89623
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
89624
+ }
89625
+ function parseCount(raw) {
89626
+ if (raw === undefined)
89627
+ return;
89628
+ if (!/^\d+$/.test(raw.trim()))
89629
+ return Number.NaN;
89630
+ const parsed = Number.parseInt(raw, 10);
89631
+ if (!Number.isFinite(parsed) || parsed < 1)
89632
+ return Number.NaN;
89633
+ return parsed;
89634
+ }
89635
+ async function commit(document4, opts, meta) {
89636
+ if (opts.dryRun || meta.dryRunOnly) {
89637
+ await respond({
89638
+ operation: "sections",
89639
+ dryRun: true,
89640
+ path: opts.filePath,
89641
+ locator: meta.at,
89642
+ columnCount: opts.count,
89643
+ mode: meta.mode,
89644
+ ...opts.outputPath ? { output: opts.outputPath } : {}
89645
+ });
89646
+ return EXIT2.OK;
89647
+ }
89648
+ await document4.save(opts.outputPath);
89649
+ const destination = opts.outputPath ?? opts.filePath;
89650
+ await respondAck({
89651
+ ok: true,
89652
+ operation: "sections",
89653
+ path: destination,
89654
+ locator: meta.at,
89655
+ columnCount: opts.count,
89656
+ mode: meta.mode
89657
+ }, renderVerifyHint(destination));
89658
+ return EXIT2.OK;
89659
+ }
89660
+ var HELP32 = `docx sections \u2014 multi-column layout & section breaks
89661
+
89662
+ Usage:
89663
+ docx sections FILE --at LOCATOR --columns N [options]
89664
+
89665
+ The verb for section layout \u2014 multi-column flow and section breaks \u2014 so you
89666
+ don't have to hand-build them with the right OOXML semantics. This is the ONLY
89667
+ way to add/change column layout; \`insert\` no longer takes --section (a raw
89668
+ section break formats the content ABOVE it, which is the classic off-by-one).
89669
+ Two addressing modes:
89670
+
89671
+ --at pN-pM Wrap the paragraph range pN\u2026pM in its own N-column section
89672
+ (inserts the bounding continuous section breaks for you, so the
89673
+ columns land on EXACTLY pN\u2026pM). Also accepts a single paragraph
89674
+ (--at pN). THIS is how you put text in columns \u2014 name the range.
89675
+ --at sN Set the column count on an EXISTING section break sN (the section
89676
+ whose content ENDS at sN). Equivalent to \`edit --at sN --columns N\`.
89677
+
89678
+ Options:
89679
+ --at LOCATOR Paragraph range (pN-pM), single paragraph (pN), or section (sN)
89680
+ --columns N Number of columns (>= 1; use 1 to collapse back to single column)
89681
+ --type T Section type for the wrapping break: continuous (default),
89682
+ nextPage, evenPage, oddPage, nextColumn. Only meaningful with
89683
+ a pN-pM/pN range; with sN it overrides the section's type.
89684
+ --author NAME Author for tracked changes (default: $DOCX_AUTHOR)
89685
+ --track Record as a tracked change even when the doc toggle is off
89686
+ -o, --output PATH Write to PATH instead of overwriting FILE
89687
+ --dry-run Print what would change; do not write the file
89688
+ -v, --verbose Print the full success ack JSON
89689
+ -h, --help Show this help
89690
+
89691
+ Agent tip: VERIFY LAYOUT VISUALLY. \`docx read\` shows the section as a
89692
+ \`<!-- docx:section \u2026 cols="N" -->\` annotation but NOT how the columns actually
89693
+ flow on the page (balance, overflow). After setting columns, render and look:
89694
+ docx render FILE --out pages/
89695
+ Adjust the range and re-render until the columns read the way you intended.
89696
+
89697
+ Output:
89698
+ Prints a one-line confirmation on success (exit 0); --verbose prints {ok:true, \u2026}. Positional ids shift
89699
+ after a range wrap (two section breaks are inserted), so re-read before further
89700
+ edits. Errors print {code, error, hint?} + nonzero exit.
89701
+
89702
+ Examples:
89703
+ docx sections doc.docx --at p4-p9 --columns 2
89704
+ docx sections doc.docx --at p4-p9 --columns 3 --type continuous
89705
+ docx sections doc.docx --at s2 --columns 1 # collapse section s2 to one column
89706
+ `, OPTION_SPEC5;
89707
+ var init_sections2 = __esm(() => {
89708
+ init_core2();
89709
+ init_respond();
89710
+ OPTION_SPEC5 = {
89711
+ at: { type: "string" },
89712
+ columns: { type: "string" },
89713
+ type: { type: "string" },
89714
+ author: { type: "string" },
89715
+ track: { type: "boolean" },
89716
+ ...SAVE_FLAGS
89717
+ };
89718
+ });
89719
+
89138
89720
  // src/cli/styles/index.ts
89139
89721
  var exports_styles = {};
89140
89722
  __export(exports_styles, {
@@ -89366,8 +89948,15 @@ async function run38(args) {
89366
89948
  return fail("USAGE", `--position must be an integer in 0..${grid.rows.length}`);
89367
89949
  }
89368
89950
  const cellTexts = parsed.values.cells ? parsed.values.cells.split(",") : [];
89369
- if (cellTexts.length > grid.colCount) {
89370
- return fail("USAGE", `--cells has ${cellTexts.length} entries but table ${tableId} has ${grid.colCount} columns`);
89951
+ for (const cell of cellTexts) {
89952
+ const mangled = await rejectShellMangledValue(cell, HELP34, "--cells");
89953
+ if (typeof mangled === "number")
89954
+ return mangled;
89955
+ }
89956
+ const reference = referenceRow(grid, position2);
89957
+ const logicalCols = reference ? reference.cells.length : grid.colCount;
89958
+ if (cellTexts.length > logicalCols) {
89959
+ return fail("USAGE", `--cells has ${cellTexts.length} entries but the row has ${logicalCols} columns (a merged cell counts once)`);
89371
89960
  }
89372
89961
  const newRow = buildRow(grid, position2, cellTexts);
89373
89962
  const tracking = resolveTracked(document4, parsed.values.track);
@@ -89409,9 +89998,16 @@ function resolveRowPosition(raw, rowCount) {
89409
89998
  return null;
89410
89999
  return value;
89411
90000
  }
90001
+ function referenceRow(grid, position2) {
90002
+ const above = position2 > 0 ? grid.rows[position2 - 1] : undefined;
90003
+ const below = position2 < grid.rows.length ? grid.rows[position2] : undefined;
90004
+ return above ?? below;
90005
+ }
89412
90006
  function buildRow(grid, position2, cellTexts) {
89413
90007
  const below = position2 > 0 && position2 < grid.rows.length ? grid.rows[position2] : undefined;
90008
+ const spans = referenceRow(grid, position2);
89414
90009
  const cells = [];
90010
+ let logical = 0;
89415
90011
  for (let col = 0;col < grid.colCount; ) {
89416
90012
  const continued = below ? cellAt(below, col) : undefined;
89417
90013
  if (continued?.vMerge === "continue") {
@@ -89425,12 +90021,15 @@ function buildRow(grid, position2, cellTexts) {
89425
90021
  col += continued.colSpan;
89426
90022
  continue;
89427
90023
  }
90024
+ const refSpan = spans ? cellAt(spans, col)?.colSpan ?? 1 : 1;
89428
90025
  cells.push(/* @__PURE__ */ jsxDEV(TableCell, {
90026
+ gridSpan: refSpan > 1 ? refSpan : undefined,
89429
90027
  children: /* @__PURE__ */ jsxDEV(Paragraph, {
89430
- text: cellTexts[col] ?? ""
90028
+ text: cellTexts[logical] ?? ""
89431
90029
  }, undefined, false, undefined, this)
89432
90030
  }, undefined, false, undefined, this));
89433
- col += 1;
90031
+ logical += 1;
90032
+ col += refSpan;
89434
90033
  }
89435
90034
  return /* @__PURE__ */ jsxDEV(w.tr, {
89436
90035
  children: cells
@@ -89450,6 +90049,7 @@ var init_insert_row = __esm(() => {
89450
90049
  init_jsx();
89451
90050
  init_locators();
89452
90051
  init_table();
90052
+ init_parse_helpers();
89453
90053
  init_respond();
89454
90054
  init_jsx_dev_runtime();
89455
90055
  AT_FORMS6 = describeForms(["table"], " ");
@@ -89465,8 +90065,11 @@ ${AT_FORMS6}
89465
90065
 
89466
90066
  Optional:
89467
90067
  --position INDEX 0-based row index to insert at (default: append at end)
89468
- --cells "a,b,c" Comma-separated text for the new cells (default: empty).
89469
- Must not exceed the table's column count.
90068
+ --cells "a,b,c" Comma-separated text, one value per VISIBLE cell left to
90069
+ right (default: empty). On a table with merged cells the new
90070
+ row copies the neighbor row's gridSpan pattern, so you pass
90071
+ one value per logical column (a spanned cell counts once),
90072
+ NOT one per underlying grid column.
89470
90073
  --author NAME Author for tracked insertion (default: $DOCX_AUTHOR)
89471
90074
  -o, --output PATH Write to PATH instead of overwriting FILE
89472
90075
  --dry-run Print what would change; do not write
@@ -89999,10 +90602,13 @@ async function run42(args) {
89999
90602
  const cols = grid.tblGrid.findChildren("w:gridCol");
90000
90603
  let twips = [];
90001
90604
  if (!auto) {
90002
- const resolved = resolveWidths(widthsSpec, cols);
90605
+ const resolved = resolveWidths(widthsSpec, cols, grid);
90003
90606
  if (typeof resolved === "string")
90004
90607
  return fail("USAGE", resolved);
90005
90608
  twips = resolved;
90609
+ const tooNarrow = findTooNarrowCell(grid, twips);
90610
+ if (tooNarrow)
90611
+ return fail("USAGE", tooNarrow);
90006
90612
  }
90007
90613
  const outputPath = parsed.values.output;
90008
90614
  if (parsed.values["dry-run"]) {
@@ -90038,20 +90644,24 @@ async function run42(args) {
90038
90644
  appendTblGridChange(grid.tblGrid, priorCols, new TrackChanges(document4).mintMeta(author));
90039
90645
  }
90040
90646
  await document4.save(outputPath);
90647
+ const destination = outputPath ?? path2;
90648
+ const echo = auto ? "" : `${describeColumnWidths(twips)}
90649
+ `;
90041
90650
  await respondAck({
90042
90651
  ok: true,
90043
90652
  operation: "tables.set-widths",
90044
- path: outputPath ?? path2,
90653
+ path: destination,
90045
90654
  table: tableId,
90046
90655
  layout: auto ? "autofit" : "fixed",
90047
90656
  widths: auto ? "auto" : twips
90048
- });
90657
+ }, `${echo}${renderVerifyHint(destination)}`);
90049
90658
  return EXIT2.OK;
90050
90659
  }
90051
- function resolveWidths(spec, cols) {
90660
+ function resolveWidths(spec, cols, grid) {
90052
90661
  const tokens = spec.split(",").map((token) => token.trim());
90053
90662
  if (tokens.length !== cols.length) {
90054
- return `--widths has ${tokens.length} entries but the table has ${cols.length} columns`;
90663
+ const base = `--widths has ${tokens.length} entries but the table has ${cols.length} grid columns`;
90664
+ return hasMergedColumns(grid) ? `${base}. This table has merged cells, so some visible columns span multiple grid columns and the grid has more columns than any single row shows. Supply one width per GRID column (${cols.length} values), left-to-right; a merged cell takes the sum of the grid columns it covers. Inspect the gridSpan layout with \`docx read --ast\`.` : base;
90055
90665
  }
90056
90666
  const percentage = tokens.every((token) => token.endsWith("%"));
90057
90667
  const anyPercent = tokens.some((token) => token.endsWith("%"));
@@ -90103,7 +90713,30 @@ function cellWidth(cell, twips) {
90103
90713
  }
90104
90714
  return width;
90105
90715
  }
90106
- var AT_FORMS10, HELP38;
90716
+ function findTooNarrowCell(grid, twips) {
90717
+ for (const row of grid.rows) {
90718
+ for (const cell of row.cells) {
90719
+ const width = cellWidth(cell, twips);
90720
+ if (width >= MIN_COL_TWIPS)
90721
+ continue;
90722
+ const text6 = cell.node.collectText().trim();
90723
+ if (text6.length === 0)
90724
+ continue;
90725
+ const where = cell.colSpan > 1 ? `grid columns ${cell.colStart}\u2013${cell.colStart + cell.colSpan - 1}` : `grid column ${cell.colStart}`;
90726
+ const sample = text6.length > 24 ? `${text6.slice(0, 24)}\u2026` : text6;
90727
+ return `--widths collapses ${where} to ${twipsToInches(width)}in (${width} twips); that cell holds "${sample}" but ~0.15in goes to cell margins, leaving under one character \u2014 Word wraps it one char per line. Widen it and lower a wider column to compensate.`;
90728
+ }
90729
+ }
90730
+ return null;
90731
+ }
90732
+ function hasMergedColumns(grid) {
90733
+ return grid.rows.some((row) => row.cells.some((cell) => cell.colSpan > 1));
90734
+ }
90735
+ function describeColumnWidths(twips) {
90736
+ const cells = twips.map((value, index2) => `g${index2}=${twipsToInches(value)}in`);
90737
+ return `widths: ${cells.join(" ")}`;
90738
+ }
90739
+ var AT_FORMS10, HELP38, MIN_COL_TWIPS = 288;
90107
90740
  var init_set_widths = __esm(() => {
90108
90741
  init_core2();
90109
90742
  init_locators();
@@ -90136,6 +90769,15 @@ cell's <w:tcW>. Under track-changes the resize is recorded as a real revision
90136
90769
  (<w:tblGridChange> for the grid plus a per-cell <w:tcPrChange>), so it can be
90137
90770
  accepted or rejected \u2014 matching what Word emits for a width change.
90138
90771
 
90772
+ Widths map one value per GRID column, not per visible column. On a table with
90773
+ merged cells (gridSpan), the grid has MORE columns than a single row shows, so
90774
+ the count you pass must match the grid (run \`docx read --ast\` to see it). A
90775
+ cell that HOLDS TEXT but lands narrower than ~0.2in is refused \u2014 after ~0.15in
90776
+ of cell margin it fits under one character, so Word wraps it one char per line
90777
+ (empty/spacer columns that thin are fine, nothing to wrap). The success line
90778
+ echoes the resulting per-column widths; since layout changes don't show in
90779
+ \`read\`, render to verify.
90780
+
90139
90781
  Output:
90140
90782
  Prints a one-line confirmation on success (exit 0). --verbose prints {ok:true, operation, path, table,
90141
90783
  layout, widths}. --dry-run prints the preview object (no ok field). Errors
@@ -90677,6 +91319,64 @@ var init_tables = __esm(() => {
90677
91319
  };
90678
91320
  });
90679
91321
 
91322
+ // src/cli/track-changes/groups.ts
91323
+ function revisionGroups(changes) {
91324
+ const sorted = [...changes].sort((a2, b) => tcIndex(a2.id) - tcIndex(b.id));
91325
+ const membersOf = new Map;
91326
+ const revOf = new Map;
91327
+ let next = 0;
91328
+ let index2 = 0;
91329
+ while (index2 < sorted.length) {
91330
+ const a2 = sorted[index2];
91331
+ const b = sorted[index2 + 1];
91332
+ if (a2 && b && isTextReplacePair(a2, b)) {
91333
+ const rev = `rev${next++}`;
91334
+ membersOf.set(rev, [a2.id, b.id]);
91335
+ revOf.set(a2.id, rev);
91336
+ revOf.set(b.id, rev);
91337
+ index2 += 2;
91338
+ continue;
91339
+ }
91340
+ index2 += 1;
91341
+ }
91342
+ return { membersOf, revOf };
91343
+ }
91344
+ function isTextReplacePair(a2, b) {
91345
+ if (a2.blockId === undefined || a2.blockId !== b.blockId)
91346
+ return false;
91347
+ const pair = `${a2.kind}+${b.kind}`;
91348
+ return pair === "del+ins" || pair === "ins+del";
91349
+ }
91350
+ function expandRevisionTargets(targets, groups) {
91351
+ const out = [];
91352
+ for (const target of targets) {
91353
+ if (/^rev\d+$/.test(target)) {
91354
+ const members = groups.membersOf.get(target);
91355
+ if (!members)
91356
+ throw new UnknownRevisionError(target);
91357
+ out.push(...members);
91358
+ continue;
91359
+ }
91360
+ out.push(target);
91361
+ }
91362
+ return out;
91363
+ }
91364
+ function tcIndex(id) {
91365
+ const match = id.match(/^tc(\d+)$/);
91366
+ return match?.[1] ? Number(match[1]) : 0;
91367
+ }
91368
+ var UnknownRevisionError;
91369
+ var init_groups = __esm(() => {
91370
+ UnknownRevisionError = class UnknownRevisionError extends Error {
91371
+ revision;
91372
+ constructor(revision) {
91373
+ super(`Unknown revision group "${revision}"`);
91374
+ this.revision = revision;
91375
+ this.name = "UnknownRevisionError";
91376
+ }
91377
+ };
91378
+ });
91379
+
90680
91380
  // src/cli/track-changes/list.ts
90681
91381
  var exports_list5 = {};
90682
91382
  __export(exports_list5, {
@@ -90737,6 +91437,12 @@ async function run47(args) {
90737
91437
  byId.set(change.id, record);
90738
91438
  }
90739
91439
  const sorted = [...byId.values()].sort((a2, b) => trackedChangeIndex(a2.id) - trackedChangeIndex(b.id));
91440
+ const { revOf } = revisionGroups(sorted);
91441
+ for (const record of sorted) {
91442
+ const group = revOf.get(record.id);
91443
+ if (group)
91444
+ record.group = group;
91445
+ }
90740
91446
  await respond(sorted);
90741
91447
  return EXIT2.OK;
90742
91448
  }
@@ -90761,7 +91467,11 @@ apart.
90761
91467
 
90762
91468
  Output: a bare JSON array of { id, kind, author, date, revisionId, blockId,
90763
91469
  text } sorted by id (document order). Each item's "id" (e.g. tc0) is its
90764
- addressable handle \u2014 pass it to \`accept\`/\`reject --at tcN\`. Errors print
91470
+ addressable handle \u2014 pass it to \`accept\`/\`reject --at tcN\`. When a del and an
91471
+ ins are an adjacent REPLACE pair on the same paragraph, BOTH carry a shared
91472
+ "group": "revN" \u2014 accept/reject the whole logical change in one call with
91473
+ \`--at revN\` instead of accepting each half separately (tcN ids renumber after
91474
+ each single accept, so the revN handle avoids the re-list ping-pong). Errors print
90765
91475
  {code, error, hint?} with a nonzero exit. kind is one of: "ins", "del", "moveFrom",
90766
91476
  "moveTo", "sectPrChange", "rowIns", "rowDel", "cellIns", "cellDel",
90767
91477
  "tblGridChange", "tblPrChange", "tcPrChange", "checkboxToggle". Paragraph-mark
@@ -90790,6 +91500,7 @@ var init_list7 = __esm(() => {
90790
91500
  init_core2();
90791
91501
  init_track_changes();
90792
91502
  init_respond();
91503
+ init_groups();
90793
91504
  });
90794
91505
 
90795
91506
  // src/cli/track-changes/apply.ts
@@ -90820,8 +91531,20 @@ async function runApply(args, verb, help) {
90820
91531
  const document4 = await openOrFail(path2);
90821
91532
  if (typeof document4 === "number")
90822
91533
  return document4;
90823
- const target = all2 ? "all" : atRaw ?? [];
90824
91534
  const trackChanges = new TrackChanges(document4);
91535
+ let target;
91536
+ if (all2) {
91537
+ target = "all";
91538
+ } else {
91539
+ try {
91540
+ target = expandRevisionTargets(atRaw ?? [], revisionGroups(trackChanges.list()));
91541
+ } catch (error) {
91542
+ if (error instanceof UnknownRevisionError) {
91543
+ return fail("TRACKED_CHANGE_NOT_FOUND", error.message, "Run 'docx track-changes list FILE' \u2014 revN handles appear as the `group` field on paired changes.");
91544
+ }
91545
+ throw error;
91546
+ }
91547
+ }
90825
91548
  const outputPath = parsed.values.output;
90826
91549
  try {
90827
91550
  if (parsed.values["dry-run"]) {
@@ -90853,6 +91576,7 @@ async function runApply(args, verb, help) {
90853
91576
  var init_apply2 = __esm(() => {
90854
91577
  init_track_changes();
90855
91578
  init_respond();
91579
+ init_groups();
90856
91580
  });
90857
91581
 
90858
91582
  // src/cli/track-changes/accept.ts
@@ -90899,6 +91623,10 @@ Target (one required, mutually exclusive):
90899
91623
  batch is not a concern. Supports:
90900
91624
  ${AT_FORMS14}
90901
91625
  See \`docx info locators\`.
91626
+ --at revN Accept a del+ins REPLACE pair in one call (both halves of one
91627
+ logical change). \`list\` tags the two tcNs with a shared
91628
+ "group": "revN"; addressing the revN saves the accept-relist-
91629
+ accept ping-pong (tcN ids renumber after each single accept).
90902
91630
  --all Accept every tracked change.
90903
91631
 
90904
91632
  Options:
@@ -90952,7 +91680,7 @@ were in effect before the tracked edit.
90952
91680
 
90953
91681
  Paragraph-mark trackings (<w:ins>/<w:del> inside <w:pPr><w:rPr>): rejecting
90954
91682
  a paragraph-mark insertion removes the entire owning paragraph (the inserted
90955
- break disappears \u2014 for sentinels created by "insert --section" this also
91683
+ break disappears \u2014 for sentinels created by "docx sections" this also
90956
91684
  removes the section break the sentinel was carrying). Rejecting a
90957
91685
  paragraph-mark deletion just removes the marker (the paragraph stays).
90958
91686
 
@@ -90970,6 +91698,10 @@ Target (one required, mutually exclusive):
90970
91698
  batch is not a concern. Supports:
90971
91699
  ${AT_FORMS15}
90972
91700
  See \`docx info locators\`.
91701
+ --at revN Reject a del+ins REPLACE pair in one call (both halves of one
91702
+ logical change). \`list\` tags the two tcNs with a shared
91703
+ "group": "revN"; addressing the revN saves the reject-relist-
91704
+ reject ping-pong (tcN ids renumber after each single reject).
90973
91705
  --all Reject every tracked change.
90974
91706
 
90975
91707
  Options:
@@ -91520,7 +92252,7 @@ Examples:
91520
92252
  // package.json
91521
92253
  var package_default = {
91522
92254
  name: "bun-docx",
91523
- version: "0.13.0",
92255
+ version: "0.14.1",
91524
92256
  description: "CLI for AI agents (Claude, Codex) to read, edit, and comment on .docx files with full format fidelity.",
91525
92257
  keywords: [
91526
92258
  "docx",
@@ -91611,13 +92343,13 @@ Commands (each one-liner names capabilities you'd otherwise miss; see <command>
91611
92343
  create FILE Create a new .docx (--from PATH.md | --from - builds from Markdown; --force to overwrite)
91612
92344
  read FILE Render as Markdown with pN locators; --from/--to to slice, --accepted (default)/--current/--baseline tracked views, --comments, --ast for JSON-AST
91613
92345
  edit FILE Replace or strip text/formatting at pN, pN:S-E, pN-pM, sN, eqN, or table-cell locators (--clear to strip formatting, --track to redline, --batch for many edits in one read)
91614
- insert FILE Insert a paragraph, image, table, equation, code block, markdown, or structural break (--after/--before LOCATOR; --track; --batch for many inserts in one read)
92346
+ insert FILE Insert a paragraph, image, table, equation, code block, markdown, or page break (--after/--before LOCATOR; --track; --batch for many inserts in one read). For COLUMN layout use "docx sections", not insert.
91615
92347
  delete FILE Remove a paragraph, range, table, or section break (--at LOCATOR; --track for tracked deletion; --batch to remove many in one read)
91616
92348
  find FILE [QUERY] Find spans by text, OR by formatting (--highlight/--color/--bold/--italic/--underline); returns locators for --at
91617
92349
  replace FILE PATTERN REPL Substitute text spans, sed-style (--regex, --track to redline, --dry-run to preview, --batch for a multi-pattern script)
91618
92350
  wc FILE [LOCATOR] Count words in the doc or a slice (--accepted/--baseline/--current tracked view, --json)
91619
92351
  outline FILE List headings as a locator tree (pN feeds --at / read --from; --style-prefix, --json)
91620
- columns FILE Lay a paragraph range out in N columns (--at pN-pM --count N) or recount a section (--at sN)
92352
+ sections FILE Multi-column layout & section breaks \u2014 put a paragraph range in N columns (--at pN-pM --columns N) or recount a section (--at sN). The ONLY way to do columns; insert does not.
91621
92353
  styles FILE List the styles you can apply (--used for ones in use; --at ID to describe one) \u2014 the catalog isn't in the body
91622
92354
  render FILE Visual page verification \u2014 render each page as PNG/JPG via Word or LibreOffice
91623
92355
  comments \u2026 Add (--at LOCATOR | --anchor PHRASE | --batch), reply, resolve (--unset to reopen), delete, list (--thread cN)
@@ -91645,7 +92377,7 @@ render (each render spins up Word and is slow). Render only for what Markdown ca
91645
92377
  multi-column sections, page/section breaks, image sizing/placement, table geometry \u2014 and
91646
92378
  then ONCE at the end (not after every edit), or one final time if you're genuinely unsure
91647
92379
  it looks right: "docx render FILE --out pages/" writes page-001.png, \u2026 which you can read.
91648
- (A multi-column section's columns apply to content BEFORE the break \u2014 see "docx insert --help".)
92380
+ (To put text in columns, name the range: "docx sections --at pN-pM --columns N" \u2014 it inserts the bounding breaks so the columns land on exactly that range. A raw section break's columns apply to the content BEFORE it, which is why insert no longer takes --section.)
91649
92381
 
91650
92382
  Environment:
91651
92383
  DOCX_AUTHOR Default author for comments and tracked-change attribution
@@ -91666,7 +92398,6 @@ async function printTopHelp() {
91666
92398
  // src/cli/index.ts
91667
92399
  init_respond();
91668
92400
  var COMMANDS = {
91669
- columns: () => Promise.resolve().then(() => (init_columns(), exports_columns)),
91670
92401
  comments: () => Promise.resolve().then(() => (init_comments3(), exports_comments)),
91671
92402
  create: () => Promise.resolve().then(() => (init_create2(), exports_create)),
91672
92403
  delete: () => Promise.resolve().then(() => (init_delete2(), exports_delete2)),
@@ -91682,6 +92413,7 @@ var COMMANDS = {
91682
92413
  read: () => Promise.resolve().then(() => (init_read3(), exports_read)),
91683
92414
  render: () => Promise.resolve().then(() => (init_render2(), exports_render)),
91684
92415
  replace: () => Promise.resolve().then(() => (init_replace4(), exports_replace3)),
92416
+ sections: () => Promise.resolve().then(() => (init_sections2(), exports_sections)),
91685
92417
  styles: () => Promise.resolve().then(() => (init_styles2(), exports_styles)),
91686
92418
  tables: () => Promise.resolve().then(() => (init_tables(), exports_tables)),
91687
92419
  "track-changes": () => Promise.resolve().then(() => (init_track_changes2(), exports_track_changes)),
@@ -91711,6 +92443,9 @@ Run "docx --help" for available commands.
91711
92443
  }
91712
92444
  return 0;
91713
92445
  }
92446
+ if (cmd === "columns") {
92447
+ return fail("USAGE", "`docx columns` was renamed to `docx sections`", "Run `docx sections --at pN-pM --columns N` (or `docx sections --help`).");
92448
+ }
91714
92449
  const loader = COMMANDS[cmd];
91715
92450
  if (!loader) {
91716
92451
  return fail("USAGE", `Unknown command: ${cmd}`, 'Run "docx --help" for the list of commands.');