This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

niri / scripts / terminal-markdown.ts
6.9 kB 235 lines
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}