cmark_writer/ast/
node.rs

1//! Node definitions for the CommonMark AST.
2
3use super::html::HtmlElement;
4use crate::traits::CustomNode;
5use ecow::EcoString;
6use std::boxed::Box;
7
8/// Code block type according to CommonMark specification
9#[derive(Debug, Clone, Copy, PartialEq, Default)]
10pub enum CodeBlockType {
11    /// Indented code block - composed of one or more indented chunks, each preceded by four or more spaces
12    Indented,
13    /// Fenced code block - surrounded by backtick or tilde fences
14    #[default]
15    Fenced,
16}
17
18/// Heading type according to CommonMark specification
19#[derive(Debug, Clone, Copy, PartialEq, Default)]
20pub enum HeadingType {
21    /// ATX Type - Beginning with #
22    #[default]
23    Atx,
24    /// Setext Type - Underlined or overlined text
25    Setext,
26}
27
28/// Table column alignment options for GFM tables
29#[cfg(feature = "gfm")]
30#[derive(Debug, Clone, PartialEq, Default)]
31pub enum TableAlignment {
32    /// Left alignment (default)
33    #[default]
34    Left,
35    /// Center alignment
36    Center,
37    /// Right alignment
38    Right,
39    /// No specific alignment specified
40    None,
41}
42
43/// Task list item status for GFM task lists
44#[cfg(feature = "gfm")]
45#[derive(Debug, Clone, PartialEq)]
46pub enum TaskListStatus {
47    /// Checked/completed task
48    Checked,
49    /// Unchecked/incomplete task
50    Unchecked,
51}
52
53/// Main node type, representing an element in a CommonMark document
54#[derive(Debug)]
55pub enum Node {
56    /// Root document node, contains child nodes
57    Document(Vec<Node>),
58
59    // Leaf blocks
60    // Thematic breaks
61    /// Thematic break (horizontal rule)
62    ThematicBreak,
63
64    // ATX headings & Setext headings
65    /// Heading, contains level (1-6) and inline content
66    Heading {
67        /// Heading level, 1-6
68        level: u8,
69        /// Heading content, containing inline elements
70        content: Vec<Node>,
71        /// Heading type (ATX or Setext)
72        heading_type: HeadingType,
73    },
74
75    // Indented code blocks & Fenced code blocks
76    /// Code block, containing optional language identifier and content
77    CodeBlock {
78        /// Optional language identifier (None for indented code blocks, Some for fenced code blocks)
79        language: Option<EcoString>,
80        /// Code content
81        content: EcoString,
82        /// The type of code block (Indented or Fenced)
83        block_type: CodeBlockType,
84    },
85
86    // HTML blocks
87    /// HTML block
88    HtmlBlock(EcoString),
89
90    // Link reference definitions
91    /// Link reference definition
92    LinkReferenceDefinition {
93        /// Link label (used for reference)
94        label: EcoString,
95        /// Link destination URL
96        destination: EcoString,
97        /// Optional link title
98        title: Option<EcoString>,
99    },
100
101    // Paragraphs
102    /// Paragraph node, containing inline elements
103    Paragraph(Vec<Node>),
104
105    // Blank lines - typically handled during parsing, not represented in AST
106
107    // Container blocks
108    // Block quotes
109    /// Block quote, containing any block-level elements
110    BlockQuote(Vec<Node>),
111
112    // & List items and Lists
113    /// Ordered list, containing starting number and list items
114    OrderedList {
115        /// List starting number
116        start: u32,
117        /// List items
118        items: Vec<ListItem>,
119    },
120
121    /// Unordered list, containing list items
122    UnorderedList(Vec<ListItem>),
123
124    /// Table (extension to CommonMark)
125    Table {
126        /// Header cells
127        headers: Vec<Node>,
128        /// Column alignments for the table
129        #[cfg(feature = "gfm")]
130        alignments: Vec<TableAlignment>,
131        /// Table rows, each row containing multiple cells
132        rows: Vec<Vec<Node>>,
133    },
134
135    // Inlines
136    // Code spans
137    /// Inline code
138    InlineCode(EcoString),
139
140    // Emphasis and strong emphasis
141    /// Emphasis (italic)
142    Emphasis(Vec<Node>),
143
144    /// Strong emphasis (bold)
145    Strong(Vec<Node>),
146
147    /// Strikethrough (GFM extension)
148    Strikethrough(Vec<Node>),
149
150    // Links
151    /// Link
152    Link {
153        /// Link URL
154        url: EcoString,
155        /// Optional link title
156        title: Option<EcoString>,
157        /// Link text
158        content: Vec<Node>,
159    },
160
161    /// Reference link
162    ReferenceLink {
163        /// Link reference label
164        label: EcoString,
165        /// Link text content (optional, if empty it's a shortcut reference)
166        content: Vec<Node>,
167    },
168
169    // Images
170    /// Image
171    Image {
172        /// Image URL
173        url: EcoString,
174        /// Optional image title
175        title: Option<EcoString>,
176        /// Alternative text, containing inline elements
177        alt: Vec<Node>,
178    },
179
180    // Autolinks
181    /// Autolink (URI or email wrapped in < and >)
182    Autolink {
183        /// Link URL
184        url: EcoString,
185        /// Whether this is an email autolink
186        is_email: bool,
187    },
188
189    /// GFM Extended Autolink (without angle brackets, automatically detected)
190    ExtendedAutolink(EcoString),
191
192    // Raw HTML
193    /// HTML inline element
194    HtmlElement(HtmlElement),
195
196    // Hard line breaks
197    /// Hard break (two spaces followed by a line break, or backslash followed by a line break)
198    HardBreak,
199
200    // Soft line breaks
201    /// Soft break (single line break)
202    SoftBreak,
203
204    // Textual content
205    /// Plain text
206    Text(EcoString),
207
208    /// Custom node that allows users to implement their own writing behavior
209    Custom(Box<dyn CustomNode>),
210}
211
212impl Default for Node {
213    fn default() -> Self {
214        Node::Document(vec![])
215    }
216}
217
218impl Clone for Node {
219    fn clone(&self) -> Self {
220        match self {
221            Node::Document(nodes) => Node::Document(nodes.clone()),
222            Node::ThematicBreak => Node::ThematicBreak,
223            Node::Heading {
224                level,
225                content,
226                heading_type,
227            } => Node::Heading {
228                level: *level,
229                content: content.clone(),
230                heading_type: *heading_type,
231            },
232            Node::CodeBlock {
233                language,
234                content,
235                block_type,
236            } => Node::CodeBlock {
237                language: language.clone(),
238                content: content.clone(),
239                block_type: *block_type,
240            },
241            Node::HtmlBlock(html) => Node::HtmlBlock(html.clone()),
242            Node::LinkReferenceDefinition {
243                label,
244                destination,
245                title,
246            } => Node::LinkReferenceDefinition {
247                label: label.clone(),
248                destination: destination.clone(),
249                title: title.clone(),
250            },
251            Node::Paragraph(content) => Node::Paragraph(content.clone()),
252            Node::BlockQuote(content) => Node::BlockQuote(content.clone()),
253            Node::OrderedList { start, items } => Node::OrderedList {
254                start: *start,
255                items: items.clone(),
256            },
257            Node::UnorderedList(items) => Node::UnorderedList(items.clone()),
258            #[cfg(feature = "gfm")]
259            Node::Table {
260                headers,
261                alignments,
262                rows,
263            } => Node::Table {
264                headers: headers.clone(),
265                alignments: alignments.clone(),
266                rows: rows.clone(),
267            },
268            #[cfg(not(feature = "gfm"))]
269            Node::Table { headers, rows } => Node::Table {
270                headers: headers.clone(),
271                rows: rows.clone(),
272            },
273            Node::InlineCode(code) => Node::InlineCode(code.clone()),
274            Node::Emphasis(content) => Node::Emphasis(content.clone()),
275            Node::Strong(content) => Node::Strong(content.clone()),
276            Node::Strikethrough(content) => Node::Strikethrough(content.clone()),
277            Node::Link {
278                url,
279                title,
280                content,
281            } => Node::Link {
282                url: url.clone(),
283                title: title.clone(),
284                content: content.clone(),
285            },
286            Node::ReferenceLink { label, content } => Node::ReferenceLink {
287                label: label.clone(),
288                content: content.clone(),
289            },
290            Node::Image { url, title, alt } => Node::Image {
291                url: url.clone(),
292                title: title.clone(),
293                alt: alt.clone(),
294            },
295            Node::Autolink { url, is_email } => Node::Autolink {
296                url: url.clone(),
297                is_email: *is_email,
298            },
299            Node::ExtendedAutolink(url) => Node::ExtendedAutolink(url.clone()),
300            Node::HtmlElement(element) => Node::HtmlElement(element.clone()),
301            Node::HardBreak => Node::HardBreak,
302            Node::SoftBreak => Node::SoftBreak,
303            Node::Text(text) => Node::Text(text.clone()),
304            Node::Custom(_custom) => {
305                // 暂时不支持自定义节点的克隆,因为我们简化了设计
306                // 用户应该使用 Format trait 而不是直接使用 Custom 节点
307                panic!("Custom node cloning not supported in simplified design")
308            }
309        }
310    }
311}
312
313impl PartialEq for Node {
314    fn eq(&self, other: &Self) -> bool {
315        match (self, other) {
316            (Node::Document(a), Node::Document(b)) => a == b,
317            (Node::ThematicBreak, Node::ThematicBreak) => true,
318            (
319                Node::Heading {
320                    level: l1,
321                    content: c1,
322                    heading_type: h1,
323                },
324                Node::Heading {
325                    level: l2,
326                    content: c2,
327                    heading_type: h2,
328                },
329            ) => l1 == l2 && c1 == c2 && h1 == h2,
330            (
331                Node::CodeBlock {
332                    language: l1,
333                    content: c1,
334                    block_type: b1,
335                },
336                Node::CodeBlock {
337                    language: l2,
338                    content: c2,
339                    block_type: b2,
340                },
341            ) => l1 == l2 && c1 == c2 && b1 == b2,
342            (Node::HtmlBlock(a), Node::HtmlBlock(b)) => a == b,
343            (
344                Node::LinkReferenceDefinition {
345                    label: l1,
346                    destination: d1,
347                    title: t1,
348                },
349                Node::LinkReferenceDefinition {
350                    label: l2,
351                    destination: d2,
352                    title: t2,
353                },
354            ) => l1 == l2 && d1 == d2 && t1 == t2,
355            (Node::Paragraph(a), Node::Paragraph(b)) => a == b,
356            (Node::BlockQuote(a), Node::BlockQuote(b)) => a == b,
357            (
358                Node::OrderedList {
359                    start: s1,
360                    items: i1,
361                },
362                Node::OrderedList {
363                    start: s2,
364                    items: i2,
365                },
366            ) => s1 == s2 && i1 == i2,
367            (Node::UnorderedList(a), Node::UnorderedList(b)) => a == b,
368            #[cfg(feature = "gfm")]
369            (
370                Node::Table {
371                    headers: h1,
372                    alignments: a1,
373                    rows: r1,
374                },
375                Node::Table {
376                    headers: h2,
377                    alignments: a2,
378                    rows: r2,
379                },
380            ) => h1 == h2 && a1 == a2 && r1 == r2,
381            #[cfg(not(feature = "gfm"))]
382            (
383                Node::Table {
384                    headers: h1,
385                    rows: r1,
386                },
387                Node::Table {
388                    headers: h2,
389                    rows: r2,
390                },
391            ) => h1 == h2 && r1 == r2,
392            (Node::InlineCode(a), Node::InlineCode(b)) => a == b,
393            (Node::Emphasis(a), Node::Emphasis(b)) => a == b,
394            (Node::Strong(a), Node::Strong(b)) => a == b,
395            #[cfg(feature = "gfm")]
396            (Node::Strikethrough(a), Node::Strikethrough(b)) => a == b,
397            (
398                Node::Link {
399                    url: u1,
400                    title: t1,
401                    content: c1,
402                },
403                Node::Link {
404                    url: u2,
405                    title: t2,
406                    content: c2,
407                },
408            ) => u1 == u2 && t1 == t2 && c1 == c2,
409            (
410                Node::ReferenceLink {
411                    label: l1,
412                    content: c1,
413                },
414                Node::ReferenceLink {
415                    label: l2,
416                    content: c2,
417                },
418            ) => l1 == l2 && c1 == c2,
419            (
420                Node::Image {
421                    url: u1,
422                    title: t1,
423                    alt: a1,
424                },
425                Node::Image {
426                    url: u2,
427                    title: t2,
428                    alt: a2,
429                },
430            ) => u1 == u2 && t1 == t2 && a1 == a2,
431            (
432                Node::Autolink {
433                    url: u1,
434                    is_email: e1,
435                },
436                Node::Autolink {
437                    url: u2,
438                    is_email: e2,
439                },
440            ) => u1 == u2 && e1 == e2,
441            #[cfg(feature = "gfm")]
442            (Node::ExtendedAutolink(a), Node::ExtendedAutolink(b)) => a == b,
443            (Node::HtmlElement(a), Node::HtmlElement(b)) => a == b,
444            (Node::HardBreak, Node::HardBreak) => true,
445            (Node::SoftBreak, Node::SoftBreak) => true,
446            (Node::Text(a), Node::Text(b)) => a == b,
447            (Node::Custom(a), Node::Custom(b)) => a.eq_box(&**b),
448            _ => false,
449        }
450    }
451}
452
453/// List item type
454#[derive(Debug, Clone, PartialEq)]
455pub enum ListItem {
456    /// Unordered list item
457    Unordered {
458        /// List item content, containing one or more block-level elements
459        content: Vec<Node>,
460    },
461    /// Ordered list item
462    Ordered {
463        /// Optional item number for ordered lists, allowing manual numbering
464        number: Option<u32>,
465        /// List item content, containing one or more block-level elements
466        content: Vec<Node>,
467    },
468    /// Task list item (GFM extension)
469    #[cfg(feature = "gfm")]
470    Task {
471        /// Task completion status
472        status: TaskListStatus,
473        /// List item content, containing one or more block-level elements
474        content: Vec<Node>,
475    },
476}
477
478impl Node {
479    /// Check if a node is a block-level node
480    pub fn is_block(&self) -> bool {
481        matches!(
482            self,
483            Node::Document(_)
484                // Leaf blocks
485                | Node::ThematicBreak
486                | Node::Heading { .. }
487                | Node::CodeBlock { .. }
488                | Node::HtmlBlock(_)
489                | Node::LinkReferenceDefinition { .. }
490                | Node::Paragraph(_)
491                // Container blocks
492                | Node::BlockQuote(_)
493                | Node::OrderedList { .. }
494                | Node::UnorderedList(_)
495                | Node::Table { .. }
496
497                | Node::Custom(_)
498        )
499    }
500
501    /// Check if a node is an inline node
502    pub fn is_inline(&self) -> bool {
503        matches!(
504            self,
505            // Inlines
506            // Code spans
507            Node::InlineCode(_)
508                // Emphasis and strong emphasis
509                | Node::Emphasis(_)
510                | Node::Strong(_)
511                | Node::Strikethrough(_)
512                // Links
513                | Node::Link { .. }
514                | Node::ReferenceLink { .. }
515                // Images
516                | Node::Image { .. }
517                // Autolinks
518                | Node::Autolink { .. }
519                | Node::ExtendedAutolink(_)
520                // Raw HTML
521                | Node::HtmlElement(_)
522                // Hard line breaks
523                | Node::HardBreak
524                // Soft line breaks
525                | Node::SoftBreak
526                // Textual content
527                | Node::Text(_)
528
529                | Node::Custom(_)
530        )
531    }
532
533    /// Get the type name of the node for debugging and error messages
534    pub fn type_name(&self) -> &'static str {
535        match self {
536            Node::Document(_) => "Document",
537            Node::ThematicBreak => "ThematicBreak",
538            Node::Heading { .. } => "Heading",
539            Node::CodeBlock { .. } => "CodeBlock",
540            Node::HtmlBlock(_) => "HtmlBlock",
541            Node::LinkReferenceDefinition { .. } => "LinkReferenceDefinition",
542            Node::Paragraph(_) => "Paragraph",
543            Node::BlockQuote(_) => "BlockQuote",
544            Node::OrderedList { .. } => "OrderedList",
545            Node::UnorderedList(_) => "UnorderedList",
546            Node::Table { .. } => "Table",
547            Node::InlineCode(_) => "InlineCode",
548            Node::Emphasis(_) => "Emphasis",
549            Node::Strong(_) => "Strong",
550            Node::Strikethrough(_) => "Strikethrough",
551            Node::Link { .. } => "Link",
552            Node::ReferenceLink { .. } => "ReferenceLink",
553            Node::Image { .. } => "Image",
554            Node::Autolink { .. } => "Autolink",
555            Node::ExtendedAutolink(_) => "ExtendedAutolink",
556            Node::HtmlElement(_) => "HtmlElement",
557            Node::HardBreak => "HardBreak",
558            Node::SoftBreak => "SoftBreak",
559            Node::Text(_) => "Text",
560            Node::Custom(_) => "Custom",
561        }
562    }
563    /// Create a heading node
564    ///
565    /// # Arguments
566    /// * `level` - Heading level (1-6)
567    /// * `content` - Heading content
568    ///
569    /// # Returns
570    /// A new heading node, default ATX type
571    pub fn heading(level: u8, content: Vec<Node>) -> Self {
572        Node::Heading {
573            level,
574            content,
575            heading_type: HeadingType::default(),
576        }
577    }
578
579    /// Create a code block node
580    ///
581    /// # Arguments
582    /// * `language` - Optional language identifier
583    /// * `content` - Code content
584    ///
585    /// # Returns
586    /// A new code block node, default Fenced type
587    pub fn code_block(language: Option<EcoString>, content: EcoString) -> Self {
588        Node::CodeBlock {
589            language,
590            content,
591            block_type: CodeBlockType::default(),
592        }
593    }
594
595    /// Create a strikethrough node
596    ///
597    /// # Arguments
598    /// * `content` - Content to be struck through
599    ///
600    /// # Returns
601    /// A new strikethrough node
602    pub fn strikethrough(content: Vec<Node>) -> Self {
603        Node::Strikethrough(content)
604    }
605
606    /// Create a task list item
607    ///
608    /// # Arguments
609    /// * `status` - Task completion status
610    /// * `content` - Task content
611    ///
612    /// # Returns
613    /// A new task list item
614    #[cfg(feature = "gfm")]
615    pub fn task_list_item(status: TaskListStatus, content: Vec<Node>) -> Self {
616        Node::UnorderedList(vec![ListItem::Task { status, content }])
617    }
618
619    /// Create a table with alignment
620    ///
621    /// # Arguments
622    /// * `headers` - Table header cells
623    /// * `alignments` - Column alignments
624    /// * `rows` - Table rows
625    ///
626    /// # Returns
627    /// A new table node with alignment information
628    #[cfg(feature = "gfm")]
629    pub fn table_with_alignment(
630        headers: Vec<Node>,
631        alignments: Vec<TableAlignment>,
632        rows: Vec<Vec<Node>>,
633    ) -> Self {
634        Node::Table {
635            headers,
636            alignments,
637            rows,
638        }
639    }
640    /// Check if a custom node is of a specific type, and return a reference to that type
641    pub fn as_custom_type<T: CustomNode + 'static>(&self) -> Option<&T> {
642        if let Node::Custom(node) = self {
643            node.as_any().downcast_ref::<T>()
644        } else {
645            None
646        }
647    }
648
649    /// Check if a node is a custom node of a specific type
650    pub fn is_custom_type<T: CustomNode + 'static>(&self) -> bool {
651        self.as_custom_type::<T>().is_some()
652    }
653}
654
655// Implement Format traits for Node
656impl crate::traits::Format<crate::writer::CommonMarkWriter> for Node {
657    fn format(
658        &self,
659        writer: &mut crate::writer::CommonMarkWriter,
660    ) -> crate::error::WriteResult<()> {
661        // For individual nodes being formatted directly (like in legacy tests),
662        // use content writing without automatic trailing newlines unless it's a block
663        if self.is_block() {
664            writer.write_node(self)
665        } else {
666            // For inline elements, write content without automatic trailing newlines
667            writer.write_node_content(self)
668        }
669    }
670}
671
672impl crate::traits::Format<crate::writer::HtmlWriter> for Node {
673    fn format(&self, writer: &mut crate::writer::HtmlWriter) -> crate::error::WriteResult<()> {
674        writer.write_node_internal(self).map_err(Into::into)
675    }
676}