Merge branch 'markdown-links'

This commit is contained in:
Chris Wanstrath 2026-02-20 15:22:13 -08:00
commit 59c90fa812

View File

@ -1,23 +1,48 @@
export function renderMarkdown(text: string): string { export function renderMarkdown(text: string): string {
// Strip code fences (``` and ```language) — we're already in a terminal // Extract fenced code blocks before anything else
let result = text.replace(/^```\w*\n?/gm, "") const codeBlocks: string[] = []
let result = text.replace(/^```\w*\n([\s\S]*?)^```\s*$/gm, (_, content) => {
codeBlocks.push(content)
return `\x00CODEBLOCK${codeBlocks.length - 1}\x00`
})
// Extract code spans first so their contents aren't processed as bold/italic // Extract backslash escapes so they aren't processed as markdown
const escapes: string[] = []
result = result.replace(/\\([\\`*_~\[\]()#>!-])/g, (_, ch) => {
escapes.push(ch)
return `\x00ESC${escapes.length - 1}\x00`
})
// Extract code spans so their contents aren't processed as bold/italic
const codeSpans: string[] = [] const codeSpans: string[] = []
result = result.replace(/`([^`]+)`/g, (_, code) => { result = result.replace(/`([^`]+)`/g, (_, code) => {
codeSpans.push(code) codeSpans.push(code)
return `\x00CODE${codeSpans.length - 1}\x00` return `\x00CODE${codeSpans.length - 1}\x00`
}) })
// Links: [text](url) → OSC 8 terminal hyperlink, underlined blue
// Must run before H1/bold/italic so ANSI `[` chars don't confuse the link regex
result = result.replace(/(?<!!)\[([^\]]+)\]\(([^)]+)\)/g, (_, text, href) => {
const url = href.replace(/\s+"[^"]*"$/, "") // strip optional title
return `\x1b]8;;${url}\x07\x1b[4;38;5;75m${text}\x1b[24;39m\x1b]8;;\x07`
})
// H1: # Header → bold+italic+underline // H1: # Header → bold+italic+underline
result = result.replace(/^# (.+)$/gm, "\x1b[1;3;4m$1\x1b[22;23;24m") result = result.replace(/^# (.+)$/gm, "\x1b[1;3;4m$1\x1b[22;23;24m")
// H2/H3: ## Header / ### Header → bold // H2/H3: ## Header / ### Header → bold
result = result.replace(/^#{2,3} (.+)$/gm, "\x1b[1m$1\x1b[22m") result = result.replace(/^#{2,6} (.+)$/gm, "\x1b[1m$1\x1b[22m")
// Blockquotes: > text → dim+italic // Blockquotes: > text → dim+italic
result = result.replace(/^> (.+)$/gm, "\x1b[2;3m$1\x1b[22;23m") result = result.replace(/^> (.+)$/gm, "\x1b[2;3m$1\x1b[22;23m")
// Bare blockquote lines: > with no text
result = result.replace(/^>\s*$/gm, "")
// Task lists: - [x] → ✓ green, - [ ] → ○ dim
result = result.replace(/^(\s*)[-*] \[x\] (.+)$/gm, "$1\x1b[32m✓\x1b[39m $2")
result = result.replace(/^(\s*)[-*] \[ \] (.+)$/gm, "$1\x1b[2m☐\x1b[22m $2")
// Bold: **text** // Bold: **text**
result = result.replace(/\*\*(.+?)\*\*/g, "\x1b[1m$1\x1b[22m") result = result.replace(/\*\*(.+?)\*\*/g, "\x1b[1m$1\x1b[22m")
@ -29,6 +54,12 @@ export function renderMarkdown(text: string): string {
return `\x1b[38;5;147m${codeSpans[parseInt(i)]}\x1b[39m` return `\x1b[38;5;147m${codeSpans[parseInt(i)]}\x1b[39m`
}) })
// Restore backslash escapes as literal characters
result = result.replace(/\x00ESC(\d+)\x00/g, (_, i) => escapes[parseInt(i)])
// Restore fenced code blocks as plain text
result = result.replace(/\x00CODEBLOCK(\d+)\x00/g, (_, i) => codeBlocks[parseInt(i)])
// Breathe: add blank line before list starts when preceded by non-empty text // Breathe: add blank line before list starts when preceded by non-empty text
const lines = result.split("\n") const lines = result.split("\n")
for (let i = lines.length - 1; i > 0; i--) { for (let i = lines.length - 1; i > 0; i--) {