markdown 形式のファイルを HTML にレンダリングするアプリケーションサーバを作成してみました。
Web フレームワークは tide クレート、markdown は pulldown-cmark クレート、Web サーバは nginx を使用しました。
システム構成
ユーザはWebブラウザを使ってWebサーバ (NGINX) にアクセスします。
この際に、.md ファイルがリクエストされた場合にはレンダリングサーバにリクエストを転送し、HTML に変換して返します。
.md 以外のコンテンツハンドリングのRust実装は面倒だったので NGINX にお任せしました。
レンダリングサーバ
tide/sync_std/pulldown-cmark/substring といったクレートを利用して実装しました。
このあたりのインストールの説明は割愛します。
// main.rs
const PORT:&str = "127.0.0.1:8081";
const TOP_DIR:&str = "C:\\opt\\nginx\\html"; // NGINX の HTML ディレクトリ
const CSS_URL:&str = "/css/markdown10.css";
use std::path::{Path, PathBuf};
use substring::Substring;
use pulldown_cmark::{html, Options, Parser};
#[async_std::main]
async fn main() -> tide::Result<()> {
eprintln! ("# Running {}", PORT);
let mut app = tide::new();
app.at("/*").get(md_handler);
app.listen(PORT).await?;
Ok(())
}
async fn md_handler (req: tide::Request<()>) -> tide::Result {
eprintln!("# {} {}", req.method(), req.url());
// パスの拡張子が ".md" でないときはエラー
let path = req.url().path();
if ! path.ends_with(".md") {
return Err(tide::Error::from_str(tide::StatusCode::InternalServerError, "not md file"))
}
let top_dir = Path::new(TOP_DIR);
let p = top_dir.join(path.substring(1, path.len())); // path の先頭の '/' より後を TOP_DIR に連結
// ファイルがないときはエラー
if ! p.is_file() {
return Err(tide::Error::from_str(tide::StatusCode::NotFound, "not found"))
}
Ok(tide::Response::builder(200)
.body(mdfile_to_html(p)?)
.content_type(tide::http::mime::HTML)
.build())
}
// mdfile_to_html は markdown ファイルを読みだして HTML にレンダリングする
fn mdfile_to_html (file: PathBuf) -> std::io::Result<String> {
let mut options = Options::empty();
options.insert(Options::all());
let text = std::fs::read_to_string(file)?;
let parser = Parser::new_ext(&text, options);
let mut html_buf = String::new();
html::push_html(&mut html_buf, parser);
Ok(format!(r#"
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="{css}">
</head>
<body>
{body}
</body>
</html>"#,
css = CSS_URL,
body = html_buf))
}
小学生が書いたような簡単なコードができてしまいました。(いや、いまどきは小学生だってもっと複雑なコードを書くに違いない)
定数の PORT はレンダリングサーバの待ち受けアドレス+ポート番号、TOP_DIR は NGINX の HTML ディレクトリ、CSS_URL は変換した HTML を修飾するスタイルシートのURLをそれぞれ指します。
(Mackdown から変換したHTML 用のスタイルシートについては "Markdown CSS" でググると色々みつかります)
エラー処理としては、リクエストされたパスの拡張子が .md 以外の場合と指定されたファイルが存在しないときをチェックしています。
なお、std::fs::read_to_string が出すエラー std:: io::Error は tide::Error に変換されるようです。
Web サーバ (NGINX) の設定
NGINX にはレンダラサーバのリバースプロキシの設定を行います。
まず、設定ファイル md.conf をお使いの NGINX の nginx.conf と同じ設定フォルダにコピーします。
# md.conf
location ~ \.md$ {
proxy_http_version 1.1;
proxy_pass http://127.0.0.1:8081;
}
この中で proxy_pass ディレクティブの http://127.0.0.1:8001 は先ほどのレンダラサーバの PORT 定数に対応させます。
nginx.conf の方では次のように server ディレクティブの中で md.conf をインクルードします。
# nginx.conf
…
server {
…
include md.conf;
…
}
…
実行例
ここでは NGINX は http://localhost:8080/ で動作させ、レンダリングサーバは http://localhost:8081/ で動作させました。
動作環境は Windows10 を使用。
以下は NGINX の HTML フォルダ直下に置いた test.md ファイルです。
(画像ファイル fuji.jpg も適切なフォルダ配下に置いています)
# まーくだうんのテスト

## 表
|あああ|いいい|ううう|
|:---|:---:|---:|
|か|き|く|
|さ|し|す|
## コード
‘‘‘
fn main() -> {
println! ("Hello, world!");
}
‘‘‘
## 箇条書きリスト
* かじょ
* がき
## 番号付きリスト
1. ばん
2. ごう
## 文字の修飾
**きょーちょー**
~~とりけし~~
## 引用
> いんよー。
>
> いんにょーではないよー。
(バッククオートをエスケープする方法がわからなかったので全角‘になっていることに注意)
ブラウザにこのファイルのURL http://localhost:8080/test.md を指定して表示させてみました。
うまく表示できました。
感想など
- tide を使ってみました。簡単にWebアプリが書けてよいのですが簡単すぎて面白味がないですね。こんなものばかり使ってると頭が悪くなりそう。
- 実は markdown を標準的な html にレンダリングするのではなくて、少しカスタマイズしようと思っていました。
ところが今回使用した pulldown-cmark クレートは markdown をパースした後の AST (Abstract Syntax Tree) を取り出す方法とか、HTMLへレンダリングする部分のカスタマイズする方法がわかりませんでした。 - Markdown ⇒ Markdown AST ⇒ DOM (HTML) といった流れになるはずなんですが、いろいろなフェーズでフックできるライブラリはないもんですかね。