use super::{
CLOSE, Ctx, Formatter, MAX_FORMAT_DEPTH, MAX_HEADING_LEVEL, NL, OL, OPEN, OrderedListMode, Out,
SPACE, UL, WRAPPING_TYPES,
};
use crate::ast::{Node, NodeType, Value};
use crate::render::js;
impl Formatter<'_> {
pub(super) fn node(&mut self, node: &Node<'_>, ctx: Ctx, out: &mut Out) {
if self.depth >= MAX_FORMAT_DEPTH {
return;
}
self.depth += 1;
self.node_inner(node, ctx, out);
self.depth -= 1;
}
fn node_inner(&mut self, node: &Node<'_>, ctx: Ctx, out: &mut Out) {
let no = child_ctx(node, ctx);
match node.node_type {
NodeType::Document => self.document(node, no, out),
NodeType::Heading => self.heading(node, ctx, no, out),
NodeType::Paragraph => self.paragraph(node, no, out),
NodeType::Inline => {
out.text(indent(ctx));
self.children(node, no, out);
}
NodeType::Image => self.image(node, no, out),
NodeType::Link => self.link(node, no, out),
NodeType::Text => self.text(node, ctx, no, out),
NodeType::Blockquote => self.blockquote(node, ctx, no, out),
NodeType::Hr => {
out.text(NL);
out.text(indent(ctx));
out.text("---");
out.text(NL);
}
NodeType::Fence => self.fence(node, ctx, out),
NodeType::Tag => self.tag(node, ctx, no, out),
NodeType::List => self.list(node, ctx, no, out),
NodeType::Item => self.item(node, no, out),
NodeType::Strong => self.wrapped(node, no, "**", out),
NodeType::Em => self.wrapped(node, no, "*", out),
NodeType::S => {
out.text("~~");
out.text(self.inline_text(node, no));
out.text("~~");
}
NodeType::Code => self.code(node, no, out),
NodeType::Hardbreak => {
out.text(format!("\\{NL}"));
out.text(indent(ctx));
}
NodeType::Softbreak => {
out.text(NL);
out.text(indent(ctx));
}
NodeType::Table => self.table(node, ctx, no, out),
NodeType::Thead => self.thead(node, no, out),
NodeType::Tr => self.tr(node, no, out),
NodeType::Td | NodeType::Th => self.cell(node, no, out),
NodeType::Tbody => self.children(node, no, out),
NodeType::Comment => self.comment(node, out),
NodeType::Error | NodeType::Node => {}
}
}
pub(super) fn children(&mut self, node: &Node<'_>, ctx: Ctx, out: &mut Out) {
for child in &node.children {
self.node(child, ctx, out);
}
}
pub(super) fn collect_children(&mut self, node: &Node<'_>, ctx: Ctx) -> Out {
let mut out = Out::default();
self.children(node, ctx, &mut out);
out
}
pub(super) fn inline_text(&mut self, node: &Node<'_>, ctx: Ctx) -> String {
self.collect_children(node, ctx).joined().trim().to_owned()
}
fn subtree(&mut self, node: &Node<'_>, ctx: Ctx) -> String {
let mut out = Out::default();
self.node(node, ctx, &mut out);
super::trim_start_owned(out.joined())
}
pub(super) fn text_of(&mut self, value: &Value) -> String {
match value {
Value::String(text) => text.clone(),
other => self.scalar(other),
}
}
fn document(&mut self, node: &Node<'_>, no: Ctx, out: &mut Out) {
if let Some(Value::String(frontmatter)) = node.get("frontmatter")
&& !frontmatter.is_empty()
{
out.text(format!("---{NL}{frontmatter}{NL}---{NL}{NL}"));
}
let children = self.collect_children(node, no).trim_start();
out.append(children);
}
fn heading(&mut self, node: &Node<'_>, ctx: Ctx, no: Ctx, out: &mut Out) {
out.text(NL);
out.text(indent(ctx));
out.text("#".repeat(heading_level(node)));
out.text(SPACE);
let children = self.collect_children(node, no).trim_start();
out.append(children);
self.annotations(node, out);
out.text(NL);
}
fn paragraph(&mut self, node: &Node<'_>, no: Ctx, out: &mut Out) {
out.text(NL);
self.children(node, no, out);
self.annotations(node, out);
out.text(NL);
}
fn blockquote(&mut self, node: &Node<'_>, ctx: Ctx, no: Ctx, out: &mut Out) {
let prefix = format!("{}>{SPACE}", indent(ctx));
let parts: Vec<String> = node
.children
.iter()
.map(|child| {
let formatted = self.subtree(child, no);
format!("{NL}{}", quote(&formatted, &prefix))
})
.collect();
out.text(parts.join(&prefix));
}
fn fence(&mut self, node: &Node<'_>, ctx: Ctx, out: &mut Out) {
let indent = indent(ctx);
let content = match node.get("content") {
Some(value) => self.text_of(value),
None => String::new(),
};
out.text(NL);
out.text(indent.clone());
let inner = longest_backtick_run(&content);
let boundary = "`".repeat(if inner > 0 { inner + 1 } else { 3 });
let needs_newline_before_close = !content.ends_with(NL);
out.text(boundary.clone());
if let Some(language) = node.get("language").filter(|value| value.is_truthy()) {
let language = self.text_of(language);
out.text(language);
}
if !node.annotations.is_empty() {
out.text(SPACE);
}
self.annotations(node, out);
out.text(NL);
out.text(indent.clone());
out.text(
content
.split('\n')
.collect::<Vec<&str>>()
.join(&format!("{NL}{indent}")),
);
if needs_newline_before_close {
out.text(NL);
out.text(indent.clone());
}
out.text(boundary);
out.text(NL);
}
fn tag(&mut self, node: &Node<'_>, ctx: Ctx, no: Ctx, out: &mut Out) {
let indent = indent(ctx);
if !node.inline {
out.text(NL);
out.text(indent.clone());
}
let open = format!("{OPEN}{SPACE}");
let name = node.tag.clone().unwrap_or_default();
let mut parts = vec![format!("{open}{name}")];
parts.extend(self.attributes(node));
let inline_tag = parts.join(SPACE);
let width = super::utf16_len(&inline_tag).saturating_add(super::utf16_len(&open) * 2);
let is_long = width > self.options.max_tag_opening_width;
let opening = if !node.inline && is_long {
parts.join(&format!(
"{NL}{}{indent}",
SPACE.repeat(super::utf16_len(&open))
))
} else {
inline_tag
};
let body = if node.children.is_empty() {
None
} else {
let body = self.collect_children(node, no);
if body.joined().trim().is_empty() {
None
} else {
Some(body)
}
};
let closer = if body.is_none() { "/" } else { "" };
out.text(format!("{opening}{SPACE}{closer}{CLOSE}"));
if let Some(body) = body {
out.append(body);
if !node.inline {
out.text(indent);
}
out.text(format!("{OPEN}{SPACE}/{name}{SPACE}{CLOSE}"));
}
if !node.inline {
out.text(NL);
}
}
fn item(&mut self, node: &Node<'_>, no: Ctx, out: &mut Out) {
for (index, child) in node.children.iter().enumerate() {
self.node(child, no, out);
if index == 0 {
self.annotations(node, out);
}
}
}
fn comment(&mut self, node: &Node<'_>, out: &mut Out) {
let content = match node.get("content") {
Some(value) => self.text_of(value),
None => String::new(),
};
let trailing = if node.inline { "" } else { NL };
out.text(format!("<!-- {content} -->{trailing}"));
}
fn list(&mut self, node: &Node<'_>, ctx: Ctx, no: Ctx, out: &mut Out) {
let indent = indent(ctx);
let is_loose = node.children.iter().any(|item| {
item.children
.iter()
.any(|child| child.node_type == NodeType::Paragraph)
});
let ordered = node.get("ordered").is_some_and(Value::is_truthy);
let marker = match node.get("marker") {
Some(Value::String(marker)) => Some(marker.clone()),
_ => None,
};
let start = match node.get("start") {
Some(Value::Number(start)) => *start,
_ => 1.0,
};
let last = node.children.len().saturating_sub(1);
for (index, item) in node.children.iter().enumerate() {
let prefix = if ordered {
let offset = f64::from(u32::try_from(index).unwrap_or(u32::MAX));
let number = match self.options.ordered_list_mode {
OrderedListMode::Increment => js::number(start.trunc() + offset),
OrderedListMode::Repeat if index == 0 => js::number(start),
OrderedListMode::Repeat => "1".to_owned(),
};
format!(
"{number}{}",
marker.clone().unwrap_or_else(|| OL.to_owned())
)
} else {
marker.clone().unwrap_or_else(|| UL.to_owned())
};
let width = super::utf16_len(&prefix).saturating_add(1);
let mut item = self.subtree(item, no.increment(width));
if !is_loose || index == last {
item = item.trim().to_owned();
}
out.text(format!("{NL}{indent}{prefix} {item}"));
}
out.text(NL);
}
}
fn quote(text: &str, prefix: &str) -> String {
if text.is_empty() {
return prefix.to_owned();
}
let mut out = String::with_capacity(text.len() + prefix.len());
for line in text.split_inclusive('\n') {
out.push_str(prefix);
out.push_str(line);
}
out
}
fn indent(ctx: Ctx) -> String {
SPACE.repeat(ctx.indent)
}
fn child_ctx(node: &Node<'_>, ctx: Ctx) -> Ctx {
Ctx {
indent: ctx.indent,
parent_wraps: WRAPPING_TYPES.contains(&node.node_type),
parent_is_table_tag: node.node_type == NodeType::Tag
&& node.tag.as_deref() == Some("table"),
}
}
fn heading_level(node: &Node<'_>) -> usize {
match node.get("level") {
Some(Value::Number(level)) if *level >= 1.0 => {
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "the guard proves the value is at least 1, and a float \
too large for `usize` saturates rather than wrapping"
)]
let level = level.trunc() as usize;
level.min(MAX_HEADING_LEVEL)
}
_ => 1,
}
}
fn longest_backtick_run(content: &str) -> usize {
let mut longest = 0;
let mut run = 0;
for character in content.chars() {
if character == '`' {
run += 1;
if run >= 3 {
longest = longest.max(run);
}
} else {
run = 0;
}
}
longest
}