skip to content
MeliPlug by doi, ywbird

Rust로 Markdown 사용하기 - Rust Static Blog

/ 7 min read

Markdown

사실 모두 알고 있으리라 생각하지만…

MarkdownDaring 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 렌더 플러그인

plugins.rs
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
}
}
}
}
main.rs
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
}
}
}

컴파일러가 다음 비명을 지르며 에러와 해결책을 알려주었다.

Terminal window
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 here
61 | return |event| {
| ____________^
62 | | match &event {
63 | | Event::Text(text) => {
64 | | if self.is_in {
... |
95 | | event
96 | | }
| |_____^
|
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을 개발해보기로 한다.