0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Markdown パーサを書きたい!~レンダリング編~

Posted at

Markdown パーサを書きたい!~レンダリング編~

この記事は某企業 2024 年度新卒 Advent Calender 2024 17 日目の記事です。

前回の記事ではMarkdownパーサを作りました。
今回はパーサして得た抽象構文木をレンダリングしてみるという記事です。
前回と比較すると軽めになると思います。

はじめに

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

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

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

レンダリング関数を作る

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

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_elementrender_contentを定義します。

render_elementrender_contentを定義する

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

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を作ります。

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

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に落とし込むだけなので割と簡単です。
最後にちょっとだけテストをしてみます。

#[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化できたらいいなーと思います。

それでは。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?