use lazy_static::lazy_static; use markdown_it::parser::block::{BlockRule, BlockState}; use markdown_it::parser::core::Root; use markdown_it::parser::inline::{InlineRule, InlineState}; use markdown_it::{MarkdownIt, Node, NodeValue, Renderer}; use maud::{Markup, PreEscaped, Render, html}; use page::{Page, PageMetadata}; use std::str::FromStr; use crate::components::fa::FAIcon; use crate::components::mangle::{MangledBetaEmail, MangledGoogleEmail}; use crate::components::misc::Backlinks; lazy_static! { static ref MdParser: MarkdownIt = { let mut md = markdown_it::MarkdownIt::new(); markdown_it::plugins::cmark::add(&mut md); markdown_it::plugins::html::add(&mut md); md.block.add_rule::().before_all(); md.block.add_rule::().before_all(); md.inline.add_rule::(); md.inline.add_rule::(); md.inline.add_rule::(); md }; } pub struct Markdown<'a>(pub &'a str); impl Render for Markdown<'_> { fn render(&self) -> Markup { let md = Self::parse(self.0); let html = md.render(); return PreEscaped(html); } } impl Markdown<'_> { pub fn parse(md_str: &str) -> Node { MdParser.parse(md_str) } } // // MARK: helpers // /// Try to read page metadata from a markdown file's frontmatter. /// - returns `none` if there is no frontmatter /// - returns an error if we fail to parse frontmatter pub fn meta_from_markdown(root_node: &Node) -> Result, toml::de::Error> { root_node .children .first() .and_then(|x| x.cast::()) .map(|x| toml::from_str::(&x.content)) .map_or(Ok(None), |v| v.map(Some)) } pub fn page_from_markdown(md: impl Into, default_image: Option) -> Page { let md: String = md.into(); let md = Markdown::parse(&md); let mut meta = meta_from_markdown(&md) .unwrap_or(Some(PageMetadata { title: "Invalid frontmatter!".into(), ..Default::default() })) .unwrap_or_default(); if meta.image.is_none() { meta.image = default_image } let html = PreEscaped(md.render()); Page { meta, generate_html: Box::new(move |page, _| { let html = html.clone(); Box::pin(async move { html! { @if let Some(slug) = &page.meta.slug { (Backlinks(&[("/", "home")], slug)) } (html) } }) }), ..Default::default() } } // // MARK: extensions // // // MARK: emote // #[derive(Debug)] pub struct InlineEmote(String); impl NodeValue for InlineEmote { fn render(&self, _node: &Node, fmt: &mut dyn Renderer) { fmt.text_raw(self.0.as_str()); } } impl InlineRule for InlineEmote { const MARKER: char = ':'; fn run(state: &mut InlineState<'_, '_>) -> Option<(Node, usize)> { let input = &state.src[state.pos..state.pos_max]; if !input.starts_with(':') { return None; } let end_idx = input[1..].find(':')? + 1; let code = &input[1..end_idx]; let mut emote = None; if emote.is_none() && let Some(code) = code.strip_prefix("fa-") { emote = FAIcon::from_str(code).ok().map(|x| x.render().0) } if emote.is_none() { emote = emojis::get_by_shortcode(code).map(|x| x.to_string()); } Some((Node::new(InlineEmote(emote?)), end_idx + 1)) } } // // MARK: mdx // #[derive(Debug)] pub struct InlineMdx(String); impl NodeValue for InlineMdx { fn render(&self, node: &Node, fmt: &mut dyn Renderer) { if mdx_style(&self.0, node, fmt) { return; } if mdx_include(&self.0, node, fmt) { return; } fmt.open("code", &[]); fmt.text(&self.0); fmt.close("code"); } } impl InlineRule for InlineMdx { const MARKER: char = '{'; fn run(state: &mut InlineState<'_, '_>) -> Option<(Node, usize)> { let input = &state.src[state.pos..state.pos_max]; if !input.starts_with('{') { return None; } let mut balance = 1; let mut end = 1; for i in input[1..].bytes() { match i { b'}' => balance -= 1, b'{' => balance += 1, _ => {} } if balance == 0 { break; } end += 1; } if balance != 0 { return None; } let content = &input[1..end]; Some((Node::new(InlineMdx(content.to_owned())), content.len() + 2)) } } fn mdx_style(mdx: &str, _node: &Node, fmt: &mut dyn Renderer) -> bool { // Parse inside of mdx: `style(