mdzilla 0.0.0 → 0.0.2
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/LICENSE +21 -0
- package/README.md +157 -0
- package/dist/_chunks/exporter.mjs +787 -0
- package/dist/cli/main.d.mts +1 -0
- package/dist/cli/main.mjs +788 -0
- package/dist/index.d.mts +161 -0
- package/dist/index.mjs +2 -0
- package/package.json +52 -1
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as DocsSourceGit, c as DocsManager, i as DocsSourceHTTP, n as DocsExporterFS, o as DocsSourceFS, r as DocsSourceNpm } from "../_chunks/exporter.mjs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { basename } from "node:path";
|
|
5
|
+
import { parseMeta, renderToAnsi, renderToText } from "md4x";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import { isAgent } from "std-env";
|
|
8
|
+
import { highlightText } from "@speed-highlight/core/terminal";
|
|
9
|
+
import { execSync } from "node:child_process";
|
|
10
|
+
//#region src/cli/_ansi.ts
|
|
11
|
+
const noColor = !!(process.env.NO_COLOR || process.env.TERM === "dumb" || !process.stdout.isTTY || isAgent);
|
|
12
|
+
const ESC = "\x1B[";
|
|
13
|
+
const ANSI_RE = /\x1B(?:\[[0-9;]*[a-zA-Z]|\][^\x07\x1B]*(?:\x07|\x1B\\))/g;
|
|
14
|
+
const clear = () => process.stdout.write(`${ESC}2J${ESC}3J${ESC}H`);
|
|
15
|
+
const enterAltScreen = () => process.stdout.write(`${ESC}?1049h`);
|
|
16
|
+
const leaveAltScreen = () => process.stdout.write(`${ESC}?1049l`);
|
|
17
|
+
const hideCursor = () => process.stdout.write(`${ESC}?25l`);
|
|
18
|
+
const showCursor = () => process.stdout.write(`${ESC}?25h`);
|
|
19
|
+
const bold = (s) => noColor ? s : `${ESC}1m${s}${ESC}0m`;
|
|
20
|
+
const dim = (s) => noColor ? s : `${ESC}2m${s}${ESC}0m`;
|
|
21
|
+
const cyan = (s) => noColor ? s : `${ESC}36m${s}${ESC}0m`;
|
|
22
|
+
const yellow = (s) => noColor ? s : `${ESC}33m${s}${ESC}0m`;
|
|
23
|
+
const bgGray = (s) => {
|
|
24
|
+
if (noColor) return s;
|
|
25
|
+
const bg = `${ESC}48;5;237m`;
|
|
26
|
+
return `${bg}${s.replaceAll(`${ESC}0m`, `${ESC}0m${bg}`)}${ESC}0m`;
|
|
27
|
+
};
|
|
28
|
+
const stripAnsi = (s) => s.replace(ANSI_RE, "");
|
|
29
|
+
const visibleLength = (s) => stripAnsi(s).length;
|
|
30
|
+
function padTo(s, width) {
|
|
31
|
+
const visible = visibleLength(s);
|
|
32
|
+
if (visible >= width) return truncateTo(s, width);
|
|
33
|
+
return s + " ".repeat(width - visible);
|
|
34
|
+
}
|
|
35
|
+
function truncateTo(s, width) {
|
|
36
|
+
if (visibleLength(s) <= width) return s;
|
|
37
|
+
return wrapAnsi(s, width)[0] || "";
|
|
38
|
+
}
|
|
39
|
+
function wrapAnsi(s, width) {
|
|
40
|
+
if (width <= 0 || visibleLength(s) <= width) return [s];
|
|
41
|
+
const tokens = [];
|
|
42
|
+
let last = 0;
|
|
43
|
+
const re = new RegExp(ANSI_RE.source, "g");
|
|
44
|
+
let match;
|
|
45
|
+
while ((match = re.exec(s)) !== null) {
|
|
46
|
+
for (let i = last; i < match.index; i++) tokens.push({
|
|
47
|
+
type: "char",
|
|
48
|
+
value: s[i]
|
|
49
|
+
});
|
|
50
|
+
tokens.push({
|
|
51
|
+
type: "esc",
|
|
52
|
+
value: match[0]
|
|
53
|
+
});
|
|
54
|
+
last = match.index + match[0].length;
|
|
55
|
+
}
|
|
56
|
+
for (let i = last; i < s.length; i++) tokens.push({
|
|
57
|
+
type: "char",
|
|
58
|
+
value: s[i]
|
|
59
|
+
});
|
|
60
|
+
const lines = [];
|
|
61
|
+
let current = "";
|
|
62
|
+
let col = 0;
|
|
63
|
+
let activeStyles = "";
|
|
64
|
+
let activeLink = "";
|
|
65
|
+
for (const token of tokens) if (token.type === "esc") {
|
|
66
|
+
current += token.value;
|
|
67
|
+
if (token.value.startsWith("\x1B[")) if (token.value === "\x1B[0m") activeStyles = "";
|
|
68
|
+
else activeStyles += token.value;
|
|
69
|
+
else if (token.value.startsWith("\x1B]8;;")) activeLink = token.value === "\x1B]8;;\x1B\\" || token.value === "\x1B]8;;\x07" ? "" : token.value;
|
|
70
|
+
} else {
|
|
71
|
+
if (col >= width) {
|
|
72
|
+
if (activeLink) current += "\x1B]8;;\x1B\\";
|
|
73
|
+
lines.push(current + "\x1B[0m");
|
|
74
|
+
current = activeLink + activeStyles;
|
|
75
|
+
col = 0;
|
|
76
|
+
}
|
|
77
|
+
current += token.value;
|
|
78
|
+
col++;
|
|
79
|
+
}
|
|
80
|
+
if (current) {
|
|
81
|
+
if (activeLink) current += "\x1B]8;;\x1B\\";
|
|
82
|
+
lines.push(current + "\x1B[0m");
|
|
83
|
+
}
|
|
84
|
+
return lines;
|
|
85
|
+
}
|
|
86
|
+
/** Highlight query matches inside an already-ANSI-styled string */
|
|
87
|
+
function highlightAnsi(s, query) {
|
|
88
|
+
if (!query) return s;
|
|
89
|
+
const re = new RegExp(ANSI_RE.source, "g");
|
|
90
|
+
const visibleChars = [];
|
|
91
|
+
let last = 0;
|
|
92
|
+
let match;
|
|
93
|
+
while ((match = re.exec(s)) !== null) {
|
|
94
|
+
for (let i = last; i < match.index; i++) visibleChars.push({
|
|
95
|
+
char: s[i],
|
|
96
|
+
rawIdx: i
|
|
97
|
+
});
|
|
98
|
+
last = match.index + match[0].length;
|
|
99
|
+
}
|
|
100
|
+
for (let i = last; i < s.length; i++) visibleChars.push({
|
|
101
|
+
char: s[i],
|
|
102
|
+
rawIdx: i
|
|
103
|
+
});
|
|
104
|
+
const lowerVisible = visibleChars.map((c) => c.char).join("").toLowerCase();
|
|
105
|
+
const lowerQuery = query.toLowerCase();
|
|
106
|
+
const matches = [];
|
|
107
|
+
let pos = 0;
|
|
108
|
+
while ((pos = lowerVisible.indexOf(lowerQuery, pos)) >= 0) {
|
|
109
|
+
matches.push([pos, pos + query.length]);
|
|
110
|
+
pos += 1;
|
|
111
|
+
}
|
|
112
|
+
if (matches.length === 0) return s;
|
|
113
|
+
const hlRanges = [];
|
|
114
|
+
for (const [start, end] of matches) {
|
|
115
|
+
const rawStart = visibleChars[start].rawIdx;
|
|
116
|
+
const rawEnd = end < visibleChars.length ? visibleChars[end].rawIdx : s.length;
|
|
117
|
+
hlRanges.push([rawStart, rawEnd]);
|
|
118
|
+
}
|
|
119
|
+
const hlOn = `${ESC}7m`;
|
|
120
|
+
const hlOff = `${ESC}27m`;
|
|
121
|
+
const resetSeq = `${ESC}0m`;
|
|
122
|
+
let result = "";
|
|
123
|
+
let inHighlight = false;
|
|
124
|
+
let rawIdx = 0;
|
|
125
|
+
const escRe = new RegExp(ANSI_RE.source, "g");
|
|
126
|
+
let escMatch;
|
|
127
|
+
const escapes = [];
|
|
128
|
+
while ((escMatch = escRe.exec(s)) !== null) escapes.push({
|
|
129
|
+
idx: escMatch.index,
|
|
130
|
+
seq: escMatch[0]
|
|
131
|
+
});
|
|
132
|
+
let escIdx = 0;
|
|
133
|
+
for (rawIdx = 0; rawIdx <= s.length; rawIdx++) {
|
|
134
|
+
const wasIn = inHighlight;
|
|
135
|
+
inHighlight = hlRanges.some(([a, b]) => rawIdx >= a && rawIdx < b);
|
|
136
|
+
if (inHighlight && !wasIn) result += hlOn;
|
|
137
|
+
if (!inHighlight && wasIn) result += hlOff;
|
|
138
|
+
if (rawIdx >= s.length) break;
|
|
139
|
+
if (escIdx < escapes.length && escapes[escIdx].idx === rawIdx) {
|
|
140
|
+
const esc = escapes[escIdx];
|
|
141
|
+
result += esc.seq;
|
|
142
|
+
if (inHighlight && esc.seq === resetSeq) result += hlOn;
|
|
143
|
+
rawIdx += esc.seq.length - 1;
|
|
144
|
+
escIdx++;
|
|
145
|
+
} else result += s[rawIdx];
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
function highlight(text, query) {
|
|
150
|
+
if (!query) return text;
|
|
151
|
+
const idx = text.toLowerCase().indexOf(query.toLowerCase());
|
|
152
|
+
if (idx < 0) return text;
|
|
153
|
+
return text.slice(0, idx) + bold(yellow(text.slice(idx, idx + query.length))) + text.slice(idx + query.length);
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/cli/_usage.ts
|
|
157
|
+
function printUsage(hasInput) {
|
|
158
|
+
const bin = `${bold(cyan("npx"))} ${bold("mdzilla")}`;
|
|
159
|
+
const banner = isAgent ? [] : [
|
|
160
|
+
dim(" /\\ /\\ /\\"),
|
|
161
|
+
dim(" / \\ / \\ / \\"),
|
|
162
|
+
dim(" ╭────────────────╮"),
|
|
163
|
+
dim(" │") + bold(" # ") + dim(" ░░░░░ │"),
|
|
164
|
+
dim(" │ ░░░░░░░░ │"),
|
|
165
|
+
dim(" │ ░░░░░░ │"),
|
|
166
|
+
dim(" │ ░░░░░░░ │"),
|
|
167
|
+
dim(" │ ░░░░ │"),
|
|
168
|
+
dim(" │ ") + cyan("◉") + dim(" ") + cyan("◉") + dim(" │"),
|
|
169
|
+
dim(" ╰─┬──┬──┬──┬──┬──╯"),
|
|
170
|
+
dim(" ▽ ▽ ▽ ▽ ▽"),
|
|
171
|
+
""
|
|
172
|
+
];
|
|
173
|
+
console.log([
|
|
174
|
+
...banner,
|
|
175
|
+
` ${bold("mdzilla")} ${dim("— Markdown browser for humans and agents")}`,
|
|
176
|
+
"",
|
|
177
|
+
`${bold("Usage:")}`,
|
|
178
|
+
` ${bin} ${cyan("<dir>")} ${dim("Browse local docs directory")}`,
|
|
179
|
+
` ${bin} ${cyan("<file.md>")} ${dim("Render a single markdown file")}`,
|
|
180
|
+
` ${bin} ${cyan("gh:owner/repo")} ${dim("Browse GitHub repo docs")}`,
|
|
181
|
+
` ${bin} ${cyan("npm:package-name")} ${dim("Browse npm package docs")}`,
|
|
182
|
+
` ${bin} ${cyan("https://example.com")} ${dim("Browse remote docs via HTTP")}`,
|
|
183
|
+
"",
|
|
184
|
+
`${bold("Options:")}`,
|
|
185
|
+
` ${cyan("--export")} ${dim("<dir>")} Export docs to flat .md files`,
|
|
186
|
+
` ${cyan("--page")} ${dim("<path>")} Print a single page and exit`,
|
|
187
|
+
` ${cyan("--plain")} Plain text output (no TUI)`,
|
|
188
|
+
` ${cyan("--headless")} Alias for --plain`,
|
|
189
|
+
` ${cyan("-h, --help")} Show this help message`,
|
|
190
|
+
"",
|
|
191
|
+
`${bold("Remarks:")}`,
|
|
192
|
+
` ${dim("Headless mode is auto-enabled when called by AI agents or when stdout is not a TTY.")}`,
|
|
193
|
+
` ${dim("GitHub source (gh:) looks for a docs/ directory in the repository.")}`,
|
|
194
|
+
` ${dim("HTTP source tries /llms.txt first, then fetches with Accept: text/markdown,")}`,
|
|
195
|
+
` ${dim("and falls back to HTML-to-markdown conversion.")}`
|
|
196
|
+
].join("\n"));
|
|
197
|
+
process.exit(hasInput ? 0 : 1);
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/cli/content.ts
|
|
201
|
+
async function renderContent(content, entry, navWidth) {
|
|
202
|
+
const contentWidth = (process.stdout.columns || 80) - navWidth - 2;
|
|
203
|
+
const codeBlocks = [];
|
|
204
|
+
const codeRe = /```(\w+)[^\n]*\n([\s\S]*?)```/g;
|
|
205
|
+
let m;
|
|
206
|
+
while ((m = codeRe.exec(content)) !== null) codeBlocks.push({
|
|
207
|
+
lang: m[1],
|
|
208
|
+
code: m[2]
|
|
209
|
+
});
|
|
210
|
+
const highlights = /* @__PURE__ */ new Map();
|
|
211
|
+
if (codeBlocks.length > 0) await Promise.all(codeBlocks.map(({ lang, code }) => highlightText(code, lang).then((h) => highlights.set(code, h)).catch(() => {})));
|
|
212
|
+
const rawLines = renderToAnsi(content).split("\n");
|
|
213
|
+
const lines = [];
|
|
214
|
+
lines.push(dim(entry.path));
|
|
215
|
+
lines.push("");
|
|
216
|
+
let inDim = false;
|
|
217
|
+
let dimLines = [];
|
|
218
|
+
let blockIdx = 0;
|
|
219
|
+
for (const rawLine of rawLines) {
|
|
220
|
+
if (!inDim && rawLine.includes("\x1B[2m") && !rawLine.includes("\x1B[22m")) {
|
|
221
|
+
inDim = true;
|
|
222
|
+
dimLines = [rawLine];
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (inDim) {
|
|
226
|
+
if (rawLine.startsWith("\x1B[22m")) {
|
|
227
|
+
inDim = false;
|
|
228
|
+
const block = codeBlocks[blockIdx];
|
|
229
|
+
const hl = block ? highlights.get(block.code) : void 0;
|
|
230
|
+
if (hl) {
|
|
231
|
+
const hlLines = hl.split("\n");
|
|
232
|
+
if (hlLines.length > 0 && hlLines[hlLines.length - 1] === "") hlLines.pop();
|
|
233
|
+
for (const hlLine of hlLines) for (const w of wrapAnsi(" " + hlLine, contentWidth)) lines.push(w + "\x1B[0m");
|
|
234
|
+
} else for (const dl of dimLines) for (const w of wrapAnsi(dl, contentWidth)) lines.push(w + "\x1B[0m");
|
|
235
|
+
blockIdx++;
|
|
236
|
+
lines.push("");
|
|
237
|
+
} else dimLines.push(rawLine);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
for (const w of wrapAnsi(rawLine, contentWidth)) lines.push(w + "\x1B[0m");
|
|
241
|
+
}
|
|
242
|
+
return lines;
|
|
243
|
+
}
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/cli/render.ts
|
|
246
|
+
async function singleFileMode(filePath, plain, isURL) {
|
|
247
|
+
const raw = isURL ? await fetch(filePath, { headers: { accept: "text/markdown, text/plain;q=0.9, text/html;q=0.8" } }).then((r) => r.text()) : await readFile(filePath, "utf8");
|
|
248
|
+
if (plain) {
|
|
249
|
+
process.stdout.write(renderToText(raw) + "\n");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const meta = parseMeta(raw);
|
|
253
|
+
const slug = isURL ? new URL(filePath).pathname.split("/").pop()?.replace(/\.md$/i, "") || "page" : basename(filePath, ".md");
|
|
254
|
+
const lines = await renderContent(raw, {
|
|
255
|
+
slug,
|
|
256
|
+
path: "/" + slug,
|
|
257
|
+
title: meta.title || slug,
|
|
258
|
+
order: 0
|
|
259
|
+
}, 0);
|
|
260
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
261
|
+
}
|
|
262
|
+
async function pageMode(docs, pagePath, plain) {
|
|
263
|
+
const normalized = pagePath.startsWith("/") ? pagePath : "/" + pagePath;
|
|
264
|
+
const { entry, raw } = await docs.resolvePage(normalized);
|
|
265
|
+
if (!raw) {
|
|
266
|
+
console.error(`Page not found: ${pagePath}`);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
const slug = (entry?.entry.path || normalized).split("/").pop() || "";
|
|
270
|
+
const navEntry = entry?.entry || {
|
|
271
|
+
slug,
|
|
272
|
+
path: normalized,
|
|
273
|
+
title: parseMeta(raw).title || slug,
|
|
274
|
+
order: 0
|
|
275
|
+
};
|
|
276
|
+
if (plain) process.stdout.write(renderToText(raw) + "\n");
|
|
277
|
+
else {
|
|
278
|
+
const lines = await renderContent(raw, navEntry, 0);
|
|
279
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async function plainMode(docs, pagePath) {
|
|
283
|
+
const navigable = docs.pages;
|
|
284
|
+
if (navigable.length === 0) {
|
|
285
|
+
console.log("No pages found.");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const tocLines = ["Table of Contents", ""];
|
|
289
|
+
for (const f of navigable) {
|
|
290
|
+
const indent = " ".repeat(f.depth);
|
|
291
|
+
tocLines.push(`${indent}- [${f.entry.title}](${f.entry.path})`);
|
|
292
|
+
}
|
|
293
|
+
process.stdout.write(tocLines.join("\n") + "\n");
|
|
294
|
+
let targetEntry = navigable[0];
|
|
295
|
+
if (pagePath) {
|
|
296
|
+
const resolved = await docs.resolvePage(pagePath);
|
|
297
|
+
if (resolved.raw) process.stdout.write(renderToText(resolved.raw) + "\n\n");
|
|
298
|
+
} else {
|
|
299
|
+
const raw = await docs.getContent(targetEntry);
|
|
300
|
+
if (raw) process.stdout.write(renderToText(raw) + "\n\n");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/cli/interactive/nav.ts
|
|
305
|
+
function calcNavWidth(flat) {
|
|
306
|
+
const cols = process.stdout.columns || 80;
|
|
307
|
+
let max = 6;
|
|
308
|
+
for (const { entry, depth } of flat) {
|
|
309
|
+
const w = depth === 0 ? 3 + entry.title.length : 2 + 2 * depth + entry.title.length;
|
|
310
|
+
if (w > max) max = w;
|
|
311
|
+
}
|
|
312
|
+
return Math.min(max + 2, Math.floor(cols * .2), 56);
|
|
313
|
+
}
|
|
314
|
+
function renderNavPanel(flat, cursor, maxRows, width, search, searchMatches) {
|
|
315
|
+
const lines = [];
|
|
316
|
+
lines.push(search !== void 0 ? bold(" mdzilla") + " " + dim("/") + search + "▌" : bold(" mdzilla"));
|
|
317
|
+
lines.push("");
|
|
318
|
+
const listRows = maxRows - 2;
|
|
319
|
+
let start = 0;
|
|
320
|
+
if (flat.length > listRows) start = Math.max(0, Math.min(cursor - Math.floor(listRows / 2), flat.length - listRows));
|
|
321
|
+
const end = Math.min(start + listRows, flat.length);
|
|
322
|
+
const treePrefixes = computeTreePrefixes(flat, computeIsLastChild(flat));
|
|
323
|
+
for (let i = start; i < end; i++) {
|
|
324
|
+
const { entry, depth } = flat[i];
|
|
325
|
+
const isPage = entry.page !== false;
|
|
326
|
+
const active = i === cursor;
|
|
327
|
+
const maxTitle = width - (depth === 0 ? 3 : 2 + 2 * depth) - 1;
|
|
328
|
+
let displayTitle = entry.title;
|
|
329
|
+
if (displayTitle.length > maxTitle && maxTitle > 1) displayTitle = displayTitle.slice(0, maxTitle - 1) + "…";
|
|
330
|
+
const isMatch = searchMatches ? searchMatches.has(i) : false;
|
|
331
|
+
const title = search ? highlight(displayTitle, search) : displayTitle;
|
|
332
|
+
let label;
|
|
333
|
+
if (depth === 0) label = `${isPage ? dim("◆") : dim("◇")} ${search ? isMatch ? title : dim(displayTitle) : active || isPage ? title : dim(title)}`;
|
|
334
|
+
else label = ` ${dim(treePrefixes[i])}${search ? isMatch ? title : dim(displayTitle) : active || isPage ? title : dim(title)}`;
|
|
335
|
+
lines.push(active ? bgGray(padTo(label, width)) : label);
|
|
336
|
+
}
|
|
337
|
+
if (flat.length > listRows) lines.push(dim(` ${start > 0 ? "↑" : " "} ${end < flat.length ? "↓" : " "} ${cursor + 1}/${flat.length}`));
|
|
338
|
+
return lines;
|
|
339
|
+
}
|
|
340
|
+
function computeIsLastChild(flat) {
|
|
341
|
+
const result = Array.from({ length: flat.length });
|
|
342
|
+
for (let i = 0; i < flat.length; i++) {
|
|
343
|
+
const d = flat[i].depth;
|
|
344
|
+
let last = true;
|
|
345
|
+
for (let j = i + 1; j < flat.length; j++) {
|
|
346
|
+
if (flat[j].depth < d) break;
|
|
347
|
+
if (flat[j].depth === d) {
|
|
348
|
+
last = false;
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
result[i] = last;
|
|
353
|
+
}
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
function computeTreePrefixes(flat, isLast) {
|
|
357
|
+
const prefixes = Array.from({ length: flat.length });
|
|
358
|
+
const continues = [];
|
|
359
|
+
for (let i = 0; i < flat.length; i++) {
|
|
360
|
+
const d = flat[i].depth;
|
|
361
|
+
if (d === 0) {
|
|
362
|
+
prefixes[i] = "";
|
|
363
|
+
continues.length = 1;
|
|
364
|
+
continues[0] = !isLast[i];
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
let prefix = "";
|
|
368
|
+
for (let level = 1; level < d; level++) prefix += continues[level] ? "│ " : " ";
|
|
369
|
+
prefix += isLast[i] ? "╰─" : "├─";
|
|
370
|
+
prefixes[i] = prefix;
|
|
371
|
+
continues[d] = !isLast[i];
|
|
372
|
+
continues.length = d + 1;
|
|
373
|
+
}
|
|
374
|
+
return prefixes;
|
|
375
|
+
}
|
|
376
|
+
//#endregion
|
|
377
|
+
//#region src/cli/interactive/render.ts
|
|
378
|
+
function renderSplit(flat, cursor, contentLines, contentScroll, search, focus, contentSearch, searchMatches, sidebarVisible = true) {
|
|
379
|
+
const rows = process.stdout.rows || 24;
|
|
380
|
+
const navWidth = sidebarVisible ? calcNavWidth(flat) : 0;
|
|
381
|
+
const bodyRows = rows - 1;
|
|
382
|
+
const navLines = sidebarVisible ? renderNavPanel(flat, cursor, bodyRows, navWidth, search, searchMatches) : [];
|
|
383
|
+
const rawRight = contentLines.slice(contentScroll, contentScroll + bodyRows);
|
|
384
|
+
const rightLines = contentSearch ? rawRight.map((l) => highlightAnsi(l, contentSearch)) : rawRight;
|
|
385
|
+
const cols = process.stdout.columns || 80;
|
|
386
|
+
const contentWidth = sidebarVisible ? cols - navWidth - 3 : cols - 2;
|
|
387
|
+
const isFocusContent = focus === "content" || focus === "content-search";
|
|
388
|
+
const reset = "\x1B[0m";
|
|
389
|
+
const output = [];
|
|
390
|
+
for (let i = 0; i < bodyRows; i++) {
|
|
391
|
+
const right = rightLines[i] || "";
|
|
392
|
+
if (sidebarVisible) output.push(reset + padTo(navLines[i] || "", navWidth) + reset + (isFocusContent ? cyan("│") : dim("│")) + reset + " " + truncateTo(right, contentWidth) + reset);
|
|
393
|
+
else output.push(reset + " " + truncateTo(right, contentWidth) + reset);
|
|
394
|
+
}
|
|
395
|
+
output.push(search !== void 0 ? dim(" esc") + " cancel " + dim("enter") + " go" : focus === "content-search" ? dim(" /") + contentSearch + "▌ " + dim("esc") + " cancel " + dim("enter") + " confirm" : focus === "content" ? dim(" ↑↓") + " scroll " + dim("tab") + " links " + dim("⏎") + " open " + dim("/") + " search" + (contentSearch ? " " + dim("n") + "/" + dim("N") + " next/prev" : "") + " " + dim("t") + " sidebar " + dim("⌫") + " back " + dim("q") + " quit" : dim(" ↑↓") + " navigate " + dim("⏎") + " read " + dim("space") + " page ↓ " + dim("/") + " search " + dim("t") + " sidebar " + dim("q") + " quit");
|
|
396
|
+
const eol = "\x1B[K";
|
|
397
|
+
return output.map((l) => l + eol).join("\n");
|
|
398
|
+
}
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/cli/interactive/index.ts
|
|
401
|
+
async function interactiveMode(docs) {
|
|
402
|
+
const flat = docs.flat;
|
|
403
|
+
if (flat.length === 0) {
|
|
404
|
+
console.log("No pages found.");
|
|
405
|
+
process.exit(0);
|
|
406
|
+
}
|
|
407
|
+
const isNavigable = (list, i) => list[i]?.entry.page !== false;
|
|
408
|
+
const nextNavigable = (list, from, dir) => {
|
|
409
|
+
let i = from + dir;
|
|
410
|
+
while (i >= 0 && i < list.length) {
|
|
411
|
+
if (isNavigable(list, i)) return i;
|
|
412
|
+
i += dir;
|
|
413
|
+
}
|
|
414
|
+
return from;
|
|
415
|
+
};
|
|
416
|
+
const firstNavigable = (list) => {
|
|
417
|
+
for (let i = 0; i < list.length; i++) if (isNavigable(list, i)) return i;
|
|
418
|
+
return 0;
|
|
419
|
+
};
|
|
420
|
+
let cursor = firstNavigable(flat);
|
|
421
|
+
let searching = false;
|
|
422
|
+
let searchQuery = "";
|
|
423
|
+
let searchMatches = [];
|
|
424
|
+
let contentScroll = 0;
|
|
425
|
+
let contentLines = [];
|
|
426
|
+
let loadedPath = "";
|
|
427
|
+
let focusContent = false;
|
|
428
|
+
let contentSearching = false;
|
|
429
|
+
let contentSearchQuery = "";
|
|
430
|
+
let contentMatches = [];
|
|
431
|
+
let contentMatchIdx = 0;
|
|
432
|
+
let sidebarVisible = true;
|
|
433
|
+
let contentLinks = [];
|
|
434
|
+
let linkIdx = -1;
|
|
435
|
+
const extractLinks = (lines) => {
|
|
436
|
+
const links = [];
|
|
437
|
+
const re = /\x1B\]8;;([^\x07\x1B]+?)(?:\x07|\x1B\\)/g;
|
|
438
|
+
for (let i = 0; i < lines.length; i++) {
|
|
439
|
+
re.lastIndex = 0;
|
|
440
|
+
let m;
|
|
441
|
+
let occ = 0;
|
|
442
|
+
while ((m = re.exec(lines[i])) !== null) links.push({
|
|
443
|
+
line: i,
|
|
444
|
+
url: m[1],
|
|
445
|
+
occurrence: occ++
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
return links;
|
|
449
|
+
};
|
|
450
|
+
const highlightLinkOnLine = (line, occurrence) => {
|
|
451
|
+
const oscRe = /\x1B\]8;;([^\x07\x1B]*?)(?:\x07|\x1B\\)/g;
|
|
452
|
+
let occ = 0;
|
|
453
|
+
let m;
|
|
454
|
+
while ((m = oscRe.exec(line)) !== null) {
|
|
455
|
+
if (!m[1]) continue;
|
|
456
|
+
if (occ === occurrence) {
|
|
457
|
+
const openerEnd = m.index + m[0].length;
|
|
458
|
+
const closer = oscRe.exec(line);
|
|
459
|
+
if (!closer) break;
|
|
460
|
+
return line.slice(0, openerEnd) + "\x1B[7m" + line.slice(openerEnd, closer.index) + "\x1B[27m" + line.slice(closer.index);
|
|
461
|
+
}
|
|
462
|
+
occ++;
|
|
463
|
+
}
|
|
464
|
+
return line;
|
|
465
|
+
};
|
|
466
|
+
const findContentMatches = (query) => {
|
|
467
|
+
if (!query) return [];
|
|
468
|
+
const lower = query.toLowerCase();
|
|
469
|
+
const matches = [];
|
|
470
|
+
for (let i = 0; i < contentLines.length; i++) if (stripAnsi(contentLines[i]).toLowerCase().includes(lower)) matches.push(i);
|
|
471
|
+
return matches;
|
|
472
|
+
};
|
|
473
|
+
const scrollToMatch = () => {
|
|
474
|
+
if (contentMatches.length === 0) return;
|
|
475
|
+
const rows = process.stdout.rows || 24;
|
|
476
|
+
const line = contentMatches[contentMatchIdx];
|
|
477
|
+
contentScroll = Math.max(0, Math.min(line - Math.floor(rows / 2), contentLines.length - rows + 2));
|
|
478
|
+
};
|
|
479
|
+
process.stdin.setRawMode(true);
|
|
480
|
+
process.stdin.resume();
|
|
481
|
+
enterAltScreen();
|
|
482
|
+
hideCursor();
|
|
483
|
+
const draw = () => {
|
|
484
|
+
let displayLines = contentLines;
|
|
485
|
+
if (linkIdx >= 0 && linkIdx < contentLinks.length) {
|
|
486
|
+
const link = contentLinks[linkIdx];
|
|
487
|
+
displayLines = [...contentLines];
|
|
488
|
+
displayLines[link.line] = highlightLinkOnLine(displayLines[link.line], link.occurrence);
|
|
489
|
+
}
|
|
490
|
+
const frame = renderSplit(flat, cursor, displayLines, contentScroll, searching ? searchQuery : void 0, contentSearching ? "content-search" : focusContent ? "content" : "nav", contentSearching ? contentSearchQuery : contentSearchQuery || void 0, searching ? new Set(searchMatches) : void 0, sidebarVisible);
|
|
491
|
+
process.stdout.write(`\x1B[H${frame}`);
|
|
492
|
+
};
|
|
493
|
+
const loadContent = (entry) => {
|
|
494
|
+
if (!entry?.filePath || entry.entry.page === false) {
|
|
495
|
+
if (loadedPath !== "") {
|
|
496
|
+
contentLines = [];
|
|
497
|
+
contentScroll = 0;
|
|
498
|
+
loadedPath = "";
|
|
499
|
+
draw();
|
|
500
|
+
}
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (entry.filePath === loadedPath) return;
|
|
504
|
+
const targetPath = entry.filePath;
|
|
505
|
+
docs.getContent(entry).then(async (raw) => {
|
|
506
|
+
if (!raw || flat[cursor]?.filePath !== targetPath) return;
|
|
507
|
+
contentLines = await renderContent(raw, entry.entry, sidebarVisible ? calcNavWidth(flat) : 0);
|
|
508
|
+
contentScroll = 0;
|
|
509
|
+
contentSearchQuery = "";
|
|
510
|
+
contentMatches = [];
|
|
511
|
+
contentLinks = extractLinks(contentLines);
|
|
512
|
+
linkIdx = -1;
|
|
513
|
+
loadedPath = targetPath;
|
|
514
|
+
draw();
|
|
515
|
+
});
|
|
516
|
+
};
|
|
517
|
+
let cleaned = false;
|
|
518
|
+
const cleanup = (code = 0) => {
|
|
519
|
+
if (cleaned) return;
|
|
520
|
+
cleaned = true;
|
|
521
|
+
showCursor();
|
|
522
|
+
leaveAltScreen();
|
|
523
|
+
process.exit(code);
|
|
524
|
+
};
|
|
525
|
+
const reloadContent = () => {
|
|
526
|
+
const entry = flat[cursor];
|
|
527
|
+
if (!entry?.filePath || entry.entry.page === false) return;
|
|
528
|
+
docs.invalidate(entry.filePath);
|
|
529
|
+
loadedPath = "";
|
|
530
|
+
loadContent(entry);
|
|
531
|
+
};
|
|
532
|
+
process.on("SIGINT", () => cleanup());
|
|
533
|
+
process.on("SIGTERM", () => cleanup());
|
|
534
|
+
process.on("uncaughtException", (err) => {
|
|
535
|
+
if (cleaned) return;
|
|
536
|
+
cleaned = true;
|
|
537
|
+
showCursor();
|
|
538
|
+
leaveAltScreen();
|
|
539
|
+
console.error(err);
|
|
540
|
+
process.exit(1);
|
|
541
|
+
});
|
|
542
|
+
process.on("unhandledRejection", (err) => {
|
|
543
|
+
if (cleaned) return;
|
|
544
|
+
cleaned = true;
|
|
545
|
+
showCursor();
|
|
546
|
+
leaveAltScreen();
|
|
547
|
+
console.error(err);
|
|
548
|
+
process.exit(1);
|
|
549
|
+
});
|
|
550
|
+
process.stdout.on("resize", () => {
|
|
551
|
+
reloadContent();
|
|
552
|
+
draw();
|
|
553
|
+
});
|
|
554
|
+
clear();
|
|
555
|
+
draw();
|
|
556
|
+
loadContent(flat[cursor]);
|
|
557
|
+
process.stdin.on("data", (data) => {
|
|
558
|
+
const key = data.toString();
|
|
559
|
+
if (key === "") return cleanup();
|
|
560
|
+
if (searching) handleSearch(key);
|
|
561
|
+
else if (contentSearching) handleContentSearch(key);
|
|
562
|
+
else if (focusContent) handleContent(key);
|
|
563
|
+
else handleNav(key);
|
|
564
|
+
draw();
|
|
565
|
+
});
|
|
566
|
+
function handleNav(key) {
|
|
567
|
+
const rows = process.stdout.rows || 24;
|
|
568
|
+
const maxScroll = Math.max(0, contentLines.length - rows + 2);
|
|
569
|
+
if (key === "q") return cleanup();
|
|
570
|
+
if (key === "/") {
|
|
571
|
+
searching = true;
|
|
572
|
+
searchQuery = "";
|
|
573
|
+
searchMatches = [];
|
|
574
|
+
showCursor();
|
|
575
|
+
} else if (key === "\x1B[A" || key === "k") {
|
|
576
|
+
cursor = nextNavigable(flat, cursor, -1);
|
|
577
|
+
loadContent(flat[cursor]);
|
|
578
|
+
} else if (key === "\x1B[B" || key === "j") {
|
|
579
|
+
cursor = nextNavigable(flat, cursor, 1);
|
|
580
|
+
loadContent(flat[cursor]);
|
|
581
|
+
} else if (key === "\r" || key === "\n" || key === " " || key === "\x1B[C") {
|
|
582
|
+
if (contentLines.length > 0) focusContent = true;
|
|
583
|
+
} else if (key === " " || key === "\x1B[6~") contentScroll = Math.min(maxScroll, contentScroll + rows - 2);
|
|
584
|
+
else if (key === "b" || key === "\x1B[5~") contentScroll = Math.max(0, contentScroll - rows + 2);
|
|
585
|
+
else if (key === "g") {
|
|
586
|
+
cursor = firstNavigable(flat);
|
|
587
|
+
loadContent(flat[cursor]);
|
|
588
|
+
} else if (key === "G") {
|
|
589
|
+
for (let i = flat.length - 1; i >= 0; i--) if (isNavigable(flat, i)) {
|
|
590
|
+
cursor = i;
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
loadContent(flat[cursor]);
|
|
594
|
+
} else if (key === "t") {
|
|
595
|
+
sidebarVisible = !sidebarVisible;
|
|
596
|
+
reloadContent();
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
const scrollToLink = () => {
|
|
600
|
+
if (linkIdx < 0 || linkIdx >= contentLinks.length) return;
|
|
601
|
+
const rows = process.stdout.rows || 24;
|
|
602
|
+
const line = contentLinks[linkIdx].line;
|
|
603
|
+
if (line < contentScroll || line >= contentScroll + rows - 2) contentScroll = Math.max(0, Math.min(line - Math.floor(rows / 3), contentLines.length - rows + 2));
|
|
604
|
+
};
|
|
605
|
+
const activateLink = (url) => {
|
|
606
|
+
if (url.startsWith("http://") || url.startsWith("https://")) try {
|
|
607
|
+
execSync(`open ${JSON.stringify(url)}`, { stdio: "ignore" });
|
|
608
|
+
} catch {}
|
|
609
|
+
else {
|
|
610
|
+
const target = url.replace(/^\.\//, "/").replace(/\/$/, "");
|
|
611
|
+
const idx = flat.indexOf(docs.findByPath(target));
|
|
612
|
+
if (idx >= 0) {
|
|
613
|
+
cursor = idx;
|
|
614
|
+
focusContent = false;
|
|
615
|
+
linkIdx = -1;
|
|
616
|
+
loadContent(flat[cursor]);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
function handleContent(key) {
|
|
621
|
+
const rows = process.stdout.rows || 24;
|
|
622
|
+
const maxScroll = Math.max(0, contentLines.length - rows + 2);
|
|
623
|
+
if (key === "" || key === "\b" || key === "\x1B[D" || key === "\x1B") {
|
|
624
|
+
focusContent = false;
|
|
625
|
+
linkIdx = -1;
|
|
626
|
+
contentSearchQuery = "";
|
|
627
|
+
contentMatches = [];
|
|
628
|
+
} else if (key === " ") {
|
|
629
|
+
if (contentLinks.length > 0) {
|
|
630
|
+
linkIdx = linkIdx < contentLinks.length - 1 ? linkIdx + 1 : 0;
|
|
631
|
+
scrollToLink();
|
|
632
|
+
}
|
|
633
|
+
} else if (key === "\x1B[Z") {
|
|
634
|
+
if (contentLinks.length > 0) {
|
|
635
|
+
linkIdx = linkIdx > 0 ? linkIdx - 1 : contentLinks.length - 1;
|
|
636
|
+
scrollToLink();
|
|
637
|
+
}
|
|
638
|
+
} else if ((key === "\r" || key === "\n") && linkIdx >= 0 && linkIdx < contentLinks.length) activateLink(contentLinks[linkIdx].url);
|
|
639
|
+
else if (key === "\x1B[A" || key === "k") contentScroll = Math.max(0, contentScroll - 1);
|
|
640
|
+
else if (key === "\x1B[B" || key === "j") contentScroll = Math.min(maxScroll, contentScroll + 1);
|
|
641
|
+
else if (key === " " || key === "\x1B[6~") contentScroll = Math.min(maxScroll, contentScroll + rows - 2);
|
|
642
|
+
else if (key === "b" || key === "\x1B[5~") contentScroll = Math.max(0, contentScroll - rows + 2);
|
|
643
|
+
else if (key === "/") {
|
|
644
|
+
contentSearching = true;
|
|
645
|
+
contentSearchQuery = "";
|
|
646
|
+
contentMatches = [];
|
|
647
|
+
contentMatchIdx = 0;
|
|
648
|
+
showCursor();
|
|
649
|
+
} else if (key === "n" && contentMatches.length > 0) {
|
|
650
|
+
contentMatchIdx = (contentMatchIdx + 1) % contentMatches.length;
|
|
651
|
+
scrollToMatch();
|
|
652
|
+
} else if (key === "N" && contentMatches.length > 0) {
|
|
653
|
+
contentMatchIdx = (contentMatchIdx - 1 + contentMatches.length) % contentMatches.length;
|
|
654
|
+
scrollToMatch();
|
|
655
|
+
} else if (key === "g") contentScroll = 0;
|
|
656
|
+
else if (key === "G") contentScroll = maxScroll;
|
|
657
|
+
else if (key === "t") {
|
|
658
|
+
sidebarVisible = !sidebarVisible;
|
|
659
|
+
reloadContent();
|
|
660
|
+
} else if (key === "q") return cleanup();
|
|
661
|
+
}
|
|
662
|
+
function handleContentSearch(key) {
|
|
663
|
+
if (key === "\x1B") {
|
|
664
|
+
contentSearching = false;
|
|
665
|
+
contentSearchQuery = "";
|
|
666
|
+
contentMatches = [];
|
|
667
|
+
hideCursor();
|
|
668
|
+
} else if (key === "\r" || key === "\n") {
|
|
669
|
+
contentSearching = false;
|
|
670
|
+
hideCursor();
|
|
671
|
+
} else if (key === "" || key === "\b") {
|
|
672
|
+
contentSearchQuery = contentSearchQuery.slice(0, -1);
|
|
673
|
+
contentMatches = findContentMatches(contentSearchQuery);
|
|
674
|
+
contentMatchIdx = 0;
|
|
675
|
+
scrollToMatch();
|
|
676
|
+
} else if (key === "\x1B[A") {
|
|
677
|
+
if (contentMatches.length > 0) {
|
|
678
|
+
contentMatchIdx = (contentMatchIdx - 1 + contentMatches.length) % contentMatches.length;
|
|
679
|
+
scrollToMatch();
|
|
680
|
+
}
|
|
681
|
+
} else if (key === "\x1B[B") {
|
|
682
|
+
if (contentMatches.length > 0) {
|
|
683
|
+
contentMatchIdx = (contentMatchIdx + 1) % contentMatches.length;
|
|
684
|
+
scrollToMatch();
|
|
685
|
+
}
|
|
686
|
+
} else if (key.length === 1 && key >= " ") {
|
|
687
|
+
contentSearchQuery += key;
|
|
688
|
+
contentMatches = findContentMatches(contentSearchQuery);
|
|
689
|
+
contentMatchIdx = 0;
|
|
690
|
+
scrollToMatch();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
function updateSearchMatches() {
|
|
694
|
+
searchMatches = docs.matchIndices(searchQuery);
|
|
695
|
+
if (searchMatches.length > 0) cursor = searchMatches[0];
|
|
696
|
+
else if (!searchQuery) cursor = firstNavigable(flat);
|
|
697
|
+
loadContent(flat[cursor]);
|
|
698
|
+
}
|
|
699
|
+
function nextSearchMatch(dir) {
|
|
700
|
+
if (searchMatches.length === 0) return;
|
|
701
|
+
const curIdx = searchMatches.indexOf(cursor);
|
|
702
|
+
if (curIdx < 0) cursor = searchMatches[0];
|
|
703
|
+
else {
|
|
704
|
+
const next = curIdx + dir;
|
|
705
|
+
cursor = searchMatches[(next + searchMatches.length) % searchMatches.length];
|
|
706
|
+
}
|
|
707
|
+
loadContent(flat[cursor]);
|
|
708
|
+
}
|
|
709
|
+
function handleSearch(key) {
|
|
710
|
+
if (key === "\x1B" || key === "\x1B[D") {
|
|
711
|
+
searching = false;
|
|
712
|
+
searchMatches = [];
|
|
713
|
+
cursor = firstNavigable(flat);
|
|
714
|
+
hideCursor();
|
|
715
|
+
loadContent(flat[cursor]);
|
|
716
|
+
} else if (key === "\r" || key === "\n") {
|
|
717
|
+
searching = false;
|
|
718
|
+
searchMatches = [];
|
|
719
|
+
hideCursor();
|
|
720
|
+
loadContent(flat[cursor]);
|
|
721
|
+
} else if (key === "" || key === "\b") {
|
|
722
|
+
searchQuery = searchQuery.slice(0, -1);
|
|
723
|
+
updateSearchMatches();
|
|
724
|
+
} else if (key === "\x1B[A") nextSearchMatch(-1);
|
|
725
|
+
else if (key === "\x1B[B") nextSearchMatch(1);
|
|
726
|
+
else if (key.length === 1 && key >= " ") {
|
|
727
|
+
searchQuery += key;
|
|
728
|
+
updateSearchMatches();
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
//#endregion
|
|
733
|
+
//#region src/cli/main.ts
|
|
734
|
+
async function main() {
|
|
735
|
+
process.stdout.on("error", (err) => {
|
|
736
|
+
if (err.code === "EPIPE") process.exit(0);
|
|
737
|
+
throw err;
|
|
738
|
+
});
|
|
739
|
+
const { values, positionals } = parseArgs({
|
|
740
|
+
args: process.argv.slice(2),
|
|
741
|
+
options: {
|
|
742
|
+
help: {
|
|
743
|
+
type: "boolean",
|
|
744
|
+
short: "h"
|
|
745
|
+
},
|
|
746
|
+
export: { type: "string" },
|
|
747
|
+
page: {
|
|
748
|
+
type: "string",
|
|
749
|
+
short: "p"
|
|
750
|
+
},
|
|
751
|
+
plain: {
|
|
752
|
+
type: "boolean",
|
|
753
|
+
default: isAgent || !process.stdout.isTTY
|
|
754
|
+
},
|
|
755
|
+
headless: { type: "boolean" }
|
|
756
|
+
},
|
|
757
|
+
allowPositionals: true
|
|
758
|
+
});
|
|
759
|
+
const exportDir = values.export;
|
|
760
|
+
const docsDir = positionals[0];
|
|
761
|
+
const plain = values.plain || values.headless || docsDir?.startsWith("npm:") || false;
|
|
762
|
+
if (values.help || !docsDir) return printUsage(!!docsDir);
|
|
763
|
+
const isURL = docsDir.startsWith("http://") || docsDir.startsWith("https://");
|
|
764
|
+
if (docsDir.endsWith(".md")) return singleFileMode(docsDir, plain, isURL);
|
|
765
|
+
const docs = new DocsManager(isURL ? new DocsSourceHTTP(docsDir) : docsDir.startsWith("gh:") ? new DocsSourceGit(docsDir) : docsDir.startsWith("npm:") ? new DocsSourceNpm(docsDir) : new DocsSourceFS(docsDir));
|
|
766
|
+
await docs.load();
|
|
767
|
+
if (exportDir) {
|
|
768
|
+
await new DocsExporterFS(exportDir).export(docs, { plainText: plain });
|
|
769
|
+
console.log(`Exported ${docs.pages.length} pages to ${exportDir}`);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
let pagePath = values.page;
|
|
773
|
+
if (!pagePath && isURL) {
|
|
774
|
+
const urlPath = new URL(docsDir).pathname.replace(/\/+$/, "");
|
|
775
|
+
if (urlPath && urlPath !== "/") pagePath = urlPath;
|
|
776
|
+
}
|
|
777
|
+
if (pagePath && !plain) return pageMode(docs, pagePath, plain);
|
|
778
|
+
if (plain) return plainMode(docs, pagePath);
|
|
779
|
+
return interactiveMode(docs);
|
|
780
|
+
}
|
|
781
|
+
main().catch((err) => {
|
|
782
|
+
showCursor();
|
|
783
|
+
leaveAltScreen();
|
|
784
|
+
console.error(err);
|
|
785
|
+
process.exit(1);
|
|
786
|
+
});
|
|
787
|
+
//#endregion
|
|
788
|
+
export {};
|