@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.
- package/dist/src/cli.js +264 -13
- 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
|
|
90782
|
-
|
|
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
|
|
90785
|
-
|
|
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
|
|
90788
|
-
(c
|
|
90789
|
-
|
|
90790
|
-
|
|
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
|
|
90837
|
-
|
|
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
|
|
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).
|
|
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) {
|