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