refactor: replace Bun.markdown with marked library and custom renderer

This commit is contained in:
mitchell 2026-05-25 19:35:47 -04:00
parent 4f9e7a7a1a
commit b212fee200
3 changed files with 169 additions and 120 deletions

View file

@ -1,5 +1,6 @@
import { Cell, Table } from "@cliffy/table";
import chalk from "chalk";
import { marked, Renderer, type Tokens } from "marked";
import {
type BundledLanguage,
type BundledTheme,
@ -18,6 +19,164 @@ class SWCell extends Cell {
}
}
class LmcRenderer extends Renderer {
private columns: number;
constructor(columns: number) {
super();
this.columns = columns;
}
override heading({ tokens, depth }: Tokens.Heading) {
const colors = [
chalk.cyan.underline,
chalk.blue,
chalk.green,
chalk.cyanBright,
chalk.blueBright,
chalk.greenBright,
];
const color = colors[depth - 1] ?? chalk.white;
const text = this.parser.parseInline(tokens);
return Bun.wrapAnsi(
color.bold(`${"#".repeat(depth)} ${text}\n\n`),
this.columns,
{ trim: false },
);
}
override paragraph({ tokens }: Tokens.Paragraph) {
const text = this.parser.parseInline(tokens);
return Bun.wrapAnsi(`${text}\n\n`, this.columns, { trim: false });
}
override blockquote({ tokens }: Tokens.Blockquote) {
const text = this.parser.parse(tokens).trim();
const lines = Bun.wrapAnsi(text, this.columns - 2, { trim: false })
.replace(/\n+/g, "\n")
.concat("\n")
.split("\n");
const toRender = lines
.map((line) => `> ${line}`)
.join("\n")
.concat("\n\n");
return toRender.replaceAll(">", chalk.dim(">"));
}
override code({ text, lang, escaped: _ }: Tokens.Code) {
let toRender = text;
if (highlighter && lang) {
try {
toRender = codeToANSI(
highlighter,
text,
lang as BundledLanguage,
"kanagawa-wave",
);
} catch {}
}
return `\`\`\`${chalk.blue(lang ?? "")}\n${toRender}\`\`\`\n`;
}
override list({ items }: Tokens.List) {
const renderedItems = items.map((item) => this.listitem(item)).join("");
const trimmed = renderedItems.replace(/\n+/g, "\n");
return `${trimmed}\n`;
}
override listitem({ task, checked, tokens }: Tokens.ListItem) {
const text = this.parser.parse(tokens);
let marker = "";
if (task) {
marker = checked ? "• [✓]" : "• [ ]";
} else {
marker = "•";
}
const cols = this.columns - 2;
return Bun.wrapAnsi(`${marker} ${text}`, cols, { trim: false })
.split("\n")
.map((line) => (line.length > 0 ? ` ${line}` : ""))
.join("\n")
.concat("\n");
}
override hr() {
return `${chalk.dim("─".repeat(this.columns))}\n\n`;
}
override table({ header, rows }: Tokens.Table) {
const headerCells = header.map((col) => {
const text = this.parser.parseInline(col.tokens);
return new SWCell(chalk.red.bold(text));
});
const body = rows.map((row) =>
row.map((col) => {
const text = this.parser.parseInline(col.tokens);
return new SWCell(text);
}),
);
const table = new Table()
.header(headerCells)
.body(body)
.border()
.maxColWidth(Math.floor((this.columns - 3) / headerCells.length))
.toString();
return `${table}\n\n`;
}
override strong({ tokens }: Tokens.Strong) {
const text = this.parser.parseInline(tokens);
return chalk.bold.rgb(254, 254, 254)(text);
}
override em({ tokens }: Tokens.Em) {
const text = this.parser.parseInline(tokens);
return chalk.italic.rgb(254, 254, 254)(text);
}
override del({ tokens }: Tokens.Del) {
const text = this.parser.parseInline(tokens);
return chalk.grey.strikethrough(text);
}
override codespan({ text }: Tokens.Codespan) {
return chalk.bgYellow.black.bold(text);
}
override link({ href, tokens }: Tokens.Link) {
const text = this.parser.parseInline(tokens);
return chalk.blueBright.underline(terminalLink(text, href));
}
override image({ text: _ }: Tokens.Image) {
return chalk.red("Images are unsupported (for now)\n");
}
override text(token: Tokens.Text) {
if (token.tokens) {
return this.parser.parseInline(token.tokens);
}
return token.text
.replaceAll("$\\rightarrow$", "-->")
.replaceAll("$\\leftarrow$", "<--")
.replaceAll("$\\leftrightarrow$", "<->")
.replaceAll("$\\Rightarrow$", "==>")
.replaceAll("$\\Leftarrow$", "<==")
.replaceAll("$\\Leftrightarrow$", "<=>")
.replaceAll("$\\to$", "->");
}
override html({ text: _ }: Tokens.HTML | Tokens.Tag) {
return chalk.red("HTML is unsupported\n");
}
}
export async function renderMarkdown(
text: PromiseLike<string> | string,
columns: number,
@ -38,127 +197,13 @@ export async function renderMarkdown(
} catch {}
}
let header: SWCell[] = [];
let body: SWCell[][] = [];
const rendered = Bun.markdown.render(md, {
heading: (children, { level }) => {
const colors = [
chalk.cyan.underline,
chalk.blue,
chalk.green,
chalk.cyanBright,
chalk.blueBright,
chalk.greenBright,
];
const color = colors[level - 1] ?? chalk.white;
return Bun.wrapAnsi(
color.bold(`${"#".repeat(level)} ${children}\n\n`),
columns,
{ trim: false },
);
},
paragraph: (children) =>
Bun.wrapAnsi(`${children}\n\n`, columns, { trim: false }),
blockquote: (children) => {
const lines = Bun.wrapAnsi(`\n${children.trim()}`, columns - 2, {
trim: false,
})
.replace(/\n+/g, "\n")
.concat("\n")
.split("\n");
const toRender = lines
.map((line) => `> ${line}`)
.join("\n")
.replace(/(\n> +\S+) *\n> */g, "$1 ")
.replaceAll(">", chalk.dim(">"))
.concat("\n\n");
return toRender;
},
code: (children, meta) => {
let toRender = children;
if (highlighter && meta?.language) {
try {
toRender = codeToANSI(
highlighter,
children,
meta.language as BundledLanguage,
"kanagawa-wave",
);
} catch {}
}
return `\`\`\`${chalk.blue(meta?.language ?? "")}\n${toRender}\`\`\`\n`;
},
list: (children, { depth }) => {
const trimmed = children.replace(/\n+/g, "\n");
return depth === 0 ? `${trimmed}\n` : `\n${trimmed}`;
},
listItem: (children, { index, depth, ordered, start, checked }) => {
let marker = "";
if (checked !== undefined) {
marker = checked ? "• [✓]" : "• [ ]";
} else if (ordered) {
const n = (start ?? 1) + index;
marker = `${n}.`;
} else {
marker = "•";
}
const cols = columns - (depth + 1) * 2;
return Bun.wrapAnsi(`${marker} ${children}`, cols, { trim: false })
.split("\n")
.map((line) => (line.length > 0 ? ` ${line}` : ""))
.join("\n")
.replace(/(\n +\S+) *\n */g, "$1 ")
.concat("\n");
},
hr: () => `${chalk.dim("─".repeat(columns))}\n\n`,
table: (_children) => {
const table = new Table()
.header(header)
.body(body)
.border()
.maxColWidth(Math.floor((columns - 3) / header.length))
.toString();
header = [];
body = [];
return `${table}\n\n`;
},
tr: (children) => {
if (!children) return;
body.push(
children
.split(":tablesep:")
.slice(1)
.map((value) => new SWCell(value)),
);
return null;
},
td: (children) => `:tablesep:${children}`,
th: (children) => {
header.push(new SWCell(chalk.red.bold(children)));
return null;
},
strong: (children) => chalk.bold.rgb(254, 254, 254)(children),
emphasis: (children) => chalk.italic.rgb(254, 254, 254)(children),
strikethrough: (children) => chalk.grey.strikethrough(children),
codespan: (children) => chalk.bgYellow.black.bold(children),
link: (children, { href }) =>
chalk.blueBright.underline(terminalLink(children, href)),
image: () => chalk.red("Images are unsupported (for now)\n"),
text: (children) =>
children
.replaceAll("$\\rightarrow$", "-->")
.replaceAll("$\\leftarrow$", "<--")
.replaceAll("$\\leftrightarrow$", "<->")
.replaceAll("$\\Rightarrow$", "==>")
.replaceAll("$\\Leftarrow$", "<==")
.replaceAll("$\\Leftrightarrow$", "<=>")
.replaceAll("$\\to$", "->"),
html: () => chalk.red("HTML is unsupported\n"),
const renderer = new LmcRenderer(columns);
marked.setOptions({
renderer,
gfm: true,
});
const rendered = await marked.parse(md);
return rendered.replace(/\n\n$/, "\n");
}