5segです。
このブログはMarkdownで書いてて、パーサーにMarked.jsを使っているのですが、どうやらBunにはマークダウンのパーサーがあるらしいです。
https://bun.com/docs/runtime/markdown
じゃあMarkedいらないじゃんとなりそうですが、まだ不安定のようで、これから仕様が変わるかもしれません。
とはいえ全然使えそうな気もするので、試してみました。
実験するにあたって書いたコードはここにあります。私が理解できて、動けばいいので構造は汚いです。
また、コードも改善の余地はあると思います。後半で比較を行っていますが、timeによる計測であることと、記法次第で結果が大きく揺らぐことに注意してください。
使い方
const md = await Bun.file("./foo.md").text();
const html = Bun.markdown.html(md);
await Bun.write("./out.html", html);以上。あほほど簡単です。
とはいえハイライターがついていないので、このままだとコードが見にくいです。なんなら背景すらないですし。
なのでshikiをがっちゃんこします。するとこうなります:
let langs: string[] = [];
const renderCallbacks: markdown.RenderCallbacks = {
heading: (children, { level }) => `<h${level}>${children}</h${level}>`,
paragraph: (children) => `<p>${children}</p>`,
strong: (children) => `<strong>${children}</strong>`,
emphasis: (children) => `<em>${children}</em>`,
codespan: (children) => `<code>${children}</code>`,
code: (children, meta) => {
const lang = meta?.language ?? "text";
if (!langs.includes(lang)) langs.push(lang);
return `<pre><code class="shiki ${lang}">${Bun.escapeHTML(children)}</code></pre>`;
},
link: (children, { href, title }) =>
title
? `<a href="${href}" title="${title}">${children}</a>`
: `<a href="${href}">${children}</a>`,
image: (children, { src, title }) =>
title
? `<img src="${src}" alt="${children}" title="${title}" />`
: `<img src="${src}" alt="${children}" />`,
list: (children, { ordered, start }) =>
ordered ? `<ol start="${start}">${children}</ol>` : `<ul>${children}</ul>`,
listItem: (children) => `<li>${children}</li>`,
blockquote: (children) => `<blockquote>${children}</blockquote>`,
hr: () => `<hr />`,
strikethrough: (children) => `<del>${children}</del>`,
table: (children) => `<table>${children}</table>`,
thead: (children) => `<thead>${children}</thead>`,
tbody: (children) => `<tbody>${children}</tbody>`,
tr: (children) => `<tr>${children}</tr>`,
th: (children) => `<th>${children}</th>`,
td: (children) => `<td>${children}</td>`,
};
const customized = await highlight(
Bun.markdown.render(md, renderCallbacks),
langs,
);
await Bun.write("./out-customized.html", customized);記述量が一気に増えました。codeの部分だけいじくり回してますが、それ以外はBunがベンチマーク用に使ってるのであろうコードから拝借しています。defaultRenderCallbacksとか用意してくれればいいんですけどね。それか、Bunがデフォルトでハイライターを搭載してくれれば...
とりあえずパーサーはこんな感じで、ハイライターはこう:
export const highlight = async (html: string, langs: string[]) => {
const hightlighter = await createHighlighter({/* ... */});
let lang: string;
const rewriter = new HTMLRewriter().on("code.shiki", {
element(elem) {
lang = elem.getAttribute("class")!.split(" ")[1]!;
},
text(text) {
const code = text.text
.replaceAll(""", '"')
// ...
.replaceAll(">", ">");
text.replace(
(() =>
hightlighter.codeToHtml(code, {/* ... */}))(),
{ html: true },
);
},
});
return rewriter.transform(html);
};ちょっと端折ってます。
shiki建てて、BunのHTMLRewriterで一致箇所をハイライトされたコードに置き換えるだけのシンプルなコードです。
速度比較
16KBのsample.mdをtime bun run {name}.tsした結果です。index.tsはBun、with-marked.tsはMarkedです。
with shiki
| Name | Time |
|---|---|
| index.ts | 1.37s |
| with-marked.ts | 1.46s |
Markedはパースとハイライトを同時に行っているのに対し、Bunはパース → ハイライトという2ステップを踏んでいます。しかしそのハンデがある上で約0.1秒の高速化に成功しています。素晴らしい!
without shiki
純粋に、Markdown → HTML の変換で勝負した場合の結果です。
| Name | Time |
|---|---|
| index.ts | 0.01s |
| with-marked.ts | 0.24s |
圧勝です。Bun最強。
デカめのも試す
サンプルファイルで遊んでみました。
5MBぐらいのサンプルがあったのでやってみたら、Bunは約1.7秒で完了。Markedの方は何分経っても終わらないのでやめました。
両方結果を確認できたのは1MBのサンプルで、Bunは0.02秒、Markedは0.38秒でした。
結論
十分に使えるでしょう。
今後仕様が変わるかもしれませんが、その時はその時でなんとかすればいいです。
それでは。