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変換に関する備忘録はこれにて以上。