use crate::ast::{Alignment, ListItem, Node};
use std::{
cmp::max,
fmt::{self},
};
#[derive(Debug, Clone)]
pub struct WriterOptions {
pub strict: bool,
pub hard_break_spaces: bool,
pub indent_spaces: usize,
}
impl Default for WriterOptions {
fn default() -> Self {
Self {
strict: true,
hard_break_spaces: true,
indent_spaces: 4,
}
}
}
#[derive(Debug)]
pub struct CommonMarkWriter {
options: WriterOptions,
buffer: String,
indent_level: usize,
}
impl CommonMarkWriter {
pub fn new() -> Self {
Self::with_options(WriterOptions::default())
}
pub fn with_options(options: WriterOptions) -> Self {
Self {
options,
buffer: String::new(),
indent_level: 0,
}
}
pub fn write(&mut self, node: &Node) -> fmt::Result {
match node {
Node::Document(children) => self.write_document(children),
Node::Heading { level, content } => self.write_heading(*level, content),
Node::Paragraph(content) => self.write_paragraph(content),
Node::BlockQuote(content) => self.write_blockquote(content),
Node::CodeBlock { language, content } => self.write_code_block(language, content),
Node::UnorderedList(items) => self.write_unordered_list(items),
Node::OrderedList { start, items } => self.write_ordered_list(*start, items),
Node::ThematicBreak => self.write_thematic_break(),
Node::Table {
headers,
rows,
alignments,
} => self.write_table(headers, rows, alignments),
Node::Link {
url,
title,
content,
} => self.write_link(url, title, content),
Node::Image { url, title, alt } => self.write_image(url, title, alt),
Node::Emphasis(content) => self.write_emphasis(content),
Node::Strong(content) => self.write_strong(content),
Node::InlineCode(content) => self.write_inline_code(content),
Node::Text(content) => self.write_text(content),
Node::Html(content) => self.write_html(content),
Node::SoftBreak => self.write_soft_break(),
Node::HardBreak => self.write_hard_break(),
}
}
fn write_document(&mut self, children: &[Node]) -> fmt::Result {
for (i, child) in children.iter().enumerate() {
self.write(child)?;
if i < children.len() - 1 {
self.write_str("\n\n")?;
}
}
Ok(())
}
fn write_heading(&mut self, level: u8, content: &[Node]) -> fmt::Result {
if !(1..=6).contains(&level) {
return Err(fmt::Error);
}
for _ in 0..level {
self.write_char('#')?;
}
self.write_char(' ')?;
for (i, node) in content.iter().enumerate() {
self.write(node)?;
if i < content.len() - 1 && !matches!(node, Node::SoftBreak | Node::HardBreak) {
self.write_char(' ')?;
}
}
Ok(())
}
fn write_paragraph(&mut self, content: &[Node]) -> fmt::Result {
let mut prev_is_inline = false;
for (i, node) in content.iter().enumerate() {
let is_inline = self.is_inline_element(node);
if prev_is_inline
&& is_inline
&& i > 0
&& !matches!(node, Node::SoftBreak | Node::HardBreak)
{
} else if i > 0 {
self.write_char('\n')?;
for _ in 0..(self.indent_level * self.options.indent_spaces) {
self.write_char(' ')?;
}
}
self.write(node)?;
prev_is_inline = is_inline;
}
Ok(())
}
fn write_blockquote(&mut self, content: &[Node]) -> fmt::Result {
self.indent_level += 1;
for (i, node) in content.iter().enumerate() {
self.write_str("> ")?;
self.write(node)?;
if i < content.len() - 1 {
self.write_str("\n> \n")?;
}
}
self.indent_level -= 1;
Ok(())
}
fn write_code_block(&mut self, language: &Option<String>, content: &str) -> fmt::Result {
let mut max_backticks = 0;
let mut current = 0;
for c in content.chars() {
if c == '`' {
current += 1;
if current > max_backticks {
max_backticks = current;
}
} else {
current = 0;
}
}
let fence_len = max(max_backticks + 1, 3);
let fence = "`".repeat(fence_len);
self.write_str(&fence)?;
if let Some(lang) = language {
self.write_str(lang)?;
}
self.write_char('\n')?;
self.write_str(content)?;
if !content.ends_with('\n') {
self.write_char('\n')?;
}
self.write_str(&fence)?;
Ok(())
}
fn write_unordered_list(&mut self, items: &[ListItem]) -> fmt::Result {
for (i, item) in items.iter().enumerate() {
self.write_list_item(item, "- ")?;
if i < items.len() - 1 {
self.write_char('\n')?;
}
}
Ok(())
}
fn write_ordered_list(&mut self, start: u32, items: &[ListItem]) -> fmt::Result {
for (i, item) in items.iter().enumerate() {
let num = start as usize + i;
let prefix = format!("{}. ", num);
self.write_list_item(item, &prefix)?;
if i < items.len() - 1 {
self.write_char('\n')?;
}
}
Ok(())
}
fn write_list_item(&mut self, item: &ListItem, prefix: &str) -> fmt::Result {
for _ in 0..(self.indent_level * self.options.indent_spaces) {
self.write_char(' ')?;
}
self.write_str(prefix)?;
if item.is_task {
if item.task_completed {
self.write_str("[x] ")?;
} else {
self.write_str("[ ] ")?;
}
}
self.indent_level += 1;
let mut prev_is_inline = false;
for (i, node) in item.content.iter().enumerate() {
let is_inline = self.is_inline_element(node);
let is_list = matches!(node, Node::OrderedList { .. } | Node::UnorderedList(..));
if is_list {
if i > 0 {
self.write_char('\n')?;
}
self.write(node)?;
prev_is_inline = false;
continue;
}
if prev_is_inline && is_inline {
} else if i > 0 {
self.write_char('\n')?;
let prefix_length = prefix.len() + if item.is_task { 4 } else { 0 };
for _ in 0..(self.indent_level * self.options.indent_spaces) + prefix_length {
self.write_char(' ')?;
}
}
self.write(node)?;
prev_is_inline = is_inline;
}
self.indent_level -= 1;
Ok(())
}
fn write_thematic_break(&mut self) -> fmt::Result {
self.write_str("---")
}
fn check_no_newline(&self, node: &Node) -> fmt::Result {
if Self::node_contains_newline(node) {
return Err(fmt::Error);
}
Ok(())
}
fn node_contains_newline(node: &Node) -> bool {
match node {
Node::Text(s) | Node::InlineCode(s) | Node::Html(s) => s.contains('\n'),
Node::Emphasis(children) | Node::Strong(children) => {
children.iter().any(Self::node_contains_newline)
}
Node::Link { content, .. } => content.iter().any(Self::node_contains_newline),
Node::Image { alt, .. } => alt.contains('\n'),
_ => false,
}
}
fn write_table(
&mut self,
headers: &[Node],
rows: &[Vec<Node>],
alignments: &[Alignment],
) -> fmt::Result {
self.write_char('|')?;
for header in headers {
self.check_no_newline(header)?;
self.write_char(' ')?;
self.write(header)?;
self.write_str(" |")?;
}
self.write_char('\n')?;
self.write_char('|')?;
for alignment in alignments {
match alignment {
Alignment::None => self.write_str(" --- |")?,
Alignment::Left => self.write_str(" :--- |")?,
Alignment::Center => self.write_str(" :---: |")?,
Alignment::Right => self.write_str(" ---: |")?,
}
}
self.write_char('\n')?;
for row in rows {
self.write_char('|')?;
for cell in row {
self.check_no_newline(cell)?;
self.write_char(' ')?;
self.write(cell)?;
self.write_str(" |")?;
}
self.write_char('\n')?;
}
Ok(())
}
fn write_link(&mut self, url: &str, title: &Option<String>, content: &[Node]) -> fmt::Result {
for node in content {
self.check_no_newline(node)?;
}
self.write_char('[')?;
for node in content {
self.write(node)?;
}
self.write_str("](")?;
self.write_str(url)?;
if let Some(title_text) = title {
self.write_str(" \"")?;
self.write_str(title_text)?;
self.write_char('"')?;
}
self.write_char(')')
}
fn write_image(&mut self, url: &str, title: &Option<String>, alt: &str) -> fmt::Result {
self.check_no_newline(&Node::Text(alt.to_string()))?;
self.write_str("?;
self.write_str(url)?;
if let Some(title_text) = title {
self.write_str(" \"")?;
self.write_str(title_text)?;
self.write_char('"')?;
}
self.write_char(')')
}
fn write_emphasis(&mut self, content: &[Node]) -> fmt::Result {
for node in content {
self.check_no_newline(node)?;
}
self.write_char('*')?;
for node in content {
self.write(node)?;
}
self.write_char('*')
}
fn write_strong(&mut self, content: &[Node]) -> fmt::Result {
for node in content {
self.check_no_newline(node)?;
}
self.write_str("**")?;
for node in content {
self.write(node)?;
}
self.write_str("**")
}
fn write_inline_code(&mut self, content: &str) -> fmt::Result {
self.check_no_newline(&Node::InlineCode(content.to_string()))?;
self.write_char('`')?;
self.write_str(content)?;
self.write_char('`')
}
fn write_text(&mut self, content: &str) -> fmt::Result {
self.check_no_newline(&Node::Text(content.to_string()))?;
let escaped = content
.replace('\\', "\\\\")
.replace('*', "\\*")
.replace('_', "\\_")
.replace('[', "\\[")
.replace(']', "\\]")
.replace('<', "\\<")
.replace('>', "\\>")
.replace('`', "\\`");
self.write_str(&escaped)
}
fn write_html(&mut self, content: &str) -> fmt::Result {
self.write_str(content)
}
fn write_soft_break(&mut self) -> fmt::Result {
self.write_char('\n')
}
fn write_hard_break(&mut self) -> fmt::Result {
if self.options.hard_break_spaces {
self.write_str(" \n")
} else {
self.write_str("\\\n")
}
}
fn is_inline_element(&self, node: &Node) -> bool {
matches!(
node,
Node::Text(_)
| Node::Emphasis(_)
| Node::Strong(_)
| Node::InlineCode(_)
| Node::Link { .. }
| Node::Image { .. }
)
}
pub fn into_string(self) -> String {
self.buffer
}
fn write_char(&mut self, c: char) -> fmt::Result {
self.buffer.push(c);
Ok(())
}
fn write_str(&mut self, s: &str) -> fmt::Result {
self.buffer.push_str(s);
Ok(())
}
}
impl Default for CommonMarkWriter {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for Node {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut writer = CommonMarkWriter::new();
writer.write(self)?;
write!(f, "{}", writer.into_string())
}
}