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    /// Check if a table contains any block-level elements in headers or cells
210    fn table_contains_block_elements(headers: &[Node], rows: &[Vec<Node>]) -> bool {
211        // Check headers for block elements
212        if headers.iter().any(|node| node.is_block()) {
213            return true;
214        }
215
216        // Check all cells in all rows for block elements
217        rows.iter()
218            .any(|row| row.iter().any(|node| node.is_block()))
219    }
220
221    /// Writes text content with character escaping
222    pub(crate) fn write_text_content(&mut self, content: &str) -> WriteResult<()> {
223        if self.options.escape_special_chars {
224            let escaped = escape_str::<CommonMarkEscapes>(content);
225            self.write_str(&escaped)?
226        } else {
227            self.write_str(content)?
228        }
229
230        Ok(())
231    }
232
233    /// Writes inline code content
234    pub(crate) fn write_code_content(&mut self, content: &str) -> WriteResult<()> {
235        self.write_char('`')?;
236        self.write_str(content)?;
237        self.write_char('`')?;
238        Ok(())
239    }
240
241    /// Helper function for writing content with delimiters
242    pub(crate) fn write_delimited(&mut self, content: &[Node], delimiter: &str) -> WriteResult<()> {
243        self.write_str(delimiter)?;
244
245        for node in content {
246            self.write(node)?;
247        }
248
249        self.write_str(delimiter)?;
250        Ok(())
251    }
252
253    /// Write a document node
254    pub(crate) fn write_document(&mut self, children: &[Node]) -> WriteResult<()> {
255        for (i, child) in children.iter().enumerate() {
256            if i > 0 {
257                self.write_str("\n")?;
258            }
259            self.write(child)?;
260        }
261        Ok(())
262    }
263
264    /// Write a heading node
265    pub(crate) fn write_heading(
266        &mut self,
267        mut level: u8,
268        content: &[Node],
269        heading_type: &HeadingType,
270    ) -> WriteResult<()> {
271        // 验证标题级别
272        if level == 0 || level > 6 {
273            if self.is_strict_mode() {
274                return Err(WriteError::InvalidHeadingLevel(level));
275            } else {
276                let original_level = level;
277                level = level.clamp(1, 6); // Clamp level to 1-6
278                log::warn!(
279                    "Invalid heading level: {}. Corrected to {}. Strict mode is off.",
280                    original_level,
281                    level
282                );
283            }
284        }
285
286        match heading_type {
287            // ATX heading, using # character
288            HeadingType::Atx => {
289                for _ in 0..level {
290                    self.write_char('#')?;
291                }
292                self.write_char(' ')?;
293
294                for node in content {
295                    self.write(node)?;
296                }
297
298                self.write_char('\n')?;
299            }
300
301            HeadingType::Setext => {
302                // First write the heading content
303                for node in content {
304                    self.write(node)?;
305                }
306                self.write_char('\n')?;
307
308                // Add underline characters based on level
309                // Setext only supports level 1 and 2 headings
310                let underline_char = if level == 1 { '=' } else { '-' };
311
312                // For good readability, we add underlines at least as long as the heading text
313                // Calculate a reasonable underline length (at least 3 characters)
314                let min_len = 3;
315
316                // Write the underline characters
317                for _ in 0..min_len {
318                    self.write_char(underline_char)?;
319                }
320
321                // Add a newline to end the heading
322                self.write_char('\n')?;
323            }
324        }
325
326        Ok(())
327    }
328
329    /// Write a paragraph node
330    pub(crate) fn write_paragraph(&mut self, content: &[Node]) -> WriteResult<()> {
331        if self.options.trim_paragraph_trailing_hard_breaks {
332            let mut last_non_hard_break_index = content.len();
333
334            while last_non_hard_break_index > 0 {
335                if !matches!(content[last_non_hard_break_index - 1], Node::HardBreak) {
336                    break;
337                }
338                last_non_hard_break_index -= 1;
339            }
340
341            for node in content.iter().take(last_non_hard_break_index) {
342                self.write(node)?;
343            }
344        } else {
345            for node in content {
346                self.write(node)?;
347            }
348        }
349
350        Ok(())
351    }
352
353    /// Write a blockquote node
354    pub(crate) fn write_blockquote(&mut self, content: &[Node]) -> WriteResult<()> {
355        // Create a temporary writer buffer to write all blockquote content
356        let mut temp_writer = CommonMarkWriter::with_options(self.options.clone());
357
358        // Write all content to temporary buffer
359        for (i, node) in content.iter().enumerate() {
360            if i > 0 {
361                temp_writer.write_char('\n')?;
362            }
363            // Write all nodes uniformly
364            temp_writer.write(node)?;
365        }
366
367        // Get all content
368        let all_content = temp_writer.into_string();
369
370        // Apply blockquote prefix "> " uniformly
371        let prefix = "> ";
372        let formatted_content = self.apply_prefix(&all_content, prefix, Some(prefix));
373
374        // Write formatted content
375        self.buffer.push_str(&formatted_content);
376        Ok(())
377    }
378
379    /// Write a thematic break (horizontal rule)
380    pub(crate) fn write_thematic_break(&mut self) -> WriteResult<()> {
381        let char = self.options.thematic_break_char;
382        self.write_str(&format!("{}{}{}", char, char, char))?;
383        Ok(())
384    }
385
386    /// Write a code block node
387    pub(crate) fn write_code_block(
388        &mut self,
389        language: &Option<EcoString>,
390        content: &str,
391        block_type: &CodeBlockType,
392    ) -> WriteResult<()> {
393        match block_type {
394            CodeBlockType::Indented => {
395                let indent = "    ";
396                let indented_content = self.apply_prefix(content, indent, Some(indent));
397                self.buffer.push_str(&indented_content);
398            }
399            CodeBlockType::Fenced => {
400                let max_backticks = content
401                    .chars()
402                    .fold((0, 0), |(max, current), c| {
403                        if c == '`' {
404                            (max.max(current + 1), current + 1)
405                        } else {
406                            (max, 0)
407                        }
408                    })
409                    .0;
410
411                let fence_len = std::cmp::max(max_backticks + 1, 3);
412                let fence = "`".repeat(fence_len);
413
414                self.write_str(&fence)?;
415                if let Some(lang) = language {
416                    self.write_str(lang)?;
417                }
418                self.write_char('\n')?;
419
420                self.buffer.push_str(content);
421                if !content.ends_with('\n') {
422                    self.write_char('\n')?;
423                }
424
425                self.write_str(&fence)?;
426            }
427        }
428
429        Ok(())
430    }
431
432    /// Write an unordered list node
433    pub(crate) fn write_unordered_list(&mut self, items: &[ListItem]) -> WriteResult<()> {
434        let list_marker = self.options.list_marker;
435        let prefix = format!("{} ", list_marker);
436
437        for (i, item) in items.iter().enumerate() {
438            if i > 0 {
439                self.write_char('\n')?;
440            }
441            self.write_list_item(item, &prefix)?;
442        }
443
444        Ok(())
445    }
446
447    /// Write an ordered list node
448    pub(crate) fn write_ordered_list(&mut self, start: u32, items: &[ListItem]) -> WriteResult<()> {
449        // Track the current item number
450        let mut current_number = start;
451
452        for (i, item) in items.iter().enumerate() {
453            if i > 0 {
454                self.write_char('\n')?;
455            }
456
457            match item {
458                // For ordered list items, check if there's a custom number
459                ListItem::Ordered { number, content: _ } => {
460                    if let Some(custom_num) = number {
461                        // Use custom numbering
462                        let prefix = format!("{}. ", custom_num);
463                        self.write_list_item(item, &prefix)?;
464                        // Next expected number
465                        current_number = custom_num + 1;
466                    } else {
467                        // No custom number, use the current calculated number
468                        let prefix = format!("{}. ", current_number);
469                        self.write_list_item(item, &prefix)?;
470                        current_number += 1;
471                    }
472                }
473                // For other types of list items, still use the current number
474                _ => {
475                    let prefix = format!("{}. ", current_number);
476                    self.write_list_item(item, &prefix)?;
477                    current_number += 1;
478                }
479            }
480        }
481
482        Ok(())
483    }
484
485    /// Write a list item
486    fn write_list_item(&mut self, item: &ListItem, prefix: &str) -> WriteResult<()> {
487        match item {
488            ListItem::Unordered { content } => {
489                self.write_str(prefix)?;
490                self.write_list_item_content(content, prefix.len())?;
491            }
492            ListItem::Ordered { number, content } => {
493                if let Some(num) = number {
494                    let custom_prefix = format!("{}. ", num);
495                    self.write_str(&custom_prefix)?;
496                    self.write_list_item_content(content, custom_prefix.len())?;
497                } else {
498                    self.write_str(prefix)?;
499                    self.write_list_item_content(content, prefix.len())?;
500                }
501            }
502            #[cfg(feature = "gfm")]
503            ListItem::Task { status, content } => {
504                // Only use task list syntax if GFM task lists are enabled
505                if self.options.gfm_tasklists {
506                    let checkbox = match status {
507                        crate::ast::TaskListStatus::Checked => "[x] ",
508                        crate::ast::TaskListStatus::Unchecked => "[ ] ",
509                    };
510
511                    // Use the original list marker (- or number) and append the checkbox
512                    let task_prefix = format!("{}{}", prefix, checkbox);
513                    self.write_str(&task_prefix)?;
514                    self.write_list_item_content(content, task_prefix.len())?;
515                } else {
516                    // If GFM task lists are disabled, just write a normal list item
517                    self.write_str(prefix)?;
518                    self.write_list_item_content(content, prefix.len())?;
519                }
520            }
521        }
522
523        Ok(())
524    }
525
526    /// Write list item content
527    fn write_list_item_content(&mut self, content: &[Node], prefix_len: usize) -> WriteResult<()> {
528        if content.is_empty() {
529            return Ok(());
530        }
531
532        let mut temp_writer = CommonMarkWriter::with_options(self.options.clone());
533
534        for (i, node) in content.iter().enumerate() {
535            if i > 0 {
536                temp_writer.write_char('\n')?;
537            }
538
539            temp_writer.write(node)?;
540        }
541
542        let all_content = temp_writer.into_string();
543
544        let indent = " ".repeat(prefix_len);
545
546        let formatted_content = self.apply_prefix(&all_content, &indent, Some(""));
547
548        self.buffer.push_str(&formatted_content);
549
550        Ok(())
551    }
552
553    /// Write a table
554    pub(crate) fn write_table(&mut self, headers: &[Node], rows: &[Vec<Node>]) -> WriteResult<()> {
555        // Check if table contains block elements
556        if Self::table_contains_block_elements(headers, rows) {
557            if self.is_strict_mode() {
558                // In strict mode, fail immediately if block elements are present
559                return Err(WriteError::InvalidStructure(
560                    "Table contains block-level elements which are not allowed in strict mode"
561                        .to_string()
562                        .into(),
563                ));
564            } else {
565                // In soft mode, fallback to HTML
566                log::info!(
567                    "Table contains block-level elements, falling back to HTML output in soft mode"
568                );
569                return self.write_table_as_html(headers, rows);
570            }
571        }
572
573        // Write header
574        self.write_char('|')?;
575        for header in headers {
576            self.check_no_newline(header, "Table Header")?;
577            self.write_char(' ')?;
578            self.write(header)?;
579            self.write_str(" |")?;
580        }
581        self.write_char('\n')?;
582
583        // Write alignment row (default to centered if no alignments provided)
584        self.write_char('|')?;
585        for _ in 0..headers.len() {
586            self.write_str(" --- |")?;
587        }
588        self.write_char('\n')?;
589
590        // Write table content
591        for row in rows {
592            self.write_char('|')?;
593            for cell in row {
594                self.check_no_newline(cell, "Table Cell")?;
595                self.write_char(' ')?;
596                self.write(cell)?;
597                self.write_str(" |")?;
598            }
599            self.write_char('\n')?;
600        }
601
602        Ok(())
603    }
604
605    #[cfg(feature = "gfm")]
606    /// Write a table with alignment (GFM extension)
607    pub(crate) fn write_table_with_alignment(
608        &mut self,
609        headers: &[Node],
610        alignments: &[TableAlignment],
611        rows: &[Vec<Node>],
612    ) -> WriteResult<()> {
613        // Only use alignment when GFM tables are enabled
614        if !self.options.gfm_tables {
615            return self.write_table(headers, rows);
616        }
617
618        // Check if table contains block elements
619        if Self::table_contains_block_elements(headers, rows) {
620            if self.is_strict_mode() {
621                // In strict mode, fail immediately if block elements are present
622                return Err(WriteError::InvalidStructure(
623                    "GFM table contains block-level elements which are not allowed in strict mode"
624                        .to_string()
625                        .into(),
626                ));
627            } else {
628                // In soft mode, fallback to HTML
629                log::info!("GFM table contains block-level elements, falling back to HTML output in soft mode");
630                return self.write_table_as_html_with_alignment(headers, alignments, rows);
631            }
632        }
633
634        // Write header
635        self.write_char('|')?;
636        for header in headers {
637            self.check_no_newline(header, "Table Header")?;
638            self.write_char(' ')?;
639            self.write(header)?;
640            self.write_str(" |")?;
641        }
642        self.write_char('\n')?;
643
644        // Write alignment row
645
646        self.write_char('|')?;
647
648        // Use provided alignments, or default to center if not enough alignments provided
649        for i in 0..headers.len() {
650            let alignment = if i < alignments.len() {
651                &alignments[i]
652            } else {
653                &TableAlignment::Center
654            };
655
656            match alignment {
657                TableAlignment::Left => self.write_str(" :--- |")?,
658                TableAlignment::Center => self.write_str(" :---: |")?,
659                TableAlignment::Right => self.write_str(" ---: |")?,
660                TableAlignment::None => self.write_str(" --- |")?,
661            }
662        }
663
664        self.write_char('\n')?;
665
666        // Write table content
667        for row in rows {
668            self.write_char('|')?;
669            for cell in row {
670                self.check_no_newline(cell, "Table Cell")?;
671                self.write_char(' ')?;
672                self.write(cell)?;
673                self.write_str(" |")?;
674            }
675            self.write_char('\n')?;
676        }
677
678        Ok(())
679    }
680
681    /// Write a link
682    pub(crate) fn write_link(
683        &mut self,
684        url: &str,
685        title: &Option<EcoString>,
686        content: &[Node],
687    ) -> WriteResult<()> {
688        for node in content {
689            self.check_no_newline(node, "Link Text")?;
690        }
691        self.write_char('[')?;
692
693        for node in content {
694            self.write(node)?;
695        }
696
697        self.write_str("](")?;
698        self.write_str(url)?;
699
700        if let Some(title_text) = title {
701            self.write_str(" \"")?;
702            self.write_str(title_text)?;
703            self.write_char('"')?;
704        }
705
706        self.write_char(')')?;
707        Ok(())
708    }
709
710    /// Write an image
711    pub(crate) fn write_image(
712        &mut self,
713        url: &str,
714        title: &Option<EcoString>,
715        alt: &[Node],
716    ) -> WriteResult<()> {
717        // Check for newlines in alt text content
718        for node in alt {
719            self.check_no_newline(node, "Image alt text")?;
720        }
721
722        self.write_str("![")?;
723
724        // Write alt text content
725        for node in alt {
726            self.write(node)?;
727        }
728
729        self.write_str("](")?;
730        self.write_str(url)?;
731
732        if let Some(title_text) = title {
733            self.write_str(" \"")?;
734            self.write_str(title_text)?;
735            self.write_char('"')?;
736        }
737
738        self.write_char(')')?;
739        Ok(())
740    }
741
742    /// Write a soft line break
743    pub(crate) fn write_soft_break(&mut self) -> WriteResult<()> {
744        self.write_char('\n')?;
745        Ok(())
746    }
747
748    /// Write a hard line break
749    pub(crate) fn write_hard_break(&mut self) -> WriteResult<()> {
750        if self.options.hard_break_spaces {
751            self.write_str("  \n")?;
752        } else {
753            self.write_str("\\\n")?;
754        }
755        Ok(())
756    }
757
758    /// Write an HTML block
759    pub(crate) fn write_html_block(&mut self, content: &str) -> WriteResult<()> {
760        self.buffer.push_str(content);
761
762        Ok(())
763    }
764
765    /// Write an autolink (URI or email address wrapped in < and >)
766    pub(crate) fn write_autolink(&mut self, url: &str, is_email: bool) -> WriteResult<()> {
767        // Autolinks shouldn't contain newlines
768        if url.contains('\n') {
769            if self.is_strict_mode() {
770                return Err(WriteError::NewlineInInlineElement(
771                    "Autolink URL".to_string().into(),
772                ));
773            } else {
774                log::warn!(
775                    "Newline character found in autolink URL '{}'. Writing it as is, which might result in an invalid link. Strict mode is off.",
776                    url
777                );
778                // Continue to write the URL as is, including the newline.
779            }
780        }
781
782        // Write the autolink with < and > delimiters
783        self.write_char('<')?;
784
785        // For email autolinks, we don't need to add any prefix
786        // For URI autolinks, ensure it has a scheme
787        if !is_email && !url.contains(':') {
788            // Default to https if no scheme is provided
789            self.write_str("https://")?;
790        }
791
792        self.write_str(url)?;
793        self.write_char('>')?;
794
795        Ok(())
796    }
797
798    /// Write an extended autolink (GFM extension)
799    #[cfg(feature = "gfm")]
800    pub(crate) fn write_extended_autolink(&mut self, url: &str) -> WriteResult<()> {
801        if !self.options.gfm_autolinks {
802            // If GFM autolinks are disabled, write as plain text
803            self.write_text_content(url)?;
804            return Ok(());
805        }
806
807        // Autolinks shouldn't contain newlines
808        if url.contains('\n') {
809            if self.is_strict_mode() {
810                // Or a specific gfm_autolinks_strict option if desired
811                return Err(WriteError::NewlineInInlineElement(
812                    "Extended Autolink URL".to_string().into(),
813                ));
814            } else {
815                log::warn!(
816                    "Newline character found in extended autolink URL '{}'. Writing it as is, which might result in an invalid link. Strict mode is off.",
817                    url
818                );
819                // Continue to write the URL as is, including the newline.
820            }
821        }
822
823        // Just write the URL as plain text for extended autolinks (no angle brackets)
824        self.write_str(url)?;
825
826        Ok(())
827    }
828
829    /// Write a link reference definition
830    pub(crate) fn write_link_reference_definition(
831        &mut self,
832        label: &str,
833        destination: &str,
834        title: &Option<EcoString>,
835    ) -> WriteResult<()> {
836        // Format: [label]: destination "optional title"
837        self.write_char('[')?;
838        self.write_str(label)?;
839        self.write_str("]: ")?;
840        self.write_str(destination)?;
841
842        if let Some(title_text) = title {
843            self.write_str(" \"")?;
844            self.write_str(title_text)?;
845            self.write_char('"')?;
846        }
847
848        Ok(())
849    }
850
851    /// Write a reference link
852    pub(crate) fn write_reference_link(
853        &mut self,
854        label: &str,
855        content: &[Node],
856    ) -> WriteResult<()> {
857        // Check for newlines in content
858        for node in content {
859            self.check_no_newline(node, "Reference Link Text")?;
860        }
861
862        // If content is empty or exactly matches the label (as plain text),
863        // this is a shortcut reference link: [label]
864        if content.is_empty() {
865            self.write_char('[')?;
866            self.write_str(label)?;
867            self.write_char(']')?;
868            return Ok(());
869        }
870
871        // Check if content is exactly the same as the label (to use shortcut syntax)
872        let is_shortcut =
873            content.len() == 1 && matches!(&content[0], Node::Text(text) if text == label);
874
875        if is_shortcut {
876            // Use shortcut reference link syntax: [label]
877            self.write_char('[')?;
878            self.write_str(label)?;
879            self.write_char(']')?;
880        } else {
881            // Use full reference link syntax: [content][label]
882            self.write_char('[')?;
883
884            for node in content {
885                self.write(node)?;
886            }
887
888            self.write_str("][")?;
889            self.write_str(label)?;
890            self.write_char(']')?;
891        }
892
893        Ok(())
894    }
895
896    /// Write an AST HtmlElement node as raw HTML string into the CommonMark output.
897    pub(crate) fn write_html_element(
898        &mut self,
899        element: &crate::ast::HtmlElement,
900    ) -> WriteResult<()> {
901        if self.options.strict {
902            if element.tag.contains('<') || element.tag.contains('>') {
903                return Err(WriteError::InvalidHtmlTag(element.tag.clone()));
904            }
905
906            for attr in &element.attributes {
907                if attr.name.contains('<') || attr.name.contains('>') {
908                    return Err(WriteError::InvalidHtmlAttribute(attr.name.clone()));
909                }
910            }
911        }
912
913        use crate::writer::html::{HtmlWriter, HtmlWriterOptions};
914
915        let html_options = if let Some(ref custom_options) = self.options.html_writer_options {
916            custom_options.clone()
917        } else {
918            HtmlWriterOptions {
919                strict: self.options.strict,
920                code_block_language_class_prefix: Some("language-".into()),
921                #[cfg(feature = "gfm")]
922                enable_gfm: self.options.enable_gfm,
923                #[cfg(feature = "gfm")]
924                gfm_disallowed_html_tags: self.options.gfm_disallowed_html_tags.clone(),
925            }
926        };
927
928        let mut html_writer = HtmlWriter::with_options(html_options);
929
930        html_writer.write_node(&Node::HtmlElement(element.clone()))?;
931
932        // Get the generated HTML
933        let html_output = html_writer.into_string();
934
935        // Otherwise write the raw HTML
936        self.write_str(&html_output)
937    }
938
939    /// Get the generated CommonMark format text
940    ///
941    /// Consumes the writer and returns the generated string
942    ///
943    /// # Example
944    ///
945    /// ```
946    /// use cmark_writer::writer::CommonMarkWriter;
947    /// use cmark_writer::ast::Node;
948    ///
949    /// let mut writer = CommonMarkWriter::new();
950    /// writer.write(&Node::Text("Hello".into())).unwrap();
951    /// let result = writer.into_string();
952    /// assert_eq!(result, "Hello");
953    /// ```
954    pub fn into_string(self) -> EcoString {
955        self.buffer
956    }
957
958    /// Write a string to the output buffer
959    ///
960    /// This method is provided for custom node implementations to use
961    pub fn write_str(&mut self, s: &str) -> WriteResult<()> {
962        self.buffer.push_str(s);
963        Ok(())
964    }
965
966    /// Write a character to the output buffer
967    ///
968    /// This method is provided for custom node implementations to use
969    pub fn write_char(&mut self, c: char) -> WriteResult<()> {
970        self.buffer.push(c);
971        Ok(())
972    }
973    /// Ensure content ends with a newline (for consistent handling at the end of block nodes)
974    ///
975    /// Adds a newline character if the content doesn't already end with one; does nothing if it already ends with a newline
976    pub(crate) fn ensure_trailing_newline(&mut self) -> WriteResult<()> {
977        if !self.buffer.ends_with('\n') {
978            self.write_char('\n')?;
979        }
980        Ok(())
981    }
982
983    /// Write an emphasis (italic) node with custom delimiter
984    pub(crate) fn write_emphasis(&mut self, content: &[Node]) -> WriteResult<()> {
985        let delimiter = self.options.emphasis_char.to_string();
986        self.write_delimited(content, &delimiter)
987    }
988
989    /// Write a strong emphasis (bold) node with custom delimiter
990    pub(crate) fn write_strong(&mut self, content: &[Node]) -> WriteResult<()> {
991        let char = self.options.strong_char;
992        let delimiter = format!("{}{}", char, char);
993        self.write_delimited(content, &delimiter)
994    }
995
996    /// Write a strikethrough node (GFM extension)
997    #[cfg(feature = "gfm")]
998    pub(crate) fn write_strikethrough(&mut self, content: &[Node]) -> WriteResult<()> {
999        if !self.options.enable_gfm || !self.options.gfm_strikethrough {
1000            // If GFM strikethrough is disabled, just write the content without strikethrough
1001            for node in content.iter() {
1002                self.write(node)?;
1003            }
1004            return Ok(());
1005        }
1006
1007        // Write content with ~~ delimiters
1008        self.write_delimited(content, "~~")
1009    }
1010}
1011
1012impl Default for CommonMarkWriter {
1013    fn default() -> Self {
1014        Self::new()
1015    }
1016}
1017
1018// Implement Display trait for Node structure
1019impl fmt::Display for Node {
1020    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1021        let mut writer = CommonMarkWriter::new();
1022        match writer.write(self) {
1023            Ok(_) => write!(f, "{}", writer.into_string()),
1024            Err(e) => write!(f, "Error writing Node: {}", e),
1025        }
1026    }
1027}
1028
1029/// A trait for character escaping behavior
1030pub(crate) trait Escapes {
1031    /// Checks if the string needs escaping
1032    fn str_needs_escaping(s: &str) -> bool;
1033
1034    /// Returns true if the character needs to be escaped
1035    fn char_needs_escaping(c: char) -> bool;
1036
1037    /// Returns the escaped version of a character (if needed)
1038    fn escape_char(c: char) -> Option<&'static str>;
1039}
1040
1041/// Markdown escaping implementation for CommonMark
1042pub(crate) struct CommonMarkEscapes;
1043
1044impl Escapes for CommonMarkEscapes {
1045    fn str_needs_escaping(s: &str) -> bool {
1046        s.chars().any(Self::char_needs_escaping)
1047    }
1048
1049    fn char_needs_escaping(c: char) -> bool {
1050        matches!(c, '\\' | '*' | '_' | '[' | ']' | '<' | '>' | '`')
1051    }
1052
1053    fn escape_char(c: char) -> Option<&'static str> {
1054        match c {
1055            '\\' => Some(r"\\"),
1056            '*' => Some(r"\*"),
1057            '_' => Some(r"\_"),
1058            '[' => Some(r"\["),
1059            ']' => Some(r"\]"),
1060            '<' => Some(r"\<"),
1061            '>' => Some(r"\>"),
1062            '`' => Some(r"\`"),
1063            _ => None,
1064        }
1065    }
1066}
1067
1068/// A wrapper for efficient escaping
1069pub(crate) struct Escaped<'a, E: Escapes> {
1070    inner: &'a str,
1071    _phantom: std::marker::PhantomData<E>,
1072}
1073
1074impl<'a, E: Escapes> Escaped<'a, E> {
1075    /// Create a new Escaped wrapper
1076    pub fn new(s: &'a str) -> Self {
1077        Self {
1078            inner: s,
1079            _phantom: std::marker::PhantomData,
1080        }
1081    }
1082}
1083
1084impl<E: Escapes> std::fmt::Display for Escaped<'_, E> {
1085    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1086        for c in self.inner.chars() {
1087            if E::char_needs_escaping(c) {
1088                f.write_str(E::escape_char(c).unwrap())?;
1089            } else {
1090                write!(f, "{}", c)?;
1091            }
1092        }
1093        Ok(())
1094    }
1095}
1096
1097impl CommonMarkWriter {
1098    /// Write a table as HTML (fallback for tables with block-level elements)
1099    fn write_table_as_html(&mut self, headers: &[Node], rows: &[Vec<Node>]) -> WriteResult<()> {
1100        use crate::writer::html::HtmlWriter;
1101
1102        let mut html_writer = HtmlWriter::new();
1103
1104        // Create table node for HTML writer
1105        let table_node = Node::Table {
1106            headers: headers.to_vec(),
1107            #[cfg(feature = "gfm")]
1108            alignments: vec![],
1109            rows: rows.to_vec(),
1110        };
1111
1112        html_writer.write_node(&table_node).map_err(|_| {
1113            WriteError::HtmlFallbackError("Failed to write table as HTML".to_string().into())
1114        })?;
1115
1116        let html_output = html_writer.into_string();
1117        self.buffer.push_str(&html_output);
1118
1119        Ok(())
1120    }
1121
1122    #[cfg(feature = "gfm")]
1123    /// Write a GFM table with alignment as HTML (fallback for tables with block-level elements)
1124    fn write_table_as_html_with_alignment(
1125        &mut self,
1126        headers: &[Node],
1127        alignments: &[TableAlignment],
1128        rows: &[Vec<Node>],
1129    ) -> WriteResult<()> {
1130        use crate::writer::html::HtmlWriter;
1131
1132        let mut html_writer = HtmlWriter::new();
1133
1134        // Create table node for HTML writer
1135        let table_node = Node::Table {
1136            headers: headers.to_vec(),
1137            alignments: alignments.to_vec(),
1138            rows: rows.to_vec(),
1139        };
1140
1141        html_writer.write_node(&table_node).map_err(|_| {
1142            WriteError::HtmlFallbackError("Failed to write GFM table as HTML".to_string().into())
1143        })?;
1144
1145        let html_output = html_writer.into_string();
1146        self.buffer.push_str(&html_output);
1147
1148        Ok(())
1149    }
1150}
1151
1152/// Escapes a string using the specified escaping strategy
1153pub(crate) fn escape_str<E: Escapes>(s: &str) -> std::borrow::Cow<'_, str> {
1154    if E::str_needs_escaping(s) {
1155        std::borrow::Cow::Owned(format!("{}", Escaped::<E>::new(s)))
1156    } else {
1157        std::borrow::Cow::Borrowed(s)
1158    }
1159}