cmark_writer/writer/
cmark.rs

1//! CommonMark writer implementation.
2//!
3//! This file contains the implementation of the CommonMarkWriter class, which serializes AST nodes to CommonMark-compliant text.
4
5use crate::ast::{CustomNode, CustomNodeWriter, HeadingType, HtmlElement, ListItem, Node};
6use crate::error::{WriteError, WriteResult};
7use crate::options::WriterOptions;
8use crate::CodeBlockType;
9use std::fmt::{self};
10
11use super::processors::{
12    BlockNodeProcessor, CustomNodeProcessor, InlineNodeProcessor, NodeProcessor,
13};
14
15/// CommonMark writer
16///
17/// This struct is responsible for serializing AST nodes to CommonMark-compliant text.
18#[derive(Debug)]
19pub struct CommonMarkWriter {
20    options: WriterOptions,
21    buffer: String,
22}
23
24impl CommonMarkWriter {
25    /// Create a new CommonMark writer with default options
26    ///
27    /// # Example
28    ///
29    /// ```
30    /// use cmark_writer::writer::CommonMarkWriter;
31    /// use cmark_writer::ast::Node;
32    ///
33    /// let mut writer = CommonMarkWriter::new();
34    /// writer.write(&Node::Text("Hello".to_string())).unwrap();
35    /// assert_eq!(writer.into_string(), "Hello");
36    /// ```
37    pub fn new() -> Self {
38        Self::with_options(WriterOptions::default())
39    }
40
41    /// Create a new CommonMark writer with specified options
42    ///
43    /// # Parameters
44    ///
45    /// * `options` - Custom CommonMark formatting options
46    ///
47    /// # Example
48    ///
49    /// ```
50    /// use cmark_writer::writer::CommonMarkWriter;
51    /// use cmark_writer::options::WriterOptions;
52    ///
53    /// let options = WriterOptions {
54    ///     strict: true,
55    ///     hard_break_spaces: false,  // Use backslash for line breaks
56    ///     indent_spaces: 2,          // Use 2 spaces for indentation
57    /// };
58    /// let writer = CommonMarkWriter::with_options(options);
59    /// ```
60    pub fn with_options(options: WriterOptions) -> Self {
61        Self {
62            options,
63            buffer: String::new(),
64        }
65    }
66
67    /// Whether the writer is in strict mode
68    pub(crate) fn is_strict_mode(&self) -> bool {
69        self.options.strict
70    }
71
72    /// Apply a specific prefix to multi-line text, used for handling container node indentation
73    ///
74    /// # Parameters
75    ///
76    /// * `content` - The multi-line content to process
77    /// * `prefix` - The prefix to apply to each line
78    /// * `first_line_prefix` - The prefix to apply to the first line (can be different from other lines)
79    ///
80    /// # Returns
81    ///
82    /// Returns a string with applied indentation
83    fn apply_prefix(&self, content: &str, prefix: &str, first_line_prefix: Option<&str>) -> String {
84        if content.is_empty() {
85            return String::new();
86        }
87
88        let mut result = String::new();
89        let lines: Vec<&str> = content.lines().collect();
90
91        if !lines.is_empty() {
92            let actual_prefix = first_line_prefix.unwrap_or(prefix);
93            result.push_str(actual_prefix);
94            result.push_str(lines[0]);
95        }
96
97        for line in &lines[1..] {
98            result.push('\n');
99            result.push_str(prefix);
100            result.push_str(line);
101        }
102
103        result
104    }
105
106    /// Write an AST node as CommonMark format
107    ///
108    /// # Parameters
109    ///
110    /// * `node` - The AST node to write
111    ///
112    /// # Returns
113    ///
114    /// If writing succeeds, returns `Ok(())`, otherwise returns `Err(WriteError)`
115    ///
116    /// # Example
117    ///
118    /// ```
119    /// use cmark_writer::writer::CommonMarkWriter;
120    /// use cmark_writer::ast::Node;
121    ///
122    /// let mut writer = CommonMarkWriter::new();
123    /// writer.write(&Node::Text("Hello".to_string())).unwrap();
124    /// ```
125    pub fn write(&mut self, node: &Node) -> WriteResult<()> {
126        if let Node::Custom(_) = node {
127            return CustomNodeProcessor.process(self, node);
128        }
129
130        if node.is_block() {
131            BlockNodeProcessor.process(self, node)
132        } else if node.is_inline() {
133            InlineNodeProcessor.process(self, node)
134        } else {
135            // Keep this branch for future implementation needs
136            Err(WriteError::UnsupportedNodeType)
137        }
138    }
139
140    /// Write a custom node using its implementation
141    #[allow(clippy::borrowed_box)]
142    pub(crate) fn write_custom_node(&mut self, node: &Box<dyn CustomNode>) -> WriteResult<()> {
143        node.write(self)
144    }
145
146    /// Get context description for a node, used for error reporting
147    pub(crate) fn get_context_for_node(&self, node: &Node) -> String {
148        match node {
149            Node::Text(_) => "Text".to_string(),
150            Node::Emphasis(_) => "Emphasis".to_string(),
151            Node::Strong(_) => "Strong".to_string(),
152            Node::InlineCode(_) => "InlineCode".to_string(),
153            Node::Link { .. } => "Link content".to_string(),
154            Node::Image { .. } => "Image alt text".to_string(),
155            Node::HtmlElement(_) => "HtmlElement content".to_string(),
156            Node::Custom(_) => "Custom node".to_string(),
157            _ => "Unknown inline element".to_string(),
158        }
159    }
160
161    /// Check if the inline node contains a newline character and return an error if it does
162    pub(crate) fn check_no_newline(&self, node: &Node, context: &str) -> WriteResult<()> {
163        if Self::node_contains_newline(node) {
164            return Err(WriteError::NewlineInInlineElement(context.to_string()));
165        }
166        Ok(())
167    }
168
169    /// Check if the inline node contains a newline character recursively
170    fn node_contains_newline(node: &Node) -> bool {
171        match node {
172            Node::Text(s) | Node::InlineCode(s) => s.contains('\n'),
173            Node::Emphasis(children) | Node::Strong(children) => {
174                children.iter().any(Self::node_contains_newline)
175            }
176            Node::HtmlElement(element) => element.children.iter().any(Self::node_contains_newline),
177            Node::Link { content, .. } => content.iter().any(Self::node_contains_newline),
178            Node::Image { alt, .. } => alt.iter().any(Self::node_contains_newline),
179            Node::SoftBreak | Node::HardBreak => true,
180            // Custom nodes are handled separately
181            Node::Custom(_) => false,
182            _ => false,
183        }
184    }
185
186    /// Writes text content with character escaping
187    pub(crate) fn write_text_content(&mut self, content: &str) -> WriteResult<()> {
188        let escaped = content
189            .replace('\\', "\\\\")
190            .replace('*', "\\*")
191            .replace('_', "\\_")
192            .replace('[', "\\[")
193            .replace(']', "\\]")
194            .replace('<', "\\<")
195            .replace('>', "\\>")
196            .replace('`', "\\`");
197
198        self.write_str(&escaped)?;
199        Ok(())
200    }
201
202    /// Writes inline code content
203    pub(crate) fn write_code_content(&mut self, content: &str) -> WriteResult<()> {
204        self.write_char('`')?;
205        self.write_str(content)?;
206        self.write_char('`')?;
207        Ok(())
208    }
209
210    /// Helper function for writing content with delimiters
211    pub(crate) fn write_delimited(&mut self, content: &[Node], delimiter: &str) -> WriteResult<()> {
212        self.write_str(delimiter)?;
213
214        for node in content {
215            self.write(node)?;
216        }
217
218        self.write_str(delimiter)?;
219        Ok(())
220    }
221
222    /// Write a document node
223    pub(crate) fn write_document(&mut self, children: &[Node]) -> WriteResult<()> {
224        for (i, child) in children.iter().enumerate() {
225            if i > 0 {
226                self.write_str("\n")?;
227            }
228            self.write(child)?;
229        }
230        Ok(())
231    }
232
233    /// Write a heading node
234    pub(crate) fn write_heading(
235        &mut self,
236        level: u8,
237        content: &[Node],
238        heading_type: &HeadingType,
239    ) -> WriteResult<()> {
240        // 验证标题级别
241        if level == 0 || level > 6 {
242            return Err(WriteError::InvalidHeadingLevel(level));
243        }
244
245        match heading_type {
246            // ATX heading, using # character
247            HeadingType::Atx => {
248                for _ in 0..level {
249                    self.write_char('#')?;
250                }
251                self.write_char(' ')?;
252
253                for node in content {
254                    self.write(node)?;
255                }
256
257                self.write_char('\n')?;
258            }
259
260            HeadingType::Setext => {
261                // First write the heading content
262                for node in content {
263                    self.write(node)?;
264                }
265                self.write_char('\n')?;
266
267                // Add underline characters based on level
268                // Setext only supports level 1 and 2 headings
269                let underline_char = if level == 1 { '=' } else { '-' };
270
271                // For good readability, we add underlines at least as long as the heading text
272                // Calculate a reasonable underline length (at least 3 characters)
273                let min_len = 3;
274
275                // Write the underline characters
276                for _ in 0..min_len {
277                    self.write_char(underline_char)?;
278                }
279
280                // Add a newline to end the heading
281                self.write_char('\n')?;
282            }
283        }
284
285        Ok(())
286    }
287
288    /// Write a paragraph node
289    pub(crate) fn write_paragraph(&mut self, content: &[Node]) -> WriteResult<()> {
290        for node in content.iter() {
291            self.write(node)?;
292        }
293
294        Ok(())
295    }
296
297    /// Write a blockquote node
298    pub(crate) fn write_blockquote(&mut self, content: &[Node]) -> WriteResult<()> {
299        // Create a temporary writer buffer to write all blockquote content
300        let mut temp_writer = CommonMarkWriter::with_options(self.options.clone());
301
302        // Write all content to temporary buffer
303        for (i, node) in content.iter().enumerate() {
304            if i > 0 {
305                temp_writer.write_char('\n')?;
306            }
307            // Write all nodes uniformly
308            temp_writer.write(node)?;
309        }
310
311        // Get all content
312        let all_content = temp_writer.into_string();
313
314        // Apply blockquote prefix "> " uniformly
315        let prefix = "> ";
316        let formatted_content = self.apply_prefix(&all_content, prefix, Some(prefix));
317
318        // Write formatted content
319        self.buffer.push_str(&formatted_content);
320        Ok(())
321    }
322
323    /// Write a thematic break (horizontal rule)
324    pub(crate) fn write_thematic_break(&mut self) -> WriteResult<()> {
325        self.write_str("---")?;
326        Ok(())
327    }
328
329    /// Write a code block node
330    pub(crate) fn write_code_block(
331        &mut self,
332        language: &Option<String>,
333        content: &str,
334        block_type: &CodeBlockType,
335    ) -> WriteResult<()> {
336        match block_type {
337            CodeBlockType::Indented => {
338                let indent = "    ";
339                let indented_content = self.apply_prefix(content, indent, Some(indent));
340                self.buffer.push_str(&indented_content);
341            }
342            CodeBlockType::Fenced => {
343                let max_backticks = content
344                    .chars()
345                    .fold((0, 0), |(max, current), c| {
346                        if c == '`' {
347                            (max.max(current + 1), current + 1)
348                        } else {
349                            (max, 0)
350                        }
351                    })
352                    .0;
353
354                let fence_len = std::cmp::max(max_backticks + 1, 3);
355                let fence = "`".repeat(fence_len);
356
357                self.write_str(&fence)?;
358                if let Some(lang) = language {
359                    self.write_str(lang)?;
360                }
361                self.write_char('\n')?;
362
363                self.buffer.push_str(content);
364                if !content.ends_with('\n') {
365                    self.write_char('\n')?;
366                }
367
368                self.write_str(&fence)?;
369            }
370        }
371
372        Ok(())
373    }
374
375    /// Write an unordered list node
376    pub(crate) fn write_unordered_list(&mut self, items: &[ListItem]) -> WriteResult<()> {
377        for (i, item) in items.iter().enumerate() {
378            if i > 0 {
379                self.write_char('\n')?;
380            }
381            self.write_list_item(item, "- ")?;
382        }
383
384        Ok(())
385    }
386
387    /// Write an ordered list node
388    pub(crate) fn write_ordered_list(&mut self, start: u32, items: &[ListItem]) -> WriteResult<()> {
389        // Track the current item number
390        let mut current_number = start;
391
392        for (i, item) in items.iter().enumerate() {
393            if i > 0 {
394                self.write_char('\n')?;
395            }
396
397            match item {
398                // For ordered list items, check if there's a custom number
399                ListItem::Ordered { number, content: _ } => {
400                    if let Some(custom_num) = number {
401                        // Use custom numbering
402                        let prefix = format!("{}. ", custom_num);
403                        self.write_list_item(item, &prefix)?;
404                        // Next expected number
405                        current_number = custom_num + 1;
406                    } else {
407                        // No custom number, use the current calculated number
408                        let prefix = format!("{}. ", current_number);
409                        self.write_list_item(item, &prefix)?;
410                        current_number += 1;
411                    }
412                }
413                // For other types of list items, still use the current number
414                _ => {
415                    let prefix = format!("{}. ", current_number);
416                    self.write_list_item(item, &prefix)?;
417                    current_number += 1;
418                }
419            }
420        }
421
422        Ok(())
423    }
424
425    /// Write a list item
426    fn write_list_item(&mut self, item: &ListItem, prefix: &str) -> WriteResult<()> {
427        match item {
428            ListItem::Unordered { content } => {
429                self.write_str(prefix)?;
430                self.write_list_item_content(content, prefix.len())?;
431            }
432            ListItem::Ordered { number, content } => {
433                if let Some(num) = number {
434                    let custom_prefix = format!("{}. ", num);
435                    self.write_str(&custom_prefix)?;
436                    self.write_list_item_content(content, custom_prefix.len())?;
437                } else {
438                    self.write_str(prefix)?;
439                    self.write_list_item_content(content, prefix.len())?;
440                }
441            }
442        }
443
444        Ok(())
445    }
446
447    /// Write list item content
448    fn write_list_item_content(&mut self, content: &[Node], prefix_len: usize) -> WriteResult<()> {
449        if content.is_empty() {
450            return Ok(());
451        }
452
453        let mut temp_writer = CommonMarkWriter::with_options(self.options.clone());
454
455        for (i, node) in content.iter().enumerate() {
456            if i > 0 {
457                temp_writer.write_char('\n')?;
458            }
459
460            temp_writer.write(node)?;
461        }
462
463        let all_content = temp_writer.into_string();
464
465        let indent = " ".repeat(prefix_len);
466
467        let formatted_content = self.apply_prefix(&all_content, &indent, Some(""));
468
469        self.buffer.push_str(&formatted_content);
470
471        Ok(())
472    }
473
474    /// Write a table
475    pub(crate) fn write_table(&mut self, headers: &[Node], rows: &[Vec<Node>]) -> WriteResult<()> {
476        // Write header
477        self.write_char('|')?;
478        for header in headers {
479            self.check_no_newline(header, "Table Header")?;
480            self.write_char(' ')?;
481            self.write(header)?;
482            self.write_str(" |")?;
483        }
484        self.write_char('\n')?;
485
486        // Write alignment row
487        self.write_char('|')?;
488        for _ in 0..headers.len() {
489            self.write_str(" --- |")?;
490        }
491        self.write_char('\n')?;
492
493        // Write table content
494        for row in rows {
495            self.write_char('|')?;
496            for cell in row {
497                self.check_no_newline(cell, "Table Cell")?;
498                self.write_char(' ')?;
499                self.write(cell)?;
500                self.write_str(" |")?;
501            }
502            self.write_char('\n')?;
503        }
504
505        Ok(())
506    }
507
508    /// Write a link
509    pub(crate) fn write_link(
510        &mut self,
511        url: &str,
512        title: &Option<String>,
513        content: &[Node],
514    ) -> WriteResult<()> {
515        for node in content {
516            self.check_no_newline(node, "Link Text")?;
517        }
518        self.write_char('[')?;
519
520        for node in content {
521            self.write(node)?;
522        }
523
524        self.write_str("](")?;
525        self.write_str(url)?;
526
527        if let Some(title_text) = title {
528            self.write_str(" \"")?;
529            self.write_str(title_text)?;
530            self.write_char('"')?;
531        }
532
533        self.write_char(')')?;
534        Ok(())
535    }
536
537    /// Write an image
538    pub(crate) fn write_image(
539        &mut self,
540        url: &str,
541        title: &Option<String>,
542        alt: &[Node],
543    ) -> WriteResult<()> {
544        // Check for newlines in alt text content
545        for node in alt {
546            self.check_no_newline(node, "Image alt text")?;
547        }
548
549        self.write_str("![")?;
550
551        // Write alt text content
552        for node in alt {
553            self.write(node)?;
554        }
555
556        self.write_str("](")?;
557        self.write_str(url)?;
558
559        if let Some(title_text) = title {
560            self.write_str(" \"")?;
561            self.write_str(title_text)?;
562            self.write_char('"')?;
563        }
564
565        self.write_char(')')?;
566        Ok(())
567    }
568
569    /// Write a soft line break
570    pub(crate) fn write_soft_break(&mut self) -> WriteResult<()> {
571        self.write_char('\n')?;
572        Ok(())
573    }
574
575    /// Write a hard line break
576    pub(crate) fn write_hard_break(&mut self) -> WriteResult<()> {
577        if self.options.hard_break_spaces {
578            self.write_str("  \n")?;
579        } else {
580            self.write_str("\\\n")?;
581        }
582        Ok(())
583    }
584
585    /// Write an HTML block
586    pub(crate) fn write_html_block(&mut self, content: &str) -> WriteResult<()> {
587        self.buffer.push_str(content);
588
589        Ok(())
590    }
591
592    /// Write an autolink (URI or email address wrapped in < and >)
593    pub(crate) fn write_autolink(&mut self, url: &str, is_email: bool) -> WriteResult<()> {
594        // Autolinks shouldn't contain newlines
595        if url.contains('\n') {
596            return Err(WriteError::NewlineInInlineElement(
597                "Autolink URL".to_string(),
598            ));
599        }
600
601        // Write the autolink with < and > delimiters
602        self.write_char('<')?;
603
604        // For email autolinks, we don't need to add any prefix
605        // For URI autolinks, ensure it has a scheme
606        if !is_email && !url.contains(':') {
607            // Default to https if no scheme is provided
608            self.write_str("https://")?;
609        }
610
611        self.write_str(url)?;
612        self.write_char('>')?;
613
614        Ok(())
615    }
616
617    /// Write a link reference definition
618    pub(crate) fn write_link_reference_definition(
619        &mut self,
620        label: &str,
621        destination: &str,
622        title: &Option<String>,
623    ) -> WriteResult<()> {
624        // Format: [label]: destination "optional title"
625        self.write_char('[')?;
626        self.write_str(label)?;
627        self.write_str("]: ")?;
628        self.write_str(destination)?;
629
630        if let Some(title_text) = title {
631            self.write_str(" \"")?;
632            self.write_str(title_text)?;
633            self.write_char('"')?;
634        }
635
636        Ok(())
637    }
638
639    /// Write a reference link
640    pub(crate) fn write_reference_link(
641        &mut self,
642        label: &str,
643        content: &[Node],
644    ) -> WriteResult<()> {
645        // Check for newlines in content
646        for node in content {
647            self.check_no_newline(node, "Reference Link Text")?;
648        }
649
650        // If content is empty or exactly matches the label (as plain text),
651        // this is a shortcut reference link: [label]
652        if content.is_empty() {
653            self.write_char('[')?;
654            self.write_str(label)?;
655            self.write_char(']')?;
656            return Ok(());
657        }
658
659        // Check if content is exactly the same as the label (to use shortcut syntax)
660        let is_shortcut =
661            content.len() == 1 && matches!(&content[0], Node::Text(text) if text == label);
662
663        if is_shortcut {
664            // Use shortcut reference link syntax: [label]
665            self.write_char('[')?;
666            self.write_str(label)?;
667            self.write_char(']')?;
668        } else {
669            // Use full reference link syntax: [content][label]
670            self.write_char('[')?;
671
672            for node in content {
673                self.write(node)?;
674            }
675
676            self.write_str("][")?;
677            self.write_str(label)?;
678            self.write_char(']')?;
679        }
680
681        Ok(())
682    }
683
684    /// Write an HTML element
685    pub(crate) fn write_html_element(&mut self, element: &HtmlElement) -> WriteResult<()> {
686        self.write_char('<')?;
687        self.write_str(&element.tag)?;
688
689        for attr in &element.attributes {
690            self.write_char(' ')?;
691            self.write_str(&attr.name)?;
692            self.write_str("=\"")?;
693            self.write_str(&attr.value)?; // Assume attributes are pre-escaped if needed
694            self.write_char('"')?;
695        }
696
697        if element.self_closing {
698            self.write_str(" />")?;
699            return Ok(());
700        }
701
702        self.write_char('>')?;
703
704        for child in &element.children {
705            // HTML element content can contain newlines, so no strict check here
706            self.write(child)?;
707        }
708
709        self.write_str("</")?;
710        self.write_str(&element.tag)?;
711        self.write_char('>')?;
712        Ok(())
713    }
714
715    /// Get the generated CommonMark format text
716    ///
717    /// Consumes the writer and returns the generated string
718    ///
719    /// # Example
720    ///
721    /// ```
722    /// use cmark_writer::writer::CommonMarkWriter;
723    /// use cmark_writer::ast::Node;
724    ///
725    /// let mut writer = CommonMarkWriter::new();
726    /// writer.write(&Node::Text("Hello".to_string())).unwrap();
727    /// let result = writer.into_string();
728    /// assert_eq!(result, "Hello");
729    /// ```
730    pub fn into_string(self) -> String {
731        self.buffer
732    }
733    /// Ensure content ends with a newline (for consistent handling at the end of block nodes)
734    ///
735    /// Adds a newline character if the content doesn't already end with one; does nothing if it already ends with a newline
736    pub(crate) fn ensure_trailing_newline(&mut self) -> WriteResult<()> {
737        if !self.buffer.ends_with('\n') {
738            self.write_char('\n')?;
739        }
740        Ok(())
741    }
742}
743
744impl Default for CommonMarkWriter {
745    fn default() -> Self {
746        Self::new()
747    }
748}
749
750// Implement Display trait for Node structure
751impl fmt::Display for Node {
752    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
753        let mut writer = CommonMarkWriter::new();
754        match writer.write(self) {
755            Ok(_) => write!(f, "{}", writer.into_string()),
756            Err(e) => write!(f, "Error writing Node: {}", e),
757        }
758    }
759}
760
761// Implement CustomNodeWriter trait for CommonMarkWriter
762impl CustomNodeWriter for CommonMarkWriter {
763    fn write_str(&mut self, s: &str) -> fmt::Result {
764        self.buffer.push_str(s);
765        Ok(())
766    }
767
768    fn write_char(&mut self, c: char) -> fmt::Result {
769        self.buffer.push(c);
770        Ok(())
771    }
772}