ブログシステムを作る(3) - marked.jsでparseしたMarkdownにshikiでSyntax Highlighting
Sun Jan 15 2023
今日は前から試したかったshikiを使ってSyntax Highlightingを実装してみました。
Shiki - a beautiful syntax highlighter
ShikiはMicrosoftでVS Codeの開発に従事していたPineさんが実装しているSyntax Highlighterで、仲間内のDiscordで話題になっていたので試してみたいと思っていたものです。
TextMateのgrammerを構文解析に使っているため、かなりの数の言語をハイライトすることができて便利です。
使用感
READMEから引っ張ってきたものですが、こんな感じ。HighlighterをAsyncで取ってこないといけないのがちょっと微妙ではあります。
getHighlighter
でThemeやLanguageの情報を非同期で取ってくる仕組みになっているので、仕方ない感じも。
const shiki = require('shiki')
shiki
.getHighlighter({
theme: 'nord'
})
.then(highlighter => {
console.log(highlighter.codeToHtml(`console.log('shiki');`, { lang: 'js' }))
})
// <pre class="shiki nord" style="background-color: #2e3440"><code>
// <!-- Highlighted Code -->
// </code></pre>
Markedと組み合わせて使う
Markedのハイライト機能を使う
これ、思ったより苦戦しました。まず、元々Markedでrenderしていた部分のコードが以下の通り。
// https://github.com/mactkg/makerbox.net/blob/eedce8477aa0efa35f8bba85b336f510111a8d7b/lib/blogs/article.ts#L36-L41
async renderHTML(): Promise<string> {
if (this.renderedHTML) return this.renderedHTML;
const html = await marked.parse(this.body, { async: true });
this.renderedHTML = html;
return html;
}
シンプルですね。MarkedにはSyntax Highlightingの機能があり、これを使えばうまく動きそうです。
async renderHTML(): Promise<string> {
if (this.renderedHTML) return this.renderedHTML;
const html = await marked.parse(this.body, {
async: true,
highlight: (code, lang, callback) {
shiki
.getHighlighter({ theme: 'nord' })
.then((highlighter) => {
const html = highlighter.codeToHtml(code, { lang })
callback ? callback(html) : null;
});
}
});
this.renderedHTML = html;
return html;
}
が、これが動かない。。。よくよくみてみると、highlightの第三引数の callback
が渡ってきていないのです。何事?
highlightの処理はここでやっているようなのですが、この処理が呼ばれるのは marked.parse
の第三引数にcallback関数が渡っている時だけ。
動いた!
仕方なく、このようなコードにしました。 highlight
の中でPromiseを扱う必要がなくなって、見通しも良くなったきも。
async renderHTML(): Promise<string> {
if (this.renderedHTML) return this.renderedHTML;
const highlighter = await shiki.getHighlighter({ theme: 'nord' })
const html = await marked.parse(this.body, {
async: true,
highlight: (code, lang, callback) {
return highlighter.codeToHtml(code, { lang })
}
});
this.renderedHTML = html;
return html;
}
スタイリングの調整
ということでひとまずSyntax Highlightingはできたのですが、見た目が微妙な感じになってしまっています。
よくよく見てみると、DOM構造がネストしていました。
markedの highlight
関数は <pre>
と <code>
の中身だけを返すことを想定している一方で、shikiは返してしまっているみたいです。今回は、shikiのカスタマイズ機能を使って実装をしてみます。
Custom rendering
shikiのREADMEにある、Custom rendering of code blocks
を参考にしながら実装していけば特に迷いませんでした。codeToThemedTokens
でトークンに分けた後、 renderToHTML
で描画をすればOK。 renderToHTML
のオプション引数で描画方式をカスタマイズできる。
import shiki, { getHighlighter } from 'shiki'
const highlighter = await getHighlighter({
theme: 'nord',
langs: ['javascript', 'python']
})
const code = `console.log("Here is your code.");`
const tokens = highlighter.codeToThemedTokens(code, 'javascript')
const html = shiki.renderToHTML(tokens)
このブログを書く過程でREADMEがおかしいことに気づいたのでPRを出しました。 doc: Fix a way to access renderToHTML
by mactkg · Pull Request #415 · shikijs/shiki
完成!
最終的にはこんな感じになりました。
async renderHTML(): Promise<string> {
if (this.renderedHTML) return this.renderedHTML;
const highlighter = await shiki.getHighlighter({
theme: "github-light",
});
const html = await marked(this.body, {
async: true,
highlight(code, lang) {
const tokens = highlighter.codeToThemedTokens(code, lang);
return shiki.renderToHtml(tokens, {
elements: {
pre({ children }) {
return children;
},
code({ children }) {
return children;
},
},
});
},
});
this.renderedHTML = html;
return html;
}
まとめ
- marked.jsで書いたMarkdown内のコードブロックをshikiでSyntax Highlightingした
- markedのasyncを使っていると、highlightオプションのcallbackが配置されない
- shikiでレンダリングするHTMLは
renderToHTML
を使えばカスタマイズできる
- 早々にmarked脱出しておくと良さそう
次はアクセスカウンタなど動的なアイテムを入れてみたいなあという気がしています。