RustでmarkdownからHTMLへの変換を試してみる備忘録。
目次
- 目次
- pulldown-cmarkを試してMarkdownからHTMLに変換するサンプル
- markdownファイルを読み込んでHTMLファイルを生成するサンプル
- ファイル変更を読み取って都度都度Markdown -> HTML変換を行う
- 指定したディレクトリ内の変更を検知して、対象のファイルをHTMLに書き出すようにする
pulldown-cmarkを試してMarkdownからHTMLに変換するサンプル
https://crates.io/crates/pulldown-cmark
下記を Cargo.toml に追加。
デフォルトではバイナリもビルドされているらしいが、今回ライブラリとして pulldown-cmark を使いたかったので下記のようにした。
これでバイナリは除外されるらしい。
pulldown-cmark = { version = "0.9.1", default-features = false }
ドキュメントを参考にしてサンプルコードを書いてみる。といっても、ドキュメントそのまま。
use pulldown_cmark::{html, Options, Parser}; fn main() { let markdown_input = "# ハローワールド * 111 * 222 * 333 ~~取り消し線~~ *太字*. "; let mut options = Options::empty(); // 取り消し線はCommonMarkの規格に含まれていないため、明示的に有効にする options.insert(Options::ENABLE_STRIKETHROUGH); let parser = Parser::new_ext(markdown_input, options); let mut html_output = String::new(); html::push_html(&mut html_output, parser); println!("{}", &html_output); }
変換後のHTMLがこちら。
<h1>ハローワールド</h1> <ul> <li>111</li> <li>222</li> <li>333</li> </ul> <p><del>取り消し線</del> <em>太字</em>.</p>
markdownファイルを読み込んでHTMLファイルを生成するサンプル
上のサンプルを基準にして、input.md というファイルから読み込んだMarkdownテキストを変換して output.html ファイルに書き出すサンプル。
input.md の中身。
# ハローワールド * 111 * 222 * 333 ~~取り消し線~~ *太字*.
下記がRustのコード。
ところどころ unwrap でしのいでしまっている。
use pulldown_cmark::{html, Options, Parser}; use std::fs; use std::fs::File; use std::io::Write; use std::path::Path; fn read_md_file(file_path: &std::path::Path) -> Result<String, Box<dyn std::error::Error>> { let md = fs::read_to_string(file_path.to_str().unwrap())?; Ok(md) } fn write_html_file( file_path: &std::path::Path, html: &str, ) -> Result<(), Box<dyn std::error::Error>> { let mut file = File::create(file_path)?; write!(file, "{}", html)?; Ok(()) } fn main() { let input_file_path = Path::new("./input.md"); let markdown_input = read_md_file(input_file_path).unwrap(); let mut options = Options::empty(); options.insert(Options::ENABLE_STRIKETHROUGH); let parser = Parser::new_ext(&markdown_input, options); let mut html_output = String::new(); html::push_html(&mut html_output, parser); let html_file_path = Path::new("./output.html"); write_html_file(html_file_path, &html_output).unwrap(); }
これで cargo run をすると、下記の内容の output.html が吐き出される。
<h1>ハローワールド</h1> <ul> <li>111</li> <li>222</li> <li>333</li> </ul> <p><del>取り消し線</del> <em>太字</em>.</p>
ファイル変更を読み取って都度都度Markdown -> HTML変換を行う
次は input.md の変更を検知して output.html への変更を適宜行うサンプル。
ファイルの変更検知には notify というcrateを用いる。
ちなみに 4.0 と 5.0.0-pre.14 という2つのバージョンが開発されているようで、ひとまず安定版かと思われる 4.0.0 を選択した。
(ちなみにそれぞれのバージョンによってだいぶ書き味は異なりそう。READMEに乗っているドキュメントを見ただけだが)
Cargo.toml に以下を追加する。
notify = "4.0.16"
そして先ほどのコードを以下のように変更した。
ファイル検知に関する部分は notify のドキュメントに載っているのをほぼほぼそのまま持ってきた感じで、markdownからHTMLに変換する部分を markdown_to_html にうつしているだけ。
use notify::{RecommendedWatcher, RecursiveMode, Watcher}; use pulldown_cmark::{html, Options, Parser}; use std::fs; use std::fs::File; use std::io::Write; use std::path::Path; use std::sync::mpsc::channel; use std::time::Duration; fn read_md_file(file_path: &std::path::Path) -> Result<String, Box<dyn std::error::Error>> { let md = fs::read_to_string(file_path.to_str().unwrap())?; Ok(md) } fn write_html_file( file_path: &std::path::Path, html: &str, ) -> Result<(), Box<dyn std::error::Error>> { let mut file = File::create(file_path)?; write!(file, "{}", html)?; Ok(()) } fn markdown_to_html(input_path: &std::path::Path, output_path: &std::path::Path) { let markdown_input = read_md_file(input_path).unwrap(); let mut options = Options::empty(); options.insert(Options::ENABLE_STRIKETHROUGH); let parser = Parser::new_ext(&markdown_input, options); let mut html_output = String::new(); html::push_html(&mut html_output, parser); write_html_file(output_path, &html_output).unwrap(); } fn main() -> notify::Result<()> { let input_file_path = Path::new("./input.md"); let output_file_path = Path::new("./output.html"); let (tx, rx) = channel(); let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(1))?; watcher.watch(input_file_path, RecursiveMode::Recursive)?; loop { match rx.recv() { Ok(_) => markdown_to_html(input_file_path, output_file_path), Err(err) => println!("watch error: {:?}", err), }; } }
これでひとまず下記のようにリアルタイムにmarkdownからHTMLに変換する処理ができた。

指定したディレクトリ内の変更を検知して、対象のファイルをHTMLに書き出すようにする
上の処理をさらに拡張して指定したディレクトリ内での変更を検知したい。
ただ notify 側でディレクトリを指定すれば、よしなに変更は検知してくれるようだった。
notifyのDebouncedEventについて
ちなみにnotify でのイベント検出に関しては、複数の種類のイベントが渡ってくる。
loop { match rx.recv() { Ok(event) => println!("event: {:?}", event), Err(err) => println!("watch error: {:?}", err), }; }
この event で渡ってくるのが DebouncedEvent で詳細は下記にある。
DebouncedEvent in notify - Rust
今回の場合対象のパスに対する書き込みイベント直後に発行される、NoticeWrite と、ファイルへの書き込みが行われ、指定された時間内にパスに対するイベントが検出されなかった場合に発行される Writeが発行される。
今回HTMLで変換を行うためにこれらのイベントを利用しようと思うのだが、保存した直後に処理を行うのではなく Write のタイミングでHTML変換処理を行うことにした。
というわけで、以下のようにコードを書き換える。
ただ、ちょっと時間がなくて以下のように恐ろしく汚いコードになってしまっている。
(後々整理する予定だが、そもそも自分のスキルでこのコードを洗練させられるのかは疑問)
追記:自分なりに調べてエラーハンドリングとコードの整理を実施した。以下のコードは整理後のコードとなる。
input ディレクトリに .md ファイルがある状態で起動してmarkdownを編集すると、outputディレクトリに変換後のHTMLファイルが生成される。
なお、プロセスを起動後に input ファイルにファイルを作成してもそのファイルの変更については変更を検知しなかった。
それは notify の仕様なのか、自分の実装化までは追っていない。
fn main() -> notify::Result<()> {
- let input_file_path = Path::new("./input.md");
- let output_file_path = Path::new("./output.html");
-
let (tx, rx) = channel();
let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(1))?;
- watcher.watch(input_file_path, RecursiveMode::Recursive)?;
+
+ let input_dir_path = Path::new("./input");
+ watcher.watch(input_dir_path, RecursiveMode::Recursive)?;
loop {
match rx.recv() {
- Ok(_) => markdown_to_html(input_file_path, output_file_path),
+ Ok(event) => match event {
+ notify::DebouncedEvent::Write(path) => {
+ let input_file_path = Path::new(&path);
+ let md_file_name = input_file_path.file_name().unwrap();
+ match Path::new(md_file_name).extension() {
+ Some(md_exntension) => {
+ if md_exntension != "md" {
+ eprintln!("ERROR: Only markdown files can be converted.");
+ std::process::exit(1);
+ }
+ }
+ None => {
+ eprintln!("ERROR: Not found extension.");
+ std::process::exit(1);
+ }
+ };
+
+ let html_file_name = md_file_name.to_str().unwrap().replace(".md", ".html");
+ let output_file_path = input_dir_path
+ .parent()
+ .unwrap()
+ .join("output")
+ .join(html_file_name);
+
+ markdown_to_html(input_file_path, output_file_path.as_path());
+
+ println!("=== Generated HTML ===");
+ println!("Input file path: {:?}", input_file_path);
+ println!(
+ "Output file path: {:?}",
+ output_file_path.canonicalize().unwrap()
+ );
+ }
+ _ => (),
+ },
Err(err) => println!("watch error: {:?}", err),
};
}
ひとまずMarkdown -> HTML変換に関する備忘録はこれにて以上。