syntectでブログにシンタックスハイライト機能を追加した
やったこと
syntectというクレートを使用してブログ記事のコードブロックがシンタックスハイライトされるようにしました。
Before

After

syntectについて
syntectとはRustでシンタックスハイライトを可能にするライブラリです。 コードを解析し、ターミナルのエスケープシーケンス、もしくはHTMLでハイライトを追加します。
このコードを実行すると、
use syntect::{highlighting::ThemeSet, html::highlighted_html_for_string, parsing::SyntaxSet}; fn main() { let code = r#" fn main() { println!("Hello, world!"); } "#; let syntax_set = SyntaxSet::load_defaults_newlines(); let syntax = syntax_set.find_syntax_by_token("rust").unwrap(); let theme = ThemeSet::load_defaults().themes["base16-ocean.dark"].clone(); let output = highlighted_html_for_string(code, &syntax_set, &syntax, &theme).unwrap(); println!("{output}"); }
このようにHTMLでハイライトが追加されます。
<pre style="background-color:#2b303b;"> <span style="color:#c0c5ce;"> </span><span style="color:#b48ead;">fn </span><span style="color:#8fa1b3;">main</span><span style="color:#c0c5ce;">() { </span><span style="color:#c0c5ce;"> println!("</span><span style="color:#a3be8c;">Hello, world!</span><span style="color:#c0c5ce;">"); </span><span style="color:#c0c5ce;">} </span><span style="color:#c0c5ce;"> </span></pre>
ブログに組み込む
このブログではpulldown-cmarkというクレートでMarkdownからHTMLを生成しています。 pulldown-cmarkがHTMLを生成する処理の中に、コードブロックを探してsyntectでハイライトを追加する処理を挟みました。
また、syntectに多数の言語の構文とカラーテーマを追加するtwo-faceというクレートも使用しています。 two-faceにはブログ全体で使用しているCatppuccin Mochaというカラーテーマが含まれているため導入しました。
これがMarkdownからHTMLに変換するコードです。
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd, html::push_html}; use syntect::html::highlighted_html_for_string; use two_face::theme::{EmbeddedThemeName, extra}; fn markdown_to_html(markdown: &str) -> String { let parser = Parser::new_ext(markdown, Options::all()); // 追加したコード let mut in_code_block = false; let mut lang = String::new(); let mut code_buf = String::new(); let mut events: Vec<Event> = Vec::new(); let theme_set = two_face::theme::extra(); let theme = theme_set.get(EmbeddedThemeName::CatppuccinMocha); // two-faceのCatppuccin Mochaテーマを使用 let syntax_set = two_face::syntax::extra_newlines(); // Markdownの要素ごとに処理 for event in parser { match event { // コードブロックの開始を検知 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(ref l))) => { in_code_block = true; lang = l.to_string(); code_buf.clear(); } // コードブロックの中身を抽出 Event::Text(ref text) if in_code_block => code_buf.push_str(text), // コードブロックの終了を検知 Event::End(TagEnd::CodeBlock) if in_code_block => { in_code_block = false; if let Some(syntax) = syntax_set.find_syntax_by_token(&lang) // ハイライト可能な言語か判定 && let Ok(highlighted) = highlighted_html_for_string(&code_buf, &syntax_set, syntax, &theme) // ハイライトを追加 { events.push(Event::Html(highlighted.into())); // HTMLでハイライトされたコードブロックを出力 } else { let code_block = format!("<pre>{code_buf}</pre>"); events.push(Event::Html(code_block.into())); // コードブロックの中身をpreタグで包んで出力 } } // その他の要素はそのまま出力 event => events.push(event), } } let mut output = String::new(); push_html(&mut output, events.into_iter()); // HTMLとして出力 // 元のコード // html::push_html(&mut output, parser); output }
他の方法との比較
Webサイト上でシンタックスハイライトを追加する方法はPrism.jsかhighlight.jsを使用するのが一般的だと思います。 どちらも、デフォルト設定で使用するなら数行追加するだけで機能します。
Dioxusで使用する例を示します。(通常のHTMLでの使用方法は公式Webサイト等を参考にしてください)
// Dioxus + Prism.js #[component] pub fn App() -> Element { rsx! { Script { src: "https://cdn.jsdelivr.net/npm/prismjs@1.30.0/prism.min.js" } // Prism.js本体 Script { src: "https://cdn.jsdelivr.net/npm/prismjs@1.30.0/plugins/autoloader/prism-autoloader.min.js" } // 必要な言語の構文を自動でロードするプラグイン Link { rel: "stylesheet", href: "https://prismjs.catppuccin.com/mocha.css" } // カラーテーマ pre { class: "language-rust", code { /* ここがハイライトされる */ } } } }
// Dioxus + highlight.js #[component] pub fn App() -> Element { rsx! { Script { src: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js" } // highlight.js本体 Link { rel: "stylesheet", href: "//cdn.jsdelivr.net/npm/@catppuccin/highlightjs@1.0.1/css/catppuccin-mocha.css" } // カラーテーマ Script { "hljs.highlightAll();" } // highlight.jsを起動 pre { class: "language-rust", code { /* ここがハイライトされる */ } } } }
手軽に使えるという点はこの2つの長所です。
また、Prism.jsとhighlight.jsはページ表示後にハイライトが表示されるのに対し、syntectはHTMLを生成するときにハイライトを生成するという違いがあります。 実際に、ブラウザでそれぞれどのように表示されるか試してみましたが、体感できるような違いはありませんでした。
ビルドに失敗する問題
シンタックスハイライトの実装が完了し、デプロイしようとした際に問題に遭遇しました。 ローカル環境では、ビルド、ブラウザ上での表示ともに正常でしたが、GitHub Actionsでビルドすると失敗するという状況に陥りました。
原因はsyntectがOnigurumaというC言語で実装された正規表現エンジンに依存しており、 GitHub Actionsの環境にはこれがインストールされていないことでした。
syntectはOnigrumaとRust製の正規表現エンジンのfancy-regexのどちらを使用するか選択できるようになっていました。
Cargo.tomlをこのようにすることでsyntectがfancy-regexを使用するようになり、Onigrumaに依存しなくなるのでGitHub Actionsでもビルドが通るようになりました。
[dependencies] syntect = { version = "5.3.0", default-features = false, features = ["default-fancy"] } # syntect = "5.3.0" two-face = { version = "0.5.1", default-features = false, features = ["syntect-default-fancy"] } # two-face = "0.5.1"
syntectのREADMEによると、fancy-regexはOnigurumaと比較して処理速度が半分ほどです。 テキストエディタのようなリアルタイムでハイライトを追加する場合には影響が出るかもしれません。
まとめ
syntectを使用してブログ記事のコードブロックにシンタックスハイライトを追加しました。 実装は少し複雑ですが、Rustで全体を作ったのでRustでできる部分はRustでやりたいと思い、syntectを採用しました。
ビルド時にハイライトを生成するので、クライアント側で柔軟に表示を変更することは難しそうだど感じました。 今後、ライトモードとダークモードを変更する機能を実装する時などは工夫が必要そうです。