Rust로 Markdown 사용하기 - Rust Static Blog
/ 7 min read
Markdown
사실 모두 알고 있으리라 생각하지만…
Markdown은 Daring Fireball을 운영하고있는 John Gruber가 만든 markup 언어이다.
https://daringfireball.net/projects/markdown/
Markdown은 ‘원문을 읽기 쉽도록’ 만들어졌다.
디테일한 설명은 질리도록 들어왔으리라 생각한다…
Rust Markdown?
사실 markdown은 웹에서 엄청나게 많이 쓰이기에 JS로 이루어진 compiler가 많지만, Rust로 작성된것도 꽤 있다.
두 crate를 모두 사용해본 결과, mardown-rs
는 간단한 compile에는 사용할 수 있지만, mdast(Markdown Abstract Syntax Tree)를 지원함에도 불구, 훨신 복잡한 plugin 개발 등에는 적합하지 않다고 판단했다.
pulldown-cmark
오히려 mdast를 지원하지 않는 pulldown-cmark
가 더 많은 cusomization을 할 수 있었다.
pulldown-cmark
는 CommonMark를 기본적으로 지원하고, 추가 기능을 켜고 끌 수 있다.
pulldown-cmark
는 기본적으로 꽤 많은 추가 기능을 지원한다.
- gfm
- heading attributes
- yaml metadata
- math
- and more…
간단하게는, 아래와 같이 Parser
를 이용할 수 있다.
let raw_markdown = "hello world";let parser = pulldown_cmark::Parser::new(raw_markdown);
let mut html_output = String::new();pulldown_cmark::html::push_html(&mut html_output, parser);assert_eq!(&html_output, "<p>hello world</p>\n");
신기한 점은, Parser
객체가 iterator
라는 점이다.
Parser
객체를 FnMut(Event<'_>) -> Event<'_>
closure로 돌면, 간단히 플러그인을 만들 수 있었다.
복잡해 보이지만, Parser
를 map하면, Event
객체가 반환되고, 이를 다시 Event
객체를 반환하면 된다는 뜻.
Frontmatter Extract
frontmatter를 추출하는 plugin을 간단히 만들 수 있다.
let raw_markdown = r#"---title: "pulldown cmark frontmatter"description: "frontmatter plugin"---"#
let options = { let mut opt = Options::empty(); opt.insert(Options::ENABLE_YAML_STYLE_METADATA_BLOCKS); opt };
let mut raw_frontmatter = String::new();let mut frontmatter_started = false;
let parser = Parser::new_ext(&raw_markdown, options) .map(|event| { match event { Event::Start(Tag::MetadataBlock(_)) => { frontmatter_started = true; }, Event::End(TagEnd::MetadataBlock(_)) => { frontmatter_started = false; }, Event::Text(text) => { if frontmatter_started { let _ = &raw_frontmatter.push_str(&text); } }, _ => () }
event })
assert_eq!( &raw_markdown, r#"title: "pulldown cmark frontmatter"description: "frontmatter plugin""#);
특정 태그가 열리고 닫히는 Event
에 따라, frontmatter를 추출할 수 있다.
Plugin
위 예시처럼, 태그가 열리고 닫히는 순간 사이에 있는 내용을 얻어서 직접 플러그인을 만들 수 있는다.
하지만, 위와같이 모든 플러그인을 작성하면, 플러그인 하나마다, 거진 하나 또는 그 이상의 mut
변수가 추가된다는 뜻이다.
그래서, 하나의 플러그인을 하나의 객체로 만들기로 했다.
mut
변수가 없는 플러그인은 다음과 같이 간단히 만들 수 있다.
예시 : KATEX 렌더 플러그인
pub struct MathPlugin {}
impl MathPlugin { pub fn new() -> Self { Self {} } pub fn apply (&self) -> impl FnMut(Event<'_>) -> Event<'_> { return |event| { // code match event { Event::InlineMath(text) => { let opts = katex::Opts::builder() .display_mode(false) .trust(true) .output_type(OutputType::Mathml) .build().unwrap(); let html = katex::render_with_opts(&text, &opts).unwrap(); Event::Html(html.into()) }, Event::DisplayMath(text) => { let opts = katex::Opts::builder() .display_mode(true) .trust(true) .output_type(OutputType::Mathml) .build().unwrap(); let html = katex::render_with_opts(&text, &opts).unwrap(); Event::Html(html.into()) }, _ => event } } }}
let math_plugin = MathPlugin::new();
let parser = Parser::new_ext(&raw_markdown, options) .map(math_plugin.apply());
CodeBlock Plugin
Math의 경우에는 내용물이 그대로 Event
의 내용으로 반환되지만, Codeblock은 그렇지 않고, 여닫는 Event
만 나오기에 앞선 frontmatter과 비슷한 접근을 해야했다.
이 경우, struct
에 item을 사용해보았다.
pub struct CodeHighlightPlugin { lang: String, source: String, is_in: bool,}
impl CodeHighlightPlugin { pub fn new() -> Self { Self { lang: Default::default(), source: Default::default(), is_in: false } } pub fn apply(&mut self) -> impl FnMut(Event<'_>) -> Event<'_> { return |event| { match &event { Event::Text(text) => { if self.is_in { self.source.push_str(text); return Event::Text("".into()); } }, Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) => { self.lang = lang.to_string(); self.source = Default::default(); self.is_in = true; return Event::Text("".into()); }, Event::End(TagEnd::CodeBlock) => { self.is_in = false;
let html = highlight_codeblock(self.source);
return match html { Ok(out) => Event::Html(out.into()), Err(err)=> Event::Html(format!("<pre>Highlight Error: {:?}</pre>", err).into()), }; }, _ => () }
event } }}
컴파일러가 다음 비명을 지르며 에러와 해결책을 알려주었다.
error[E0700]: hidden type for `impl for<'a> FnMut(pulldown_cmark::Event<'a>) -> for<'a> pulldown_cmark::Event<'a>` captures lifetime that does not appear in bounds --> src/plugins.rs:61:9 |60 | pub fn apply(&mut self) -> impl FnMut(Event<'_>) -> Event<'_> { | --------- ---------------------------------- opaque type defined here | | | hidden type `{closure@src/plugins.rs:61:9: 61:16}` captures the anonymous lifetime defined here61 | return |event| { | ____________^62 | | match &event {63 | | Event::Text(text) => {64 | | if self.is_in {... |95 | | event96 | | } | |_____^ |help: add a `use<...>` bound to explicitly capture `'_` |60 | pub fn apply(&mut self) -> impl FnMut(Event<'_>) -> Event<'_> + use<'_> { | +++++++++
그래서 컴파일러의 명령대로 + use<'_>
를 추가했더니, 잘 작동하였다.
... } } pub fn apply(&mut self) -> impl FnMut(Event<'_>) -> Event<'_> + use<'_> { return |event| { match &event {...
Rust Lifetime Specifier 작동을 잘 몰라서 정확히는 모르겠지만, 아마 &mut self
가 추가되었기에 이를 다룰 것이 필요했던 모양.
Next
다음에는 이 포스팅에도 쓰고있는 Directive, plugin을 개발해보기로 한다.