| @@ -53,6 +53,12 @@ pub(crate) fn render_markdown_gemtext(md: &str) -> String { |
| let mut code_kind = String::new(); |
| let mut code_body = String::new(); |
| |
| + fn in_ctx(stack: &[Ctx], target: Ctx) -> bool { |
| + stack |
| + .iter() |
| + .any(|c| std::mem::discriminant(c) == std::mem::discriminant(&target)) |
| + } |
| + |
| fn flush_line( |
| out: &mut Vec<String>, |
| buf: &mut String, |
| @@ -65,6 +71,9 @@ pub(crate) fn render_markdown_gemtext(md: &str) -> String { |
| return; |
| } |
| |
| + let in_blockquote = in_ctx(stack, Ctx::BlockQuote); |
| + let in_list = in_ctx(stack, Ctx::ListItem); |
| + |
| if let Some(Ctx::Heading(lvl)) = stack.last() { |
| let prefix = match lvl { |
| 1 => "#", |
| @@ -78,7 +87,7 @@ pub(crate) fn render_markdown_gemtext(md: &str) -> String { |
| out.push(format!("=> {url} {t}")); |
| } |
| *_last_blank = false; |
| - } else if let Some(Ctx::BlockQuote) = stack.last() { |
| + } else if in_blockquote { |
| if !text.is_empty() { |
| for line in text.lines() { |
| out.push(format!("> {line}")); |
| @@ -88,7 +97,7 @@ pub(crate) fn render_markdown_gemtext(md: &str) -> String { |
| out.push(format!("=> {url} {t}")); |
| } |
| *_last_blank = false; |
| - } else if let Some(Ctx::ListItem) = stack.last() { |
| + } else if in_list { |
| if !text.is_empty() { |
| out.push(format!("* {text}")); |
| } |
| @@ -156,7 +165,7 @@ pub(crate) fn render_markdown_gemtext(md: &str) -> String { |
| } |
| TagEnd::Paragraph => { |
| flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank); |
| - if !matches!(stack.last(), Some(Ctx::BlockQuote | Ctx::ListItem)) { |
| + if !in_ctx(&stack, Ctx::BlockQuote) && !in_ctx(&stack, Ctx::ListItem) { |
| out.push(String::new()); |
| last_blank = true; |
| } |
| @@ -249,3 +258,147 @@ pub(crate) fn render_markdown_gemtext(md: &str) -> String { |
| |
| out.join("\n") + "\n" |
| } |
| + |
| +#[cfg(test)] |
| +mod tests { |
| + use super::*; |
| + |
| + #[test] |
| + fn test_extract_local_refs_none() { |
| + let md = "# Hello\n\nJust text, no links.\n"; |
| + assert!(extract_local_refs(md).is_empty()); |
| + } |
| + |
| + #[test] |
| + fn test_extract_local_refs_absolute_url() { |
| + let md = "[foo](https://example.com)"; |
| + assert!(extract_local_refs(md).is_empty()); |
| + } |
| + |
| + #[test] |
| + fn test_extract_local_refs_relative() { |
| + let md = "[foo](/bar/baz)"; |
| + assert_eq!(extract_local_refs(md), vec!["/bar/baz"]); |
| + } |
| + |
| + #[test] |
| + fn test_extract_local_refs_fragment() { |
| + let md = "[foo](#section)"; |
| + assert!(extract_local_refs(md).is_empty()); |
| + } |
| + |
| + #[test] |
| + fn test_render_markdown_html_basic() { |
| + let md = "# Hello\n\nThis is **bold**."; |
| + let html = render_markdown_html(md); |
| + assert!(html.contains("<h1>Hello</h1>")); |
| + assert!(html.contains("<strong>bold</strong>")); |
| + } |
| + |
| + #[test] |
| + fn test_render_markdown_html_table() { |
| + let md = "| A | B |\n|---|---|\n| 1 | 2 |"; |
| + let html = render_markdown_html(md); |
| + assert!(html.contains("<table>")); |
| + } |
| + |
| + #[test] |
| + fn test_render_markdown_gemtext_heading() { |
| + let md = "# Title\n\n## Subtitle\n\n### H3\n\n#### H4 (clamped)"; |
| + let gmi = render_markdown_gemtext(md); |
| + assert_eq!(gmi.lines().next(), Some("# Title")); |
| + assert!(gmi.contains("## Subtitle")); |
| + assert!(gmi.contains("### H3")); |
| + assert!(gmi.contains("### H4 (clamped)")); |
| + } |
| + |
| + #[test] |
| + fn test_render_markdown_gemtext_paragraph() { |
| + let md = "Hello, world!"; |
| + let gmi = render_markdown_gemtext(md); |
| + assert!(gmi.starts_with("Hello, world!")); |
| + } |
| + |
| + #[test] |
| + fn test_render_markdown_gemtext_link() { |
| + let md = "[example](https://example.com)"; |
| + let gmi = render_markdown_gemtext(md); |
| + assert!(gmi.contains("=> https://example.com example")); |
| + } |
| + |
| + #[test] |
| + fn test_render_markdown_gemtext_image() { |
| + let md = ""; |
| + let gmi = render_markdown_gemtext(md); |
| + assert!(gmi.contains("=> img.png alt")); |
| + } |
| + |
| + #[test] |
| + fn test_render_markdown_gemtext_image_no_alt() { |
| + let md = ""; |
| + let gmi = render_markdown_gemtext(md); |
| + assert!(gmi.contains("=> img.png image")); |
| + } |
| + |
| + #[test] |
| + fn test_render_markdown_gemtext_blockquote() { |
| + let md = "> A wise quote."; |
| + let gmi = render_markdown_gemtext(md); |
| + assert!(gmi.contains("> A wise quote.")); |
| + } |
| + |
| + #[test] |
| + fn test_render_markdown_gemtext_list() { |
| + let md = "- one\n- two\n- three"; |
| + let gmi = render_markdown_gemtext(md); |
| + assert!(gmi.contains("* one")); |
| + assert!(gmi.contains("* two")); |
| + assert!(gmi.contains("* three")); |
| + } |
| + |
| + #[test] |
| + fn test_render_markdown_gemtext_code_block() { |
| + let md = "```rust\nfn main() {}\n```"; |
| + let gmi = render_markdown_gemtext(md); |
| + assert!(gmi.contains("```rust")); |
| + assert!(gmi.contains("fn main() {}")); |
| + assert!(gmi.contains("```")); |
| + } |
| + |
| + #[test] |
| + fn test_render_markdown_gemtext_empty_code_block() { |
| + let md = "```\n```"; |
| + let gmi = render_markdown_gemtext(md); |
| + // Empty code blocks are silently dropped. |
| + assert!(!gmi.contains("```")); |
| + } |
| + |
| + #[test] |
| + fn test_render_markdown_gemtext_trailing_newline() { |
| + let md = "# Hi"; |
| + let gmi = render_markdown_gemtext(md); |
| + assert!(gmi.ends_with('\n')); |
| + } |
| + |
| + #[test] |
| + fn test_render_markdown_gemtext_inline_code() { |
| + let md = "use `foo` here"; |
| + let gmi = render_markdown_gemtext(md); |
| + assert!(gmi.contains("foo")); |
| + } |
| + |
| + #[test] |
| + fn test_render_markdown_gemtext_soft_break() { |
| + let md = "line1\nline2"; |
| + let gmi = render_markdown_gemtext(md); |
| + assert!(gmi.contains("line1 line2") || gmi.contains("line1\nline2")); |
| + } |
| + |
| + #[test] |
| + fn test_render_markdown_gemtext_config_option() { |
| + // Ensure ENABLE_TABLES and ENABLE_STRIKETHROUGH are set |
| + let md = "~~strike~~"; |
| + let gmi = render_markdown_gemtext(md); |
| + assert!(gmi.contains("strike")); |
| + } |
| +} |