bun-docx 0.3.0 → 0.5.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 +10 -2
  2. package/dist/index.js +1541 -305
  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,6 +13774,7 @@ 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
  };
@@ -13813,17 +13814,30 @@ function readParagraph(view, node, id, state) {
13813
13814
  if (paragraphProperties) {
13814
13815
  applyParagraphProperties(paragraph, paragraphProperties);
13815
13816
  }
13816
- const activeComments = new Set;
13817
- let offset = 0;
13818
- for (const child of node.children) {
13817
+ const context = {
13818
+ view,
13819
+ blockId: id,
13820
+ paragraph,
13821
+ activeComments: new Set,
13822
+ state,
13823
+ offsetRef: { value: 0 }
13824
+ };
13825
+ walkRunContainer(context, node, undefined, undefined);
13826
+ return paragraph;
13827
+ }
13828
+ function walkRunContainer(context, container, trackedChange, hyperlink) {
13829
+ for (const child of container.children) {
13819
13830
  if (child.tag === "w:pPr")
13820
13831
  continue;
13821
13832
  if (child.tag === "w:commentRangeStart") {
13822
13833
  const commentId = child.getAttribute("w:id");
13823
13834
  if (commentId) {
13824
13835
  const key = `c${commentId}`;
13825
- activeComments.add(key);
13826
- state.openComments.set(key, { blockId: id, offset });
13836
+ context.activeComments.add(key);
13837
+ context.state.openComments.set(key, {
13838
+ blockId: context.blockId,
13839
+ offset: context.offsetRef.value
13840
+ });
13827
13841
  }
13828
13842
  continue;
13829
13843
  }
@@ -13831,26 +13845,26 @@ function readParagraph(view, node, id, state) {
13831
13845
  const commentId = child.getAttribute("w:id");
13832
13846
  if (commentId) {
13833
13847
  const key = `c${commentId}`;
13834
- activeComments.delete(key);
13835
- const opened = state.openComments.get(key);
13848
+ context.activeComments.delete(key);
13849
+ const opened = context.state.openComments.get(key);
13836
13850
  if (opened) {
13837
- state.commentAnchors.set(key, {
13851
+ context.state.commentAnchors.set(key, {
13838
13852
  startBlockId: opened.blockId,
13839
13853
  startOffset: opened.offset,
13840
- endBlockId: id,
13841
- endOffset: offset
13854
+ endBlockId: context.blockId,
13855
+ endOffset: context.offsetRef.value
13842
13856
  });
13843
- state.openComments.delete(key);
13857
+ context.state.openComments.delete(key);
13844
13858
  }
13845
13859
  }
13846
13860
  continue;
13847
13861
  }
13848
13862
  if (child.tag === "w:r") {
13849
- const run = readRun(view, child, activeComments, undefined, state);
13863
+ const run = readRun(context.view, child, context.activeComments, trackedChange, hyperlink, context.state);
13850
13864
  if (run) {
13851
13865
  if (run.type === "text")
13852
- offset += run.text.length;
13853
- paragraph.runs.push(run);
13866
+ context.offsetRef.value += run.text.length;
13867
+ context.paragraph.runs.push(run);
13854
13868
  }
13855
13869
  continue;
13856
13870
  }
@@ -13861,46 +13875,40 @@ function readParagraph(view, node, id, state) {
13861
13875
  date: child.getAttribute("w:date") ?? "",
13862
13876
  revisionId: child.getAttribute("w:id") ?? ""
13863
13877
  };
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
- }
13878
+ walkRunContainer(context, child, change, hyperlink);
13879
+ continue;
13880
+ }
13881
+ if (child.tag === "w:hyperlink") {
13882
+ const link = readHyperlinkProperties(context.view, child, container.children, context.state);
13883
+ walkRunContainer(context, child, trackedChange, link);
13901
13884
  }
13902
13885
  }
13903
- return paragraph;
13886
+ }
13887
+ function readHyperlinkProperties(view, node, parent, state) {
13888
+ const id = `link${state.hyperlinkIndex++}`;
13889
+ const link = { id };
13890
+ const relationshipId = node.getAttribute("r:id");
13891
+ if (relationshipId) {
13892
+ const relationship = view.hyperlinksByRelationshipId.get(relationshipId);
13893
+ if (relationship?.url)
13894
+ link.url = relationship.url;
13895
+ }
13896
+ const anchor = node.getAttribute("w:anchor");
13897
+ if (anchor)
13898
+ link.anchor = anchor;
13899
+ const tooltip = node.getAttribute("w:tooltip");
13900
+ if (tooltip)
13901
+ link.tooltip = tooltip;
13902
+ if (!link.url && !link.anchor && !link.tooltip) {
13903
+ state.hyperlinkIndex--;
13904
+ return;
13905
+ }
13906
+ view.hyperlinkById.set(id, {
13907
+ node,
13908
+ parent,
13909
+ ...relationshipId ? { relationshipId } : {}
13910
+ });
13911
+ return link;
13904
13912
  }
13905
13913
  function applyParagraphProperties(paragraph, paragraphProperties) {
13906
13914
  const styleNode = paragraphProperties.findChild("w:pStyle");
@@ -13925,7 +13933,7 @@ function applyParagraphProperties(paragraph, paragraphProperties) {
13925
13933
  paragraph.list = { level, numId: id };
13926
13934
  }
13927
13935
  }
13928
- function readRun(view, node, activeComments, trackedChange, state) {
13936
+ function readRun(view, node, activeComments, trackedChange, hyperlink, state) {
13929
13937
  const runProperties = node.findChild("w:rPr");
13930
13938
  for (const child of node.children) {
13931
13939
  if (child.tag === "w:drawing") {
@@ -13956,6 +13964,8 @@ function readRun(view, node, activeComments, trackedChange, state) {
13956
13964
  run.comments = [...activeComments];
13957
13965
  if (trackedChange)
13958
13966
  run.trackedChange = trackedChange;
13967
+ if (hyperlink)
13968
+ run.hyperlink = hyperlink;
13959
13969
  return run;
13960
13970
  }
13961
13971
  function applyRunProperties(run, runProperties) {
@@ -14060,22 +14070,30 @@ function readCellBlocks(view, cell, tableId, rowIndex, columnIndex, state) {
14060
14070
  }
14061
14071
  return blocks;
14062
14072
  }
14063
- function readImageRelationships(view) {
14073
+ function readRelationships(view) {
14064
14074
  const relationships = XmlNode2.findRoot(view.relationshipsTree, "Relationships");
14065
14075
  if (!relationships)
14066
14076
  return;
14067
14077
  for (const child of relationships.children) {
14068
14078
  if (child.tag !== "Relationship")
14069
14079
  continue;
14070
- if (child.getAttribute("Type") !== RELATIONSHIP_NAMESPACE_IMAGE)
14071
- continue;
14080
+ const type = child.getAttribute("Type");
14072
14081
  const relationshipId = child.getAttribute("Id");
14073
14082
  const target = child.getAttribute("Target");
14074
14083
  if (!relationshipId || !target)
14075
14084
  continue;
14076
- const partName = target.startsWith("/") ? target.slice(1) : `word/${target}`;
14077
- const contentType = lookupContentType(view, partName);
14078
- view.imagesByRelationshipId.set(relationshipId, { partName, contentType });
14085
+ if (type === RELATIONSHIP_NAMESPACE_IMAGE) {
14086
+ const partName = target.startsWith("/") ? target.slice(1) : `word/${target}`;
14087
+ const contentType = lookupContentType(view, partName);
14088
+ view.imagesByRelationshipId.set(relationshipId, {
14089
+ partName,
14090
+ contentType
14091
+ });
14092
+ continue;
14093
+ }
14094
+ if (type === RELATIONSHIP_NAMESPACE_HYPERLINK) {
14095
+ view.hyperlinksByRelationshipId.set(relationshipId, { url: target });
14096
+ }
14079
14097
  }
14080
14098
  }
14081
14099
  function lookupContentType(view, partName) {
@@ -14204,7 +14222,7 @@ function readCommentsExtended(view) {
14204
14222
  }
14205
14223
  return out;
14206
14224
  }
14207
- var RELATIONSHIP_NAMESPACE_IMAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
14225
+ var RELATIONSHIP_NAMESPACE_IMAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", RELATIONSHIP_NAMESPACE_HYPERLINK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
14208
14226
  var init_read = __esm(() => {
14209
14227
  init_parser();
14210
14228
  });
@@ -14232,7 +14250,9 @@ async function openDocView(path) {
14232
14250
  blockReferences: new Map,
14233
14251
  commentReferences: new Map,
14234
14252
  imagesByRelationshipId: new Map,
14235
- imageById: new Map
14253
+ imageById: new Map,
14254
+ hyperlinksByRelationshipId: new Map,
14255
+ hyperlinkById: new Map
14236
14256
  };
14237
14257
  view.doc = buildDoc(view, pkg.path);
14238
14258
  return view;
@@ -14319,6 +14339,10 @@ function parseLocator(input) {
14319
14339
  const imageMatch = trimmed.match(IMAGE_RE);
14320
14340
  if (imageMatch)
14321
14341
  return { kind: "image", imageId: `img${imageMatch[1]}` };
14342
+ const linkMatch = trimmed.match(LINK_RE);
14343
+ if (linkMatch) {
14344
+ return { kind: "hyperlink", hyperlinkId: `link${linkMatch[1]}` };
14345
+ }
14322
14346
  const cellMatch = trimmed.match(CELL_RE);
14323
14347
  if (cellMatch) {
14324
14348
  const [, tableIndex, rowIndex, columnIndex, rest] = cellMatch;
@@ -14367,7 +14391,7 @@ function validateOffsets(input, start, end, crossBlock) {
14367
14391
  throw new LocatorParseError(input, "end offset precedes start");
14368
14392
  }
14369
14393
  }
14370
- var LocatorParseError, BLOCK_RE, SPAN_RE, RANGE_RE, COMMENT_RE, IMAGE_RE, CELL_RE;
14394
+ var LocatorParseError, BLOCK_RE, SPAN_RE, RANGE_RE, COMMENT_RE, IMAGE_RE, LINK_RE, CELL_RE;
14371
14395
  var init_parse = __esm(() => {
14372
14396
  LocatorParseError = class LocatorParseError extends Error {
14373
14397
  input;
@@ -14382,6 +14406,7 @@ var init_parse = __esm(() => {
14382
14406
  RANGE_RE = /^p(\d+):(\d+)-p(\d+):(\d+)$/;
14383
14407
  COMMENT_RE = /^c(\d+)$/;
14384
14408
  IMAGE_RE = /^img(\d+)$/;
14409
+ LINK_RE = /^link(\d+)$/;
14385
14410
  CELL_RE = /^t(\d+):r(\d+)c(\d+)(?::(.+))?$/;
14386
14411
  });
14387
14412
 
@@ -14431,6 +14456,39 @@ var init_locators = __esm(() => {
14431
14456
  init_resolve();
14432
14457
  });
14433
14458
 
14459
+ // src/core/relationships.ts
14460
+ function mintRelationshipId(relationshipsRoot) {
14461
+ let highest = 0;
14462
+ for (const child of relationshipsRoot.children) {
14463
+ if (child.tag !== "Relationship")
14464
+ continue;
14465
+ const id = child.getAttribute("Id");
14466
+ if (!id)
14467
+ continue;
14468
+ const match = id.match(/^rId(\d+)$/);
14469
+ if (match?.[1]) {
14470
+ const number = Number(match[1]);
14471
+ if (number > highest)
14472
+ highest = number;
14473
+ }
14474
+ }
14475
+ return `rId${highest + 1}`;
14476
+ }
14477
+ function addHyperlinkRelationship(relationshipsRoot, url) {
14478
+ const id = mintRelationshipId(relationshipsRoot);
14479
+ relationshipsRoot.children.push(new XmlNode2("Relationship", {
14480
+ Id: id,
14481
+ Type: HYPERLINK_RELATIONSHIP_TYPE,
14482
+ Target: url,
14483
+ TargetMode: "External"
14484
+ }));
14485
+ return id;
14486
+ }
14487
+ var HYPERLINK_RELATIONSHIP_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
14488
+ var init_relationships = __esm(() => {
14489
+ init_parser();
14490
+ });
14491
+
14434
14492
  // src/core/track-changes/index.ts
14435
14493
  function isTrackChangesEnabled(view) {
14436
14494
  if (!view.settingsTree)
@@ -14607,7 +14665,8 @@ var init_jsx = __esm(() => {
14607
14665
  "comments",
14608
14666
  "comment",
14609
14667
  "settings",
14610
- "trackChanges"
14668
+ "trackChanges",
14669
+ "hyperlink"
14611
14670
  ];
14612
14671
  R_TAGS = ["embed", "link", "id"];
14613
14672
  A_TAGS = [
@@ -14722,6 +14781,7 @@ var init_core = __esm(() => {
14722
14781
  init_locators();
14723
14782
  init_package();
14724
14783
  init_parser();
14784
+ init_relationships();
14725
14785
  init_track_changes();
14726
14786
  init_emit();
14727
14787
  });
@@ -14755,6 +14815,7 @@ function exitCodeFor(code) {
14755
14815
  case "BLOCK_NOT_FOUND":
14756
14816
  case "COMMENT_NOT_FOUND":
14757
14817
  case "IMAGE_NOT_FOUND":
14818
+ case "HYPERLINK_NOT_FOUND":
14758
14819
  case "MATCH_NOT_FOUND":
14759
14820
  return EXIT.NOT_FOUND;
14760
14821
  case "NOT_A_ZIP":
@@ -16629,12 +16690,107 @@ var init_find = __esm(() => {
16629
16690
  init_respond();
16630
16691
  });
16631
16692
 
16632
- // src/cli/images/extract.ts
16633
- var exports_extract = {};
16634
- __export(exports_extract, {
16693
+ // src/cli/hyperlinks/wrap.tsx
16694
+ function wrapSpanInHyperlink(paragraph, span, relationshipId) {
16695
+ if (span.start >= span.end) {
16696
+ throw new HyperlinkWrapError(`Empty or inverted span ${span.start}-${span.end}`);
16697
+ }
16698
+ const newChildren = [];
16699
+ const wrappedRuns = [];
16700
+ let offset = 0;
16701
+ let placed = false;
16702
+ const placeWrapper = () => {
16703
+ if (placed || wrappedRuns.length === 0)
16704
+ return;
16705
+ newChildren.push(hyperlinkWrapper(relationshipId, wrappedRuns));
16706
+ wrappedRuns.length = 0;
16707
+ placed = true;
16708
+ };
16709
+ for (const child of paragraph.children) {
16710
+ if (child.tag === "w:r") {
16711
+ const length = runTextLength(child);
16712
+ const runStart = offset;
16713
+ const runEnd = offset + length;
16714
+ offset = runEnd;
16715
+ if (runEnd <= span.start || runStart >= span.end) {
16716
+ placeWrapper();
16717
+ newChildren.push(child);
16718
+ continue;
16719
+ }
16720
+ const sliceStartInRun = Math.max(0, span.start - runStart);
16721
+ const sliceEndInRun = Math.min(length, span.end - runStart);
16722
+ if (sliceStartInRun > 0) {
16723
+ newChildren.push(sliceRun(child, 0, sliceStartInRun));
16724
+ }
16725
+ wrappedRuns.push(sliceRun(child, sliceStartInRun, sliceEndInRun));
16726
+ if (runEnd >= span.end)
16727
+ placeWrapper();
16728
+ if (sliceEndInRun < length) {
16729
+ newChildren.push(sliceRun(child, sliceEndInRun, length));
16730
+ }
16731
+ continue;
16732
+ }
16733
+ if (child.tag === "w:hyperlink") {
16734
+ const hyperlinkLength = sumInnerRunLengths(child);
16735
+ const hyperlinkStart = offset;
16736
+ const hyperlinkEnd = offset + hyperlinkLength;
16737
+ offset = hyperlinkEnd;
16738
+ if (hyperlinkEnd <= span.start || hyperlinkStart >= span.end) {
16739
+ placeWrapper();
16740
+ newChildren.push(child);
16741
+ continue;
16742
+ }
16743
+ throw new HyperlinkWrapError(`Span ${span.start}-${span.end} overlaps an existing hyperlink at ${hyperlinkStart}-${hyperlinkEnd}; nested hyperlinks are not allowed`);
16744
+ }
16745
+ if (child.tag === "w:ins" || child.tag === "w:del") {
16746
+ const innerLength = sumInnerRunLengths(child);
16747
+ const wrapperStart = offset;
16748
+ const wrapperEnd = offset + innerLength;
16749
+ offset = wrapperEnd;
16750
+ if (wrapperEnd <= span.start || wrapperStart >= span.end) {
16751
+ placeWrapper();
16752
+ newChildren.push(child);
16753
+ continue;
16754
+ }
16755
+ throw new HyperlinkWrapError(`Span ${span.start}-${span.end} crosses a tracked-change wrapper at ${wrapperStart}-${wrapperEnd}; resolve or accept the change first`);
16756
+ }
16757
+ newChildren.push(child);
16758
+ }
16759
+ placeWrapper();
16760
+ paragraph.children = newChildren;
16761
+ }
16762
+ function hyperlinkWrapper(relationshipId, runs) {
16763
+ return /* @__PURE__ */ jsxDEV(w.hyperlink, {
16764
+ "r:id": relationshipId,
16765
+ children: runs
16766
+ }, undefined, false, undefined, this);
16767
+ }
16768
+ function sumInnerRunLengths(wrapper) {
16769
+ let total = 0;
16770
+ for (const inner of wrapper.children) {
16771
+ if (inner.tag === "w:r")
16772
+ total += runTextLength(inner);
16773
+ }
16774
+ return total;
16775
+ }
16776
+ var HyperlinkWrapError;
16777
+ var init_wrap = __esm(() => {
16778
+ init_jsx();
16779
+ init_parser();
16780
+ init_jsx_dev_runtime();
16781
+ HyperlinkWrapError = class HyperlinkWrapError extends Error {
16782
+ constructor(message) {
16783
+ super(message);
16784
+ this.name = "HyperlinkWrapError";
16785
+ }
16786
+ };
16787
+ });
16788
+
16789
+ // src/cli/hyperlinks/add.tsx
16790
+ var exports_add2 = {};
16791
+ __export(exports_add2, {
16635
16792
  run: () => run11
16636
16793
  });
16637
- import { join } from "path";
16638
16794
  import { parseArgs as parseArgs10 } from "util";
16639
16795
  async function run11(args) {
16640
16796
  let parsed;
@@ -16643,8 +16799,10 @@ async function run11(args) {
16643
16799
  args,
16644
16800
  allowPositionals: true,
16645
16801
  options: {
16646
- to: { type: "string" },
16647
- id: { type: "string" },
16802
+ at: { type: "string" },
16803
+ url: { type: "string" },
16804
+ output: { type: "string", short: "o" },
16805
+ "dry-run": { type: "boolean" },
16648
16806
  help: { type: "boolean", short: "h" }
16649
16807
  }
16650
16808
  });
@@ -16659,109 +16817,100 @@ async function run11(args) {
16659
16817
  const path = parsed.positionals[0];
16660
16818
  if (!path)
16661
16819
  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;
16820
+ const atInput = parsed.values.at;
16821
+ const url = parsed.values.url;
16822
+ if (!atInput)
16823
+ return fail("USAGE", "Missing --at LOCATOR", HELP11);
16824
+ if (!url)
16825
+ return fail("USAGE", "Missing --url URL", HELP11);
16826
+ let locator;
16827
+ try {
16828
+ locator = parseLocator(atInput);
16829
+ } catch (error) {
16830
+ if (error instanceof LocatorParseError) {
16831
+ return fail("INVALID_LOCATOR", error.message);
16832
+ }
16833
+ throw error;
16834
+ }
16835
+ const target = locatorToBlockTarget(locator);
16836
+ if (!target?.span) {
16837
+ return fail("INVALID_LOCATOR", "hyperlinks add requires a span locator like pN:S-E or tT:rRcC:pK:S-E");
16838
+ }
16666
16839
  const view = await openOrFail(path);
16667
16840
  if (typeof view === "number")
16668
16841
  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}`);
16675
- }
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);
16689
- }
16690
- manifest.push({
16691
- id: image.id,
16692
- path: outputPath,
16693
- bytes: (await Bun.file(outputPath).arrayBuffer()).byteLength
16842
+ const paragraphRef = await resolveBlockOrFail(view, target.blockId);
16843
+ if (typeof paragraphRef === "number")
16844
+ return paragraphRef;
16845
+ const outputPath = parsed.values.output;
16846
+ if (parsed.values["dry-run"]) {
16847
+ await respond({
16848
+ ok: true,
16849
+ operation: "hyperlinks.add",
16850
+ dryRun: true,
16851
+ path,
16852
+ at: atInput,
16853
+ url,
16854
+ ...outputPath ? { output: outputPath } : {}
16694
16855
  });
16856
+ return EXIT.OK;
16695
16857
  }
16696
- await respond({ ok: true, operation: "images.extract", path, manifest });
16697
- return EXIT.OK;
16698
- }
16699
- function collectImages(blocks, out) {
16700
- for (const block of blocks) {
16701
- if (block.type === "paragraph") {
16702
- for (const run12 of block.runs) {
16703
- if (run12.type === "image")
16704
- out.push(run12);
16705
- }
16706
- continue;
16707
- }
16708
- if (block.type === "table") {
16709
- for (const row of block.rows) {
16710
- for (const cell of row.cells) {
16711
- collectImages(cell.blocks, out);
16712
- }
16713
- }
16858
+ const relationships = XmlNode2.findRoot(view.relationshipsTree, "Relationships");
16859
+ if (!relationships) {
16860
+ return fail("UNHANDLED", "Missing <Relationships> root in document rels");
16861
+ }
16862
+ const relationshipId = addHyperlinkRelationship(relationships, url);
16863
+ try {
16864
+ wrapSpanInHyperlink(paragraphRef.node, target.span, relationshipId);
16865
+ } catch (error) {
16866
+ if (error instanceof HyperlinkWrapError) {
16867
+ return fail("USAGE", error.message);
16714
16868
  }
16869
+ throw error;
16715
16870
  }
16871
+ view.hyperlinksByRelationshipId.set(relationshipId, { url });
16872
+ await saveDocView(view, outputPath);
16873
+ await respond({
16874
+ ok: true,
16875
+ operation: "hyperlinks.add",
16876
+ path: outputPath ?? path,
16877
+ at: atInput,
16878
+ url
16879
+ });
16880
+ return EXIT.OK;
16716
16881
  }
16717
- function extensionFor(contentType, partName) {
16718
- const fromType = EXTENSION_BY_CONTENT_TYPE[contentType.toLowerCase()];
16719
- if (fromType)
16720
- return fromType;
16721
- const fromName = partName.split(".").pop()?.toLowerCase();
16722
- return fromName ?? "bin";
16723
- }
16724
- var HELP11 = `docx images extract \u2014 dump image bytes to a directory
16882
+ var HELP11 = `docx hyperlinks add \u2014 wrap an existing span in a hyperlink
16725
16883
 
16726
16884
  Usage:
16727
- docx images extract FILE --to DIR [options]
16885
+ docx hyperlinks add FILE --at LOCATOR --url URL [options]
16728
16886
 
16729
16887
  Required:
16730
- --to DIR Output directory (created if missing)
16888
+ --at LOCATOR Where to wrap. Supports:
16889
+ pN:S-E chars S..E of pN
16890
+ tT:rRcC:pK:S-E chars S..E of a cell paragraph
16891
+ --url URL Target URL
16731
16892
 
16732
16893
  Optional:
16733
- --id IMG_ID Extract a single image (default: extract all)
16894
+ -o, --output PATH Write to PATH instead of overwriting FILE
16895
+ --dry-run Print what would change; do not write the file
16734
16896
  -h, --help Show this help
16735
16897
 
16736
- Files are named <hash>.<ext> where hash is the sha256 of the image bytes
16737
- and ext is derived from contentType. Returns a manifest mapping image ids
16738
- to written paths.
16898
+ The span must lie inside a single paragraph and must not overlap an existing
16899
+ hyperlink or a tracked-change wrapper.
16739
16900
 
16740
16901
  Examples:
16741
- docx images extract doc.docx --to ./media
16742
- docx images extract doc.docx --to ./media --id img2
16743
- `, EXTENSION_BY_CONTENT_TYPE;
16744
- var init_extract = __esm(() => {
16902
+ docx hyperlinks add doc.docx --at p3:5-20 --url https://example.com
16903
+ `;
16904
+ var init_add2 = __esm(() => {
16745
16905
  init_core();
16906
+ init_parser();
16746
16907
  init_respond();
16747
- EXTENSION_BY_CONTENT_TYPE = {
16748
- "image/png": "png",
16749
- "image/jpeg": "jpg",
16750
- "image/jpg": "jpg",
16751
- "image/gif": "gif",
16752
- "image/svg+xml": "svg",
16753
- "image/webp": "webp",
16754
- "image/tiff": "tif",
16755
- "image/bmp": "bmp",
16756
- "image/x-emf": "emf",
16757
- "image/x-wmf": "wmf",
16758
- "image/vnd.microsoft.icon": "ico"
16759
- };
16908
+ init_wrap();
16760
16909
  });
16761
16910
 
16762
- // src/cli/images/list.ts
16763
- var exports_list2 = {};
16764
- __export(exports_list2, {
16911
+ // src/cli/hyperlinks/delete.ts
16912
+ var exports_delete3 = {};
16913
+ __export(exports_delete3, {
16765
16914
  run: () => run12
16766
16915
  });
16767
16916
  import { parseArgs as parseArgs11 } from "util";
@@ -16772,6 +16921,9 @@ async function run12(args) {
16772
16921
  args,
16773
16922
  allowPositionals: true,
16774
16923
  options: {
16924
+ at: { type: "string" },
16925
+ output: { type: "string", short: "o" },
16926
+ "dry-run": { type: "boolean" },
16775
16927
  help: { type: "boolean", short: "h" }
16776
16928
  }
16777
16929
  });
@@ -16786,34 +16938,560 @@ async function run12(args) {
16786
16938
  const path = parsed.positionals[0];
16787
16939
  if (!path)
16788
16940
  return fail("USAGE", "Missing FILE argument", HELP12);
16941
+ const targetId = parsed.values.at;
16942
+ if (!targetId)
16943
+ return fail("USAGE", "Missing --at LINK_ID", HELP12);
16789
16944
  const view = await openOrFail(path);
16790
16945
  if (typeof view === "number")
16791
16946
  return view;
16792
- await enrichImageHashes(view);
16793
- const images = [];
16794
- collectImages2(view.doc.blocks, images);
16795
- await respond(images);
16796
- return EXIT.OK;
16797
- }
16798
- function collectImages2(blocks, out) {
16799
- for (const block of blocks) {
16800
- if (block.type === "paragraph") {
16801
- for (const run13 of block.runs) {
16802
- if (run13.type === "image")
16803
- out.push(run13);
16804
- }
16805
- continue;
16806
- }
16807
- if (block.type === "table") {
16808
- for (const row of block.rows) {
16809
- for (const cell of row.cells) {
16810
- collectImages2(cell.blocks, out);
16811
- }
16812
- }
16813
- }
16947
+ const reference = view.hyperlinkById.get(targetId);
16948
+ if (!reference) {
16949
+ return fail("HYPERLINK_NOT_FOUND", `Hyperlink not found: ${targetId}`);
16950
+ }
16951
+ const oldUrl = reference.relationshipId ? view.hyperlinksByRelationshipId.get(reference.relationshipId)?.url : undefined;
16952
+ const outputPath = parsed.values.output;
16953
+ if (parsed.values["dry-run"]) {
16954
+ await respond({
16955
+ ok: true,
16956
+ operation: "hyperlinks.delete",
16957
+ dryRun: true,
16958
+ path,
16959
+ hyperlinkId: targetId,
16960
+ from: oldUrl,
16961
+ ...outputPath ? { output: outputPath } : {}
16962
+ });
16963
+ return EXIT.OK;
16964
+ }
16965
+ const index = reference.parent.indexOf(reference.node);
16966
+ if (index === -1) {
16967
+ return fail("HYPERLINK_NOT_FOUND", `Hyperlink reference is stale (parent does not contain it): ${targetId}`);
16968
+ }
16969
+ reference.parent.splice(index, 1, ...reference.node.children);
16970
+ view.hyperlinkById.delete(targetId);
16971
+ if (reference.relationshipId) {
16972
+ const remaining = countHyperlinkUsages(view.documentTree, reference.relationshipId);
16973
+ if (remaining === 0) {
16974
+ pruneRelationship(view.relationshipsTree, reference.relationshipId);
16975
+ view.hyperlinksByRelationshipId.delete(reference.relationshipId);
16976
+ }
16977
+ }
16978
+ await saveDocView(view, outputPath);
16979
+ await respond({
16980
+ ok: true,
16981
+ operation: "hyperlinks.delete",
16982
+ path: outputPath ?? path,
16983
+ hyperlinkId: targetId,
16984
+ from: oldUrl
16985
+ });
16986
+ return EXIT.OK;
16987
+ }
16988
+ function countHyperlinkUsages(documentTree, relationshipId) {
16989
+ let count = 0;
16990
+ for (const root of documentTree) {
16991
+ count += countInNode(root, relationshipId);
16992
+ }
16993
+ return count;
16994
+ }
16995
+ function countInNode(node, relationshipId) {
16996
+ let count = 0;
16997
+ if (node.tag === "w:hyperlink" && node.getAttribute("r:id") === relationshipId) {
16998
+ count++;
16999
+ }
17000
+ for (const child of node.children) {
17001
+ count += countInNode(child, relationshipId);
17002
+ }
17003
+ return count;
17004
+ }
17005
+ function pruneRelationship(relationshipsTree, relationshipId) {
17006
+ const relationships = XmlNode2.findRoot(relationshipsTree, "Relationships");
17007
+ if (!relationships)
17008
+ return;
17009
+ relationships.children = relationships.children.filter((child) => !(child.tag === "Relationship" && child.getAttribute("Id") === relationshipId));
17010
+ }
17011
+ var HELP12 = `docx hyperlinks delete \u2014 unwrap a hyperlink (keep the text)
17012
+
17013
+ Usage:
17014
+ docx hyperlinks delete FILE --at LINK_ID [options]
17015
+
17016
+ Required:
17017
+ --at LINK_ID Existing hyperlink to remove (e.g., link0)
17018
+
17019
+ Optional:
17020
+ -o, --output PATH Write to PATH instead of overwriting FILE
17021
+ --dry-run Print what would change; do not write the file
17022
+ -h, --help Show this help
17023
+
17024
+ The display text stays in place; only the <w:hyperlink> wrapper is removed.
17025
+ If the underlying relationship is no longer referenced, it is pruned from the
17026
+ rels file too.
17027
+
17028
+ Examples:
17029
+ docx hyperlinks delete doc.docx --at link0
17030
+ `;
17031
+ var init_delete3 = __esm(() => {
17032
+ init_core();
17033
+ init_parser();
17034
+ init_respond();
17035
+ });
17036
+
17037
+ // src/cli/hyperlinks/list.ts
17038
+ var exports_list2 = {};
17039
+ __export(exports_list2, {
17040
+ run: () => run13
17041
+ });
17042
+ import { parseArgs as parseArgs12 } from "util";
17043
+ async function run13(args) {
17044
+ let parsed;
17045
+ try {
17046
+ parsed = parseArgs12({
17047
+ args,
17048
+ allowPositionals: true,
17049
+ options: {
17050
+ help: { type: "boolean", short: "h" }
17051
+ }
17052
+ });
17053
+ } catch (parseError) {
17054
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
17055
+ return fail("USAGE", message, HELP13);
17056
+ }
17057
+ if (parsed.values.help) {
17058
+ await writeStdout(HELP13);
17059
+ return EXIT.OK;
17060
+ }
17061
+ const path = parsed.positionals[0];
17062
+ if (!path)
17063
+ return fail("USAGE", "Missing FILE argument", HELP13);
17064
+ const view = await openOrFail(path);
17065
+ if (typeof view === "number")
17066
+ return view;
17067
+ const entries = new Map;
17068
+ collectHyperlinks(view.doc.blocks, entries);
17069
+ await respond([...entries.values()]);
17070
+ return EXIT.OK;
17071
+ }
17072
+ function collectHyperlinks(blocks, entries) {
17073
+ for (const block of blocks) {
17074
+ if (block.type === "paragraph") {
17075
+ for (const run14 of block.runs) {
17076
+ if (run14.type !== "text" || !run14.hyperlink)
17077
+ continue;
17078
+ addToEntry(entries, run14.hyperlink, block.id, run14.text);
17079
+ }
17080
+ continue;
17081
+ }
17082
+ if (block.type === "table") {
17083
+ for (const row of block.rows) {
17084
+ for (const cell of row.cells) {
17085
+ collectHyperlinks(cell.blocks, entries);
17086
+ }
17087
+ }
17088
+ }
17089
+ }
17090
+ }
17091
+ function addToEntry(entries, hyperlink, blockId, text) {
17092
+ const existing = entries.get(hyperlink.id);
17093
+ if (existing) {
17094
+ existing.text += text;
17095
+ return;
17096
+ }
17097
+ const entry = {
17098
+ id: hyperlink.id,
17099
+ text,
17100
+ blockId
17101
+ };
17102
+ if (hyperlink.url)
17103
+ entry.url = hyperlink.url;
17104
+ if (hyperlink.anchor)
17105
+ entry.anchor = hyperlink.anchor;
17106
+ if (hyperlink.tooltip)
17107
+ entry.tooltip = hyperlink.tooltip;
17108
+ entries.set(hyperlink.id, entry);
17109
+ }
17110
+ var HELP13 = `docx hyperlinks list \u2014 print hyperlink manifest as JSON
17111
+
17112
+ Usage:
17113
+ docx hyperlinks list FILE [options]
17114
+
17115
+ Options:
17116
+ -h, --help Show this help
17117
+
17118
+ Each entry has: id, url (or anchor), tooltip (if set), text (display text),
17119
+ and blockId (the paragraph containing the link).
17120
+
17121
+ Examples:
17122
+ docx hyperlinks list doc.docx | jq -c '.[] | {id, url, text}'
17123
+ `;
17124
+ var init_list2 = __esm(() => {
17125
+ init_respond();
17126
+ });
17127
+
17128
+ // src/cli/hyperlinks/replace.ts
17129
+ var exports_replace = {};
17130
+ __export(exports_replace, {
17131
+ run: () => run14
17132
+ });
17133
+ import { parseArgs as parseArgs13 } from "util";
17134
+ async function run14(args) {
17135
+ let parsed;
17136
+ try {
17137
+ parsed = parseArgs13({
17138
+ args,
17139
+ allowPositionals: true,
17140
+ options: {
17141
+ at: { type: "string" },
17142
+ with: { type: "string" },
17143
+ output: { type: "string", short: "o" },
17144
+ "dry-run": { type: "boolean" },
17145
+ help: { type: "boolean", short: "h" }
17146
+ }
17147
+ });
17148
+ } catch (parseError) {
17149
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
17150
+ return fail("USAGE", message, HELP14);
17151
+ }
17152
+ if (parsed.values.help) {
17153
+ await writeStdout(HELP14);
17154
+ return EXIT.OK;
17155
+ }
17156
+ const path = parsed.positionals[0];
17157
+ if (!path)
17158
+ return fail("USAGE", "Missing FILE argument", HELP14);
17159
+ const targetId = parsed.values.at;
17160
+ if (!targetId)
17161
+ return fail("USAGE", "Missing --at LINK_ID", HELP14);
17162
+ const newUrl = parsed.values.with;
17163
+ if (!newUrl)
17164
+ return fail("USAGE", "Missing --with URL", HELP14);
17165
+ const view = await openOrFail(path);
17166
+ if (typeof view === "number")
17167
+ return view;
17168
+ const reference = view.hyperlinkById.get(targetId);
17169
+ if (!reference) {
17170
+ return fail("HYPERLINK_NOT_FOUND", `Hyperlink not found: ${targetId}`);
17171
+ }
17172
+ const relationships = XmlNode2.findRoot(view.relationshipsTree, "Relationships");
17173
+ if (!relationships) {
17174
+ return fail("UNHANDLED", "Missing <Relationships> root in document rels");
17175
+ }
17176
+ const existingId = reference.relationshipId;
17177
+ const oldUrl = existingId ? view.hyperlinksByRelationshipId.get(existingId)?.url : undefined;
17178
+ const sharedCount = existingId ? countHyperlinkUsages2(view.documentTree, existingId) : 0;
17179
+ const willAllocateNew = !existingId || sharedCount > 1;
17180
+ const outputPath = parsed.values.output;
17181
+ if (parsed.values["dry-run"]) {
17182
+ await respond({
17183
+ ok: true,
17184
+ operation: "hyperlinks.replace",
17185
+ dryRun: true,
17186
+ path,
17187
+ hyperlinkId: targetId,
17188
+ from: oldUrl,
17189
+ to: newUrl,
17190
+ sharedRelationship: sharedCount > 1,
17191
+ ...outputPath ? { output: outputPath } : {}
17192
+ });
17193
+ return EXIT.OK;
17194
+ }
17195
+ if (willAllocateNew) {
17196
+ const newRelationshipId = addHyperlinkRelationship(relationships, newUrl);
17197
+ reference.node.setAttribute("r:id", newRelationshipId);
17198
+ reference.relationshipId = newRelationshipId;
17199
+ view.hyperlinksByRelationshipId.set(newRelationshipId, { url: newUrl });
17200
+ } else if (existingId) {
17201
+ updateRelationshipTarget(relationships, existingId, newUrl);
17202
+ view.hyperlinksByRelationshipId.set(existingId, { url: newUrl });
17203
+ }
17204
+ await saveDocView(view, outputPath);
17205
+ await respond({
17206
+ ok: true,
17207
+ operation: "hyperlinks.replace",
17208
+ path: outputPath ?? path,
17209
+ hyperlinkId: targetId,
17210
+ from: oldUrl,
17211
+ to: newUrl
17212
+ });
17213
+ return EXIT.OK;
17214
+ }
17215
+ function updateRelationshipTarget(relationships, relationshipId, newTarget) {
17216
+ for (const child of relationships.children) {
17217
+ if (child.tag !== "Relationship")
17218
+ continue;
17219
+ if (child.getAttribute("Id") === relationshipId) {
17220
+ child.setAttribute("Target", newTarget);
17221
+ return;
17222
+ }
17223
+ }
17224
+ }
17225
+ function countHyperlinkUsages2(documentTree, relationshipId) {
17226
+ let count = 0;
17227
+ for (const root of documentTree) {
17228
+ count += countInNode2(root, relationshipId);
17229
+ }
17230
+ return count;
17231
+ }
17232
+ function countInNode2(node, relationshipId) {
17233
+ let count = 0;
17234
+ if (node.tag === "w:hyperlink" && node.getAttribute("r:id") === relationshipId) {
17235
+ count++;
17236
+ }
17237
+ for (const child of node.children) {
17238
+ count += countInNode2(child, relationshipId);
17239
+ }
17240
+ return count;
17241
+ }
17242
+ var HELP14 = `docx hyperlinks replace \u2014 change a hyperlink's URL
17243
+
17244
+ Usage:
17245
+ docx hyperlinks replace FILE --at LINK_ID --with URL [options]
17246
+
17247
+ Required:
17248
+ --at LINK_ID Existing hyperlink to update (e.g., link0)
17249
+ --with URL New target URL
17250
+
17251
+ Optional:
17252
+ -o, --output PATH Write to PATH instead of overwriting FILE
17253
+ --dry-run Print what would change; do not write the file
17254
+ -h, --help Show this help
17255
+
17256
+ Replaces only the targeted hyperlink. If multiple hyperlinks shared the same
17257
+ underlying relationship, a new relationship is allocated so the others are
17258
+ unaffected.
17259
+
17260
+ Examples:
17261
+ docx hyperlinks replace doc.docx --at link0 --with https://example.com
17262
+ `;
17263
+ var init_replace = __esm(() => {
17264
+ init_core();
17265
+ init_parser();
17266
+ init_respond();
17267
+ });
17268
+
17269
+ // src/cli/hyperlinks/index.ts
17270
+ var exports_hyperlinks = {};
17271
+ __export(exports_hyperlinks, {
17272
+ run: () => run15
17273
+ });
17274
+ async function run15(args) {
17275
+ const verb = args[0];
17276
+ if (!verb || verb === "--help" || verb === "-h" || verb === "help") {
17277
+ await writeStdout(HELP15);
17278
+ return verb ? 0 : 2;
17279
+ }
17280
+ const loader = SUBCOMMANDS2[verb];
17281
+ if (!loader) {
17282
+ return fail("USAGE", `Unknown hyperlinks subcommand: ${verb}`, 'Run "docx hyperlinks --help".');
17283
+ }
17284
+ const module_ = await loader();
17285
+ return module_.run(args.slice(1));
17286
+ }
17287
+ var SUBCOMMANDS2, HELP15 = `docx hyperlinks \u2014 manage hyperlinks
17288
+
17289
+ Usage:
17290
+ docx hyperlinks <verb> FILE [options]
17291
+
17292
+ Verbs:
17293
+ add Wrap an existing span in a hyperlink
17294
+ delete Unwrap a hyperlink (keep the text)
17295
+ list Print hyperlink manifest as JSON
17296
+ replace Change a hyperlink's URL
17297
+
17298
+ Run "docx hyperlinks <verb> --help" for verb-specific help.
17299
+ `;
17300
+ var init_hyperlinks = __esm(() => {
17301
+ init_respond();
17302
+ SUBCOMMANDS2 = {
17303
+ add: () => Promise.resolve().then(() => (init_add2(), exports_add2)),
17304
+ delete: () => Promise.resolve().then(() => (init_delete3(), exports_delete3)),
17305
+ list: () => Promise.resolve().then(() => (init_list2(), exports_list2)),
17306
+ replace: () => Promise.resolve().then(() => (init_replace(), exports_replace))
17307
+ };
17308
+ });
17309
+
17310
+ // src/cli/images/extract.ts
17311
+ var exports_extract = {};
17312
+ __export(exports_extract, {
17313
+ run: () => run16
17314
+ });
17315
+ import { join } from "path";
17316
+ import { parseArgs as parseArgs14 } from "util";
17317
+ async function run16(args) {
17318
+ let parsed;
17319
+ try {
17320
+ parsed = parseArgs14({
17321
+ args,
17322
+ allowPositionals: true,
17323
+ options: {
17324
+ to: { type: "string" },
17325
+ id: { type: "string" },
17326
+ help: { type: "boolean", short: "h" }
17327
+ }
17328
+ });
17329
+ } catch (parseError) {
17330
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
17331
+ return fail("USAGE", message, HELP16);
17332
+ }
17333
+ if (parsed.values.help) {
17334
+ await writeStdout(HELP16);
17335
+ return EXIT.OK;
17336
+ }
17337
+ const path = parsed.positionals[0];
17338
+ if (!path)
17339
+ return fail("USAGE", "Missing FILE argument", HELP16);
17340
+ const outputDir = parsed.values.to;
17341
+ if (!outputDir)
17342
+ return fail("USAGE", "Missing --to DIR", HELP16);
17343
+ const targetId = parsed.values.id;
17344
+ const view = await openOrFail(path);
17345
+ if (typeof view === "number")
17346
+ return view;
17347
+ await enrichImageHashes(view);
17348
+ const allImages = [];
17349
+ collectImages(view.doc.blocks, allImages);
17350
+ const targets = targetId ? allImages.filter((image) => image.id === targetId) : allImages;
17351
+ if (targetId && targets.length === 0) {
17352
+ return fail("IMAGE_NOT_FOUND", `Image not found: ${targetId}`);
17353
+ }
17354
+ const manifest = [];
17355
+ const seenHashes = new Set;
17356
+ for (const image of targets) {
17357
+ const reference = view.imageById.get(image.id);
17358
+ if (!reference)
17359
+ continue;
17360
+ const extension = extensionFor(image.contentType, reference.partName);
17361
+ const fileName = `${image.hash}.${extension}`;
17362
+ const outputPath = join(outputDir, fileName);
17363
+ if (!seenHashes.has(image.hash)) {
17364
+ const bytes = await view.pkg.readBytes(reference.partName);
17365
+ await Bun.write(outputPath, bytes);
17366
+ seenHashes.add(image.hash);
17367
+ }
17368
+ manifest.push({
17369
+ id: image.id,
17370
+ path: outputPath,
17371
+ bytes: (await Bun.file(outputPath).arrayBuffer()).byteLength
17372
+ });
17373
+ }
17374
+ await respond({ ok: true, operation: "images.extract", path, manifest });
17375
+ return EXIT.OK;
17376
+ }
17377
+ function collectImages(blocks, out) {
17378
+ for (const block of blocks) {
17379
+ if (block.type === "paragraph") {
17380
+ for (const run17 of block.runs) {
17381
+ if (run17.type === "image")
17382
+ out.push(run17);
17383
+ }
17384
+ continue;
17385
+ }
17386
+ if (block.type === "table") {
17387
+ for (const row of block.rows) {
17388
+ for (const cell of row.cells) {
17389
+ collectImages(cell.blocks, out);
17390
+ }
17391
+ }
17392
+ }
17393
+ }
17394
+ }
17395
+ function extensionFor(contentType, partName) {
17396
+ const fromType = EXTENSION_BY_CONTENT_TYPE[contentType.toLowerCase()];
17397
+ if (fromType)
17398
+ return fromType;
17399
+ const fromName = partName.split(".").pop()?.toLowerCase();
17400
+ return fromName ?? "bin";
17401
+ }
17402
+ var HELP16 = `docx images extract \u2014 dump image bytes to a directory
17403
+
17404
+ Usage:
17405
+ docx images extract FILE --to DIR [options]
17406
+
17407
+ Required:
17408
+ --to DIR Output directory (created if missing)
17409
+
17410
+ Optional:
17411
+ --id IMG_ID Extract a single image (default: extract all)
17412
+ -h, --help Show this help
17413
+
17414
+ Files are named <hash>.<ext> where hash is the sha256 of the image bytes
17415
+ and ext is derived from contentType. Returns a manifest mapping image ids
17416
+ to written paths.
17417
+
17418
+ Examples:
17419
+ docx images extract doc.docx --to ./media
17420
+ docx images extract doc.docx --to ./media --id img2
17421
+ `, EXTENSION_BY_CONTENT_TYPE;
17422
+ var init_extract = __esm(() => {
17423
+ init_core();
17424
+ init_respond();
17425
+ EXTENSION_BY_CONTENT_TYPE = {
17426
+ "image/png": "png",
17427
+ "image/jpeg": "jpg",
17428
+ "image/jpg": "jpg",
17429
+ "image/gif": "gif",
17430
+ "image/svg+xml": "svg",
17431
+ "image/webp": "webp",
17432
+ "image/tiff": "tif",
17433
+ "image/bmp": "bmp",
17434
+ "image/x-emf": "emf",
17435
+ "image/x-wmf": "wmf",
17436
+ "image/vnd.microsoft.icon": "ico"
17437
+ };
17438
+ });
17439
+
17440
+ // src/cli/images/list.ts
17441
+ var exports_list3 = {};
17442
+ __export(exports_list3, {
17443
+ run: () => run17
17444
+ });
17445
+ import { parseArgs as parseArgs15 } from "util";
17446
+ async function run17(args) {
17447
+ let parsed;
17448
+ try {
17449
+ parsed = parseArgs15({
17450
+ args,
17451
+ allowPositionals: true,
17452
+ options: {
17453
+ help: { type: "boolean", short: "h" }
17454
+ }
17455
+ });
17456
+ } catch (parseError) {
17457
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
17458
+ return fail("USAGE", message, HELP17);
17459
+ }
17460
+ if (parsed.values.help) {
17461
+ await writeStdout(HELP17);
17462
+ return EXIT.OK;
17463
+ }
17464
+ const path = parsed.positionals[0];
17465
+ if (!path)
17466
+ return fail("USAGE", "Missing FILE argument", HELP17);
17467
+ const view = await openOrFail(path);
17468
+ if (typeof view === "number")
17469
+ return view;
17470
+ await enrichImageHashes(view);
17471
+ const images = [];
17472
+ collectImages2(view.doc.blocks, images);
17473
+ await respond(images);
17474
+ return EXIT.OK;
17475
+ }
17476
+ function collectImages2(blocks, out) {
17477
+ for (const block of blocks) {
17478
+ if (block.type === "paragraph") {
17479
+ for (const run18 of block.runs) {
17480
+ if (run18.type === "image")
17481
+ out.push(run18);
17482
+ }
17483
+ continue;
17484
+ }
17485
+ if (block.type === "table") {
17486
+ for (const row of block.rows) {
17487
+ for (const cell of row.cells) {
17488
+ collectImages2(cell.blocks, out);
17489
+ }
17490
+ }
17491
+ }
16814
17492
  }
16815
17493
  }
16816
- var HELP12 = `docx images list \u2014 print image manifest as JSON
17494
+ var HELP17 = `docx images list \u2014 print image manifest as JSON
16817
17495
 
16818
17496
  Usage:
16819
17497
  docx images list FILE [options]
@@ -16824,21 +17502,21 @@ Options:
16824
17502
  Examples:
16825
17503
  docx images list doc.docx | jq -c '.[] | {id, contentType, hash}'
16826
17504
  `;
16827
- var init_list2 = __esm(() => {
17505
+ var init_list3 = __esm(() => {
16828
17506
  init_core();
16829
17507
  init_respond();
16830
17508
  });
16831
17509
 
16832
17510
  // src/cli/images/replace.ts
16833
- var exports_replace = {};
16834
- __export(exports_replace, {
16835
- run: () => run13
17511
+ var exports_replace2 = {};
17512
+ __export(exports_replace2, {
17513
+ run: () => run18
16836
17514
  });
16837
- import { parseArgs as parseArgs12 } from "util";
16838
- async function run13(args) {
17515
+ import { parseArgs as parseArgs16 } from "util";
17516
+ async function run18(args) {
16839
17517
  let parsed;
16840
17518
  try {
16841
- parsed = parseArgs12({
17519
+ parsed = parseArgs16({
16842
17520
  args,
16843
17521
  allowPositionals: true,
16844
17522
  options: {
@@ -16851,21 +17529,21 @@ async function run13(args) {
16851
17529
  });
16852
17530
  } catch (parseError) {
16853
17531
  const message = parseError instanceof Error ? parseError.message : String(parseError);
16854
- return fail("USAGE", message, HELP13);
17532
+ return fail("USAGE", message, HELP18);
16855
17533
  }
16856
17534
  if (parsed.values.help) {
16857
- await writeStdout(HELP13);
17535
+ await writeStdout(HELP18);
16858
17536
  return EXIT.OK;
16859
17537
  }
16860
17538
  const path = parsed.positionals[0];
16861
17539
  if (!path)
16862
- return fail("USAGE", "Missing FILE argument", HELP13);
17540
+ return fail("USAGE", "Missing FILE argument", HELP18);
16863
17541
  const targetId = parsed.values.at;
16864
17542
  if (!targetId)
16865
- return fail("USAGE", "Missing --at IMG_ID", HELP13);
17543
+ return fail("USAGE", "Missing --at IMG_ID", HELP18);
16866
17544
  const sourcePath = parsed.values.with;
16867
17545
  if (!sourcePath)
16868
- return fail("USAGE", "Missing --with PATH", HELP13);
17546
+ return fail("USAGE", "Missing --with PATH", HELP18);
16869
17547
  const sourceFile = Bun.file(sourcePath);
16870
17548
  if (!await sourceFile.exists()) {
16871
17549
  return fail("FILE_NOT_FOUND", `Replacement file not found: ${sourcePath}`);
@@ -16904,7 +17582,7 @@ async function run13(args) {
16904
17582
  } else {
16905
17583
  view.pkg.writeBytes(newPartName, bytes);
16906
17584
  view.pkg.deletePart(originalPartName);
16907
- updateRelationshipTarget(view.relationshipsTree, reference.relationshipId, relativeTargetFor(newPartName));
17585
+ updateRelationshipTarget2(view.relationshipsTree, reference.relationshipId, relativeTargetFor(newPartName));
16908
17586
  ensureContentTypeDefault(view.contentTypesTree, newExtension, newMimeType);
16909
17587
  reference.partName = newPartName;
16910
17588
  reference.contentType = newMimeType;
@@ -16932,7 +17610,7 @@ function renameExtension(partName, newExtension) {
16932
17610
  function relativeTargetFor(partName) {
16933
17611
  return partName.startsWith("word/") ? partName.slice("word/".length) : partName;
16934
17612
  }
16935
- function updateRelationshipTarget(relationshipsTree, relationshipId, newTarget) {
17613
+ function updateRelationshipTarget2(relationshipsTree, relationshipId, newTarget) {
16936
17614
  const relationships = XmlNode2.findRoot(relationshipsTree, "Relationships");
16937
17615
  if (!relationships)
16938
17616
  return;
@@ -16957,7 +17635,7 @@ function ensureContentTypeDefault(contentTypesTree, extension, mimeType) {
16957
17635
  }
16958
17636
  types.children.push(new XmlNode2("Default", { Extension: extension, ContentType: mimeType }));
16959
17637
  }
16960
- var HELP13 = `docx images replace \u2014 swap an image's bytes
17638
+ var HELP18 = `docx images replace \u2014 swap an image's bytes
16961
17639
 
16962
17640
  Usage:
16963
17641
  docx images replace FILE --at IMG_ID --with PATH [options]
@@ -16979,7 +17657,7 @@ Examples:
16979
17657
  docx images replace doc.docx --at img2 --with ./new-photo.png
16980
17658
  docx images replace doc.docx --at img0 --with ./diagram.svg
16981
17659
  `, EXTENSION_BY_MIME;
16982
- var init_replace = __esm(() => {
17660
+ var init_replace2 = __esm(() => {
16983
17661
  init_core();
16984
17662
  init_parser();
16985
17663
  init_respond();
@@ -17000,22 +17678,22 @@ var init_replace = __esm(() => {
17000
17678
  // src/cli/images/index.ts
17001
17679
  var exports_images = {};
17002
17680
  __export(exports_images, {
17003
- run: () => run14
17681
+ run: () => run19
17004
17682
  });
17005
- async function run14(args) {
17683
+ async function run19(args) {
17006
17684
  const verb = args[0];
17007
17685
  if (!verb || verb === "--help" || verb === "-h" || verb === "help") {
17008
- await writeStdout(HELP14);
17686
+ await writeStdout(HELP19);
17009
17687
  return verb ? 0 : 2;
17010
17688
  }
17011
- const loader = SUBCOMMANDS2[verb];
17689
+ const loader = SUBCOMMANDS3[verb];
17012
17690
  if (!loader) {
17013
17691
  return fail("USAGE", `Unknown images subcommand: ${verb}`, 'Run "docx images --help".');
17014
17692
  }
17015
17693
  const module_ = await loader();
17016
17694
  return module_.run(args.slice(1));
17017
17695
  }
17018
- var SUBCOMMANDS2, HELP14 = `docx images \u2014 manage embedded images
17696
+ var SUBCOMMANDS3, HELP19 = `docx images \u2014 manage embedded images
17019
17697
 
17020
17698
  Usage:
17021
17699
  docx images <verb> FILE [options]
@@ -17029,10 +17707,10 @@ Run "docx images <verb> --help" for verb-specific help.
17029
17707
  `;
17030
17708
  var init_images = __esm(() => {
17031
17709
  init_respond();
17032
- SUBCOMMANDS2 = {
17710
+ SUBCOMMANDS3 = {
17033
17711
  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))
17712
+ list: () => Promise.resolve().then(() => (init_list3(), exports_list3)),
17713
+ replace: () => Promise.resolve().then(() => (init_replace2(), exports_replace2))
17036
17714
  };
17037
17715
  });
17038
17716
 
@@ -17097,6 +17775,14 @@ export type TextRun = {
17097
17775
  sizeHalfPoints?: number;
17098
17776
  comments?: string[];
17099
17777
  trackedChange?: TrackedChange;
17778
+ hyperlink?: Hyperlink;
17779
+ };
17780
+
17781
+ export type Hyperlink = {
17782
+ id: string;
17783
+ url?: string;
17784
+ anchor?: string;
17785
+ tooltip?: string;
17100
17786
  };
17101
17787
 
17102
17788
  export type ImageRun = {
@@ -17148,13 +17834,13 @@ var init_types = () => {};
17148
17834
  // src/cli/info/schema.ts
17149
17835
  var exports_schema = {};
17150
17836
  __export(exports_schema, {
17151
- run: () => run15
17837
+ run: () => run20
17152
17838
  });
17153
- import { parseArgs as parseArgs13 } from "util";
17154
- async function run15(args) {
17839
+ import { parseArgs as parseArgs17 } from "util";
17840
+ async function run20(args) {
17155
17841
  let parsed;
17156
17842
  try {
17157
- parsed = parseArgs13({
17843
+ parsed = parseArgs17({
17158
17844
  args,
17159
17845
  allowPositionals: true,
17160
17846
  options: {
@@ -17165,10 +17851,10 @@ async function run15(args) {
17165
17851
  });
17166
17852
  } catch (parseError) {
17167
17853
  const message = parseError instanceof Error ? parseError.message : String(parseError);
17168
- return fail("USAGE", message, HELP15);
17854
+ return fail("USAGE", message, HELP20);
17169
17855
  }
17170
17856
  if (parsed.values.help) {
17171
- await writeStdout(HELP15);
17857
+ await writeStdout(HELP20);
17172
17858
  return EXIT.OK;
17173
17859
  }
17174
17860
  if (parsed.values.ts) {
@@ -17178,7 +17864,7 @@ async function run15(args) {
17178
17864
  await respond(JSON_SCHEMA);
17179
17865
  return EXIT.OK;
17180
17866
  }
17181
- var HELP15 = `docx schema \u2014 print the AST type definitions
17867
+ var HELP20 = `docx schema \u2014 print the AST type definitions
17182
17868
 
17183
17869
  Usage:
17184
17870
  docx schema [options]
@@ -17275,7 +17961,18 @@ var init_schema = __esm(() => {
17275
17961
  date: { type: "string" },
17276
17962
  revisionId: { type: "string" }
17277
17963
  }
17278
- }
17964
+ },
17965
+ hyperlink: { $ref: "#/$defs/Hyperlink" }
17966
+ }
17967
+ },
17968
+ Hyperlink: {
17969
+ type: "object",
17970
+ required: ["id"],
17971
+ properties: {
17972
+ id: { type: "string" },
17973
+ url: { type: "string" },
17974
+ anchor: { type: "string" },
17975
+ tooltip: { type: "string" }
17279
17976
  }
17280
17977
  },
17281
17978
  ImageRun: {
@@ -17370,13 +18067,13 @@ var init_schema = __esm(() => {
17370
18067
  // src/cli/info/locators.ts
17371
18068
  var exports_locators = {};
17372
18069
  __export(exports_locators, {
17373
- run: () => run16
18070
+ run: () => run21
17374
18071
  });
17375
- import { parseArgs as parseArgs14 } from "util";
17376
- async function run16(args) {
18072
+ import { parseArgs as parseArgs18 } from "util";
18073
+ async function run21(args) {
17377
18074
  let parsed;
17378
18075
  try {
17379
- parsed = parseArgs14({
18076
+ parsed = parseArgs18({
17380
18077
  args,
17381
18078
  allowPositionals: true,
17382
18079
  options: {
@@ -17386,10 +18083,10 @@ async function run16(args) {
17386
18083
  });
17387
18084
  } catch (parseError) {
17388
18085
  const message = parseError instanceof Error ? parseError.message : String(parseError);
17389
- return fail("USAGE", message, HELP16);
18086
+ return fail("USAGE", message, HELP21);
17390
18087
  }
17391
18088
  if (parsed.values.help) {
17392
- await writeStdout(HELP16);
18089
+ await writeStdout(HELP21);
17393
18090
  return EXIT.OK;
17394
18091
  }
17395
18092
  if (parsed.values.json) {
@@ -17400,7 +18097,7 @@ async function run16(args) {
17400
18097
  await writeStdout(REFERENCE);
17401
18098
  return EXIT.OK;
17402
18099
  }
17403
- var HELP16 = `docx locators \u2014 print the locator grammar reference
18100
+ var HELP21 = `docx locators \u2014 print the locator grammar reference
17404
18101
 
17405
18102
  Usage:
17406
18103
  docx locators [options]
@@ -17426,6 +18123,7 @@ Range locators (across blocks):
17426
18123
  Entity locators:
17427
18124
  cN Comment id (e.g., c0)
17428
18125
  imgN Image id (e.g., img2)
18126
+ linkN Hyperlink id (e.g., link0)
17429
18127
  tN:rRcC Cell at row R, column C of table tN
17430
18128
 
17431
18129
  Examples:
@@ -17434,6 +18132,7 @@ Examples:
17434
18132
  p3:5-p5:10 -> from char 5 of p3 to char 10 of p5
17435
18133
  c1 -> comment c1
17436
18134
  img0 -> image img0
18135
+ link0 -> hyperlink link0
17437
18136
  t0:r1c2 -> cell at row 1, col 2 of table t0
17438
18137
  t0:r1c2:p0 -> first paragraph of that cell
17439
18138
  t0:r1c2:p0:5-10 -> chars 5..10 of that paragraph
@@ -17466,6 +18165,7 @@ var init_locators2 = __esm(() => {
17466
18165
  entityLocators: {
17467
18166
  comment: { syntax: "cN", example: "c1" },
17468
18167
  image: { syntax: "imgN", example: "img0" },
18168
+ hyperlink: { syntax: "linkN", example: "link0" },
17469
18169
  cell: { syntax: "tN:rRcC", example: "t0:r1c2" },
17470
18170
  nestedCell: { syntax: "tN:rRcC:pK", example: "t0:r1c2:p0" }
17471
18171
  },
@@ -17480,22 +18180,22 @@ var init_locators2 = __esm(() => {
17480
18180
  // src/cli/info/index.ts
17481
18181
  var exports_info = {};
17482
18182
  __export(exports_info, {
17483
- run: () => run17
18183
+ run: () => run22
17484
18184
  });
17485
- async function run17(args) {
18185
+ async function run22(args) {
17486
18186
  const topic = args[0];
17487
18187
  if (!topic || topic === "--help" || topic === "-h" || topic === "help") {
17488
- await writeStdout(HELP17);
18188
+ await writeStdout(HELP22);
17489
18189
  return topic ? 0 : 2;
17490
18190
  }
17491
- const loader = SUBCOMMANDS3[topic];
18191
+ const loader = SUBCOMMANDS4[topic];
17492
18192
  if (!loader) {
17493
18193
  return fail("USAGE", `Unknown info topic: ${topic}`, 'Run "docx info --help".');
17494
18194
  }
17495
18195
  const module_ = await loader();
17496
18196
  return module_.run(args.slice(1));
17497
18197
  }
17498
- var SUBCOMMANDS3, HELP17 = `docx info \u2014 print reference material about the CLI
18198
+ var SUBCOMMANDS4, HELP22 = `docx info \u2014 print reference material about the CLI
17499
18199
 
17500
18200
  Usage:
17501
18201
  docx info <topic> [options]
@@ -17508,7 +18208,7 @@ Run "docx info <topic> --help" for topic-specific help.
17508
18208
  `;
17509
18209
  var init_info = __esm(() => {
17510
18210
  init_respond();
17511
- SUBCOMMANDS3 = {
18211
+ SUBCOMMANDS4 = {
17512
18212
  schema: () => Promise.resolve().then(() => (init_schema(), exports_schema)),
17513
18213
  locators: () => Promise.resolve().then(() => (init_locators2(), exports_locators))
17514
18214
  };
@@ -17517,13 +18217,13 @@ var init_info = __esm(() => {
17517
18217
  // src/cli/insert/index.tsx
17518
18218
  var exports_insert = {};
17519
18219
  __export(exports_insert, {
17520
- run: () => run18
18220
+ run: () => run23
17521
18221
  });
17522
- import { parseArgs as parseArgs15 } from "util";
17523
- async function run18(args) {
18222
+ import { parseArgs as parseArgs19 } from "util";
18223
+ async function run23(args) {
17524
18224
  let parsed;
17525
18225
  try {
17526
- parsed = parseArgs15({
18226
+ parsed = parseArgs19({
17527
18227
  args,
17528
18228
  allowPositionals: true,
17529
18229
  options: {
@@ -17536,6 +18236,7 @@ async function run18(args) {
17536
18236
  color: { type: "string" },
17537
18237
  bold: { type: "boolean" },
17538
18238
  italic: { type: "boolean" },
18239
+ url: { type: "string" },
17539
18240
  output: { type: "string", short: "o" },
17540
18241
  "dry-run": { type: "boolean" },
17541
18242
  help: { type: "boolean", short: "h" }
@@ -17543,30 +18244,34 @@ async function run18(args) {
17543
18244
  });
17544
18245
  } catch (parseError) {
17545
18246
  const message = parseError instanceof Error ? parseError.message : String(parseError);
17546
- return fail("USAGE", message, HELP18);
18247
+ return fail("USAGE", message, HELP23);
17547
18248
  }
17548
18249
  if (parsed.values.help) {
17549
- await writeStdout(HELP18);
18250
+ await writeStdout(HELP23);
17550
18251
  return EXIT.OK;
17551
18252
  }
17552
18253
  const path = parsed.positionals[0];
17553
18254
  if (!path)
17554
- return fail("USAGE", "Missing FILE argument", HELP18);
18255
+ return fail("USAGE", "Missing FILE argument", HELP23);
17555
18256
  const after = parsed.values.after;
17556
18257
  const before = parsed.values.before;
17557
18258
  if (!after && !before) {
17558
- return fail("USAGE", "Missing locator: pass --after or --before", HELP18);
18259
+ return fail("USAGE", "Missing locator: pass --after or --before", HELP23);
17559
18260
  }
17560
18261
  if (after && before) {
17561
- return fail("USAGE", "Pass either --after or --before, not both", HELP18);
18262
+ return fail("USAGE", "Pass either --after or --before, not both", HELP23);
17562
18263
  }
17563
18264
  const text = parsed.values.text;
17564
18265
  const runsJson = parsed.values.runs;
18266
+ const url = parsed.values.url;
17565
18267
  if (!text && !runsJson) {
17566
- return fail("USAGE", "Missing content: pass --text or --runs", HELP18);
18268
+ return fail("USAGE", "Missing content: pass --text or --runs", HELP23);
17567
18269
  }
17568
18270
  if (text && runsJson) {
17569
- return fail("USAGE", "Pass either --text or --runs, not both", HELP18);
18271
+ return fail("USAGE", "Pass either --text or --runs, not both", HELP23);
18272
+ }
18273
+ if (url && !text) {
18274
+ return fail("USAGE", "--url requires --text", HELP23);
17570
18275
  }
17571
18276
  const paragraphOptions = {};
17572
18277
  const styleValue = parsed.values.style;
@@ -17596,6 +18301,8 @@ async function run18(args) {
17596
18301
  ...parsed.values.bold ? { bold: true } : {},
17597
18302
  ...parsed.values.italic ? { italic: true } : {}
17598
18303
  }, undefined, false, undefined, this);
18304
+ if (url)
18305
+ wrapFirstRunInHyperlink(view, paragraphNode, url);
17599
18306
  } else {
17600
18307
  let runsValue;
17601
18308
  try {
@@ -17644,6 +18351,29 @@ async function run18(args) {
17644
18351
  });
17645
18352
  return EXIT.OK;
17646
18353
  }
18354
+ function wrapFirstRunInHyperlink(view, paragraph, url) {
18355
+ const relationships = XmlNode2.findRoot(view.relationshipsTree, "Relationships");
18356
+ if (!relationships) {
18357
+ throw new Error("Missing <Relationships> root in document rels");
18358
+ }
18359
+ const relationshipId = addHyperlinkRelationship(relationships, url);
18360
+ view.hyperlinksByRelationshipId.set(relationshipId, { url });
18361
+ const newChildren = [];
18362
+ let wrapped = false;
18363
+ for (const child of paragraph.children) {
18364
+ if (!wrapped && child.tag === "w:r") {
18365
+ const wrapper = /* @__PURE__ */ jsxDEV(w.hyperlink, {
18366
+ "r:id": relationshipId,
18367
+ children: child
18368
+ }, undefined, false, undefined, this);
18369
+ newChildren.push(wrapper);
18370
+ wrapped = true;
18371
+ continue;
18372
+ }
18373
+ newChildren.push(child);
18374
+ }
18375
+ paragraph.children = newChildren;
18376
+ }
17647
18377
  function applyTrackedInsertion(paragraph, view) {
17648
18378
  const allocator = createRevisionAllocator(view);
17649
18379
  const baseMeta = { author: resolveAuthor(), date: resolveDate() };
@@ -17670,58 +18400,197 @@ function applyTrackedInsertion(paragraph, view) {
17670
18400
  flush();
17671
18401
  newChildren.push(child);
17672
18402
  }
17673
- flush();
17674
- paragraph.children = newChildren;
17675
- markParagraphMarkAs(paragraph, "ins", mintMeta());
18403
+ flush();
18404
+ paragraph.children = newChildren;
18405
+ markParagraphMarkAs(paragraph, "ins", mintMeta());
18406
+ }
18407
+ var HELP23 = `docx insert \u2014 insert a paragraph at a locator
18408
+
18409
+ Usage:
18410
+ docx insert FILE [options]
18411
+
18412
+ Locator (one required):
18413
+ --after LOCATOR Insert after the block at LOCATOR (e.g., p3)
18414
+ --before LOCATOR Insert before the block at LOCATOR
18415
+
18416
+ Content (one required):
18417
+ --text TEXT Insert a paragraph with this text
18418
+ --runs JSON Insert a paragraph with custom runs (Run[] JSON)
18419
+
18420
+ Paragraph options:
18421
+ --style NAME Apply paragraph style (e.g., Heading1)
18422
+ --alignment ALIGN left | center | right | justify
18423
+
18424
+ Run options (only with --text):
18425
+ --color HEX Run color, hex (e.g., 800080 for purple)
18426
+ --bold Bold
18427
+ --italic Italic
18428
+ --url URL Wrap the inserted text in a hyperlink to URL
18429
+
18430
+ -o, --output PATH Write to PATH instead of overwriting FILE
18431
+ --dry-run Print what would be inserted; do not write the file
18432
+ -h, --help Show this help
18433
+
18434
+ Examples:
18435
+ docx insert doc.docx --after p3 --text "Section header" --style Heading2
18436
+ docx insert doc.docx --before p0 --text "ALERT" --color CC0000 --bold
18437
+ docx insert doc.docx --after p2 --runs '[{"type":"text","text":"X","bold":true}]'
18438
+ docx insert doc.docx --after p3 --text "click here" --url https://example.com
18439
+ `;
18440
+ var init_insert = __esm(() => {
18441
+ init_core();
18442
+ init_jsx();
18443
+ init_parser();
18444
+ init_respond();
18445
+ init_emit2();
18446
+ init_jsx_dev_runtime();
18447
+ });
18448
+
18449
+ // src/cli/outline/build.ts
18450
+ function buildOutline(doc, options = {}) {
18451
+ const stylePrefix = options.stylePrefix ?? "Heading";
18452
+ const root = {
18453
+ id: "",
18454
+ locator: "",
18455
+ level: 0,
18456
+ style: "",
18457
+ text: "",
18458
+ children: []
18459
+ };
18460
+ const stack = [root];
18461
+ for (const paragraph of headingParagraphs(doc.blocks, stylePrefix)) {
18462
+ const level = headingLevel(paragraph.style, stylePrefix);
18463
+ if (level === null)
18464
+ continue;
18465
+ while ((stack[stack.length - 1]?.level ?? 0) >= level)
18466
+ stack.pop();
18467
+ const parent = stack[stack.length - 1] ?? root;
18468
+ const entry = {
18469
+ id: paragraph.id,
18470
+ locator: paragraph.id,
18471
+ level,
18472
+ style: paragraph.style ?? "",
18473
+ text: paragraphText(paragraph),
18474
+ children: []
18475
+ };
18476
+ parent.children.push(entry);
18477
+ stack.push(entry);
18478
+ }
18479
+ return root.children;
18480
+ }
18481
+ function headingLevel(style, stylePrefix) {
18482
+ if (!style)
18483
+ return null;
18484
+ if (!style.startsWith(stylePrefix))
18485
+ return null;
18486
+ const remainder = style.slice(stylePrefix.length).trim();
18487
+ if (remainder === "")
18488
+ return 1;
18489
+ const parsed = Number(remainder);
18490
+ if (!Number.isInteger(parsed) || parsed < 1)
18491
+ return null;
18492
+ return parsed;
18493
+ }
18494
+ function* headingParagraphs(blocks, stylePrefix) {
18495
+ for (const block of blocks) {
18496
+ if (block.type !== "paragraph")
18497
+ continue;
18498
+ if (headingLevel(block.style, stylePrefix) === null)
18499
+ continue;
18500
+ yield block;
18501
+ }
18502
+ }
18503
+ function paragraphText(paragraph) {
18504
+ let out = "";
18505
+ for (const run24 of paragraph.runs) {
18506
+ if (run24.type === "text")
18507
+ out += run24.text;
18508
+ }
18509
+ return out;
18510
+ }
18511
+
18512
+ // src/cli/outline/index.ts
18513
+ var exports_outline = {};
18514
+ __export(exports_outline, {
18515
+ run: () => run24
18516
+ });
18517
+ import { parseArgs as parseArgs20 } from "util";
18518
+ async function run24(args) {
18519
+ let parsed;
18520
+ try {
18521
+ parsed = parseArgs20({
18522
+ args,
18523
+ allowPositionals: true,
18524
+ options: {
18525
+ "style-prefix": { type: "string" },
18526
+ help: { type: "boolean", short: "h" }
18527
+ }
18528
+ });
18529
+ } catch (parseError) {
18530
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
18531
+ return fail("USAGE", message, HELP24);
18532
+ }
18533
+ if (parsed.values.help) {
18534
+ await writeStdout(HELP24);
18535
+ return EXIT.OK;
18536
+ }
18537
+ const path = parsed.positionals[0];
18538
+ if (!path)
18539
+ return fail("USAGE", "Missing FILE argument", HELP24);
18540
+ const stylePrefix = parsed.values["style-prefix"] ?? "Heading";
18541
+ if (stylePrefix.length === 0) {
18542
+ return fail("USAGE", "--style-prefix cannot be empty");
18543
+ }
18544
+ const view = await openOrFail(path);
18545
+ if (typeof view === "number")
18546
+ return view;
18547
+ const outline = buildOutline(view.doc, { stylePrefix });
18548
+ await respond({
18549
+ ok: true,
18550
+ operation: "outline",
18551
+ path,
18552
+ stylePrefix,
18553
+ outline
18554
+ });
18555
+ return EXIT.OK;
17676
18556
  }
17677
- var HELP18 = `docx insert \u2014 insert a paragraph at a locator
18557
+ var HELP24 = `docx outline \u2014 list headings as a hierarchical tree
17678
18558
 
17679
18559
  Usage:
17680
- docx insert FILE [options]
17681
-
17682
- Locator (one required):
17683
- --after LOCATOR Insert after the block at LOCATOR (e.g., p3)
17684
- --before LOCATOR Insert before the block at LOCATOR
17685
-
17686
- Content (one required):
17687
- --text TEXT Insert a paragraph with this text
17688
- --runs JSON Insert a paragraph with custom runs (Run[] JSON)
18560
+ docx outline FILE [options]
17689
18561
 
17690
- Paragraph options:
17691
- --style NAME Apply paragraph style (e.g., Heading1)
17692
- --alignment ALIGN left | center | right | justify
18562
+ Options:
18563
+ --style-prefix S paragraph-style prefix that marks a heading (default: "Heading")
18564
+ -h, --help show this help
17693
18565
 
17694
- Run options (only with --text):
17695
- --color HEX Run color, hex (e.g., 800080 for purple)
17696
- --bold Bold
17697
- --italic Italic
18566
+ Walks top-level paragraphs whose style starts with the prefix and parses the
18567
+ trailing number as the heading level (e.g. "Heading1" \u2192 1, "Heading 2" \u2192 2).
18568
+ Paragraphs nested inside table cells are skipped \u2014 outlines reflect the
18569
+ document's structural skeleton, not embedded labels. Lower levels nest under
18570
+ higher ones; missing intermediate levels nest directly (H1 \u2192 H3 is fine).
17698
18571
 
17699
- -o, --output PATH Write to PATH instead of overwriting FILE
17700
- --dry-run Print what would be inserted; do not write the file
17701
- -h, --help Show this help
18572
+ Output is JSON: an array of entries, each shaped like
18573
+ { id, locator, level, style, text, children }.
17702
18574
 
17703
18575
  Examples:
17704
- docx insert doc.docx --after p3 --text "Section header" --style Heading2
17705
- docx insert doc.docx --before p0 --text "ALERT" --color CC0000 --bold
17706
- docx insert doc.docx --after p2 --runs '[{"type":"text","text":"X","bold":true}]'
18576
+ docx outline doc.docx
18577
+ docx outline doc.docx --style-prefix "Section"
18578
+ docx outline doc.docx | jq '.outline[].text'
17707
18579
  `;
17708
- var init_insert = __esm(() => {
17709
- init_core();
18580
+ var init_outline = __esm(() => {
17710
18581
  init_respond();
17711
- init_emit2();
17712
- init_jsx_dev_runtime();
17713
18582
  });
17714
18583
 
17715
18584
  // src/cli/read/index.ts
17716
18585
  var exports_read = {};
17717
18586
  __export(exports_read, {
17718
- run: () => run19
18587
+ run: () => run25
17719
18588
  });
17720
- import { parseArgs as parseArgs16 } from "util";
17721
- async function run19(args) {
18589
+ import { parseArgs as parseArgs21 } from "util";
18590
+ async function run25(args) {
17722
18591
  let parsed;
17723
18592
  try {
17724
- parsed = parseArgs16({
18593
+ parsed = parseArgs21({
17725
18594
  args,
17726
18595
  allowPositionals: true,
17727
18596
  options: {
@@ -17730,15 +18599,15 @@ async function run19(args) {
17730
18599
  }
17731
18600
  });
17732
18601
  } catch (e) {
17733
- return fail("USAGE", e instanceof Error ? e.message : String(e), HELP19);
18602
+ return fail("USAGE", e instanceof Error ? e.message : String(e), HELP25);
17734
18603
  }
17735
18604
  if (parsed.values.help) {
17736
- await writeStdout(HELP19);
18605
+ await writeStdout(HELP25);
17737
18606
  return EXIT.OK;
17738
18607
  }
17739
18608
  const path = parsed.positionals[0];
17740
18609
  if (!path)
17741
- return fail("USAGE", "Missing FILE argument", HELP19);
18610
+ return fail("USAGE", "Missing FILE argument", HELP25);
17742
18611
  const view = await openOrFail(path);
17743
18612
  if (typeof view === "number")
17744
18613
  return view;
@@ -17746,7 +18615,7 @@ async function run19(args) {
17746
18615
  await respond(view.doc);
17747
18616
  return EXIT.OK;
17748
18617
  }
17749
- var HELP19 = `docx read \u2014 print AST as JSON
18618
+ var HELP25 = `docx read \u2014 print AST as JSON
17750
18619
 
17751
18620
  Usage:
17752
18621
  docx read FILE [options]
@@ -17804,7 +18673,7 @@ function collectRunSlots(paragraph) {
17804
18673
  offset += length;
17805
18674
  continue;
17806
18675
  }
17807
- if (child.tag === "w:ins" || child.tag === "w:del") {
18676
+ if (child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:hyperlink") {
17808
18677
  for (const inner of child.children) {
17809
18678
  if (inner.tag !== "w:r")
17810
18679
  continue;
@@ -17829,14 +18698,14 @@ function rebuildContainer(container, baseOffset, span, replacement, runPropertie
17829
18698
  if (placed)
17830
18699
  return;
17831
18700
  placed = true;
17832
- const run20 = /* @__PURE__ */ jsxDEV(ReplacementRun, {
18701
+ const run26 = /* @__PURE__ */ jsxDEV(ReplacementRun, {
17833
18702
  runProperties,
17834
18703
  text: replacement
17835
18704
  }, undefined, false, undefined, this);
17836
18705
  newChildren.push(tracked && isParagraph ? /* @__PURE__ */ jsxDEV(Ins, {
17837
18706
  meta: mintMeta(tracked),
17838
- children: run20
17839
- }, undefined, false, undefined, this) : run20);
18707
+ children: run26
18708
+ }, undefined, false, undefined, this) : run26);
17840
18709
  };
17841
18710
  for (const child of container.children) {
17842
18711
  if (child.tag === "w:r") {
@@ -17872,7 +18741,7 @@ function rebuildContainer(container, baseOffset, span, replacement, runPropertie
17872
18741
  }
17873
18742
  continue;
17874
18743
  }
17875
- if (isParagraph && (child.tag === "w:ins" || child.tag === "w:del")) {
18744
+ if (isParagraph && (child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:hyperlink")) {
17876
18745
  let innerLength = 0;
17877
18746
  for (const inner of child.children) {
17878
18747
  if (inner.tag === "w:r")
@@ -17896,14 +18765,14 @@ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tr
17896
18765
  if (placed)
17897
18766
  return;
17898
18767
  placed = true;
17899
- const run20 = /* @__PURE__ */ jsxDEV(ReplacementRun, {
18768
+ const run26 = /* @__PURE__ */ jsxDEV(ReplacementRun, {
17900
18769
  runProperties,
17901
18770
  text: replacement
17902
18771
  }, undefined, false, undefined, this);
17903
18772
  newChildren.push(tracked ? /* @__PURE__ */ jsxDEV(Ins, {
17904
18773
  meta: mintMeta(tracked),
17905
- children: run20
17906
- }, undefined, false, undefined, this) : run20);
18774
+ children: run26
18775
+ }, undefined, false, undefined, this) : run26);
17907
18776
  };
17908
18777
  for (const child of paragraph.children) {
17909
18778
  if (child.tag === "w:r") {
@@ -17940,7 +18809,7 @@ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tr
17940
18809
  continue;
17941
18810
  }
17942
18811
  if (child.tag === "w:ins" || child.tag === "w:del") {
17943
- const innerLength = sumInnerRunLengths(child);
18812
+ const innerLength = sumInnerRunLengths2(child);
17944
18813
  const wrapperStart = offset;
17945
18814
  const wrapperEnd = offset + innerLength;
17946
18815
  offset = wrapperEnd;
@@ -17956,6 +18825,25 @@ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tr
17956
18825
  splitWrapperAcrossSpan(child, wrapperStart, span, tracked, newChildren, placeReplacement);
17957
18826
  continue;
17958
18827
  }
18828
+ if (child.tag === "w:hyperlink") {
18829
+ const innerLength = sumInnerRunLengths2(child);
18830
+ const wrapperStart = offset;
18831
+ const wrapperEnd = offset + innerLength;
18832
+ offset = wrapperEnd;
18833
+ if (wrapperEnd <= span.start) {
18834
+ newChildren.push(child);
18835
+ continue;
18836
+ }
18837
+ if (wrapperStart >= span.end) {
18838
+ placeReplacement();
18839
+ newChildren.push(child);
18840
+ continue;
18841
+ }
18842
+ splitHyperlinkAcrossSpan(child, wrapperStart, span, runProperties, replacement, tracked, newChildren, placeReplacement, () => {
18843
+ placed = true;
18844
+ });
18845
+ continue;
18846
+ }
17959
18847
  newChildren.push(child);
17960
18848
  }
17961
18849
  if (!placed)
@@ -18016,7 +18904,62 @@ function splitWrapperAcrossSpan(wrapper, wrapperStart, span, tracked, out, place
18016
18904
  out.push(postWrapper);
18017
18905
  }
18018
18906
  }
18019
- function sumInnerRunLengths(wrapper) {
18907
+ function splitHyperlinkAcrossSpan(wrapper, wrapperStart, span, runProperties, replacement, tracked, out, placeReplacement, markReplacementPlaced) {
18908
+ const wrapperEnd = wrapperStart + wrapper.children.reduce((total, inner) => inner.tag === "w:r" ? total + runTextLength(inner) : total, 0);
18909
+ const startsInside = span.start > wrapperStart && span.start < wrapperEnd;
18910
+ const preInner = [];
18911
+ const postInner = [];
18912
+ let innerOffset = wrapperStart;
18913
+ for (const inner of wrapper.children) {
18914
+ if (inner.tag !== "w:r") {
18915
+ preInner.push(inner);
18916
+ continue;
18917
+ }
18918
+ const length = runTextLength(inner);
18919
+ const runStart = innerOffset;
18920
+ const runEnd = innerOffset + length;
18921
+ innerOffset = runEnd;
18922
+ if (runEnd <= span.start) {
18923
+ preInner.push(inner);
18924
+ continue;
18925
+ }
18926
+ if (runStart >= span.end) {
18927
+ postInner.push(inner);
18928
+ continue;
18929
+ }
18930
+ const sliceStartInRun = Math.max(0, span.start - runStart);
18931
+ const sliceEndInRun = Math.min(length, span.end - runStart);
18932
+ if (sliceStartInRun > 0)
18933
+ preInner.push(sliceRun(inner, 0, sliceStartInRun));
18934
+ if (sliceEndInRun < length) {
18935
+ postInner.push(sliceRun(inner, sliceEndInRun, length));
18936
+ }
18937
+ }
18938
+ if (startsInside) {
18939
+ const innerReplacement = /* @__PURE__ */ jsxDEV(ReplacementRun, {
18940
+ runProperties,
18941
+ text: replacement
18942
+ }, undefined, false, undefined, this);
18943
+ preInner.push(tracked ? /* @__PURE__ */ jsxDEV(Ins, {
18944
+ meta: mintMeta(tracked),
18945
+ children: innerReplacement
18946
+ }, undefined, false, undefined, this) : innerReplacement);
18947
+ markReplacementPlaced();
18948
+ }
18949
+ if (preInner.length > 0) {
18950
+ const preWrapper = new XmlNode2("w:hyperlink", { ...wrapper.attributes });
18951
+ preWrapper.children = preInner;
18952
+ out.push(preWrapper);
18953
+ }
18954
+ if (!startsInside)
18955
+ placeReplacement();
18956
+ if (postInner.length > 0) {
18957
+ const postWrapper = new XmlNode2("w:hyperlink", { ...wrapper.attributes });
18958
+ postWrapper.children = postInner;
18959
+ out.push(postWrapper);
18960
+ }
18961
+ }
18962
+ function sumInnerRunLengths2(wrapper) {
18020
18963
  let total = 0;
18021
18964
  for (const inner of wrapper.children) {
18022
18965
  if (inner.tag === "w:r")
@@ -18027,8 +18970,8 @@ function sumInnerRunLengths(wrapper) {
18027
18970
  function mintMeta(tracked) {
18028
18971
  return { ...tracked.meta, revisionId: tracked.allocator.next() };
18029
18972
  }
18030
- function convertRunTextToDelText(run20) {
18031
- for (const child of run20.children) {
18973
+ function convertRunTextToDelText(run26) {
18974
+ for (const child of run26.children) {
18032
18975
  if (child.tag === "w:t")
18033
18976
  child.tag = "w:delText";
18034
18977
  }
@@ -18055,15 +18998,15 @@ var init_replace_span = __esm(() => {
18055
18998
  });
18056
18999
 
18057
19000
  // src/cli/replace/index.ts
18058
- var exports_replace2 = {};
18059
- __export(exports_replace2, {
18060
- run: () => run20
19001
+ var exports_replace3 = {};
19002
+ __export(exports_replace3, {
19003
+ run: () => run26
18061
19004
  });
18062
- import { parseArgs as parseArgs17 } from "util";
18063
- async function run20(args) {
19005
+ import { parseArgs as parseArgs22 } from "util";
19006
+ async function run26(args) {
18064
19007
  let parsed;
18065
19008
  try {
18066
- parsed = parseArgs17({
19009
+ parsed = parseArgs22({
18067
19010
  args,
18068
19011
  allowPositionals: true,
18069
19012
  options: {
@@ -18078,21 +19021,21 @@ async function run20(args) {
18078
19021
  });
18079
19022
  } catch (parseError) {
18080
19023
  const message = parseError instanceof Error ? parseError.message : String(parseError);
18081
- return fail("USAGE", message, HELP20);
19024
+ return fail("USAGE", message, HELP26);
18082
19025
  }
18083
19026
  if (parsed.values.help) {
18084
- await writeStdout(HELP20);
19027
+ await writeStdout(HELP26);
18085
19028
  return EXIT.OK;
18086
19029
  }
18087
19030
  const path = parsed.positionals[0];
18088
19031
  const pattern = parsed.positionals[1];
18089
19032
  const replacement = parsed.positionals[2];
18090
19033
  if (!path)
18091
- return fail("USAGE", "Missing FILE argument", HELP20);
19034
+ return fail("USAGE", "Missing FILE argument", HELP26);
18092
19035
  if (pattern == null)
18093
- return fail("USAGE", "Missing PATTERN argument", HELP20);
19036
+ return fail("USAGE", "Missing PATTERN argument", HELP26);
18094
19037
  if (replacement == null) {
18095
- return fail("USAGE", "Missing REPLACEMENT argument", HELP20);
19038
+ return fail("USAGE", "Missing REPLACEMENT argument", HELP26);
18096
19039
  }
18097
19040
  const ignoreCase = Boolean(parsed.values["ignore-case"]);
18098
19041
  const useRegex = Boolean(parsed.values.regex);
@@ -18194,7 +19137,7 @@ async function run20(args) {
18194
19137
  });
18195
19138
  return EXIT.OK;
18196
19139
  }
18197
- var HELP20 = `docx replace \u2014 substitute text spans (sed for docx)
19140
+ var HELP26 = `docx replace \u2014 substitute text spans (sed for docx)
18198
19141
 
18199
19142
  Usage:
18200
19143
  docx replace FILE PATTERN REPLACEMENT [options]
@@ -18228,7 +19171,7 @@ Examples:
18228
19171
  docx replace doc.docx "(\\w+) (\\w+)" "$2 $1" --regex --all
18229
19172
  docx replace doc.docx "wordy phrase" "tighter phrase" --all --dry-run
18230
19173
  `;
18231
- var init_replace2 = __esm(() => {
19174
+ var init_replace3 = __esm(() => {
18232
19175
  init_core();
18233
19176
  init_respond();
18234
19177
  init_replace_span();
@@ -18237,13 +19180,13 @@ var init_replace2 = __esm(() => {
18237
19180
  // src/cli/track-changes/index.tsx
18238
19181
  var exports_track_changes = {};
18239
19182
  __export(exports_track_changes, {
18240
- run: () => run21
19183
+ run: () => run27
18241
19184
  });
18242
- import { parseArgs as parseArgs18 } from "util";
18243
- async function run21(args) {
19185
+ import { parseArgs as parseArgs23 } from "util";
19186
+ async function run27(args) {
18244
19187
  let parsed;
18245
19188
  try {
18246
- parsed = parseArgs18({
19189
+ parsed = parseArgs23({
18247
19190
  args,
18248
19191
  allowPositionals: true,
18249
19192
  options: {
@@ -18254,18 +19197,18 @@ async function run21(args) {
18254
19197
  });
18255
19198
  } catch (parseError) {
18256
19199
  const message = parseError instanceof Error ? parseError.message : String(parseError);
18257
- return fail("USAGE", message, HELP21);
19200
+ return fail("USAGE", message, HELP27);
18258
19201
  }
18259
19202
  if (parsed.values.help) {
18260
- await writeStdout(HELP21);
19203
+ await writeStdout(HELP27);
18261
19204
  return EXIT.OK;
18262
19205
  }
18263
19206
  const path = parsed.positionals[0];
18264
19207
  if (!path)
18265
- return fail("USAGE", "Missing FILE argument", HELP21);
19208
+ return fail("USAGE", "Missing FILE argument", HELP27);
18266
19209
  const mode = parsed.positionals[1];
18267
19210
  if (mode !== "on" && mode !== "off") {
18268
- return fail("USAGE", `Expected "on" or "off", got: ${mode ?? "<nothing>"}`, HELP21);
19211
+ return fail("USAGE", `Expected "on" or "off", got: ${mode ?? "<nothing>"}`, HELP27);
18269
19212
  }
18270
19213
  const view = await openOrFail(path);
18271
19214
  if (typeof view === "number")
@@ -18317,7 +19260,7 @@ async function run21(args) {
18317
19260
  });
18318
19261
  return EXIT.OK;
18319
19262
  }
18320
- var HELP21 = `docx track-changes \u2014 toggle the document's tracked-changes mode
19263
+ var HELP27 = `docx track-changes \u2014 toggle the document's tracked-changes mode
18321
19264
 
18322
19265
  Usage:
18323
19266
  docx track-changes FILE on|off [options]
@@ -18344,10 +19287,297 @@ var init_track_changes2 = __esm(() => {
18344
19287
  init_respond();
18345
19288
  init_jsx_dev_runtime();
18346
19289
  });
19290
+
19291
+ // src/cli/wc/count.ts
19292
+ function countWords(text) {
19293
+ const matches = text.match(/\S+/g);
19294
+ return matches?.length ?? 0;
19295
+ }
19296
+ function paragraphText2(paragraph) {
19297
+ let out = "";
19298
+ for (const run28 of paragraph.runs) {
19299
+ if (run28.type === "text")
19300
+ out += run28.text;
19301
+ }
19302
+ return out;
19303
+ }
19304
+ function countWordsInBlocks(blocks) {
19305
+ let total = 0;
19306
+ for (const block of blocks) {
19307
+ if (block.type === "paragraph") {
19308
+ total += countWords(paragraphText2(block));
19309
+ } else if (block.type === "table") {
19310
+ for (const row of block.rows) {
19311
+ for (const cell of row.cells) {
19312
+ total += countWordsInBlocks(cell.blocks);
19313
+ }
19314
+ }
19315
+ }
19316
+ }
19317
+ return total;
19318
+ }
19319
+ function findBlockById(blocks, blockId) {
19320
+ for (const block of blocks) {
19321
+ if (block.id === blockId)
19322
+ return block;
19323
+ if (block.type === "table") {
19324
+ for (const row of block.rows) {
19325
+ for (const cell of row.cells) {
19326
+ const inner = findBlockById(cell.blocks, blockId);
19327
+ if (inner)
19328
+ return inner;
19329
+ }
19330
+ }
19331
+ }
19332
+ }
19333
+ return null;
19334
+ }
19335
+ function countWordsInParagraphSpan(paragraph, start, end) {
19336
+ const text = paragraphText2(paragraph);
19337
+ const clampedEnd = Math.min(Math.max(end, 0), text.length);
19338
+ const clampedStart = Math.min(Math.max(start, 0), clampedEnd);
19339
+ return countWords(text.slice(clampedStart, clampedEnd));
19340
+ }
19341
+ function countWordsInRange(paragraphsInOrder, startBlockId, startOffset, endBlockId, endOffset) {
19342
+ const startIndex = paragraphsInOrder.findIndex((p) => p.id === startBlockId);
19343
+ const endIndex = paragraphsInOrder.findIndex((p) => p.id === endBlockId);
19344
+ if (startIndex === -1 || endIndex === -1)
19345
+ return 0;
19346
+ if (endIndex < startIndex)
19347
+ return 0;
19348
+ if (startIndex === endIndex) {
19349
+ const paragraph = paragraphsInOrder[startIndex];
19350
+ if (!paragraph)
19351
+ return 0;
19352
+ return countWordsInParagraphSpan(paragraph, startOffset, endOffset);
19353
+ }
19354
+ let total = 0;
19355
+ const first = paragraphsInOrder[startIndex];
19356
+ if (first) {
19357
+ total += countWordsInParagraphSpan(first, startOffset, paragraphText2(first).length);
19358
+ }
19359
+ for (let index = startIndex + 1;index < endIndex; index++) {
19360
+ const middle = paragraphsInOrder[index];
19361
+ if (middle)
19362
+ total += countWords(paragraphText2(middle));
19363
+ }
19364
+ const last = paragraphsInOrder[endIndex];
19365
+ if (last)
19366
+ total += countWordsInParagraphSpan(last, 0, endOffset);
19367
+ return total;
19368
+ }
19369
+ function flattenParagraphs(blocks) {
19370
+ const out = [];
19371
+ for (const block of blocks) {
19372
+ if (block.type === "paragraph") {
19373
+ out.push(block);
19374
+ continue;
19375
+ }
19376
+ if (block.type === "table") {
19377
+ for (const row of block.rows) {
19378
+ for (const cell of row.cells) {
19379
+ out.push(...flattenParagraphs(cell.blocks));
19380
+ }
19381
+ }
19382
+ }
19383
+ }
19384
+ return out;
19385
+ }
19386
+
19387
+ // src/cli/wc/index.ts
19388
+ var exports_wc = {};
19389
+ __export(exports_wc, {
19390
+ run: () => run28
19391
+ });
19392
+ import { parseArgs as parseArgs24 } from "util";
19393
+ async function run28(args) {
19394
+ let parsed;
19395
+ try {
19396
+ parsed = parseArgs24({
19397
+ args,
19398
+ allowPositionals: true,
19399
+ options: {
19400
+ help: { type: "boolean", short: "h" }
19401
+ }
19402
+ });
19403
+ } catch (parseError) {
19404
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
19405
+ return fail("USAGE", message, HELP28);
19406
+ }
19407
+ if (parsed.values.help) {
19408
+ await writeStdout(HELP28);
19409
+ return EXIT.OK;
19410
+ }
19411
+ const path = parsed.positionals[0];
19412
+ const locatorInput = parsed.positionals[1];
19413
+ if (!path)
19414
+ return fail("USAGE", "Missing FILE argument", HELP28);
19415
+ const view = await openOrFail(path);
19416
+ if (typeof view === "number")
19417
+ return view;
19418
+ if (!locatorInput) {
19419
+ await respond({
19420
+ ok: true,
19421
+ operation: "wc",
19422
+ path,
19423
+ scope: "document",
19424
+ words: countWordsInBlocks(view.doc.blocks)
19425
+ });
19426
+ return EXIT.OK;
19427
+ }
19428
+ let locator;
19429
+ try {
19430
+ locator = parseLocator(locatorInput);
19431
+ } catch (error) {
19432
+ if (error instanceof LocatorParseError) {
19433
+ return fail("INVALID_LOCATOR", error.message);
19434
+ }
19435
+ throw error;
19436
+ }
19437
+ if (locator.kind === "comment" || locator.kind === "image") {
19438
+ return fail("USAGE", `Locator ${locatorInput} addresses a ${locator.kind}, not text`, "wc accepts paragraph, span, range, table, and cell locators.");
19439
+ }
19440
+ const blocks = view.doc.blocks;
19441
+ if (locator.kind === "block") {
19442
+ const block = findBlockById(blocks, locator.blockId);
19443
+ if (!block) {
19444
+ return fail("BLOCK_NOT_FOUND", `Block not found: ${locator.blockId}`);
19445
+ }
19446
+ const words = block.type === "paragraph" ? countWords(paragraphText2(block)) : block.type === "table" ? countWordsInBlocks([block]) : 0;
19447
+ await respond({
19448
+ ok: true,
19449
+ operation: "wc",
19450
+ path,
19451
+ locator: locatorInput,
19452
+ scope: block.type,
19453
+ words
19454
+ });
19455
+ return EXIT.OK;
19456
+ }
19457
+ if (locator.kind === "blockSpan") {
19458
+ const block = findBlockById(blocks, locator.blockId);
19459
+ if (!block || block.type !== "paragraph") {
19460
+ return fail("BLOCK_NOT_FOUND", `Paragraph not found: ${locator.blockId}`);
19461
+ }
19462
+ await respond({
19463
+ ok: true,
19464
+ operation: "wc",
19465
+ path,
19466
+ locator: locatorInput,
19467
+ scope: "paragraphSpan",
19468
+ words: countWordsInParagraphSpan(block, locator.start, locator.end)
19469
+ });
19470
+ return EXIT.OK;
19471
+ }
19472
+ if (locator.kind === "range") {
19473
+ const paragraphs = flattenParagraphs(blocks);
19474
+ const startExists = paragraphs.some((p) => p.id === locator.start.blockId);
19475
+ const endExists = paragraphs.some((p) => p.id === locator.end.blockId);
19476
+ if (!startExists) {
19477
+ return fail("BLOCK_NOT_FOUND", `Paragraph not found: ${locator.start.blockId}`);
19478
+ }
19479
+ if (!endExists) {
19480
+ return fail("BLOCK_NOT_FOUND", `Paragraph not found: ${locator.end.blockId}`);
19481
+ }
19482
+ await respond({
19483
+ ok: true,
19484
+ operation: "wc",
19485
+ path,
19486
+ locator: locatorInput,
19487
+ scope: "range",
19488
+ words: countWordsInRange(paragraphs, locator.start.blockId, locator.start.offset, locator.end.blockId, locator.end.offset)
19489
+ });
19490
+ return EXIT.OK;
19491
+ }
19492
+ if (locator.kind === "cell") {
19493
+ const cellPath = `${locator.tableId}:r${locator.row}c${locator.col}`;
19494
+ if (!locator.inner) {
19495
+ const cellBlocks = findCellBlocks(blocks, locator);
19496
+ if (!cellBlocks) {
19497
+ return fail("BLOCK_NOT_FOUND", `Cell not found: ${cellPath}`);
19498
+ }
19499
+ await respond({
19500
+ ok: true,
19501
+ operation: "wc",
19502
+ path,
19503
+ locator: locatorInput,
19504
+ scope: "cell",
19505
+ words: countWordsInBlocks(cellBlocks)
19506
+ });
19507
+ return EXIT.OK;
19508
+ }
19509
+ const innerBlockId = locator.inner.kind === "block" ? locator.inner.blockId : locator.inner.kind === "blockSpan" ? locator.inner.blockId : null;
19510
+ if (!innerBlockId) {
19511
+ return fail("USAGE", `Unsupported inner locator for cell: ${locatorInput}`, "Inside a cell, wc accepts pK or pK:S-E only.");
19512
+ }
19513
+ const composedId = `${cellPath}:${innerBlockId}`;
19514
+ const block = findBlockById(blocks, composedId);
19515
+ if (!block || block.type !== "paragraph") {
19516
+ return fail("BLOCK_NOT_FOUND", `Paragraph not found: ${composedId}`);
19517
+ }
19518
+ const words = locator.inner.kind === "blockSpan" ? countWordsInParagraphSpan(block, locator.inner.start, locator.inner.end) : countWords(paragraphText2(block));
19519
+ await respond({
19520
+ ok: true,
19521
+ operation: "wc",
19522
+ path,
19523
+ locator: locatorInput,
19524
+ scope: locator.inner.kind === "blockSpan" ? "paragraphSpan" : "paragraph",
19525
+ words
19526
+ });
19527
+ return EXIT.OK;
19528
+ }
19529
+ return fail("USAGE", `Unsupported locator: ${locatorInput}`);
19530
+ }
19531
+ function findCellBlocks(blocks, locator) {
19532
+ const block = findBlockById(blocks, locator.tableId);
19533
+ if (!block || block.type !== "table")
19534
+ return null;
19535
+ const row = block.rows[locator.row];
19536
+ if (!row)
19537
+ return null;
19538
+ const cell = row.cells[locator.col];
19539
+ if (!cell)
19540
+ return null;
19541
+ return cell.blocks;
19542
+ }
19543
+ var HELP28 = `docx wc \u2014 count words in a document or a locator-addressed slice
19544
+
19545
+ Usage:
19546
+ docx wc FILE [LOCATOR] [options]
19547
+
19548
+ Locators (optional; default: whole document):
19549
+ pN whole paragraph N
19550
+ pN:S-E chars S..E within paragraph N
19551
+ pN:S-pM:E cross-paragraph range
19552
+ tN whole table
19553
+ tN:rRcC whole cell
19554
+ tN:rRcC:pK paragraph K inside that cell
19555
+ tN:rRcC:pK:S-E span within a cell paragraph
19556
+
19557
+ Options:
19558
+ -h, --help show this help
19559
+
19560
+ Counting is whitespace-segmented (\\S+) over the joined paragraph text. Hidden
19561
+ content like images/breaks/tabs contributes no words. Tracked-change deletions
19562
+ are counted alongside insertions because their characters are still part of the
19563
+ on-disk text; accept or reject changes first if you need a delta on the
19564
+ "accepted" view.
19565
+
19566
+ Examples:
19567
+ docx wc doc.docx
19568
+ docx wc doc.docx p3
19569
+ docx wc doc.docx p3:0-120
19570
+ docx wc doc.docx p5:10-p9:42
19571
+ docx wc doc.docx t0:r1c0
19572
+ `;
19573
+ var init_wc = __esm(() => {
19574
+ init_core();
19575
+ init_respond();
19576
+ });
18347
19577
  // package.json
18348
19578
  var package_default = {
18349
19579
  name: "bun-docx",
18350
- version: "0.3.0",
19580
+ version: "0.5.0",
18351
19581
  description: "CLI for AI agents (Claude, Codex) to read, edit, and comment on .docx files with full format fidelity.",
18352
19582
  keywords: [
18353
19583
  "docx",
@@ -18422,8 +19652,11 @@ Commands:
18422
19652
  delete FILE Remove a block or run range
18423
19653
  find FILE QUERY Find text spans, return locators
18424
19654
  replace FILE PATTERN REPL Substitute text spans (sed for docx)
19655
+ wc FILE [LOCATOR] Count words in the doc or a slice
19656
+ outline FILE List headings as a hierarchical tree
18425
19657
  comments \u2026 Add, reply, resolve, delete, list comments
18426
19658
  images \u2026 Extract, replace, list images
19659
+ hyperlinks \u2026 List, replace hyperlinks
18427
19660
  track-changes FILE on|off Toggle tracked-changes mode
18428
19661
  info \u2026 Reference material (schema, locators)
18429
19662
 
@@ -18450,12 +19683,15 @@ var COMMANDS = {
18450
19683
  delete: () => Promise.resolve().then(() => (init_delete2(), exports_delete2)),
18451
19684
  edit: () => Promise.resolve().then(() => (init_edit(), exports_edit)),
18452
19685
  find: () => Promise.resolve().then(() => (init_find(), exports_find)),
19686
+ hyperlinks: () => Promise.resolve().then(() => (init_hyperlinks(), exports_hyperlinks)),
18453
19687
  images: () => Promise.resolve().then(() => (init_images(), exports_images)),
18454
19688
  info: () => Promise.resolve().then(() => (init_info(), exports_info)),
18455
19689
  insert: () => Promise.resolve().then(() => (init_insert(), exports_insert)),
19690
+ outline: () => Promise.resolve().then(() => (init_outline(), exports_outline)),
18456
19691
  read: () => Promise.resolve().then(() => (init_read2(), exports_read)),
18457
- replace: () => Promise.resolve().then(() => (init_replace2(), exports_replace2)),
18458
- "track-changes": () => Promise.resolve().then(() => (init_track_changes2(), exports_track_changes))
19692
+ replace: () => Promise.resolve().then(() => (init_replace3(), exports_replace3)),
19693
+ "track-changes": () => Promise.resolve().then(() => (init_track_changes2(), exports_track_changes)),
19694
+ wc: () => Promise.resolve().then(() => (init_wc(), exports_wc))
18459
19695
  };
18460
19696
  async function main(argv) {
18461
19697
  const args = argv.slice(2);