refactor: replace Bun.markdown with marked library and custom renderer
This commit is contained in:
parent
4f9e7a7a1a
commit
b212fee200
3 changed files with 169 additions and 120 deletions
3
bun.lock
3
bun.lock
|
|
@ -13,6 +13,7 @@
|
||||||
"ansi-styles": "^6.2.3",
|
"ansi-styles": "^6.2.3",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"happy-dom-without-node": "^14.12.3",
|
"happy-dom-without-node": "^14.12.3",
|
||||||
|
"marked": "^18.0.4",
|
||||||
"shiki": "^4.0.2",
|
"shiki": "^4.0.2",
|
||||||
"terminal-link": "^5.0.0",
|
"terminal-link": "^5.0.0",
|
||||||
},
|
},
|
||||||
|
|
@ -142,6 +143,8 @@
|
||||||
|
|
||||||
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||||
|
|
||||||
|
"marked": ["marked@18.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-c/BTaKzg0G6ezQx97DAkYU7k0HM6ys0FqYeKBL6hlBByZwy+ycA1+f0vDdjMHKKeEjdgkx0GOv9Il6D+85cOqA=="],
|
||||||
|
|
||||||
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
|
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
|
||||||
|
|
||||||
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
"ansi-styles": "^6.2.3",
|
"ansi-styles": "^6.2.3",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"happy-dom-without-node": "^14.12.3",
|
"happy-dom-without-node": "^14.12.3",
|
||||||
|
"marked": "^18.0.4",
|
||||||
"shiki": "^4.0.2",
|
"shiki": "^4.0.2",
|
||||||
"terminal-link": "^5.0.0"
|
"terminal-link": "^5.0.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Cell, Table } from "@cliffy/table";
|
import { Cell, Table } from "@cliffy/table";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
import { marked, Renderer, type Tokens } from "marked";
|
||||||
import {
|
import {
|
||||||
type BundledLanguage,
|
type BundledLanguage,
|
||||||
type BundledTheme,
|
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(
|
export async function renderMarkdown(
|
||||||
text: PromiseLike<string> | string,
|
text: PromiseLike<string> | string,
|
||||||
columns: number,
|
columns: number,
|
||||||
|
|
@ -38,127 +197,13 @@ export async function renderMarkdown(
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let header: SWCell[] = [];
|
const renderer = new LmcRenderer(columns);
|
||||||
let body: SWCell[][] = [];
|
marked.setOptions({
|
||||||
|
renderer,
|
||||||
const rendered = Bun.markdown.render(md, {
|
gfm: true,
|
||||||
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 rendered = await marked.parse(md);
|
||||||
|
|
||||||
return rendered.replace(/\n\n$/, "\n");
|
return rendered.replace(/\n\n$/, "\n");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue