cmark_writer/writer/cmark/
writer.rs

1//! Main CommonMark writer struct and core functionality.
2
3use crate::ast::{CustomNode, Node};
4use crate::error::{WriteError, WriteResult};
5use crate::options::WriterOptions;
6use crate::writer::context::{NewlineContext, NewlineStrategy, RenderingMode};
7use ecow::EcoString;
8use std::fmt;
9
10/// CommonMark writer with flexible newline control
11///
12/// This writer uses a context-based system for intelligent newline handling,
13/// allowing fine-grained control over formatting in different scenarios.
14#[derive(Debug)]
15pub struct CommonMarkWriter {
16    /// Writer options
17    pub options: WriterOptions,
18    /// Buffer for storing the output text
19    pub(super) buffer: EcoString,
20    /// Current rendering context
21    context: NewlineContext,
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    /// use cmark_writer::ToCommonMark;
33    ///
34    /// let mut writer = CommonMarkWriter::new();
35    /// Node::Text("Hello".into()).to_commonmark(&mut writer).unwrap();
36    /// assert_eq!(writer.into_string(), "Hello");
37    /// ```
38    pub fn new() -> Self {
39        Self::with_options(WriterOptions::default())
40    }
41
42    /// Create a new CommonMark writer with specified options
43    ///
44    /// # Parameters
45    ///
46    /// * `options` - Custom CommonMark formatting options
47    ///
48    /// # Example
49    ///
50    /// ```
51    /// use cmark_writer::writer::CommonMarkWriter;
52    /// use cmark_writer::options::WriterOptions;
53    ///
54    /// let options = WriterOptions {
55    ///     strict: true,
56    ///     hard_break_spaces: false,  // Use backslash for line breaks
57    ///     indent_spaces: 2,          // Use 2 spaces for indentation
58    ///     ..Default::default()       // Other options can be set as needed
59    /// };
60    /// let writer = CommonMarkWriter::with_options(options);
61    /// ```
62    pub fn with_options(options: WriterOptions) -> Self {
63        Self {
64            options,
65            buffer: EcoString::new(),
66            context: NewlineContext::block(),
67        }
68    }
69
70    /// Create a writer with a specific rendering context
71    pub fn with_context(options: WriterOptions, context: NewlineContext) -> Self {
72        Self {
73            options,
74            buffer: EcoString::new(),
75            context,
76        }
77    }
78
79    /// Whether the writer is in strict mode
80    pub(super) fn is_strict_mode(&self) -> bool {
81        self.options.strict
82    }
83
84    /// Apply a specific prefix to multi-line text, used for handling container node indentation
85    ///
86    /// # Parameters
87    ///
88    /// * `content` - The multi-line content to process
89    /// * `prefix` - The prefix to apply to each line
90    /// * `first_line_prefix` - The prefix to apply to the first line (can be different from other lines)
91    ///
92    /// # Returns
93    ///
94    /// Returns a string with applied indentation
95    pub(super) fn apply_prefix(
96        &self,
97        content: &str,
98        prefix: &str,
99        first_line_prefix: Option<&str>,
100    ) -> EcoString {
101        if content.is_empty() {
102            return EcoString::new();
103        }
104
105        let mut result = EcoString::new();
106        let lines: Vec<&str> = content.lines().collect();
107
108        if !lines.is_empty() {
109            let actual_prefix = first_line_prefix.unwrap_or(prefix);
110            result.push_str(actual_prefix);
111            result.push_str(lines[0]);
112        }
113
114        for line in &lines[1..] {
115            result.push('\n');
116            result.push_str(prefix);
117            result.push_str(line);
118        }
119
120        result
121    }
122
123    /// Write document children with proper spacing
124    pub(super) fn write_document_children(&mut self, children: &[Node]) -> WriteResult<()> {
125        for (i, node) in children.iter().enumerate() {
126            if i > 0 {
127                self.write_node_separator(&children[i - 1], node)?;
128            }
129
130            // For the last child, be selective about trailing newlines
131            if i == children.len() - 1 {
132                // If it's a block element, add trailing newline
133                if node.is_block() {
134                    self.write_node(node)?;
135                } else {
136                    // For inline elements, don't add trailing newline
137                    self.write_node_content(node)?;
138                }
139            } else {
140                self.write_node(node)?;
141            }
142        }
143        Ok(())
144    }
145
146    /// Write node content without context-aware newline handling
147    /// This is called by write_node() which handles the newline logic
148    pub fn write_node_content(&mut self, node: &Node) -> WriteResult<()> {
149        // 处理自定义节点
150        if let Node::Custom(custom_node) = node {
151            // Ensure that CustomNode trait requires render_commonmark method
152            return custom_node.render_commonmark(self);
153        }
154
155        // 处理文档节点
156        if let Node::Document(children) = node {
157            return self.write_document_children(children);
158        }
159
160        // 在严格模式下检查内联元素中的换行符
161        if self.options.strict
162            && !node.is_block()
163            && !matches!(node, Node::SoftBreak | Node::HardBreak)
164        {
165            match node {
166                Node::Text(content) => {
167                    if content.contains('\n') {
168                        return Err(WriteError::NewlineInInlineElement("Text".into()));
169                    }
170                }
171                Node::InlineCode(content) => {
172                    if content.contains('\n') {
173                        return Err(WriteError::NewlineInInlineElement("InlineCode".into()));
174                    }
175                }
176                Node::Emphasis(children) | Node::Strong(children) => {
177                    for child in children {
178                        if let Node::Text(content) = child {
179                            if content.contains('\n') {
180                                return Err(WriteError::NewlineInInlineElement(
181                                    "Text in formatting".into(),
182                                ));
183                            }
184                        }
185                    }
186                }
187                _ => {}
188            }
189        }
190
191        // Delegate to specific writing methods
192        match node {
193            // Block elements
194            Node::Heading {
195                level,
196                content,
197                heading_type,
198            } => self.write_heading(*level, content, heading_type),
199            Node::Paragraph(content) => self.write_paragraph(content),
200            Node::BlockQuote(content) => self.write_blockquote(content),
201            Node::CodeBlock {
202                language,
203                content,
204                block_type,
205            } => self.write_code_block(language, content, block_type),
206            Node::UnorderedList(items) => self.write_unordered_list(items),
207            Node::OrderedList { start, items } => self.write_ordered_list(items, *start, true), // Default to tight
208            Node::ThematicBreak => self.write_thematic_break(),
209
210            // Inline elements
211            Node::Text(content) => self.write_text_content(content),
212            Node::Emphasis(content) => self.write_emphasis(content),
213            Node::Strong(content) => self.write_strong(content),
214            Node::InlineCode(content) => self.write_code_content(content),
215            Node::Link {
216                url,
217                title,
218                content,
219            } => self.write_link(url, title, content),
220            Node::Image { url, title, alt } => self.write_image(url, title, alt),
221            Node::SoftBreak => self.write_soft_break(),
222            Node::HardBreak => self.write_hard_break(),
223            Node::Autolink { url, is_email } => self.write_autolink(url, *is_email),
224            Node::ReferenceLink { label, content } => self.write_reference_link(label, content),
225            Node::LinkReferenceDefinition {
226                label,
227                destination,
228                title,
229            } => self.write_link_reference_definition(label, destination, title),
230
231            // HTML elements
232            Node::HtmlBlock(content) => self.write_html_block(content),
233            Node::HtmlElement(element) => self.write_html_element(element),
234
235            // Table elements
236            #[cfg(feature = "gfm")]
237            Node::Table {
238                headers,
239                alignments,
240                rows,
241            } => self.write_table_with_alignment(headers, alignments, rows),
242            #[cfg(not(feature = "gfm"))]
243            Node::Table { headers, rows, .. } => self.write_table(headers, rows),
244
245            // GFM-specific elements
246            #[cfg(feature = "gfm")]
247            Node::Strikethrough(content) => self.write_strikethrough(content),
248            #[cfg(feature = "gfm")]
249            Node::ExtendedAutolink(url) => self.write_extended_autolink(url),
250
251            // Custom nodes
252            Node::Custom(custom_node) => self.write_custom_node(custom_node),
253
254            _ => {
255                log::warn!("Unsupported node type encountered and skipped: {:?}", node);
256                Ok(())
257            }
258        }
259    }
260
261    /// Write a custom node using its implementation
262    #[allow(clippy::borrowed_box)]
263    pub(super) fn write_custom_node(&mut self, node: &Box<dyn CustomNode>) -> WriteResult<()> {
264        node.render_commonmark(self)
265    }
266
267    /// Check if the inline node contains a newline character and return an error if it does
268    pub(super) fn check_no_newline(&self, node: &Node, context: &str) -> WriteResult<()> {
269        if Self::node_contains_newline(node) {
270            if self.is_strict_mode() {
271                return Err(WriteError::NewlineInInlineElement(
272                    context.to_string().into(),
273                ));
274            } else {
275                log::warn!(
276                    "Newline character found in inline element '{}', but non-strict mode allows it (output may be affected).",
277                    context
278                );
279            }
280        }
281        Ok(())
282    }
283
284    /// Check if the inline node contains a newline character recursively
285    pub(super) fn node_contains_newline(node: &Node) -> bool {
286        match node {
287            Node::Text(s) | Node::InlineCode(s) => s.contains('\n'),
288            Node::Emphasis(children) | Node::Strong(children) => {
289                children.iter().any(Self::node_contains_newline)
290            }
291            #[cfg(feature = "gfm")]
292            Node::Strikethrough(children) => children.iter().any(Self::node_contains_newline),
293            Node::HtmlElement(element) => element.children.iter().any(Self::node_contains_newline),
294            Node::Link { content, .. } => content.iter().any(Self::node_contains_newline),
295            Node::Image { alt, .. } => alt.iter().any(Self::node_contains_newline),
296            Node::SoftBreak | Node::HardBreak => true,
297            // Custom nodes are handled separately
298            Node::Custom(_) => false,
299            _ => false,
300        }
301    }
302
303    /// Get the generated CommonMark format text
304    ///
305    /// Consumes the writer and returns the generated string
306    ///
307    /// # Example
308    ///
309    /// ```
310    /// use cmark_writer::writer::CommonMarkWriter;
311    /// use cmark_writer::ast::Node;
312    /// use cmark_writer::ToCommonMark;
313    ///
314    /// let mut writer = CommonMarkWriter::new();
315    /// Node::Text("Hello".into()).to_commonmark(&mut writer).unwrap();
316    /// let result = writer.into_string();
317    /// assert_eq!(result, "Hello");
318    /// ```
319    pub fn into_string(self) -> EcoString {
320        self.buffer
321    }
322
323    /// Write a string to the output buffer
324    ///
325    /// This method is provided for custom node implementations to use
326    pub fn write_str(&mut self, s: &str) -> WriteResult<()> {
327        self.buffer.push_str(s);
328        Ok(())
329    }
330
331    /// Write a character to the output buffer
332    ///
333    /// This method is provided for custom node implementations to use
334    pub fn write_char(&mut self, c: char) -> WriteResult<()> {
335        self.buffer.push(c);
336        Ok(())
337    }
338
339    /// Get current rendering context
340    pub fn context(&self) -> &NewlineContext {
341        &self.context
342    }
343
344    /// Get current rendering context (alias for backwards compatibility with tests)
345    pub fn current_context(&self) -> &NewlineContext {
346        &self.context
347    }
348
349    /// Set new rendering context
350    pub fn set_context(&mut self, context: NewlineContext) {
351        self.context = context;
352    }
353
354    /// Push a new context (for stack-based context management in tests)
355    pub fn push_context(&mut self, context: NewlineContext) {
356        // For simplicity in tests, just replace current context
357        // In the future, we could implement a real stack if needed
358        self.context = context;
359    }
360
361    /// Pop the current context (for stack-based context management in tests)
362    pub fn pop_context(&mut self) -> Option<NewlineContext> {
363        // Return to default context
364        let old = std::mem::take(&mut self.context);
365        Some(old)
366    }
367
368    /// Execute a closure with a temporary context
369    pub fn with_temporary_context<F, R>(&mut self, context: NewlineContext, f: F) -> WriteResult<R>
370    where
371        F: FnOnce(&mut Self) -> WriteResult<R>,
372    {
373        let original_context = std::mem::replace(&mut self.context, context);
374        let result = f(self);
375        self.context = original_context;
376        result
377    }
378
379    /// Execute a closure with a temporary context (alias for examples)
380    pub fn with_temp_context<F, R>(&mut self, context: NewlineContext, f: F) -> WriteResult<R>
381    where
382        F: FnOnce(&mut Self) -> WriteResult<R>,
383    {
384        self.with_temporary_context(context, f)
385    }
386
387    /// Write a single node with context-aware formatting
388    pub fn write_node(&mut self, node: &Node) -> WriteResult<()> {
389        // Handle document nodes specially - they manage their own newlines
390        if let Node::Document(children) = node {
391            return self.write_document_children(children);
392        }
393
394        // Validate node is allowed in current context
395        self.context.validate_node(node)?;
396
397        // Remember buffer state before writing
398        let buffer_start = self.buffer.len();
399
400        // Write the actual node content
401        self.write_node_content(node)?;
402
403        // Get the content that was just written
404        let new_content = &self.buffer[buffer_start..];
405
406        // Apply context-aware trailing newline logic
407        if self
408            .context
409            .should_add_trailing_newline(new_content, Some(node))
410        {
411            self.write_char('\n')?;
412        }
413
414        Ok(())
415    }
416
417    /// Write multiple nodes with intelligent spacing
418    pub fn write_nodes(&mut self, nodes: &[Node]) -> WriteResult<()> {
419        for (i, node) in nodes.iter().enumerate() {
420            if i > 0 {
421                self.write_node_separator(&nodes[i - 1], node)?;
422            }
423            self.write_node(node)?;
424        }
425        Ok(())
426    }
427
428    /// Write multiple nodes with intelligent spacing (alias for examples)
429    pub fn write_nodes_with_context(&mut self, nodes: &[Node]) -> WriteResult<()> {
430        self.write_nodes(nodes)
431    }
432
433    /// Write content with a specific node context (for examples)
434    pub fn write_content_with_context(
435        &mut self,
436        _node: &Node,
437        content_fn: impl FnOnce(&mut Self) -> WriteResult<()>,
438    ) -> WriteResult<()> {
439        content_fn(self)
440    }
441
442    /// Write separator between nodes based on context
443    fn write_node_separator(&mut self, prev_node: &Node, current_node: &Node) -> WriteResult<()> {
444        match self.context.mode {
445            RenderingMode::Block => {
446                // Traditional block spacing
447                if prev_node.is_block() && current_node.is_block() {
448                    self.ensure_double_newline()?;
449                }
450            }
451            RenderingMode::InlineWithBlocks => {
452                // Smart spacing for mixed content
453                if prev_node.is_block() || current_node.is_block() {
454                    self.ensure_single_newline()?;
455                }
456            }
457            RenderingMode::PureInline => {
458                // No automatic spacing
459            }
460            RenderingMode::TableCell => {
461                // Space separation
462                if !self.buffer.ends_with(' ') && !self.buffer.is_empty() {
463                    self.write_char(' ')?;
464                }
465            }
466            RenderingMode::ListItem => {
467                // Conditional newlines for list items
468                if prev_node.is_block() && current_node.is_block() {
469                    self.ensure_single_newline()?;
470                }
471            }
472            RenderingMode::Custom => {
473                // Custom logic based on strategy
474                if (prev_node.is_block() || current_node.is_block())
475                    && self.context.strategy == NewlineStrategy::Always
476                {
477                    self.ensure_single_newline()?;
478                }
479            }
480        }
481        Ok(())
482    }
483
484    /// Ensure buffer ends with a single newline
485    fn ensure_single_newline(&mut self) -> WriteResult<()> {
486        if !self.buffer.ends_with('\n') {
487            self.write_char('\n')?;
488        }
489        Ok(())
490    }
491
492    /// Ensure buffer ends with a double newline
493    fn ensure_double_newline(&mut self) -> WriteResult<()> {
494        if self.buffer.ends_with("\n\n") {
495            // Already has double newline
496        } else if self.buffer.ends_with('\n') {
497            self.write_char('\n')?;
498        } else {
499            self.write_str("\n\n")?;
500        }
501        Ok(())
502    }
503
504    /// Helper function for writing content with delimiters
505    pub(super) fn write_delimited(&mut self, content: &[Node], delimiter: &str) -> WriteResult<()> {
506        self.write_str(delimiter)?;
507
508        // Use pure inline context for delimited content (like emphasis, strong, etc.)
509        let original_context = std::mem::replace(&mut self.context, NewlineContext::pure_inline());
510
511        for node in content {
512            self.write_node_content(node)?;
513        }
514
515        self.context = original_context;
516        self.write_str(delimiter)?;
517        Ok(())
518    }
519}
520
521impl Default for CommonMarkWriter {
522    fn default() -> Self {
523        Self::new()
524    }
525}
526
527// Implement Display trait for Node structure
528impl fmt::Display for Node {
529    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
530        let mut writer = CommonMarkWriter::new();
531        let result = if self.is_block() {
532            writer.write_node(self)
533        } else {
534            // For inline elements, don't add automatic trailing newlines
535            writer.write_node_content(self)
536        };
537        match result {
538            Ok(_) => write!(f, "{}", writer.into_string()),
539            Err(e) => write!(f, "Error writing Node: {}", e),
540        }
541    }
542}