use serde_json::Value;
use crate::error::{MdocError, Result};
pub fn json_to_markdown(json_doc: &Value) -> Result<String> {
let blocks = resolve_content_array(json_doc)?;
let mut md = String::new();
for block in blocks {
let block_type = block["type"]
.as_str()
.unwrap_or("paragraph");
match block_type {
"heading" => render_heading(block, &mut md),
"paragraph" | "text" => render_paragraph(block, &mut md),
"code" => render_code(block, &mut md),
"list" => render_list(block, &mut md),
"quote" | "blockquote" => render_quote(block, &mut md),
"table" => render_table(block, &mut md),
"image" | "img" => render_image(block, &mut md),
"divider" | "hr" => render_divider(&mut md),
"html" | "raw" => render_raw_html(block, &mut md),
_ => {
if let Some(text) = block["text"].as_str() {
md.push_str(text);
md.push_str("\n\n");
}
}
}
}
Ok(md)
}
fn resolve_content_array(json_doc: &Value) -> Result<&Vec<Value>> {
if let Some(arr) = json_doc.as_array() {
return Ok(arr);
}
if let Some(arr) = json_doc["content"].as_array() {
return Ok(arr);
}
Err(MdocError::Json(
"JSON must be an array of content blocks, or an object with a \"content\" array."
.to_string(),
))
}
fn sequence_depth(seq: &str) -> usize {
seq.split('.').count()
}
fn render_heading(block: &Value, md: &mut String) {
let para_seq = block["paraSequence"].as_str();
let text = block["text"].as_str().unwrap_or("");
let level = if let Some(lvl) = block["level"].as_u64() {
lvl.min(6).max(1) as usize
} else if let Some(seq) = para_seq {
sequence_depth(seq).min(6).max(1)
} else {
1
};
let hashes = "#".repeat(level);
match para_seq {
Some(seq) => md.push_str(&format!("{} {} {}\n\n", hashes, seq, text)),
None => md.push_str(&format!("{} {}\n\n", hashes, text)),
}
}
fn render_paragraph(block: &Value, md: &mut String) {
let text = block["text"].as_str().unwrap_or("");
if !text.is_empty() {
md.push_str(text);
md.push_str("\n\n");
}
}
fn render_code(block: &Value, md: &mut String) {
let lang = block["language"].as_str().unwrap_or("");
let text = block["text"].as_str().unwrap_or("");
md.push_str(&format!("```{}\n{}\n```\n\n", lang, text));
}
fn render_list(block: &Value, md: &mut String) {
let ordered = block["ordered"].as_bool().unwrap_or(false);
if let Some(items) = block["items"].as_array() {
for (i, item) in items.iter().enumerate() {
let text = item.as_str().unwrap_or("");
if ordered {
md.push_str(&format!("{}. {}\n", i + 1, text));
} else {
md.push_str(&format!("- {}\n", text));
}
}
md.push('\n');
}
}
fn render_quote(block: &Value, md: &mut String) {
let text = block["text"].as_str().unwrap_or("");
for line in text.lines() {
md.push_str(&format!("> {}\n", line));
}
md.push('\n');
}
fn render_table(block: &Value, md: &mut String) {
let headers = match block["headers"].as_array() {
Some(h) => h,
None => return,
};
let rows = block["rows"].as_array();
let header_cells: Vec<&str> = headers
.iter()
.map(|h| h.as_str().unwrap_or(""))
.collect();
md.push_str(&format!("| {} |\n", header_cells.join(" | ")));
let sep: Vec<&str> = header_cells.iter().map(|_| "---").collect();
md.push_str(&format!("| {} |\n", sep.join(" | ")));
if let Some(rows) = rows {
for row in rows {
if let Some(cells) = row.as_array() {
let cell_strs: Vec<&str> = cells
.iter()
.map(|c| c.as_str().unwrap_or(""))
.collect();
md.push_str(&format!("| {} |\n", cell_strs.join(" | ")));
}
}
}
md.push('\n');
}
fn render_image(block: &Value, md: &mut String) {
let src = block["src"].as_str().unwrap_or("");
let alt = block["alt"].as_str().unwrap_or("");
md.push_str(&format!("\n\n", alt, src));
}
fn render_divider(md: &mut String) {
md.push_str("---\n\n");
}
fn render_raw_html(block: &Value, md: &mut String) {
let text = block["text"].as_str().unwrap_or("");
if !text.is_empty() {
md.push_str(text);
md.push_str("\n\n");
}
}