use crate::ast::Node;
use crate::mdnorm::{self, NfBlock, NfInline, NfItem};
pub fn markdown(nodes: &[Node]) -> String {
let blocks = mdnorm::from_ast(nodes);
let mut out = String::new();
render_blocks(&blocks, 0, &mut out);
out.trim_end().to_string()
}
fn render_blocks(blocks: &[NfBlock], indent: usize, out: &mut String) {
let mut flip_list = false;
let mut prev_list_ordered: Option<bool> = None;
for (i, b) in blocks.iter().enumerate() {
if i > 0 {
out.push('\n'); }
if let NfBlock::List { ordered, .. } = b {
flip_list = prev_list_ordered == Some(*ordered) && !flip_list;
prev_list_ordered = Some(*ordered);
} else {
prev_list_ordered = None;
flip_list = false;
}
render_block_markers(b, indent, flip_list, out);
}
}
fn render_block_markers(b: &NfBlock, indent: usize, flip_list: bool, out: &mut String) {
let pad = " ".repeat(indent);
match b {
NfBlock::Heading(level, inl) => {
out.push_str(&pad);
for _ in 0..*level {
out.push('#');
}
out.push(' ');
render_inlines(inl, Ctx::Heading, out);
out.push('\n');
}
NfBlock::Para(inl) => {
out.push_str(&pad);
render_inlines(inl, Ctx::LineStart, out);
out.push('\n');
}
NfBlock::List { ordered, items } => {
for item in items {
render_item(*ordered, flip_list, item, indent, out);
}
}
NfBlock::Code { info, text } => {
let fence_len = 3.max(longest_backtick_run(text) + 1);
let fence: String = "`".repeat(fence_len);
out.push_str(&pad);
out.push_str(&fence);
out.push_str(info);
out.push('\n');
for line in text.lines() {
out.push_str(&pad);
out.push_str(line);
out.push('\n');
}
out.push_str(&pad);
out.push_str(&fence);
out.push('\n');
}
NfBlock::Table { rows } => {
let cols = rows.iter().map(|r| r.len()).max().unwrap_or(0);
for (ri, row) in rows.iter().enumerate() {
out.push_str(&pad);
out.push('|');
for ci in 0..cols {
out.push(' ');
if let Some(cell) = row.get(ci) {
render_inlines(cell, Ctx::TableCell, out);
}
out.push_str(" |");
}
out.push('\n');
if ri == 0 {
out.push_str(&pad);
out.push('|');
for _ in 0..cols {
out.push_str(" --- |");
}
out.push('\n');
}
}
}
}
}
fn render_item(ordered: bool, flip: bool, item: &NfItem, indent: usize, out: &mut String) {
let pad = " ".repeat(indent);
let marker = match (ordered, flip) {
(true, false) => "1. ",
(true, true) => "1) ",
(false, false) => "- ",
(false, true) => "* ",
};
out.push_str(&pad);
out.push_str(marker);
render_inlines(&item.content, Ctx::ListItemStart, out);
out.push('\n');
let mut sub_flip = false;
let mut prev_ordered: Option<bool> = None;
for (si, sub) in item.sublists.iter().enumerate() {
if let NfBlock::List {
ordered,
items: sub_items,
} = sub
{
if si == 0
&& !item.content.is_empty()
&& sub_items.first().is_some_and(|it| it.content.is_empty())
{
out.push('\n');
}
sub_flip = prev_ordered == Some(*ordered) && !sub_flip;
prev_ordered = Some(*ordered);
}
render_block_markers(sub, indent + marker.len(), sub_flip, out);
}
}
#[derive(Clone, Copy, PartialEq)]
enum Ctx {
LineStart,
Heading,
ListItemStart,
TableCell,
}
fn render_inlines(inlines: &[NfInline], ctx: Ctx, out: &mut String) {
for (idx, inl) in inlines.iter().enumerate() {
let at_start = idx == 0 && ctx != Ctx::TableCell;
match inl {
NfInline::Run { text, bold, italic } => {
if *bold {
out.push_str("**");
}
if *italic {
out.push('*');
}
push_escaped_text(text, at_start && !*bold && !*italic, ctx, out);
if *italic {
out.push('*');
}
if *bold {
out.push_str("**");
}
}
NfInline::Link { href, label } => {
if out.ends_with('!') {
out.pop();
out.push_str("\\!");
}
out.push('[');
render_inlines(label, Ctx::TableCell, out); out.push_str("](");
push_href(href, out);
out.push(')');
}
}
}
}
fn push_escaped_text(text: &str, at_line_start: bool, ctx: Ctx, out: &mut String) {
for (i, ch) in text.char_indices() {
match ch {
'\\' | '*' | '_' | '[' | ']' | '`' => {
out.push('\\');
out.push(ch);
}
'<' => out.push_str("<"),
'&' => out.push_str("&"),
'|' => out.push_str("\\|"),
'#' if ctx == Ctx::Heading => {
out.push('\\');
out.push(ch);
}
'#' | '>' | '-' | '+' | '=' | '~' if at_line_start && i == 0 => {
out.push('\\');
out.push(ch);
}
'.' | ')' if at_line_start && leading_digits(text, i) => {
out.push('\\');
out.push(ch);
}
_ => out.push(ch),
}
}
}
fn leading_digits(text: &str, i: usize) -> bool {
i > 0 && text[..i].bytes().all(|b| b.is_ascii_digit())
}
fn push_href(href: &str, out: &mut String) {
let needs_angle = href.contains([' ', '(', ')']);
if needs_angle {
out.push('<');
}
out.push_str(href);
if needs_angle {
out.push('>');
}
}
fn longest_backtick_run(text: &str) -> usize {
let mut max = 0;
let mut cur = 0;
for ch in text.chars() {
if ch == '`' {
cur += 1;
max = max.max(cur);
} else {
cur = 0;
}
}
max
}