at backyard

Color my life with the chaos of trouble.

RustでmarkdownをHTMLに変換してみる

Rustでmarkdownから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.05.0.0-pre.14 という2つのバージョンが開発されているようで、ひとまず安定版かと思われる 4.0.0 を選択した。
(ちなみにそれぞれのバージョンによってだいぶ書き味は異なりそう。READMEに乗っているドキュメントを見ただけだが)

github.com

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に変換する処理ができた。

f:id:shinshin86:20220408221904g:plain
実際に動作させているところ

指定したディレクトリ内の変更を検知して、対象のファイルを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変換に関する備忘録はこれにて以上。