github-search-cli 9.2.1 → 9.3.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/README.md +5 -5
- package/dist/cli.js +189 -17
- package/package.json +1 -1
- package/src/compare.ts +4 -3
- package/src/markdown-render.ts +216 -0
- package/src/tui.ts +5 -12
package/README.md
CHANGED
|
@@ -203,11 +203,11 @@ npm run build # build dist/
|
|
|
203
203
|
```
|
|
204
204
|
|
|
205
205
|
---
|
|
206
|
-
###
|
|
207
|
-
- Fixed
|
|
208
|
-
-
|
|
209
|
-
-
|
|
210
|
-
-
|
|
206
|
+
### v0.9.3
|
|
207
|
+
- Fixed `c` keybinding in TUI not adding repos to comparison — missing closing brace in space-key handler broke all subsequent key handlers (`c`, `t`, `?`)
|
|
208
|
+
- Widened compare table columns (min 35 chars) and added Description + URL rows for richer side-by-side comparison
|
|
209
|
+
- Compare table now shows up to 5 topics per repo (was 3)
|
|
210
|
+
- README viewer now renders full markdown (headings, code blocks, lists, tables, blockquotes, rules) instead of stripping formatting
|
|
211
211
|
---
|
|
212
212
|
### v0.9.2
|
|
213
213
|
|
package/dist/cli.js
CHANGED
|
@@ -1144,9 +1144,10 @@ function buildComparisonTable(repos) {
|
|
|
1144
1144
|
rows.push({ label: "Language", values: repos.map((r) => r.language ?? "\u2014") });
|
|
1145
1145
|
rows.push({ label: "Created", values: repos.map((r) => r.createdAt?.slice(0, 7) ?? "\u2014") });
|
|
1146
1146
|
rows.push({ label: "Updated", values: repos.map((r) => r.updatedAt?.slice(0, 10) ?? "\u2014") });
|
|
1147
|
-
rows.push({ label: "Topics", values: repos.map((r) => (r.topics?.slice(0,
|
|
1148
|
-
|
|
1149
|
-
|
|
1147
|
+
rows.push({ label: "Topics", values: repos.map((r) => (r.topics?.slice(0, 5).join(", ") ?? "\u2014") || "\u2014") });
|
|
1148
|
+
rows.push({ label: "Description", values: repos.map((r) => r.description?.slice(0, 50) ?? "\u2014") });
|
|
1149
|
+
rows.push({ label: "URL", values: repos.map((r) => r.url || "\u2014") });
|
|
1150
|
+
const colW = Math.max(35, ...names.map((n) => n.length));
|
|
1150
1151
|
const header = ["".padEnd(14), ...names.map((n) => n.padEnd(colW))];
|
|
1151
1152
|
const sep = ["".padEnd(14, "\u2500"), ...names.map(() => "".padEnd(colW, "\u2500"))];
|
|
1152
1153
|
const lines = [
|
|
@@ -1575,6 +1576,177 @@ function buildDeepDiveText(data) {
|
|
|
1575
1576
|
].join("\n");
|
|
1576
1577
|
}
|
|
1577
1578
|
|
|
1579
|
+
// src/markdown-render.ts
|
|
1580
|
+
function renderMarkdown(md, opts = {}) {
|
|
1581
|
+
const width = opts.width ?? 80;
|
|
1582
|
+
const lines = md.split("\n");
|
|
1583
|
+
const out = [];
|
|
1584
|
+
let inCodeBlock = false;
|
|
1585
|
+
let codeLang = "";
|
|
1586
|
+
let codeLines = [];
|
|
1587
|
+
let inTable = false;
|
|
1588
|
+
let tableRows = [];
|
|
1589
|
+
function flushCode() {
|
|
1590
|
+
if (codeLines.length === 0) return;
|
|
1591
|
+
const maxLen = Math.max(...codeLines.map((l) => l.length), codeLang.length);
|
|
1592
|
+
const barW = Math.min(maxLen + 4, width - 2);
|
|
1593
|
+
out.push("\u250C" + "\u2500".repeat(barW) + "\u2510");
|
|
1594
|
+
if (codeLang) out.push("\u2502 " + codeLang.padEnd(barW - 1) + "\u2502");
|
|
1595
|
+
for (const cl of codeLines) {
|
|
1596
|
+
out.push("\u2502 " + cl.padEnd(barW - 1) + "\u2502");
|
|
1597
|
+
}
|
|
1598
|
+
out.push("\u2514" + "\u2500".repeat(barW) + "\u2518");
|
|
1599
|
+
out.push("");
|
|
1600
|
+
codeLines = [];
|
|
1601
|
+
codeLang = "";
|
|
1602
|
+
}
|
|
1603
|
+
function flushTable() {
|
|
1604
|
+
if (tableRows.length === 0) return;
|
|
1605
|
+
const dataRows = tableRows.filter(
|
|
1606
|
+
(r) => !r.every((c) => /^[\s\-:|]+$/.test(c))
|
|
1607
|
+
);
|
|
1608
|
+
if (dataRows.length === 0) {
|
|
1609
|
+
tableRows = [];
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
const colCount = Math.max(...dataRows.map((r) => r.length));
|
|
1613
|
+
const colW = [];
|
|
1614
|
+
for (let ci = 0; ci < colCount; ci++) {
|
|
1615
|
+
colW.push(
|
|
1616
|
+
Math.max(
|
|
1617
|
+
3,
|
|
1618
|
+
...dataRows.map((r) => (r[ci] ?? "").length)
|
|
1619
|
+
)
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
const totalW = colW.reduce((a, b) => a + b, 0) + colCount * 3 - 1;
|
|
1623
|
+
if (totalW > width) {
|
|
1624
|
+
for (const row of dataRows) {
|
|
1625
|
+
out.push(" " + row.map((c) => c.trim()).join(" \u2502 "));
|
|
1626
|
+
}
|
|
1627
|
+
} else {
|
|
1628
|
+
const sep = "\u250C" + colW.map((w) => "\u2500".repeat(w + 2)).join("\u252C") + "\u2510";
|
|
1629
|
+
const mid = "\u251C" + colW.map((w) => "\u2500".repeat(w + 2)).join("\u253C") + "\u2524";
|
|
1630
|
+
const end = "\u2514" + colW.map((w) => "\u2500".repeat(w + 2)).join("\u2534") + "\u2518";
|
|
1631
|
+
out.push(sep);
|
|
1632
|
+
dataRows.forEach((row, i) => {
|
|
1633
|
+
const cells = colW.map((w, ci) => (row[ci] ?? "").trim().padEnd(w));
|
|
1634
|
+
out.push("\u2502 " + cells.map((c) => ` ${c} `).join("\u2502") + "\u2502");
|
|
1635
|
+
if (i === 0) out.push(mid);
|
|
1636
|
+
});
|
|
1637
|
+
out.push(end);
|
|
1638
|
+
}
|
|
1639
|
+
out.push("");
|
|
1640
|
+
tableRows = [];
|
|
1641
|
+
}
|
|
1642
|
+
function renderInline(line) {
|
|
1643
|
+
line = line.replace(/\*\*(.+?)\*\*/g, "$1");
|
|
1644
|
+
line = line.replace(/\*([^*]+?)\*/g, "$1");
|
|
1645
|
+
line = line.replace(/`([^`]+)`/g, "\u27E8$1\u27E9");
|
|
1646
|
+
line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)");
|
|
1647
|
+
line = line.replace(/!\[([^\]]*)\]\([^)]+\)/g, "\u{1F5BC} $1");
|
|
1648
|
+
return line;
|
|
1649
|
+
}
|
|
1650
|
+
function wrapLine(line, maxW) {
|
|
1651
|
+
if (line.length <= maxW) return [line];
|
|
1652
|
+
const parts = [];
|
|
1653
|
+
let remaining = line;
|
|
1654
|
+
while (remaining.length > maxW) {
|
|
1655
|
+
let breakAt = maxW;
|
|
1656
|
+
const spaceIdx = remaining.lastIndexOf(" ", maxW);
|
|
1657
|
+
if (spaceIdx > maxW / 2) breakAt = spaceIdx;
|
|
1658
|
+
parts.push(remaining.slice(0, breakAt));
|
|
1659
|
+
remaining = remaining.slice(breakAt).replace(/^ /, "");
|
|
1660
|
+
}
|
|
1661
|
+
if (remaining) parts.push(remaining);
|
|
1662
|
+
return parts;
|
|
1663
|
+
}
|
|
1664
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1665
|
+
let line = lines[i];
|
|
1666
|
+
if (/^```/.test(line)) {
|
|
1667
|
+
if (inCodeBlock) {
|
|
1668
|
+
flushCode();
|
|
1669
|
+
inCodeBlock = false;
|
|
1670
|
+
} else {
|
|
1671
|
+
inCodeBlock = true;
|
|
1672
|
+
codeLang = line.slice(3).trim();
|
|
1673
|
+
}
|
|
1674
|
+
continue;
|
|
1675
|
+
}
|
|
1676
|
+
if (inCodeBlock) {
|
|
1677
|
+
codeLines.push(line);
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1680
|
+
if (line.includes("|")) {
|
|
1681
|
+
const cells = line.split("|").map((c) => c.trim()).filter((_, idx, arr) => idx > 0 && idx < arr.length - 1);
|
|
1682
|
+
if (cells.length >= 2) {
|
|
1683
|
+
inTable = true;
|
|
1684
|
+
tableRows.push(cells);
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
} else if (inTable) {
|
|
1688
|
+
flushTable();
|
|
1689
|
+
inTable = false;
|
|
1690
|
+
}
|
|
1691
|
+
if (/^(\s*[-*_]){3,}\s*$/.test(line)) {
|
|
1692
|
+
out.push("\u2500".repeat(Math.min(width, 40)));
|
|
1693
|
+
out.push("");
|
|
1694
|
+
continue;
|
|
1695
|
+
}
|
|
1696
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
|
|
1697
|
+
if (headingMatch) {
|
|
1698
|
+
const level = headingMatch[1].length;
|
|
1699
|
+
const text = renderInline(headingMatch[2]);
|
|
1700
|
+
const prefix = "\u2588 ".repeat(level);
|
|
1701
|
+
out.push("");
|
|
1702
|
+
out.push(prefix + text);
|
|
1703
|
+
out.push("");
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
const bqMatch = line.match(/^>\s?(.*)/);
|
|
1707
|
+
if (bqMatch) {
|
|
1708
|
+
const inner = renderInline(bqMatch[1]);
|
|
1709
|
+
for (const wl of wrapLine(inner, width - 4)) {
|
|
1710
|
+
out.push("\u2502 " + wl);
|
|
1711
|
+
}
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1714
|
+
const ulMatch = line.match(/^(\s*)[-*+]\s+(.*)/);
|
|
1715
|
+
if (ulMatch) {
|
|
1716
|
+
const indent = ulMatch[1].length;
|
|
1717
|
+
const depth = Math.floor(indent / 2);
|
|
1718
|
+
const prefix = " ".repeat(depth) + "\u2022 ";
|
|
1719
|
+
const inner = renderInline(ulMatch[2]);
|
|
1720
|
+
for (const wl of wrapLine(inner, width - prefix.length)) {
|
|
1721
|
+
out.push(prefix + wl);
|
|
1722
|
+
}
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
const olMatch = line.match(/^(\s*)\d+\.\s+(.*)/);
|
|
1726
|
+
if (olMatch) {
|
|
1727
|
+
const indent = olMatch[1].length;
|
|
1728
|
+
const depth = Math.floor(indent / 2);
|
|
1729
|
+
const prefix = " ".repeat(depth) + "\u25E6 ";
|
|
1730
|
+
const inner = renderInline(olMatch[2]);
|
|
1731
|
+
for (const wl of wrapLine(inner, width - prefix.length)) {
|
|
1732
|
+
out.push(prefix + wl);
|
|
1733
|
+
}
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1736
|
+
if (line.trim() === "") {
|
|
1737
|
+
out.push("");
|
|
1738
|
+
continue;
|
|
1739
|
+
}
|
|
1740
|
+
const rendered = renderInline(line);
|
|
1741
|
+
for (const wl of wrapLine(rendered, width)) {
|
|
1742
|
+
out.push(wl);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
if (inCodeBlock) flushCode();
|
|
1746
|
+
if (inTable) flushTable();
|
|
1747
|
+
return out.join("\n");
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1578
1750
|
// src/explore.ts
|
|
1579
1751
|
async function fetchTopics() {
|
|
1580
1752
|
const res = await fetch("https://api.github.com/search/topics?q=popular&per_page=30", {
|
|
@@ -3265,8 +3437,8 @@ async function launchBrowser(theme_override) {
|
|
|
3265
3437
|
if (!text) {
|
|
3266
3438
|
readmeText.content = ` (no README found for ${repo.fullName})`;
|
|
3267
3439
|
} else {
|
|
3268
|
-
const
|
|
3269
|
-
readmeText.content =
|
|
3440
|
+
const termW = Math.max(40, (process.stdout.columns || 80) - 4);
|
|
3441
|
+
readmeText.content = renderMarkdown(text, { width: termW });
|
|
3270
3442
|
}
|
|
3271
3443
|
} catch {
|
|
3272
3444
|
readmeText.content = " Failed to load README";
|
|
@@ -4050,18 +4222,18 @@ ${pack.description ?? ""}`;
|
|
|
4050
4222
|
showLeaderMenu();
|
|
4051
4223
|
}
|
|
4052
4224
|
return;
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4225
|
+
}
|
|
4226
|
+
if (key.name === "t" && !searchInput.focused) {
|
|
4227
|
+
const current = config.theme;
|
|
4228
|
+
const themes = listThemes();
|
|
4229
|
+
const idx = themes.indexOf(current);
|
|
4230
|
+
const next = themes[(idx + 1) % themes.length];
|
|
4231
|
+
const nextTheme = loadTheme(next);
|
|
4232
|
+
Object.assign(colors, nextTheme);
|
|
4233
|
+
config.theme = next;
|
|
4234
|
+
saveConfig(config);
|
|
4235
|
+
renderer.requestRender();
|
|
4236
|
+
return;
|
|
4065
4237
|
}
|
|
4066
4238
|
if (key.name === "c" && !searchInput.focused) {
|
|
4067
4239
|
if (currentOverlay === "compare") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-search-cli",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.3.0",
|
|
4
4
|
"description": "Terminal-native GitHub search — search, trending, bookmarks, history, deep-dive, compare, export, and more. All keyboard-driven, no browser needed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "bun@1",
|
package/src/compare.ts
CHANGED
|
@@ -20,11 +20,12 @@ export function buildComparisonTable(repos: Repo[]): string {
|
|
|
20
20
|
rows.push({ label: "Language", values: repos.map((r) => r.language ?? "—") });
|
|
21
21
|
rows.push({ label: "Created", values: repos.map((r) => r.createdAt?.slice(0, 7) ?? "—") });
|
|
22
22
|
rows.push({ label: "Updated", values: repos.map((r) => r.updatedAt?.slice(0, 10) ?? "—") });
|
|
23
|
-
rows.push({ label: "Topics", values: repos.map((r) => (r.topics?.slice(0,
|
|
23
|
+
rows.push({ label: "Topics", values: repos.map((r) => (r.topics?.slice(0, 5).join(", ") ?? "—") || "—") });
|
|
24
|
+
rows.push({ label: "Description", values: repos.map((r) => (r.description?.slice(0, 50) ?? "—")) });
|
|
25
|
+
rows.push({ label: "URL", values: repos.map((r) => r.url || "—") });
|
|
24
26
|
|
|
25
27
|
// Calculate the max width for each column
|
|
26
|
-
const colW = Math.max(
|
|
27
|
-
const valW = Math.max(8, ...rows.flatMap((r) => r.values.map((v) => v.length)));
|
|
28
|
+
const colW = Math.max(35, ...names.map((n) => n.length));
|
|
28
29
|
|
|
29
30
|
// Header row
|
|
30
31
|
const header = ["".padEnd(14), ...names.map((n) => n.padEnd(colW))];
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal markdown-to-terminal-text renderer.
|
|
3
|
+
*
|
|
4
|
+
* Renders headings, code blocks, lists, tables, blockquotes, horizontal rules,
|
|
5
|
+
* bold/italic, and links into a plain-text representation suitable for a
|
|
6
|
+
* TextRenderable in the TUI.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface RenderOptions {
|
|
10
|
+
/** Terminal width for wrapping and table layout. Default: 80. */
|
|
11
|
+
width?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Render markdown text to terminal-friendly plain text. */
|
|
15
|
+
export function renderMarkdown(md: string, opts: RenderOptions = {}): string {
|
|
16
|
+
const width = opts.width ?? 80;
|
|
17
|
+
const lines = md.split("\n");
|
|
18
|
+
const out: string[] = [];
|
|
19
|
+
let inCodeBlock = false;
|
|
20
|
+
let codeLang = "";
|
|
21
|
+
let codeLines: string[] = [];
|
|
22
|
+
let inTable = false;
|
|
23
|
+
let tableRows: string[][] = [];
|
|
24
|
+
|
|
25
|
+
function flushCode() {
|
|
26
|
+
if (codeLines.length === 0) return;
|
|
27
|
+
const maxLen = Math.max(...codeLines.map((l) => l.length), codeLang.length);
|
|
28
|
+
const barW = Math.min(maxLen + 4, width - 2);
|
|
29
|
+
out.push("┌" + "─".repeat(barW) + "┐");
|
|
30
|
+
if (codeLang) out.push("│ " + codeLang.padEnd(barW - 1) + "│");
|
|
31
|
+
for (const cl of codeLines) {
|
|
32
|
+
out.push("│ " + cl.padEnd(barW - 1) + "│");
|
|
33
|
+
}
|
|
34
|
+
out.push("└" + "─".repeat(barW) + "┘");
|
|
35
|
+
out.push("");
|
|
36
|
+
codeLines = [];
|
|
37
|
+
codeLang = "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function flushTable() {
|
|
41
|
+
if (tableRows.length === 0) return;
|
|
42
|
+
const dataRows = tableRows.filter(
|
|
43
|
+
(r) => !r.every((c) => /^[\s\-:|]+$/.test(c)),
|
|
44
|
+
);
|
|
45
|
+
if (dataRows.length === 0) {
|
|
46
|
+
tableRows = [];
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const colCount = Math.max(...dataRows.map((r) => r.length));
|
|
50
|
+
const colW: number[] = [];
|
|
51
|
+
for (let ci = 0; ci < colCount; ci++) {
|
|
52
|
+
colW.push(
|
|
53
|
+
Math.max(
|
|
54
|
+
3,
|
|
55
|
+
...dataRows.map((r) => (r[ci] ?? "").length),
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const totalW = colW.reduce((a, b) => a + b, 0) + colCount * 3 - 1;
|
|
60
|
+
if (totalW > width) {
|
|
61
|
+
for (const row of dataRows) {
|
|
62
|
+
out.push(" " + row.map((c) => c.trim()).join(" │ "));
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
const sep = "┌" + colW.map((w) => "─".repeat(w + 2)).join("┬") + "┐";
|
|
66
|
+
const mid = "├" + colW.map((w) => "─".repeat(w + 2)).join("┼") + "┤";
|
|
67
|
+
const end = "└" + colW.map((w) => "─".repeat(w + 2)).join("┴") + "┘";
|
|
68
|
+
out.push(sep);
|
|
69
|
+
dataRows.forEach((row, i) => {
|
|
70
|
+
const cells = colW.map((w, ci) => (row[ci] ?? "").trim().padEnd(w));
|
|
71
|
+
out.push("│ " + cells.map((c) => ` ${c} `).join("│") + "│");
|
|
72
|
+
if (i === 0) out.push(mid);
|
|
73
|
+
});
|
|
74
|
+
out.push(end);
|
|
75
|
+
}
|
|
76
|
+
out.push("");
|
|
77
|
+
tableRows = [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function renderInline(line: string): string {
|
|
81
|
+
// Bold
|
|
82
|
+
line = line.replace(/\*\*(.+?)\*\*/g, "$1");
|
|
83
|
+
// Italic (no asterisks)
|
|
84
|
+
line = line.replace(/\*([^*]+?)\*/g, "$1");
|
|
85
|
+
// Inline code
|
|
86
|
+
line = line.replace(/`([^`]+)`/g, "⟨$1⟩");
|
|
87
|
+
// Links [text](url)
|
|
88
|
+
line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)");
|
|
89
|
+
// Images
|
|
90
|
+
line = line.replace(/!\[([^\]]*)\]\([^)]+\)/g, "🖼 $1");
|
|
91
|
+
return line;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function wrapLine(line: string, maxW: number): string[] {
|
|
95
|
+
if (line.length <= maxW) return [line];
|
|
96
|
+
const parts: string[] = [];
|
|
97
|
+
let remaining = line;
|
|
98
|
+
while (remaining.length > maxW) {
|
|
99
|
+
let breakAt = maxW;
|
|
100
|
+
const spaceIdx = remaining.lastIndexOf(" ", maxW);
|
|
101
|
+
if (spaceIdx > maxW / 2) breakAt = spaceIdx;
|
|
102
|
+
parts.push(remaining.slice(0, breakAt));
|
|
103
|
+
remaining = remaining.slice(breakAt).replace(/^ /, "");
|
|
104
|
+
}
|
|
105
|
+
if (remaining) parts.push(remaining);
|
|
106
|
+
return parts;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < lines.length; i++) {
|
|
110
|
+
let line = lines[i];
|
|
111
|
+
|
|
112
|
+
// Code block boundaries
|
|
113
|
+
if (/^```/.test(line)) {
|
|
114
|
+
if (inCodeBlock) {
|
|
115
|
+
flushCode();
|
|
116
|
+
inCodeBlock = false;
|
|
117
|
+
} else {
|
|
118
|
+
inCodeBlock = true;
|
|
119
|
+
codeLang = line.slice(3).trim();
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (inCodeBlock) {
|
|
124
|
+
codeLines.push(line);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Table row
|
|
129
|
+
if (line.includes("|")) {
|
|
130
|
+
const cells = line
|
|
131
|
+
.split("|")
|
|
132
|
+
.map((c) => c.trim())
|
|
133
|
+
.filter((_, idx, arr) => idx > 0 && idx < arr.length - 1);
|
|
134
|
+
if (cells.length >= 2) {
|
|
135
|
+
inTable = true;
|
|
136
|
+
tableRows.push(cells);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
} else if (inTable) {
|
|
140
|
+
flushTable();
|
|
141
|
+
inTable = false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Horizontal rule
|
|
145
|
+
if (/^(\s*[-*_]){3,}\s*$/.test(line)) {
|
|
146
|
+
out.push("─".repeat(Math.min(width, 40)));
|
|
147
|
+
out.push("");
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Headings
|
|
152
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
|
|
153
|
+
if (headingMatch) {
|
|
154
|
+
const level = headingMatch[1].length;
|
|
155
|
+
const text = renderInline(headingMatch[2]);
|
|
156
|
+
const prefix = "█ ".repeat(level);
|
|
157
|
+
out.push("");
|
|
158
|
+
out.push(prefix + text);
|
|
159
|
+
out.push("");
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Blockquote
|
|
164
|
+
const bqMatch = line.match(/^>\s?(.*)/);
|
|
165
|
+
if (bqMatch) {
|
|
166
|
+
const inner = renderInline(bqMatch[1]);
|
|
167
|
+
for (const wl of wrapLine(inner, width - 4)) {
|
|
168
|
+
out.push("│ " + wl);
|
|
169
|
+
}
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Unordered list
|
|
174
|
+
const ulMatch = line.match(/^(\s*)[-*+]\s+(.*)/);
|
|
175
|
+
if (ulMatch) {
|
|
176
|
+
const indent = ulMatch[1].length;
|
|
177
|
+
const depth = Math.floor(indent / 2);
|
|
178
|
+
const prefix = " ".repeat(depth) + "• ";
|
|
179
|
+
const inner = renderInline(ulMatch[2]);
|
|
180
|
+
for (const wl of wrapLine(inner, width - prefix.length)) {
|
|
181
|
+
out.push(prefix + wl);
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Ordered list
|
|
187
|
+
const olMatch = line.match(/^(\s*)\d+\.\s+(.*)/);
|
|
188
|
+
if (olMatch) {
|
|
189
|
+
const indent = olMatch[1].length;
|
|
190
|
+
const depth = Math.floor(indent / 2);
|
|
191
|
+
const prefix = " ".repeat(depth) + "◦ ";
|
|
192
|
+
const inner = renderInline(olMatch[2]);
|
|
193
|
+
for (const wl of wrapLine(inner, width - prefix.length)) {
|
|
194
|
+
out.push(prefix + wl);
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Empty line
|
|
200
|
+
if (line.trim() === "") {
|
|
201
|
+
out.push("");
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Paragraph
|
|
206
|
+
const rendered = renderInline(line);
|
|
207
|
+
for (const wl of wrapLine(rendered, width)) {
|
|
208
|
+
out.push(wl);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (inCodeBlock) flushCode();
|
|
213
|
+
if (inTable) flushTable();
|
|
214
|
+
|
|
215
|
+
return out.join("\n");
|
|
216
|
+
}
|
package/src/tui.ts
CHANGED
|
@@ -79,6 +79,7 @@ import {
|
|
|
79
79
|
import { saveSession, restoreSession } from "./session";
|
|
80
80
|
import { fetchDeepDive, buildDeepDiveText } from "./deepdive";
|
|
81
81
|
import { buildComparisonTable } from "./compare";
|
|
82
|
+
import { renderMarkdown } from "./markdown-render";
|
|
82
83
|
import { fetchTopics, type TopicItem } from "./explore";
|
|
83
84
|
import { exportToFile, type ExportFormat } from "./output";
|
|
84
85
|
import { listThemes, loadTheme } from "./themes";
|
|
@@ -1709,17 +1710,8 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1709
1710
|
if (!text) {
|
|
1710
1711
|
readmeText.content = ` (no README found for ${repo.fullName})`;
|
|
1711
1712
|
} else {
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
.replace(/```[\s\S]*?```/g, "[code block]")
|
|
1715
|
-
.replace(/#{1,6}\s/g, "")
|
|
1716
|
-
.replace(/\*\*(.+?)\*\*/g, "$1")
|
|
1717
|
-
.replace(/\[(.+?)\]\(.+?\)/g, "$1")
|
|
1718
|
-
.replace(/`([^`]+)`/g, "$1")
|
|
1719
|
-
.split("\n")
|
|
1720
|
-
.map((l) => l.trimEnd())
|
|
1721
|
-
.join("\n");
|
|
1722
|
-
readmeText.content = stripped;
|
|
1713
|
+
const termW = Math.max(40, (process.stdout.columns || 80) - 4);
|
|
1714
|
+
readmeText.content = renderMarkdown(text, { width: termW });
|
|
1723
1715
|
}
|
|
1724
1716
|
} catch {
|
|
1725
1717
|
readmeText.content = " Failed to load README";
|
|
@@ -2646,6 +2638,8 @@ ${pack.description ?? ""}`;
|
|
|
2646
2638
|
showLeaderMenu();
|
|
2647
2639
|
}
|
|
2648
2640
|
return;
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2649
2643
|
// 't' toggles theme and persists to config
|
|
2650
2644
|
if (key.name === "t" && !searchInput.focused) {
|
|
2651
2645
|
const current = config.theme;
|
|
@@ -2659,7 +2653,6 @@ ${pack.description ?? ""}`;
|
|
|
2659
2653
|
renderer.requestRender();
|
|
2660
2654
|
return;
|
|
2661
2655
|
}
|
|
2662
|
-
}
|
|
2663
2656
|
|
|
2664
2657
|
// 'c' toggles compare on selected repo (when not typing in search)
|
|
2665
2658
|
if (key.name === "c" && !searchInput.focused) {
|