*UNDER CONSTRUCTION*

Rust로 Markdown 사용하기 - Rust Blog

created:
<a>

Markdown

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

Markdown[↗]Daring Fireball[↗] 을 운영하고있는 John Gruber가 만든 markup 언어이다.
https://daringfireball.net/projects/markdown/[↗]

Markdown은 ‘원문을 읽기 쉽도록’ 만들어졌다.

:h[디테일한 설명은 질리도록 들어왔으리라 생각한다…]{.sm}

Rust Markdown?

사실 markdown은 웹에서 엄청나게 많이 쓰이기에 JS로 이루어진 compiler가 많지만, Rust로 작성된것도 꽤 있다.

두 crate를 모두 사용해본 결과, mardown-rs는 간단한 compile에는 사용할 수 있지만, :h[md]{.font-mono.red}:h[ast]{.font-mono}(Markdown Abstract Syntax Tree)를 지원함에도 불구, 훨신 복잡한 plugin 개발 등에는 적합하지 않다고 판단했다.

pulldown-cmark

오히려 mdast를 지원하지 않는 pulldown-cmark가 더 많은 cusomization을 할 수 있었다.

pulldown-cmark는 CommonMark를 기본적으로 지원하고, 추가 기능을 켜고 끌 수 있다.
pulldown-cmark는 기본적으로 꽤 많은 추가 기능을 지원한다.

  • gfm
  • heading attributes
  • yaml metadata
  • math
  • and more…

:::note Every Options
pulldown_cmark - Rust::Options[↗] :::


간단하게는, 아래와 같이 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 변수가 추가된다는 뜻이다.

그래서, 하나의 플러그인을 하나의 객체로 만들기로 했다.

:::note 나도 Rust 초보이기 때문에, 이게 맞는 접근인지 잘 모른다는 점… :::

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을 사용해보았다.

:::note syntax highlight에는 syntect라는 크레이트를 사용해보았다.
추후 가능하면 tree-sitter-highlight로 바꿔보고싶. :::

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 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을 개발해보기로 한다.

:::note directive는 이거다. :::

Webmentions(?)

Q. Why isn't my webmention not appearing?

A. I deploy this site not so often and this site is static site. So don't expect your webmention to appear instantly. Thanks for the webmention and sorry for the inconvenience.
last deployed: see footer