use crate::extract::{Block, Doc};
pub fn render_body(doc: &Doc) -> String {
let mut s = String::new();
for b in &doc.blocks {
match b {
Block::Heading(lv, t) => {
s.push_str(&"#".repeat(*lv as usize));
s.push(' ');
s.push_str(t);
}
Block::Para(t) => s.push_str(t),
Block::Item(depth, t) => {
s.push_str(&" ".repeat(*depth as usize));
s.push_str("- ");
s.push_str(t);
}
Block::Quote(t) => {
s.push_str("> ");
s.push_str(t);
}
Block::Code(t) => {
s.push_str("```\n");
s.push_str(t);
s.push_str("\n```");
}
Block::Link(href, t) => {
s.push_str("@ ");
s.push_str(href);
s.push_str(" | ");
s.push_str(t);
}
Block::Img(src, alt) => {
s.push_str("! ");
s.push_str(src);
if !alt.is_empty() {
s.push_str(" | ");
s.push_str(alt);
}
}
Block::Table { headers, rows } => {
render_table(&mut s, headers, rows);
}
}
s.push('\n');
}
s
}
fn render_table(s: &mut String, headers: &[String], rows: &[Vec<String>]) {
let n = rows.len();
if headers.is_empty() {
s.push_str(&format!("table[{n}]:\n"));
} else {
let hdr = headers.join(",");
s.push_str(&format!("table[{n}]{{{hdr}}}:\n"));
}
for row in rows {
s.push_str(&escape_row(row));
s.push('\n');
}
}
fn escape_row(cells: &[String]) -> String {
cells
.iter()
.map(|c| {
if c.contains(',') || c.contains('"') {
let escaped = c.replace('"', "\"\"");
format!("\"{escaped}\"")
} else {
c.clone()
}
})
.collect::<Vec<_>>()
.join(",")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::extract::Block;
fn body(blocks: Vec<Block>) -> String {
render_body(&Doc {
title: String::new(),
url: String::new(),
lang: String::new(),
desc: String::new(),
blocks,
})
}
fn s(x: &str) -> String {
x.to_string()
}
#[test]
fn heading_levels() {
assert_eq!(body(vec![Block::Heading(1, s("Top"))]), "# Top\n");
assert_eq!(body(vec![Block::Heading(3, s("Sub"))]), "### Sub\n");
}
#[test]
fn paragraph_and_quote() {
assert_eq!(body(vec![Block::Para(s("hello"))]), "hello\n");
assert_eq!(body(vec![Block::Quote(s("cited"))]), "> cited\n");
}
#[test]
fn list_items_indent_by_depth() {
let out = body(vec![
Block::Item(0, s("a")),
Block::Item(1, s("b")),
Block::Item(2, s("c")),
]);
assert_eq!(out, "- a\n - b\n - c\n");
}
#[test]
fn code_is_fenced() {
assert_eq!(
body(vec![Block::Code(s("let x = 1;"))]),
"```\nlet x = 1;\n```\n"
);
}
#[test]
fn link_format() {
assert_eq!(
body(vec![Block::Link(s("https://x.com"), s("label"))]),
"@ https://x.com | label\n"
);
}
#[test]
fn img_with_and_without_alt() {
assert_eq!(
body(vec![Block::Img(s("https://x/a.png"), s("alt"))]),
"! https://x/a.png | alt\n"
);
assert_eq!(
body(vec![Block::Img(s("https://x/a.png"), s(""))]),
"! https://x/a.png\n"
);
}
#[test]
fn table_with_headers() {
let t = Block::Table {
headers: vec![s("name"), s("price")],
rows: vec![vec![s("a"), s("100")], vec![s("b"), s("200")]],
};
assert_eq!(body(vec![t]), "table[2]{name,price}:\na,100\nb,200\n\n");
}
#[test]
fn table_without_headers() {
let t = Block::Table {
headers: vec![],
rows: vec![vec![s("x")], vec![s("y")]],
};
assert_eq!(body(vec![t]), "table[2]:\nx\ny\n\n");
}
#[test]
fn cell_with_comma_is_quoted() {
let t = Block::Table {
headers: vec![],
rows: vec![vec![s("a,b"), s("c")]],
};
assert_eq!(body(vec![t]), "table[1]:\n\"a,b\",c\n\n");
}
#[test]
fn cell_with_quote_is_escaped() {
let t = Block::Table {
headers: vec![],
rows: vec![vec![s(r#"say "hi""#)]],
};
assert_eq!(body(vec![t]), "table[1]:\n\"say \"\"hi\"\"\"\n\n");
}
}