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
5#[cfg(feature = "gfm")]
6use crate::ast::TableAlignment;
7use crate::ast::{
8    CodeBlockType, CustomNode, CustomNodeWriter, HeadingType, HtmlElement, ListItem, Node,
9};
10use crate::error::{WriteError, WriteResult};
11use crate::options::WriterOptions;
12use log;
13use std::fmt::{self};
14
15use super::processors::{
16    BlockNodeProcessor, CustomNodeProcessor, InlineNodeProcessor, NodeProcessor,
17};
18
19use crate::writer::html;
20
21/// CommonMark writer
22///
23/// This struct is responsible for serializing AST nodes to CommonMark-compliant text.
24#[derive(Debug)]
25pub struct CommonMarkWriter {
26    options: WriterOptions,
27    buffer: String,
28}
29
30impl CommonMarkWriter {
31    /// Create a new CommonMark writer with default options
32    ///
33    /// # Example
34    ///
35    /// ```
36    /// use cmark_writer::writer::CommonMarkWriter;
37    /// use cmark_writer::ast::Node;
38    ///
39    /// let mut writer = CommonMarkWriter::new();
40    /// writer.write(&Node::Text("Hello".to_string())).unwrap();
41    /// assert_eq!(writer.into_string(), "Hello");
42    /// ```
43    pub fn new() -> Self {
44        Self::with_options(WriterOptions::default())
45    }
46
47    /// Create a new CommonMark writer with specified options
48    ///
49    /// # Parameters
50    ///
51    /// * `options` - Custom CommonMark formatting options
52    ///
53    /// # Example
54    ///
55    /// ```
56    /// use cmark_writer::writer::CommonMarkWriter;
57    /// use cmark_writer::options::WriterOptions;
58    ///
59    /// let options = WriterOptions {
60    ///     strict: true,
61    ///     hard_break_spaces: false,  // Use backslash for line breaks
62    ///     indent_spaces: 2,          // Use 2 spaces for indentation
63    ///     ..Default::default()       // Other options can be set as needed
64    /// };
65    /// let writer = CommonMarkWriter::with_options(options);
66    /// ```
67    pub fn with_options(options: WriterOptions) -> Self {
68        Self {
69            options,
70            buffer: String::new(),
71        }
72    }
73
74    /// Whether the writer is in strict mode
75    pub(crate) fn is_strict_mode(&self) -> bool {
76        self.options.strict
77    }
78
79    /// Apply a specific prefix to multi-line text, used for handling container node indentation
80    ///
81    /// # Parameters
82    ///
83    /// * `content` - The multi-line content to process
84    /// * `prefix` - The prefix to apply to each line
85    /// * `first_line_prefix` - The prefix to apply to the first line (can be different from other lines)
86    ///
87    /// # Returns
88    ///
89    /// Returns a string with applied indentation
90    fn apply_prefix(&self, content: &str, prefix: &str, first_line_prefix: Option<&str>) -> String {
91        if content.is_empty() {
92            return String::new();
93        }
94
95        let mut result = String::new();
96        let lines: Vec<&str> = content.lines().collect();
97
98        if !lines.is_empty() {
99            let actual_prefix = first_line_prefix.unwrap_or(prefix);
100            result.push_str(actual_prefix);
101            result.push_str(lines[0]);
102        }
103
104        for line in &lines[1..] {
105            result.push('\n');
106            result.push_str(prefix);
107            result.push_str(line);
108        }
109
110        result
111    }
112
113    /// Write an AST node as CommonMark format
114    ///
115    /// # Parameters
116    ///
117    /// * `node` - The AST node to write
118    ///
119    /// # Returns
120    ///
121    /// If writing succeeds, returns `Ok(())`, otherwise returns `Err(WriteError)`
122    ///
123    /// # Example
124    ///
125    /// ```
126    /// use cmark_writer::writer::CommonMarkWriter;
127    /// use cmark_writer::ast::Node;
128    ///
129    /// let mut writer = CommonMarkWriter::new();
130    /// writer.write(&Node::Text("Hello".to_string())).unwrap();
131    /// ```
132    pub fn write(&mut self, node: &Node) -> WriteResult<()> {
133        if let Node::Custom(_) = node {
134            return CustomNodeProcessor.process(self, node);
135        }
136
137        if node.is_block() {
138            BlockNodeProcessor.process(self, node)
139        } else if node.is_inline() {
140            InlineNodeProcessor.process(self, node)
141        } else {
142            log::warn!("Unsupported node type encountered and skipped: {:?}", node);
143            Ok(())
144        }
145    }
146
147    /// Write a custom node using its implementation
148    #[allow(clippy::borrowed_box)]
149    pub(crate) fn write_custom_node(&mut self, node: &Box<dyn CustomNode>) -> WriteResult<()> {
150        node.write(self)
151    }
152
153    /// Get context description for a node, used for error reporting
154    pub(crate) fn get_context_for_node(&self, node: &Node) -> String {
155        match node {
156            Node::Text(_) => "Text".to_string(),
157            Node::Emphasis(_) => "Emphasis".to_string(),
158            Node::Strong(_) => "Strong".to_string(),
159            #[cfg(feature = "gfm")]
160            Node::Strikethrough(_) => "Strikethrough".to_string(),
161            Node::InlineCode(_) => "InlineCode".to_string(),
162            Node::Link { .. } => "Link content".to_string(),
163            Node::Image { .. } => "Image alt text".to_string(),
164            Node::HtmlElement(_) => "HtmlElement content".to_string(),
165            Node::Custom(_) => "Custom node".to_string(),
166            _ => "Unknown inline element".to_string(),
167        }
168    }
169
170    /// Check if the inline node contains a newline character and return an error if it does
171    pub(crate) fn check_no_newline(&self, node: &Node, context: &str) -> WriteResult<()> {
172        if Self::node_contains_newline(node) {
173            if self.is_strict_mode() {
174                return Err(WriteError::NewlineInInlineElement(context.to_string()));
175            } else {
176                log::warn!(
177                    "Newline character found in inline element '{}', but non-strict mode allows it (output may be affected).",
178                    context
179                );
180            }
181        }
182        Ok(())
183    }
184
185    /// Check if the inline node contains a newline character recursively
186    fn node_contains_newline(node: &Node) -> bool {
187        match node {
188            Node::Text(s) | Node::InlineCode(s) => s.contains('\n'),
189            Node::Emphasis(children) | Node::Strong(children) => {
190                children.iter().any(Self::node_contains_newline)
191            }
192            #[cfg(feature = "gfm")]
193            Node::Strikethrough(children) => children.iter().any(Self::node_contains_newline),
194            Node::HtmlElement(element) => element.children.iter().any(Self::node_contains_newline),
195            Node::Link { content, .. } => content.iter().any(Self::node_contains_newline),
196            Node::Image { alt, .. } => alt.iter().any(Self::node_contains_newline),
197            Node::SoftBreak | Node::HardBreak => true,
198            // Custom nodes are handled separately
199            Node::Custom(_) => false,
200            _ => false,
201        }
202    }
203
204    /// Writes text content with character escaping
205    pub(crate) fn write_text_content(&mut self, content: &str) -> WriteResult<()> {
206        if self.options.escape_special_chars {
207            let escaped = content
208                .replace('\\', "\\\\")
209                .replace('*', "\\*")
210                .replace('_', "\\_")
211                .replace('[', "\\[")
212                .replace(']', "\\]")
213                .replace('<', "\\<")
214                .replace('>', "\\>")
215                .replace('`', "\\`");
216
217            self.write_str(&escaped)?;
218        } else {
219            self.write_str(content)?;
220        }
221
222        Ok(())
223    }
224
225    /// Writes inline code content
226    pub(crate) fn write_code_content(&mut self, content: &str) -> WriteResult<()> {
227        self.write_char('`')?;
228        self.write_str(content)?;
229        self.write_char('`')?;
230        Ok(())
231    }
232
233    /// Helper function for writing content with delimiters
234    pub(crate) fn write_delimited(&mut self, content: &[Node], delimiter: &str) -> WriteResult<()> {
235        self.write_str(delimiter)?;
236
237        for node in content {
238            self.write(node)?;
239        }
240
241        self.write_str(delimiter)?;
242        Ok(())
243    }
244
245    /// Write a document node
246    pub(crate) fn write_document(&mut self, children: &[Node]) -> WriteResult<()> {
247        for (i, child) in children.iter().enumerate() {
248            if i > 0 {
249                self.write_str("\n")?;
250            }
251            self.write(child)?;
252        }
253        Ok(())
254    }
255
256    /// Write a heading node
257    pub(crate) fn write_heading(
258        &mut self,
259        mut level: u8,
260        content: &[Node],
261        heading_type: &HeadingType,
262    ) -> WriteResult<()> {
263        // 验证标题级别
264        if level == 0 || level > 6 {
265            if self.is_strict_mode() {
266                return Err(WriteError::InvalidHeadingLevel(level));
267            } else {
268                let original_level = level;
269                level = level.clamp(1, 6); // Clamp level to 1-6
270                log::warn!(
271                    "Invalid heading level: {}. Corrected to {}. Strict mode is off.",
272                    original_level,
273                    level
274                );
275            }
276        }
277
278        match heading_type {
279            // ATX heading, using # character
280            HeadingType::Atx => {
281                for _ in 0..level {
282                    self.write_char('#')?;
283                }
284                self.write_char(' ')?;
285
286                for node in content {
287                    self.write(node)?;
288                }
289
290                self.write_char('\n')?;
291            }
292
293            HeadingType::Setext => {
294                // First write the heading content
295                for node in content {
296                    self.write(node)?;
297                }
298                self.write_char('\n')?;
299
300                // Add underline characters based on level
301                // Setext only supports level 1 and 2 headings
302                let underline_char = if level == 1 { '=' } else { '-' };
303
304                // For good readability, we add underlines at least as long as the heading text
305                // Calculate a reasonable underline length (at least 3 characters)
306                let min_len = 3;
307
308                // Write the underline characters
309                for _ in 0..min_len {
310                    self.write_char(underline_char)?;
311                }
312
313                // Add a newline to end the heading
314                self.write_char('\n')?;
315            }
316        }
317
318        Ok(())
319    }
320
321    /// Write a paragraph node
322    pub(crate) fn write_paragraph(&mut self, content: &[Node]) -> WriteResult<()> {
323        for node in content.iter() {
324            self.write(node)?;
325        }
326
327        Ok(())
328    }
329
330    /// Write a blockquote node
331    pub(crate) fn write_blockquote(&mut self, content: &[Node]) -> WriteResult<()> {
332        // Create a temporary writer buffer to write all blockquote content
333        let mut temp_writer = CommonMarkWriter::with_options(self.options.clone());
334
335        // Write all content to temporary buffer
336        for (i, node) in content.iter().enumerate() {
337            if i > 0 {
338                temp_writer.write_char('\n')?;
339            }
340            // Write all nodes uniformly
341            temp_writer.write(node)?;
342        }
343
344        // Get all content
345        let all_content = temp_writer.into_string();
346
347        // Apply blockquote prefix "> " uniformly
348        let prefix = "> ";
349        let formatted_content = self.apply_prefix(&all_content, prefix, Some(prefix));
350
351        // Write formatted content
352        self.buffer.push_str(&formatted_content);
353        Ok(())
354    }
355
356    /// Write a thematic break (horizontal rule)
357    pub(crate) fn write_thematic_break(&mut self) -> WriteResult<()> {
358        let char = self.options.thematic_break_char;
359        self.write_str(&format!("{}{}{}", char, char, char))?;
360        Ok(())
361    }
362
363    /// Write a code block node
364    pub(crate) fn write_code_block(
365        &mut self,
366        language: &Option<String>,
367        content: &str,
368        block_type: &CodeBlockType,
369    ) -> WriteResult<()> {
370        match block_type {
371            CodeBlockType::Indented => {
372                let indent = "    ";
373                let indented_content = self.apply_prefix(content, indent, Some(indent));
374                self.buffer.push_str(&indented_content);
375            }
376            CodeBlockType::Fenced => {
377                let max_backticks = content
378                    .chars()
379                    .fold((0, 0), |(max, current), c| {
380                        if c == '`' {
381                            (max.max(current + 1), current + 1)
382                        } else {
383                            (max, 0)
384                        }
385                    })
386                    .0;
387
388                let fence_len = std::cmp::max(max_backticks + 1, 3);
389                let fence = "`".repeat(fence_len);
390
391                self.write_str(&fence)?;
392                if let Some(lang) = language {
393                    self.write_str(lang)?;
394                }
395                self.write_char('\n')?;
396
397                self.buffer.push_str(content);
398                if !content.ends_with('\n') {
399                    self.write_char('\n')?;
400                }
401
402                self.write_str(&fence)?;
403            }
404        }
405
406        Ok(())
407    }
408
409    /// Write an unordered list node
410    pub(crate) fn write_unordered_list(&mut self, items: &[ListItem]) -> WriteResult<()> {
411        let list_marker = self.options.list_marker;
412        let prefix = format!("{} ", list_marker);
413
414        for (i, item) in items.iter().enumerate() {
415            if i > 0 {
416                self.write_char('\n')?;
417            }
418            self.write_list_item(item, &prefix)?;
419        }
420
421        Ok(())
422    }
423
424    /// Write an ordered list node
425    pub(crate) fn write_ordered_list(&mut self, start: u32, items: &[ListItem]) -> WriteResult<()> {
426        // Track the current item number
427        let mut current_number = start;
428
429        for (i, item) in items.iter().enumerate() {
430            if i > 0 {
431                self.write_char('\n')?;
432            }
433
434            match item {
435                // For ordered list items, check if there's a custom number
436                ListItem::Ordered { number, content: _ } => {
437                    if let Some(custom_num) = number {
438                        // Use custom numbering
439                        let prefix = format!("{}. ", custom_num);
440                        self.write_list_item(item, &prefix)?;
441                        // Next expected number
442                        current_number = custom_num + 1;
443                    } else {
444                        // No custom number, use the current calculated number
445                        let prefix = format!("{}. ", current_number);
446                        self.write_list_item(item, &prefix)?;
447                        current_number += 1;
448                    }
449                }
450                // For other types of list items, still use the current number
451                _ => {
452                    let prefix = format!("{}. ", current_number);
453                    self.write_list_item(item, &prefix)?;
454                    current_number += 1;
455                }
456            }
457        }
458
459        Ok(())
460    }
461
462    /// Write a list item
463    fn write_list_item(&mut self, item: &ListItem, prefix: &str) -> WriteResult<()> {
464        match item {
465            ListItem::Unordered { content } => {
466                self.write_str(prefix)?;
467                self.write_list_item_content(content, prefix.len())?;
468            }
469            ListItem::Ordered { number, content } => {
470                if let Some(num) = number {
471                    let custom_prefix = format!("{}. ", num);
472                    self.write_str(&custom_prefix)?;
473                    self.write_list_item_content(content, custom_prefix.len())?;
474                } else {
475                    self.write_str(prefix)?;
476                    self.write_list_item_content(content, prefix.len())?;
477                }
478            }
479            #[cfg(feature = "gfm")]
480            ListItem::Task { status, content } => {
481                // Only use task list syntax if GFM task lists are enabled
482                if self.options.gfm_tasklists {
483                    let checkbox = match status {
484                        crate::ast::TaskListStatus::Checked => "[x] ",
485                        crate::ast::TaskListStatus::Unchecked => "[ ] ",
486                    };
487
488                    // Use the original list marker (- or number) and append the checkbox
489                    let task_prefix = format!("{}{}", prefix, checkbox);
490                    self.write_str(&task_prefix)?;
491                    self.write_list_item_content(content, task_prefix.len())?;
492                } else {
493                    // If GFM task lists are disabled, just write a normal list item
494                    self.write_str(prefix)?;
495                    self.write_list_item_content(content, prefix.len())?;
496                }
497            }
498        }
499
500        Ok(())
501    }
502
503    /// Write list item content
504    fn write_list_item_content(&mut self, content: &[Node], prefix_len: usize) -> WriteResult<()> {
505        if content.is_empty() {
506            return Ok(());
507        }
508
509        let mut temp_writer = CommonMarkWriter::with_options(self.options.clone());
510
511        for (i, node) in content.iter().enumerate() {
512            if i > 0 {
513                temp_writer.write_char('\n')?;
514            }
515
516            temp_writer.write(node)?;
517        }
518
519        let all_content = temp_writer.into_string();
520
521        let indent = " ".repeat(prefix_len);
522
523        let formatted_content = self.apply_prefix(&all_content, &indent, Some(""));
524
525        self.buffer.push_str(&formatted_content);
526
527        Ok(())
528    }
529
530    /// Write a table
531    pub(crate) fn write_table(&mut self, headers: &[Node], rows: &[Vec<Node>]) -> WriteResult<()> {
532        // Write header
533        self.write_char('|')?;
534        for header in headers {
535            self.check_no_newline(header, "Table Header")?;
536            self.write_char(' ')?;
537            self.write(header)?;
538            self.write_str(" |")?;
539        }
540        self.write_char('\n')?;
541
542        // Write alignment row (default to centered if no alignments provided)
543        self.write_char('|')?;
544        for _ in 0..headers.len() {
545            self.write_str(" --- |")?;
546        }
547        self.write_char('\n')?;
548
549        // Write table content
550        for row in rows {
551            self.write_char('|')?;
552            for cell in row {
553                self.check_no_newline(cell, "Table Cell")?;
554                self.write_char(' ')?;
555                self.write(cell)?;
556                self.write_str(" |")?;
557            }
558            self.write_char('\n')?;
559        }
560
561        Ok(())
562    }
563
564    #[cfg(feature = "gfm")]
565    /// Write a table with alignment (GFM extension)
566    pub(crate) fn write_table_with_alignment(
567        &mut self,
568        headers: &[Node],
569        alignments: &[TableAlignment],
570        rows: &[Vec<Node>],
571    ) -> WriteResult<()> {
572        // Only use alignment when GFM tables are enabled
573        if !self.options.gfm_tables {
574            return self.write_table(headers, rows);
575        }
576
577        // Write header
578        self.write_char('|')?;
579        for header in headers {
580            self.check_no_newline(header, "Table Header")?;
581            self.write_char(' ')?;
582            self.write(header)?;
583            self.write_str(" |")?;
584        }
585        self.write_char('\n')?;
586
587        // Write alignment row
588
589        self.write_char('|')?;
590
591        // Use provided alignments, or default to center if not enough alignments provided
592        for i in 0..headers.len() {
593            let alignment = if i < alignments.len() {
594                &alignments[i]
595            } else {
596                &TableAlignment::Center
597            };
598
599            match alignment {
600                TableAlignment::Left => self.write_str(" :--- |")?,
601                TableAlignment::Center => self.write_str(" :---: |")?,
602                TableAlignment::Right => self.write_str(" ---: |")?,
603                TableAlignment::None => self.write_str(" --- |")?,
604            }
605        }
606
607        self.write_char('\n')?;
608
609        // Write table content
610        for row in rows {
611            self.write_char('|')?;
612            for cell in row {
613                self.check_no_newline(cell, "Table Cell")?;
614                self.write_char(' ')?;
615                self.write(cell)?;
616                self.write_str(" |")?;
617            }
618            self.write_char('\n')?;
619        }
620
621        Ok(())
622    }
623
624    /// Write a link
625    pub(crate) fn write_link(
626        &mut self,
627        url: &str,
628        title: &Option<String>,
629        content: &[Node],
630    ) -> WriteResult<()> {
631        for node in content {
632            self.check_no_newline(node, "Link Text")?;
633        }
634        self.write_char('[')?;
635
636        for node in content {
637            self.write(node)?;
638        }
639
640        self.write_str("](")?;
641        self.write_str(url)?;
642
643        if let Some(title_text) = title {
644            self.write_str(" \"")?;
645            self.write_str(title_text)?;
646            self.write_char('"')?;
647        }
648
649        self.write_char(')')?;
650        Ok(())
651    }
652
653    /// Write an image
654    pub(crate) fn write_image(
655        &mut self,
656        url: &str,
657        title: &Option<String>,
658        alt: &[Node],
659    ) -> WriteResult<()> {
660        // Check for newlines in alt text content
661        for node in alt {
662            self.check_no_newline(node, "Image alt text")?;
663        }
664
665        self.write_str("![")?;
666
667        // Write alt text content
668        for node in alt {
669            self.write(node)?;
670        }
671
672        self.write_str("](")?;
673        self.write_str(url)?;
674
675        if let Some(title_text) = title {
676            self.write_str(" \"")?;
677            self.write_str(title_text)?;
678            self.write_char('"')?;
679        }
680
681        self.write_char(')')?;
682        Ok(())
683    }
684
685    /// Write a soft line break
686    pub(crate) fn write_soft_break(&mut self) -> WriteResult<()> {
687        self.write_char('\n')?;
688        Ok(())
689    }
690
691    /// Write a hard line break
692    pub(crate) fn write_hard_break(&mut self) -> WriteResult<()> {
693        if self.options.hard_break_spaces {
694            self.write_str("  \n")?;
695        } else {
696            self.write_str("\\\n")?;
697        }
698        Ok(())
699    }
700
701    /// Write an HTML block
702    pub(crate) fn write_html_block(&mut self, content: &str) -> WriteResult<()> {
703        self.buffer.push_str(content);
704
705        Ok(())
706    }
707
708    /// Write an autolink (URI or email address wrapped in < and >)
709    pub(crate) fn write_autolink(&mut self, url: &str, is_email: bool) -> WriteResult<()> {
710        // Autolinks shouldn't contain newlines
711        if url.contains('\n') {
712            if self.is_strict_mode() {
713                return Err(WriteError::NewlineInInlineElement(
714                    "Autolink URL".to_string(),
715                ));
716            } else {
717                log::warn!(
718                    "Newline character found in autolink URL '{}'. Writing it as is, which might result in an invalid link. Strict mode is off.",
719                    url
720                );
721                // Continue to write the URL as is, including the newline.
722            }
723        }
724
725        // Write the autolink with < and > delimiters
726        self.write_char('<')?;
727
728        // For email autolinks, we don't need to add any prefix
729        // For URI autolinks, ensure it has a scheme
730        if !is_email && !url.contains(':') {
731            // Default to https if no scheme is provided
732            self.write_str("https://")?;
733        }
734
735        self.write_str(url)?;
736        self.write_char('>')?;
737
738        Ok(())
739    }
740
741    /// Write an extended autolink (GFM extension)
742    #[cfg(feature = "gfm")]
743    pub(crate) fn write_extended_autolink(&mut self, url: &str) -> WriteResult<()> {
744        if !self.options.gfm_autolinks {
745            // If GFM autolinks are disabled, write as plain text
746            self.write_text_content(url)?;
747            return Ok(());
748        }
749
750        // Autolinks shouldn't contain newlines
751        if url.contains('\n') {
752            if self.is_strict_mode() {
753                // Or a specific gfm_autolinks_strict option if desired
754                return Err(WriteError::NewlineInInlineElement(
755                    "Extended Autolink URL".to_string(),
756                ));
757            } else {
758                log::warn!(
759                    "Newline character found in extended autolink URL '{}'. Writing it as is, which might result in an invalid link. Strict mode is off.",
760                    url
761                );
762                // Continue to write the URL as is, including the newline.
763            }
764        }
765
766        // Just write the URL as plain text for extended autolinks (no angle brackets)
767        self.write_str(url)?;
768
769        Ok(())
770    }
771
772    /// Write a link reference definition
773    pub(crate) fn write_link_reference_definition(
774        &mut self,
775        label: &str,
776        destination: &str,
777        title: &Option<String>,
778    ) -> WriteResult<()> {
779        // Format: [label]: destination "optional title"
780        self.write_char('[')?;
781        self.write_str(label)?;
782        self.write_str("]: ")?;
783        self.write_str(destination)?;
784
785        if let Some(title_text) = title {
786            self.write_str(" \"")?;
787            self.write_str(title_text)?;
788            self.write_char('"')?;
789        }
790
791        Ok(())
792    }
793
794    /// Write a reference link
795    pub(crate) fn write_reference_link(
796        &mut self,
797        label: &str,
798        content: &[Node],
799    ) -> WriteResult<()> {
800        // Check for newlines in content
801        for node in content {
802            self.check_no_newline(node, "Reference Link Text")?;
803        }
804
805        // If content is empty or exactly matches the label (as plain text),
806        // this is a shortcut reference link: [label]
807        if content.is_empty() {
808            self.write_char('[')?;
809            self.write_str(label)?;
810            self.write_char(']')?;
811            return Ok(());
812        }
813
814        // Check if content is exactly the same as the label (to use shortcut syntax)
815        let is_shortcut =
816            content.len() == 1 && matches!(&content[0], Node::Text(text) if text == label);
817
818        if is_shortcut {
819            // Use shortcut reference link syntax: [label]
820            self.write_char('[')?;
821            self.write_str(label)?;
822            self.write_char(']')?;
823        } else {
824            // Use full reference link syntax: [content][label]
825            self.write_char('[')?;
826
827            for node in content {
828                self.write(node)?;
829            }
830
831            self.write_str("][")?;
832            self.write_str(label)?;
833            self.write_char(']')?;
834        }
835
836        Ok(())
837    }
838
839    /// Write an HTML element
840    pub(crate) fn write_html_element(&mut self, element: &HtmlElement) -> WriteResult<()> {
841        #[cfg(feature = "gfm")]
842        let mut html_render_options = html::HtmlRenderOptions::default();
843        #[cfg(not(feature = "gfm"))]
844        let html_render_options = html::HtmlRenderOptions::default();
845
846        #[cfg(feature = "gfm")]
847        {
848            html_render_options.enable_gfm = self.options.enable_gfm;
849            html_render_options.gfm_disallowed_html_tags =
850                self.options.gfm_disallowed_html_tags.clone();
851        }
852
853        // code_block_language_class_prefix from HtmlRenderOptions::default() is used.
854
855        let mut html_output_buffer = Vec::new();
856        let mut temp_html_writer =
857            html::HtmlWriter::new(std::io::Cursor::new(&mut html_output_buffer));
858        let node_to_render = Node::HtmlElement(element.clone());
859
860        temp_html_writer.write_node(&node_to_render, &html_render_options)?;
861        temp_html_writer
862            .flush()
863            .map_err(html::error::HtmlWriteError::Io)?;
864
865        let html_string =
866            String::from_utf8(html_output_buffer).map_err(|utf8_err| WriteError::Custom {
867                message: format!(
868                    "HTML output from HtmlWriter was not valid UTF-8: {}",
869                    utf8_err
870                ),
871                code: None,
872            })?;
873
874        self.buffer.push_str(&html_string);
875        Ok(())
876    }
877
878    /// Get the generated CommonMark format text
879    ///
880    /// Consumes the writer and returns the generated string
881    ///
882    /// # Example
883    ///
884    /// ```
885    /// use cmark_writer::writer::CommonMarkWriter;
886    /// use cmark_writer::ast::Node;
887    ///
888    /// let mut writer = CommonMarkWriter::new();
889    /// writer.write(&Node::Text("Hello".to_string())).unwrap();
890    /// let result = writer.into_string();
891    /// assert_eq!(result, "Hello");
892    /// ```
893    pub fn into_string(self) -> String {
894        self.buffer
895    }
896    /// Ensure content ends with a newline (for consistent handling at the end of block nodes)
897    ///
898    /// Adds a newline character if the content doesn't already end with one; does nothing if it already ends with a newline
899    pub(crate) fn ensure_trailing_newline(&mut self) -> WriteResult<()> {
900        if !self.buffer.ends_with('\n') {
901            self.write_char('\n')?;
902        }
903        Ok(())
904    }
905
906    /// Write an emphasis (italic) node with custom delimiter
907    pub(crate) fn write_emphasis(&mut self, content: &[Node]) -> WriteResult<()> {
908        let delimiter = self.options.emphasis_char.to_string();
909        self.write_delimited(content, &delimiter)
910    }
911
912    /// Write a strong emphasis (bold) node with custom delimiter
913    pub(crate) fn write_strong(&mut self, content: &[Node]) -> WriteResult<()> {
914        let char = self.options.strong_char;
915        let delimiter = format!("{}{}", char, char);
916        self.write_delimited(content, &delimiter)
917    }
918
919    /// Write a strikethrough node (GFM extension)
920    #[cfg(feature = "gfm")]
921    pub(crate) fn write_strikethrough(&mut self, content: &[Node]) -> WriteResult<()> {
922        if !self.options.enable_gfm || !self.options.gfm_strikethrough {
923            // If GFM strikethrough is disabled, just write the content without strikethrough
924            for node in content.iter() {
925                self.write(node)?;
926            }
927            return Ok(());
928        }
929
930        // Write content with ~~ delimiters
931        self.write_delimited(content, "~~")
932    }
933}
934
935impl Default for CommonMarkWriter {
936    fn default() -> Self {
937        Self::new()
938    }
939}
940
941// Implement Display trait for Node structure
942impl fmt::Display for Node {
943    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
944        let mut writer = CommonMarkWriter::new();
945        match writer.write(self) {
946            Ok(_) => write!(f, "{}", writer.into_string()),
947            Err(e) => write!(f, "Error writing Node: {}", e),
948        }
949    }
950}
951
952// Implement CustomNodeWriter trait for CommonMarkWriter
953impl CustomNodeWriter for CommonMarkWriter {
954    fn write_str(&mut self, s: &str) -> fmt::Result {
955        self.buffer.push_str(s);
956        Ok(())
957    }
958
959    fn write_char(&mut self, c: char) -> fmt::Result {
960        self.buffer.push(c);
961        Ok(())
962    }
963}