This repository has no description
1const c = {
2 reset: "\x1b[0m",
3 bold: "\x1b[1m",
4 dim: "\x1b[2m",
5 italic: "\x1b[3m",
6 green: "\x1b[32m",
7 cyan: "\x1b[36m",
8 gray: "\x1b[90m",
9 bYellow: "\x1b[93m",
10 bBlue: "\x1b[94m",
11 bMagenta: "\x1b[95m",
12 bCyan: "\x1b[96m",
13}
14
15const KW_JS = new Set([
16 "if", "else", "for", "while", "do", "switch", "case", "break", "continue", "return",
17 "function", "class", "const", "let", "var", "new", "delete", "typeof", "instanceof",
18 "import", "export", "default", "from", "async", "await", "try", "catch", "finally",
19 "throw", "null", "undefined", "true", "false", "void", "in", "of", "this", "super",
20 "extends", "implements", "interface", "type", "enum", "namespace", "declare", "abstract",
21 "public", "private", "protected", "static", "readonly", "override", "satisfies", "as",
22 "keyof", "infer", "never", "unknown", "any",
23])
24
25const KW_PY = new Set([
26 "if", "elif", "else", "for", "while", "in", "not", "and", "or", "is", "import", "from",
27 "as", "def", "class", "return", "yield", "pass", "break", "continue", "try", "except",
28 "finally", "raise", "with", "lambda", "True", "False", "None", "global", "nonlocal",
29 "del", "assert", "async", "await", "self", "super",
30])
31
32const KW_RUST = new Set([
33 "fn", "let", "mut", "const", "static", "pub", "use", "mod", "crate", "super", "self",
34 "struct", "enum", "impl", "trait", "type", "where", "for", "while", "if", "else", "match",
35 "return", "break", "continue", "loop", "move", "ref", "in", "as", "unsafe", "dyn",
36 "async", "await", "true", "false", "Some", "None", "Ok", "Err",
37])
38
39const KW_BASH = new Set([
40 "if", "then", "else", "elif", "fi", "for", "do", "done", "while", "until", "case", "in",
41 "esac", "function", "return", "local", "export", "echo", "exit", "source", "set", "unset",
42])
43
44const getKeywords = (lang: string): Set<string> => {
45 switch (lang) {
46 case "python":
47 case "py":
48 return KW_PY
49 case "rust":
50 case "rs":
51 return KW_RUST
52 case "bash":
53 case "sh":
54 case "shell":
55 case "zsh":
56 case "fish":
57 return KW_BASH
58 default:
59 return KW_JS
60 }
61}
62
63const highlightCode = (code: string, lang: string): string => {
64 const l = lang.toLowerCase()
65 const keywords = getKeywords(l)
66 const isPython = l === "python" || l === "py"
67 const isBash = ["bash", "sh", "shell", "zsh", "fish"].includes(l)
68 const isPlain = l === "" || l === "text" || l === "txt" || l === "plain"
69
70 if (isPlain) return code
71
72 let result = ""
73 let i = 0
74
75 while (i < code.length) {
76 const ch = code[i]!
77
78 if (!isPython && !isBash && ch === "/" && code[i + 1] === "/") {
79 const end = code.indexOf("\n", i)
80 const comment = end === -1 ? code.slice(i) : code.slice(i, end)
81 result += c.gray + c.italic + comment + c.reset
82 i += comment.length
83 continue
84 }
85
86 if (!isPython && !isBash && ch === "/" && code[i + 1] === "*") {
87 const end = code.indexOf("*/", i + 2)
88 const comment = end === -1 ? code.slice(i) : code.slice(i, end + 2)
89 result += c.gray + c.italic + comment + c.reset
90 i += comment.length
91 continue
92 }
93
94 if ((isPython || isBash) && ch === "#") {
95 const end = code.indexOf("\n", i)
96 const comment = end === -1 ? code.slice(i) : code.slice(i, end)
97 result += c.gray + c.italic + comment + c.reset
98 i += comment.length
99 continue
100 }
101
102 if (ch === '"' || ch === "'" || ch === "`") {
103 let j = i + 1
104 while (j < code.length) {
105 if (code[j] === "\\") {
106 j += 2
107 continue
108 }
109 if (code[j] === ch) {
110 j++
111 break
112 }
113 j++
114 }
115 result += c.green + code.slice(i, j) + c.reset
116 i = j
117 continue
118 }
119
120 if (/[0-9]/.test(ch) && (i === 0 || /\W/.test(code[i - 1]!))) {
121 let j = i
122 while (j < code.length && /[0-9._xXa-fA-FobBnNlLeE+\-]/.test(code[j]!)) j++
123 result += c.bYellow + code.slice(i, j) + c.reset
124 i = j
125 continue
126 }
127
128 if (/[a-zA-Z_$]/.test(ch)) {
129 let j = i
130 while (j < code.length && /[a-zA-Z0-9_$]/.test(code[j]!)) j++
131 const word = code.slice(i, j)
132
133 if (keywords.has(word)) {
134 result += c.bMagenta + word + c.reset
135 } else if (/^[A-Z]/.test(word)) {
136 result += c.bYellow + word + c.reset
137 } else if (code[j] === "(") {
138 result += c.bBlue + word + c.reset
139 } else {
140 result += word
141 }
142 i = j
143 continue
144 }
145
146 result += ch
147 i++
148 }
149
150 return result
151}
152
153const renderInline = (text: string): string =>
154 text
155 .replace(/\*\*\*(.*?)\*\*\*/g, `${c.bold}${c.italic}$1${c.reset}`)
156 .replace(/\*\*(.*?)\*\*/g, `${c.bold}$1${c.reset}`)
157 .replace(/\*(.*?)\*/g, `${c.italic}$1${c.reset}`)
158 .replace(/~~(.*?)~~/g, `${c.dim}$1${c.reset}`)
159 .replace(/`([^`]+)`/g, `${c.cyan}$1${c.reset}`)
160
161export const renderMarkdownAnsi = (text: string): string => {
162 const lines = text.split("\n")
163 const out: string[] = []
164 const width = Math.min(process.stdout.columns || 80, 120)
165 let i = 0
166
167 while (i < lines.length) {
168 const line = lines[i]!
169
170 if (line.startsWith("```")) {
171 const lang = line.slice(3).trim()
172 const codeLines: string[] = []
173 i++
174 while (i < lines.length && !lines[i]!.startsWith("```")) {
175 codeLines.push(lines[i]!)
176 i++
177 }
178 i++
179
180 const code = codeLines.join("\n")
181 const highlighted = highlightCode(code, lang)
182
183 if (lang) out.push(`${c.dim}${c.cyan}┌─ ${lang}${c.reset}`)
184 for (const highlightedLine of highlighted.split("\n")) {
185 out.push(`${c.dim}│${c.reset} ${highlightedLine}`)
186 }
187 out.push(`${c.dim}└${c.reset}`)
188 continue
189 }
190
191 const hMatch = line.match(/^(#{1,6})\s+(.*)/)
192 if (hMatch) {
193 const level = hMatch[1]!.length
194 const title = renderInline(hMatch[2]!)
195 const colors = [c.bCyan, c.cyan, c.bBlue, c.bBlue, c.bMagenta, c.bMagenta]
196 const color = colors[Math.min(level - 1, colors.length - 1)]!
197 out.push(c.bold + color + title + c.reset)
198 i++
199 continue
200 }
201
202 if (/^(\*\*\*|---|___)$/.test(line.trim())) {
203 out.push(c.dim + "─".repeat(width) + c.reset)
204 i++
205 continue
206 }
207
208 const ulMatch = line.match(/^(\s*)[*\-+]\s+(.*)/)
209 if (ulMatch) {
210 const indent = ulMatch[1]!
211 out.push(`${indent}${c.dim}•${c.reset} ${renderInline(ulMatch[2]!)}`)
212 i++
213 continue
214 }
215
216 const olMatch = line.match(/^(\s*)(\d+)\.\s+(.*)/)
217 if (olMatch) {
218 const indent = olMatch[1]!
219 out.push(`${indent}${c.dim}${olMatch[2]}.${c.reset} ${renderInline(olMatch[3]!)}`)
220 i++
221 continue
222 }
223
224 if (line.startsWith("> ")) {
225 out.push(`${c.dim}│${c.reset} ${c.italic}${renderInline(line.slice(2))}${c.reset}`)
226 i++
227 continue
228 }
229
230 out.push(renderInline(line))
231 i++
232 }
233
234 return out.join("\n")
235}