cmark_writer/
writer.rs

1//! CommonMark writer implementation.
2//!
3//! This module provides functionality to convert AST nodes to CommonMark format text.
4//! The main component is the CommonMarkWriter class, which serializes AST nodes to CommonMark-compliant text.
5
6use crate::ast::{Alignment, BlockNode, HtmlElement, InlineNode, ListItem, Node};
7use std::{
8    cmp::max,
9    fmt::{self},
10};
11
12/// CommonMark formatting options
13#[derive(Debug, Clone)]
14pub struct WriterOptions {
15    /// Whether to enable strict mode (strictly following CommonMark specification)
16    pub strict: bool,
17    /// Hard break mode (true uses two spaces followed by a newline, false uses backslash followed by a newline)
18    pub hard_break_spaces: bool,
19    /// Number of spaces to use for indentation levels
20    pub indent_spaces: usize,
21}
22
23impl Default for WriterOptions {
24    fn default() -> Self {
25        Self {
26            strict: true,
27            hard_break_spaces: false,
28            indent_spaces: 4,
29        }
30    }
31}
32
33/// CommonMark writer
34///
35/// This struct is responsible for serializing AST nodes to CommonMark-compliant text.
36#[derive(Debug)]
37pub struct CommonMarkWriter {
38    options: WriterOptions,
39    buffer: String,
40    /// Current indentation level
41    indent_level: usize,
42}
43
44impl CommonMarkWriter {
45    /// Create a new CommonMark writer with default options
46    ///
47    /// # Example
48    ///
49    /// ```
50    /// use cmark_writer::writer::CommonMarkWriter;
51    /// use cmark_writer::ast::{Node, InlineNode};
52    ///
53    /// let mut writer = CommonMarkWriter::new();
54    /// writer.write(&Node::Inline(InlineNode::Text("Hello".to_string()))).unwrap();
55    /// assert_eq!(writer.into_string(), "Hello");
56    /// ```
57    pub fn new() -> Self {
58        Self::with_options(WriterOptions::default())
59    }
60
61    /// Create a new CommonMark writer with specified options
62    ///
63    /// # Parameters
64    ///
65    /// * `options` - Custom CommonMark formatting options
66    ///
67    /// # Example
68    ///
69    /// ```
70    /// use cmark_writer::writer::{CommonMarkWriter, WriterOptions};
71    ///
72    /// let options = WriterOptions {
73    ///     strict: true,
74    ///     hard_break_spaces: false,  // Use backslash for line breaks
75    ///     indent_spaces: 2,          // Use 2 spaces for indentation
76    /// };
77    /// let writer = CommonMarkWriter::with_options(options);
78    /// ```
79    pub fn with_options(options: WriterOptions) -> Self {
80        Self {
81            options,
82            buffer: String::new(),
83            indent_level: 0,
84        }
85    }
86
87    /// Write an AST node as CommonMark format
88    ///
89    /// # Parameters
90    ///
91    /// * `node` - The AST node to write
92    ///
93    /// # Returns
94    ///
95    /// If writing succeeds, returns `Ok(())`, otherwise returns `Err(fmt::Error)`
96    ///
97    /// # Example
98    ///
99    /// ```
100    /// use cmark_writer::writer::CommonMarkWriter;
101    /// use cmark_writer::ast::{Node, InlineNode};
102    ///
103    /// let mut writer = CommonMarkWriter::new();
104    /// writer.write(&Node::Inline(InlineNode::Text("Hello".to_string()))).unwrap();
105    /// ```
106    pub fn write(&mut self, node: &Node) -> fmt::Result {
107        match node {
108            Node::Block(block_node) => self.write_block(block_node),
109            Node::Inline(inline_node) => self.write_inline(inline_node),
110        }
111    }
112
113    /// Write a block node as CommonMark format
114    fn write_block(&mut self, node: &BlockNode) -> fmt::Result {
115        match node {
116            BlockNode::Document(children) => self.write_document(children),
117            BlockNode::Heading { level, content } => self.write_heading(*level, content),
118            BlockNode::Paragraph(content) => self.write_paragraph(content),
119            BlockNode::BlockQuote(content) => self.write_blockquote(content),
120            BlockNode::CodeBlock { language, content } => self.write_code_block(language, content),
121            BlockNode::UnorderedList(items) => self.write_unordered_list(items),
122            BlockNode::OrderedList { start, items } => self.write_ordered_list(*start, items),
123            BlockNode::ThematicBreak => self.write_thematic_break(),
124            BlockNode::Table {
125                headers,
126                rows,
127                alignments,
128            } => self.write_table(headers, rows, alignments),
129            BlockNode::HtmlBlock(content) => self.write_html_block(content),
130        }
131    }
132
133    /// Write an inline node as CommonMark format
134    fn write_inline(&mut self, node: &InlineNode) -> fmt::Result {
135        match node {
136            InlineNode::Text(content) => self.write_text(content),
137            InlineNode::Emphasis(content) => self.write_emphasis(content),
138            InlineNode::Strong(content) => self.write_strong(content),
139            InlineNode::Strike(content) => self.write_strike(content),
140            InlineNode::InlineCode(content) => self.write_inline_code(content),
141            InlineNode::Link {
142                url,
143                title,
144                content,
145            } => self.write_link(url, title, content),
146            InlineNode::Image { url, title, alt } => self.write_image(url, title, alt),
147            InlineNode::HtmlElement(element) => self.write_html_element(element),
148            InlineNode::InlineContainer(content) => self.write_inline_container(content),
149            InlineNode::SoftBreak => self.write_soft_break(),
150            InlineNode::HardBreak => self.write_hard_break(),
151        }
152    }
153
154    /// Write a document node
155    fn write_document(&mut self, children: &[BlockNode]) -> fmt::Result {
156        for (i, child) in children.iter().enumerate() {
157            self.write_block(child)?;
158            if i < children.len() - 1 {
159                self.write_str("\n\n")?;
160            }
161        }
162        Ok(())
163    }
164
165    /// Write a heading node
166    fn write_heading(&mut self, level: u8, content: &[InlineNode]) -> fmt::Result {
167        if !(1..=6).contains(&level) {
168            return Err(fmt::Error);
169        }
170
171        for _ in 0..level {
172            self.write_char('#')?;
173        }
174        self.write_char(' ')?;
175
176        for (i, node) in content.iter().enumerate() {
177            self.write_inline(node)?;
178            if i < content.len() - 1
179                && !matches!(node, InlineNode::SoftBreak | InlineNode::HardBreak)
180            {
181                self.write_char(' ')?;
182            }
183        }
184
185        Ok(())
186    }
187
188    /// Write a paragraph node
189    fn write_paragraph(&mut self, content: &[InlineNode]) -> fmt::Result {
190        let mut prev_is_inline = false;
191
192        for (i, node) in content.iter().enumerate() {
193            // Check if the current node is an inline element that should be kept inline
194            let is_inline = !matches!(node, InlineNode::SoftBreak | InlineNode::HardBreak);
195
196            // If both current and previous elements should be kept inline, and it's not the first element
197            if prev_is_inline && is_inline && i > 0 {
198                // Don't add extra whitespace to prevent incorrect line breaks
199            } else if i > 0 {
200                // Non-consecutive inline elements, add normal line break and indentation
201                self.write_char('\n')?;
202                // Add appropriate indentation (current indent level)
203                for _ in 0..(self.indent_level * self.options.indent_spaces) {
204                    self.write_char(' ')?;
205                }
206            }
207
208            self.write_inline(node)?;
209            prev_is_inline = is_inline;
210        }
211        Ok(())
212    }
213
214    /// Write a blockquote node
215    fn write_blockquote(&mut self, content: &[BlockNode]) -> fmt::Result {
216        self.indent_level += 1;
217
218        for (i, node) in content.iter().enumerate() {
219            self.write_str("> ")?;
220            self.write_block(node)?;
221            if i < content.len() - 1 {
222                self.write_str("\n> \n")?;
223            }
224        }
225
226        self.indent_level -= 1;
227        Ok(())
228    }
229
230    /// Write a code block node
231    fn write_code_block(&mut self, language: &Option<String>, content: &str) -> fmt::Result {
232        let mut max_backticks = 0;
233        let mut current = 0;
234        for c in content.chars() {
235            if c == '`' {
236                current += 1;
237                if current > max_backticks {
238                    max_backticks = current;
239                }
240            } else {
241                current = 0;
242            }
243        }
244        let fence_len = max(max_backticks + 1, 3);
245        let fence = "`".repeat(fence_len);
246
247        self.write_str(&fence)?;
248        if let Some(lang) = language {
249            self.write_str(lang)?;
250        }
251        self.write_char('\n')?;
252        self.write_str(content)?;
253
254        // Ensure content ends with a newline
255        if !content.ends_with('\n') {
256            self.write_char('\n')?;
257        }
258
259        self.write_str(&fence)?;
260        Ok(())
261    }
262
263    /// Write an unordered list node
264    fn write_unordered_list(&mut self, items: &[ListItem]) -> fmt::Result {
265        for (i, item) in items.iter().enumerate() {
266            self.write_list_item(item, "- ")?;
267            if i < items.len() - 1 {
268                self.write_char('\n')?;
269            }
270        }
271        Ok(())
272    }
273
274    /// Write an ordered list node
275    fn write_ordered_list(&mut self, start: u32, items: &[ListItem]) -> fmt::Result {
276        for (i, item) in items.iter().enumerate() {
277            let num = start as usize + i;
278            let prefix = format!("{}. ", num);
279            self.write_list_item(item, &prefix)?;
280            if i < items.len() - 1 {
281                self.write_char('\n')?;
282            }
283        }
284        Ok(())
285    }
286
287    /// Write a list item
288    fn write_list_item(&mut self, item: &ListItem, prefix: &str) -> fmt::Result {
289        // Apply indentation based on current level
290        for _ in 0..(self.indent_level * self.options.indent_spaces) {
291            self.write_char(' ')?;
292        }
293        self.write_str(prefix)?;
294
295        match item {
296            ListItem::Regular { content } => {
297                self.write_list_item_content(content, prefix, false)?;
298            }
299            ListItem::Task { completed, content } => {
300                // Write task checkbox
301                if *completed {
302                    self.write_str("[x] ")?;
303                } else {
304                    self.write_str("[ ] ")?;
305                }
306                self.write_list_item_content(content, prefix, true)?;
307            }
308        }
309
310        Ok(())
311    }
312
313    /// Write list item content
314    fn write_list_item_content(
315        &mut self,
316        content: &[BlockNode],
317        prefix: &str,
318        is_task: bool,
319    ) -> fmt::Result {
320        self.indent_level += 1;
321
322        for (i, node) in content.iter().enumerate() {
323            let is_list = matches!(
324                node,
325                BlockNode::OrderedList { .. } | BlockNode::UnorderedList(..)
326            );
327
328            // Nested lists need special line break handling
329            if is_list {
330                if i > 0 {
331                    self.write_char('\n')?;
332                }
333                self.write_block(node)?;
334                continue;
335            }
336
337            if i > 0 {
338                self.write_char('\n')?;
339                // Add appropriate indentation (list item prefix length + current indent level)
340                let prefix_length = prefix.len() + if is_task { 4 } else { 0 };
341                for _ in 0..(self.indent_level * self.options.indent_spaces) + prefix_length {
342                    self.write_char(' ')?;
343                }
344            }
345
346            self.write_block(node)?;
347        }
348
349        self.indent_level -= 1;
350        Ok(())
351    }
352
353    /// Write a thematic break (horizontal rule)
354    fn write_thematic_break(&mut self) -> fmt::Result {
355        self.write_str("---")
356    }
357
358    /// Check if the inline node contains a newline character and return an error if it does
359    fn check_no_newline(&self, node: &InlineNode) -> fmt::Result {
360        if Self::inline_node_contains_newline(node) {
361            return Err(fmt::Error);
362        }
363        Ok(())
364    }
365
366    /// Check if the inline node contains a newline character recursively
367    fn inline_node_contains_newline(node: &InlineNode) -> bool {
368        match node {
369            InlineNode::Text(s) | InlineNode::InlineCode(s) => s.contains('\n'),
370            InlineNode::Emphasis(children)
371            | InlineNode::Strong(children)
372            | InlineNode::Strike(children)
373            | InlineNode::InlineContainer(children) => {
374                children.iter().any(Self::inline_node_contains_newline)
375            }
376            InlineNode::HtmlElement(element) => element
377                .children
378                .iter()
379                .any(Self::inline_node_contains_newline),
380            InlineNode::Link { content, .. } => {
381                content.iter().any(Self::inline_node_contains_newline)
382            }
383            InlineNode::Image { alt, .. } => alt.contains('\n'),
384            InlineNode::SoftBreak | InlineNode::HardBreak => true,
385        }
386    }
387
388    /// Write a table
389    fn write_table(
390        &mut self,
391        headers: &[InlineNode],
392        rows: &[Vec<InlineNode>],
393        alignments: &[Alignment],
394    ) -> fmt::Result {
395        // Write header
396        self.write_char('|')?;
397        for header in headers {
398            self.check_no_newline(header)?;
399            self.write_char(' ')?;
400            self.write_inline(header)?;
401            self.write_str(" |")?;
402        }
403        self.write_char('\n')?;
404
405        // Write alignment row
406        self.write_char('|')?;
407        for alignment in alignments {
408            match alignment {
409                Alignment::None => self.write_str(" --- |")?,
410                Alignment::Left => self.write_str(" :--- |")?,
411                Alignment::Center => self.write_str(" :---: |")?,
412                Alignment::Right => self.write_str(" ---: |")?,
413            }
414        }
415        self.write_char('\n')?;
416
417        // Write table content
418        for row in rows {
419            self.write_char('|')?;
420            for cell in row {
421                self.check_no_newline(cell)?;
422                self.write_char(' ')?;
423                self.write_inline(cell)?;
424                self.write_str(" |")?;
425            }
426            self.write_char('\n')?;
427        }
428
429        Ok(())
430    }
431
432    /// Write a link
433    fn write_link(
434        &mut self,
435        url: &str,
436        title: &Option<String>,
437        content: &[InlineNode],
438    ) -> fmt::Result {
439        for node in content {
440            self.check_no_newline(node)?;
441        }
442        self.write_char('[')?;
443
444        for node in content {
445            self.write_inline(node)?;
446        }
447
448        self.write_str("](")?;
449        self.write_str(url)?;
450
451        if let Some(title_text) = title {
452            self.write_str(" \"")?;
453            self.write_str(title_text)?;
454            self.write_char('"')?;
455        }
456
457        self.write_char(')')
458    }
459
460    /// Write an image
461    fn write_image(&mut self, url: &str, title: &Option<String>, alt: &str) -> fmt::Result {
462        if alt.contains('\n') {
463            return Err(fmt::Error);
464        }
465
466        self.write_str("![")?;
467        self.write_str(alt)?;
468        self.write_str("](")?;
469        self.write_str(url)?;
470
471        if let Some(title_text) = title {
472            self.write_str(" \"")?;
473            self.write_str(title_text)?;
474            self.write_char('"')?;
475        }
476
477        self.write_char(')')
478    }
479
480    /// Write emphasis (italic)
481    fn write_emphasis(&mut self, content: &[InlineNode]) -> fmt::Result {
482        for node in content {
483            self.check_no_newline(node)?;
484        }
485        self.write_char('*')?;
486
487        for node in content {
488            self.write_inline(node)?;
489        }
490
491        self.write_char('*')
492    }
493
494    /// Write strong emphasis (bold)
495    fn write_strong(&mut self, content: &[InlineNode]) -> fmt::Result {
496        for node in content {
497            self.check_no_newline(node)?;
498        }
499        self.write_str("**")?;
500
501        for node in content {
502            self.write_inline(node)?;
503        }
504
505        self.write_str("**")
506    }
507
508    /// Write a strikethrough text
509    fn write_strike(&mut self, content: &[InlineNode]) -> fmt::Result {
510        for node in content {
511            self.check_no_newline(node)?;
512        }
513        self.write_str("~~")?;
514
515        for node in content {
516            self.write_inline(node)?;
517        }
518
519        self.write_str("~~")
520    }
521
522    /// Write inline code
523    fn write_inline_code(&mut self, content: &str) -> fmt::Result {
524        if content.contains('\n') {
525            return Err(fmt::Error);
526        }
527
528        self.write_char('`')?;
529        self.write_str(content)?;
530        self.write_char('`')
531    }
532
533    /// Write plain text
534    fn write_text(&mut self, content: &str) -> fmt::Result {
535        if content.contains('\n') {
536            return Err(fmt::Error);
537        }
538
539        // Escape special characters
540        let escaped = content
541            .replace('\\', "\\\\")
542            .replace('*', "\\*")
543            .replace('_', "\\_")
544            .replace('[', "\\[")
545            .replace(']', "\\]")
546            .replace('<', "\\<")
547            .replace('>', "\\>")
548            .replace('`', "\\`");
549
550        self.write_str(&escaped)
551    }
552
553    /// Write an HTML element with attributes and children
554    fn write_html_element(&mut self, element: &HtmlElement) -> fmt::Result {
555        self.write_char('<')?;
556        self.write_str(&element.tag)?;
557
558        // Write attributes
559        for attr in &element.attributes {
560            self.write_char(' ')?;
561            self.write_str(&attr.name)?;
562            self.write_str("=\"")?;
563            // Escape quotes in attribute values
564            let escaped_value = attr.value.replace('"', "&quot;");
565            self.write_str(&escaped_value)?;
566            self.write_char('"')?;
567        }
568
569        if element.self_closing {
570            // Self-closing tag like <img />
571            self.write_str(" />")?;
572            return Ok(());
573        }
574
575        self.write_char('>')?;
576
577        // Process children
578        for child in &element.children {
579            self.check_no_newline(child)?;
580            self.write_inline(child)?;
581        }
582
583        // Close tag
584        self.write_str("</")?;
585        self.write_str(&element.tag)?;
586        self.write_char('>')?;
587
588        Ok(())
589    }
590
591    /// Write HTML block
592    fn write_html_block(&mut self, content: &str) -> fmt::Result {
593        self.write_str(content)
594    }
595
596    /// Write inline container
597    fn write_inline_container(&mut self, children: &[InlineNode]) -> fmt::Result {
598        for (i, child) in children.iter().enumerate() {
599            self.check_no_newline(child)?;
600            self.write_inline(child)?;
601            if i < children.len() - 1 {
602                self.write_str(" ")?;
603            }
604        }
605        Ok(())
606    }
607
608    /// Write a soft line break
609    fn write_soft_break(&mut self) -> fmt::Result {
610        self.write_char('\n')
611    }
612
613    /// Write a hard line break
614    fn write_hard_break(&mut self) -> fmt::Result {
615        if self.options.hard_break_spaces {
616            self.write_str("  \n")
617        } else {
618            self.write_str("\\\n")
619        }
620    }
621
622    /// Get the generated CommonMark format text
623    ///
624    /// Consumes the writer and returns the generated string
625    ///
626    /// # Example
627    ///
628    /// ```
629    /// use cmark_writer::writer::CommonMarkWriter;
630    /// use cmark_writer::ast::{Node, InlineNode};
631    ///
632    /// let mut writer = CommonMarkWriter::new();
633    /// writer.write(&Node::Inline(InlineNode::Text("Hello".to_string()))).unwrap();
634    /// let result = writer.into_string();
635    /// assert_eq!(result, "Hello");
636    /// ```
637    pub fn into_string(self) -> String {
638        self.buffer
639    }
640
641    /// Write a character to the buffer
642    fn write_char(&mut self, c: char) -> fmt::Result {
643        self.buffer.push(c);
644        Ok(())
645    }
646
647    /// Write a string to the buffer
648    fn write_str(&mut self, s: &str) -> fmt::Result {
649        self.buffer.push_str(s);
650        Ok(())
651    }
652}
653
654impl Default for CommonMarkWriter {
655    fn default() -> Self {
656        Self::new()
657    }
658}
659
660// Implement Display trait for new Node structure
661impl fmt::Display for Node {
662    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
663        let mut writer = CommonMarkWriter::new();
664        writer.write(self)?;
665        write!(f, "{}", writer.into_string())
666    }
667}
668
669// Implement Display trait for BlockNode
670impl fmt::Display for BlockNode {
671    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
672        let mut writer = CommonMarkWriter::new();
673        writer.write_block(self)?;
674        write!(f, "{}", writer.into_string())
675    }
676}
677
678// Implement Display trait for InlineNode
679impl fmt::Display for InlineNode {
680    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
681        let mut writer = CommonMarkWriter::new();
682        writer.write_inline(self)?;
683        write!(f, "{}", writer.into_string())
684    }
685}