use crate::ast::{Block, ListKind, TableAlignment, TableData};
pub fn render_markdown(root: &Block) -> String {
let mut out = String::new();
render_block(root, &mut out, 0, false);
let trimmed = out.trim_end();
if trimmed.is_empty() {
String::new()
} else {
format!("{}\n", trimmed)
}
}
fn render_block(block: &Block, out: &mut String, depth: usize, in_list_item: bool) {
match block {
Block::Document { children } => {
let mut first = true;
for child in children {
if !first {
if !out.ends_with("\n\n") && !out.ends_with("\n>\n") {
out.push('\n');
}
}
first = false;
render_block(child, out, depth, false);
}
}
Block::Paragraph { raw } => {
if in_list_item && !out.ends_with('\n') && !out.is_empty() {
}
out.push_str(raw);
out.push('\n');
}
Block::Heading { level, raw } => {
for _ in 0..*level {
out.push('#');
}
out.push(' ');
out.push_str(raw);
out.push('\n');
}
Block::ThematicBreak => {
out.push_str("---\n");
}
Block::CodeBlock { info, literal } => {
out.push_str("```");
if !info.is_empty() {
out.push_str(info.as_str());
}
out.push('\n');
out.push_str(literal);
if !literal.ends_with('\n') {
out.push('\n');
}
out.push_str("```\n");
}
Block::BlockQuote { children } => {
for child in children {
let mut child_out = String::new();
render_block(child, &mut child_out, depth + 1, false);
for line in child_out.lines() {
out.push_str("> ");
out.push_str(line);
out.push('\n');
}
}
}
Block::List {
kind,
start,
tight,
children,
} => {
let mut num = *start;
for (i, child) in children.iter().enumerate() {
if !*tight && i > 0 {
out.push('\n');
}
match kind {
ListKind::Bullet(marker) => {
out.push(*marker as char);
out.push(' ');
}
ListKind::Ordered(delimiter) => {
out.push_str(&num.to_string());
out.push(*delimiter as char);
out.push(' ');
num += 1;
}
}
render_list_item_content(child, out, depth + 1, *tight);
}
}
Block::ListItem { children, checked } => {
if let Some(c) = *checked {
out.push_str(if c { "[x] " } else { "[ ] " });
}
for (i, child) in children.iter().enumerate() {
if i > 0 {
out.push_str(" ");
}
render_block(child, out, depth, i == 0);
}
}
Block::HtmlBlock { literal } => {
out.push_str(literal);
if !literal.ends_with('\n') {
out.push('\n');
}
}
Block::Table(table_data) => {
render_table(table_data, out);
}
}
}
fn render_list_item_content(block: &Block, out: &mut String, depth: usize, tight: bool) {
if let Block::ListItem { children, checked } = block {
if let Some(c) = *checked {
out.push_str(if c { "[x] " } else { "[ ] " });
}
for (i, child) in children.iter().enumerate() {
if i > 0 {
for _ in 0..depth {
out.push_str(" ");
}
}
render_block(child, out, depth, i == 0);
if !tight && i < children.len() - 1 {
out.push('\n');
}
}
} else {
render_block(block, out, depth, true);
}
}
fn render_table(table: &TableData, out: &mut String) {
let num_cols = table.num_cols;
if num_cols == 0 {
return;
}
out.push('|');
for (i, cell) in table.header.iter().enumerate() {
out.push(' ');
out.push_str(cell.as_str());
out.push_str(" |");
if i >= num_cols - 1 {
break;
}
}
out.push('\n');
out.push('|');
for i in 0..num_cols {
let alignment = table
.alignments
.get(i)
.copied()
.unwrap_or(TableAlignment::None);
match alignment {
TableAlignment::None => out.push_str(" --- |"),
TableAlignment::Left => out.push_str(" :-- |"),
TableAlignment::Center => out.push_str(" :-: |"),
TableAlignment::Right => out.push_str(" --: |"),
}
}
out.push('\n');
let num_rows = table.rows.len() / num_cols;
for row_idx in 0..num_rows {
out.push('|');
for col_idx in 0..num_cols {
let cell_idx = row_idx * num_cols + col_idx;
if let Some(cell) = table.rows.get(cell_idx) {
out.push(' ');
out.push_str(cell.as_str());
out.push_str(" |");
} else {
out.push_str(" |");
}
}
out.push('\n');
}
}