Markdown パーサを書きたい！～レンダリング編～

# Markdown パーサを書きたい！～レンダリング編～

この記事は[某企業 2024 年度新卒 Advent Calender 2024](https://adventar.org/calendars/10291) 17 日目の記事です。

[前回の記事](https://qiita.com/keiwa-tech/items/3312478861ea9a54f5fc)ではMarkdownパーサを作りました。
今回はパーサして得た抽象構文木をレンダリングしてみるという記事です。
前回と比較すると軽めになると思います。

## はじめに

前回作成した抽象構文木`Token`はこのような構造でした。

```rust
#[derive(Debug, PartialEq, Clone)]
struct Token {
    element: MDElement,
    children: Vec<Token>,
}
```

`Token`は木構造なので親の`Token`をレンダリングするためには子どもの`Vec<Token>`をレンダリングする必要があります。

## レンダリング関数を作る

まず大枠のレンダリング関数`render`を作ります。

```rust
fn render(token: &Token) -> String {
    if token.children.is_empty() {
        return render_content(&token.element);
    }

    let render_children = token
        .children
        .iter()
        .map(render)
        .collect::<Vec<String>>()
        .join("");

    render_element(&token.element, &render_children)
}
```

シンプルですね。
`render`関数は`Token`をとり`String`を返します。
`Token`は木構造なので、親のレンダリングをするために子どものレンダリングを行います。(変数`render_children`の部分)
ここは再帰処理となっています。

そして子どものレンダリングした結果を使って、HTMLを実際にレンダリングします。(関数`render_element`の部分)
関数`render_element`はHTMLタグだけ追加するイメージです。

この再帰関数の停止条件は、`Token`にトークンの子どもがいなくなったらです。
停止条件に達したときは関数`render_content`を呼び、`Token`の保持するテキストを含めたHTMLを返します。

では2つの関数`render_element`と`render_content`を定義します。

## `render_element`と`render_content`を定義する

この2つの関数を定義する前に、`MDElement`に対応するHTMLを生成する関数を作ります。

```rust
fn render_root(content: &String) -> String {
    format!("<div>{}</div>", content)
}

fn render_line_break() -> String {
    "".to_string()
}

fn render_header1(content: &String) -> String {
    format!("<h1>{}</h1>", content)
}

fn render_header2(content: &String) -> String {
    format!("<h2>{}</h2>", content)
}

fn render_header3(content: &String) -> String {
    format!("<h3>{}</h3>", content)
}

fn render_header4(content: &String) -> String {
    format!("<h4>{}</h4>", content)
}

fn render_header5(content: &String) -> String {
    format!("<h5>{}</h5>", content)
}

fn render_header6(content: &String) -> String {
    format!("<h6>{}</h6>", content)
}

fn render_paragraph(content: &String) -> String {
    format!("<p>{}</p>", content)
}

fn render_span(content: &String) -> String {
    format!("<span>{}</span>", content)
}

fn render_blockquote(content: &String) -> String {
    format!("<blockquote>{}</blockquote>", content)
}

fn render_code_block(content: &String) -> String {
    format!("<pre><code>{}</code></pre>", content)
}

fn render_unordered_list(content: &String) -> String {
    format!("<ul>{}</ul>", content)
}

fn render_ordered_list(content: &String) -> String {
    format!("<ol>{}</ol>", content)
}

fn render_list_item(content: &String) -> String {
    format!("<li>{}</li>", content)
}

fn render_horizontal_rule() -> String {
    "<hr>".to_string()
}

fn render_emphasis(content: &String) -> String {
    format!("<em>{}</em>", content)
}

fn render_strong(content: &String) -> String {
    format!("<strong>{}</strong>", content)
}

fn render_strikethrough(content: &String) -> String {
    format!("<del>{}</del>", content)
}

fn render_link(text: &String, url: &String) -> String {
    format!("<a href=\"{}\">{}</a>", url, text)
}

fn render_image(text: &String, url: &String) -> String {
    format!("<img src=\"{}\" alt=\"{}\">", url, text)
}
```

これらを使ってまず`render_element`を作ります。

```rust
fn render_element(parent: &MDElement, content: &String) -> String {
    match parent {
        MDElement::Root => render_root(content),
        MDElement::LineBreak => render_line_break(),
        MDElement::HorizontalRule => render_horizontal_rule(),
        MDElement::Header1(_) => render_header1(content),
        MDElement::Header2(_) => render_header2(content),
        MDElement::Header3(_) => render_header3(content),
        MDElement::Header4(_) => render_header4(content),
        MDElement::Header5(_) => render_header5(content),
        MDElement::Header6(_) => render_header6(content),
        MDElement::Paragraph(_) => render_paragraph(content),
        MDElement::Span(_) => render_span(content),
        MDElement::Blockquote(_) => render_blockquote(content),
        MDElement::CodeBlock(_) => render_code_block(content),
        MDElement::Emphasis(_) => render_emphasis(content),
        MDElement::Strong(_) => render_strong(content),
        MDElement::Strikethrough(_) => render_strikethrough(content),
        MDElement::OrderedList => render_ordered_list(content),
        MDElement::UnorderedList => render_unordered_list(content),
        MDElement::OrderedListItem(_, _) => render_list_item(content),
        MDElement::UnorderedListItem(_, _) => render_list_item(content),
        MDElement::Link(_, url) => render_link(content, url),
        MDElement::Image(text, url) => render_image(text, url),
        MDElement::ListItems(_) => "".to_string(),
    }
}
```

この関数は引数にテキスト取って、そのテキストにHTMLのタグを付与する感じです。
一方`render_content`は

```rust
fn render_content(markdown: &MDElement) -> String {
    match markdown {
        MDElement::LineBreak => render_line_break(),
        MDElement::Header1(content) => render_header1(content),
        MDElement::Header2(content) => render_header2(content),
        MDElement::Header3(content) => render_header3(content),
        MDElement::Header4(content) => render_header4(content),
        MDElement::Header5(content) => render_header5(content),
        MDElement::Header6(content) => render_header6(content),
        MDElement::Paragraph(content) => render_paragraph(content),
        MDElement::Span(content) => render_span(content),
        MDElement::Blockquote(content) => render_blockquote(content),
        MDElement::CodeBlock(content) => render_code_block(content),
        MDElement::Emphasis(content) => render_emphasis(content),
        MDElement::Strong(content) => render_strong(content),
        MDElement::Strikethrough(content) => render_strikethrough(content),
        MDElement::OrderedListItem(_, content) => render_list_item(content).to_string(),
        MDElement::UnorderedListItem(_, content) => render_list_item(content).to_string(),
        MDElement::HorizontalRule => render_horizontal_rule(),
        MDElement::Link(text, url) => render_link(text, url),
        MDElement::Image(text, url) => render_image(text, url),
        _ => "".to_string(),
    }
}
```

と、`render_element`と似ていますが、こちらは`MDElement`で保持しているテキストにHTMLのタグをつけています。

これでレンダリング関数は完成です。
レンダリング関数自体は`Token`の木構造そのままHTMLに落とし込むだけなので割と簡単です。
最後にちょっとだけテストをしてみます。

```rust
#[test]
fn test_redner() {
    let markdown = Token {
        element: MDElement::Root,
        children: vec![
            Token {
                element: MDElement::Header1("Hello, world!".to_string()),
                children: vec![],
            },
            Token {
                element: MDElement::Paragraph("*Hello, world!*".to_string()),
                children: vec![Token {
                    element: MDElement::Emphasis("Hello, world!".to_string()),
                    children: vec![],
                }],
            },
            Token {
                element: MDElement::UnorderedList,
                children: vec![
                    Token {
                        element: MDElement::UnorderedListItem(0, "**Hello1**".to_string()),
                        children: vec![Token {
                            element: MDElement::Strong("Hello1".to_string()),
                            children: vec![],
                        }],
                    },
                    Token {
                        element: MDElement::UnorderedListItem(0, "Hello2".to_string()),
                        children: vec![],
                    },
                    Token {
                        element: MDElement::UnorderedList,
                        children: vec![Token {
                            element: MDElement::UnorderedListItem(1, "Hello3".to_string()),
                            children: vec![],
                        }],
                    },
                ],
            },
        ],
    };
    let expected = "<div><h1>Hello, world!</h1><p><em>Hello, world!</em></p><ul><li><strong>Hello1</strong></li><li>Hello2</li><ul><li>Hello3</li></ul></ul></div>";

    assert_eq!(render(&markdown), expected);
}
```

## おわりに

ここまで、Markdownのパーサを作ってみるという3部作の記事を書いてきました。
書いてみた感想ですが、パーサコンビネータはかなり強力なツールだなとコードを書いていて思いました。
単体テストのしやすさもあって、テストで動作を保証しつつ色々試行錯誤でき、パーサの勉強にもなりました。
おかげで、なんとなくパーサを理解することができたと思います（書いてきた内容が王道かどうかは知りません）。

ただ、本当はパーサとレンダラを使ってCLIツールでも作ってみたかったのですが、アドベントカレンダーに間に合わなく断念しました。
というのも、Markdownのパーサにまだまだバグを含んでいて、普通のMarkdownですらぐちゃぐちゃにレンダリングされてしまいます。
なので、これからちょこちょこ修正していって、CLI化できたらいいなーと思います。

それでは。