cmark_writer/ast/
node.rs

1//! Node definitions for the CommonMark AST.
2
3use super::custom::CustomNode;
4use super::html::HtmlElement;
5use ecow::EcoString;
6use std::boxed::Box;
7
8/// Code block type according to CommonMark specification
9#[derive(Debug, Clone, 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, 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, Clone, PartialEq)]
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
218/// List item type
219#[derive(Debug, Clone, PartialEq)]
220pub enum ListItem {
221    /// Unordered list item
222    Unordered {
223        /// List item content, containing one or more block-level elements
224        content: Vec<Node>,
225    },
226    /// Ordered list item
227    Ordered {
228        /// Optional item number for ordered lists, allowing manual numbering
229        number: Option<u32>,
230        /// List item content, containing one or more block-level elements
231        content: Vec<Node>,
232    },
233    /// Task list item (GFM extension)
234    #[cfg(feature = "gfm")]
235    Task {
236        /// Task completion status
237        status: TaskListStatus,
238        /// List item content, containing one or more block-level elements
239        content: Vec<Node>,
240    },
241}
242
243impl Node {
244    /// Check if a node is a block-level node
245    pub fn is_block(&self) -> bool {
246        match self {
247            Node::Custom(node) => node.is_block(),
248            _ => matches!(
249                self,
250                Node::Document(_)
251                    // Leaf blocks
252                    | Node::ThematicBreak
253                    | Node::Heading { .. }
254                    | Node::CodeBlock { .. }
255                    | Node::HtmlBlock(_)
256                    | Node::LinkReferenceDefinition { .. }
257                    | Node::Paragraph(_)
258                    // Container blocks
259                    | Node::BlockQuote(_)
260                    | Node::OrderedList { .. }
261                    | Node::UnorderedList(_)
262                    | Node::Table { .. }
263            ),
264        }
265    }
266
267    /// Check if a node is an inline node
268    pub fn is_inline(&self) -> bool {
269        match self {
270            Node::Custom(node) => !node.is_block(),
271            _ => matches!(
272                self,
273                // Inlines
274                // Code spans
275                Node::InlineCode(_)
276                    // Emphasis and strong emphasis
277                    | Node::Emphasis(_)
278                    | Node::Strong(_)
279                    | Node::Strikethrough(_)
280                    // Links
281                    | Node::Link { .. }
282                    | Node::ReferenceLink { .. }
283                    // Images
284                    | Node::Image { .. }
285                    // Autolinks
286                    | Node::Autolink { .. }
287                    | Node::ExtendedAutolink(_)
288                    // Raw HTML
289                    | Node::HtmlElement(_)
290                    // Hard line breaks
291                    | Node::HardBreak
292                    // Soft line breaks
293                    | Node::SoftBreak
294                    // Textual content
295                    | Node::Text(_)
296            ),
297        }
298    }
299    /// Create a heading node
300    ///
301    /// # Arguments
302    /// * `level` - Heading level (1-6)
303    /// * `content` - Heading content
304    ///
305    /// # Returns
306    /// A new heading node, default ATX type
307    pub fn heading(level: u8, content: Vec<Node>) -> Self {
308        Node::Heading {
309            level,
310            content,
311            heading_type: HeadingType::default(),
312        }
313    }
314
315    /// Create a code block node
316    ///
317    /// # Arguments
318    /// * `language` - Optional language identifier
319    /// * `content` - Code content
320    ///
321    /// # Returns
322    /// A new code block node, default Fenced type
323    pub fn code_block(language: Option<EcoString>, content: EcoString) -> Self {
324        Node::CodeBlock {
325            language,
326            content,
327            block_type: CodeBlockType::default(),
328        }
329    }
330
331    /// Create a strikethrough node
332    ///
333    /// # Arguments
334    /// * `content` - Content to be struck through
335    ///
336    /// # Returns
337    /// A new strikethrough node
338    pub fn strikethrough(content: Vec<Node>) -> Self {
339        Node::Strikethrough(content)
340    }
341
342    /// Create a task list item
343    ///
344    /// # Arguments
345    /// * `status` - Task completion status
346    /// * `content` - Task content
347    ///
348    /// # Returns
349    /// A new task list item
350    #[cfg(feature = "gfm")]
351    pub fn task_list_item(status: TaskListStatus, content: Vec<Node>) -> ListItem {
352        ListItem::Task { status, content }
353    }
354
355    /// Create a table with alignment
356    ///
357    /// # Arguments
358    /// * `headers` - Table header cells
359    /// * `alignments` - Column alignments
360    /// * `rows` - Table rows
361    ///
362    /// # Returns
363    /// A new table node with alignment information
364    #[cfg(feature = "gfm")]
365    pub fn table_with_alignment(
366        headers: Vec<Node>,
367        alignments: Vec<TableAlignment>,
368        rows: Vec<Vec<Node>>,
369    ) -> Self {
370        Node::Table {
371            headers,
372            alignments,
373            rows,
374        }
375    }
376    /// Check if a custom node is of a specific type, and return a reference to that type
377    pub fn as_custom_type<T: CustomNode + 'static>(&self) -> Option<&T> {
378        if let Node::Custom(node) = self {
379            node.as_any().downcast_ref::<T>()
380        } else {
381            None
382        }
383    }
384
385    /// Check if a node is a custom node of a specific type
386    pub fn is_custom_type<T: CustomNode + 'static>(&self) -> bool {
387        self.as_custom_type::<T>().is_some()
388    }
389}