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        let escaped = content
197            .replace('\\', "\\\\")
198            .replace('*', "\\*")
199            .replace('_', "\\_")
200            .replace('[', "\\[")
201            .replace(']', "\\]")
202            .replace('<', "\\<")
203            .replace('>', "\\>")
204            .replace('`', "\\`");
205
206        self.write_str(&escaped)?;
207        Ok(())
208    }
209
210    /// Writes inline code content
211    pub(crate) fn write_code_content(&mut self, content: &str) -> WriteResult<()> {
212        self.write_char('`')?;
213        self.write_str(content)?;
214        self.write_char('`')?;
215        Ok(())
216    }
217
218    /// Helper function for writing content with delimiters
219    pub(crate) fn write_delimited(&mut self, content: &[Node], delimiter: &str) -> WriteResult<()> {
220        self.write_str(delimiter)?;
221
222        for node in content {
223            self.write(node)?;
224        }
225
226        self.write_str(delimiter)?;
227        Ok(())
228    }
229
230    /// Write a document node
231    pub(crate) fn write_document(&mut self, children: &[Node]) -> WriteResult<()> {
232        for (i, child) in children.iter().enumerate() {
233            if i > 0 {
234                self.write_str("\n")?;
235            }
236            self.write(child)?;
237        }
238        Ok(())
239    }
240
241    /// Write a heading node
242    pub(crate) fn write_heading(
243        &mut self,
244        level: u8,
245        content: &[Node],
246        heading_type: &HeadingType,
247    ) -> WriteResult<()> {
248        // 验证标题级别
249        if level == 0 || level > 6 {
250            return Err(WriteError::InvalidHeadingLevel(level));
251        }
252
253        match heading_type {
254            // ATX heading, using # character
255            HeadingType::Atx => {
256                for _ in 0..level {
257                    self.write_char('#')?;
258                }
259                self.write_char(' ')?;
260
261                for node in content {
262                    self.write(node)?;
263                }
264
265                self.write_char('\n')?;
266            }
267
268            HeadingType::Setext => {
269                // First write the heading content
270                for node in content {
271                    self.write(node)?;
272                }
273                self.write_char('\n')?;
274
275                // Add underline characters based on level
276                // Setext only supports level 1 and 2 headings
277                let underline_char = if level == 1 { '=' } else { '-' };
278
279                // For good readability, we add underlines at least as long as the heading text
280                // Calculate a reasonable underline length (at least 3 characters)
281                let min_len = 3;
282
283                // Write the underline characters
284                for _ in 0..min_len {
285                    self.write_char(underline_char)?;
286                }
287
288                // Add a newline to end the heading
289                self.write_char('\n')?;
290            }
291        }
292
293        Ok(())
294    }
295
296    /// Write a paragraph node
297    pub(crate) fn write_paragraph(&mut self, content: &[Node]) -> WriteResult<()> {
298        for node in content.iter() {
299            self.write(node)?;
300        }
301
302        Ok(())
303    }
304
305    /// Write a blockquote node
306    pub(crate) fn write_blockquote(&mut self, content: &[Node]) -> WriteResult<()> {
307        // Create a temporary writer buffer to write all blockquote content
308        let mut temp_writer = CommonMarkWriter::with_options(self.options.clone());
309
310        // Write all content to temporary buffer
311        for (i, node) in content.iter().enumerate() {
312            if i > 0 {
313                temp_writer.write_char('\n')?;
314            }
315            // Write all nodes uniformly
316            temp_writer.write(node)?;
317        }
318
319        // Get all content
320        let all_content = temp_writer.into_string();
321
322        // Apply blockquote prefix "> " uniformly
323        let prefix = "> ";
324        let formatted_content = self.apply_prefix(&all_content, prefix, Some(prefix));
325
326        // Write formatted content
327        self.buffer.push_str(&formatted_content);
328        Ok(())
329    }
330
331    /// Write a thematic break (horizontal rule)
332    pub(crate) fn write_thematic_break(&mut self) -> WriteResult<()> {
333        self.write_str("---")?;
334        Ok(())
335    }
336
337    /// Write a code block node
338    pub(crate) fn write_code_block(
339        &mut self,
340        language: &Option<String>,
341        content: &str,
342        block_type: &CodeBlockType,
343    ) -> WriteResult<()> {
344        match block_type {
345            CodeBlockType::Indented => {
346                let indent = "    ";
347                let indented_content = self.apply_prefix(content, indent, Some(indent));
348                self.buffer.push_str(&indented_content);
349            }
350            CodeBlockType::Fenced => {
351                let max_backticks = content
352                    .chars()
353                    .fold((0, 0), |(max, current), c| {
354                        if c == '`' {
355                            (max.max(current + 1), current + 1)
356                        } else {
357                            (max, 0)
358                        }
359                    })
360                    .0;
361
362                let fence_len = std::cmp::max(max_backticks + 1, 3);
363                let fence = "`".repeat(fence_len);
364
365                self.write_str(&fence)?;
366                if let Some(lang) = language {
367                    self.write_str(lang)?;
368                }
369                self.write_char('\n')?;
370
371                self.buffer.push_str(content);
372                if !content.ends_with('\n') {
373                    self.write_char('\n')?;
374                }
375
376                self.write_str(&fence)?;
377            }
378        }
379
380        Ok(())
381    }
382
383    /// Write an unordered list node
384    pub(crate) fn write_unordered_list(&mut self, items: &[ListItem]) -> WriteResult<()> {
385        for (i, item) in items.iter().enumerate() {
386            if i > 0 {
387                self.write_char('\n')?;
388            }
389            self.write_list_item(item, "- ")?;
390        }
391
392        Ok(())
393    }
394
395    /// Write an ordered list node
396    pub(crate) fn write_ordered_list(&mut self, start: u32, items: &[ListItem]) -> WriteResult<()> {
397        // Track the current item number
398        let mut current_number = start;
399
400        for (i, item) in items.iter().enumerate() {
401            if i > 0 {
402                self.write_char('\n')?;
403            }
404
405            match item {
406                // For ordered list items, check if there's a custom number
407                ListItem::Ordered { number, content: _ } => {
408                    if let Some(custom_num) = number {
409                        // Use custom numbering
410                        let prefix = format!("{}. ", custom_num);
411                        self.write_list_item(item, &prefix)?;
412                        // Next expected number
413                        current_number = custom_num + 1;
414                    } else {
415                        // No custom number, use the current calculated number
416                        let prefix = format!("{}. ", current_number);
417                        self.write_list_item(item, &prefix)?;
418                        current_number += 1;
419                    }
420                }
421                // For other types of list items, still use the current number
422                _ => {
423                    let prefix = format!("{}. ", current_number);
424                    self.write_list_item(item, &prefix)?;
425                    current_number += 1;
426                }
427            }
428        }
429
430        Ok(())
431    }
432
433    /// Write a list item
434    fn write_list_item(&mut self, item: &ListItem, prefix: &str) -> WriteResult<()> {
435        match item {
436            ListItem::Unordered { content } => {
437                self.write_str(prefix)?;
438                self.write_list_item_content(content, prefix.len())?;
439            }
440            ListItem::Ordered { number, content } => {
441                if let Some(num) = number {
442                    let custom_prefix = format!("{}. ", num);
443                    self.write_str(&custom_prefix)?;
444                    self.write_list_item_content(content, custom_prefix.len())?;
445                } else {
446                    self.write_str(prefix)?;
447                    self.write_list_item_content(content, prefix.len())?;
448                }
449            }
450            #[cfg(feature = "gfm")]
451            ListItem::Task { status, content } => {
452                // Only use task list syntax if GFM task lists are enabled
453                if self.options.gfm_tasklists {
454                    let checkbox = match status {
455                        crate::ast::TaskListStatus::Checked => "[x] ",
456                        crate::ast::TaskListStatus::Unchecked => "[ ] ",
457                    };
458
459                    // Use the original list marker (- or number) and append the checkbox
460                    let task_prefix = format!("{}{}", prefix, checkbox);
461                    self.write_str(&task_prefix)?;
462                    self.write_list_item_content(content, task_prefix.len())?;
463                } else {
464                    // If GFM task lists are disabled, just write a normal list item
465                    self.write_str(prefix)?;
466                    self.write_list_item_content(content, prefix.len())?;
467                }
468            }
469        }
470
471        Ok(())
472    }
473
474    /// Write list item content
475    fn write_list_item_content(&mut self, content: &[Node], prefix_len: usize) -> WriteResult<()> {
476        if content.is_empty() {
477            return Ok(());
478        }
479
480        let mut temp_writer = CommonMarkWriter::with_options(self.options.clone());
481
482        for (i, node) in content.iter().enumerate() {
483            if i > 0 {
484                temp_writer.write_char('\n')?;
485            }
486
487            temp_writer.write(node)?;
488        }
489
490        let all_content = temp_writer.into_string();
491
492        let indent = " ".repeat(prefix_len);
493
494        let formatted_content = self.apply_prefix(&all_content, &indent, Some(""));
495
496        self.buffer.push_str(&formatted_content);
497
498        Ok(())
499    }
500
501    /// Write a table
502    pub(crate) fn write_table(&mut self, headers: &[Node], rows: &[Vec<Node>]) -> WriteResult<()> {
503        // Write header
504        self.write_char('|')?;
505        for header in headers {
506            self.check_no_newline(header, "Table Header")?;
507            self.write_char(' ')?;
508            self.write(header)?;
509            self.write_str(" |")?;
510        }
511        self.write_char('\n')?;
512
513        // Write alignment row (default to centered if no alignments provided)
514        self.write_char('|')?;
515        for _ in 0..headers.len() {
516            self.write_str(" --- |")?;
517        }
518        self.write_char('\n')?;
519
520        // Write table content
521        for row in rows {
522            self.write_char('|')?;
523            for cell in row {
524                self.check_no_newline(cell, "Table Cell")?;
525                self.write_char(' ')?;
526                self.write(cell)?;
527                self.write_str(" |")?;
528            }
529            self.write_char('\n')?;
530        }
531
532        Ok(())
533    }
534
535    #[cfg(feature = "gfm")]
536    /// Write a table with alignment (GFM extension)
537    pub(crate) fn write_table_with_alignment(
538        &mut self,
539        headers: &[Node],
540        alignments: &[TableAlignment],
541        rows: &[Vec<Node>],
542    ) -> WriteResult<()> {
543        // Only use alignment when GFM tables are enabled
544        if !self.options.gfm_tables {
545            return self.write_table(headers, rows);
546        }
547
548        // Write header
549        self.write_char('|')?;
550        for header in headers {
551            self.check_no_newline(header, "Table Header")?;
552            self.write_char(' ')?;
553            self.write(header)?;
554            self.write_str(" |")?;
555        }
556        self.write_char('\n')?;
557
558        // Write alignment row
559        self.write_char('|')?;
560
561        // Use provided alignments, or default to center if not enough alignments provided
562        for i in 0..headers.len() {
563            let alignment = if i < alignments.len() {
564                &alignments[i]
565            } else {
566                &TableAlignment::Center
567            };
568
569            match alignment {
570                TableAlignment::Left => self.write_str(" :--- |")?,
571                TableAlignment::Center => self.write_str(" :---: |")?,
572                TableAlignment::Right => self.write_str(" ---: |")?,
573                TableAlignment::None => self.write_str(" --- |")?,
574            }
575        }
576
577        self.write_char('\n')?;
578
579        // Write table content
580        for row in rows {
581            self.write_char('|')?;
582            for cell in row {
583                self.check_no_newline(cell, "Table Cell")?;
584                self.write_char(' ')?;
585                self.write(cell)?;
586                self.write_str(" |")?;
587            }
588            self.write_char('\n')?;
589        }
590
591        Ok(())
592    }
593
594    /// Write a link
595    pub(crate) fn write_link(
596        &mut self,
597        url: &str,
598        title: &Option<String>,
599        content: &[Node],
600    ) -> WriteResult<()> {
601        for node in content {
602            self.check_no_newline(node, "Link Text")?;
603        }
604        self.write_char('[')?;
605
606        for node in content {
607            self.write(node)?;
608        }
609
610        self.write_str("](")?;
611        self.write_str(url)?;
612
613        if let Some(title_text) = title {
614            self.write_str(" \"")?;
615            self.write_str(title_text)?;
616            self.write_char('"')?;
617        }
618
619        self.write_char(')')?;
620        Ok(())
621    }
622
623    /// Write an image
624    pub(crate) fn write_image(
625        &mut self,
626        url: &str,
627        title: &Option<String>,
628        alt: &[Node],
629    ) -> WriteResult<()> {
630        // Check for newlines in alt text content
631        for node in alt {
632            self.check_no_newline(node, "Image alt text")?;
633        }
634
635        self.write_str("![")?;
636
637        // Write alt text content
638        for node in alt {
639            self.write(node)?;
640        }
641
642        self.write_str("](")?;
643        self.write_str(url)?;
644
645        if let Some(title_text) = title {
646            self.write_str(" \"")?;
647            self.write_str(title_text)?;
648            self.write_char('"')?;
649        }
650
651        self.write_char(')')?;
652        Ok(())
653    }
654
655    /// Write a soft line break
656    pub(crate) fn write_soft_break(&mut self) -> WriteResult<()> {
657        self.write_char('\n')?;
658        Ok(())
659    }
660
661    /// Write a hard line break
662    pub(crate) fn write_hard_break(&mut self) -> WriteResult<()> {
663        if self.options.hard_break_spaces {
664            self.write_str("  \n")?;
665        } else {
666            self.write_str("\\\n")?;
667        }
668        Ok(())
669    }
670
671    /// Write an HTML block
672    pub(crate) fn write_html_block(&mut self, content: &str) -> WriteResult<()> {
673        self.buffer.push_str(content);
674
675        Ok(())
676    }
677
678    /// Write an autolink (URI or email address wrapped in < and >)
679    pub(crate) fn write_autolink(&mut self, url: &str, is_email: bool) -> WriteResult<()> {
680        // Autolinks shouldn't contain newlines
681        if url.contains('\n') {
682            return Err(WriteError::NewlineInInlineElement(
683                "Autolink URL".to_string(),
684            ));
685        }
686
687        // Write the autolink with < and > delimiters
688        self.write_char('<')?;
689
690        // For email autolinks, we don't need to add any prefix
691        // For URI autolinks, ensure it has a scheme
692        if !is_email && !url.contains(':') {
693            // Default to https if no scheme is provided
694            self.write_str("https://")?;
695        }
696
697        self.write_str(url)?;
698        self.write_char('>')?;
699
700        Ok(())
701    }
702
703    /// Write an extended autolink (GFM extension)
704    #[cfg(feature = "gfm")]
705    pub(crate) fn write_extended_autolink(&mut self, url: &str) -> WriteResult<()> {
706        if !self.options.gfm_autolinks {
707            // If GFM autolinks are disabled, write as plain text
708            self.write_text_content(url)?;
709            return Ok(());
710        }
711
712        // Autolinks shouldn't contain newlines
713        if url.contains('\n') {
714            return Err(WriteError::NewlineInInlineElement(
715                "Extended Autolink URL".to_string(),
716            ));
717        }
718
719        // Just write the URL as plain text for extended autolinks (no angle brackets)
720        self.write_str(url)?;
721
722        Ok(())
723    }
724
725    /// Write a link reference definition
726    pub(crate) fn write_link_reference_definition(
727        &mut self,
728        label: &str,
729        destination: &str,
730        title: &Option<String>,
731    ) -> WriteResult<()> {
732        // Format: [label]: destination "optional title"
733        self.write_char('[')?;
734        self.write_str(label)?;
735        self.write_str("]: ")?;
736        self.write_str(destination)?;
737
738        if let Some(title_text) = title {
739            self.write_str(" \"")?;
740            self.write_str(title_text)?;
741            self.write_char('"')?;
742        }
743
744        Ok(())
745    }
746
747    /// Write a reference link
748    pub(crate) fn write_reference_link(
749        &mut self,
750        label: &str,
751        content: &[Node],
752    ) -> WriteResult<()> {
753        // Check for newlines in content
754        for node in content {
755            self.check_no_newline(node, "Reference Link Text")?;
756        }
757
758        // If content is empty or exactly matches the label (as plain text),
759        // this is a shortcut reference link: [label]
760        if content.is_empty() {
761            self.write_char('[')?;
762            self.write_str(label)?;
763            self.write_char(']')?;
764            return Ok(());
765        }
766
767        // Check if content is exactly the same as the label (to use shortcut syntax)
768        let is_shortcut =
769            content.len() == 1 && matches!(&content[0], Node::Text(text) if text == label);
770
771        if is_shortcut {
772            // Use shortcut reference link syntax: [label]
773            self.write_char('[')?;
774            self.write_str(label)?;
775            self.write_char(']')?;
776        } else {
777            // Use full reference link syntax: [content][label]
778            self.write_char('[')?;
779
780            for node in content {
781                self.write(node)?;
782            }
783
784            self.write_str("][")?;
785            self.write_str(label)?;
786            self.write_char(']')?;
787        }
788
789        Ok(())
790    }
791
792    /// Write an HTML element
793    pub(crate) fn write_html_element(&mut self, element: &HtmlElement) -> WriteResult<()> {
794        // Check if the HTML element is disallowed in GFM mode
795        #[cfg(feature = "gfm")]
796        if self.options.enable_gfm
797            && self
798                .options
799                .gfm_disallowed_html_tags
800                .iter()
801                .any(|tag| tag.eq_ignore_ascii_case(&element.tag))
802        {
803            // If the tag is disallowed, write it as escaped text to prevent HTML rendering
804            self.write_str("&lt;")?;
805            self.write_str(&element.tag)?;
806
807            for attr in &element.attributes {
808                self.write_char(' ')?;
809                self.write_str(&attr.name)?;
810                self.write_str("=\"")?;
811                self.write_str(&escape_attribute_value(&attr.value))?;
812                self.write_char('"')?;
813            }
814
815            if element.self_closing {
816                self.write_str(" /&gt;")?;
817                return Ok(());
818            }
819
820            self.write_str("&gt;")?;
821
822            for child in &element.children {
823                self.write(child)?;
824            }
825
826            self.write_str("&lt;/")?;
827            self.write_str(&element.tag)?;
828            self.write_str("&gt;")?;
829            return Ok(());
830        }
831
832        // 检查标签名是否包含潜在危险字符
833        if !is_safe_tag_name(&element.tag) {
834            return Err(WriteError::InvalidHtmlTag(element.tag.clone()));
835        }
836
837        // Normal HTML element rendering if not disallowed or if GFM mode is not enabled
838        self.write_char('<')?;
839        self.write_str(&element.tag)?;
840
841        for attr in &element.attributes {
842            // 检查属性名是否安全
843            if !is_safe_attribute_name(&attr.name) {
844                return Err(WriteError::InvalidHtmlAttribute(attr.name.clone()));
845            }
846
847            self.write_char(' ')?;
848            self.write_str(&attr.name)?;
849            self.write_str("=\"")?;
850            self.write_str(&escape_attribute_value(&attr.value))?;
851            self.write_char('"')?;
852        }
853
854        if element.self_closing {
855            self.write_str(" />")?;
856            return Ok(());
857        }
858
859        self.write_char('>')?;
860
861        for child in &element.children {
862            // HTML element content can contain newlines, so no strict check here
863            self.write(child)?;
864        }
865
866        self.write_str("</")?;
867        self.write_str(&element.tag)?;
868        self.write_char('>')?;
869        Ok(())
870    }
871
872    /// Get the generated CommonMark format text
873    ///
874    /// Consumes the writer and returns the generated string
875    ///
876    /// # Example
877    ///
878    /// ```
879    /// use cmark_writer::writer::CommonMarkWriter;
880    /// use cmark_writer::ast::Node;
881    ///
882    /// let mut writer = CommonMarkWriter::new();
883    /// writer.write(&Node::Text("Hello".to_string())).unwrap();
884    /// let result = writer.into_string();
885    /// assert_eq!(result, "Hello");
886    /// ```
887    pub fn into_string(self) -> String {
888        self.buffer
889    }
890    /// Ensure content ends with a newline (for consistent handling at the end of block nodes)
891    ///
892    /// Adds a newline character if the content doesn't already end with one; does nothing if it already ends with a newline
893    pub(crate) fn ensure_trailing_newline(&mut self) -> WriteResult<()> {
894        if !self.buffer.ends_with('\n') {
895            self.write_char('\n')?;
896        }
897        Ok(())
898    }
899
900    /// Write a strikethrough node (GFM extension)
901    #[cfg(feature = "gfm")]
902    pub(crate) fn write_strikethrough(&mut self, content: &[Node]) -> WriteResult<()> {
903        if !self.options.enable_gfm || !self.options.gfm_strikethrough {
904            // If GFM strikethrough is disabled, just write the content without strikethrough
905            for node in content.iter() {
906                self.write(node)?;
907            }
908            return Ok(());
909        }
910
911        // Write content with ~~ delimiters
912        self.write_delimited(content, "~~")
913    }
914}
915
916impl Default for CommonMarkWriter {
917    fn default() -> Self {
918        Self::new()
919    }
920}
921
922// Implement Display trait for Node structure
923impl fmt::Display for Node {
924    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
925        let mut writer = CommonMarkWriter::new();
926        match writer.write(self) {
927            Ok(_) => write!(f, "{}", writer.into_string()),
928            Err(e) => write!(f, "Error writing Node: {}", e),
929        }
930    }
931}
932
933// Implement CustomNodeWriter trait for CommonMarkWriter
934impl CustomNodeWriter for CommonMarkWriter {
935    fn write_str(&mut self, s: &str) -> fmt::Result {
936        self.buffer.push_str(s);
937        Ok(())
938    }
939
940    fn write_char(&mut self, c: char) -> fmt::Result {
941        self.buffer.push(c);
942        Ok(())
943    }
944}
945// Helper functions for HTML safety
946
947/// Check if an HTML tag name is safe
948///
949/// Tag names should only contain letters, numbers, underscores, colons, and hyphens
950fn is_safe_tag_name(tag: &str) -> bool {
951    !tag.is_empty()
952        && tag
953            .chars()
954            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == ':' || c == '-')
955}
956
957/// Check if an HTML attribute name is safe
958///
959/// Attribute names should only contain letters, numbers, underscores, colons, dots, and hyphens
960fn is_safe_attribute_name(name: &str) -> bool {
961    !name.is_empty()
962        && name
963            .chars()
964            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == ':' || c == '-' || c == '.')
965}
966
967/// Escape special characters in HTML attribute values
968///
969/// Converts quotes, less-than signs, greater-than signs, and other special characters to HTML entities
970fn escape_attribute_value(value: &str) -> String {
971    value
972        .replace('&', "&amp;")
973        .replace('"', "&quot;")
974        .replace('\'', "&#39;")
975        .replace('<', "&lt;")
976        .replace('>', "&gt;")
977}