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        if self.options.trim_paragraph_trailing_hard_breaks {
320            let mut last_non_hard_break_index = content.len();
321
322            while last_non_hard_break_index > 0 {
323                if !matches!(content[last_non_hard_break_index - 1], Node::HardBreak) {
324                    break;
325                }
326                last_non_hard_break_index -= 1;
327            }
328
329            for node in content.iter().take(last_non_hard_break_index) {
330                self.write(node)?;
331            }
332        } else {
333            for node in content {
334                self.write(node)?;
335            }
336        }
337
338        Ok(())
339    }
340
341    /// Write a blockquote node
342    pub(crate) fn write_blockquote(&mut self, content: &[Node]) -> WriteResult<()> {
343        // Create a temporary writer buffer to write all blockquote content
344        let mut temp_writer = CommonMarkWriter::with_options(self.options.clone());
345
346        // Write all content to temporary buffer
347        for (i, node) in content.iter().enumerate() {
348            if i > 0 {
349                temp_writer.write_char('\n')?;
350            }
351            // Write all nodes uniformly
352            temp_writer.write(node)?;
353        }
354
355        // Get all content
356        let all_content = temp_writer.into_string();
357
358        // Apply blockquote prefix "> " uniformly
359        let prefix = "> ";
360        let formatted_content = self.apply_prefix(&all_content, prefix, Some(prefix));
361
362        // Write formatted content
363        self.buffer.push_str(&formatted_content);
364        Ok(())
365    }
366
367    /// Write a thematic break (horizontal rule)
368    pub(crate) fn write_thematic_break(&mut self) -> WriteResult<()> {
369        let char = self.options.thematic_break_char;
370        self.write_str(&format!("{}{}{}", char, char, char))?;
371        Ok(())
372    }
373
374    /// Write a code block node
375    pub(crate) fn write_code_block(
376        &mut self,
377        language: &Option<EcoString>,
378        content: &str,
379        block_type: &CodeBlockType,
380    ) -> WriteResult<()> {
381        match block_type {
382            CodeBlockType::Indented => {
383                let indent = "    ";
384                let indented_content = self.apply_prefix(content, indent, Some(indent));
385                self.buffer.push_str(&indented_content);
386            }
387            CodeBlockType::Fenced => {
388                let max_backticks = content
389                    .chars()
390                    .fold((0, 0), |(max, current), c| {
391                        if c == '`' {
392                            (max.max(current + 1), current + 1)
393                        } else {
394                            (max, 0)
395                        }
396                    })
397                    .0;
398
399                let fence_len = std::cmp::max(max_backticks + 1, 3);
400                let fence = "`".repeat(fence_len);
401
402                self.write_str(&fence)?;
403                if let Some(lang) = language {
404                    self.write_str(lang)?;
405                }
406                self.write_char('\n')?;
407
408                self.buffer.push_str(content);
409                if !content.ends_with('\n') {
410                    self.write_char('\n')?;
411                }
412
413                self.write_str(&fence)?;
414            }
415        }
416
417        Ok(())
418    }
419
420    /// Write an unordered list node
421    pub(crate) fn write_unordered_list(&mut self, items: &[ListItem]) -> WriteResult<()> {
422        let list_marker = self.options.list_marker;
423        let prefix = format!("{} ", list_marker);
424
425        for (i, item) in items.iter().enumerate() {
426            if i > 0 {
427                self.write_char('\n')?;
428            }
429            self.write_list_item(item, &prefix)?;
430        }
431
432        Ok(())
433    }
434
435    /// Write an ordered list node
436    pub(crate) fn write_ordered_list(&mut self, start: u32, items: &[ListItem]) -> WriteResult<()> {
437        // Track the current item number
438        let mut current_number = start;
439
440        for (i, item) in items.iter().enumerate() {
441            if i > 0 {
442                self.write_char('\n')?;
443            }
444
445            match item {
446                // For ordered list items, check if there's a custom number
447                ListItem::Ordered { number, content: _ } => {
448                    if let Some(custom_num) = number {
449                        // Use custom numbering
450                        let prefix = format!("{}. ", custom_num);
451                        self.write_list_item(item, &prefix)?;
452                        // Next expected number
453                        current_number = custom_num + 1;
454                    } else {
455                        // No custom number, use the current calculated number
456                        let prefix = format!("{}. ", current_number);
457                        self.write_list_item(item, &prefix)?;
458                        current_number += 1;
459                    }
460                }
461                // For other types of list items, still use the current number
462                _ => {
463                    let prefix = format!("{}. ", current_number);
464                    self.write_list_item(item, &prefix)?;
465                    current_number += 1;
466                }
467            }
468        }
469
470        Ok(())
471    }
472
473    /// Write a list item
474    fn write_list_item(&mut self, item: &ListItem, prefix: &str) -> WriteResult<()> {
475        match item {
476            ListItem::Unordered { content } => {
477                self.write_str(prefix)?;
478                self.write_list_item_content(content, prefix.len())?;
479            }
480            ListItem::Ordered { number, content } => {
481                if let Some(num) = number {
482                    let custom_prefix = format!("{}. ", num);
483                    self.write_str(&custom_prefix)?;
484                    self.write_list_item_content(content, custom_prefix.len())?;
485                } else {
486                    self.write_str(prefix)?;
487                    self.write_list_item_content(content, prefix.len())?;
488                }
489            }
490            #[cfg(feature = "gfm")]
491            ListItem::Task { status, content } => {
492                // Only use task list syntax if GFM task lists are enabled
493                if self.options.gfm_tasklists {
494                    let checkbox = match status {
495                        crate::ast::TaskListStatus::Checked => "[x] ",
496                        crate::ast::TaskListStatus::Unchecked => "[ ] ",
497                    };
498
499                    // Use the original list marker (- or number) and append the checkbox
500                    let task_prefix = format!("{}{}", prefix, checkbox);
501                    self.write_str(&task_prefix)?;
502                    self.write_list_item_content(content, task_prefix.len())?;
503                } else {
504                    // If GFM task lists are disabled, just write a normal list item
505                    self.write_str(prefix)?;
506                    self.write_list_item_content(content, prefix.len())?;
507                }
508            }
509        }
510
511        Ok(())
512    }
513
514    /// Write list item content
515    fn write_list_item_content(&mut self, content: &[Node], prefix_len: usize) -> WriteResult<()> {
516        if content.is_empty() {
517            return Ok(());
518        }
519
520        let mut temp_writer = CommonMarkWriter::with_options(self.options.clone());
521
522        for (i, node) in content.iter().enumerate() {
523            if i > 0 {
524                temp_writer.write_char('\n')?;
525            }
526
527            temp_writer.write(node)?;
528        }
529
530        let all_content = temp_writer.into_string();
531
532        let indent = " ".repeat(prefix_len);
533
534        let formatted_content = self.apply_prefix(&all_content, &indent, Some(""));
535
536        self.buffer.push_str(&formatted_content);
537
538        Ok(())
539    }
540
541    /// Write a table
542    pub(crate) fn write_table(&mut self, headers: &[Node], rows: &[Vec<Node>]) -> WriteResult<()> {
543        // Write header
544        self.write_char('|')?;
545        for header in headers {
546            self.check_no_newline(header, "Table Header")?;
547            self.write_char(' ')?;
548            self.write(header)?;
549            self.write_str(" |")?;
550        }
551        self.write_char('\n')?;
552
553        // Write alignment row (default to centered if no alignments provided)
554        self.write_char('|')?;
555        for _ in 0..headers.len() {
556            self.write_str(" --- |")?;
557        }
558        self.write_char('\n')?;
559
560        // Write table content
561        for row in rows {
562            self.write_char('|')?;
563            for cell in row {
564                self.check_no_newline(cell, "Table Cell")?;
565                self.write_char(' ')?;
566                self.write(cell)?;
567                self.write_str(" |")?;
568            }
569            self.write_char('\n')?;
570        }
571
572        Ok(())
573    }
574
575    #[cfg(feature = "gfm")]
576    /// Write a table with alignment (GFM extension)
577    pub(crate) fn write_table_with_alignment(
578        &mut self,
579        headers: &[Node],
580        alignments: &[TableAlignment],
581        rows: &[Vec<Node>],
582    ) -> WriteResult<()> {
583        // Only use alignment when GFM tables are enabled
584        if !self.options.gfm_tables {
585            return self.write_table(headers, rows);
586        }
587
588        // Write header
589        self.write_char('|')?;
590        for header in headers {
591            self.check_no_newline(header, "Table Header")?;
592            self.write_char(' ')?;
593            self.write(header)?;
594            self.write_str(" |")?;
595        }
596        self.write_char('\n')?;
597
598        // Write alignment row
599
600        self.write_char('|')?;
601
602        // Use provided alignments, or default to center if not enough alignments provided
603        for i in 0..headers.len() {
604            let alignment = if i < alignments.len() {
605                &alignments[i]
606            } else {
607                &TableAlignment::Center
608            };
609
610            match alignment {
611                TableAlignment::Left => self.write_str(" :--- |")?,
612                TableAlignment::Center => self.write_str(" :---: |")?,
613                TableAlignment::Right => self.write_str(" ---: |")?,
614                TableAlignment::None => self.write_str(" --- |")?,
615            }
616        }
617
618        self.write_char('\n')?;
619
620        // Write table content
621        for row in rows {
622            self.write_char('|')?;
623            for cell in row {
624                self.check_no_newline(cell, "Table Cell")?;
625                self.write_char(' ')?;
626                self.write(cell)?;
627                self.write_str(" |")?;
628            }
629            self.write_char('\n')?;
630        }
631
632        Ok(())
633    }
634
635    /// Write a link
636    pub(crate) fn write_link(
637        &mut self,
638        url: &str,
639        title: &Option<EcoString>,
640        content: &[Node],
641    ) -> WriteResult<()> {
642        for node in content {
643            self.check_no_newline(node, "Link Text")?;
644        }
645        self.write_char('[')?;
646
647        for node in content {
648            self.write(node)?;
649        }
650
651        self.write_str("](")?;
652        self.write_str(url)?;
653
654        if let Some(title_text) = title {
655            self.write_str(" \"")?;
656            self.write_str(title_text)?;
657            self.write_char('"')?;
658        }
659
660        self.write_char(')')?;
661        Ok(())
662    }
663
664    /// Write an image
665    pub(crate) fn write_image(
666        &mut self,
667        url: &str,
668        title: &Option<EcoString>,
669        alt: &[Node],
670    ) -> WriteResult<()> {
671        // Check for newlines in alt text content
672        for node in alt {
673            self.check_no_newline(node, "Image alt text")?;
674        }
675
676        self.write_str("![")?;
677
678        // Write alt text content
679        for node in alt {
680            self.write(node)?;
681        }
682
683        self.write_str("](")?;
684        self.write_str(url)?;
685
686        if let Some(title_text) = title {
687            self.write_str(" \"")?;
688            self.write_str(title_text)?;
689            self.write_char('"')?;
690        }
691
692        self.write_char(')')?;
693        Ok(())
694    }
695
696    /// Write a soft line break
697    pub(crate) fn write_soft_break(&mut self) -> WriteResult<()> {
698        self.write_char('\n')?;
699        Ok(())
700    }
701
702    /// Write a hard line break
703    pub(crate) fn write_hard_break(&mut self) -> WriteResult<()> {
704        if self.options.hard_break_spaces {
705            self.write_str("  \n")?;
706        } else {
707            self.write_str("\\\n")?;
708        }
709        Ok(())
710    }
711
712    /// Write an HTML block
713    pub(crate) fn write_html_block(&mut self, content: &str) -> WriteResult<()> {
714        self.buffer.push_str(content);
715
716        Ok(())
717    }
718
719    /// Write an autolink (URI or email address wrapped in < and >)
720    pub(crate) fn write_autolink(&mut self, url: &str, is_email: bool) -> WriteResult<()> {
721        // Autolinks shouldn't contain newlines
722        if url.contains('\n') {
723            if self.is_strict_mode() {
724                return Err(WriteError::NewlineInInlineElement(
725                    "Autolink URL".to_string().into(),
726                ));
727            } else {
728                log::warn!(
729                    "Newline character found in autolink URL '{}'. Writing it as is, which might result in an invalid link. Strict mode is off.",
730                    url
731                );
732                // Continue to write the URL as is, including the newline.
733            }
734        }
735
736        // Write the autolink with < and > delimiters
737        self.write_char('<')?;
738
739        // For email autolinks, we don't need to add any prefix
740        // For URI autolinks, ensure it has a scheme
741        if !is_email && !url.contains(':') {
742            // Default to https if no scheme is provided
743            self.write_str("https://")?;
744        }
745
746        self.write_str(url)?;
747        self.write_char('>')?;
748
749        Ok(())
750    }
751
752    /// Write an extended autolink (GFM extension)
753    #[cfg(feature = "gfm")]
754    pub(crate) fn write_extended_autolink(&mut self, url: &str) -> WriteResult<()> {
755        if !self.options.gfm_autolinks {
756            // If GFM autolinks are disabled, write as plain text
757            self.write_text_content(url)?;
758            return Ok(());
759        }
760
761        // Autolinks shouldn't contain newlines
762        if url.contains('\n') {
763            if self.is_strict_mode() {
764                // Or a specific gfm_autolinks_strict option if desired
765                return Err(WriteError::NewlineInInlineElement(
766                    "Extended Autolink URL".to_string().into(),
767                ));
768            } else {
769                log::warn!(
770                    "Newline character found in extended autolink URL '{}'. Writing it as is, which might result in an invalid link. Strict mode is off.",
771                    url
772                );
773                // Continue to write the URL as is, including the newline.
774            }
775        }
776
777        // Just write the URL as plain text for extended autolinks (no angle brackets)
778        self.write_str(url)?;
779
780        Ok(())
781    }
782
783    /// Write a link reference definition
784    pub(crate) fn write_link_reference_definition(
785        &mut self,
786        label: &str,
787        destination: &str,
788        title: &Option<EcoString>,
789    ) -> WriteResult<()> {
790        // Format: [label]: destination "optional title"
791        self.write_char('[')?;
792        self.write_str(label)?;
793        self.write_str("]: ")?;
794        self.write_str(destination)?;
795
796        if let Some(title_text) = title {
797            self.write_str(" \"")?;
798            self.write_str(title_text)?;
799            self.write_char('"')?;
800        }
801
802        Ok(())
803    }
804
805    /// Write a reference link
806    pub(crate) fn write_reference_link(
807        &mut self,
808        label: &str,
809        content: &[Node],
810    ) -> WriteResult<()> {
811        // Check for newlines in content
812        for node in content {
813            self.check_no_newline(node, "Reference Link Text")?;
814        }
815
816        // If content is empty or exactly matches the label (as plain text),
817        // this is a shortcut reference link: [label]
818        if content.is_empty() {
819            self.write_char('[')?;
820            self.write_str(label)?;
821            self.write_char(']')?;
822            return Ok(());
823        }
824
825        // Check if content is exactly the same as the label (to use shortcut syntax)
826        let is_shortcut =
827            content.len() == 1 && matches!(&content[0], Node::Text(text) if text == label);
828
829        if is_shortcut {
830            // Use shortcut reference link syntax: [label]
831            self.write_char('[')?;
832            self.write_str(label)?;
833            self.write_char(']')?;
834        } else {
835            // Use full reference link syntax: [content][label]
836            self.write_char('[')?;
837
838            for node in content {
839                self.write(node)?;
840            }
841
842            self.write_str("][")?;
843            self.write_str(label)?;
844            self.write_char(']')?;
845        }
846
847        Ok(())
848    }
849
850    /// Write an AST HtmlElement node as raw HTML string into the CommonMark output.
851    pub(crate) fn write_html_element(
852        &mut self,
853        element: &crate::ast::HtmlElement,
854    ) -> WriteResult<()> {
855        if self.options.strict {
856            if element.tag.contains('<') || element.tag.contains('>') {
857                return Err(WriteError::InvalidHtmlTag(element.tag.clone()));
858            }
859
860            for attr in &element.attributes {
861                if attr.name.contains('<') || attr.name.contains('>') {
862                    return Err(WriteError::InvalidHtmlAttribute(attr.name.clone()));
863                }
864            }
865        }
866
867        use crate::writer::html::{HtmlWriter, HtmlWriterOptions};
868
869        // 从 CommonMarkWriter 的选项中派生 HTML 渲染选项
870        let html_options = HtmlWriterOptions {
871            // 使用相同的严格模式设置
872            strict: self.options.strict,
873            // 代码块语言类前缀,保持默认
874            code_block_language_class_prefix: Some("language-".into()),
875            #[cfg(feature = "gfm")]
876            enable_gfm: self.options.enable_gfm,
877            #[cfg(feature = "gfm")]
878            gfm_disallowed_html_tags: self.options.gfm_disallowed_html_tags.clone(),
879        };
880
881        let mut html_writer = HtmlWriter::with_options(html_options);
882
883        html_writer.write_node(&Node::HtmlElement(element.clone()))?;
884
885        // Get the generated HTML
886        let html_output = html_writer.into_string();
887
888        // Otherwise write the raw HTML
889        self.write_str(&html_output)
890    }
891
892    /// Get the generated CommonMark format text
893    ///
894    /// Consumes the writer and returns the generated string
895    ///
896    /// # Example
897    ///
898    /// ```
899    /// use cmark_writer::writer::CommonMarkWriter;
900    /// use cmark_writer::ast::Node;
901    ///
902    /// let mut writer = CommonMarkWriter::new();
903    /// writer.write(&Node::Text("Hello".into())).unwrap();
904    /// let result = writer.into_string();
905    /// assert_eq!(result, "Hello");
906    /// ```
907    pub fn into_string(self) -> EcoString {
908        self.buffer
909    }
910
911    /// Write a string to the output buffer
912    ///
913    /// This method is provided for custom node implementations to use
914    pub fn write_str(&mut self, s: &str) -> WriteResult<()> {
915        self.buffer.push_str(s);
916        Ok(())
917    }
918
919    /// Write a character to the output buffer
920    ///
921    /// This method is provided for custom node implementations to use
922    pub fn write_char(&mut self, c: char) -> WriteResult<()> {
923        self.buffer.push(c);
924        Ok(())
925    }
926    /// Ensure content ends with a newline (for consistent handling at the end of block nodes)
927    ///
928    /// Adds a newline character if the content doesn't already end with one; does nothing if it already ends with a newline
929    pub(crate) fn ensure_trailing_newline(&mut self) -> WriteResult<()> {
930        if !self.buffer.ends_with('\n') {
931            self.write_char('\n')?;
932        }
933        Ok(())
934    }
935
936    /// Write an emphasis (italic) node with custom delimiter
937    pub(crate) fn write_emphasis(&mut self, content: &[Node]) -> WriteResult<()> {
938        let delimiter = self.options.emphasis_char.to_string();
939        self.write_delimited(content, &delimiter)
940    }
941
942    /// Write a strong emphasis (bold) node with custom delimiter
943    pub(crate) fn write_strong(&mut self, content: &[Node]) -> WriteResult<()> {
944        let char = self.options.strong_char;
945        let delimiter = format!("{}{}", char, char);
946        self.write_delimited(content, &delimiter)
947    }
948
949    /// Write a strikethrough node (GFM extension)
950    #[cfg(feature = "gfm")]
951    pub(crate) fn write_strikethrough(&mut self, content: &[Node]) -> WriteResult<()> {
952        if !self.options.enable_gfm || !self.options.gfm_strikethrough {
953            // If GFM strikethrough is disabled, just write the content without strikethrough
954            for node in content.iter() {
955                self.write(node)?;
956            }
957            return Ok(());
958        }
959
960        // Write content with ~~ delimiters
961        self.write_delimited(content, "~~")
962    }
963}
964
965impl Default for CommonMarkWriter {
966    fn default() -> Self {
967        Self::new()
968    }
969}
970
971// Implement Display trait for Node structure
972impl fmt::Display for Node {
973    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
974        let mut writer = CommonMarkWriter::new();
975        match writer.write(self) {
976            Ok(_) => write!(f, "{}", writer.into_string()),
977            Err(e) => write!(f, "Error writing Node: {}", e),
978        }
979    }
980}
981
982/// A trait for character escaping behavior
983pub(crate) trait Escapes {
984    /// Checks if the string needs escaping
985    fn str_needs_escaping(s: &str) -> bool;
986
987    /// Returns true if the character needs to be escaped
988    fn char_needs_escaping(c: char) -> bool;
989
990    /// Returns the escaped version of a character (if needed)
991    fn escape_char(c: char) -> Option<&'static str>;
992}
993
994/// Markdown escaping implementation for CommonMark
995pub(crate) struct CommonMarkEscapes;
996
997impl Escapes for CommonMarkEscapes {
998    fn str_needs_escaping(s: &str) -> bool {
999        s.chars().any(Self::char_needs_escaping)
1000    }
1001
1002    fn char_needs_escaping(c: char) -> bool {
1003        matches!(c, '\\' | '*' | '_' | '[' | ']' | '<' | '>' | '`')
1004    }
1005
1006    fn escape_char(c: char) -> Option<&'static str> {
1007        match c {
1008            '\\' => Some(r"\\"),
1009            '*' => Some(r"\*"),
1010            '_' => Some(r"\_"),
1011            '[' => Some(r"\["),
1012            ']' => Some(r"\]"),
1013            '<' => Some(r"\<"),
1014            '>' => Some(r"\>"),
1015            '`' => Some(r"\`"),
1016            _ => None,
1017        }
1018    }
1019}
1020
1021/// A wrapper for efficient escaping
1022pub(crate) struct Escaped<'a, E: Escapes> {
1023    inner: &'a str,
1024    _phantom: std::marker::PhantomData<E>,
1025}
1026
1027impl<'a, E: Escapes> Escaped<'a, E> {
1028    /// Create a new Escaped wrapper
1029    pub fn new(s: &'a str) -> Self {
1030        Self {
1031            inner: s,
1032            _phantom: std::marker::PhantomData,
1033        }
1034    }
1035}
1036
1037impl<E: Escapes> std::fmt::Display for Escaped<'_, E> {
1038    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1039        for c in self.inner.chars() {
1040            if E::char_needs_escaping(c) {
1041                f.write_str(E::escape_char(c).unwrap())?;
1042            } else {
1043                write!(f, "{}", c)?;
1044            }
1045        }
1046        Ok(())
1047    }
1048}
1049
1050/// Escapes a string using the specified escaping strategy
1051pub(crate) fn escape_str<E: Escapes>(s: &str) -> std::borrow::Cow<'_, str> {
1052    if E::str_needs_escaping(s) {
1053        std::borrow::Cow::Owned(format!("{}", Escaped::<E>::new(s)))
1054    } else {
1055        std::borrow::Cow::Borrowed(s)
1056    }
1057}