MD upgrades

This commit is contained in:
2025-11-03 09:53:54 -08:00
parent 3205827f3f
commit 4ee5c16098
6 changed files with 633 additions and 165 deletions

View File

@@ -1,106 +1,170 @@
use markdown::{CompileOptions, Constructs, LineEnding, Options, ParseOptions};
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);
const OPTS: Options = Options {
parse: ParseOptions {
constructs: Constructs {
attention: true,
autolink: false,
block_quote: true,
character_escape: true,
character_reference: true,
code_indented: false,
code_fenced: true,
code_text: true,
definition: true,
frontmatter: false,
gfm_autolink_literal: true,
gfm_footnote_definition: false,
gfm_label_start_footnote: false,
gfm_strikethrough: false,
gfm_table: false,
gfm_task_list_item: false,
hard_break_escape: false,
hard_break_trailing: false,
heading_atx: true,
heading_setext: false,
label_start_image: false,
label_start_link: true,
label_end: true,
list_item: true,
math_flow: false,
math_text: false,
mdx_esm: false,
thematic_break: false,
// INLINE HTML
html_flow: false,
html_text: false,
// INLINE {}
mdx_expression_flow: true,
mdx_expression_text: true,
// INLINE HTML (alternative)
mdx_jsx_flow: false,
mdx_jsx_text: false,
},
gfm_strikethrough_single_tilde: false,
math_text_single_dollar: false,
mdx_expression_parse: None,
mdx_esm_parse: None,
},
compile: CompileOptions {
allow_any_img_src: false,
allow_dangerous_html: false,
allow_dangerous_protocol: false,
default_line_ending: LineEnding::LineFeed,
gfm_footnote_back_label: None,
gfm_footnote_clobber_prefix: None,
gfm_footnote_label_attributes: None,
gfm_footnote_label_tag_name: None,
gfm_footnote_label: None,
gfm_task_list_item_checkable: false,
gfm_tagfilter: false,
},
};
impl Render for Markdown<'_> {
fn render(&self) -> Markup {
/*
let mut ast = markdown::to_mdast(MD_A, &opts.parse).unwrap();
let walk = AstWalkMut::new(&mut ast);
// 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>();
for i in walk {
match i {
AstWalkMutStep::Exit(node) => {
match node {
Node::MdxTextExpression(x) => {
println!("{x:?}");
}
Node::MdxFlowExpression(x) => {
println!("{x:?}");
}
_ => continue,
}
let md = md.parse(&self.0);
let html = md.render();
*node = Node::Text(Text {
value: "LOL".to_owned(),
position: node.position().cloned(),
})
}
_ => {}
}
}
println!("{ast:?}");
*/
let md = markdown::to_html_with_options(self.0, &OPTS).unwrap();
return PreEscaped(md);
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;
}