cmark_writer/writer/
cmark.rs

1//! CommonMark writer implementation.
2//!
3//! This file contains the implementation of the CommonMarkWriter class, which serializes AST nodes to CommonMark-compliant text.
4
5use super::processors::{
6    BlockNodeProcessor, CustomNodeProcessor, InlineNodeProcessor, NodeProcessor,
7};
8#[cfg(feature = "gfm")]
9use crate::ast::TableAlignment;
10use crate::ast::{CodeBlockType, CustomNode, HeadingType, ListItem, Node};
11use crate::error::{WriteError, WriteResult};
12use crate::options::WriterOptions;
13use ecow::EcoString;
14use log;
15use std::fmt::{self};
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: EcoString,
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".into())).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: EcoString::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(
89        &self,
90        content: &str,
91        prefix: &str,
92        first_line_prefix: Option<&str>,
93    ) -> EcoString {
94        if content.is_empty() {
95            return EcoString::new();
96        }
97
98        let mut result = EcoString::new();
99        let lines: Vec<&str> = content.lines().collect();
100
101        if !lines.is_empty() {
102            let actual_prefix = first_line_prefix.unwrap_or(prefix);
103            result.push_str(actual_prefix);
104            result.push_str(lines[0]);
105        }
106
107        for line in &lines[1..] {
108            result.push('\n');
109            result.push_str(prefix);
110            result.push_str(line);
111        }
112
113        result
114    }
115
116    /// Write an AST node as CommonMark format
117    ///
118    /// # Parameters
119    ///
120    /// * `node` - The AST node to write
121    ///
122    /// # Returns
123    ///
124    /// If writing succeeds, returns `Ok(())`, otherwise returns `Err(WriteError)`
125    ///
126    /// # Example
127    ///
128    /// ```
129    /// use cmark_writer::writer::CommonMarkWriter;
130    /// use cmark_writer::ast::Node;
131    ///
132    /// let mut writer = CommonMarkWriter::new();
133    /// writer.write(&Node::Text("Hello".into())).unwrap();
134    /// ```
135    pub fn write(&mut self, node: &Node) -> WriteResult<()> {
136        if let Node::Custom(_) = node {
137            return CustomNodeProcessor.process(self, node);
138        }
139
140        if node.is_block() {
141            BlockNodeProcessor.process(self, node)
142        } else if node.is_inline() {
143            InlineNodeProcessor.process(self, node)
144        } else {
145            log::warn!("Unsupported node type encountered and skipped: {:?}", node);
146            Ok(())
147        }
148    }
149
150    /// Write a custom node using its implementation
151    #[allow(clippy::borrowed_box)]
152    pub(crate) fn write_custom_node(&mut self, node: &Box<dyn CustomNode>) -> WriteResult<()> {
153        node.write(self)
154    }
155
156    /// Get context description for a node, used for error reporting
157    pub(crate) fn get_context_for_node(&self, node: &Node) -> EcoString {
158        match node {
159            Node::Text(_) => "Text".into(),
160            Node::Emphasis(_) => "Emphasis".into(),
161            Node::Strong(_) => "Strong".into(),
162            #[cfg(feature = "gfm")]
163            Node::Strikethrough(_) => "Strikethrough".into(),
164            Node::InlineCode(_) => "InlineCode".into(),
165            Node::Link { .. } => "Link content".into(),
166            Node::Image { .. } => "Image alt text".into(),
167            Node::HtmlElement(_) => "HtmlElement content".into(),
168            Node::Custom(_) => "Custom node".into(),
169            _ => "Unknown inline element".into(),
170        }
171    }
172
173    /// Check if the inline node contains a newline character and return an error if it does
174    pub(crate) fn check_no_newline(&self, node: &Node, context: &str) -> WriteResult<()> {
175        if Self::node_contains_newline(node) {
176            if self.is_strict_mode() {
177                return Err(WriteError::NewlineInInlineElement(
178                    context.to_string().into(),
179                ));
180            } else {
181                log::warn!(
182                    "Newline character found in inline element '{}', but non-strict mode allows it (output may be affected).",
183                    context
184                );
185            }
186        }
187        Ok(())
188    }
189
190    /// Check if the inline node contains a newline character recursively
191    fn node_contains_newline(node: &Node) -> bool {
192        match node {
193            Node::Text(s) | Node::InlineCode(s) => s.contains('\n'),
194            Node::Emphasis(children) | Node::Strong(children) => {
195                children.iter().any(Self::node_contains_newline)
196            }
197            #[cfg(feature = "gfm")]
198            Node::Strikethrough(children) => children.iter().any(Self::node_contains_newline),
199            Node::HtmlElement(element) => element.children.iter().any(Self::node_contains_newline),
200            Node::Link { content, .. } => content.iter().any(Self::node_contains_newline),
201            Node::Image { alt, .. } => alt.iter().any(Self::node_contains_newline),
202            Node::SoftBreak | Node::HardBreak => true,
203            // Custom nodes are handled separately
204            Node::Custom(_) => false,
205            _ => false,
206        }
207    }
208
209    /// Writes text content with character escaping
210    pub(crate) fn write_text_content(&mut self, content: &str) -> WriteResult<()> {
211        if self.options.escape_special_chars {
212            let escaped = escape_str::<CommonMarkEscapes>(content);
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<EcoString>,
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<EcoString>,
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<EcoString>,
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().into(),
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().into(),
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<EcoString>,
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        if self.options.strict {
841            if element.tag.contains('<') || element.tag.contains('>') {
842                return Err(WriteError::InvalidHtmlTag(element.tag.clone()));
843            }
844
845            for attr in &element.attributes {
846                if attr.name.contains('<') || attr.name.contains('>') {
847                    return Err(WriteError::InvalidHtmlAttribute(attr.name.clone()));
848                }
849            }
850        }
851
852        use crate::writer::html::{HtmlWriter, HtmlWriterOptions};
853
854        // 从 CommonMarkWriter 的选项中派生 HTML 渲染选项
855        let html_options = HtmlWriterOptions {
856            // 使用相同的严格模式设置
857            strict: self.options.strict,
858            // 代码块语言类前缀,保持默认
859            code_block_language_class_prefix: Some("language-".into()),
860            #[cfg(feature = "gfm")]
861            enable_gfm: self.options.enable_gfm,
862            #[cfg(feature = "gfm")]
863            gfm_disallowed_html_tags: self.options.gfm_disallowed_html_tags.clone(),
864        };
865
866        let mut html_writer = HtmlWriter::with_options(html_options);
867
868        html_writer.write_node(&Node::HtmlElement(element.clone()))?;
869
870        // Get the generated HTML
871        let html_output = html_writer.into_string();
872
873        // Otherwise write the raw HTML
874        self.write_str(&html_output)
875    }
876
877    /// Get the generated CommonMark format text
878    ///
879    /// Consumes the writer and returns the generated string
880    ///
881    /// # Example
882    ///
883    /// ```
884    /// use cmark_writer::writer::CommonMarkWriter;
885    /// use cmark_writer::ast::Node;
886    ///
887    /// let mut writer = CommonMarkWriter::new();
888    /// writer.write(&Node::Text("Hello".into())).unwrap();
889    /// let result = writer.into_string();
890    /// assert_eq!(result, "Hello");
891    /// ```
892    pub fn into_string(self) -> EcoString {
893        self.buffer
894    }
895
896    /// Write a string to the output buffer
897    ///
898    /// This method is provided for custom node implementations to use
899    pub fn write_str(&mut self, s: &str) -> WriteResult<()> {
900        self.buffer.push_str(s);
901        Ok(())
902    }
903
904    /// Write a character to the output buffer
905    ///
906    /// This method is provided for custom node implementations to use
907    pub fn write_char(&mut self, c: char) -> WriteResult<()> {
908        self.buffer.push(c);
909        Ok(())
910    }
911    /// Ensure content ends with a newline (for consistent handling at the end of block nodes)
912    ///
913    /// Adds a newline character if the content doesn't already end with one; does nothing if it already ends with a newline
914    pub(crate) fn ensure_trailing_newline(&mut self) -> WriteResult<()> {
915        if !self.buffer.ends_with('\n') {
916            self.write_char('\n')?;
917        }
918        Ok(())
919    }
920
921    /// Write an emphasis (italic) node with custom delimiter
922    pub(crate) fn write_emphasis(&mut self, content: &[Node]) -> WriteResult<()> {
923        let delimiter = self.options.emphasis_char.to_string();
924        self.write_delimited(content, &delimiter)
925    }
926
927    /// Write a strong emphasis (bold) node with custom delimiter
928    pub(crate) fn write_strong(&mut self, content: &[Node]) -> WriteResult<()> {
929        let char = self.options.strong_char;
930        let delimiter = format!("{}{}", char, char);
931        self.write_delimited(content, &delimiter)
932    }
933
934    /// Write a strikethrough node (GFM extension)
935    #[cfg(feature = "gfm")]
936    pub(crate) fn write_strikethrough(&mut self, content: &[Node]) -> WriteResult<()> {
937        if !self.options.enable_gfm || !self.options.gfm_strikethrough {
938            // If GFM strikethrough is disabled, just write the content without strikethrough
939            for node in content.iter() {
940                self.write(node)?;
941            }
942            return Ok(());
943        }
944
945        // Write content with ~~ delimiters
946        self.write_delimited(content, "~~")
947    }
948}
949
950impl Default for CommonMarkWriter {
951    fn default() -> Self {
952        Self::new()
953    }
954}
955
956// Implement Display trait for Node structure
957impl fmt::Display for Node {
958    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
959        let mut writer = CommonMarkWriter::new();
960        match writer.write(self) {
961            Ok(_) => write!(f, "{}", writer.into_string()),
962            Err(e) => write!(f, "Error writing Node: {}", e),
963        }
964    }
965}
966
967/// A trait for character escaping behavior
968pub(crate) trait Escapes {
969    /// Checks if the string needs escaping
970    fn str_needs_escaping(s: &str) -> bool;
971
972    /// Returns true if the character needs to be escaped
973    fn char_needs_escaping(c: char) -> bool;
974
975    /// Returns the escaped version of a character (if needed)
976    fn escape_char(c: char) -> Option<&'static str>;
977}
978
979/// Markdown escaping implementation for CommonMark
980pub(crate) struct CommonMarkEscapes;
981
982impl Escapes for CommonMarkEscapes {
983    fn str_needs_escaping(s: &str) -> bool {
984        s.chars().any(Self::char_needs_escaping)
985    }
986
987    fn char_needs_escaping(c: char) -> bool {
988        matches!(c, '\\' | '*' | '_' | '[' | ']' | '<' | '>' | '`')
989    }
990
991    fn escape_char(c: char) -> Option<&'static str> {
992        match c {
993            '\\' => Some(r"\\"),
994            '*' => Some(r"\*"),
995            '_' => Some(r"\_"),
996            '[' => Some(r"\["),
997            ']' => Some(r"\]"),
998            '<' => Some(r"\<"),
999            '>' => Some(r"\>"),
1000            '`' => Some(r"\`"),
1001            _ => None,
1002        }
1003    }
1004}
1005
1006/// A wrapper for efficient escaping
1007pub(crate) struct Escaped<'a, E: Escapes> {
1008    inner: &'a str,
1009    _phantom: std::marker::PhantomData<E>,
1010}
1011
1012impl<'a, E: Escapes> Escaped<'a, E> {
1013    /// Create a new Escaped wrapper
1014    pub fn new(s: &'a str) -> Self {
1015        Self {
1016            inner: s,
1017            _phantom: std::marker::PhantomData,
1018        }
1019    }
1020}
1021
1022impl<E: Escapes> std::fmt::Display for Escaped<'_, E> {
1023    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1024        for c in self.inner.chars() {
1025            if E::char_needs_escaping(c) {
1026                f.write_str(E::escape_char(c).unwrap())?;
1027            } else {
1028                write!(f, "{}", c)?;
1029            }
1030        }
1031        Ok(())
1032    }
1033}
1034
1035/// Escapes a string using the specified escaping strategy
1036pub(crate) fn escape_str<E: Escapes>(s: &str) -> std::borrow::Cow<'_, str> {
1037    if E::str_needs_escaping(s) {
1038        std::borrow::Cow::Owned(format!("{}", Escaped::<E>::new(s)))
1039    } else {
1040        std::borrow::Cow::Borrowed(s)
1041    }
1042}