171 lines
3.1 KiB
Rust
171 lines
3.1 KiB
Rust
use emojis::Emoji;
|
|
use markdown_it::parser::inline::{InlineRule, InlineState};
|
|
use markdown_it::{Node, NodeValue, Renderer};
|
|
use maud::{Markup, PreEscaped, Render};
|
|
|
|
pub struct Markdown<'a>(pub &'a str);
|
|
|
|
impl Render for Markdown<'_> {
|
|
fn render(&self) -> Markup {
|
|
// TODO: init once
|
|
let md = &mut markdown_it::MarkdownIt::new();
|
|
markdown_it::plugins::cmark::add(md);
|
|
markdown_it::plugins::html::add(md);
|
|
md.inline.add_rule::<InlineEmote>();
|
|
md.inline.add_rule::<InlineMdx>();
|
|
|
|
let md = md.parse(&self.0);
|
|
let html = md.render();
|
|
|
|
return PreEscaped(html);
|
|
}
|
|
}
|
|
|
|
//
|
|
// MARK: extensions
|
|
//
|
|
|
|
#[derive(Debug)]
|
|
pub struct InlineEmote(&'static Emoji);
|
|
|
|
impl NodeValue for InlineEmote {
|
|
fn render(&self, _node: &Node, fmt: &mut dyn Renderer) {
|
|
fmt.text(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 emote = emojis::get_by_shortcode(&input[1..end_idx])?;
|
|
|
|
Some((Node::new(InlineEmote(emote)), end_idx + 1))
|
|
}
|
|
}
|
|
|
|
#[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;
|
|
}
|
|
|
|
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 {
|
|
let mdx = mdx.trim();
|
|
|
|
if !mdx.starts_with("style(") {
|
|
return false;
|
|
}
|
|
|
|
let skip = 6;
|
|
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 style = &mdx[skip..end].trim();
|
|
let content = &mdx[end + 1..].trim();
|
|
|
|
let mut style_str = String::new();
|
|
|
|
for kv in style.split(";") {
|
|
let mut s = kv.split(":");
|
|
let k = s.next();
|
|
let v = s.next();
|
|
|
|
if k.is_none() || v.is_none() || s.next().is_some() {
|
|
return false;
|
|
}
|
|
|
|
let k = k.unwrap().trim();
|
|
let v = v.unwrap().trim();
|
|
|
|
match k {
|
|
"color" => {
|
|
style_str.push_str("color:");
|
|
style_str.push_str(v);
|
|
style_str.push_str(";");
|
|
}
|
|
|
|
"color_var" => {
|
|
style_str.push_str("color:var(--");
|
|
style_str.push_str(v);
|
|
style_str.push_str(");");
|
|
}
|
|
|
|
_ => continue,
|
|
}
|
|
}
|
|
|
|
fmt.open("span", &[("style", style_str)]);
|
|
fmt.text(&content);
|
|
fmt.close("span");
|
|
|
|
return true;
|
|
}
|