lilmd 0.1.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.
- package/BENCHMARK.md +106 -0
- package/README.md +113 -0
- package/dist/index.cjs +364 -0
- package/dist/index.d.cts +112 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +331 -0
- package/dist/index.js.map +13 -0
- package/dist/mdq.js +735 -0
- package/dist/mdq.js.map +16 -0
- package/package.json +42 -0
package/dist/mdq.js
ADDED
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// src/cli.ts
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
|
|
9
|
+
// src/scan.ts
|
|
10
|
+
function scan(src) {
|
|
11
|
+
const out = [];
|
|
12
|
+
const len = src.length;
|
|
13
|
+
let i = 0;
|
|
14
|
+
let lineNo = 0;
|
|
15
|
+
let inFence = false;
|
|
16
|
+
let fenceChar = 0;
|
|
17
|
+
let fenceLen = 0;
|
|
18
|
+
while (i <= len) {
|
|
19
|
+
const start = i;
|
|
20
|
+
while (i < len && src.charCodeAt(i) !== 10)
|
|
21
|
+
i++;
|
|
22
|
+
let line = src.slice(start, i);
|
|
23
|
+
if (line.length > 0 && line.charCodeAt(line.length - 1) === 13) {
|
|
24
|
+
line = line.slice(0, line.length - 1);
|
|
25
|
+
}
|
|
26
|
+
lineNo++;
|
|
27
|
+
const fence = matchFence(line);
|
|
28
|
+
if (fence) {
|
|
29
|
+
if (!inFence) {
|
|
30
|
+
inFence = true;
|
|
31
|
+
fenceChar = fence.char;
|
|
32
|
+
fenceLen = fence.len;
|
|
33
|
+
} else if (fence.char === fenceChar && fence.len >= fenceLen) {
|
|
34
|
+
inFence = false;
|
|
35
|
+
}
|
|
36
|
+
} else if (!inFence) {
|
|
37
|
+
const h = matchHeading(line, lineNo);
|
|
38
|
+
if (h)
|
|
39
|
+
out.push(h);
|
|
40
|
+
}
|
|
41
|
+
if (i >= len)
|
|
42
|
+
break;
|
|
43
|
+
i++;
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function matchFence(line) {
|
|
48
|
+
let p = 0;
|
|
49
|
+
while (p < 3 && line.charCodeAt(p) === 32)
|
|
50
|
+
p++;
|
|
51
|
+
const ch = line.charCodeAt(p);
|
|
52
|
+
if (ch !== 96 && ch !== 126)
|
|
53
|
+
return null;
|
|
54
|
+
let run = 0;
|
|
55
|
+
while (line.charCodeAt(p + run) === ch)
|
|
56
|
+
run++;
|
|
57
|
+
if (run < 3)
|
|
58
|
+
return null;
|
|
59
|
+
return { char: ch, len: run };
|
|
60
|
+
}
|
|
61
|
+
function matchHeading(line, lineNo) {
|
|
62
|
+
let p = 0;
|
|
63
|
+
while (p < 3 && line.charCodeAt(p) === 32)
|
|
64
|
+
p++;
|
|
65
|
+
if (line.charCodeAt(p) !== 35)
|
|
66
|
+
return null;
|
|
67
|
+
let hashes = 0;
|
|
68
|
+
while (line.charCodeAt(p + hashes) === 35)
|
|
69
|
+
hashes++;
|
|
70
|
+
if (hashes < 1 || hashes > 6)
|
|
71
|
+
return null;
|
|
72
|
+
const after = p + hashes;
|
|
73
|
+
const afterCh = line.charCodeAt(after);
|
|
74
|
+
if (after < line.length && afterCh !== 32 && afterCh !== 9) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
let contentStart = after;
|
|
78
|
+
while (contentStart < line.length && (line.charCodeAt(contentStart) === 32 || line.charCodeAt(contentStart) === 9)) {
|
|
79
|
+
contentStart++;
|
|
80
|
+
}
|
|
81
|
+
let end = line.length;
|
|
82
|
+
while (end > contentStart && (line.charCodeAt(end - 1) === 32 || line.charCodeAt(end - 1) === 9)) {
|
|
83
|
+
end--;
|
|
84
|
+
}
|
|
85
|
+
let closing = end;
|
|
86
|
+
while (closing > contentStart && line.charCodeAt(closing - 1) === 35)
|
|
87
|
+
closing--;
|
|
88
|
+
if (closing < end && (closing === contentStart || line.charCodeAt(closing - 1) === 32 || line.charCodeAt(closing - 1) === 9)) {
|
|
89
|
+
end = closing;
|
|
90
|
+
while (end > contentStart && (line.charCodeAt(end - 1) === 32 || line.charCodeAt(end - 1) === 9)) {
|
|
91
|
+
end--;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const title = line.slice(contentStart, end);
|
|
95
|
+
return { level: hashes, title, line: lineNo };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/sections.ts
|
|
99
|
+
function buildSections(headings, totalLines) {
|
|
100
|
+
const out = [];
|
|
101
|
+
const stack = [];
|
|
102
|
+
for (const h of headings) {
|
|
103
|
+
while (stack.length > 0 && stack[stack.length - 1].level >= h.level) {
|
|
104
|
+
const closing = stack.pop();
|
|
105
|
+
closing.line_end = h.line - 1;
|
|
106
|
+
}
|
|
107
|
+
const parent = stack.length > 0 ? stack[stack.length - 1] : null;
|
|
108
|
+
const sec = {
|
|
109
|
+
level: h.level,
|
|
110
|
+
title: h.title,
|
|
111
|
+
line_start: h.line,
|
|
112
|
+
line_end: totalLines,
|
|
113
|
+
parent
|
|
114
|
+
};
|
|
115
|
+
out.push(sec);
|
|
116
|
+
stack.push(sec);
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
function pathOf(sec) {
|
|
121
|
+
const path = [];
|
|
122
|
+
let cur = sec.parent;
|
|
123
|
+
while (cur) {
|
|
124
|
+
path.push(cur.title);
|
|
125
|
+
cur = cur.parent;
|
|
126
|
+
}
|
|
127
|
+
return path.reverse();
|
|
128
|
+
}
|
|
129
|
+
function countLines(src) {
|
|
130
|
+
if (src.length === 0)
|
|
131
|
+
return 0;
|
|
132
|
+
let n = 1;
|
|
133
|
+
for (let i = 0;i < src.length; i++) {
|
|
134
|
+
if (src.charCodeAt(i) === 10)
|
|
135
|
+
n++;
|
|
136
|
+
}
|
|
137
|
+
if (src.charCodeAt(src.length - 1) === 10)
|
|
138
|
+
n--;
|
|
139
|
+
return n;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/select.ts
|
|
143
|
+
function parseSelector(input) {
|
|
144
|
+
const trimmed = input.trim();
|
|
145
|
+
if (trimmed.length === 0)
|
|
146
|
+
return [];
|
|
147
|
+
const rawSegments = [];
|
|
148
|
+
const ops = ["descendant"];
|
|
149
|
+
let cur = "";
|
|
150
|
+
let i = 0;
|
|
151
|
+
let inRegex = false;
|
|
152
|
+
let atSegmentStart = true;
|
|
153
|
+
while (i < trimmed.length) {
|
|
154
|
+
const ch = trimmed[i];
|
|
155
|
+
if (ch === "/" && (atSegmentStart || inRegex)) {
|
|
156
|
+
inRegex = !inRegex;
|
|
157
|
+
cur += ch;
|
|
158
|
+
atSegmentStart = false;
|
|
159
|
+
i++;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (!inRegex && ch === ">") {
|
|
163
|
+
rawSegments.push(cur.trim());
|
|
164
|
+
cur = "";
|
|
165
|
+
atSegmentStart = true;
|
|
166
|
+
if (trimmed[i + 1] === ">") {
|
|
167
|
+
ops.push("child");
|
|
168
|
+
i += 2;
|
|
169
|
+
} else {
|
|
170
|
+
ops.push("descendant");
|
|
171
|
+
i += 1;
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
cur += ch;
|
|
176
|
+
if (ch !== " " && ch !== "\t")
|
|
177
|
+
atSegmentStart = false;
|
|
178
|
+
i++;
|
|
179
|
+
}
|
|
180
|
+
rawSegments.push(cur.trim());
|
|
181
|
+
return rawSegments.map((s, idx) => parseSegment(s, ops[idx] ?? "descendant"));
|
|
182
|
+
}
|
|
183
|
+
function parseSegment(raw, op) {
|
|
184
|
+
let s = raw;
|
|
185
|
+
let level = null;
|
|
186
|
+
const levelMatch = /^(#{1,6})(?!#)\s*(.*)$/.exec(s);
|
|
187
|
+
if (levelMatch) {
|
|
188
|
+
level = levelMatch[1].length;
|
|
189
|
+
s = levelMatch[2] ?? "";
|
|
190
|
+
}
|
|
191
|
+
const regexMatch = /^\/(.+)\/([gimsuy]*)$/.exec(s);
|
|
192
|
+
if (regexMatch) {
|
|
193
|
+
const pattern = regexMatch[1];
|
|
194
|
+
const flags = regexMatch[2] || "i";
|
|
195
|
+
return {
|
|
196
|
+
op,
|
|
197
|
+
level,
|
|
198
|
+
kind: "regex",
|
|
199
|
+
value: pattern,
|
|
200
|
+
regex: new RegExp(pattern, flags)
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (s.startsWith("=")) {
|
|
204
|
+
return { op, level, kind: "exact", value: s.slice(1).trim() };
|
|
205
|
+
}
|
|
206
|
+
return { op, level, kind: "fuzzy", value: s.trim() };
|
|
207
|
+
}
|
|
208
|
+
function match(sections, selector) {
|
|
209
|
+
if (selector.length === 0)
|
|
210
|
+
return [];
|
|
211
|
+
const out = [];
|
|
212
|
+
for (const sec of sections) {
|
|
213
|
+
if (matches(sec, selector))
|
|
214
|
+
out.push(sec);
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
function matches(sec, segs) {
|
|
219
|
+
const last = segs[segs.length - 1];
|
|
220
|
+
if (!last || !segmentMatchesSection(last, sec))
|
|
221
|
+
return false;
|
|
222
|
+
let cursor = sec.parent;
|
|
223
|
+
for (let i = segs.length - 2;i >= 0; i--) {
|
|
224
|
+
const op = segs[i + 1].op;
|
|
225
|
+
const seg = segs[i];
|
|
226
|
+
if (op === "child") {
|
|
227
|
+
if (!cursor || !segmentMatchesSection(seg, cursor))
|
|
228
|
+
return false;
|
|
229
|
+
cursor = cursor.parent;
|
|
230
|
+
} else {
|
|
231
|
+
let found = null;
|
|
232
|
+
while (cursor) {
|
|
233
|
+
if (segmentMatchesSection(seg, cursor)) {
|
|
234
|
+
found = cursor;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
cursor = cursor.parent;
|
|
238
|
+
}
|
|
239
|
+
if (!found)
|
|
240
|
+
return false;
|
|
241
|
+
cursor = found.parent;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
function segmentMatchesSection(seg, sec) {
|
|
247
|
+
if (seg.level !== null && seg.level !== sec.level)
|
|
248
|
+
return false;
|
|
249
|
+
const title = sec.title;
|
|
250
|
+
switch (seg.kind) {
|
|
251
|
+
case "exact":
|
|
252
|
+
return title.toLowerCase() === seg.value.toLowerCase();
|
|
253
|
+
case "regex":
|
|
254
|
+
return seg.regex.test(title);
|
|
255
|
+
case "fuzzy":
|
|
256
|
+
return title.toLowerCase().includes(seg.value.toLowerCase());
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/render.ts
|
|
261
|
+
function renderToc(file, src, sections, opts) {
|
|
262
|
+
const totalLines = countLines(src);
|
|
263
|
+
const headerCount = sections.length;
|
|
264
|
+
const headerRange = totalLines === 0 ? "L0" : `L1-${totalLines}`;
|
|
265
|
+
const plural = headerCount === 1 ? "heading" : "headings";
|
|
266
|
+
const out = [];
|
|
267
|
+
out.push(`${file} ${headerRange} ${headerCount} ${plural}`);
|
|
268
|
+
for (const sec of sections) {
|
|
269
|
+
if (opts.depth != null && sec.level > opts.depth)
|
|
270
|
+
continue;
|
|
271
|
+
const indent = opts.flat ? "" : " ".repeat(Math.max(0, sec.level - 1));
|
|
272
|
+
const hashes = "#".repeat(sec.level);
|
|
273
|
+
const range = `L${sec.line_start}-${sec.line_end}`;
|
|
274
|
+
out.push(`${indent}${hashes} ${sec.title} ${range}`);
|
|
275
|
+
}
|
|
276
|
+
return out.join(`
|
|
277
|
+
`);
|
|
278
|
+
}
|
|
279
|
+
function renderSection(file, srcLines, sec, opts) {
|
|
280
|
+
const start = sec.line_start;
|
|
281
|
+
let end = sec.line_end;
|
|
282
|
+
if (opts.bodyOnly && opts.allSections) {
|
|
283
|
+
const firstChild = findFirstChild(sec, opts.allSections);
|
|
284
|
+
if (firstChild)
|
|
285
|
+
end = firstChild.line_start - 1;
|
|
286
|
+
}
|
|
287
|
+
if (opts.noBody) {
|
|
288
|
+
end = start;
|
|
289
|
+
}
|
|
290
|
+
const clampedEnd = Math.min(end, srcLines.length);
|
|
291
|
+
let body = srcLines.slice(start - 1, clampedEnd).join(`
|
|
292
|
+
`);
|
|
293
|
+
if (opts.maxLines != null && opts.maxLines > 0) {
|
|
294
|
+
body = truncateBody(body, opts.maxLines);
|
|
295
|
+
}
|
|
296
|
+
if (opts.pretty) {
|
|
297
|
+
body = opts.pretty(body);
|
|
298
|
+
}
|
|
299
|
+
if (opts.raw)
|
|
300
|
+
return body;
|
|
301
|
+
const hashes = "#".repeat(sec.level);
|
|
302
|
+
const header = `── ${file} L${start}-${end} ${hashes} ${sec.title} ${"─".repeat(8)}`;
|
|
303
|
+
const footer = `── end ${"─".repeat(40)}`;
|
|
304
|
+
return `${header}
|
|
305
|
+
${body}
|
|
306
|
+
${footer}`;
|
|
307
|
+
}
|
|
308
|
+
function truncateBody(body, maxLines) {
|
|
309
|
+
if (maxLines <= 0)
|
|
310
|
+
return body;
|
|
311
|
+
const lines = body.split(`
|
|
312
|
+
`);
|
|
313
|
+
if (lines.length <= maxLines)
|
|
314
|
+
return body;
|
|
315
|
+
const kept = lines.slice(0, maxLines).join(`
|
|
316
|
+
`);
|
|
317
|
+
const remaining = lines.length - maxLines;
|
|
318
|
+
return `${kept}
|
|
319
|
+
|
|
320
|
+
… ${remaining} more lines (use --max-lines=0 for full)`;
|
|
321
|
+
}
|
|
322
|
+
function findFirstChild(sec, all) {
|
|
323
|
+
for (const candidate of all) {
|
|
324
|
+
if (candidate.parent === sec)
|
|
325
|
+
return candidate;
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/pretty.ts
|
|
331
|
+
var formatterPromise = null;
|
|
332
|
+
function loadPrettyFormatter() {
|
|
333
|
+
return formatterPromise ??= buildFormatter();
|
|
334
|
+
}
|
|
335
|
+
async function buildFormatter() {
|
|
336
|
+
const [{ marked }, { markedTerminal }] = await Promise.all([
|
|
337
|
+
import("marked"),
|
|
338
|
+
import("marked-terminal")
|
|
339
|
+
]);
|
|
340
|
+
marked.use(markedTerminal({
|
|
341
|
+
reflowText: false,
|
|
342
|
+
tab: 2,
|
|
343
|
+
hr: "─"
|
|
344
|
+
}));
|
|
345
|
+
return (md) => {
|
|
346
|
+
const originalError = console.error;
|
|
347
|
+
console.error = (...args) => {
|
|
348
|
+
if (typeof args[0] === "string" && /Could not find the language/i.test(args[0]))
|
|
349
|
+
return;
|
|
350
|
+
originalError.apply(console, args);
|
|
351
|
+
};
|
|
352
|
+
let rendered;
|
|
353
|
+
try {
|
|
354
|
+
rendered = marked.parse(md);
|
|
355
|
+
} finally {
|
|
356
|
+
console.error = originalError;
|
|
357
|
+
}
|
|
358
|
+
if (typeof rendered !== "string") {
|
|
359
|
+
throw new Error("mdq: pretty renderer returned a Promise unexpectedly");
|
|
360
|
+
}
|
|
361
|
+
return rendered.replace(/\n+$/, "");
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/cli.ts
|
|
366
|
+
var HELP = `mdq — CLI for working with large Markdown files
|
|
367
|
+
|
|
368
|
+
Usage:
|
|
369
|
+
mdq show this help
|
|
370
|
+
mdq <file> print table of contents
|
|
371
|
+
mdq <file> <selector> alias for 'mdq read'
|
|
372
|
+
mdq read <file> <selector> print sections matching selector
|
|
373
|
+
mdq ls <file> <selector> list direct child headings
|
|
374
|
+
mdq grep <file> <pattern> regex-search section bodies
|
|
375
|
+
|
|
376
|
+
Selector grammar:
|
|
377
|
+
Install fuzzy, case-insensitive substring
|
|
378
|
+
=Install exact, case-insensitive equality
|
|
379
|
+
/^inst/i regex (JS syntax); flags default to 'i'
|
|
380
|
+
##Install level filter (1..6 '#'s)
|
|
381
|
+
Guide > Install descendant, any depth under 'Guide'
|
|
382
|
+
Guide >> Install direct child of 'Guide'
|
|
383
|
+
|
|
384
|
+
Options:
|
|
385
|
+
--depth <n> TOC: max heading depth to show (0 = none)
|
|
386
|
+
--flat TOC: flat list, no indentation
|
|
387
|
+
--max-results <n> cap matches for read/ls (default 25)
|
|
388
|
+
--max-lines <n> truncate long bodies (0 = unlimited)
|
|
389
|
+
--body-only read: skip subsections
|
|
390
|
+
--no-body read: print headings only
|
|
391
|
+
--raw read: drop delimiter lines
|
|
392
|
+
--pretty read: render markdown with ANSI styling (for humans)
|
|
393
|
+
--json machine-readable JSON output
|
|
394
|
+
|
|
395
|
+
Use '-' as <file> to read from stdin. Exit code is 1 when no matches.
|
|
396
|
+
`;
|
|
397
|
+
function ok(s) {
|
|
398
|
+
return { code: 0, stdout: s, stderr: "" };
|
|
399
|
+
}
|
|
400
|
+
function noMatch(s) {
|
|
401
|
+
return { code: 1, stdout: s, stderr: "" };
|
|
402
|
+
}
|
|
403
|
+
function err(s, code = 1) {
|
|
404
|
+
return { code, stdout: "", stderr: s };
|
|
405
|
+
}
|
|
406
|
+
var OPTIONS = {
|
|
407
|
+
depth: { type: "string" },
|
|
408
|
+
flat: { type: "boolean" },
|
|
409
|
+
"max-results": { type: "string" },
|
|
410
|
+
"max-lines": { type: "string" },
|
|
411
|
+
"body-only": { type: "boolean" },
|
|
412
|
+
"no-body": { type: "boolean" },
|
|
413
|
+
raw: { type: "boolean" },
|
|
414
|
+
pretty: { type: "boolean" },
|
|
415
|
+
json: { type: "boolean" },
|
|
416
|
+
help: { type: "boolean", short: "h" }
|
|
417
|
+
};
|
|
418
|
+
async function run(argv) {
|
|
419
|
+
let parsed;
|
|
420
|
+
try {
|
|
421
|
+
parsed = parseArgs({
|
|
422
|
+
args: argv,
|
|
423
|
+
options: OPTIONS,
|
|
424
|
+
allowPositionals: true,
|
|
425
|
+
strict: true
|
|
426
|
+
});
|
|
427
|
+
} catch (e) {
|
|
428
|
+
return err(`mdq: ${e.message}
|
|
429
|
+
${HELP}`, 2);
|
|
430
|
+
}
|
|
431
|
+
const { values, positionals } = parsed;
|
|
432
|
+
if (values.help || positionals.length === 0) {
|
|
433
|
+
return ok(HELP);
|
|
434
|
+
}
|
|
435
|
+
const head = positionals[0];
|
|
436
|
+
if (head === "read" || head === "ls" || head === "grep" || head === "toc") {
|
|
437
|
+
return dispatch(head, positionals.slice(1), values);
|
|
438
|
+
}
|
|
439
|
+
if (positionals.length === 1)
|
|
440
|
+
return dispatch("toc", positionals, values);
|
|
441
|
+
return dispatch("read", positionals, values);
|
|
442
|
+
}
|
|
443
|
+
async function dispatch(cmd, rest, values) {
|
|
444
|
+
switch (cmd) {
|
|
445
|
+
case "toc":
|
|
446
|
+
return cmdToc(rest, values);
|
|
447
|
+
case "read":
|
|
448
|
+
return cmdRead(rest, values);
|
|
449
|
+
case "ls":
|
|
450
|
+
return cmdLs(rest, values);
|
|
451
|
+
case "grep":
|
|
452
|
+
return cmdGrep(rest, values);
|
|
453
|
+
default:
|
|
454
|
+
return err(`mdq: unknown command '${cmd}'
|
|
455
|
+
${HELP}`, 2);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function loadFile(file) {
|
|
459
|
+
try {
|
|
460
|
+
const src = file === "-" ? readFileSync(0, "utf8") : readFileSync(file, "utf8");
|
|
461
|
+
return { src };
|
|
462
|
+
} catch (e) {
|
|
463
|
+
const msg = e.code === "ENOENT" ? `mdq: cannot open '${file}': not found
|
|
464
|
+
` : `mdq: cannot open '${file}': ${e.message}
|
|
465
|
+
`;
|
|
466
|
+
return err(msg, 2);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function parseIntOrNull(v) {
|
|
470
|
+
if (v == null)
|
|
471
|
+
return null;
|
|
472
|
+
const n = Number.parseInt(v, 10);
|
|
473
|
+
if (Number.isNaN(n) || String(n) !== v.trim())
|
|
474
|
+
return null;
|
|
475
|
+
return n;
|
|
476
|
+
}
|
|
477
|
+
function readFlag(v, name, fallback) {
|
|
478
|
+
const raw = v[name];
|
|
479
|
+
if (raw == null)
|
|
480
|
+
return { value: fallback };
|
|
481
|
+
const n = parseIntOrNull(raw);
|
|
482
|
+
if (n == null || n < 0) {
|
|
483
|
+
return err(`mdq: --${name} expects a non-negative integer, got '${raw}'
|
|
484
|
+
`, 2);
|
|
485
|
+
}
|
|
486
|
+
return { value: n };
|
|
487
|
+
}
|
|
488
|
+
function cmdToc(rest, v) {
|
|
489
|
+
const file = rest[0];
|
|
490
|
+
if (file == null)
|
|
491
|
+
return err(`mdq toc: missing <file>
|
|
492
|
+
`, 2);
|
|
493
|
+
const loaded = loadFile(file);
|
|
494
|
+
if ("code" in loaded)
|
|
495
|
+
return loaded;
|
|
496
|
+
const { src } = loaded;
|
|
497
|
+
const sections = buildSections(scan(src), countLines(src));
|
|
498
|
+
if (v.json) {
|
|
499
|
+
return ok(JSON.stringify({
|
|
500
|
+
file,
|
|
501
|
+
total_lines: countLines(src),
|
|
502
|
+
headings: sections.map(sectionToJSON)
|
|
503
|
+
}, null, 2));
|
|
504
|
+
}
|
|
505
|
+
const depth = readFlag(v, "depth", null);
|
|
506
|
+
if ("code" in depth)
|
|
507
|
+
return depth;
|
|
508
|
+
return ok(renderToc(file, src, sections, {
|
|
509
|
+
depth: depth.value ?? undefined,
|
|
510
|
+
flat: !!v.flat
|
|
511
|
+
}));
|
|
512
|
+
}
|
|
513
|
+
async function cmdRead(rest, v) {
|
|
514
|
+
const file = rest[0];
|
|
515
|
+
const selectorStr = rest[1];
|
|
516
|
+
if (file == null || selectorStr == null) {
|
|
517
|
+
return err(`mdq read: missing <file> or <selector>
|
|
518
|
+
`, 2);
|
|
519
|
+
}
|
|
520
|
+
if (v.pretty && v.json) {
|
|
521
|
+
return err(`mdq read: --pretty cannot be combined with --json
|
|
522
|
+
`, 2);
|
|
523
|
+
}
|
|
524
|
+
const loaded = loadFile(file);
|
|
525
|
+
if ("code" in loaded)
|
|
526
|
+
return loaded;
|
|
527
|
+
const { src } = loaded;
|
|
528
|
+
const sections = buildSections(scan(src), countLines(src));
|
|
529
|
+
const maxResults = readFlag(v, "max-results", 25);
|
|
530
|
+
if ("code" in maxResults)
|
|
531
|
+
return maxResults;
|
|
532
|
+
const maxLines = readFlag(v, "max-lines", 0);
|
|
533
|
+
if ("code" in maxLines)
|
|
534
|
+
return maxLines;
|
|
535
|
+
const selector = parseSelector(selectorStr);
|
|
536
|
+
const matches2 = match(sections, selector);
|
|
537
|
+
const srcLines = src.split(`
|
|
538
|
+
`);
|
|
539
|
+
if (v.json) {
|
|
540
|
+
return emitReadJson(file, srcLines, sections, matches2, maxResults.value ?? 25, maxLines.value ?? 0, v);
|
|
541
|
+
}
|
|
542
|
+
if (matches2.length === 0)
|
|
543
|
+
return noMatch(`(no match)
|
|
544
|
+
`);
|
|
545
|
+
let pretty;
|
|
546
|
+
if (v.pretty) {
|
|
547
|
+
try {
|
|
548
|
+
pretty = await loadPrettyFormatter();
|
|
549
|
+
} catch (e) {
|
|
550
|
+
return err(`mdq read: ${e.message}
|
|
551
|
+
`, 2);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
const cap = maxResults.value ?? 25;
|
|
555
|
+
const toPrint = matches2.slice(0, cap);
|
|
556
|
+
const out = [];
|
|
557
|
+
if (matches2.length > cap) {
|
|
558
|
+
out.push(`${matches2.length} matches, showing first ${cap}. Use --max-results=N to raise the cap.`);
|
|
559
|
+
}
|
|
560
|
+
for (const sec of toPrint) {
|
|
561
|
+
out.push(renderSection(file, srcLines, sec, {
|
|
562
|
+
bodyOnly: !!v["body-only"],
|
|
563
|
+
noBody: !!v["no-body"],
|
|
564
|
+
raw: !!v.raw,
|
|
565
|
+
maxLines: maxLines.value ?? 0,
|
|
566
|
+
allSections: sections,
|
|
567
|
+
pretty
|
|
568
|
+
}));
|
|
569
|
+
}
|
|
570
|
+
return ok(out.join(`
|
|
571
|
+
`));
|
|
572
|
+
}
|
|
573
|
+
function emitReadJson(file, srcLines, all, matches2, maxResults, maxLines, v) {
|
|
574
|
+
const body = JSON.stringify({
|
|
575
|
+
file,
|
|
576
|
+
matches: matches2.slice(0, maxResults).map((s) => ({
|
|
577
|
+
...sectionToJSON(s),
|
|
578
|
+
body: v["no-body"] ? "" : maybeTruncate(sliceBody(srcLines, s, all, !!v["body-only"]), maxLines)
|
|
579
|
+
})),
|
|
580
|
+
truncated: matches2.length > maxResults
|
|
581
|
+
}, null, 2);
|
|
582
|
+
return matches2.length === 0 ? { code: 1, stdout: body, stderr: "" } : ok(body);
|
|
583
|
+
}
|
|
584
|
+
function cmdLs(rest, v) {
|
|
585
|
+
const file = rest[0];
|
|
586
|
+
const selectorStr = rest[1];
|
|
587
|
+
if (file == null || selectorStr == null) {
|
|
588
|
+
return err(`mdq ls: missing <file> or <selector>
|
|
589
|
+
`, 2);
|
|
590
|
+
}
|
|
591
|
+
const loaded = loadFile(file);
|
|
592
|
+
if ("code" in loaded)
|
|
593
|
+
return loaded;
|
|
594
|
+
const { src } = loaded;
|
|
595
|
+
const sections = buildSections(scan(src), countLines(src));
|
|
596
|
+
const maxResults = readFlag(v, "max-results", 25);
|
|
597
|
+
if ("code" in maxResults)
|
|
598
|
+
return maxResults;
|
|
599
|
+
const cap = maxResults.value ?? 25;
|
|
600
|
+
const selector = parseSelector(selectorStr);
|
|
601
|
+
const matches2 = match(sections, selector).slice(0, cap);
|
|
602
|
+
const childrenOf = new Map;
|
|
603
|
+
for (const sec of sections) {
|
|
604
|
+
if (sec.parent) {
|
|
605
|
+
const list = childrenOf.get(sec.parent);
|
|
606
|
+
if (list)
|
|
607
|
+
list.push(sec);
|
|
608
|
+
else
|
|
609
|
+
childrenOf.set(sec.parent, [sec]);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (v.json) {
|
|
613
|
+
const results = matches2.map((parent) => ({
|
|
614
|
+
parent: sectionToJSON(parent),
|
|
615
|
+
children: (childrenOf.get(parent) ?? []).map(sectionToJSON)
|
|
616
|
+
}));
|
|
617
|
+
const body = JSON.stringify({ file, results }, null, 2);
|
|
618
|
+
return matches2.length === 0 ? { code: 1, stdout: body, stderr: "" } : ok(body);
|
|
619
|
+
}
|
|
620
|
+
if (matches2.length === 0)
|
|
621
|
+
return noMatch(`(no match)
|
|
622
|
+
`);
|
|
623
|
+
const out = [];
|
|
624
|
+
for (const parent of matches2) {
|
|
625
|
+
const children = childrenOf.get(parent) ?? [];
|
|
626
|
+
out.push(`${"#".repeat(parent.level)} ${parent.title} L${parent.line_start}-${parent.line_end}`);
|
|
627
|
+
if (children.length === 0) {
|
|
628
|
+
out.push(" (no children)");
|
|
629
|
+
} else {
|
|
630
|
+
for (const c of children) {
|
|
631
|
+
out.push(` ${"#".repeat(c.level)} ${c.title} L${c.line_start}-${c.line_end}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return ok(out.join(`
|
|
636
|
+
`));
|
|
637
|
+
}
|
|
638
|
+
function cmdGrep(rest, v) {
|
|
639
|
+
const file = rest[0];
|
|
640
|
+
const pattern = rest[1];
|
|
641
|
+
if (file == null || pattern == null) {
|
|
642
|
+
return err(`mdq grep: missing <file> or <pattern>
|
|
643
|
+
`, 2);
|
|
644
|
+
}
|
|
645
|
+
const loaded = loadFile(file);
|
|
646
|
+
if ("code" in loaded)
|
|
647
|
+
return loaded;
|
|
648
|
+
const { src } = loaded;
|
|
649
|
+
const sections = buildSections(scan(src), countLines(src));
|
|
650
|
+
let re;
|
|
651
|
+
try {
|
|
652
|
+
re = new RegExp(pattern);
|
|
653
|
+
} catch (e) {
|
|
654
|
+
return err(`mdq grep: invalid regex: ${e.message}
|
|
655
|
+
`, 2);
|
|
656
|
+
}
|
|
657
|
+
const srcLines = src.split(`
|
|
658
|
+
`);
|
|
659
|
+
const hits = [];
|
|
660
|
+
let secIdx = -1;
|
|
661
|
+
for (let lineNo = 1;lineNo <= srcLines.length; lineNo++) {
|
|
662
|
+
while (secIdx + 1 < sections.length && sections[secIdx + 1].line_start <= lineNo) {
|
|
663
|
+
secIdx++;
|
|
664
|
+
}
|
|
665
|
+
const line = srcLines[lineNo - 1];
|
|
666
|
+
if (re.test(line)) {
|
|
667
|
+
const section = secIdx >= 0 ? sections[secIdx] ?? null : null;
|
|
668
|
+
hits.push({ section, line: lineNo, text: line });
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (v.json) {
|
|
672
|
+
const body = JSON.stringify(hits.map((h) => ({
|
|
673
|
+
file,
|
|
674
|
+
line: h.line,
|
|
675
|
+
text: h.text,
|
|
676
|
+
section: h.section ? sectionToJSON(h.section) : null
|
|
677
|
+
})), null, 2);
|
|
678
|
+
return hits.length === 0 ? { code: 1, stdout: body, stderr: "" } : ok(body);
|
|
679
|
+
}
|
|
680
|
+
if (hits.length === 0)
|
|
681
|
+
return noMatch(`(no match)
|
|
682
|
+
`);
|
|
683
|
+
const out = [];
|
|
684
|
+
let lastSection = undefined;
|
|
685
|
+
for (const hit of hits) {
|
|
686
|
+
if (hit.section !== lastSection) {
|
|
687
|
+
if (hit.section) {
|
|
688
|
+
const path = pathOf(hit.section).concat(hit.section.title).join(" > ");
|
|
689
|
+
out.push(`── ${path} L${hit.section.line_start}-${hit.section.line_end}`);
|
|
690
|
+
} else {
|
|
691
|
+
out.push(`── ${file} (no enclosing heading)`);
|
|
692
|
+
}
|
|
693
|
+
lastSection = hit.section;
|
|
694
|
+
}
|
|
695
|
+
out.push(` L${hit.line}: ${hit.text}`);
|
|
696
|
+
}
|
|
697
|
+
return ok(out.join(`
|
|
698
|
+
`));
|
|
699
|
+
}
|
|
700
|
+
function sliceBody(srcLines, sec, all, bodyOnly) {
|
|
701
|
+
let end = sec.line_end;
|
|
702
|
+
if (bodyOnly) {
|
|
703
|
+
const firstChild = all.find((s) => s.parent === sec);
|
|
704
|
+
if (firstChild)
|
|
705
|
+
end = firstChild.line_start - 1;
|
|
706
|
+
}
|
|
707
|
+
return srcLines.slice(sec.line_start - 1, end).join(`
|
|
708
|
+
`);
|
|
709
|
+
}
|
|
710
|
+
function maybeTruncate(body, maxLines) {
|
|
711
|
+
return maxLines > 0 ? truncateBody(body, maxLines) : body;
|
|
712
|
+
}
|
|
713
|
+
function sectionToJSON(sec) {
|
|
714
|
+
return {
|
|
715
|
+
level: sec.level,
|
|
716
|
+
title: sec.title,
|
|
717
|
+
line_start: sec.line_start,
|
|
718
|
+
line_end: sec.line_end,
|
|
719
|
+
path: pathOf(sec)
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// bin/mdq.ts
|
|
724
|
+
var result = await run(process.argv.slice(2));
|
|
725
|
+
if (result.stdout) {
|
|
726
|
+
process.stdout.write(result.stdout.endsWith(`
|
|
727
|
+
`) ? result.stdout : result.stdout + `
|
|
728
|
+
`);
|
|
729
|
+
}
|
|
730
|
+
if (result.stderr)
|
|
731
|
+
process.stderr.write(result.stderr);
|
|
732
|
+
process.exit(result.code);
|
|
733
|
+
|
|
734
|
+
//# debugId=21A3DDC69C33B5A064756E2164756E21
|
|
735
|
+
//# sourceMappingURL=mdq.js.map
|