at 8d154b4
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd, html}; pub(crate) fn extract_local_refs(md: &str) -> Vec<String> { let opts = Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH; let mut refs = Vec::new(); for event in Parser::new_ext(md, opts) { let url: Option<pulldown_cmark::CowStr> = match event { Event::Start(Tag::Image { dest_url, .. }) => Some(dest_url), Event::Start(Tag::Link { dest_url, .. }) => Some(dest_url), Event::End(TagEnd::Image | TagEnd::Link) => None, _ => None, }; if let Some(url) = url { let s = url.as_ref(); if !s.contains("://") && !s.starts_with('#') && !s.is_empty() { refs.push(s.to_owned()); } } } refs } pub(crate) fn render_markdown_html(md: &str) -> String { let opts = Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH; let parser = Parser::new_ext(md, opts); let mut buf = String::new(); html::push_html(&mut buf, parser); buf } pub(crate) fn render_markdown_gemtext(md: &str) -> String { let opts = Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH; let parser = Parser::new_ext(md, opts); let mut out: Vec<String> = Vec::new(); let mut buf = String::new(); let mut links: Vec<(String, String)> = Vec::new(); let mut last_blank = false; enum Ctx { Paragraph, BlockQuote, ListItem, Heading(u8), } let mut stack: Vec<Ctx> = Vec::new(); let mut in_link = false; let mut link_url = String::new(); let mut link_text = String::new(); let mut in_code = false; let mut code_kind = String::new(); let mut code_body = String::new(); fn flush_line( out: &mut Vec<String>, buf: &mut String, links: &mut Vec<(String, String)>, stack: &[Ctx], _last_blank: &mut bool, ) { let text = buf.trim(); if text.is_empty() && links.is_empty() { return; } if let Some(Ctx::Heading(lvl)) = stack.last() { let prefix = match lvl { 1 => "#", 2 => "##", _ => "###", }; if !text.is_empty() { out.push(format!("{prefix} {text}")); } for (url, t) in links.drain(..) { out.push(format!("=> {url} {t}")); } *_last_blank = false; } else if let Some(Ctx::BlockQuote) = stack.last() { if !text.is_empty() { for line in text.lines() { out.push(format!("> {line}")); } } for (url, t) in links.drain(..) { out.push(format!("=> {url} {t}")); } *_last_blank = false; } else if let Some(Ctx::ListItem) = stack.last() { if !text.is_empty() { out.push(format!("* {text}")); } for (url, t) in links.drain(..) { out.push(format!("=> {url} {t}")); } *_last_blank = false; } else { if !text.is_empty() { out.push(text.to_string()); } for (url, t) in links.drain(..) { out.push(format!("=> {url} {t}")); } *_last_blank = false; } buf.clear(); } for event in parser { match event { Event::Start(tag) => match tag { Tag::Heading { level, .. } => { flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank); let lvl = level as u8; stack.push(Ctx::Heading(if lvl > 3 { 3 } else { lvl })); } Tag::Paragraph => { stack.push(Ctx::Paragraph); } Tag::BlockQuote(_) => { flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank); stack.push(Ctx::BlockQuote); } Tag::CodeBlock(kind) => { flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank); in_code = true; code_kind = match kind { CodeBlockKind::Fenced(info) => info.to_string(), CodeBlockKind::Indented => String::new(), }; code_body.clear(); } Tag::List { .. } => {} Tag::Item => { flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank); stack.push(Ctx::ListItem); } Tag::Link { dest_url, .. } => { in_link = true; link_url = dest_url.to_string(); link_text.clear(); } Tag::Image { dest_url, .. } => { in_link = true; link_url = dest_url.to_string(); link_text.clear(); } _ => {} }, Event::End(tag) => match tag { TagEnd::Heading(_) => { flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank); stack.pop(); } TagEnd::Paragraph => { flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank); if !matches!(stack.last(), Some(Ctx::BlockQuote | Ctx::ListItem)) { out.push(String::new()); last_blank = true; } stack.pop(); } TagEnd::BlockQuote(_) => { flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank); out.push(String::new()); last_blank = true; stack.pop(); } TagEnd::CodeBlock => { if !code_body.is_empty() { out.push(format!("```{}", code_kind)); for line in code_body.trim().lines() { out.push(line.to_string()); } out.push("```".to_string()); out.push(String::new()); last_blank = true; } in_code = false; code_kind.clear(); code_body.clear(); } TagEnd::List(_) => { out.push(String::new()); last_blank = true; } TagEnd::Item => { flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank); stack.pop(); } TagEnd::Link => { let t = link_text.trim().to_string(); if in_link && !link_url.is_empty() && !t.is_empty() { links.push((link_url.clone(), t)); } in_link = false; link_url.clear(); link_text.clear(); } TagEnd::Image => { let t = if link_text.trim().is_empty() { "image".to_string() } else { link_text.trim().to_string() }; if in_link { out.push(format!("=> {} {}", link_url, t)); } in_link = false; link_url.clear(); link_text.clear(); } _ => {} }, Event::Text(text) => { if in_code { code_body.push_str(&text); } else if in_link { link_text.push_str(&text); } else { buf.push_str(&text); } } Event::Code(text) => { if in_link { link_text.push_str(&text); } else { buf.push_str(&text); } } Event::SoftBreak | Event::HardBreak => { if in_code { code_body.push('\n'); } else { buf.push(' '); } } _ => {} } } flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank); while out.last().is_some_and(|l| l.is_empty()) { out.pop(); } out.join("\n") + "\n" }