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