bun-docx 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +44 -5
  2. package/dist/index.js +1834 -381
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13762,7 +13762,7 @@ var init_package = __esm(() => {
13762
13762
 
13763
13763
  // src/core/ast/read.ts
13764
13764
  function buildDoc(view, path) {
13765
- readImageRelationships(view);
13765
+ readRelationships(view);
13766
13766
  const properties = readDocProperties(view);
13767
13767
  const documentRoot = XmlNode2.findRoot(view.documentTree, "w:document");
13768
13768
  if (!documentRoot) {
@@ -13774,12 +13774,23 @@ function buildDoc(view, path) {
13774
13774
  }
13775
13775
  const state = {
13776
13776
  imageIndex: 0,
13777
+ hyperlinkIndex: 0,
13777
13778
  commentAnchors: new Map,
13778
13779
  openComments: new Map
13779
13780
  };
13780
13781
  const blocks = readBlocks(view, body, state);
13781
13782
  const comments = readComments(view, state.commentAnchors);
13782
- return { schemaVersion: 1, path, properties, blocks, comments };
13783
+ const footnotes = readNotes(view.footnotesTree, "w:footnotes", "w:footnote", "fn");
13784
+ const endnotes = readNotes(view.endnotesTree, "w:endnotes", "w:endnote", "en");
13785
+ return {
13786
+ schemaVersion: 1,
13787
+ path,
13788
+ properties,
13789
+ blocks,
13790
+ comments,
13791
+ footnotes,
13792
+ endnotes
13793
+ };
13783
13794
  }
13784
13795
  function readBlocks(view, body, state) {
13785
13796
  const blocks = [];
@@ -13813,17 +13824,30 @@ function readParagraph(view, node, id, state) {
13813
13824
  if (paragraphProperties) {
13814
13825
  applyParagraphProperties(paragraph, paragraphProperties);
13815
13826
  }
13816
- const activeComments = new Set;
13817
- let offset = 0;
13818
- for (const child of node.children) {
13827
+ const context = {
13828
+ view,
13829
+ blockId: id,
13830
+ paragraph,
13831
+ activeComments: new Set,
13832
+ state,
13833
+ offsetRef: { value: 0 }
13834
+ };
13835
+ walkRunContainer(context, node, undefined, undefined);
13836
+ return paragraph;
13837
+ }
13838
+ function walkRunContainer(context, container, trackedChange, hyperlink) {
13839
+ for (const child of container.children) {
13819
13840
  if (child.tag === "w:pPr")
13820
13841
  continue;
13821
13842
  if (child.tag === "w:commentRangeStart") {
13822
13843
  const commentId = child.getAttribute("w:id");
13823
13844
  if (commentId) {
13824
13845
  const key = `c${commentId}`;
13825
- activeComments.add(key);
13826
- state.openComments.set(key, { blockId: id, offset });
13846
+ context.activeComments.add(key);
13847
+ context.state.openComments.set(key, {
13848
+ blockId: context.blockId,
13849
+ offset: context.offsetRef.value
13850
+ });
13827
13851
  }
13828
13852
  continue;
13829
13853
  }
@@ -13831,26 +13855,48 @@ function readParagraph(view, node, id, state) {
13831
13855
  const commentId = child.getAttribute("w:id");
13832
13856
  if (commentId) {
13833
13857
  const key = `c${commentId}`;
13834
- activeComments.delete(key);
13835
- const opened = state.openComments.get(key);
13858
+ context.activeComments.delete(key);
13859
+ const opened = context.state.openComments.get(key);
13836
13860
  if (opened) {
13837
- state.commentAnchors.set(key, {
13861
+ context.state.commentAnchors.set(key, {
13838
13862
  startBlockId: opened.blockId,
13839
13863
  startOffset: opened.offset,
13840
- endBlockId: id,
13841
- endOffset: offset
13864
+ endBlockId: context.blockId,
13865
+ endOffset: context.offsetRef.value
13842
13866
  });
13843
- state.openComments.delete(key);
13867
+ context.state.openComments.delete(key);
13844
13868
  }
13845
13869
  }
13846
13870
  continue;
13847
13871
  }
13848
13872
  if (child.tag === "w:r") {
13849
- const run = readRun(view, child, activeComments, undefined, state);
13873
+ const run = readRun(context.view, child, context.activeComments, trackedChange, hyperlink, context.state);
13850
13874
  if (run) {
13851
13875
  if (run.type === "text")
13852
- offset += run.text.length;
13853
- paragraph.runs.push(run);
13876
+ context.offsetRef.value += run.text.length;
13877
+ context.paragraph.runs.push(run);
13878
+ }
13879
+ continue;
13880
+ }
13881
+ if (child.tag === "m:oMath") {
13882
+ const text = collectMathText(child);
13883
+ if (text.length > 0) {
13884
+ context.paragraph.runs.push({
13885
+ type: "equation",
13886
+ text,
13887
+ display: false
13888
+ });
13889
+ }
13890
+ continue;
13891
+ }
13892
+ if (child.tag === "m:oMathPara") {
13893
+ const text = collectMathText(child);
13894
+ if (text.length > 0) {
13895
+ context.paragraph.runs.push({
13896
+ type: "equation",
13897
+ text,
13898
+ display: true
13899
+ });
13854
13900
  }
13855
13901
  continue;
13856
13902
  }
@@ -13861,46 +13907,40 @@ function readParagraph(view, node, id, state) {
13861
13907
  date: child.getAttribute("w:date") ?? "",
13862
13908
  revisionId: child.getAttribute("w:id") ?? ""
13863
13909
  };
13864
- for (const inner of child.children) {
13865
- if (inner.tag === "w:commentRangeStart") {
13866
- const commentId = inner.getAttribute("w:id");
13867
- if (commentId) {
13868
- const key = `c${commentId}`;
13869
- activeComments.add(key);
13870
- state.openComments.set(key, { blockId: id, offset });
13871
- }
13872
- continue;
13873
- }
13874
- if (inner.tag === "w:commentRangeEnd") {
13875
- const commentId = inner.getAttribute("w:id");
13876
- if (commentId) {
13877
- const key = `c${commentId}`;
13878
- activeComments.delete(key);
13879
- const opened = state.openComments.get(key);
13880
- if (opened) {
13881
- state.commentAnchors.set(key, {
13882
- startBlockId: opened.blockId,
13883
- startOffset: opened.offset,
13884
- endBlockId: id,
13885
- endOffset: offset
13886
- });
13887
- state.openComments.delete(key);
13888
- }
13889
- }
13890
- continue;
13891
- }
13892
- if (inner.tag !== "w:r")
13893
- continue;
13894
- const run = readRun(view, inner, activeComments, change, state);
13895
- if (!run)
13896
- continue;
13897
- if (run.type === "text")
13898
- offset += run.text.length;
13899
- paragraph.runs.push(run);
13900
- }
13910
+ walkRunContainer(context, child, change, hyperlink);
13911
+ continue;
13912
+ }
13913
+ if (child.tag === "w:hyperlink") {
13914
+ const link = readHyperlinkProperties(context.view, child, container.children, context.state);
13915
+ walkRunContainer(context, child, trackedChange, link);
13901
13916
  }
13902
13917
  }
13903
- return paragraph;
13918
+ }
13919
+ function readHyperlinkProperties(view, node, parent, state) {
13920
+ const id = `link${state.hyperlinkIndex++}`;
13921
+ const link = { id };
13922
+ const relationshipId = node.getAttribute("r:id");
13923
+ if (relationshipId) {
13924
+ const relationship = view.hyperlinksByRelationshipId.get(relationshipId);
13925
+ if (relationship?.url)
13926
+ link.url = relationship.url;
13927
+ }
13928
+ const anchor = node.getAttribute("w:anchor");
13929
+ if (anchor)
13930
+ link.anchor = anchor;
13931
+ const tooltip = node.getAttribute("w:tooltip");
13932
+ if (tooltip)
13933
+ link.tooltip = tooltip;
13934
+ if (!link.url && !link.anchor && !link.tooltip) {
13935
+ state.hyperlinkIndex--;
13936
+ return;
13937
+ }
13938
+ view.hyperlinkById.set(id, {
13939
+ node,
13940
+ parent,
13941
+ ...relationshipId ? { relationshipId } : {}
13942
+ });
13943
+ return link;
13904
13944
  }
13905
13945
  function applyParagraphProperties(paragraph, paragraphProperties) {
13906
13946
  const styleNode = paragraphProperties.findChild("w:pStyle");
@@ -13925,13 +13965,13 @@ function applyParagraphProperties(paragraph, paragraphProperties) {
13925
13965
  paragraph.list = { level, numId: id };
13926
13966
  }
13927
13967
  }
13928
- function readRun(view, node, activeComments, trackedChange, state) {
13968
+ function readRun(view, node, activeComments, trackedChange, hyperlink, state) {
13929
13969
  const runProperties = node.findChild("w:rPr");
13930
13970
  for (const child of node.children) {
13931
13971
  if (child.tag === "w:drawing") {
13932
- const image = readImageFromDrawing(view, child, state);
13933
- if (image)
13934
- return image;
13972
+ const drawing = readDrawing(view, child, state);
13973
+ if (drawing)
13974
+ return drawing;
13935
13975
  }
13936
13976
  if (child.tag === "w:br") {
13937
13977
  const kind = child.getAttribute("w:type") ?? "line";
@@ -13940,6 +13980,16 @@ function readRun(view, node, activeComments, trackedChange, state) {
13940
13980
  if (child.tag === "w:tab") {
13941
13981
  return { type: "tab" };
13942
13982
  }
13983
+ if (child.tag === "w:footnoteReference") {
13984
+ const id = child.getAttribute("w:id");
13985
+ if (id)
13986
+ return { type: "footnoteRef", kind: "footnote", id: `fn${id}` };
13987
+ }
13988
+ if (child.tag === "w:endnoteReference") {
13989
+ const id = child.getAttribute("w:id");
13990
+ if (id)
13991
+ return { type: "footnoteRef", kind: "endnote", id: `en${id}` };
13992
+ }
13943
13993
  }
13944
13994
  let combinedText = "";
13945
13995
  for (const child of node.children) {
@@ -13956,8 +14006,33 @@ function readRun(view, node, activeComments, trackedChange, state) {
13956
14006
  run.comments = [...activeComments];
13957
14007
  if (trackedChange)
13958
14008
  run.trackedChange = trackedChange;
14009
+ if (hyperlink)
14010
+ run.hyperlink = hyperlink;
13959
14011
  return run;
13960
14012
  }
14013
+ function readDrawing(view, drawing, state) {
14014
+ const image = readImageFromDrawing(view, drawing, state);
14015
+ if (image)
14016
+ return image;
14017
+ if (drawing.findDescendant("c:chart"))
14018
+ return { type: "chart", kind: "chart" };
14019
+ if (drawing.findDescendant("dgm:relIds"))
14020
+ return { type: "chart", kind: "smartart" };
14021
+ if (drawing.findDescendant("wps:wsp"))
14022
+ return { type: "chart", kind: "shape" };
14023
+ return { type: "chart", kind: "drawing" };
14024
+ }
14025
+ function collectMathText(node) {
14026
+ let out = "";
14027
+ for (const child of node.children) {
14028
+ if (child.tag === "m:t") {
14029
+ out += child.collectText();
14030
+ continue;
14031
+ }
14032
+ out += collectMathText(child);
14033
+ }
14034
+ return out;
14035
+ }
13961
14036
  function applyRunProperties(run, runProperties) {
13962
14037
  const colorNode = runProperties.findChild("w:color");
13963
14038
  if (colorNode) {
@@ -14060,22 +14135,30 @@ function readCellBlocks(view, cell, tableId, rowIndex, columnIndex, state) {
14060
14135
  }
14061
14136
  return blocks;
14062
14137
  }
14063
- function readImageRelationships(view) {
14138
+ function readRelationships(view) {
14064
14139
  const relationships = XmlNode2.findRoot(view.relationshipsTree, "Relationships");
14065
14140
  if (!relationships)
14066
14141
  return;
14067
14142
  for (const child of relationships.children) {
14068
14143
  if (child.tag !== "Relationship")
14069
14144
  continue;
14070
- if (child.getAttribute("Type") !== RELATIONSHIP_NAMESPACE_IMAGE)
14071
- continue;
14145
+ const type = child.getAttribute("Type");
14072
14146
  const relationshipId = child.getAttribute("Id");
14073
14147
  const target = child.getAttribute("Target");
14074
14148
  if (!relationshipId || !target)
14075
14149
  continue;
14076
- const partName = target.startsWith("/") ? target.slice(1) : `word/${target}`;
14077
- const contentType = lookupContentType(view, partName);
14078
- view.imagesByRelationshipId.set(relationshipId, { partName, contentType });
14150
+ if (type === RELATIONSHIP_NAMESPACE_IMAGE) {
14151
+ const partName = target.startsWith("/") ? target.slice(1) : `word/${target}`;
14152
+ const contentType = lookupContentType(view, partName);
14153
+ view.imagesByRelationshipId.set(relationshipId, {
14154
+ partName,
14155
+ contentType
14156
+ });
14157
+ continue;
14158
+ }
14159
+ if (type === RELATIONSHIP_NAMESPACE_HYPERLINK) {
14160
+ view.hyperlinksByRelationshipId.set(relationshipId, { url: target });
14161
+ }
14079
14162
  }
14080
14163
  }
14081
14164
  function lookupContentType(view, partName) {
@@ -14204,7 +14287,27 @@ function readCommentsExtended(view) {
14204
14287
  }
14205
14288
  return out;
14206
14289
  }
14207
- var RELATIONSHIP_NAMESPACE_IMAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
14290
+ function readNotes(tree, rootTag, itemTag, idPrefix) {
14291
+ if (!tree)
14292
+ return [];
14293
+ const root = XmlNode2.findRoot(tree, rootTag);
14294
+ if (!root)
14295
+ return [];
14296
+ const out = [];
14297
+ for (const child of root.children) {
14298
+ if (child.tag !== itemTag)
14299
+ continue;
14300
+ if (child.getAttribute("w:type"))
14301
+ continue;
14302
+ const numericId = child.getAttribute("w:id");
14303
+ if (numericId == null)
14304
+ continue;
14305
+ const text = child.collectText().replace(/\s+/g, " ").trim();
14306
+ out.push({ id: `${idPrefix}${numericId}`, text });
14307
+ }
14308
+ return out;
14309
+ }
14310
+ var RELATIONSHIP_NAMESPACE_IMAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", RELATIONSHIP_NAMESPACE_HYPERLINK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
14208
14311
  var init_read = __esm(() => {
14209
14312
  init_parser();
14210
14313
  });
@@ -14219,11 +14322,15 @@ async function openDocView(path) {
14219
14322
  const commentsExtTree = pkg.hasPart("word/commentsExtended.xml") ? XmlNode2.parse(await pkg.readText("word/commentsExtended.xml")) : undefined;
14220
14323
  const corePropertiesTree = pkg.hasPart("docProps/core.xml") ? XmlNode2.parse(await pkg.readText("docProps/core.xml")) : undefined;
14221
14324
  const settingsTree = pkg.hasPart("word/settings.xml") ? XmlNode2.parse(await pkg.readText("word/settings.xml")) : undefined;
14325
+ const footnotesTree = pkg.hasPart("word/footnotes.xml") ? XmlNode2.parse(await pkg.readText("word/footnotes.xml")) : undefined;
14326
+ const endnotesTree = pkg.hasPart("word/endnotes.xml") ? XmlNode2.parse(await pkg.readText("word/endnotes.xml")) : undefined;
14222
14327
  const view = {
14223
14328
  pkg,
14224
14329
  documentTree,
14225
14330
  commentsTree,
14226
14331
  commentsExtTree,
14332
+ footnotesTree,
14333
+ endnotesTree,
14227
14334
  relationshipsTree,
14228
14335
  contentTypesTree,
14229
14336
  corePropertiesTree,
@@ -14232,7 +14339,9 @@ async function openDocView(path) {
14232
14339
  blockReferences: new Map,
14233
14340
  commentReferences: new Map,
14234
14341
  imagesByRelationshipId: new Map,
14235
- imageById: new Map
14342
+ imageById: new Map,
14343
+ hyperlinksByRelationshipId: new Map,
14344
+ hyperlinkById: new Map
14236
14345
  };
14237
14346
  view.doc = buildDoc(view, pkg.path);
14238
14347
  return view;
@@ -14302,6 +14411,49 @@ var init_doc_view = __esm(() => {
14302
14411
  init_read();
14303
14412
  });
14304
14413
 
14414
+ // src/core/ast/text.ts
14415
+ function paragraphText(paragraph) {
14416
+ let out = "";
14417
+ for (const run of paragraph.runs) {
14418
+ if (run.type === "text")
14419
+ out += run.text;
14420
+ }
14421
+ return out;
14422
+ }
14423
+ function flattenParagraphs(blocks) {
14424
+ const out = [];
14425
+ for (const block of blocks) {
14426
+ if (block.type === "paragraph") {
14427
+ out.push(block);
14428
+ continue;
14429
+ }
14430
+ if (block.type === "table") {
14431
+ for (const row of block.rows) {
14432
+ for (const cell of row.cells) {
14433
+ out.push(...flattenParagraphs(cell.blocks));
14434
+ }
14435
+ }
14436
+ }
14437
+ }
14438
+ return out;
14439
+ }
14440
+ function findBlockById(blocks, blockId) {
14441
+ for (const block of blocks) {
14442
+ if (block.id === blockId)
14443
+ return block;
14444
+ if (block.type === "table") {
14445
+ for (const row of block.rows) {
14446
+ for (const cell of row.cells) {
14447
+ const inner = findBlockById(cell.blocks, blockId);
14448
+ if (inner)
14449
+ return inner;
14450
+ }
14451
+ }
14452
+ }
14453
+ }
14454
+ return null;
14455
+ }
14456
+
14305
14457
  // src/core/ast/index.ts
14306
14458
  var init_ast = __esm(() => {
14307
14459
  init_doc_view();
@@ -14319,6 +14471,10 @@ function parseLocator(input) {
14319
14471
  const imageMatch = trimmed.match(IMAGE_RE);
14320
14472
  if (imageMatch)
14321
14473
  return { kind: "image", imageId: `img${imageMatch[1]}` };
14474
+ const linkMatch = trimmed.match(LINK_RE);
14475
+ if (linkMatch) {
14476
+ return { kind: "hyperlink", hyperlinkId: `link${linkMatch[1]}` };
14477
+ }
14322
14478
  const cellMatch = trimmed.match(CELL_RE);
14323
14479
  if (cellMatch) {
14324
14480
  const [, tableIndex, rowIndex, columnIndex, rest] = cellMatch;
@@ -14367,7 +14523,7 @@ function validateOffsets(input, start, end, crossBlock) {
14367
14523
  throw new LocatorParseError(input, "end offset precedes start");
14368
14524
  }
14369
14525
  }
14370
- var LocatorParseError, BLOCK_RE, SPAN_RE, RANGE_RE, COMMENT_RE, IMAGE_RE, CELL_RE;
14526
+ var LocatorParseError, BLOCK_RE, SPAN_RE, RANGE_RE, COMMENT_RE, IMAGE_RE, LINK_RE, CELL_RE;
14371
14527
  var init_parse = __esm(() => {
14372
14528
  LocatorParseError = class LocatorParseError extends Error {
14373
14529
  input;
@@ -14382,6 +14538,7 @@ var init_parse = __esm(() => {
14382
14538
  RANGE_RE = /^p(\d+):(\d+)-p(\d+):(\d+)$/;
14383
14539
  COMMENT_RE = /^c(\d+)$/;
14384
14540
  IMAGE_RE = /^img(\d+)$/;
14541
+ LINK_RE = /^link(\d+)$/;
14385
14542
  CELL_RE = /^t(\d+):r(\d+)c(\d+)(?::(.+))?$/;
14386
14543
  });
14387
14544
 
@@ -14431,6 +14588,39 @@ var init_locators = __esm(() => {
14431
14588
  init_resolve();
14432
14589
  });
14433
14590
 
14591
+ // src/core/relationships.ts
14592
+ function mintRelationshipId(relationshipsRoot) {
14593
+ let highest = 0;
14594
+ for (const child of relationshipsRoot.children) {
14595
+ if (child.tag !== "Relationship")
14596
+ continue;
14597
+ const id = child.getAttribute("Id");
14598
+ if (!id)
14599
+ continue;
14600
+ const match = id.match(/^rId(\d+)$/);
14601
+ if (match?.[1]) {
14602
+ const number = Number(match[1]);
14603
+ if (number > highest)
14604
+ highest = number;
14605
+ }
14606
+ }
14607
+ return `rId${highest + 1}`;
14608
+ }
14609
+ function addHyperlinkRelationship(relationshipsRoot, url) {
14610
+ const id = mintRelationshipId(relationshipsRoot);
14611
+ relationshipsRoot.children.push(new XmlNode2("Relationship", {
14612
+ Id: id,
14613
+ Type: HYPERLINK_RELATIONSHIP_TYPE,
14614
+ Target: url,
14615
+ TargetMode: "External"
14616
+ }));
14617
+ return id;
14618
+ }
14619
+ var HYPERLINK_RELATIONSHIP_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
14620
+ var init_relationships = __esm(() => {
14621
+ init_parser();
14622
+ });
14623
+
14434
14624
  // src/core/track-changes/index.ts
14435
14625
  function isTrackChangesEnabled(view) {
14436
14626
  if (!view.settingsTree)
@@ -14607,7 +14797,8 @@ var init_jsx = __esm(() => {
14607
14797
  "comments",
14608
14798
  "comment",
14609
14799
  "settings",
14610
- "trackChanges"
14800
+ "trackChanges",
14801
+ "hyperlink"
14611
14802
  ];
14612
14803
  R_TAGS = ["embed", "link", "id"];
14613
14804
  A_TAGS = [
@@ -14722,6 +14913,7 @@ var init_core = __esm(() => {
14722
14913
  init_locators();
14723
14914
  init_package();
14724
14915
  init_parser();
14916
+ init_relationships();
14725
14917
  init_track_changes();
14726
14918
  init_emit();
14727
14919
  });
@@ -14755,6 +14947,7 @@ function exitCodeFor(code) {
14755
14947
  case "BLOCK_NOT_FOUND":
14756
14948
  case "COMMENT_NOT_FOUND":
14757
14949
  case "IMAGE_NOT_FOUND":
14950
+ case "HYPERLINK_NOT_FOUND":
14758
14951
  case "MATCH_NOT_FOUND":
14759
14952
  return EXIT.NOT_FOUND;
14760
14953
  case "NOT_A_ZIP":
@@ -16124,7 +16317,7 @@ function RunElement({ run: run9 }) {
16124
16317
  children: /* @__PURE__ */ jsxDEV(w.tab, {}, undefined, false, undefined, this)
16125
16318
  }, undefined, false, undefined, this);
16126
16319
  }
16127
- throw new Error(`Cannot emit run of type "${run9.type}" \u2014 image emission lives in the images command.`);
16320
+ return null;
16128
16321
  }
16129
16322
  function TextRunElement({ run: run9 }) {
16130
16323
  return /* @__PURE__ */ jsxDEV(w.r, {
@@ -16426,15 +16619,15 @@ function literalMatcher(query, ignoreCase) {
16426
16619
  throw new Error("query cannot be empty");
16427
16620
  }
16428
16621
  const needle = ignoreCase ? query.toLowerCase() : query;
16429
- return (paragraphText) => {
16430
- const haystack = ignoreCase ? paragraphText.toLowerCase() : paragraphText;
16622
+ return (paragraphText2) => {
16623
+ const haystack = ignoreCase ? paragraphText2.toLowerCase() : paragraphText2;
16431
16624
  const matches = [];
16432
16625
  let cursor = haystack.indexOf(needle);
16433
16626
  while (cursor !== -1) {
16434
16627
  matches.push({
16435
16628
  start: cursor,
16436
16629
  end: cursor + needle.length,
16437
- text: paragraphText.slice(cursor, cursor + needle.length)
16630
+ text: paragraphText2.slice(cursor, cursor + needle.length)
16438
16631
  });
16439
16632
  cursor = haystack.indexOf(needle, cursor + needle.length);
16440
16633
  }
@@ -16444,15 +16637,15 @@ function literalMatcher(query, ignoreCase) {
16444
16637
  function regexMatcher(pattern, ignoreCase) {
16445
16638
  const flags = `g${ignoreCase ? "i" : ""}`;
16446
16639
  const regex = new RegExp(pattern, flags);
16447
- return (paragraphText) => {
16640
+ return (paragraphText2) => {
16448
16641
  const matches = [];
16449
16642
  regex.lastIndex = 0;
16450
- let result = regex.exec(paragraphText);
16643
+ let result = regex.exec(paragraphText2);
16451
16644
  while (result !== null) {
16452
16645
  const matched = result[0];
16453
16646
  if (matched.length === 0) {
16454
16647
  regex.lastIndex += 1;
16455
- result = regex.exec(paragraphText);
16648
+ result = regex.exec(paragraphText2);
16456
16649
  continue;
16457
16650
  }
16458
16651
  matches.push({
@@ -16460,7 +16653,7 @@ function regexMatcher(pattern, ignoreCase) {
16460
16653
  end: result.index + matched.length,
16461
16654
  text: matched
16462
16655
  });
16463
- result = regex.exec(paragraphText);
16656
+ result = regex.exec(paragraphText2);
16464
16657
  }
16465
16658
  return matches;
16466
16659
  };
@@ -16468,8 +16661,8 @@ function regexMatcher(pattern, ignoreCase) {
16468
16661
  function collectMatches(blocks, matcher, out) {
16469
16662
  for (const block of blocks) {
16470
16663
  if (block.type === "paragraph") {
16471
- const paragraphText = block.runs.map((run10) => run10.type === "text" ? run10.text : "").join("");
16472
- for (const span of matcher(paragraphText)) {
16664
+ const paragraphText2 = block.runs.map((run10) => run10.type === "text" ? run10.text : "").join("");
16665
+ for (const span of matcher(paragraphText2)) {
16473
16666
  const match = {
16474
16667
  blockId: block.id,
16475
16668
  start: span.start,
@@ -16629,12 +16822,107 @@ var init_find = __esm(() => {
16629
16822
  init_respond();
16630
16823
  });
16631
16824
 
16632
- // src/cli/images/extract.ts
16633
- var exports_extract = {};
16634
- __export(exports_extract, {
16825
+ // src/cli/hyperlinks/wrap.tsx
16826
+ function wrapSpanInHyperlink(paragraph, span, relationshipId) {
16827
+ if (span.start >= span.end) {
16828
+ throw new HyperlinkWrapError(`Empty or inverted span ${span.start}-${span.end}`);
16829
+ }
16830
+ const newChildren = [];
16831
+ const wrappedRuns = [];
16832
+ let offset = 0;
16833
+ let placed = false;
16834
+ const placeWrapper = () => {
16835
+ if (placed || wrappedRuns.length === 0)
16836
+ return;
16837
+ newChildren.push(hyperlinkWrapper(relationshipId, wrappedRuns));
16838
+ wrappedRuns.length = 0;
16839
+ placed = true;
16840
+ };
16841
+ for (const child of paragraph.children) {
16842
+ if (child.tag === "w:r") {
16843
+ const length = runTextLength(child);
16844
+ const runStart = offset;
16845
+ const runEnd = offset + length;
16846
+ offset = runEnd;
16847
+ if (runEnd <= span.start || runStart >= span.end) {
16848
+ placeWrapper();
16849
+ newChildren.push(child);
16850
+ continue;
16851
+ }
16852
+ const sliceStartInRun = Math.max(0, span.start - runStart);
16853
+ const sliceEndInRun = Math.min(length, span.end - runStart);
16854
+ if (sliceStartInRun > 0) {
16855
+ newChildren.push(sliceRun(child, 0, sliceStartInRun));
16856
+ }
16857
+ wrappedRuns.push(sliceRun(child, sliceStartInRun, sliceEndInRun));
16858
+ if (runEnd >= span.end)
16859
+ placeWrapper();
16860
+ if (sliceEndInRun < length) {
16861
+ newChildren.push(sliceRun(child, sliceEndInRun, length));
16862
+ }
16863
+ continue;
16864
+ }
16865
+ if (child.tag === "w:hyperlink") {
16866
+ const hyperlinkLength = sumInnerRunLengths(child);
16867
+ const hyperlinkStart = offset;
16868
+ const hyperlinkEnd = offset + hyperlinkLength;
16869
+ offset = hyperlinkEnd;
16870
+ if (hyperlinkEnd <= span.start || hyperlinkStart >= span.end) {
16871
+ placeWrapper();
16872
+ newChildren.push(child);
16873
+ continue;
16874
+ }
16875
+ throw new HyperlinkWrapError(`Span ${span.start}-${span.end} overlaps an existing hyperlink at ${hyperlinkStart}-${hyperlinkEnd}; nested hyperlinks are not allowed`);
16876
+ }
16877
+ if (child.tag === "w:ins" || child.tag === "w:del") {
16878
+ const innerLength = sumInnerRunLengths(child);
16879
+ const wrapperStart = offset;
16880
+ const wrapperEnd = offset + innerLength;
16881
+ offset = wrapperEnd;
16882
+ if (wrapperEnd <= span.start || wrapperStart >= span.end) {
16883
+ placeWrapper();
16884
+ newChildren.push(child);
16885
+ continue;
16886
+ }
16887
+ throw new HyperlinkWrapError(`Span ${span.start}-${span.end} crosses a tracked-change wrapper at ${wrapperStart}-${wrapperEnd}; resolve or accept the change first`);
16888
+ }
16889
+ newChildren.push(child);
16890
+ }
16891
+ placeWrapper();
16892
+ paragraph.children = newChildren;
16893
+ }
16894
+ function hyperlinkWrapper(relationshipId, runs) {
16895
+ return /* @__PURE__ */ jsxDEV(w.hyperlink, {
16896
+ "r:id": relationshipId,
16897
+ children: runs
16898
+ }, undefined, false, undefined, this);
16899
+ }
16900
+ function sumInnerRunLengths(wrapper) {
16901
+ let total = 0;
16902
+ for (const inner of wrapper.children) {
16903
+ if (inner.tag === "w:r")
16904
+ total += runTextLength(inner);
16905
+ }
16906
+ return total;
16907
+ }
16908
+ var HyperlinkWrapError;
16909
+ var init_wrap = __esm(() => {
16910
+ init_jsx();
16911
+ init_parser();
16912
+ init_jsx_dev_runtime();
16913
+ HyperlinkWrapError = class HyperlinkWrapError extends Error {
16914
+ constructor(message) {
16915
+ super(message);
16916
+ this.name = "HyperlinkWrapError";
16917
+ }
16918
+ };
16919
+ });
16920
+
16921
+ // src/cli/hyperlinks/add.tsx
16922
+ var exports_add2 = {};
16923
+ __export(exports_add2, {
16635
16924
  run: () => run11
16636
16925
  });
16637
- import { join } from "path";
16638
16926
  import { parseArgs as parseArgs10 } from "util";
16639
16927
  async function run11(args) {
16640
16928
  let parsed;
@@ -16643,8 +16931,10 @@ async function run11(args) {
16643
16931
  args,
16644
16932
  allowPositionals: true,
16645
16933
  options: {
16646
- to: { type: "string" },
16647
- id: { type: "string" },
16934
+ at: { type: "string" },
16935
+ url: { type: "string" },
16936
+ output: { type: "string", short: "o" },
16937
+ "dry-run": { type: "boolean" },
16648
16938
  help: { type: "boolean", short: "h" }
16649
16939
  }
16650
16940
  });
@@ -16659,49 +16949,569 @@ async function run11(args) {
16659
16949
  const path = parsed.positionals[0];
16660
16950
  if (!path)
16661
16951
  return fail("USAGE", "Missing FILE argument", HELP11);
16662
- const outputDir = parsed.values.to;
16663
- if (!outputDir)
16664
- return fail("USAGE", "Missing --to DIR", HELP11);
16665
- const targetId = parsed.values.id;
16952
+ const atInput = parsed.values.at;
16953
+ const url = parsed.values.url;
16954
+ if (!atInput)
16955
+ return fail("USAGE", "Missing --at LOCATOR", HELP11);
16956
+ if (!url)
16957
+ return fail("USAGE", "Missing --url URL", HELP11);
16958
+ let locator;
16959
+ try {
16960
+ locator = parseLocator(atInput);
16961
+ } catch (error) {
16962
+ if (error instanceof LocatorParseError) {
16963
+ return fail("INVALID_LOCATOR", error.message);
16964
+ }
16965
+ throw error;
16966
+ }
16967
+ const target = locatorToBlockTarget(locator);
16968
+ if (!target?.span) {
16969
+ return fail("INVALID_LOCATOR", "hyperlinks add requires a span locator like pN:S-E or tT:rRcC:pK:S-E");
16970
+ }
16666
16971
  const view = await openOrFail(path);
16667
16972
  if (typeof view === "number")
16668
16973
  return view;
16669
- await enrichImageHashes(view);
16670
- const allImages = [];
16671
- collectImages(view.doc.blocks, allImages);
16672
- const targets = targetId ? allImages.filter((image) => image.id === targetId) : allImages;
16673
- if (targetId && targets.length === 0) {
16674
- return fail("IMAGE_NOT_FOUND", `Image not found: ${targetId}`);
16974
+ const paragraphRef = await resolveBlockOrFail(view, target.blockId);
16975
+ if (typeof paragraphRef === "number")
16976
+ return paragraphRef;
16977
+ const outputPath = parsed.values.output;
16978
+ if (parsed.values["dry-run"]) {
16979
+ await respond({
16980
+ ok: true,
16981
+ operation: "hyperlinks.add",
16982
+ dryRun: true,
16983
+ path,
16984
+ at: atInput,
16985
+ url,
16986
+ ...outputPath ? { output: outputPath } : {}
16987
+ });
16988
+ return EXIT.OK;
16675
16989
  }
16676
- const manifest = [];
16677
- const seenHashes = new Set;
16678
- for (const image of targets) {
16679
- const reference = view.imageById.get(image.id);
16680
- if (!reference)
16681
- continue;
16682
- const extension = extensionFor(image.contentType, reference.partName);
16683
- const fileName = `${image.hash}.${extension}`;
16684
- const outputPath = join(outputDir, fileName);
16685
- if (!seenHashes.has(image.hash)) {
16686
- const bytes = await view.pkg.readBytes(reference.partName);
16687
- await Bun.write(outputPath, bytes);
16688
- seenHashes.add(image.hash);
16990
+ const relationships = XmlNode2.findRoot(view.relationshipsTree, "Relationships");
16991
+ if (!relationships) {
16992
+ return fail("UNHANDLED", "Missing <Relationships> root in document rels");
16993
+ }
16994
+ const relationshipId = addHyperlinkRelationship(relationships, url);
16995
+ try {
16996
+ wrapSpanInHyperlink(paragraphRef.node, target.span, relationshipId);
16997
+ } catch (error) {
16998
+ if (error instanceof HyperlinkWrapError) {
16999
+ return fail("USAGE", error.message);
16689
17000
  }
16690
- manifest.push({
16691
- id: image.id,
16692
- path: outputPath,
16693
- bytes: (await Bun.file(outputPath).arrayBuffer()).byteLength
16694
- });
17001
+ throw error;
16695
17002
  }
16696
- await respond({ ok: true, operation: "images.extract", path, manifest });
17003
+ view.hyperlinksByRelationshipId.set(relationshipId, { url });
17004
+ await saveDocView(view, outputPath);
17005
+ await respond({
17006
+ ok: true,
17007
+ operation: "hyperlinks.add",
17008
+ path: outputPath ?? path,
17009
+ at: atInput,
17010
+ url
17011
+ });
16697
17012
  return EXIT.OK;
16698
17013
  }
16699
- function collectImages(blocks, out) {
16700
- for (const block of blocks) {
17014
+ var HELP11 = `docx hyperlinks add \u2014 wrap an existing span in a hyperlink
17015
+
17016
+ Usage:
17017
+ docx hyperlinks add FILE --at LOCATOR --url URL [options]
17018
+
17019
+ Required:
17020
+ --at LOCATOR Where to wrap. Supports:
17021
+ pN:S-E chars S..E of pN
17022
+ tT:rRcC:pK:S-E chars S..E of a cell paragraph
17023
+ --url URL Target URL
17024
+
17025
+ Optional:
17026
+ -o, --output PATH Write to PATH instead of overwriting FILE
17027
+ --dry-run Print what would change; do not write the file
17028
+ -h, --help Show this help
17029
+
17030
+ The span must lie inside a single paragraph and must not overlap an existing
17031
+ hyperlink or a tracked-change wrapper.
17032
+
17033
+ Examples:
17034
+ docx hyperlinks add doc.docx --at p3:5-20 --url https://example.com
17035
+ `;
17036
+ var init_add2 = __esm(() => {
17037
+ init_core();
17038
+ init_parser();
17039
+ init_respond();
17040
+ init_wrap();
17041
+ });
17042
+
17043
+ // src/cli/hyperlinks/delete.ts
17044
+ var exports_delete3 = {};
17045
+ __export(exports_delete3, {
17046
+ run: () => run12
17047
+ });
17048
+ import { parseArgs as parseArgs11 } from "util";
17049
+ async function run12(args) {
17050
+ let parsed;
17051
+ try {
17052
+ parsed = parseArgs11({
17053
+ args,
17054
+ allowPositionals: true,
17055
+ options: {
17056
+ at: { type: "string" },
17057
+ output: { type: "string", short: "o" },
17058
+ "dry-run": { type: "boolean" },
17059
+ help: { type: "boolean", short: "h" }
17060
+ }
17061
+ });
17062
+ } catch (parseError) {
17063
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
17064
+ return fail("USAGE", message, HELP12);
17065
+ }
17066
+ if (parsed.values.help) {
17067
+ await writeStdout(HELP12);
17068
+ return EXIT.OK;
17069
+ }
17070
+ const path = parsed.positionals[0];
17071
+ if (!path)
17072
+ return fail("USAGE", "Missing FILE argument", HELP12);
17073
+ const targetId = parsed.values.at;
17074
+ if (!targetId)
17075
+ return fail("USAGE", "Missing --at LINK_ID", HELP12);
17076
+ const view = await openOrFail(path);
17077
+ if (typeof view === "number")
17078
+ return view;
17079
+ const reference = view.hyperlinkById.get(targetId);
17080
+ if (!reference) {
17081
+ return fail("HYPERLINK_NOT_FOUND", `Hyperlink not found: ${targetId}`);
17082
+ }
17083
+ const oldUrl = reference.relationshipId ? view.hyperlinksByRelationshipId.get(reference.relationshipId)?.url : undefined;
17084
+ const outputPath = parsed.values.output;
17085
+ if (parsed.values["dry-run"]) {
17086
+ await respond({
17087
+ ok: true,
17088
+ operation: "hyperlinks.delete",
17089
+ dryRun: true,
17090
+ path,
17091
+ hyperlinkId: targetId,
17092
+ from: oldUrl,
17093
+ ...outputPath ? { output: outputPath } : {}
17094
+ });
17095
+ return EXIT.OK;
17096
+ }
17097
+ const index = reference.parent.indexOf(reference.node);
17098
+ if (index === -1) {
17099
+ return fail("HYPERLINK_NOT_FOUND", `Hyperlink reference is stale (parent does not contain it): ${targetId}`);
17100
+ }
17101
+ reference.parent.splice(index, 1, ...reference.node.children);
17102
+ view.hyperlinkById.delete(targetId);
17103
+ if (reference.relationshipId) {
17104
+ const remaining = countHyperlinkUsages(view.documentTree, reference.relationshipId);
17105
+ if (remaining === 0) {
17106
+ pruneRelationship(view.relationshipsTree, reference.relationshipId);
17107
+ view.hyperlinksByRelationshipId.delete(reference.relationshipId);
17108
+ }
17109
+ }
17110
+ await saveDocView(view, outputPath);
17111
+ await respond({
17112
+ ok: true,
17113
+ operation: "hyperlinks.delete",
17114
+ path: outputPath ?? path,
17115
+ hyperlinkId: targetId,
17116
+ from: oldUrl
17117
+ });
17118
+ return EXIT.OK;
17119
+ }
17120
+ function countHyperlinkUsages(documentTree, relationshipId) {
17121
+ let count = 0;
17122
+ for (const root of documentTree) {
17123
+ count += countInNode(root, relationshipId);
17124
+ }
17125
+ return count;
17126
+ }
17127
+ function countInNode(node, relationshipId) {
17128
+ let count = 0;
17129
+ if (node.tag === "w:hyperlink" && node.getAttribute("r:id") === relationshipId) {
17130
+ count++;
17131
+ }
17132
+ for (const child of node.children) {
17133
+ count += countInNode(child, relationshipId);
17134
+ }
17135
+ return count;
17136
+ }
17137
+ function pruneRelationship(relationshipsTree, relationshipId) {
17138
+ const relationships = XmlNode2.findRoot(relationshipsTree, "Relationships");
17139
+ if (!relationships)
17140
+ return;
17141
+ relationships.children = relationships.children.filter((child) => !(child.tag === "Relationship" && child.getAttribute("Id") === relationshipId));
17142
+ }
17143
+ var HELP12 = `docx hyperlinks delete \u2014 unwrap a hyperlink (keep the text)
17144
+
17145
+ Usage:
17146
+ docx hyperlinks delete FILE --at LINK_ID [options]
17147
+
17148
+ Required:
17149
+ --at LINK_ID Existing hyperlink to remove (e.g., link0)
17150
+
17151
+ Optional:
17152
+ -o, --output PATH Write to PATH instead of overwriting FILE
17153
+ --dry-run Print what would change; do not write the file
17154
+ -h, --help Show this help
17155
+
17156
+ The display text stays in place; only the <w:hyperlink> wrapper is removed.
17157
+ If the underlying relationship is no longer referenced, it is pruned from the
17158
+ rels file too.
17159
+
17160
+ Examples:
17161
+ docx hyperlinks delete doc.docx --at link0
17162
+ `;
17163
+ var init_delete3 = __esm(() => {
17164
+ init_core();
17165
+ init_parser();
17166
+ init_respond();
17167
+ });
17168
+
17169
+ // src/cli/hyperlinks/list.ts
17170
+ var exports_list2 = {};
17171
+ __export(exports_list2, {
17172
+ run: () => run13
17173
+ });
17174
+ import { parseArgs as parseArgs12 } from "util";
17175
+ async function run13(args) {
17176
+ let parsed;
17177
+ try {
17178
+ parsed = parseArgs12({
17179
+ args,
17180
+ allowPositionals: true,
17181
+ options: {
17182
+ help: { type: "boolean", short: "h" }
17183
+ }
17184
+ });
17185
+ } catch (parseError) {
17186
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
17187
+ return fail("USAGE", message, HELP13);
17188
+ }
17189
+ if (parsed.values.help) {
17190
+ await writeStdout(HELP13);
17191
+ return EXIT.OK;
17192
+ }
17193
+ const path = parsed.positionals[0];
17194
+ if (!path)
17195
+ return fail("USAGE", "Missing FILE argument", HELP13);
17196
+ const view = await openOrFail(path);
17197
+ if (typeof view === "number")
17198
+ return view;
17199
+ const entries = new Map;
17200
+ collectHyperlinks(view.doc.blocks, entries);
17201
+ await respond([...entries.values()]);
17202
+ return EXIT.OK;
17203
+ }
17204
+ function collectHyperlinks(blocks, entries) {
17205
+ for (const block of blocks) {
16701
17206
  if (block.type === "paragraph") {
16702
- for (const run12 of block.runs) {
16703
- if (run12.type === "image")
16704
- out.push(run12);
17207
+ for (const run14 of block.runs) {
17208
+ if (run14.type !== "text" || !run14.hyperlink)
17209
+ continue;
17210
+ addToEntry(entries, run14.hyperlink, block.id, run14.text);
17211
+ }
17212
+ continue;
17213
+ }
17214
+ if (block.type === "table") {
17215
+ for (const row of block.rows) {
17216
+ for (const cell of row.cells) {
17217
+ collectHyperlinks(cell.blocks, entries);
17218
+ }
17219
+ }
17220
+ }
17221
+ }
17222
+ }
17223
+ function addToEntry(entries, hyperlink, blockId, text) {
17224
+ const existing = entries.get(hyperlink.id);
17225
+ if (existing) {
17226
+ existing.text += text;
17227
+ return;
17228
+ }
17229
+ const entry = {
17230
+ id: hyperlink.id,
17231
+ text,
17232
+ blockId
17233
+ };
17234
+ if (hyperlink.url)
17235
+ entry.url = hyperlink.url;
17236
+ if (hyperlink.anchor)
17237
+ entry.anchor = hyperlink.anchor;
17238
+ if (hyperlink.tooltip)
17239
+ entry.tooltip = hyperlink.tooltip;
17240
+ entries.set(hyperlink.id, entry);
17241
+ }
17242
+ var HELP13 = `docx hyperlinks list \u2014 print hyperlink manifest as JSON
17243
+
17244
+ Usage:
17245
+ docx hyperlinks list FILE [options]
17246
+
17247
+ Options:
17248
+ -h, --help Show this help
17249
+
17250
+ Each entry has: id, url (or anchor), tooltip (if set), text (display text),
17251
+ and blockId (the paragraph containing the link).
17252
+
17253
+ Examples:
17254
+ docx hyperlinks list doc.docx | jq -c '.[] | {id, url, text}'
17255
+ `;
17256
+ var init_list2 = __esm(() => {
17257
+ init_respond();
17258
+ });
17259
+
17260
+ // src/cli/hyperlinks/replace.ts
17261
+ var exports_replace = {};
17262
+ __export(exports_replace, {
17263
+ run: () => run14
17264
+ });
17265
+ import { parseArgs as parseArgs13 } from "util";
17266
+ async function run14(args) {
17267
+ let parsed;
17268
+ try {
17269
+ parsed = parseArgs13({
17270
+ args,
17271
+ allowPositionals: true,
17272
+ options: {
17273
+ at: { type: "string" },
17274
+ with: { type: "string" },
17275
+ output: { type: "string", short: "o" },
17276
+ "dry-run": { type: "boolean" },
17277
+ help: { type: "boolean", short: "h" }
17278
+ }
17279
+ });
17280
+ } catch (parseError) {
17281
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
17282
+ return fail("USAGE", message, HELP14);
17283
+ }
17284
+ if (parsed.values.help) {
17285
+ await writeStdout(HELP14);
17286
+ return EXIT.OK;
17287
+ }
17288
+ const path = parsed.positionals[0];
17289
+ if (!path)
17290
+ return fail("USAGE", "Missing FILE argument", HELP14);
17291
+ const targetId = parsed.values.at;
17292
+ if (!targetId)
17293
+ return fail("USAGE", "Missing --at LINK_ID", HELP14);
17294
+ const newUrl = parsed.values.with;
17295
+ if (!newUrl)
17296
+ return fail("USAGE", "Missing --with URL", HELP14);
17297
+ const view = await openOrFail(path);
17298
+ if (typeof view === "number")
17299
+ return view;
17300
+ const reference = view.hyperlinkById.get(targetId);
17301
+ if (!reference) {
17302
+ return fail("HYPERLINK_NOT_FOUND", `Hyperlink not found: ${targetId}`);
17303
+ }
17304
+ const relationships = XmlNode2.findRoot(view.relationshipsTree, "Relationships");
17305
+ if (!relationships) {
17306
+ return fail("UNHANDLED", "Missing <Relationships> root in document rels");
17307
+ }
17308
+ const existingId = reference.relationshipId;
17309
+ const oldUrl = existingId ? view.hyperlinksByRelationshipId.get(existingId)?.url : undefined;
17310
+ const sharedCount = existingId ? countHyperlinkUsages2(view.documentTree, existingId) : 0;
17311
+ const willAllocateNew = !existingId || sharedCount > 1;
17312
+ const outputPath = parsed.values.output;
17313
+ if (parsed.values["dry-run"]) {
17314
+ await respond({
17315
+ ok: true,
17316
+ operation: "hyperlinks.replace",
17317
+ dryRun: true,
17318
+ path,
17319
+ hyperlinkId: targetId,
17320
+ from: oldUrl,
17321
+ to: newUrl,
17322
+ sharedRelationship: sharedCount > 1,
17323
+ ...outputPath ? { output: outputPath } : {}
17324
+ });
17325
+ return EXIT.OK;
17326
+ }
17327
+ if (willAllocateNew) {
17328
+ const newRelationshipId = addHyperlinkRelationship(relationships, newUrl);
17329
+ reference.node.setAttribute("r:id", newRelationshipId);
17330
+ reference.relationshipId = newRelationshipId;
17331
+ view.hyperlinksByRelationshipId.set(newRelationshipId, { url: newUrl });
17332
+ } else if (existingId) {
17333
+ updateRelationshipTarget(relationships, existingId, newUrl);
17334
+ view.hyperlinksByRelationshipId.set(existingId, { url: newUrl });
17335
+ }
17336
+ await saveDocView(view, outputPath);
17337
+ await respond({
17338
+ ok: true,
17339
+ operation: "hyperlinks.replace",
17340
+ path: outputPath ?? path,
17341
+ hyperlinkId: targetId,
17342
+ from: oldUrl,
17343
+ to: newUrl
17344
+ });
17345
+ return EXIT.OK;
17346
+ }
17347
+ function updateRelationshipTarget(relationships, relationshipId, newTarget) {
17348
+ for (const child of relationships.children) {
17349
+ if (child.tag !== "Relationship")
17350
+ continue;
17351
+ if (child.getAttribute("Id") === relationshipId) {
17352
+ child.setAttribute("Target", newTarget);
17353
+ return;
17354
+ }
17355
+ }
17356
+ }
17357
+ function countHyperlinkUsages2(documentTree, relationshipId) {
17358
+ let count = 0;
17359
+ for (const root of documentTree) {
17360
+ count += countInNode2(root, relationshipId);
17361
+ }
17362
+ return count;
17363
+ }
17364
+ function countInNode2(node, relationshipId) {
17365
+ let count = 0;
17366
+ if (node.tag === "w:hyperlink" && node.getAttribute("r:id") === relationshipId) {
17367
+ count++;
17368
+ }
17369
+ for (const child of node.children) {
17370
+ count += countInNode2(child, relationshipId);
17371
+ }
17372
+ return count;
17373
+ }
17374
+ var HELP14 = `docx hyperlinks replace \u2014 change a hyperlink's URL
17375
+
17376
+ Usage:
17377
+ docx hyperlinks replace FILE --at LINK_ID --with URL [options]
17378
+
17379
+ Required:
17380
+ --at LINK_ID Existing hyperlink to update (e.g., link0)
17381
+ --with URL New target URL
17382
+
17383
+ Optional:
17384
+ -o, --output PATH Write to PATH instead of overwriting FILE
17385
+ --dry-run Print what would change; do not write the file
17386
+ -h, --help Show this help
17387
+
17388
+ Replaces only the targeted hyperlink. If multiple hyperlinks shared the same
17389
+ underlying relationship, a new relationship is allocated so the others are
17390
+ unaffected.
17391
+
17392
+ Examples:
17393
+ docx hyperlinks replace doc.docx --at link0 --with https://example.com
17394
+ `;
17395
+ var init_replace = __esm(() => {
17396
+ init_core();
17397
+ init_parser();
17398
+ init_respond();
17399
+ });
17400
+
17401
+ // src/cli/hyperlinks/index.ts
17402
+ var exports_hyperlinks = {};
17403
+ __export(exports_hyperlinks, {
17404
+ run: () => run15
17405
+ });
17406
+ async function run15(args) {
17407
+ const verb = args[0];
17408
+ if (!verb || verb === "--help" || verb === "-h" || verb === "help") {
17409
+ await writeStdout(HELP15);
17410
+ return verb ? 0 : 2;
17411
+ }
17412
+ const loader = SUBCOMMANDS2[verb];
17413
+ if (!loader) {
17414
+ return fail("USAGE", `Unknown hyperlinks subcommand: ${verb}`, 'Run "docx hyperlinks --help".');
17415
+ }
17416
+ const module_ = await loader();
17417
+ return module_.run(args.slice(1));
17418
+ }
17419
+ var SUBCOMMANDS2, HELP15 = `docx hyperlinks \u2014 manage hyperlinks
17420
+
17421
+ Usage:
17422
+ docx hyperlinks <verb> FILE [options]
17423
+
17424
+ Verbs:
17425
+ add Wrap an existing span in a hyperlink
17426
+ delete Unwrap a hyperlink (keep the text)
17427
+ list Print hyperlink manifest as JSON
17428
+ replace Change a hyperlink's URL
17429
+
17430
+ Run "docx hyperlinks <verb> --help" for verb-specific help.
17431
+ `;
17432
+ var init_hyperlinks = __esm(() => {
17433
+ init_respond();
17434
+ SUBCOMMANDS2 = {
17435
+ add: () => Promise.resolve().then(() => (init_add2(), exports_add2)),
17436
+ delete: () => Promise.resolve().then(() => (init_delete3(), exports_delete3)),
17437
+ list: () => Promise.resolve().then(() => (init_list2(), exports_list2)),
17438
+ replace: () => Promise.resolve().then(() => (init_replace(), exports_replace))
17439
+ };
17440
+ });
17441
+
17442
+ // src/cli/images/extract.ts
17443
+ var exports_extract = {};
17444
+ __export(exports_extract, {
17445
+ run: () => run16
17446
+ });
17447
+ import { join } from "path";
17448
+ import { parseArgs as parseArgs14 } from "util";
17449
+ async function run16(args) {
17450
+ let parsed;
17451
+ try {
17452
+ parsed = parseArgs14({
17453
+ args,
17454
+ allowPositionals: true,
17455
+ options: {
17456
+ to: { type: "string" },
17457
+ id: { type: "string" },
17458
+ help: { type: "boolean", short: "h" }
17459
+ }
17460
+ });
17461
+ } catch (parseError) {
17462
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
17463
+ return fail("USAGE", message, HELP16);
17464
+ }
17465
+ if (parsed.values.help) {
17466
+ await writeStdout(HELP16);
17467
+ return EXIT.OK;
17468
+ }
17469
+ const path = parsed.positionals[0];
17470
+ if (!path)
17471
+ return fail("USAGE", "Missing FILE argument", HELP16);
17472
+ const outputDir = parsed.values.to;
17473
+ if (!outputDir)
17474
+ return fail("USAGE", "Missing --to DIR", HELP16);
17475
+ const targetId = parsed.values.id;
17476
+ const view = await openOrFail(path);
17477
+ if (typeof view === "number")
17478
+ return view;
17479
+ await enrichImageHashes(view);
17480
+ const allImages = [];
17481
+ collectImages(view.doc.blocks, allImages);
17482
+ const targets = targetId ? allImages.filter((image) => image.id === targetId) : allImages;
17483
+ if (targetId && targets.length === 0) {
17484
+ return fail("IMAGE_NOT_FOUND", `Image not found: ${targetId}`);
17485
+ }
17486
+ const manifest = [];
17487
+ const seenHashes = new Set;
17488
+ for (const image of targets) {
17489
+ const reference = view.imageById.get(image.id);
17490
+ if (!reference)
17491
+ continue;
17492
+ const extension = extensionFor(image.contentType, reference.partName);
17493
+ const fileName = `${image.hash}.${extension}`;
17494
+ const outputPath = join(outputDir, fileName);
17495
+ if (!seenHashes.has(image.hash)) {
17496
+ const bytes = await view.pkg.readBytes(reference.partName);
17497
+ await Bun.write(outputPath, bytes);
17498
+ seenHashes.add(image.hash);
17499
+ }
17500
+ manifest.push({
17501
+ id: image.id,
17502
+ path: outputPath,
17503
+ bytes: (await Bun.file(outputPath).arrayBuffer()).byteLength
17504
+ });
17505
+ }
17506
+ await respond({ ok: true, operation: "images.extract", path, manifest });
17507
+ return EXIT.OK;
17508
+ }
17509
+ function collectImages(blocks, out) {
17510
+ for (const block of blocks) {
17511
+ if (block.type === "paragraph") {
17512
+ for (const run17 of block.runs) {
17513
+ if (run17.type === "image")
17514
+ out.push(run17);
16705
17515
  }
16706
17516
  continue;
16707
17517
  }
@@ -16721,7 +17531,7 @@ function extensionFor(contentType, partName) {
16721
17531
  const fromName = partName.split(".").pop()?.toLowerCase();
16722
17532
  return fromName ?? "bin";
16723
17533
  }
16724
- var HELP11 = `docx images extract \u2014 dump image bytes to a directory
17534
+ var HELP16 = `docx images extract \u2014 dump image bytes to a directory
16725
17535
 
16726
17536
  Usage:
16727
17537
  docx images extract FILE --to DIR [options]
@@ -16760,15 +17570,15 @@ var init_extract = __esm(() => {
16760
17570
  });
16761
17571
 
16762
17572
  // src/cli/images/list.ts
16763
- var exports_list2 = {};
16764
- __export(exports_list2, {
16765
- run: () => run12
17573
+ var exports_list3 = {};
17574
+ __export(exports_list3, {
17575
+ run: () => run17
16766
17576
  });
16767
- import { parseArgs as parseArgs11 } from "util";
16768
- async function run12(args) {
17577
+ import { parseArgs as parseArgs15 } from "util";
17578
+ async function run17(args) {
16769
17579
  let parsed;
16770
17580
  try {
16771
- parsed = parseArgs11({
17581
+ parsed = parseArgs15({
16772
17582
  args,
16773
17583
  allowPositionals: true,
16774
17584
  options: {
@@ -16777,15 +17587,15 @@ async function run12(args) {
16777
17587
  });
16778
17588
  } catch (parseError) {
16779
17589
  const message = parseError instanceof Error ? parseError.message : String(parseError);
16780
- return fail("USAGE", message, HELP12);
17590
+ return fail("USAGE", message, HELP17);
16781
17591
  }
16782
17592
  if (parsed.values.help) {
16783
- await writeStdout(HELP12);
17593
+ await writeStdout(HELP17);
16784
17594
  return EXIT.OK;
16785
17595
  }
16786
17596
  const path = parsed.positionals[0];
16787
17597
  if (!path)
16788
- return fail("USAGE", "Missing FILE argument", HELP12);
17598
+ return fail("USAGE", "Missing FILE argument", HELP17);
16789
17599
  const view = await openOrFail(path);
16790
17600
  if (typeof view === "number")
16791
17601
  return view;
@@ -16798,9 +17608,9 @@ async function run12(args) {
16798
17608
  function collectImages2(blocks, out) {
16799
17609
  for (const block of blocks) {
16800
17610
  if (block.type === "paragraph") {
16801
- for (const run13 of block.runs) {
16802
- if (run13.type === "image")
16803
- out.push(run13);
17611
+ for (const run18 of block.runs) {
17612
+ if (run18.type === "image")
17613
+ out.push(run18);
16804
17614
  }
16805
17615
  continue;
16806
17616
  }
@@ -16813,7 +17623,7 @@ function collectImages2(blocks, out) {
16813
17623
  }
16814
17624
  }
16815
17625
  }
16816
- var HELP12 = `docx images list \u2014 print image manifest as JSON
17626
+ var HELP17 = `docx images list \u2014 print image manifest as JSON
16817
17627
 
16818
17628
  Usage:
16819
17629
  docx images list FILE [options]
@@ -16824,21 +17634,21 @@ Options:
16824
17634
  Examples:
16825
17635
  docx images list doc.docx | jq -c '.[] | {id, contentType, hash}'
16826
17636
  `;
16827
- var init_list2 = __esm(() => {
17637
+ var init_list3 = __esm(() => {
16828
17638
  init_core();
16829
17639
  init_respond();
16830
17640
  });
16831
17641
 
16832
17642
  // src/cli/images/replace.ts
16833
- var exports_replace = {};
16834
- __export(exports_replace, {
16835
- run: () => run13
17643
+ var exports_replace2 = {};
17644
+ __export(exports_replace2, {
17645
+ run: () => run18
16836
17646
  });
16837
- import { parseArgs as parseArgs12 } from "util";
16838
- async function run13(args) {
17647
+ import { parseArgs as parseArgs16 } from "util";
17648
+ async function run18(args) {
16839
17649
  let parsed;
16840
17650
  try {
16841
- parsed = parseArgs12({
17651
+ parsed = parseArgs16({
16842
17652
  args,
16843
17653
  allowPositionals: true,
16844
17654
  options: {
@@ -16851,21 +17661,21 @@ async function run13(args) {
16851
17661
  });
16852
17662
  } catch (parseError) {
16853
17663
  const message = parseError instanceof Error ? parseError.message : String(parseError);
16854
- return fail("USAGE", message, HELP13);
17664
+ return fail("USAGE", message, HELP18);
16855
17665
  }
16856
17666
  if (parsed.values.help) {
16857
- await writeStdout(HELP13);
17667
+ await writeStdout(HELP18);
16858
17668
  return EXIT.OK;
16859
17669
  }
16860
17670
  const path = parsed.positionals[0];
16861
17671
  if (!path)
16862
- return fail("USAGE", "Missing FILE argument", HELP13);
17672
+ return fail("USAGE", "Missing FILE argument", HELP18);
16863
17673
  const targetId = parsed.values.at;
16864
17674
  if (!targetId)
16865
- return fail("USAGE", "Missing --at IMG_ID", HELP13);
17675
+ return fail("USAGE", "Missing --at IMG_ID", HELP18);
16866
17676
  const sourcePath = parsed.values.with;
16867
17677
  if (!sourcePath)
16868
- return fail("USAGE", "Missing --with PATH", HELP13);
17678
+ return fail("USAGE", "Missing --with PATH", HELP18);
16869
17679
  const sourceFile = Bun.file(sourcePath);
16870
17680
  if (!await sourceFile.exists()) {
16871
17681
  return fail("FILE_NOT_FOUND", `Replacement file not found: ${sourcePath}`);
@@ -16904,7 +17714,7 @@ async function run13(args) {
16904
17714
  } else {
16905
17715
  view.pkg.writeBytes(newPartName, bytes);
16906
17716
  view.pkg.deletePart(originalPartName);
16907
- updateRelationshipTarget(view.relationshipsTree, reference.relationshipId, relativeTargetFor(newPartName));
17717
+ updateRelationshipTarget2(view.relationshipsTree, reference.relationshipId, relativeTargetFor(newPartName));
16908
17718
  ensureContentTypeDefault(view.contentTypesTree, newExtension, newMimeType);
16909
17719
  reference.partName = newPartName;
16910
17720
  reference.contentType = newMimeType;
@@ -16932,7 +17742,7 @@ function renameExtension(partName, newExtension) {
16932
17742
  function relativeTargetFor(partName) {
16933
17743
  return partName.startsWith("word/") ? partName.slice("word/".length) : partName;
16934
17744
  }
16935
- function updateRelationshipTarget(relationshipsTree, relationshipId, newTarget) {
17745
+ function updateRelationshipTarget2(relationshipsTree, relationshipId, newTarget) {
16936
17746
  const relationships = XmlNode2.findRoot(relationshipsTree, "Relationships");
16937
17747
  if (!relationships)
16938
17748
  return;
@@ -16957,7 +17767,7 @@ function ensureContentTypeDefault(contentTypesTree, extension, mimeType) {
16957
17767
  }
16958
17768
  types.children.push(new XmlNode2("Default", { Extension: extension, ContentType: mimeType }));
16959
17769
  }
16960
- var HELP13 = `docx images replace \u2014 swap an image's bytes
17770
+ var HELP18 = `docx images replace \u2014 swap an image's bytes
16961
17771
 
16962
17772
  Usage:
16963
17773
  docx images replace FILE --at IMG_ID --with PATH [options]
@@ -16979,7 +17789,7 @@ Examples:
16979
17789
  docx images replace doc.docx --at img2 --with ./new-photo.png
16980
17790
  docx images replace doc.docx --at img0 --with ./diagram.svg
16981
17791
  `, EXTENSION_BY_MIME;
16982
- var init_replace = __esm(() => {
17792
+ var init_replace2 = __esm(() => {
16983
17793
  init_core();
16984
17794
  init_parser();
16985
17795
  init_respond();
@@ -17000,22 +17810,22 @@ var init_replace = __esm(() => {
17000
17810
  // src/cli/images/index.ts
17001
17811
  var exports_images = {};
17002
17812
  __export(exports_images, {
17003
- run: () => run14
17813
+ run: () => run19
17004
17814
  });
17005
- async function run14(args) {
17815
+ async function run19(args) {
17006
17816
  const verb = args[0];
17007
17817
  if (!verb || verb === "--help" || verb === "-h" || verb === "help") {
17008
- await writeStdout(HELP14);
17818
+ await writeStdout(HELP19);
17009
17819
  return verb ? 0 : 2;
17010
17820
  }
17011
- const loader = SUBCOMMANDS2[verb];
17821
+ const loader = SUBCOMMANDS3[verb];
17012
17822
  if (!loader) {
17013
17823
  return fail("USAGE", `Unknown images subcommand: ${verb}`, 'Run "docx images --help".');
17014
17824
  }
17015
17825
  const module_ = await loader();
17016
17826
  return module_.run(args.slice(1));
17017
17827
  }
17018
- var SUBCOMMANDS2, HELP14 = `docx images \u2014 manage embedded images
17828
+ var SUBCOMMANDS3, HELP19 = `docx images \u2014 manage embedded images
17019
17829
 
17020
17830
  Usage:
17021
17831
  docx images <verb> FILE [options]
@@ -17029,10 +17839,10 @@ Run "docx images <verb> --help" for verb-specific help.
17029
17839
  `;
17030
17840
  var init_images = __esm(() => {
17031
17841
  init_respond();
17032
- SUBCOMMANDS2 = {
17842
+ SUBCOMMANDS3 = {
17033
17843
  extract: () => Promise.resolve().then(() => (init_extract(), exports_extract)),
17034
- list: () => Promise.resolve().then(() => (init_list2(), exports_list2)),
17035
- replace: () => Promise.resolve().then(() => (init_replace(), exports_replace))
17844
+ list: () => Promise.resolve().then(() => (init_list3(), exports_list3)),
17845
+ replace: () => Promise.resolve().then(() => (init_replace2(), exports_replace2))
17036
17846
  };
17037
17847
  });
17038
17848
 
@@ -17043,6 +17853,8 @@ var types_default = `export type Doc = {
17043
17853
  properties: DocProperties;
17044
17854
  blocks: Block[];
17045
17855
  comments: Comment[];
17856
+ footnotes: Footnote[];
17857
+ endnotes: Footnote[];
17046
17858
  };
17047
17859
 
17048
17860
  export type DocProperties = {
@@ -17082,7 +17894,14 @@ export type SectionBreak = {
17082
17894
  type: "sectionBreak";
17083
17895
  };
17084
17896
 
17085
- export type Run = TextRun | ImageRun | BreakRun | TabRun;
17897
+ export type Run =
17898
+ | TextRun
17899
+ | ImageRun
17900
+ | BreakRun
17901
+ | TabRun
17902
+ | EquationRun
17903
+ | FootnoteRefRun
17904
+ | ChartRun;
17086
17905
 
17087
17906
  export type TextRun = {
17088
17907
  type: "text";
@@ -17097,6 +17916,14 @@ export type TextRun = {
17097
17916
  sizeHalfPoints?: number;
17098
17917
  comments?: string[];
17099
17918
  trackedChange?: TrackedChange;
17919
+ hyperlink?: Hyperlink;
17920
+ };
17921
+
17922
+ export type Hyperlink = {
17923
+ id: string;
17924
+ url?: string;
17925
+ anchor?: string;
17926
+ tooltip?: string;
17100
17927
  };
17101
17928
 
17102
17929
  export type ImageRun = {
@@ -17118,6 +17945,37 @@ export type TabRun = {
17118
17945
  type: "tab";
17119
17946
  };
17120
17947
 
17948
+ /** A math equation surfaced as concatenated <m:t> plaintext. The structural
17949
+ * OOMath markup (subscripts, fractions, etc.) is collapsed to literal characters,
17950
+ * so the rendering is degraded. \`display\` distinguishes inline equations
17951
+ * (<m:oMath>) from block-level display equations (<m:oMathPara>). */
17952
+ export type EquationRun = {
17953
+ type: "equation";
17954
+ text: string;
17955
+ display: boolean;
17956
+ };
17957
+
17958
+ /** Reference to a footnote or endnote whose body lives in Doc.footnotes or
17959
+ * Doc.endnotes. The id matches the footnote/endnote id (\`fn1\`, \`en1\`). */
17960
+ export type FootnoteRefRun = {
17961
+ type: "footnoteRef";
17962
+ kind: "footnote" | "endnote";
17963
+ id: string;
17964
+ };
17965
+
17966
+ /** Placeholder for a non-picture drawing (chart, shape, SmartArt, etc.). The
17967
+ * full content is preserved in the underlying XML; the run is a marker so
17968
+ * callers can know "something visual lives here." */
17969
+ export type ChartRun = {
17970
+ type: "chart";
17971
+ kind: "chart" | "shape" | "smartart" | "drawing";
17972
+ };
17973
+
17974
+ export type Footnote = {
17975
+ id: string;
17976
+ text: string;
17977
+ };
17978
+
17121
17979
  export type TrackedChange = {
17122
17980
  kind: "ins" | "del";
17123
17981
  author: string;
@@ -17148,13 +18006,13 @@ var init_types = () => {};
17148
18006
  // src/cli/info/schema.ts
17149
18007
  var exports_schema = {};
17150
18008
  __export(exports_schema, {
17151
- run: () => run15
18009
+ run: () => run20
17152
18010
  });
17153
- import { parseArgs as parseArgs13 } from "util";
17154
- async function run15(args) {
18011
+ import { parseArgs as parseArgs17 } from "util";
18012
+ async function run20(args) {
17155
18013
  let parsed;
17156
18014
  try {
17157
- parsed = parseArgs13({
18015
+ parsed = parseArgs17({
17158
18016
  args,
17159
18017
  allowPositionals: true,
17160
18018
  options: {
@@ -17165,10 +18023,10 @@ async function run15(args) {
17165
18023
  });
17166
18024
  } catch (parseError) {
17167
18025
  const message = parseError instanceof Error ? parseError.message : String(parseError);
17168
- return fail("USAGE", message, HELP15);
18026
+ return fail("USAGE", message, HELP20);
17169
18027
  }
17170
18028
  if (parsed.values.help) {
17171
- await writeStdout(HELP15);
18029
+ await writeStdout(HELP20);
17172
18030
  return EXIT.OK;
17173
18031
  }
17174
18032
  if (parsed.values.ts) {
@@ -17178,7 +18036,7 @@ async function run15(args) {
17178
18036
  await respond(JSON_SCHEMA);
17179
18037
  return EXIT.OK;
17180
18038
  }
17181
- var HELP15 = `docx schema \u2014 print the AST type definitions
18039
+ var HELP20 = `docx schema \u2014 print the AST type definitions
17182
18040
 
17183
18041
  Usage:
17184
18042
  docx schema [options]
@@ -17200,7 +18058,15 @@ var init_schema = __esm(() => {
17200
18058
  $id: "https://github.com/kklimuk/docx-cli/schema",
17201
18059
  title: "docx-cli AST",
17202
18060
  type: "object",
17203
- required: ["schemaVersion", "path", "properties", "blocks", "comments"],
18061
+ required: [
18062
+ "schemaVersion",
18063
+ "path",
18064
+ "properties",
18065
+ "blocks",
18066
+ "comments",
18067
+ "footnotes",
18068
+ "endnotes"
18069
+ ],
17204
18070
  properties: {
17205
18071
  schemaVersion: { const: 1 },
17206
18072
  path: { type: "string" },
@@ -17214,7 +18080,9 @@ var init_schema = __esm(() => {
17214
18080
  }
17215
18081
  },
17216
18082
  blocks: { type: "array", items: { $ref: "#/$defs/Block" } },
17217
- comments: { type: "array", items: { $ref: "#/$defs/Comment" } }
18083
+ comments: { type: "array", items: { $ref: "#/$defs/Comment" } },
18084
+ footnotes: { type: "array", items: { $ref: "#/$defs/Footnote" } },
18085
+ endnotes: { type: "array", items: { $ref: "#/$defs/Footnote" } }
17218
18086
  },
17219
18087
  $defs: {
17220
18088
  Block: {
@@ -17248,7 +18116,10 @@ var init_schema = __esm(() => {
17248
18116
  { $ref: "#/$defs/TextRun" },
17249
18117
  { $ref: "#/$defs/ImageRun" },
17250
18118
  { $ref: "#/$defs/BreakRun" },
17251
- { $ref: "#/$defs/TabRun" }
18119
+ { $ref: "#/$defs/TabRun" },
18120
+ { $ref: "#/$defs/EquationRun" },
18121
+ { $ref: "#/$defs/FootnoteRefRun" },
18122
+ { $ref: "#/$defs/ChartRun" }
17252
18123
  ]
17253
18124
  },
17254
18125
  TextRun: {
@@ -17275,7 +18146,18 @@ var init_schema = __esm(() => {
17275
18146
  date: { type: "string" },
17276
18147
  revisionId: { type: "string" }
17277
18148
  }
17278
- }
18149
+ },
18150
+ hyperlink: { $ref: "#/$defs/Hyperlink" }
18151
+ }
18152
+ },
18153
+ Hyperlink: {
18154
+ type: "object",
18155
+ required: ["id"],
18156
+ properties: {
18157
+ id: { type: "string" },
18158
+ url: { type: "string" },
18159
+ anchor: { type: "string" },
18160
+ tooltip: { type: "string" }
17279
18161
  }
17280
18162
  },
17281
18163
  ImageRun: {
@@ -17304,6 +18186,40 @@ var init_schema = __esm(() => {
17304
18186
  required: ["type"],
17305
18187
  properties: { type: { const: "tab" } }
17306
18188
  },
18189
+ EquationRun: {
18190
+ type: "object",
18191
+ required: ["type", "text", "display"],
18192
+ properties: {
18193
+ type: { const: "equation" },
18194
+ text: { type: "string" },
18195
+ display: { type: "boolean" }
18196
+ }
18197
+ },
18198
+ FootnoteRefRun: {
18199
+ type: "object",
18200
+ required: ["type", "kind", "id"],
18201
+ properties: {
18202
+ type: { const: "footnoteRef" },
18203
+ kind: { enum: ["footnote", "endnote"] },
18204
+ id: { type: "string" }
18205
+ }
18206
+ },
18207
+ ChartRun: {
18208
+ type: "object",
18209
+ required: ["type", "kind"],
18210
+ properties: {
18211
+ type: { const: "chart" },
18212
+ kind: { enum: ["chart", "shape", "smartart", "drawing"] }
18213
+ }
18214
+ },
18215
+ Footnote: {
18216
+ type: "object",
18217
+ required: ["id", "text"],
18218
+ properties: {
18219
+ id: { type: "string" },
18220
+ text: { type: "string" }
18221
+ }
18222
+ },
17307
18223
  Table: {
17308
18224
  type: "object",
17309
18225
  required: ["id", "type", "rows"],
@@ -17370,13 +18286,13 @@ var init_schema = __esm(() => {
17370
18286
  // src/cli/info/locators.ts
17371
18287
  var exports_locators = {};
17372
18288
  __export(exports_locators, {
17373
- run: () => run16
18289
+ run: () => run21
17374
18290
  });
17375
- import { parseArgs as parseArgs14 } from "util";
17376
- async function run16(args) {
18291
+ import { parseArgs as parseArgs18 } from "util";
18292
+ async function run21(args) {
17377
18293
  let parsed;
17378
18294
  try {
17379
- parsed = parseArgs14({
18295
+ parsed = parseArgs18({
17380
18296
  args,
17381
18297
  allowPositionals: true,
17382
18298
  options: {
@@ -17386,10 +18302,10 @@ async function run16(args) {
17386
18302
  });
17387
18303
  } catch (parseError) {
17388
18304
  const message = parseError instanceof Error ? parseError.message : String(parseError);
17389
- return fail("USAGE", message, HELP16);
18305
+ return fail("USAGE", message, HELP21);
17390
18306
  }
17391
18307
  if (parsed.values.help) {
17392
- await writeStdout(HELP16);
18308
+ await writeStdout(HELP21);
17393
18309
  return EXIT.OK;
17394
18310
  }
17395
18311
  if (parsed.values.json) {
@@ -17400,7 +18316,7 @@ async function run16(args) {
17400
18316
  await writeStdout(REFERENCE);
17401
18317
  return EXIT.OK;
17402
18318
  }
17403
- var HELP16 = `docx locators \u2014 print the locator grammar reference
18319
+ var HELP21 = `docx locators \u2014 print the locator grammar reference
17404
18320
 
17405
18321
  Usage:
17406
18322
  docx locators [options]
@@ -17426,6 +18342,7 @@ Range locators (across blocks):
17426
18342
  Entity locators:
17427
18343
  cN Comment id (e.g., c0)
17428
18344
  imgN Image id (e.g., img2)
18345
+ linkN Hyperlink id (e.g., link0)
17429
18346
  tN:rRcC Cell at row R, column C of table tN
17430
18347
 
17431
18348
  Examples:
@@ -17434,6 +18351,7 @@ Examples:
17434
18351
  p3:5-p5:10 -> from char 5 of p3 to char 10 of p5
17435
18352
  c1 -> comment c1
17436
18353
  img0 -> image img0
18354
+ link0 -> hyperlink link0
17437
18355
  t0:r1c2 -> cell at row 1, col 2 of table t0
17438
18356
  t0:r1c2:p0 -> first paragraph of that cell
17439
18357
  t0:r1c2:p0:5-10 -> chars 5..10 of that paragraph
@@ -17466,6 +18384,7 @@ var init_locators2 = __esm(() => {
17466
18384
  entityLocators: {
17467
18385
  comment: { syntax: "cN", example: "c1" },
17468
18386
  image: { syntax: "imgN", example: "img0" },
18387
+ hyperlink: { syntax: "linkN", example: "link0" },
17469
18388
  cell: { syntax: "tN:rRcC", example: "t0:r1c2" },
17470
18389
  nestedCell: { syntax: "tN:rRcC:pK", example: "t0:r1c2:p0" }
17471
18390
  },
@@ -17480,22 +18399,22 @@ var init_locators2 = __esm(() => {
17480
18399
  // src/cli/info/index.ts
17481
18400
  var exports_info = {};
17482
18401
  __export(exports_info, {
17483
- run: () => run17
18402
+ run: () => run22
17484
18403
  });
17485
- async function run17(args) {
18404
+ async function run22(args) {
17486
18405
  const topic = args[0];
17487
18406
  if (!topic || topic === "--help" || topic === "-h" || topic === "help") {
17488
- await writeStdout(HELP17);
18407
+ await writeStdout(HELP22);
17489
18408
  return topic ? 0 : 2;
17490
18409
  }
17491
- const loader = SUBCOMMANDS3[topic];
18410
+ const loader = SUBCOMMANDS4[topic];
17492
18411
  if (!loader) {
17493
18412
  return fail("USAGE", `Unknown info topic: ${topic}`, 'Run "docx info --help".');
17494
18413
  }
17495
18414
  const module_ = await loader();
17496
18415
  return module_.run(args.slice(1));
17497
18416
  }
17498
- var SUBCOMMANDS3, HELP17 = `docx info \u2014 print reference material about the CLI
18417
+ var SUBCOMMANDS4, HELP22 = `docx info \u2014 print reference material about the CLI
17499
18418
 
17500
18419
  Usage:
17501
18420
  docx info <topic> [options]
@@ -17508,7 +18427,7 @@ Run "docx info <topic> --help" for topic-specific help.
17508
18427
  `;
17509
18428
  var init_info = __esm(() => {
17510
18429
  init_respond();
17511
- SUBCOMMANDS3 = {
18430
+ SUBCOMMANDS4 = {
17512
18431
  schema: () => Promise.resolve().then(() => (init_schema(), exports_schema)),
17513
18432
  locators: () => Promise.resolve().then(() => (init_locators2(), exports_locators))
17514
18433
  };
@@ -17517,13 +18436,13 @@ var init_info = __esm(() => {
17517
18436
  // src/cli/insert/index.tsx
17518
18437
  var exports_insert = {};
17519
18438
  __export(exports_insert, {
17520
- run: () => run18
18439
+ run: () => run23
17521
18440
  });
17522
- import { parseArgs as parseArgs15 } from "util";
17523
- async function run18(args) {
18441
+ import { parseArgs as parseArgs19 } from "util";
18442
+ async function run23(args) {
17524
18443
  let parsed;
17525
18444
  try {
17526
- parsed = parseArgs15({
18445
+ parsed = parseArgs19({
17527
18446
  args,
17528
18447
  allowPositionals: true,
17529
18448
  options: {
@@ -17536,6 +18455,7 @@ async function run18(args) {
17536
18455
  color: { type: "string" },
17537
18456
  bold: { type: "boolean" },
17538
18457
  italic: { type: "boolean" },
18458
+ url: { type: "string" },
17539
18459
  output: { type: "string", short: "o" },
17540
18460
  "dry-run": { type: "boolean" },
17541
18461
  help: { type: "boolean", short: "h" }
@@ -17543,30 +18463,34 @@ async function run18(args) {
17543
18463
  });
17544
18464
  } catch (parseError) {
17545
18465
  const message = parseError instanceof Error ? parseError.message : String(parseError);
17546
- return fail("USAGE", message, HELP18);
18466
+ return fail("USAGE", message, HELP23);
17547
18467
  }
17548
18468
  if (parsed.values.help) {
17549
- await writeStdout(HELP18);
18469
+ await writeStdout(HELP23);
17550
18470
  return EXIT.OK;
17551
18471
  }
17552
18472
  const path = parsed.positionals[0];
17553
18473
  if (!path)
17554
- return fail("USAGE", "Missing FILE argument", HELP18);
18474
+ return fail("USAGE", "Missing FILE argument", HELP23);
17555
18475
  const after = parsed.values.after;
17556
18476
  const before = parsed.values.before;
17557
18477
  if (!after && !before) {
17558
- return fail("USAGE", "Missing locator: pass --after or --before", HELP18);
18478
+ return fail("USAGE", "Missing locator: pass --after or --before", HELP23);
17559
18479
  }
17560
18480
  if (after && before) {
17561
- return fail("USAGE", "Pass either --after or --before, not both", HELP18);
18481
+ return fail("USAGE", "Pass either --after or --before, not both", HELP23);
17562
18482
  }
17563
18483
  const text = parsed.values.text;
17564
18484
  const runsJson = parsed.values.runs;
18485
+ const url = parsed.values.url;
17565
18486
  if (!text && !runsJson) {
17566
- return fail("USAGE", "Missing content: pass --text or --runs", HELP18);
18487
+ return fail("USAGE", "Missing content: pass --text or --runs", HELP23);
17567
18488
  }
17568
18489
  if (text && runsJson) {
17569
- return fail("USAGE", "Pass either --text or --runs, not both", HELP18);
18490
+ return fail("USAGE", "Pass either --text or --runs, not both", HELP23);
18491
+ }
18492
+ if (url && !text) {
18493
+ return fail("USAGE", "--url requires --text", HELP23);
17570
18494
  }
17571
18495
  const paragraphOptions = {};
17572
18496
  const styleValue = parsed.values.style;
@@ -17596,6 +18520,8 @@ async function run18(args) {
17596
18520
  ...parsed.values.bold ? { bold: true } : {},
17597
18521
  ...parsed.values.italic ? { italic: true } : {}
17598
18522
  }, undefined, false, undefined, this);
18523
+ if (url)
18524
+ wrapFirstRunInHyperlink(view, paragraphNode, url);
17599
18525
  } else {
17600
18526
  let runsValue;
17601
18527
  try {
@@ -17644,6 +18570,29 @@ async function run18(args) {
17644
18570
  });
17645
18571
  return EXIT.OK;
17646
18572
  }
18573
+ function wrapFirstRunInHyperlink(view, paragraph, url) {
18574
+ const relationships = XmlNode2.findRoot(view.relationshipsTree, "Relationships");
18575
+ if (!relationships) {
18576
+ throw new Error("Missing <Relationships> root in document rels");
18577
+ }
18578
+ const relationshipId = addHyperlinkRelationship(relationships, url);
18579
+ view.hyperlinksByRelationshipId.set(relationshipId, { url });
18580
+ const newChildren = [];
18581
+ let wrapped = false;
18582
+ for (const child of paragraph.children) {
18583
+ if (!wrapped && child.tag === "w:r") {
18584
+ const wrapper = /* @__PURE__ */ jsxDEV(w.hyperlink, {
18585
+ "r:id": relationshipId,
18586
+ children: child
18587
+ }, undefined, false, undefined, this);
18588
+ newChildren.push(wrapper);
18589
+ wrapped = true;
18590
+ continue;
18591
+ }
18592
+ newChildren.push(child);
18593
+ }
18594
+ paragraph.children = newChildren;
18595
+ }
17647
18596
  function applyTrackedInsertion(paragraph, view) {
17648
18597
  const allocator = createRevisionAllocator(view);
17649
18598
  const baseMeta = { author: resolveAuthor(), date: resolveDate() };
@@ -17674,7 +18623,7 @@ function applyTrackedInsertion(paragraph, view) {
17674
18623
  paragraph.children = newChildren;
17675
18624
  markParagraphMarkAs(paragraph, "ins", mintMeta());
17676
18625
  }
17677
- var HELP18 = `docx insert \u2014 insert a paragraph at a locator
18626
+ var HELP23 = `docx insert \u2014 insert a paragraph at a locator
17678
18627
 
17679
18628
  Usage:
17680
18629
  docx insert FILE [options]
@@ -17695,6 +18644,7 @@ Run options (only with --text):
17695
18644
  --color HEX Run color, hex (e.g., 800080 for purple)
17696
18645
  --bold Bold
17697
18646
  --italic Italic
18647
+ --url URL Wrap the inserted text in a hyperlink to URL
17698
18648
 
17699
18649
  -o, --output PATH Write to PATH instead of overwriting FILE
17700
18650
  --dry-run Print what would be inserted; do not write the file
@@ -17704,9 +18654,12 @@ Examples:
17704
18654
  docx insert doc.docx --after p3 --text "Section header" --style Heading2
17705
18655
  docx insert doc.docx --before p0 --text "ALERT" --color CC0000 --bold
17706
18656
  docx insert doc.docx --after p2 --runs '[{"type":"text","text":"X","bold":true}]'
18657
+ docx insert doc.docx --after p3 --text "click here" --url https://example.com
17707
18658
  `;
17708
18659
  var init_insert = __esm(() => {
17709
18660
  init_core();
18661
+ init_jsx();
18662
+ init_parser();
17710
18663
  init_respond();
17711
18664
  init_emit2();
17712
18665
  init_jsx_dev_runtime();
@@ -17744,159 +18697,620 @@ function buildOutline(doc, options = {}) {
17744
18697
  }
17745
18698
  return root.children;
17746
18699
  }
17747
- function headingLevel(style, stylePrefix) {
17748
- if (!style)
18700
+ function headingLevel(style, stylePrefix) {
18701
+ if (!style)
18702
+ return null;
18703
+ if (!style.startsWith(stylePrefix))
18704
+ return null;
18705
+ const remainder = style.slice(stylePrefix.length).trim();
18706
+ if (remainder === "")
18707
+ return 1;
18708
+ const parsed = Number(remainder);
18709
+ if (!Number.isInteger(parsed) || parsed < 1)
18710
+ return null;
18711
+ return parsed;
18712
+ }
18713
+ function* headingParagraphs(blocks, stylePrefix) {
18714
+ for (const block of blocks) {
18715
+ if (block.type !== "paragraph")
18716
+ continue;
18717
+ if (headingLevel(block.style, stylePrefix) === null)
18718
+ continue;
18719
+ yield block;
18720
+ }
18721
+ }
18722
+ var init_build = __esm(() => {
18723
+ init_core();
18724
+ });
18725
+
18726
+ // src/cli/outline/index.ts
18727
+ var exports_outline = {};
18728
+ __export(exports_outline, {
18729
+ run: () => run24
18730
+ });
18731
+ import { parseArgs as parseArgs20 } from "util";
18732
+ async function run24(args) {
18733
+ let parsed;
18734
+ try {
18735
+ parsed = parseArgs20({
18736
+ args,
18737
+ allowPositionals: true,
18738
+ options: {
18739
+ "style-prefix": { type: "string" },
18740
+ help: { type: "boolean", short: "h" }
18741
+ }
18742
+ });
18743
+ } catch (parseError) {
18744
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
18745
+ return fail("USAGE", message, HELP24);
18746
+ }
18747
+ if (parsed.values.help) {
18748
+ await writeStdout(HELP24);
18749
+ return EXIT.OK;
18750
+ }
18751
+ const path = parsed.positionals[0];
18752
+ if (!path)
18753
+ return fail("USAGE", "Missing FILE argument", HELP24);
18754
+ const stylePrefix = parsed.values["style-prefix"] ?? "Heading";
18755
+ if (stylePrefix.length === 0) {
18756
+ return fail("USAGE", "--style-prefix cannot be empty");
18757
+ }
18758
+ const view = await openOrFail(path);
18759
+ if (typeof view === "number")
18760
+ return view;
18761
+ const outline = buildOutline(view.doc, { stylePrefix });
18762
+ await respond({
18763
+ ok: true,
18764
+ operation: "outline",
18765
+ path,
18766
+ stylePrefix,
18767
+ outline
18768
+ });
18769
+ return EXIT.OK;
18770
+ }
18771
+ var HELP24 = `docx outline \u2014 list headings as a hierarchical tree
18772
+
18773
+ Usage:
18774
+ docx outline FILE [options]
18775
+
18776
+ Options:
18777
+ --style-prefix S paragraph-style prefix that marks a heading (default: "Heading")
18778
+ -h, --help show this help
18779
+
18780
+ Walks top-level paragraphs whose style starts with the prefix and parses the
18781
+ trailing number as the heading level (e.g. "Heading1" \u2192 1, "Heading 2" \u2192 2).
18782
+ Paragraphs nested inside table cells are skipped \u2014 outlines reflect the
18783
+ document's structural skeleton, not embedded labels. Lower levels nest under
18784
+ higher ones; missing intermediate levels nest directly (H1 \u2192 H3 is fine).
18785
+
18786
+ Output is JSON: an array of entries, each shaped like
18787
+ { id, locator, level, style, text, children }.
18788
+
18789
+ Examples:
18790
+ docx outline doc.docx
18791
+ docx outline doc.docx --style-prefix "Section"
18792
+ docx outline doc.docx | jq '.outline[].text'
18793
+ `;
18794
+ var init_outline = __esm(() => {
18795
+ init_respond();
18796
+ init_build();
18797
+ });
18798
+
18799
+ // src/cli/read/markdown.ts
18800
+ function renderMarkdown(doc, options = {}) {
18801
+ const blocks = sliceBlocks(doc.blocks, options.from, options.to);
18802
+ const commentIndex = options.showComments ? buildCommentIndex(blocks, options) : emptyCommentIndex();
18803
+ const ctx = {
18804
+ options,
18805
+ commentIndex,
18806
+ referencedFootnoteIds: new Set,
18807
+ referencedEndnoteIds: new Set
18808
+ };
18809
+ const parts = [];
18810
+ for (const block of blocks) {
18811
+ const rendered = renderBlock(block, ctx);
18812
+ if (rendered !== null)
18813
+ parts.push(rendered);
18814
+ }
18815
+ const definitions = [];
18816
+ if (options.showComments) {
18817
+ const commentFootnotes = renderCommentFootnotes(commentIndex, doc.comments);
18818
+ if (commentFootnotes.length > 0)
18819
+ definitions.push(commentFootnotes);
18820
+ }
18821
+ const footnoteDefs = renderNoteDefinitions(doc.footnotes, ctx.referencedFootnoteIds);
18822
+ if (footnoteDefs.length > 0)
18823
+ definitions.push(footnoteDefs);
18824
+ const endnoteDefs = renderNoteDefinitions(doc.endnotes, ctx.referencedEndnoteIds);
18825
+ if (endnoteDefs.length > 0)
18826
+ definitions.push(endnoteDefs);
18827
+ if (definitions.length > 0)
18828
+ parts.push(definitions.join(`
18829
+ `));
18830
+ if (parts.length === 0)
18831
+ return "";
18832
+ return `${parts.join(`
18833
+
18834
+ `)}
18835
+ `;
18836
+ }
18837
+ function emptyCommentIndex() {
18838
+ return {
18839
+ endingsByRun: new Map,
18840
+ spanText: new Map,
18841
+ orderedIds: []
18842
+ };
18843
+ }
18844
+ function buildCommentIndex(blocks, options) {
18845
+ const showChanges = options.showChanges ?? false;
18846
+ const lastSlot = new Map;
18847
+ const spanText = new Map;
18848
+ const orderedIds = [];
18849
+ for (const paragraph of flattenParagraphs(blocks)) {
18850
+ paragraph.runs.forEach((run25, index) => {
18851
+ if (run25.type !== "text")
18852
+ return;
18853
+ if (!showChanges && run25.trackedChange?.kind === "del")
18854
+ return;
18855
+ for (const commentId of run25.comments ?? []) {
18856
+ if (!spanText.has(commentId))
18857
+ orderedIds.push(commentId);
18858
+ spanText.set(commentId, (spanText.get(commentId) ?? "") + run25.text);
18859
+ lastSlot.set(commentId, slotKey(paragraph.id, index));
18860
+ }
18861
+ });
18862
+ }
18863
+ const endingsByRun = new Map;
18864
+ for (const commentId of orderedIds) {
18865
+ const slot = lastSlot.get(commentId);
18866
+ if (!slot)
18867
+ continue;
18868
+ const list = endingsByRun.get(slot) ?? [];
18869
+ list.push(commentId);
18870
+ endingsByRun.set(slot, list);
18871
+ }
18872
+ return { endingsByRun, spanText, orderedIds };
18873
+ }
18874
+ function slotKey(paragraphId, runIndex) {
18875
+ return `${paragraphId}#${runIndex}`;
18876
+ }
18877
+ function renderBlock(block, ctx) {
18878
+ if (block.type === "paragraph")
18879
+ return renderParagraph(block, ctx);
18880
+ if (block.type === "table")
18881
+ return renderTable(block, ctx);
18882
+ if (block.type === "sectionBreak")
18883
+ return `--- <!-- ${block.id} -->`;
18884
+ return null;
18885
+ }
18886
+ function renderParagraph(paragraph, ctx) {
18887
+ const body = renderRuns(paragraph.id, paragraph.runs, ctx);
18888
+ if (body.length === 0)
18889
+ return null;
18890
+ const prefix = paragraphPrefix(paragraph);
18891
+ return `${prefix}${body} <!-- ${paragraph.id} -->`;
18892
+ }
18893
+ function paragraphPrefix(paragraph) {
18894
+ const headingLevel2 = headingLevelFor(paragraph.style);
18895
+ if (headingLevel2 !== null)
18896
+ return `${"#".repeat(headingLevel2)} `;
18897
+ if (paragraph.list) {
18898
+ const indent = " ".repeat(paragraph.list.level);
18899
+ return `${indent}- `;
18900
+ }
18901
+ return "";
18902
+ }
18903
+ function headingLevelFor(style) {
18904
+ if (!style)
18905
+ return null;
18906
+ if (!style.startsWith("Heading"))
18907
+ return null;
18908
+ const remainder = style.slice("Heading".length).trim();
18909
+ if (remainder === "")
18910
+ return 1;
18911
+ const parsed = Number(remainder);
18912
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 6)
18913
+ return null;
18914
+ return parsed;
18915
+ }
18916
+ function renderRuns(paragraphId, runs, ctx) {
18917
+ const showChanges = ctx.options.showChanges ?? false;
18918
+ const visibleEntries = [];
18919
+ runs.forEach((run25, index) => {
18920
+ if (!showChanges && run25.type === "text" && run25.trackedChange?.kind === "del") {
18921
+ return;
18922
+ }
18923
+ visibleEntries.push({ run: run25, originalIndex: index });
18924
+ });
18925
+ let out = "";
18926
+ let cursor = 0;
18927
+ while (cursor < visibleEntries.length) {
18928
+ const entry = visibleEntries[cursor];
18929
+ if (!entry) {
18930
+ cursor++;
18931
+ continue;
18932
+ }
18933
+ const { run: run25 } = entry;
18934
+ if (run25.type === "text") {
18935
+ let lookahead = cursor + 1;
18936
+ while (lookahead < visibleEntries.length) {
18937
+ const next = visibleEntries[lookahead];
18938
+ if (!next || next.run.type !== "text")
18939
+ break;
18940
+ if (!sameDecoration(run25, next.run))
18941
+ break;
18942
+ lookahead++;
18943
+ }
18944
+ const segment = visibleEntries.slice(cursor, lookahead);
18945
+ out += renderTextSegment(segment.map((entry2) => entry2.run), showChanges);
18946
+ out += commentEndingsFor(paragraphId, segment, ctx.commentIndex);
18947
+ cursor = lookahead;
18948
+ continue;
18949
+ }
18950
+ if (run25.type === "image") {
18951
+ const alt = sanitizeAltText(run25.alt ?? run25.id);
18952
+ out += `![${alt}](${run25.id})`;
18953
+ } else if (run25.type === "break") {
18954
+ if (run25.kind === "line")
18955
+ out += "<br>";
18956
+ } else if (run25.type === "tab") {
18957
+ out += "\t";
18958
+ } else if (run25.type === "equation") {
18959
+ const escaped = run25.text.replace(/`/g, "\\`");
18960
+ out += `\`equation: ${escaped}\``;
18961
+ } else if (run25.type === "footnoteRef") {
18962
+ if (run25.kind === "footnote")
18963
+ ctx.referencedFootnoteIds.add(run25.id);
18964
+ else
18965
+ ctx.referencedEndnoteIds.add(run25.id);
18966
+ out += `[^${run25.id}]`;
18967
+ } else if (run25.type === "chart") {
18968
+ out += `\`[${run25.kind}]\``;
18969
+ }
18970
+ cursor++;
18971
+ }
18972
+ return out;
18973
+ }
18974
+ function commentEndingsFor(paragraphId, segment, commentIndex) {
18975
+ if (commentIndex.endingsByRun.size === 0)
18976
+ return "";
18977
+ let out = "";
18978
+ for (const entry of segment) {
18979
+ const ids = commentIndex.endingsByRun.get(slotKey(paragraphId, entry.originalIndex));
18980
+ if (!ids)
18981
+ continue;
18982
+ for (const commentId of ids)
18983
+ out += `[^${commentId}]`;
18984
+ }
18985
+ return out;
18986
+ }
18987
+ function sameDecoration(a2, b) {
18988
+ return (a2.bold ?? false) === (b.bold ?? false) && (a2.italic ?? false) === (b.italic ?? false) && (a2.strike ?? false) === (b.strike ?? false) && (a2.underline ?? "") === (b.underline ?? "") && (a2.color ?? "") === (b.color ?? "") && (a2.highlight ?? "") === (b.highlight ?? "") && a2.hyperlink?.id === b.hyperlink?.id && a2.trackedChange?.kind === b.trackedChange?.kind && sameCommentSet(a2.comments, b.comments);
18989
+ }
18990
+ function sameCommentSet(left, right) {
18991
+ const a2 = left ?? [];
18992
+ const b = right ?? [];
18993
+ if (a2.length !== b.length)
18994
+ return false;
18995
+ for (let i = 0;i < a2.length; i++) {
18996
+ if (a2[i] !== b[i])
18997
+ return false;
18998
+ }
18999
+ return true;
19000
+ }
19001
+ function renderTextSegment(runs, showChanges) {
19002
+ const text = runs.map((run25) => run25.text).join("");
19003
+ if (text.length === 0)
19004
+ return "";
19005
+ const first = runs[0];
19006
+ if (!first)
19007
+ return "";
19008
+ let out = text;
19009
+ if (first.bold)
19010
+ out = `**${out}**`;
19011
+ if (first.italic)
19012
+ out = `*${out}*`;
19013
+ if (first.strike)
19014
+ out = `~~${out}~~`;
19015
+ if (first.underline)
19016
+ out = `<u>${out}</u>`;
19017
+ const color = colorAttrFor(first.color);
19018
+ if (color)
19019
+ out = `<span style="color:${color}">${out}</span>`;
19020
+ const highlight = highlightCssFor(first.highlight);
19021
+ if (highlight)
19022
+ out = `<span style="background-color:${highlight}">${out}</span>`;
19023
+ if (first.hyperlink) {
19024
+ const target = first.hyperlink.url ?? `#${first.hyperlink.anchor ?? ""}`;
19025
+ out = `[${out}](${target})`;
19026
+ }
19027
+ if (showChanges && first.trackedChange) {
19028
+ out = `<${first.trackedChange.kind}>${out}</${first.trackedChange.kind}>`;
19029
+ }
19030
+ return out;
19031
+ }
19032
+ function colorAttrFor(value) {
19033
+ if (!value)
17749
19034
  return null;
17750
- if (!style.startsWith(stylePrefix))
19035
+ const lowered = value.toLowerCase();
19036
+ if (lowered === "auto")
17751
19037
  return null;
17752
- const remainder = style.slice(stylePrefix.length).trim();
17753
- if (remainder === "")
17754
- return 1;
17755
- const parsed = Number(remainder);
17756
- if (!Number.isInteger(parsed) || parsed < 1)
19038
+ if (/^[0-9a-f]{6}$/.test(lowered))
19039
+ return `#${lowered}`;
19040
+ return value;
19041
+ }
19042
+ function highlightCssFor(value) {
19043
+ if (!value)
17757
19044
  return null;
17758
- return parsed;
19045
+ if (value === "none")
19046
+ return null;
19047
+ return value.replace(/[A-Z]/g, (letter) => letter.toLowerCase());
17759
19048
  }
17760
- function* headingParagraphs(blocks, stylePrefix) {
17761
- for (const block of blocks) {
17762
- if (block.type !== "paragraph")
19049
+ function renderTable(table, ctx) {
19050
+ if (table.rows.length === 0)
19051
+ return null;
19052
+ const colCount = Math.max(...table.rows.map((row) => row.cells.length));
19053
+ if (colCount === 0)
19054
+ return null;
19055
+ const renderedRows = table.rows.map((row) => {
19056
+ const cells = [];
19057
+ for (let columnIndex = 0;columnIndex < colCount; columnIndex++) {
19058
+ const cell = row.cells[columnIndex];
19059
+ cells.push(cell ? renderCell(cell, ctx) : "");
19060
+ }
19061
+ return cells;
19062
+ });
19063
+ const lines = [];
19064
+ const headerRow = renderedRows[0];
19065
+ if (!headerRow)
19066
+ return null;
19067
+ lines.push(rowToLine(headerRow));
19068
+ lines.push(`| ${Array(colCount).fill("---").join(" | ")} |`);
19069
+ for (let rowIndex = 1;rowIndex < renderedRows.length; rowIndex++) {
19070
+ const row = renderedRows[rowIndex];
19071
+ if (row)
19072
+ lines.push(rowToLine(row));
19073
+ }
19074
+ return lines.join(`
19075
+ `);
19076
+ }
19077
+ function rowToLine(cells) {
19078
+ return `| ${cells.join(" | ")} |`;
19079
+ }
19080
+ function renderCell(cell, ctx) {
19081
+ const parts = [];
19082
+ for (const block of cell.blocks) {
19083
+ if (block.type === "paragraph") {
19084
+ const body = renderRuns(block.id, block.runs, ctx);
19085
+ if (body.length === 0)
19086
+ continue;
19087
+ parts.push(`${body} <!-- ${block.id} -->`);
17763
19088
  continue;
17764
- if (headingLevel(block.style, stylePrefix) === null)
19089
+ }
19090
+ if (block.type === "table") {
19091
+ parts.push(renderNestedTable(block, ctx));
19092
+ }
19093
+ }
19094
+ return escapeCell(parts.join("<br>"));
19095
+ }
19096
+ function renderNestedTable(table, ctx) {
19097
+ const rows = table.rows.map((row) => row.cells.map((cell) => renderCell(cell, ctx)).join(" / "));
19098
+ return rows.join(" // ");
19099
+ }
19100
+ function escapeCell(text) {
19101
+ return text.replace(/\|/g, "\\|");
19102
+ }
19103
+ function sanitizeAltText(text) {
19104
+ return text.replace(/[\r\n]+/g, " ").replace(/\]/g, "\\]");
19105
+ }
19106
+ function renderCommentFootnotes(commentIndex, comments) {
19107
+ if (commentIndex.orderedIds.length === 0)
19108
+ return "";
19109
+ const byId = new Map(comments.map((comment) => [comment.id, comment]));
19110
+ const sorted = [...commentIndex.orderedIds].sort(commentIdCompare);
19111
+ const lines = [];
19112
+ for (const commentId of sorted) {
19113
+ const comment = byId.get(commentId);
19114
+ if (!comment)
17765
19115
  continue;
17766
- yield block;
19116
+ const span = commentIndex.spanText.get(commentId) ?? "";
19117
+ lines.push(formatFootnote(comment, span));
17767
19118
  }
19119
+ return lines.join(`
19120
+ `);
17768
19121
  }
17769
- function paragraphText(paragraph) {
17770
- let out = "";
17771
- for (const run19 of paragraph.runs) {
17772
- if (run19.type === "text")
17773
- out += run19.text;
19122
+ function renderNoteDefinitions(notes, referenced) {
19123
+ if (referenced.size === 0)
19124
+ return "";
19125
+ const byId = new Map(notes.map((note) => [note.id, note]));
19126
+ const sorted = [...referenced].sort(noteIdCompare);
19127
+ const lines = [];
19128
+ for (const id of sorted) {
19129
+ const note = byId.get(id);
19130
+ const body = (note?.text ?? "").replace(/\s+/g, " ").trim();
19131
+ lines.push(`[^${id}]: ${body}`);
19132
+ }
19133
+ return lines.join(`
19134
+ `);
19135
+ }
19136
+ function noteIdCompare(left, right) {
19137
+ return numericIdCompare(left, right, /(\d+)$/);
19138
+ }
19139
+ function commentIdCompare(left, right) {
19140
+ return numericIdCompare(left, right, /^c(\d+)$/);
19141
+ }
19142
+ function numericIdCompare(left, right, pattern) {
19143
+ const leftMatch = left.match(pattern);
19144
+ const rightMatch = right.match(pattern);
19145
+ if (leftMatch?.[1] && rightMatch?.[1]) {
19146
+ return Number(leftMatch[1]) - Number(rightMatch[1]);
17774
19147
  }
17775
- return out;
19148
+ return left.localeCompare(right);
17776
19149
  }
17777
-
17778
- // src/cli/outline/index.ts
17779
- var exports_outline = {};
17780
- __export(exports_outline, {
17781
- run: () => run19
17782
- });
17783
- import { parseArgs as parseArgs16 } from "util";
17784
- async function run19(args) {
19150
+ function formatFootnote(comment, spanText) {
19151
+ const quoted = quoteSpan(spanText);
19152
+ const reply = comment.parentId ? ` \u21B3 ${comment.parentId}` : "";
19153
+ const resolved = comment.resolved ? "\u2713 " : "";
19154
+ const body = comment.text.replace(/\s+/g, " ").trim();
19155
+ return `[^${comment.id}]: ${quoted} \u2014 ${resolved}${comment.author} (${comment.date})${reply}: ${body}`;
19156
+ }
19157
+ function quoteSpan(text) {
19158
+ const collapsed = text.replace(/\s+/g, " ").trim();
19159
+ const escaped = collapsed.replace(/"/g, "\\\"");
19160
+ return `"${escaped}"`;
19161
+ }
19162
+ function sliceBlocks(blocks, from, to) {
19163
+ if (!from && !to)
19164
+ return blocks;
19165
+ const fromId = from ? blockIdForLocator(from, "from") : null;
19166
+ const toId = to ? blockIdForLocator(to, "to") : null;
19167
+ const fromIndex = fromId ? blocks.findIndex((b) => b.id === fromId) : 0;
19168
+ if (from && fromId && fromIndex === -1) {
19169
+ throw new MarkdownLocatorError(from, `--from ${from} not found at document top level`);
19170
+ }
19171
+ const toIndex = toId ? blocks.findIndex((b) => b.id === toId) : blocks.length - 1;
19172
+ if (to && toId && toIndex === -1) {
19173
+ throw new MarkdownLocatorError(to, `--to ${to} not found at document top level`);
19174
+ }
19175
+ if (toIndex < fromIndex)
19176
+ return [];
19177
+ return blocks.slice(fromIndex, toIndex + 1);
19178
+ }
19179
+ function blockIdForLocator(input, position) {
17785
19180
  let parsed;
17786
19181
  try {
17787
- parsed = parseArgs16({
17788
- args,
17789
- allowPositionals: true,
17790
- options: {
17791
- "style-prefix": { type: "string" },
17792
- help: { type: "boolean", short: "h" }
17793
- }
17794
- });
17795
- } catch (parseError) {
17796
- const message = parseError instanceof Error ? parseError.message : String(parseError);
17797
- return fail("USAGE", message, HELP19);
17798
- }
17799
- if (parsed.values.help) {
17800
- await writeStdout(HELP19);
17801
- return EXIT.OK;
19182
+ parsed = parseLocator(input);
19183
+ } catch (err) {
19184
+ if (err instanceof LocatorParseError) {
19185
+ throw new MarkdownLocatorError(input, err.message);
19186
+ }
19187
+ throw err;
17802
19188
  }
17803
- const path = parsed.positionals[0];
17804
- if (!path)
17805
- return fail("USAGE", "Missing FILE argument", HELP19);
17806
- const stylePrefix = parsed.values["style-prefix"] ?? "Heading";
17807
- if (stylePrefix.length === 0) {
17808
- return fail("USAGE", "--style-prefix cannot be empty");
19189
+ switch (parsed.kind) {
19190
+ case "block":
19191
+ return parsed.blockId;
19192
+ case "blockSpan":
19193
+ return parsed.blockId;
19194
+ case "range":
19195
+ return position === "from" ? parsed.start.blockId : parsed.end.blockId;
19196
+ case "cell":
19197
+ return parsed.tableId;
19198
+ case "comment":
19199
+ case "image":
19200
+ case "hyperlink":
19201
+ throw new MarkdownLocatorError(input, `--${position} does not accept a ${parsed.kind} locator \u2014 use a paragraph or table locator`);
17809
19202
  }
17810
- const view = await openOrFail(path);
17811
- if (typeof view === "number")
17812
- return view;
17813
- const outline = buildOutline(view.doc, { stylePrefix });
17814
- await respond({
17815
- ok: true,
17816
- operation: "outline",
17817
- path,
17818
- stylePrefix,
17819
- outline
17820
- });
17821
- return EXIT.OK;
17822
19203
  }
17823
- var HELP19 = `docx outline \u2014 list headings as a hierarchical tree
17824
-
17825
- Usage:
17826
- docx outline FILE [options]
17827
-
17828
- Options:
17829
- --style-prefix S paragraph-style prefix that marks a heading (default: "Heading")
17830
- -h, --help show this help
17831
-
17832
- Walks top-level paragraphs whose style starts with the prefix and parses the
17833
- trailing number as the heading level (e.g. "Heading1" \u2192 1, "Heading 2" \u2192 2).
17834
- Paragraphs nested inside table cells are skipped \u2014 outlines reflect the
17835
- document's structural skeleton, not embedded labels. Lower levels nest under
17836
- higher ones; missing intermediate levels nest directly (H1 \u2192 H3 is fine).
17837
-
17838
- Output is JSON: an array of entries, each shaped like
17839
- { id, locator, level, style, text, children }.
17840
-
17841
- Examples:
17842
- docx outline doc.docx
17843
- docx outline doc.docx --style-prefix "Section"
17844
- docx outline doc.docx | jq '.outline[].text'
17845
- `;
17846
- var init_outline = __esm(() => {
17847
- init_respond();
19204
+ var MarkdownLocatorError;
19205
+ var init_markdown = __esm(() => {
19206
+ init_core();
19207
+ MarkdownLocatorError = class MarkdownLocatorError extends Error {
19208
+ input;
19209
+ constructor(input, message) {
19210
+ super(message);
19211
+ this.input = input;
19212
+ this.name = "MarkdownLocatorError";
19213
+ }
19214
+ };
17848
19215
  });
17849
19216
 
17850
19217
  // src/cli/read/index.ts
17851
19218
  var exports_read = {};
17852
19219
  __export(exports_read, {
17853
- run: () => run20
19220
+ run: () => run25
17854
19221
  });
17855
- import { parseArgs as parseArgs17 } from "util";
17856
- async function run20(args) {
19222
+ import { parseArgs as parseArgs21 } from "util";
19223
+ async function run25(args) {
17857
19224
  let parsed;
17858
19225
  try {
17859
- parsed = parseArgs17({
19226
+ parsed = parseArgs21({
17860
19227
  args,
17861
19228
  allowPositionals: true,
17862
19229
  options: {
17863
- text: { type: "boolean" },
19230
+ markdown: { type: "boolean" },
19231
+ from: { type: "string" },
19232
+ to: { type: "string" },
19233
+ changes: { type: "boolean" },
19234
+ comments: { type: "boolean" },
17864
19235
  help: { type: "boolean", short: "h" }
17865
19236
  }
17866
19237
  });
17867
19238
  } catch (e) {
17868
- return fail("USAGE", e instanceof Error ? e.message : String(e), HELP20);
19239
+ return fail("USAGE", e instanceof Error ? e.message : String(e), HELP25);
17869
19240
  }
17870
19241
  if (parsed.values.help) {
17871
- await writeStdout(HELP20);
19242
+ await writeStdout(HELP25);
17872
19243
  return EXIT.OK;
17873
19244
  }
17874
19245
  const path = parsed.positionals[0];
17875
19246
  if (!path)
17876
- return fail("USAGE", "Missing FILE argument", HELP20);
19247
+ return fail("USAGE", "Missing FILE argument", HELP25);
19248
+ const markdown = Boolean(parsed.values.markdown);
19249
+ const from = parsed.values.from;
19250
+ const to = parsed.values.to;
19251
+ const showChanges = Boolean(parsed.values.changes);
19252
+ const showComments = Boolean(parsed.values.comments);
19253
+ if (!markdown && (from || to || showChanges || showComments)) {
19254
+ return fail("USAGE", "--from, --to, --changes, and --comments require --markdown", HELP25);
19255
+ }
17877
19256
  const view = await openOrFail(path);
17878
19257
  if (typeof view === "number")
17879
19258
  return view;
19259
+ if (markdown) {
19260
+ try {
19261
+ const rendered = renderMarkdown(view.doc, {
19262
+ from,
19263
+ to,
19264
+ showChanges,
19265
+ showComments
19266
+ });
19267
+ await writeStdout(rendered);
19268
+ return EXIT.OK;
19269
+ } catch (err) {
19270
+ if (err instanceof MarkdownLocatorError) {
19271
+ return fail("INVALID_LOCATOR", err.message);
19272
+ }
19273
+ throw err;
19274
+ }
19275
+ }
17880
19276
  await enrichImageHashes(view);
17881
19277
  await respond(view.doc);
17882
19278
  return EXIT.OK;
17883
19279
  }
17884
- var HELP20 = `docx read \u2014 print AST as JSON
19280
+ var HELP25 = `docx read \u2014 print AST as JSON, or render document body as Markdown
17885
19281
 
17886
19282
  Usage:
17887
19283
  docx read FILE [options]
17888
19284
 
17889
19285
  Options:
17890
- --text Render as human-readable text instead of JSON (NOT YET IMPLEMENTED)
17891
- -h, --help Show this help
19286
+ --markdown Render the body as GitHub-flavored Markdown
19287
+ (instead of JSON). Locators are emitted as
19288
+ <!-- pN --> HTML comments after each paragraph and
19289
+ inside cell content (invisible in rendered view,
19290
+ but parseable from raw markdown).
19291
+ --from LOC Start markdown rendering at top-level block LOC (inclusive)
19292
+ --to LOC End markdown rendering at top-level block LOC (inclusive)
19293
+ Accepts pN, tN, tN:rRcC[:pK[:S-E]], pN:S-E, pN:S-pM:E.
19294
+ Cell/span/range locators collapse to their enclosing
19295
+ top-level block (the table or paragraph).
19296
+ --changes With --markdown, render tracked insertions/deletions as
19297
+ <ins>/<del> instead of producing the accepted view.
19298
+ --comments With --markdown, append [^cN] after each commented span
19299
+ and emit a footnote definition for each comment at the
19300
+ end of the output (author, date, body).
19301
+ -h, --help Show this help
17892
19302
 
17893
19303
  Examples:
17894
19304
  docx read input.docx
19305
+ docx read input.docx --markdown
19306
+ docx read input.docx --markdown --from p3 --to p20
19307
+ docx read input.docx --markdown --changes --comments
17895
19308
  docx read input.docx | jq '.blocks[] | select(.type == "paragraph")'
17896
19309
  `;
17897
19310
  var init_read2 = __esm(() => {
17898
19311
  init_core();
17899
19312
  init_respond();
19313
+ init_markdown();
17900
19314
  });
17901
19315
 
17902
19316
  // src/cli/replace/replace-span.tsx
@@ -17939,7 +19353,7 @@ function collectRunSlots(paragraph) {
17939
19353
  offset += length;
17940
19354
  continue;
17941
19355
  }
17942
- if (child.tag === "w:ins" || child.tag === "w:del") {
19356
+ if (child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:hyperlink") {
17943
19357
  for (const inner of child.children) {
17944
19358
  if (inner.tag !== "w:r")
17945
19359
  continue;
@@ -17964,14 +19378,14 @@ function rebuildContainer(container, baseOffset, span, replacement, runPropertie
17964
19378
  if (placed)
17965
19379
  return;
17966
19380
  placed = true;
17967
- const run21 = /* @__PURE__ */ jsxDEV(ReplacementRun, {
19381
+ const run26 = /* @__PURE__ */ jsxDEV(ReplacementRun, {
17968
19382
  runProperties,
17969
19383
  text: replacement
17970
19384
  }, undefined, false, undefined, this);
17971
19385
  newChildren.push(tracked && isParagraph ? /* @__PURE__ */ jsxDEV(Ins, {
17972
19386
  meta: mintMeta(tracked),
17973
- children: run21
17974
- }, undefined, false, undefined, this) : run21);
19387
+ children: run26
19388
+ }, undefined, false, undefined, this) : run26);
17975
19389
  };
17976
19390
  for (const child of container.children) {
17977
19391
  if (child.tag === "w:r") {
@@ -18007,7 +19421,7 @@ function rebuildContainer(container, baseOffset, span, replacement, runPropertie
18007
19421
  }
18008
19422
  continue;
18009
19423
  }
18010
- if (isParagraph && (child.tag === "w:ins" || child.tag === "w:del")) {
19424
+ if (isParagraph && (child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:hyperlink")) {
18011
19425
  let innerLength = 0;
18012
19426
  for (const inner of child.children) {
18013
19427
  if (inner.tag === "w:r")
@@ -18031,14 +19445,14 @@ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tr
18031
19445
  if (placed)
18032
19446
  return;
18033
19447
  placed = true;
18034
- const run21 = /* @__PURE__ */ jsxDEV(ReplacementRun, {
19448
+ const run26 = /* @__PURE__ */ jsxDEV(ReplacementRun, {
18035
19449
  runProperties,
18036
19450
  text: replacement
18037
19451
  }, undefined, false, undefined, this);
18038
19452
  newChildren.push(tracked ? /* @__PURE__ */ jsxDEV(Ins, {
18039
19453
  meta: mintMeta(tracked),
18040
- children: run21
18041
- }, undefined, false, undefined, this) : run21);
19454
+ children: run26
19455
+ }, undefined, false, undefined, this) : run26);
18042
19456
  };
18043
19457
  for (const child of paragraph.children) {
18044
19458
  if (child.tag === "w:r") {
@@ -18075,7 +19489,7 @@ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tr
18075
19489
  continue;
18076
19490
  }
18077
19491
  if (child.tag === "w:ins" || child.tag === "w:del") {
18078
- const innerLength = sumInnerRunLengths(child);
19492
+ const innerLength = sumInnerRunLengths2(child);
18079
19493
  const wrapperStart = offset;
18080
19494
  const wrapperEnd = offset + innerLength;
18081
19495
  offset = wrapperEnd;
@@ -18091,6 +19505,25 @@ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tr
18091
19505
  splitWrapperAcrossSpan(child, wrapperStart, span, tracked, newChildren, placeReplacement);
18092
19506
  continue;
18093
19507
  }
19508
+ if (child.tag === "w:hyperlink") {
19509
+ const innerLength = sumInnerRunLengths2(child);
19510
+ const wrapperStart = offset;
19511
+ const wrapperEnd = offset + innerLength;
19512
+ offset = wrapperEnd;
19513
+ if (wrapperEnd <= span.start) {
19514
+ newChildren.push(child);
19515
+ continue;
19516
+ }
19517
+ if (wrapperStart >= span.end) {
19518
+ placeReplacement();
19519
+ newChildren.push(child);
19520
+ continue;
19521
+ }
19522
+ splitHyperlinkAcrossSpan(child, wrapperStart, span, runProperties, replacement, tracked, newChildren, placeReplacement, () => {
19523
+ placed = true;
19524
+ });
19525
+ continue;
19526
+ }
18094
19527
  newChildren.push(child);
18095
19528
  }
18096
19529
  if (!placed)
@@ -18151,7 +19584,62 @@ function splitWrapperAcrossSpan(wrapper, wrapperStart, span, tracked, out, place
18151
19584
  out.push(postWrapper);
18152
19585
  }
18153
19586
  }
18154
- function sumInnerRunLengths(wrapper) {
19587
+ function splitHyperlinkAcrossSpan(wrapper, wrapperStart, span, runProperties, replacement, tracked, out, placeReplacement, markReplacementPlaced) {
19588
+ const wrapperEnd = wrapperStart + wrapper.children.reduce((total, inner) => inner.tag === "w:r" ? total + runTextLength(inner) : total, 0);
19589
+ const startsInside = span.start > wrapperStart && span.start < wrapperEnd;
19590
+ const preInner = [];
19591
+ const postInner = [];
19592
+ let innerOffset = wrapperStart;
19593
+ for (const inner of wrapper.children) {
19594
+ if (inner.tag !== "w:r") {
19595
+ preInner.push(inner);
19596
+ continue;
19597
+ }
19598
+ const length = runTextLength(inner);
19599
+ const runStart = innerOffset;
19600
+ const runEnd = innerOffset + length;
19601
+ innerOffset = runEnd;
19602
+ if (runEnd <= span.start) {
19603
+ preInner.push(inner);
19604
+ continue;
19605
+ }
19606
+ if (runStart >= span.end) {
19607
+ postInner.push(inner);
19608
+ continue;
19609
+ }
19610
+ const sliceStartInRun = Math.max(0, span.start - runStart);
19611
+ const sliceEndInRun = Math.min(length, span.end - runStart);
19612
+ if (sliceStartInRun > 0)
19613
+ preInner.push(sliceRun(inner, 0, sliceStartInRun));
19614
+ if (sliceEndInRun < length) {
19615
+ postInner.push(sliceRun(inner, sliceEndInRun, length));
19616
+ }
19617
+ }
19618
+ if (startsInside) {
19619
+ const innerReplacement = /* @__PURE__ */ jsxDEV(ReplacementRun, {
19620
+ runProperties,
19621
+ text: replacement
19622
+ }, undefined, false, undefined, this);
19623
+ preInner.push(tracked ? /* @__PURE__ */ jsxDEV(Ins, {
19624
+ meta: mintMeta(tracked),
19625
+ children: innerReplacement
19626
+ }, undefined, false, undefined, this) : innerReplacement);
19627
+ markReplacementPlaced();
19628
+ }
19629
+ if (preInner.length > 0) {
19630
+ const preWrapper = new XmlNode2("w:hyperlink", { ...wrapper.attributes });
19631
+ preWrapper.children = preInner;
19632
+ out.push(preWrapper);
19633
+ }
19634
+ if (!startsInside)
19635
+ placeReplacement();
19636
+ if (postInner.length > 0) {
19637
+ const postWrapper = new XmlNode2("w:hyperlink", { ...wrapper.attributes });
19638
+ postWrapper.children = postInner;
19639
+ out.push(postWrapper);
19640
+ }
19641
+ }
19642
+ function sumInnerRunLengths2(wrapper) {
18155
19643
  let total = 0;
18156
19644
  for (const inner of wrapper.children) {
18157
19645
  if (inner.tag === "w:r")
@@ -18162,8 +19650,8 @@ function sumInnerRunLengths(wrapper) {
18162
19650
  function mintMeta(tracked) {
18163
19651
  return { ...tracked.meta, revisionId: tracked.allocator.next() };
18164
19652
  }
18165
- function convertRunTextToDelText(run21) {
18166
- for (const child of run21.children) {
19653
+ function convertRunTextToDelText(run26) {
19654
+ for (const child of run26.children) {
18167
19655
  if (child.tag === "w:t")
18168
19656
  child.tag = "w:delText";
18169
19657
  }
@@ -18190,15 +19678,15 @@ var init_replace_span = __esm(() => {
18190
19678
  });
18191
19679
 
18192
19680
  // src/cli/replace/index.ts
18193
- var exports_replace2 = {};
18194
- __export(exports_replace2, {
18195
- run: () => run21
19681
+ var exports_replace3 = {};
19682
+ __export(exports_replace3, {
19683
+ run: () => run26
18196
19684
  });
18197
- import { parseArgs as parseArgs18 } from "util";
18198
- async function run21(args) {
19685
+ import { parseArgs as parseArgs22 } from "util";
19686
+ async function run26(args) {
18199
19687
  let parsed;
18200
19688
  try {
18201
- parsed = parseArgs18({
19689
+ parsed = parseArgs22({
18202
19690
  args,
18203
19691
  allowPositionals: true,
18204
19692
  options: {
@@ -18213,21 +19701,21 @@ async function run21(args) {
18213
19701
  });
18214
19702
  } catch (parseError) {
18215
19703
  const message = parseError instanceof Error ? parseError.message : String(parseError);
18216
- return fail("USAGE", message, HELP21);
19704
+ return fail("USAGE", message, HELP26);
18217
19705
  }
18218
19706
  if (parsed.values.help) {
18219
- await writeStdout(HELP21);
19707
+ await writeStdout(HELP26);
18220
19708
  return EXIT.OK;
18221
19709
  }
18222
19710
  const path = parsed.positionals[0];
18223
19711
  const pattern = parsed.positionals[1];
18224
19712
  const replacement = parsed.positionals[2];
18225
19713
  if (!path)
18226
- return fail("USAGE", "Missing FILE argument", HELP21);
19714
+ return fail("USAGE", "Missing FILE argument", HELP26);
18227
19715
  if (pattern == null)
18228
- return fail("USAGE", "Missing PATTERN argument", HELP21);
19716
+ return fail("USAGE", "Missing PATTERN argument", HELP26);
18229
19717
  if (replacement == null) {
18230
- return fail("USAGE", "Missing REPLACEMENT argument", HELP21);
19718
+ return fail("USAGE", "Missing REPLACEMENT argument", HELP26);
18231
19719
  }
18232
19720
  const ignoreCase = Boolean(parsed.values["ignore-case"]);
18233
19721
  const useRegex = Boolean(parsed.values.regex);
@@ -18329,7 +19817,7 @@ async function run21(args) {
18329
19817
  });
18330
19818
  return EXIT.OK;
18331
19819
  }
18332
- var HELP21 = `docx replace \u2014 substitute text spans (sed for docx)
19820
+ var HELP26 = `docx replace \u2014 substitute text spans (sed for docx)
18333
19821
 
18334
19822
  Usage:
18335
19823
  docx replace FILE PATTERN REPLACEMENT [options]
@@ -18363,7 +19851,7 @@ Examples:
18363
19851
  docx replace doc.docx "(\\w+) (\\w+)" "$2 $1" --regex --all
18364
19852
  docx replace doc.docx "wordy phrase" "tighter phrase" --all --dry-run
18365
19853
  `;
18366
- var init_replace2 = __esm(() => {
19854
+ var init_replace3 = __esm(() => {
18367
19855
  init_core();
18368
19856
  init_respond();
18369
19857
  init_replace_span();
@@ -18372,13 +19860,13 @@ var init_replace2 = __esm(() => {
18372
19860
  // src/cli/track-changes/index.tsx
18373
19861
  var exports_track_changes = {};
18374
19862
  __export(exports_track_changes, {
18375
- run: () => run22
19863
+ run: () => run27
18376
19864
  });
18377
- import { parseArgs as parseArgs19 } from "util";
18378
- async function run22(args) {
19865
+ import { parseArgs as parseArgs23 } from "util";
19866
+ async function run27(args) {
18379
19867
  let parsed;
18380
19868
  try {
18381
- parsed = parseArgs19({
19869
+ parsed = parseArgs23({
18382
19870
  args,
18383
19871
  allowPositionals: true,
18384
19872
  options: {
@@ -18389,18 +19877,18 @@ async function run22(args) {
18389
19877
  });
18390
19878
  } catch (parseError) {
18391
19879
  const message = parseError instanceof Error ? parseError.message : String(parseError);
18392
- return fail("USAGE", message, HELP22);
19880
+ return fail("USAGE", message, HELP27);
18393
19881
  }
18394
19882
  if (parsed.values.help) {
18395
- await writeStdout(HELP22);
19883
+ await writeStdout(HELP27);
18396
19884
  return EXIT.OK;
18397
19885
  }
18398
19886
  const path = parsed.positionals[0];
18399
19887
  if (!path)
18400
- return fail("USAGE", "Missing FILE argument", HELP22);
19888
+ return fail("USAGE", "Missing FILE argument", HELP27);
18401
19889
  const mode = parsed.positionals[1];
18402
19890
  if (mode !== "on" && mode !== "off") {
18403
- return fail("USAGE", `Expected "on" or "off", got: ${mode ?? "<nothing>"}`, HELP22);
19891
+ return fail("USAGE", `Expected "on" or "off", got: ${mode ?? "<nothing>"}`, HELP27);
18404
19892
  }
18405
19893
  const view = await openOrFail(path);
18406
19894
  if (typeof view === "number")
@@ -18452,7 +19940,7 @@ async function run22(args) {
18452
19940
  });
18453
19941
  return EXIT.OK;
18454
19942
  }
18455
- var HELP22 = `docx track-changes \u2014 toggle the document's tracked-changes mode
19943
+ var HELP27 = `docx track-changes \u2014 toggle the document's tracked-changes mode
18456
19944
 
18457
19945
  Usage:
18458
19946
  docx track-changes FILE on|off [options]
@@ -18485,19 +19973,11 @@ function countWords(text) {
18485
19973
  const matches = text.match(/\S+/g);
18486
19974
  return matches?.length ?? 0;
18487
19975
  }
18488
- function paragraphText2(paragraph) {
18489
- let out = "";
18490
- for (const run23 of paragraph.runs) {
18491
- if (run23.type === "text")
18492
- out += run23.text;
18493
- }
18494
- return out;
18495
- }
18496
19976
  function countWordsInBlocks(blocks) {
18497
19977
  let total = 0;
18498
19978
  for (const block of blocks) {
18499
19979
  if (block.type === "paragraph") {
18500
- total += countWords(paragraphText2(block));
19980
+ total += countWords(paragraphText(block));
18501
19981
  } else if (block.type === "table") {
18502
19982
  for (const row of block.rows) {
18503
19983
  for (const cell of row.cells) {
@@ -18508,24 +19988,8 @@ function countWordsInBlocks(blocks) {
18508
19988
  }
18509
19989
  return total;
18510
19990
  }
18511
- function findBlockById(blocks, blockId) {
18512
- for (const block of blocks) {
18513
- if (block.id === blockId)
18514
- return block;
18515
- if (block.type === "table") {
18516
- for (const row of block.rows) {
18517
- for (const cell of row.cells) {
18518
- const inner = findBlockById(cell.blocks, blockId);
18519
- if (inner)
18520
- return inner;
18521
- }
18522
- }
18523
- }
18524
- }
18525
- return null;
18526
- }
18527
19991
  function countWordsInParagraphSpan(paragraph, start, end) {
18528
- const text = paragraphText2(paragraph);
19992
+ const text = paragraphText(paragraph);
18529
19993
  const clampedEnd = Math.min(Math.max(end, 0), text.length);
18530
19994
  const clampedStart = Math.min(Math.max(start, 0), clampedEnd);
18531
19995
  return countWords(text.slice(clampedStart, clampedEnd));
@@ -18546,46 +20010,32 @@ function countWordsInRange(paragraphsInOrder, startBlockId, startOffset, endBloc
18546
20010
  let total = 0;
18547
20011
  const first = paragraphsInOrder[startIndex];
18548
20012
  if (first) {
18549
- total += countWordsInParagraphSpan(first, startOffset, paragraphText2(first).length);
20013
+ total += countWordsInParagraphSpan(first, startOffset, paragraphText(first).length);
18550
20014
  }
18551
20015
  for (let index = startIndex + 1;index < endIndex; index++) {
18552
20016
  const middle = paragraphsInOrder[index];
18553
20017
  if (middle)
18554
- total += countWords(paragraphText2(middle));
20018
+ total += countWords(paragraphText(middle));
18555
20019
  }
18556
20020
  const last = paragraphsInOrder[endIndex];
18557
20021
  if (last)
18558
20022
  total += countWordsInParagraphSpan(last, 0, endOffset);
18559
20023
  return total;
18560
20024
  }
18561
- function flattenParagraphs(blocks) {
18562
- const out = [];
18563
- for (const block of blocks) {
18564
- if (block.type === "paragraph") {
18565
- out.push(block);
18566
- continue;
18567
- }
18568
- if (block.type === "table") {
18569
- for (const row of block.rows) {
18570
- for (const cell of row.cells) {
18571
- out.push(...flattenParagraphs(cell.blocks));
18572
- }
18573
- }
18574
- }
18575
- }
18576
- return out;
18577
- }
20025
+ var init_count = __esm(() => {
20026
+ init_core();
20027
+ });
18578
20028
 
18579
20029
  // src/cli/wc/index.ts
18580
20030
  var exports_wc = {};
18581
20031
  __export(exports_wc, {
18582
- run: () => run23
20032
+ run: () => run28
18583
20033
  });
18584
- import { parseArgs as parseArgs20 } from "util";
18585
- async function run23(args) {
20034
+ import { parseArgs as parseArgs24 } from "util";
20035
+ async function run28(args) {
18586
20036
  let parsed;
18587
20037
  try {
18588
- parsed = parseArgs20({
20038
+ parsed = parseArgs24({
18589
20039
  args,
18590
20040
  allowPositionals: true,
18591
20041
  options: {
@@ -18594,16 +20044,16 @@ async function run23(args) {
18594
20044
  });
18595
20045
  } catch (parseError) {
18596
20046
  const message = parseError instanceof Error ? parseError.message : String(parseError);
18597
- return fail("USAGE", message, HELP23);
20047
+ return fail("USAGE", message, HELP28);
18598
20048
  }
18599
20049
  if (parsed.values.help) {
18600
- await writeStdout(HELP23);
20050
+ await writeStdout(HELP28);
18601
20051
  return EXIT.OK;
18602
20052
  }
18603
20053
  const path = parsed.positionals[0];
18604
20054
  const locatorInput = parsed.positionals[1];
18605
20055
  if (!path)
18606
- return fail("USAGE", "Missing FILE argument", HELP23);
20056
+ return fail("USAGE", "Missing FILE argument", HELP28);
18607
20057
  const view = await openOrFail(path);
18608
20058
  if (typeof view === "number")
18609
20059
  return view;
@@ -18635,7 +20085,7 @@ async function run23(args) {
18635
20085
  if (!block) {
18636
20086
  return fail("BLOCK_NOT_FOUND", `Block not found: ${locator.blockId}`);
18637
20087
  }
18638
- const words = block.type === "paragraph" ? countWords(paragraphText2(block)) : block.type === "table" ? countWordsInBlocks([block]) : 0;
20088
+ const words = block.type === "paragraph" ? countWords(paragraphText(block)) : block.type === "table" ? countWordsInBlocks([block]) : 0;
18639
20089
  await respond({
18640
20090
  ok: true,
18641
20091
  operation: "wc",
@@ -18707,7 +20157,7 @@ async function run23(args) {
18707
20157
  if (!block || block.type !== "paragraph") {
18708
20158
  return fail("BLOCK_NOT_FOUND", `Paragraph not found: ${composedId}`);
18709
20159
  }
18710
- const words = locator.inner.kind === "blockSpan" ? countWordsInParagraphSpan(block, locator.inner.start, locator.inner.end) : countWords(paragraphText2(block));
20160
+ const words = locator.inner.kind === "blockSpan" ? countWordsInParagraphSpan(block, locator.inner.start, locator.inner.end) : countWords(paragraphText(block));
18711
20161
  await respond({
18712
20162
  ok: true,
18713
20163
  operation: "wc",
@@ -18732,7 +20182,7 @@ function findCellBlocks(blocks, locator) {
18732
20182
  return null;
18733
20183
  return cell.blocks;
18734
20184
  }
18735
- var HELP23 = `docx wc \u2014 count words in a document or a locator-addressed slice
20185
+ var HELP28 = `docx wc \u2014 count words in a document or a locator-addressed slice
18736
20186
 
18737
20187
  Usage:
18738
20188
  docx wc FILE [LOCATOR] [options]
@@ -18765,11 +20215,12 @@ Examples:
18765
20215
  var init_wc = __esm(() => {
18766
20216
  init_core();
18767
20217
  init_respond();
20218
+ init_count();
18768
20219
  });
18769
20220
  // package.json
18770
20221
  var package_default = {
18771
20222
  name: "bun-docx",
18772
- version: "0.4.0",
20223
+ version: "0.6.0",
18773
20224
  description: "CLI for AI agents (Claude, Codex) to read, edit, and comment on .docx files with full format fidelity.",
18774
20225
  keywords: [
18775
20226
  "docx",
@@ -18848,6 +20299,7 @@ Commands:
18848
20299
  outline FILE List headings as a hierarchical tree
18849
20300
  comments \u2026 Add, reply, resolve, delete, list comments
18850
20301
  images \u2026 Extract, replace, list images
20302
+ hyperlinks \u2026 List, replace hyperlinks
18851
20303
  track-changes FILE on|off Toggle tracked-changes mode
18852
20304
  info \u2026 Reference material (schema, locators)
18853
20305
 
@@ -18874,12 +20326,13 @@ var COMMANDS = {
18874
20326
  delete: () => Promise.resolve().then(() => (init_delete2(), exports_delete2)),
18875
20327
  edit: () => Promise.resolve().then(() => (init_edit(), exports_edit)),
18876
20328
  find: () => Promise.resolve().then(() => (init_find(), exports_find)),
20329
+ hyperlinks: () => Promise.resolve().then(() => (init_hyperlinks(), exports_hyperlinks)),
18877
20330
  images: () => Promise.resolve().then(() => (init_images(), exports_images)),
18878
20331
  info: () => Promise.resolve().then(() => (init_info(), exports_info)),
18879
20332
  insert: () => Promise.resolve().then(() => (init_insert(), exports_insert)),
18880
20333
  outline: () => Promise.resolve().then(() => (init_outline(), exports_outline)),
18881
20334
  read: () => Promise.resolve().then(() => (init_read2(), exports_read)),
18882
- replace: () => Promise.resolve().then(() => (init_replace2(), exports_replace2)),
20335
+ replace: () => Promise.resolve().then(() => (init_replace3(), exports_replace3)),
18883
20336
  "track-changes": () => Promise.resolve().then(() => (init_track_changes2(), exports_track_changes)),
18884
20337
  wc: () => Promise.resolve().then(() => (init_wc(), exports_wc))
18885
20338
  };