Add markdown table rendering with box-drawing characters and alignment support
This commit is contained in:
parent
1022a75722
commit
1fc053ce70
|
|
@ -1,3 +1,74 @@
|
||||||
|
function stripAnsi(s: string): string {
|
||||||
|
return s.replace(/\x1b\]8;;[^\x07]*\x07/g, "").replace(/\x1b\[[0-9;]*m/g, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(block: string): string {
|
||||||
|
const lines = block.trim().split("\n")
|
||||||
|
if (lines.length < 2) return block
|
||||||
|
|
||||||
|
const parseRow = (line: string): string[] =>
|
||||||
|
line.replace(/^\|/, "").replace(/\|$/, "").split("|").map(c => c.trim())
|
||||||
|
|
||||||
|
const header = parseRow(lines[0])
|
||||||
|
const sepCells = parseRow(lines[1])
|
||||||
|
|
||||||
|
// Validate separator row
|
||||||
|
if (!sepCells.every(s => /^:?-+:?$/.test(s.trim()))) return block
|
||||||
|
|
||||||
|
const cols = header.length
|
||||||
|
const align: ("left" | "center" | "right")[] = sepCells.map(s => {
|
||||||
|
const t = s.trim()
|
||||||
|
if (t.startsWith(":") && t.endsWith(":")) return "center"
|
||||||
|
if (t.endsWith(":")) return "right"
|
||||||
|
return "left"
|
||||||
|
})
|
||||||
|
|
||||||
|
const rows = lines.slice(2).map(parseRow)
|
||||||
|
|
||||||
|
// Column widths based on visible text (no ANSI codes)
|
||||||
|
const widths = new Array(cols).fill(0)
|
||||||
|
for (let c = 0; c < cols; c++) {
|
||||||
|
widths[c] = Math.max(widths[c], stripAnsi(header[c] ?? "").length)
|
||||||
|
for (const row of rows) {
|
||||||
|
widths[c] = Math.max(widths[c], stripAnsi(row[c] ?? "").length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pad = (text: string, width: number, a: "left" | "center" | "right"): string => {
|
||||||
|
const needed = width - stripAnsi(text).length
|
||||||
|
if (needed <= 0) return text
|
||||||
|
if (a === "right") return " ".repeat(needed) + text
|
||||||
|
if (a === "center") {
|
||||||
|
const l = Math.floor(needed / 2)
|
||||||
|
return " ".repeat(l) + text + " ".repeat(needed - l)
|
||||||
|
}
|
||||||
|
return text + " ".repeat(needed)
|
||||||
|
}
|
||||||
|
|
||||||
|
const D = "\x1b[2m" // dim
|
||||||
|
const R = "\x1b[22m" // reset intensity
|
||||||
|
|
||||||
|
const renderRow = (cells: string[], bold: boolean): string => {
|
||||||
|
const parts = cells.map((c, i) => pad(c ?? "", widths[i] ?? 0, align[i] ?? "left"))
|
||||||
|
if (bold) {
|
||||||
|
return `${D}│${R} ${parts.map(p => `\x1b[1m${p}\x1b[22m`).join(` ${D}│${R} `)} ${D}│${R}`
|
||||||
|
}
|
||||||
|
return `${D}│${R} ${parts.join(` ${D}│${R} `)} ${D}│${R}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const hline = (l: string, m: string, r: string): string => {
|
||||||
|
return `${D}${l}${widths.map(w => "─".repeat(w + 2)).join(m)}${r}${R}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const out: string[] = []
|
||||||
|
out.push(hline("┌", "┬", "┐"))
|
||||||
|
out.push(renderRow(header, true))
|
||||||
|
out.push(hline("├", "┼", "┤"))
|
||||||
|
for (const row of rows) out.push(renderRow(row, false))
|
||||||
|
out.push(hline("└", "┴", "┘"))
|
||||||
|
return out.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
export function renderMarkdown(text: string): string {
|
export function renderMarkdown(text: string): string {
|
||||||
// Extract fenced code blocks before anything else
|
// Extract fenced code blocks before anything else
|
||||||
const codeBlocks: string[] = []
|
const codeBlocks: string[] = []
|
||||||
|
|
@ -57,6 +128,14 @@ export function renderMarkdown(text: string): string {
|
||||||
// Restore backslash escapes as literal characters
|
// Restore backslash escapes as literal characters
|
||||||
result = result.replace(/\x00ESC(\d+)\x00/g, (_, i) => escapes[parseInt(i)])
|
result = result.replace(/\x00ESC(\d+)\x00/g, (_, i) => escapes[parseInt(i)])
|
||||||
|
|
||||||
|
// Tables: render pipe tables with box-drawing characters
|
||||||
|
// Processed after inline formatting so cell contents are styled,
|
||||||
|
// but before code block restoration so tables inside code blocks are ignored.
|
||||||
|
result = result.replace(
|
||||||
|
/^(\|[^\n]+\|\n)(\|[\s:|-]+\|\n)((?:\|[^\n]+\|\n?)*)/gm,
|
||||||
|
(match) => renderTable(match),
|
||||||
|
)
|
||||||
|
|
||||||
// Restore fenced code blocks as plain text
|
// Restore fenced code blocks as plain text
|
||||||
result = result.replace(/\x00CODEBLOCK(\d+)\x00/g, (_, i) => codeBlocks[parseInt(i)])
|
result = result.replace(/\x00CODEBLOCK(\d+)\x00/g, (_, i) => codeBlocks[parseInt(i)])
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user