use crate::model::{BlockKind, Document};
pub fn to_text(doc: &Document) -> String {
doc.pages
.iter()
.filter(|p| !p.text.trim().is_empty())
.map(|p| p.text.as_str())
.collect::<Vec<_>>()
.join("\n\n")
}
fn table_md(rows: &[Vec<String>]) -> String {
if rows.is_empty() {
return String::new();
}
let width = rows.iter().map(|r| r.len()).max().unwrap_or(0);
let norm: Vec<Vec<String>> = rows
.iter()
.map(|r| {
let mut v = r.clone();
while v.len() < width {
v.push(String::new());
}
v
})
.collect();
let cell = |r: &[String]| format!("| {} |", r.join(" | "));
let mut out = vec![cell(&norm[0])];
out.push(format!("| {} |", vec!["---"; width].join(" | ")));
for r in &norm[1..] {
out.push(cell(r));
}
out.join("\n")
}
fn block_md(b: &crate::model::Block) -> String {
match b.kind {
BlockKind::Heading => {
let lvl = (b.level.unwrap_or(1) as usize).clamp(1, 6);
format!("{} {}", "#".repeat(lvl), b.content)
}
BlockKind::List => {
let prefix = if b.ordered.unwrap_or(false) {
"1."
} else {
"-"
};
b.content
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| format!("{prefix} {l}"))
.collect::<Vec<_>>()
.join("\n")
}
BlockKind::Code => format!(
"```{}\n{}\n```",
b.lang.clone().unwrap_or_default(),
b.content
),
BlockKind::Table => b
.table
.as_ref()
.map(|t| table_md(&t.rows))
.unwrap_or_default(),
BlockKind::Quote => b
.content
.lines()
.map(|l| format!("> {l}"))
.collect::<Vec<_>>()
.join("\n"),
_ => b.content.clone(),
}
}
pub fn to_markdown(doc: &Document) -> String {
let mut out: Vec<String> = Vec::new();
for page in &doc.pages {
let mut prev_list: Option<bool> = None;
for b in page
.blocks
.iter()
.filter(|b| !b.content.is_empty() || b.table.is_some())
{
let rendered = block_md(b);
if rendered.is_empty() {
continue;
}
if b.kind == BlockKind::List {
let ordered = b.ordered.unwrap_or(false);
if prev_list == Some(ordered) {
if let Some(last) = out.last_mut() {
last.push('\n');
last.push_str(&rendered);
}
continue;
}
prev_list = Some(ordered);
} else {
prev_list = None;
}
out.push(rendered);
}
}
out.join("\n\n")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Block, BlockKind, Document, Page};
fn list(ordered: bool, content: &str) -> Block {
Block {
kind: BlockKind::List,
content: content.to_string(),
ordered: Some(ordered),
..Default::default()
}
}
#[test]
fn adjacent_lists_merge_same_kind_only() {
let doc = Document {
pages: vec![Page {
blocks: vec![
Block {
kind: BlockKind::Heading,
content: "T".into(),
level: Some(2),
..Default::default()
},
list(false, "a"),
list(false, "b"),
list(true, "x"),
],
..Default::default()
}],
..Default::default()
};
let md = to_markdown(&doc);
assert_eq!(md, "## T\n\n- a\n- b\n\n1. x");
}
#[test]
fn table_rendered_as_markdown() {
let doc = Document {
pages: vec![Page {
blocks: vec![Block {
kind: BlockKind::Table,
table: Some(crate::model::TableData {
rows: vec![vec!["a".into(), "b".into()], vec!["1".into(), "2".into()]],
}),
..Default::default()
}],
..Default::default()
}],
..Default::default()
};
assert_eq!(to_markdown(&doc), "| a | b |\n| --- | --- |\n| 1 | 2 |");
}
}