@lotics/cli 0.96.5 → 0.96.6

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 (2) hide show
  1. package/dist/src/cli.js +264 -13
  2. package/package.json +1 -1
package/dist/src/cli.js CHANGED
@@ -90252,6 +90252,170 @@ async function ensureContentTypeOverride(doc, partName, contentType) {
90252
90252
  doc.zip.file("[Content_Types].xml", updated);
90253
90253
  }
90254
90254
 
90255
+ // ../ooxml/src/queries.ts
90256
+ function getBodyElementType(el) {
90257
+ const tag = getTagName(el);
90258
+ if (tag === "w:p") return "paragraph";
90259
+ if (tag === "w:tbl") return "table";
90260
+ return "other";
90261
+ }
90262
+ function extractParagraphText(el) {
90263
+ const tag = getTagName(el);
90264
+ if (tag !== "w:p") return "";
90265
+ const parts = [];
90266
+ for (const child of getChildren(el)) {
90267
+ const childTag = getTagName(child);
90268
+ if (childTag === "w:r") {
90269
+ for (const runChild of getChildren(child)) {
90270
+ if (getTagName(runChild) === "w:t") {
90271
+ parts.push(getTextContent(runChild));
90272
+ }
90273
+ }
90274
+ } else if (childTag === "w:hyperlink") {
90275
+ for (const hlChild of getChildren(child)) {
90276
+ if (getTagName(hlChild) === "w:r") {
90277
+ for (const runChild of getChildren(hlChild)) {
90278
+ if (getTagName(runChild) === "w:t") {
90279
+ parts.push(getTextContent(runChild));
90280
+ }
90281
+ }
90282
+ }
90283
+ }
90284
+ }
90285
+ }
90286
+ return parts.join("");
90287
+ }
90288
+ function getRunsFromParagraph(el) {
90289
+ const runs = [];
90290
+ for (const child of getChildren(el)) {
90291
+ if (getTagName(child) !== "w:r") continue;
90292
+ let text = "";
90293
+ let textIdx = -1;
90294
+ const children = getChildren(child);
90295
+ for (let i2 = 0; i2 < children.length; i2++) {
90296
+ if (getTagName(children[i2]) === "w:t") {
90297
+ const tChildren = getChildren(children[i2]);
90298
+ for (const tn of tChildren) {
90299
+ if (typeof tn["#text"] === "string") {
90300
+ text += tn["#text"];
90301
+ }
90302
+ }
90303
+ textIdx = i2;
90304
+ }
90305
+ }
90306
+ if (textIdx >= 0) {
90307
+ runs.push({ element: child, text, textNodeIndex: textIdx });
90308
+ }
90309
+ }
90310
+ return runs;
90311
+ }
90312
+ function setRunText(run, newText) {
90313
+ const children = getChildren(run.element);
90314
+ children[run.textNodeIndex] = {
90315
+ "w:t": [{ "#text": newText }],
90316
+ ":@": { "@_xml:space": "preserve" }
90317
+ };
90318
+ run.text = newText;
90319
+ }
90320
+ function computeReplacementRanges(text, query, replacement, matchType, maxReplacements, startCount) {
90321
+ const ranges = [];
90322
+ let count = startCount;
90323
+ if (matchType === "exact") {
90324
+ if (text === query && count < maxReplacements) {
90325
+ ranges.push({ start: 0, end: text.length, replacement });
90326
+ count++;
90327
+ }
90328
+ return { ranges, count };
90329
+ }
90330
+ if (matchType === "regex") {
90331
+ const re2 = new RegExp(query, "gi");
90332
+ let match2;
90333
+ while (count < maxReplacements && (match2 = re2.exec(text)) !== null) {
90334
+ ranges.push({ start: match2.index, end: match2.index + match2[0].length, replacement });
90335
+ count++;
90336
+ if (match2[0].length === 0) re2.lastIndex++;
90337
+ }
90338
+ return { ranges, count };
90339
+ }
90340
+ if (query.length === 0) return { ranges, count };
90341
+ const lower2 = text.toLowerCase();
90342
+ const lowerQuery = query.toLowerCase();
90343
+ let searchFrom = 0;
90344
+ while (count < maxReplacements) {
90345
+ const idx = lower2.indexOf(lowerQuery, searchFrom);
90346
+ if (idx === -1) break;
90347
+ ranges.push({ start: idx, end: idx + query.length, replacement });
90348
+ searchFrom = idx + query.length;
90349
+ count++;
90350
+ }
90351
+ return { ranges, count };
90352
+ }
90353
+ function applyRangeReplacements(runs, ranges) {
90354
+ if (ranges.length === 0) return;
90355
+ const bounds = [];
90356
+ let offset = 0;
90357
+ for (const run of runs) {
90358
+ bounds.push({ run, start: offset, end: offset + run.text.length });
90359
+ offset += run.text.length;
90360
+ }
90361
+ const joined = runs.map((r) => r.text).join("");
90362
+ const sorted = [...ranges].sort((a, b) => a.start - b.start);
90363
+ for (const { run, start: runStart, end: runEnd } of bounds) {
90364
+ let next = "";
90365
+ let cursor = runStart;
90366
+ for (const range2 of sorted) {
90367
+ if (range2.end <= runStart || range2.start >= runEnd) continue;
90368
+ const keepUntil = Math.min(range2.start, runEnd);
90369
+ const from = Math.max(cursor, runStart);
90370
+ if (keepUntil > from) next += joined.slice(from, keepUntil);
90371
+ if (runStart <= range2.start && range2.start < runEnd) next += range2.replacement;
90372
+ cursor = Math.max(cursor, range2.end);
90373
+ }
90374
+ const tail = Math.max(cursor, runStart);
90375
+ if (runEnd > tail) next += joined.slice(tail, runEnd);
90376
+ if (next !== run.text) setRunText(run, next);
90377
+ }
90378
+ }
90379
+ function replaceInParagraph(el, query, replacement, matchType, maxReplacements, replacementsMade) {
90380
+ if (!extractParagraphText(el)) return replacementsMade;
90381
+ const runs = getRunsFromParagraph(el);
90382
+ if (runs.length === 0) return replacementsMade;
90383
+ const text = runs.length === 1 ? runs[0].text : runs.map((r) => r.text).join("");
90384
+ const { ranges, count } = computeReplacementRanges(text, query, replacement, matchType, maxReplacements, replacementsMade);
90385
+ applyRangeReplacements(runs, ranges);
90386
+ return count;
90387
+ }
90388
+ function replaceText(bodyElements, query, replacement, matchType = "contains", maxReplacements = Infinity) {
90389
+ if (matchType === "regex") {
90390
+ try {
90391
+ new RegExp(query);
90392
+ } catch {
90393
+ throw new Error(`Invalid regex pattern: ${query}`);
90394
+ }
90395
+ }
90396
+ let replacementsMade = 0;
90397
+ for (let i2 = 0; i2 < bodyElements.length && replacementsMade < maxReplacements; i2++) {
90398
+ const el = bodyElements[i2];
90399
+ const type = getBodyElementType(el);
90400
+ if (type === "paragraph") {
90401
+ replacementsMade = replaceInParagraph(el, query, replacement, matchType, maxReplacements, replacementsMade);
90402
+ } else if (type === "table") {
90403
+ for (const child of getChildren(el)) {
90404
+ if (getTagName(child) !== "w:tr") continue;
90405
+ for (const cell of getChildren(child)) {
90406
+ if (getTagName(cell) !== "w:tc") continue;
90407
+ for (const p of getChildren(cell)) {
90408
+ if (getTagName(p) === "w:p" && replacementsMade < maxReplacements) {
90409
+ replacementsMade = replaceInParagraph(p, query, replacement, matchType, maxReplacements, replacementsMade);
90410
+ }
90411
+ }
90412
+ }
90413
+ }
90414
+ }
90415
+ }
90416
+ return replacementsMade;
90417
+ }
90418
+
90255
90419
  // ../docx/src/parse/parser.ts
90256
90420
  var import_jszip2 = __toESM(require_lib4(), 1);
90257
90421
  async function parseDocx(buffer) {
@@ -90778,18 +90942,102 @@ async function docxReplaceText(filePath, rest) {
90778
90942
  fail2("Usage: lotics docx replace-text <file> '<search>' '<replace>'");
90779
90943
  }
90780
90944
  const doc = await loadFile2(filePath);
90781
- const newChildren = doc.body.children.map((b) => b.kind === "paragraph" ? replaceInParagraph(b, search, replace2) : b);
90782
- await writeDoc(filePath, replaceChildren(doc, newChildren));
90945
+ const { children, count } = replaceEverywhere(doc.body.children, search, replace2);
90946
+ if (count === 0) {
90947
+ fail2(`No occurrence of ${JSON.stringify(search)} in ${filePath} \u2014 nothing written.`);
90948
+ }
90949
+ await writeDoc(filePath, replaceChildren(doc, children));
90950
+ console.error(`Wrote ${filePath} (${count} replacement${count === 1 ? "" : "s"})`);
90951
+ }
90952
+ function replaceEverywhere(blocks, search, replace2) {
90953
+ let count = 0;
90954
+ const children = blocks.map((b) => {
90955
+ if (b.kind === "paragraph") {
90956
+ const [paragraph, n] = replaceInParagraph2(b, search, replace2);
90957
+ count += n;
90958
+ return paragraph;
90959
+ }
90960
+ if (b.kind === "opaque_block") {
90961
+ const xml = structuredClone(b.xml);
90962
+ const n = replaceText([xml], search, replace2, "contains");
90963
+ if (n === 0) return b;
90964
+ count += n;
90965
+ return { kind: "opaque_block", xml };
90966
+ }
90967
+ return b;
90968
+ });
90969
+ return { children, count };
90783
90970
  }
90784
- function replaceInParagraph(p, search, replace2) {
90785
- const content = p.content.map((inline) => {
90971
+ function replaceInParagraph2(p, search, replace2) {
90972
+ if (search === "") return [p, 0];
90973
+ const rewritten = /* @__PURE__ */ new Map();
90974
+ let count = 0;
90975
+ for (const segment of collectTextSegments(p)) {
90976
+ const joined = segment.map((s) => s.value).join("");
90977
+ const matches = [];
90978
+ for (let at2 = joined.indexOf(search); at2 !== -1; at2 = joined.indexOf(search, at2 + search.length)) {
90979
+ matches.push({ start: at2, end: at2 + search.length });
90980
+ }
90981
+ if (matches.length === 0) continue;
90982
+ count += matches.length;
90983
+ for (const slot of segment) {
90984
+ let out = "";
90985
+ let cursor = slot.start;
90986
+ for (const m of matches) {
90987
+ if (m.end <= slot.start || m.start >= slot.end) continue;
90988
+ const keepUntil = Math.max(cursor, Math.min(m.start, slot.end));
90989
+ if (keepUntil > cursor) out += joined.slice(cursor, keepUntil);
90990
+ if (m.start >= slot.start && m.start < slot.end) out += replace2;
90991
+ cursor = Math.max(cursor, Math.min(m.end, slot.end));
90992
+ }
90993
+ if (cursor < slot.end) out += joined.slice(cursor, slot.end);
90994
+ rewritten.set(`${slot.inlineIndex}:${slot.childIndex}`, out);
90995
+ }
90996
+ }
90997
+ if (count === 0) return [p, 0];
90998
+ const content = p.content.map((inline, inlineIndex) => {
90786
90999
  if (inline.kind !== "run") return inline;
90787
- const newChildren = inline.content.map(
90788
- (c) => c.kind === "text" ? { kind: "text", value: c.value.split(search).join(replace2), preserveSpace: c.preserveSpace } : c
90789
- );
90790
- return { kind: "run", properties: inline.properties, content: newChildren };
91000
+ const children = inline.content.map((c, childIndex) => {
91001
+ if (c.kind !== "text") return c;
91002
+ const next = rewritten.get(`${inlineIndex}:${childIndex}`);
91003
+ if (next === void 0 || next === c.value) return c;
91004
+ return { kind: "text", value: next, preserveSpace: next.startsWith(" ") || next.endsWith(" ") };
91005
+ });
91006
+ return { kind: "run", properties: inline.properties, content: children };
90791
91007
  });
90792
- return { kind: "paragraph", properties: p.properties, content };
91008
+ return [{ kind: "paragraph", properties: p.properties, content }, count];
91009
+ }
91010
+ function collectTextSegments(p) {
91011
+ const segments = [];
91012
+ let current = [];
91013
+ let offset = 0;
91014
+ const breakSegment = () => {
91015
+ if (current.length > 0) segments.push(current);
91016
+ current = [];
91017
+ offset = 0;
91018
+ };
91019
+ p.content.forEach((inline, inlineIndex) => {
91020
+ if (inline.kind !== "run") {
91021
+ breakSegment();
91022
+ return;
91023
+ }
91024
+ inline.content.forEach((child, childIndex) => {
91025
+ if (child.kind !== "text") {
91026
+ breakSegment();
91027
+ return;
91028
+ }
91029
+ current.push({
91030
+ inlineIndex,
91031
+ childIndex,
91032
+ value: child.value,
91033
+ start: offset,
91034
+ end: offset + child.value.length
91035
+ });
91036
+ offset += child.value.length;
91037
+ });
91038
+ });
91039
+ breakSegment();
91040
+ return segments.filter((s) => s.length > 0);
90793
91041
  }
90794
91042
  async function docxBatch(filePath, json2) {
90795
91043
  if (!filePath || !json2) fail2("Usage: lotics docx batch <file> '<json-array-of-ops>'");
@@ -90833,8 +91081,9 @@ function applyBatchOp(doc, op, index) {
90833
91081
  return replaceChildren(doc, doc.body.children.filter((_, i2) => i2 !== op.at));
90834
91082
  }
90835
91083
  case "replace-text": {
90836
- const next = doc.body.children.map((b) => b.kind === "paragraph" ? replaceInParagraph(b, op.search, op.replace) : b);
90837
- return replaceChildren(doc, next);
91084
+ const { children, count } = replaceEverywhere(doc.body.children, op.search, op.replace);
91085
+ if (count === 0) fail2(`[op ${index}] No occurrence of ${JSON.stringify(op.search)} \u2014 batch aborted.`);
91086
+ return replaceChildren(doc, children);
90838
91087
  }
90839
91088
  default: {
90840
91089
  const exhaustive = op;
@@ -90854,10 +91103,12 @@ Uses Lotics' own OOXML engine; round-trips faithfully with the Lotics editor and
90854
91103
  lotics docx insert-paragraph <file> '<text>' --at=<i> [--style=NAME]
90855
91104
  Insert a paragraph at index <i> (0-based)
90856
91105
  lotics docx delete-block <file> --at=<i> Remove the block at index <i>
90857
- lotics docx replace-text <file> '<search>' '<replace>' Replace text in all paragraph runs
91106
+ lotics docx replace-text <file> '<search>' '<replace>' Replace text in paragraphs AND table cells
91107
+ (errors if nothing matched \u2014 never a silent no-op)
90858
91108
  lotics docx batch <file> '<json-array-of-ops>' Apply many ops in one parse/serialize cycle
90859
91109
 
90860
- Edit ops mutate the file atomically (temp file + rename). Opaque blocks (tables, custom XML) are preserved verbatim.`);
91110
+ Edit ops mutate the file atomically (temp file + rename). Structure inside opaque blocks (tables, custom XML) is
91111
+ preserved verbatim; replace-text rewrites the text within them.`);
90861
91112
  }
90862
91113
  async function runDocxCommand(subcommand, toolArgs, restArgs) {
90863
91114
  switch (subcommand) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.96.5",
3
+ "version": "0.96.6",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {