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