use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use crate::ast::{
Alert, AlertKind, Block, BlockQuote, CodeBlock, ContainerDirective, DescriptionList, Heading,
LeafDirective, List, ListItem, MathBlock, Paragraph,
};
use super::escape::{attr_escape, escape_text};
use super::inlines::{directive_attrs, render_inlines, render_raw_html};
use super::refs::flatten_alt;
use super::tables::render_table;
use super::{Ctx, TasklistAttrOrder};
pub fn render_blocks_joined(blocks: &[Block], ctx: &Ctx) -> String {
let mut parts: Vec<String> = Vec::new();
for block in blocks {
let rendered = render_block(block, ctx);
if !rendered.is_empty() {
parts.push(rendered);
}
}
parts.join("\n")
}
pub fn render_block(block: &Block, ctx: &Ctx) -> String {
match block {
Block::Paragraph(p) => render_paragraph(p, ctx),
Block::Heading(h) => render_heading(h, ctx),
Block::ThematicBreak(_) => String::from("<hr />"),
Block::BlockQuote(bq) => render_blockquote(bq, ctx),
Block::Alert(a) => render_alert(a, ctx),
Block::List(list) => render_list(list, ctx),
Block::DescriptionList(dl) => render_description_list(dl, ctx),
Block::CodeBlock(cb) => render_code_block(cb),
Block::HtmlBlock(hb) => render_raw_html(&hb.value, ctx),
Block::Definition(_) => String::new(),
Block::FootnoteDefinition(_) => String::new(),
Block::Table(t) => render_table(t, ctx),
Block::MathBlock(mb) => render_math_block(mb, ctx),
Block::Frontmatter(_) => String::new(),
Block::MdxEsm(_) => String::new(),
Block::MdxExpression(_) => String::new(),
Block::MdxJsx(_) => String::new(),
Block::LeafDirective(d) => render_leaf_directive(d, ctx),
Block::ContainerDirective(d) => render_container_directive(d, ctx),
}
}
fn render_paragraph(p: &Paragraph, ctx: &Ctx) -> String {
format!("<p>{}</p>", render_inlines(&p.children, ctx))
}
fn render_heading(h: &Heading, ctx: &Ctx) -> String {
let depth = h.depth.clamp(1, 6);
format!("<h{depth}>{}</h{depth}>", render_inlines(&h.children, ctx))
}
fn render_blockquote(bq: &BlockQuote, ctx: &Ctx) -> String {
let inner = render_blocks_joined(&bq.children, ctx);
if inner.is_empty() {
String::from("<blockquote>\n</blockquote>")
} else {
format!("<blockquote>\n{inner}\n</blockquote>")
}
}
fn alert_default_title(kind: AlertKind) -> &'static str {
match kind {
AlertKind::Note => "Note",
AlertKind::Tip => "Tip",
AlertKind::Important => "Important",
AlertKind::Warning => "Warning",
AlertKind::Caution => "Caution",
}
}
fn alert_class_suffix(kind: AlertKind) -> &'static str {
match kind {
AlertKind::Note => "note",
AlertKind::Tip => "tip",
AlertKind::Important => "important",
AlertKind::Warning => "warning",
AlertKind::Caution => "caution",
}
}
fn render_alert(a: &Alert, ctx: &Ctx) -> String {
let suffix = alert_class_suffix(a.kind);
let title = match a.title.as_deref() {
Some(t) => escape_text(t),
None => String::from(alert_default_title(a.kind)),
};
let inner = render_blocks_joined(&a.children, ctx);
let body = if inner.is_empty() {
String::new()
} else {
format!("\n{inner}")
};
format!(
"<div class=\"markdown-alert markdown-alert-{suffix}\">\n<p class=\"markdown-alert-title\">{title}</p>{body}\n</div>",
)
}
fn ordered_start_attr(list: &List) -> String {
match list.start {
Some(n) if n != 1 => format!(" start=\"{n}\""),
_ => String::new(),
}
}
fn render_list(list: &List, ctx: &Ctx) -> String {
let items = render_list_items(list, ctx);
if list.ordered {
format!("<ol{}>\n{items}\n</ol>", ordered_start_attr(list))
} else {
format!("<ul>\n{items}\n</ul>")
}
}
fn render_list_items(list: &List, ctx: &Ctx) -> String {
let mut parts: Vec<String> = Vec::with_capacity(list.children.len());
for item in &list.children {
parts.push(render_list_item(item, list.tight, ctx));
}
parts.join("\n")
}
fn render_list_item(item: &ListItem, tight: bool, ctx: &Ctx) -> String {
let checkbox = item
.checked
.map(|checked| task_checkbox(checked, ctx.tasklist_checkable, ctx.tasklist_attr_order));
if tight {
render_tight_item(item, checkbox, ctx)
} else {
render_loose_item(item, checkbox, ctx)
}
}
fn render_tight_item(item: &ListItem, checkbox: Option<String>, ctx: &Ctx) -> String {
let mut parts: Vec<(String, bool)> = Vec::new();
let mut checkbox = checkbox;
for child in &item.children {
let (rendered, is_paragraph) = match child {
Block::Paragraph(p) => {
let mut inner = render_inlines(&p.children, ctx);
if let Some(cb) = checkbox.take() {
inner = format!("{cb} {inner}");
}
(inner, true)
}
other => (render_block(other, ctx), false),
};
if !rendered.is_empty() {
parts.push((rendered, is_paragraph));
}
}
if let Some(cb) = checkbox.take() {
parts.insert(0, (cb, true));
}
if parts.is_empty() {
return String::from("<li></li>");
}
let begins_with_block = !parts.first().map(|(_, p)| *p).unwrap_or(true);
let ends_with_block = !parts.last().map(|(_, p)| *p).unwrap_or(true);
let body = parts
.into_iter()
.map(|(s, _)| s)
.collect::<Vec<_>>()
.join("\n");
let lead = if begins_with_block { "\n" } else { "" };
let trail = if ends_with_block { "\n" } else { "" };
format!("<li>{lead}{body}{trail}</li>")
}
fn render_loose_item(item: &ListItem, checkbox: Option<String>, ctx: &Ctx) -> String {
let mut parts: Vec<String> = Vec::new();
let mut checkbox = checkbox;
for child in &item.children {
let rendered = match child {
Block::Paragraph(p) if checkbox.is_some() => {
let cb = checkbox.take().unwrap();
format!("<p>{cb} {}</p>", render_inlines(&p.children, ctx))
}
other => render_block(other, ctx),
};
if !rendered.is_empty() {
parts.push(rendered);
}
}
if let Some(cb) = checkbox.take() {
parts.insert(0, cb);
}
let inner = parts.join("\n");
if inner.is_empty() {
String::from("<li>\n</li>")
} else {
format!("<li>\n{inner}\n</li>")
}
}
fn task_checkbox(checked: bool, checkable: bool, attr_order: TasklistAttrOrder) -> String {
let checked_attr = if checked { " checked=\"\"" } else { "" };
if checkable {
return format!("<input type=\"checkbox\"{checked_attr} />");
}
match attr_order {
TasklistAttrOrder::CheckedFirst => {
format!("<input type=\"checkbox\"{checked_attr} disabled=\"\" />")
}
TasklistAttrOrder::DisabledFirst => {
format!("<input type=\"checkbox\" disabled=\"\"{checked_attr} />")
}
}
}
fn render_description_list(dl: &DescriptionList, ctx: &Ctx) -> String {
let mut parts: Vec<String> = Vec::new();
for item in &dl.children {
parts.push(format!("<dt>{}</dt>", render_inlines(&item.term, ctx)));
for details in &item.details {
parts.push(render_description_details(&details.children, dl.tight, ctx));
}
}
format!("<dl>\n{}\n</dl>", parts.join("\n"))
}
fn render_description_details(children: &[Block], tight: bool, ctx: &Ctx) -> String {
if tight {
let mut parts: Vec<String> = Vec::new();
for child in children {
let rendered = match child {
Block::Paragraph(p) => render_inlines(&p.children, ctx),
other => render_block(other, ctx),
};
if !rendered.is_empty() {
parts.push(rendered);
}
}
format!("<dd>{}</dd>", parts.join("\n"))
} else {
let inner = render_blocks_joined(children, ctx);
if inner.is_empty() {
String::from("<dd>\n</dd>")
} else {
format!("<dd>\n{inner}\n</dd>")
}
}
}
fn code_body(value: &str) -> String {
if value.is_empty() {
String::new()
} else {
let mut s = escape_text(value);
if !value.ends_with('\n') && !value.ends_with('\r') {
s.push_str(preferred_code_line_ending(value));
}
s
}
}
fn preferred_code_line_ending(value: &str) -> &str {
let bytes = value.as_bytes();
let mut cursor = 0;
while cursor < bytes.len() {
match bytes[cursor] {
b'\r' if bytes.get(cursor + 1) == Some(&b'\n') => return "\r\n",
b'\r' => return "\r",
b'\n' => return "\n",
_ => cursor += 1,
}
}
"\n"
}
fn info_first_token(info: Option<&str>) -> Option<&str> {
let token = info?.split(|c: char| c.is_ascii_whitespace()).next()?;
if token.is_empty() {
None
} else {
Some(token)
}
}
fn render_code_block(cb: &CodeBlock) -> String {
let body = code_body(&cb.value);
match info_first_token(cb.info.as_deref()) {
Some(token) => {
let lang = escape_text(token);
let math_attr = if token == "math" {
" data-math-style=\"display\""
} else {
""
};
format!("<pre><code class=\"language-{lang}\"{math_attr}>{body}</code></pre>")
}
None => format!("<pre><code>{body}</code></pre>"),
}
}
fn render_math_block(mb: &MathBlock, _ctx: &Ctx) -> String {
let body = code_body(&mb.value);
format!("<pre><code class=\"language-math\" data-math-style=\"display\">{body}</code></pre>")
}
fn render_leaf_directive(d: &LeafDirective, ctx: &Ctx) -> String {
let attrs = directive_attrs(&d.attributes);
format!(
"<div class=\"directive directive-leaf\" data-directive-name=\"{}\"{attrs}>{}</div>",
attr_escape(&d.name),
render_inlines(&d.label, ctx)
)
}
fn render_container_directive(d: &ContainerDirective, ctx: &Ctx) -> String {
let mut attrs = directive_attrs(&d.attributes);
if !d.label.is_empty() {
attrs.push_str(&format!(
" data-directive-label=\"{}\"",
attr_escape(&flatten_alt(&d.label))
));
}
let inner = render_blocks_joined(&d.children, ctx);
let body = if inner.is_empty() {
String::new()
} else {
format!("\n{inner}")
};
format!(
"<div class=\"{}\"{attrs}>{body}\n</div>",
attr_escape(&d.name)
)
}