bun-docx 0.4.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 +1107 -297
  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() };
@@ -17674,7 +18404,7 @@ function applyTrackedInsertion(paragraph, view) {
17674
18404
  paragraph.children = newChildren;
17675
18405
  markParagraphMarkAs(paragraph, "ins", mintMeta());
17676
18406
  }
17677
- var HELP18 = `docx insert \u2014 insert a paragraph at a locator
18407
+ var HELP23 = `docx insert \u2014 insert a paragraph at a locator
17678
18408
 
17679
18409
  Usage:
17680
18410
  docx insert FILE [options]
@@ -17695,6 +18425,7 @@ Run options (only with --text):
17695
18425
  --color HEX Run color, hex (e.g., 800080 for purple)
17696
18426
  --bold Bold
17697
18427
  --italic Italic
18428
+ --url URL Wrap the inserted text in a hyperlink to URL
17698
18429
 
17699
18430
  -o, --output PATH Write to PATH instead of overwriting FILE
17700
18431
  --dry-run Print what would be inserted; do not write the file
@@ -17704,9 +18435,12 @@ Examples:
17704
18435
  docx insert doc.docx --after p3 --text "Section header" --style Heading2
17705
18436
  docx insert doc.docx --before p0 --text "ALERT" --color CC0000 --bold
17706
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
17707
18439
  `;
17708
18440
  var init_insert = __esm(() => {
17709
18441
  init_core();
18442
+ init_jsx();
18443
+ init_parser();
17710
18444
  init_respond();
17711
18445
  init_emit2();
17712
18446
  init_jsx_dev_runtime();
@@ -17768,9 +18502,9 @@ function* headingParagraphs(blocks, stylePrefix) {
17768
18502
  }
17769
18503
  function paragraphText(paragraph) {
17770
18504
  let out = "";
17771
- for (const run19 of paragraph.runs) {
17772
- if (run19.type === "text")
17773
- out += run19.text;
18505
+ for (const run24 of paragraph.runs) {
18506
+ if (run24.type === "text")
18507
+ out += run24.text;
17774
18508
  }
17775
18509
  return out;
17776
18510
  }
@@ -17778,13 +18512,13 @@ function paragraphText(paragraph) {
17778
18512
  // src/cli/outline/index.ts
17779
18513
  var exports_outline = {};
17780
18514
  __export(exports_outline, {
17781
- run: () => run19
18515
+ run: () => run24
17782
18516
  });
17783
- import { parseArgs as parseArgs16 } from "util";
17784
- async function run19(args) {
18517
+ import { parseArgs as parseArgs20 } from "util";
18518
+ async function run24(args) {
17785
18519
  let parsed;
17786
18520
  try {
17787
- parsed = parseArgs16({
18521
+ parsed = parseArgs20({
17788
18522
  args,
17789
18523
  allowPositionals: true,
17790
18524
  options: {
@@ -17794,15 +18528,15 @@ async function run19(args) {
17794
18528
  });
17795
18529
  } catch (parseError) {
17796
18530
  const message = parseError instanceof Error ? parseError.message : String(parseError);
17797
- return fail("USAGE", message, HELP19);
18531
+ return fail("USAGE", message, HELP24);
17798
18532
  }
17799
18533
  if (parsed.values.help) {
17800
- await writeStdout(HELP19);
18534
+ await writeStdout(HELP24);
17801
18535
  return EXIT.OK;
17802
18536
  }
17803
18537
  const path = parsed.positionals[0];
17804
18538
  if (!path)
17805
- return fail("USAGE", "Missing FILE argument", HELP19);
18539
+ return fail("USAGE", "Missing FILE argument", HELP24);
17806
18540
  const stylePrefix = parsed.values["style-prefix"] ?? "Heading";
17807
18541
  if (stylePrefix.length === 0) {
17808
18542
  return fail("USAGE", "--style-prefix cannot be empty");
@@ -17820,7 +18554,7 @@ async function run19(args) {
17820
18554
  });
17821
18555
  return EXIT.OK;
17822
18556
  }
17823
- var HELP19 = `docx outline \u2014 list headings as a hierarchical tree
18557
+ var HELP24 = `docx outline \u2014 list headings as a hierarchical tree
17824
18558
 
17825
18559
  Usage:
17826
18560
  docx outline FILE [options]
@@ -17850,13 +18584,13 @@ var init_outline = __esm(() => {
17850
18584
  // src/cli/read/index.ts
17851
18585
  var exports_read = {};
17852
18586
  __export(exports_read, {
17853
- run: () => run20
18587
+ run: () => run25
17854
18588
  });
17855
- import { parseArgs as parseArgs17 } from "util";
17856
- async function run20(args) {
18589
+ import { parseArgs as parseArgs21 } from "util";
18590
+ async function run25(args) {
17857
18591
  let parsed;
17858
18592
  try {
17859
- parsed = parseArgs17({
18593
+ parsed = parseArgs21({
17860
18594
  args,
17861
18595
  allowPositionals: true,
17862
18596
  options: {
@@ -17865,15 +18599,15 @@ async function run20(args) {
17865
18599
  }
17866
18600
  });
17867
18601
  } catch (e) {
17868
- return fail("USAGE", e instanceof Error ? e.message : String(e), HELP20);
18602
+ return fail("USAGE", e instanceof Error ? e.message : String(e), HELP25);
17869
18603
  }
17870
18604
  if (parsed.values.help) {
17871
- await writeStdout(HELP20);
18605
+ await writeStdout(HELP25);
17872
18606
  return EXIT.OK;
17873
18607
  }
17874
18608
  const path = parsed.positionals[0];
17875
18609
  if (!path)
17876
- return fail("USAGE", "Missing FILE argument", HELP20);
18610
+ return fail("USAGE", "Missing FILE argument", HELP25);
17877
18611
  const view = await openOrFail(path);
17878
18612
  if (typeof view === "number")
17879
18613
  return view;
@@ -17881,7 +18615,7 @@ async function run20(args) {
17881
18615
  await respond(view.doc);
17882
18616
  return EXIT.OK;
17883
18617
  }
17884
- var HELP20 = `docx read \u2014 print AST as JSON
18618
+ var HELP25 = `docx read \u2014 print AST as JSON
17885
18619
 
17886
18620
  Usage:
17887
18621
  docx read FILE [options]
@@ -17939,7 +18673,7 @@ function collectRunSlots(paragraph) {
17939
18673
  offset += length;
17940
18674
  continue;
17941
18675
  }
17942
- if (child.tag === "w:ins" || child.tag === "w:del") {
18676
+ if (child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:hyperlink") {
17943
18677
  for (const inner of child.children) {
17944
18678
  if (inner.tag !== "w:r")
17945
18679
  continue;
@@ -17964,14 +18698,14 @@ function rebuildContainer(container, baseOffset, span, replacement, runPropertie
17964
18698
  if (placed)
17965
18699
  return;
17966
18700
  placed = true;
17967
- const run21 = /* @__PURE__ */ jsxDEV(ReplacementRun, {
18701
+ const run26 = /* @__PURE__ */ jsxDEV(ReplacementRun, {
17968
18702
  runProperties,
17969
18703
  text: replacement
17970
18704
  }, undefined, false, undefined, this);
17971
18705
  newChildren.push(tracked && isParagraph ? /* @__PURE__ */ jsxDEV(Ins, {
17972
18706
  meta: mintMeta(tracked),
17973
- children: run21
17974
- }, undefined, false, undefined, this) : run21);
18707
+ children: run26
18708
+ }, undefined, false, undefined, this) : run26);
17975
18709
  };
17976
18710
  for (const child of container.children) {
17977
18711
  if (child.tag === "w:r") {
@@ -18007,7 +18741,7 @@ function rebuildContainer(container, baseOffset, span, replacement, runPropertie
18007
18741
  }
18008
18742
  continue;
18009
18743
  }
18010
- 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")) {
18011
18745
  let innerLength = 0;
18012
18746
  for (const inner of child.children) {
18013
18747
  if (inner.tag === "w:r")
@@ -18031,14 +18765,14 @@ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tr
18031
18765
  if (placed)
18032
18766
  return;
18033
18767
  placed = true;
18034
- const run21 = /* @__PURE__ */ jsxDEV(ReplacementRun, {
18768
+ const run26 = /* @__PURE__ */ jsxDEV(ReplacementRun, {
18035
18769
  runProperties,
18036
18770
  text: replacement
18037
18771
  }, undefined, false, undefined, this);
18038
18772
  newChildren.push(tracked ? /* @__PURE__ */ jsxDEV(Ins, {
18039
18773
  meta: mintMeta(tracked),
18040
- children: run21
18041
- }, undefined, false, undefined, this) : run21);
18774
+ children: run26
18775
+ }, undefined, false, undefined, this) : run26);
18042
18776
  };
18043
18777
  for (const child of paragraph.children) {
18044
18778
  if (child.tag === "w:r") {
@@ -18075,7 +18809,7 @@ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tr
18075
18809
  continue;
18076
18810
  }
18077
18811
  if (child.tag === "w:ins" || child.tag === "w:del") {
18078
- const innerLength = sumInnerRunLengths(child);
18812
+ const innerLength = sumInnerRunLengths2(child);
18079
18813
  const wrapperStart = offset;
18080
18814
  const wrapperEnd = offset + innerLength;
18081
18815
  offset = wrapperEnd;
@@ -18091,6 +18825,25 @@ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tr
18091
18825
  splitWrapperAcrossSpan(child, wrapperStart, span, tracked, newChildren, placeReplacement);
18092
18826
  continue;
18093
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
+ }
18094
18847
  newChildren.push(child);
18095
18848
  }
18096
18849
  if (!placed)
@@ -18151,7 +18904,62 @@ function splitWrapperAcrossSpan(wrapper, wrapperStart, span, tracked, out, place
18151
18904
  out.push(postWrapper);
18152
18905
  }
18153
18906
  }
18154
- 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) {
18155
18963
  let total = 0;
18156
18964
  for (const inner of wrapper.children) {
18157
18965
  if (inner.tag === "w:r")
@@ -18162,8 +18970,8 @@ function sumInnerRunLengths(wrapper) {
18162
18970
  function mintMeta(tracked) {
18163
18971
  return { ...tracked.meta, revisionId: tracked.allocator.next() };
18164
18972
  }
18165
- function convertRunTextToDelText(run21) {
18166
- for (const child of run21.children) {
18973
+ function convertRunTextToDelText(run26) {
18974
+ for (const child of run26.children) {
18167
18975
  if (child.tag === "w:t")
18168
18976
  child.tag = "w:delText";
18169
18977
  }
@@ -18190,15 +18998,15 @@ var init_replace_span = __esm(() => {
18190
18998
  });
18191
18999
 
18192
19000
  // src/cli/replace/index.ts
18193
- var exports_replace2 = {};
18194
- __export(exports_replace2, {
18195
- run: () => run21
19001
+ var exports_replace3 = {};
19002
+ __export(exports_replace3, {
19003
+ run: () => run26
18196
19004
  });
18197
- import { parseArgs as parseArgs18 } from "util";
18198
- async function run21(args) {
19005
+ import { parseArgs as parseArgs22 } from "util";
19006
+ async function run26(args) {
18199
19007
  let parsed;
18200
19008
  try {
18201
- parsed = parseArgs18({
19009
+ parsed = parseArgs22({
18202
19010
  args,
18203
19011
  allowPositionals: true,
18204
19012
  options: {
@@ -18213,21 +19021,21 @@ async function run21(args) {
18213
19021
  });
18214
19022
  } catch (parseError) {
18215
19023
  const message = parseError instanceof Error ? parseError.message : String(parseError);
18216
- return fail("USAGE", message, HELP21);
19024
+ return fail("USAGE", message, HELP26);
18217
19025
  }
18218
19026
  if (parsed.values.help) {
18219
- await writeStdout(HELP21);
19027
+ await writeStdout(HELP26);
18220
19028
  return EXIT.OK;
18221
19029
  }
18222
19030
  const path = parsed.positionals[0];
18223
19031
  const pattern = parsed.positionals[1];
18224
19032
  const replacement = parsed.positionals[2];
18225
19033
  if (!path)
18226
- return fail("USAGE", "Missing FILE argument", HELP21);
19034
+ return fail("USAGE", "Missing FILE argument", HELP26);
18227
19035
  if (pattern == null)
18228
- return fail("USAGE", "Missing PATTERN argument", HELP21);
19036
+ return fail("USAGE", "Missing PATTERN argument", HELP26);
18229
19037
  if (replacement == null) {
18230
- return fail("USAGE", "Missing REPLACEMENT argument", HELP21);
19038
+ return fail("USAGE", "Missing REPLACEMENT argument", HELP26);
18231
19039
  }
18232
19040
  const ignoreCase = Boolean(parsed.values["ignore-case"]);
18233
19041
  const useRegex = Boolean(parsed.values.regex);
@@ -18329,7 +19137,7 @@ async function run21(args) {
18329
19137
  });
18330
19138
  return EXIT.OK;
18331
19139
  }
18332
- var HELP21 = `docx replace \u2014 substitute text spans (sed for docx)
19140
+ var HELP26 = `docx replace \u2014 substitute text spans (sed for docx)
18333
19141
 
18334
19142
  Usage:
18335
19143
  docx replace FILE PATTERN REPLACEMENT [options]
@@ -18363,7 +19171,7 @@ Examples:
18363
19171
  docx replace doc.docx "(\\w+) (\\w+)" "$2 $1" --regex --all
18364
19172
  docx replace doc.docx "wordy phrase" "tighter phrase" --all --dry-run
18365
19173
  `;
18366
- var init_replace2 = __esm(() => {
19174
+ var init_replace3 = __esm(() => {
18367
19175
  init_core();
18368
19176
  init_respond();
18369
19177
  init_replace_span();
@@ -18372,13 +19180,13 @@ var init_replace2 = __esm(() => {
18372
19180
  // src/cli/track-changes/index.tsx
18373
19181
  var exports_track_changes = {};
18374
19182
  __export(exports_track_changes, {
18375
- run: () => run22
19183
+ run: () => run27
18376
19184
  });
18377
- import { parseArgs as parseArgs19 } from "util";
18378
- async function run22(args) {
19185
+ import { parseArgs as parseArgs23 } from "util";
19186
+ async function run27(args) {
18379
19187
  let parsed;
18380
19188
  try {
18381
- parsed = parseArgs19({
19189
+ parsed = parseArgs23({
18382
19190
  args,
18383
19191
  allowPositionals: true,
18384
19192
  options: {
@@ -18389,18 +19197,18 @@ async function run22(args) {
18389
19197
  });
18390
19198
  } catch (parseError) {
18391
19199
  const message = parseError instanceof Error ? parseError.message : String(parseError);
18392
- return fail("USAGE", message, HELP22);
19200
+ return fail("USAGE", message, HELP27);
18393
19201
  }
18394
19202
  if (parsed.values.help) {
18395
- await writeStdout(HELP22);
19203
+ await writeStdout(HELP27);
18396
19204
  return EXIT.OK;
18397
19205
  }
18398
19206
  const path = parsed.positionals[0];
18399
19207
  if (!path)
18400
- return fail("USAGE", "Missing FILE argument", HELP22);
19208
+ return fail("USAGE", "Missing FILE argument", HELP27);
18401
19209
  const mode = parsed.positionals[1];
18402
19210
  if (mode !== "on" && mode !== "off") {
18403
- return fail("USAGE", `Expected "on" or "off", got: ${mode ?? "<nothing>"}`, HELP22);
19211
+ return fail("USAGE", `Expected "on" or "off", got: ${mode ?? "<nothing>"}`, HELP27);
18404
19212
  }
18405
19213
  const view = await openOrFail(path);
18406
19214
  if (typeof view === "number")
@@ -18452,7 +19260,7 @@ async function run22(args) {
18452
19260
  });
18453
19261
  return EXIT.OK;
18454
19262
  }
18455
- var HELP22 = `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
18456
19264
 
18457
19265
  Usage:
18458
19266
  docx track-changes FILE on|off [options]
@@ -18487,9 +19295,9 @@ function countWords(text) {
18487
19295
  }
18488
19296
  function paragraphText2(paragraph) {
18489
19297
  let out = "";
18490
- for (const run23 of paragraph.runs) {
18491
- if (run23.type === "text")
18492
- out += run23.text;
19298
+ for (const run28 of paragraph.runs) {
19299
+ if (run28.type === "text")
19300
+ out += run28.text;
18493
19301
  }
18494
19302
  return out;
18495
19303
  }
@@ -18579,13 +19387,13 @@ function flattenParagraphs(blocks) {
18579
19387
  // src/cli/wc/index.ts
18580
19388
  var exports_wc = {};
18581
19389
  __export(exports_wc, {
18582
- run: () => run23
19390
+ run: () => run28
18583
19391
  });
18584
- import { parseArgs as parseArgs20 } from "util";
18585
- async function run23(args) {
19392
+ import { parseArgs as parseArgs24 } from "util";
19393
+ async function run28(args) {
18586
19394
  let parsed;
18587
19395
  try {
18588
- parsed = parseArgs20({
19396
+ parsed = parseArgs24({
18589
19397
  args,
18590
19398
  allowPositionals: true,
18591
19399
  options: {
@@ -18594,16 +19402,16 @@ async function run23(args) {
18594
19402
  });
18595
19403
  } catch (parseError) {
18596
19404
  const message = parseError instanceof Error ? parseError.message : String(parseError);
18597
- return fail("USAGE", message, HELP23);
19405
+ return fail("USAGE", message, HELP28);
18598
19406
  }
18599
19407
  if (parsed.values.help) {
18600
- await writeStdout(HELP23);
19408
+ await writeStdout(HELP28);
18601
19409
  return EXIT.OK;
18602
19410
  }
18603
19411
  const path = parsed.positionals[0];
18604
19412
  const locatorInput = parsed.positionals[1];
18605
19413
  if (!path)
18606
- return fail("USAGE", "Missing FILE argument", HELP23);
19414
+ return fail("USAGE", "Missing FILE argument", HELP28);
18607
19415
  const view = await openOrFail(path);
18608
19416
  if (typeof view === "number")
18609
19417
  return view;
@@ -18732,7 +19540,7 @@ function findCellBlocks(blocks, locator) {
18732
19540
  return null;
18733
19541
  return cell.blocks;
18734
19542
  }
18735
- var HELP23 = `docx wc \u2014 count words in a document or a locator-addressed slice
19543
+ var HELP28 = `docx wc \u2014 count words in a document or a locator-addressed slice
18736
19544
 
18737
19545
  Usage:
18738
19546
  docx wc FILE [LOCATOR] [options]
@@ -18769,7 +19577,7 @@ var init_wc = __esm(() => {
18769
19577
  // package.json
18770
19578
  var package_default = {
18771
19579
  name: "bun-docx",
18772
- version: "0.4.0",
19580
+ version: "0.5.0",
18773
19581
  description: "CLI for AI agents (Claude, Codex) to read, edit, and comment on .docx files with full format fidelity.",
18774
19582
  keywords: [
18775
19583
  "docx",
@@ -18848,6 +19656,7 @@ Commands:
18848
19656
  outline FILE List headings as a hierarchical tree
18849
19657
  comments \u2026 Add, reply, resolve, delete, list comments
18850
19658
  images \u2026 Extract, replace, list images
19659
+ hyperlinks \u2026 List, replace hyperlinks
18851
19660
  track-changes FILE on|off Toggle tracked-changes mode
18852
19661
  info \u2026 Reference material (schema, locators)
18853
19662
 
@@ -18874,12 +19683,13 @@ var COMMANDS = {
18874
19683
  delete: () => Promise.resolve().then(() => (init_delete2(), exports_delete2)),
18875
19684
  edit: () => Promise.resolve().then(() => (init_edit(), exports_edit)),
18876
19685
  find: () => Promise.resolve().then(() => (init_find(), exports_find)),
19686
+ hyperlinks: () => Promise.resolve().then(() => (init_hyperlinks(), exports_hyperlinks)),
18877
19687
  images: () => Promise.resolve().then(() => (init_images(), exports_images)),
18878
19688
  info: () => Promise.resolve().then(() => (init_info(), exports_info)),
18879
19689
  insert: () => Promise.resolve().then(() => (init_insert(), exports_insert)),
18880
19690
  outline: () => Promise.resolve().then(() => (init_outline(), exports_outline)),
18881
19691
  read: () => Promise.resolve().then(() => (init_read2(), exports_read)),
18882
- replace: () => Promise.resolve().then(() => (init_replace2(), exports_replace2)),
19692
+ replace: () => Promise.resolve().then(() => (init_replace3(), exports_replace3)),
18883
19693
  "track-changes": () => Promise.resolve().then(() => (init_track_changes2(), exports_track_changes)),
18884
19694
  wc: () => Promise.resolve().then(() => (init_wc(), exports_wc))
18885
19695
  };