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, 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: true,
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;
52    ///
53    /// let mut writer = CommonMarkWriter::new();
54    /// writer.write(&Node::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;
102    ///
103    /// let mut writer = CommonMarkWriter::new();
104    /// writer.write(&Node::Text("Hello".to_string())).unwrap();
105    /// ```
106    pub fn write(&mut self, node: &Node) -> fmt::Result {
107        match node {
108            Node::Document(children) => self.write_document(children),
109            Node::Heading { level, content } => self.write_heading(*level, content),
110            Node::Paragraph(content) => self.write_paragraph(content),
111            Node::BlockQuote(content) => self.write_blockquote(content),
112            Node::CodeBlock { language, content } => self.write_code_block(language, content),
113            Node::UnorderedList(items) => self.write_unordered_list(items),
114            Node::OrderedList { start, items } => self.write_ordered_list(*start, items),
115            Node::ThematicBreak => self.write_thematic_break(),
116            Node::Table {
117                headers,
118                rows,
119                alignments,
120            } => self.write_table(headers, rows, alignments),
121            Node::Link {
122                url,
123                title,
124                content,
125            } => self.write_link(url, title, content),
126            Node::Image { url, title, alt } => self.write_image(url, title, alt),
127            Node::Emphasis(content) => self.write_emphasis(content),
128            Node::Strong(content) => self.write_strong(content),
129            Node::InlineCode(content) => self.write_inline_code(content),
130            Node::Text(content) => self.write_text(content),
131            Node::Html(content) => self.write_html(content),
132            Node::SoftBreak => self.write_soft_break(),
133            Node::HardBreak => self.write_hard_break(),
134        }
135    }
136
137    /// Write a document node
138    fn write_document(&mut self, children: &[Node]) -> fmt::Result {
139        for (i, child) in children.iter().enumerate() {
140            self.write(child)?;
141            if i < children.len() - 1 {
142                self.write_str("\n\n")?;
143            }
144        }
145        Ok(())
146    }
147
148    /// Write a heading node
149    fn write_heading(&mut self, level: u8, content: &[Node]) -> fmt::Result {
150        if !(1..=6).contains(&level) {
151            return Err(fmt::Error);
152        }
153
154        for _ in 0..level {
155            self.write_char('#')?;
156        }
157        self.write_char(' ')?;
158
159        for (i, node) in content.iter().enumerate() {
160            self.write(node)?;
161            if i < content.len() - 1 && !matches!(node, Node::SoftBreak | Node::HardBreak) {
162                self.write_char(' ')?;
163            }
164        }
165
166        Ok(())
167    }
168    /// Write a paragraph node
169    fn write_paragraph(&mut self, content: &[Node]) -> fmt::Result {
170        let mut prev_is_inline = false;
171
172        for (i, node) in content.iter().enumerate() {
173            // Check if the current node is an inline element
174            let is_inline = self.is_inline_element(node);
175
176            // If both current and previous nodes are inline elements, and it's not the first element,
177            // ensure there's no line break between them
178            if prev_is_inline
179                && is_inline
180                && i > 0
181                && !matches!(node, Node::SoftBreak | Node::HardBreak)
182            {
183                // Don't add extra whitespace to prevent incorrect line breaks
184            } else if i > 0 {
185                // Non-consecutive inline elements, add normal line break and indentation
186                self.write_char('\n')?;
187                // Add appropriate indentation (current indent level)
188                for _ in 0..(self.indent_level * self.options.indent_spaces) {
189                    self.write_char(' ')?;
190                }
191            }
192
193            self.write(node)?;
194            prev_is_inline = is_inline;
195        }
196        Ok(())
197    }
198
199    /// Write a blockquote node
200    fn write_blockquote(&mut self, content: &[Node]) -> fmt::Result {
201        self.indent_level += 1;
202
203        for (i, node) in content.iter().enumerate() {
204            self.write_str("> ")?;
205            self.write(node)?;
206            if i < content.len() - 1 {
207                self.write_str("\n> \n")?;
208            }
209        }
210
211        self.indent_level -= 1;
212        Ok(())
213    }
214
215    /// Write a code block node
216    fn write_code_block(&mut self, language: &Option<String>, content: &str) -> fmt::Result {
217        let mut max_backticks = 0;
218        let mut current = 0;
219        for c in content.chars() {
220            if c == '`' {
221                current += 1;
222                if current > max_backticks {
223                    max_backticks = current;
224                }
225            } else {
226                current = 0;
227            }
228        }
229        let fence_len = max(max_backticks + 1, 3);
230        let fence = "`".repeat(fence_len);
231
232        self.write_str(&fence)?;
233        if let Some(lang) = language {
234            self.write_str(lang)?;
235        }
236        self.write_char('\n')?;
237        self.write_str(content)?;
238
239        // Ensure content ends with a newline
240        if !content.ends_with('\n') {
241            self.write_char('\n')?;
242        }
243
244        self.write_str(&fence)?;
245        Ok(())
246    }
247
248    /// Write an unordered list node
249    fn write_unordered_list(&mut self, items: &[ListItem]) -> fmt::Result {
250        for (i, item) in items.iter().enumerate() {
251            self.write_list_item(item, "- ")?;
252            if i < items.len() - 1 {
253                self.write_char('\n')?;
254            }
255        }
256        Ok(())
257    }
258
259    /// Write an ordered list node
260    fn write_ordered_list(&mut self, start: u32, items: &[ListItem]) -> fmt::Result {
261        for (i, item) in items.iter().enumerate() {
262            let num = start as usize + i;
263            let prefix = format!("{}. ", num);
264            self.write_list_item(item, &prefix)?;
265            if i < items.len() - 1 {
266                self.write_char('\n')?;
267            }
268        }
269        Ok(())
270    }
271
272    /// Write a list item
273    fn write_list_item(&mut self, item: &ListItem, prefix: &str) -> fmt::Result {
274        // Apply indentation based on current level
275        for _ in 0..(self.indent_level * self.options.indent_spaces) {
276            self.write_char(' ')?;
277        }
278        self.write_str(prefix)?;
279
280        if item.is_task {
281            if item.task_completed {
282                self.write_str("[x] ")?;
283            } else {
284                self.write_str("[ ] ")?;
285            }
286        }
287
288        self.indent_level += 1;
289
290        // Track whether the previous element was an inline element
291        let mut prev_is_inline = false;
292
293        for (i, node) in item.content.iter().enumerate() {
294            // Determine if the current node is an inline element
295            let is_inline = self.is_inline_element(node);
296            let is_list = matches!(node, Node::OrderedList { .. } | Node::UnorderedList(..));
297
298            // Nested lists need special line break handling
299            if is_list {
300                if i > 0 {
301                    self.write_char('\n')?;
302                }
303                self.write(node)?;
304                prev_is_inline = false;
305                continue;
306            }
307
308            // If both previous and current are inline elements, prevent incorrect line breaks
309            if prev_is_inline && is_inline {
310                // Don't add extra separators to prevent incorrect line breaks for inline elements
311            } else if i > 0 {
312                // Non-consecutive inline elements, add normal line break and indentation
313                self.write_char('\n')?;
314                // Add appropriate indentation (list item prefix length + current indent level)
315                let prefix_length = prefix.len() + if item.is_task { 4 } else { 0 };
316                for _ in 0..(self.indent_level * self.options.indent_spaces) + prefix_length {
317                    self.write_char(' ')?;
318                }
319            }
320
321            self.write(node)?;
322            prev_is_inline = is_inline;
323        }
324
325        self.indent_level -= 1;
326        Ok(())
327    }
328
329    /// Write a thematic break (horizontal rule)
330    fn write_thematic_break(&mut self) -> fmt::Result {
331        self.write_str("---")
332    }
333
334    /// Check if the node contains a newline character and return an error if it does
335    fn check_no_newline(&self, node: &Node) -> fmt::Result {
336        if Self::node_contains_newline(node) {
337            return Err(fmt::Error);
338        }
339        Ok(())
340    }
341
342    /// Check if the node contains a newline character recursively
343    fn node_contains_newline(node: &Node) -> bool {
344        match node {
345            Node::Text(s) | Node::InlineCode(s) | Node::Html(s) => s.contains('\n'),
346            Node::Emphasis(children) | Node::Strong(children) => {
347                children.iter().any(Self::node_contains_newline)
348            }
349            Node::Link { content, .. } => content.iter().any(Self::node_contains_newline),
350            Node::Image { alt, .. } => alt.contains('\n'),
351            _ => false,
352        }
353    }
354
355    /// Write a table
356    fn write_table(
357        &mut self,
358        headers: &[Node],
359        rows: &[Vec<Node>],
360        alignments: &[Alignment],
361    ) -> fmt::Result {
362        // Write header
363        self.write_char('|')?;
364        for header in headers {
365            self.check_no_newline(header)?;
366            self.write_char(' ')?;
367            self.write(header)?;
368            self.write_str(" |")?;
369        }
370        self.write_char('\n')?;
371
372        // Write alignment row
373        self.write_char('|')?;
374        for alignment in alignments {
375            match alignment {
376                Alignment::None => self.write_str(" --- |")?,
377                Alignment::Left => self.write_str(" :--- |")?,
378                Alignment::Center => self.write_str(" :---: |")?,
379                Alignment::Right => self.write_str(" ---: |")?,
380            }
381        }
382        self.write_char('\n')?;
383
384        // Write table content
385        for row in rows {
386            self.write_char('|')?;
387            for cell in row {
388                self.check_no_newline(cell)?;
389                self.write_char(' ')?;
390                self.write(cell)?;
391                self.write_str(" |")?;
392            }
393            self.write_char('\n')?;
394        }
395
396        Ok(())
397    }
398
399    /// Write a link
400    fn write_link(&mut self, url: &str, title: &Option<String>, content: &[Node]) -> fmt::Result {
401        for node in content {
402            self.check_no_newline(node)?;
403        }
404        self.write_char('[')?;
405
406        for node in content {
407            self.write(node)?;
408        }
409
410        self.write_str("](")?;
411        self.write_str(url)?;
412
413        if let Some(title_text) = title {
414            self.write_str(" \"")?;
415            self.write_str(title_text)?;
416            self.write_char('"')?;
417        }
418
419        self.write_char(')')
420    }
421
422    /// Write an image
423    fn write_image(&mut self, url: &str, title: &Option<String>, alt: &str) -> fmt::Result {
424        self.check_no_newline(&Node::Text(alt.to_string()))?;
425        self.write_str("![")?;
426        self.write_str(alt)?;
427        self.write_str("](")?;
428        self.write_str(url)?;
429
430        if let Some(title_text) = title {
431            self.write_str(" \"")?;
432            self.write_str(title_text)?;
433            self.write_char('"')?;
434        }
435
436        self.write_char(')')
437    }
438
439    /// Write emphasis (italic)
440    fn write_emphasis(&mut self, content: &[Node]) -> fmt::Result {
441        for node in content {
442            self.check_no_newline(node)?;
443        }
444        self.write_char('*')?;
445
446        for node in content {
447            self.write(node)?;
448        }
449
450        self.write_char('*')
451    }
452
453    /// Write strong emphasis (bold)
454    fn write_strong(&mut self, content: &[Node]) -> fmt::Result {
455        for node in content {
456            self.check_no_newline(node)?;
457        }
458        self.write_str("**")?;
459
460        for node in content {
461            self.write(node)?;
462        }
463
464        self.write_str("**")
465    }
466
467    /// Write inline code
468    fn write_inline_code(&mut self, content: &str) -> fmt::Result {
469        self.check_no_newline(&Node::InlineCode(content.to_string()))?;
470        self.write_char('`')?;
471        self.write_str(content)?;
472        self.write_char('`')
473    }
474
475    /// Write plain text
476    fn write_text(&mut self, content: &str) -> fmt::Result {
477        self.check_no_newline(&Node::Text(content.to_string()))?;
478        // Escape special characters
479        let escaped = content
480            .replace('\\', "\\\\")
481            .replace('*', "\\*")
482            .replace('_', "\\_")
483            .replace('[', "\\[")
484            .replace(']', "\\]")
485            .replace('<', "\\<")
486            .replace('>', "\\>")
487            .replace('`', "\\`");
488
489        self.write_str(&escaped)
490    }
491
492    /// Write HTML
493    fn write_html(&mut self, content: &str) -> fmt::Result {
494        self.write_str(content)
495    }
496
497    /// Write a soft line break
498    fn write_soft_break(&mut self) -> fmt::Result {
499        self.write_char('\n')
500    }
501
502    /// Write a hard line break
503    fn write_hard_break(&mut self) -> fmt::Result {
504        if self.options.hard_break_spaces {
505            self.write_str("  \n")
506        } else {
507            self.write_str("\\\n")
508        }
509    }
510
511    /// Check if a node is an inline element that shouldn't be broken across lines
512    fn is_inline_element(&self, node: &Node) -> bool {
513        matches!(
514            node,
515            Node::Text(_)
516                | Node::Emphasis(_)
517                | Node::Strong(_)
518                | Node::InlineCode(_)
519                | Node::Link { .. }
520                | Node::Image { .. }
521        )
522    }
523
524    /// Get the generated CommonMark format text
525    ///
526    /// Consumes the writer and returns the generated string
527    ///
528    /// # Example
529    ///
530    /// ```
531    /// use cmark_writer::writer::CommonMarkWriter;
532    /// use cmark_writer::ast::Node;
533    ///
534    /// let mut writer = CommonMarkWriter::new();
535    /// writer.write(&Node::Text("Hello".to_string())).unwrap();
536    /// let result = writer.into_string();
537    /// assert_eq!(result, "Hello");
538    /// ```
539    pub fn into_string(self) -> String {
540        self.buffer
541    }
542
543    /// Write a character to the buffer
544    fn write_char(&mut self, c: char) -> fmt::Result {
545        self.buffer.push(c);
546        Ok(())
547    }
548
549    /// Write a string to the buffer
550    fn write_str(&mut self, s: &str) -> fmt::Result {
551        self.buffer.push_str(s);
552        Ok(())
553    }
554}
555
556impl Default for CommonMarkWriter {
557    fn default() -> Self {
558        Self::new()
559    }
560}
561
562// Implement Display trait
563impl fmt::Display for Node {
564    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
565        let mut writer = CommonMarkWriter::new();
566        writer.write(self)?;
567        write!(f, "{}", writer.into_string())
568    }
569}