cmark_writer/writer/
cmark.rs

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