use markdown_it::parser::inline::{InlineRule, InlineState}; use markdown_it::{Node, NodeValue, Renderer}; use maud::Render; use crate::components::mangle::{MangledBetaEmail, MangledGoogleEmail}; #[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: `color(value, "text")` let mdx = mdx .trim() .trim_start_matches('{') .trim_end_matches('}') .trim(); // Find the function name (everything before the opening parenthesis) let paren_pos = match mdx.find('(') { Some(x) => x, None => return false, }; if mdx[..paren_pos].trim() != "color" { return false; }; // Find matching closing parenthesis let skip = paren_pos + 1; let mut balance = 1; let mut end = skip; for i in mdx[skip..].bytes() { match i { b')' => balance -= 1, b'(' => balance += 1, _ => {} } if balance == 0 { break; } end += 1; } if balance != 0 { return false; } let args = mdx[skip..end].trim(); // Parse arguments: should be "value, text" or "value, \"text\"" let comma_pos = match args.find(',') { Some(x) => x, None => return false, }; let value = args[..comma_pos].trim(); let text = args[comma_pos + 1..].trim(); // Strip quotes from text if present let text = if (text.starts_with('"') && text.ends_with('"')) || (text.starts_with('\'') && text.ends_with('\'')) { &text[1..text.len() - 1] } else { text }; let mut style_str = String::new(); if value.starts_with("#") { style_str.push_str("color:"); style_str.push_str(value); style_str.push(';'); } else if value.starts_with("--") { style_str.push_str("color:var("); style_str.push_str(value); style_str.push_str(");"); } else { style_str.push_str("color:"); style_str.push_str(value); style_str.push(';'); } fmt.open("span", &[("style", style_str)]); fmt.text(text); fmt.close("span"); return true; } fn mdx_include(mdx: &str, _node: &Node, fmt: &mut dyn Renderer) -> bool { // Parse inside of mdx: `include()` let args = { let mdx = mdx .trim() .trim_start_matches('{') .trim_end_matches('}') .trim(); if !mdx.starts_with("include(") { return false; } let skip = 8; let mut balance = 1; let mut end = skip; for i in mdx[skip..].bytes() { match i { b')' => balance -= 1, b'(' => balance += 1, _ => {} } if balance == 0 { break; } end += 1; } if balance != 0 { return false; } let args = mdx[skip..end].trim(); let trail = mdx[end + 1..].trim(); if !trail.is_empty() { return false; } args }; let str = match args { "email_beta" => MangledBetaEmail {}.render().0, "email_goog" => MangledGoogleEmail {}.render().0, _ => return false, }; fmt.text_raw(&str); return true; }