cmark_writer/ast/node.rs
1//! Node definitions for the CommonMark AST.
2
3use super::custom::CustomNode;
4use super::html::HtmlElement;
5use std::boxed::Box;
6
7/// Code block type according to CommonMark specification
8#[derive(Debug, Clone, PartialEq, Default)]
9pub enum CodeBlockType {
10 /// Indented code block - composed of one or more indented chunks, each preceded by four or more spaces
11 Indented,
12 /// Fenced code block - surrounded by backtick or tilde fences
13 #[default]
14 Fenced,
15}
16
17/// Heading type according to CommonMark specification
18#[derive(Debug, Clone, PartialEq, Default)]
19pub enum HeadingType {
20 /// ATX Type - Beginning with #
21 #[default]
22 Atx,
23 /// Setext Type - Underlined or overlined text
24 Setext,
25}
26
27/// Table column alignment options for GFM tables
28#[cfg(feature = "gfm")]
29#[derive(Debug, Clone, PartialEq, Default)]
30pub enum TableAlignment {
31 /// Left alignment (default)
32 #[default]
33 Left,
34 /// Center alignment
35 Center,
36 /// Right alignment
37 Right,
38 /// No specific alignment specified
39 None,
40}
41
42/// Task list item status for GFM task lists
43#[cfg(feature = "gfm")]
44#[derive(Debug, Clone, PartialEq)]
45pub enum TaskListStatus {
46 /// Checked/completed task
47 Checked,
48 /// Unchecked/incomplete task
49 Unchecked,
50}
51
52/// Main node type, representing an element in a CommonMark document
53#[derive(Debug, Clone, PartialEq)]
54pub enum Node {
55 /// Root document node, contains child nodes
56 Document(Vec<Node>),
57
58 // Leaf blocks
59 // Thematic breaks
60 /// Thematic break (horizontal rule)
61 ThematicBreak,
62
63 // ATX headings & Setext headings
64 /// Heading, contains level (1-6) and inline content
65 Heading {
66 /// Heading level, 1-6
67 level: u8,
68 /// Heading content, containing inline elements
69 content: Vec<Node>,
70 /// Heading type (ATX or Setext)
71 heading_type: HeadingType,
72 },
73
74 // Indented code blocks & Fenced code blocks
75 /// Code block, containing optional language identifier and content
76 CodeBlock {
77 /// Optional language identifier (None for indented code blocks, Some for fenced code blocks)
78 language: Option<String>,
79 /// Code content
80 content: String,
81 /// The type of code block (Indented or Fenced)
82 block_type: CodeBlockType,
83 },
84
85 // HTML blocks
86 /// HTML block
87 HtmlBlock(String),
88
89 // Link reference definitions
90 /// Link reference definition
91 LinkReferenceDefinition {
92 /// Link label (used for reference)
93 label: String,
94 /// Link destination URL
95 destination: String,
96 /// Optional link title
97 title: Option<String>,
98 },
99
100 // Paragraphs
101 /// Paragraph node, containing inline elements
102 Paragraph(Vec<Node>),
103
104 // Blank lines - typically handled during parsing, not represented in AST
105
106 // Container blocks
107 // Block quotes
108 /// Block quote, containing any block-level elements
109 BlockQuote(Vec<Node>),
110
111 // & List items and Lists
112 /// Ordered list, containing starting number and list items
113 OrderedList {
114 /// List starting number
115 start: u32,
116 /// List items
117 items: Vec<ListItem>,
118 },
119
120 /// Unordered list, containing list items
121 UnorderedList(Vec<ListItem>),
122
123 /// Table (extension to CommonMark)
124 Table {
125 /// Header cells
126 headers: Vec<Node>,
127 /// Column alignments for the table
128 #[cfg(feature = "gfm")]
129 alignments: Vec<TableAlignment>,
130 /// Table rows, each row containing multiple cells
131 rows: Vec<Vec<Node>>,
132 },
133
134 // Inlines
135 // Code spans
136 /// Inline code
137 InlineCode(String),
138
139 // Emphasis and strong emphasis
140 /// Emphasis (italic)
141 Emphasis(Vec<Node>),
142
143 /// Strong emphasis (bold)
144 Strong(Vec<Node>),
145
146 /// Strikethrough (GFM extension)
147 Strikethrough(Vec<Node>),
148
149 // Links
150 /// Link
151 Link {
152 /// Link URL
153 url: String,
154 /// Optional link title
155 title: Option<String>,
156 /// Link text
157 content: Vec<Node>,
158 },
159
160 /// Reference link
161 ReferenceLink {
162 /// Link reference label
163 label: String,
164 /// Link text content (optional, if empty it's a shortcut reference)
165 content: Vec<Node>,
166 },
167
168 // Images
169 /// Image
170 Image {
171 /// Image URL
172 url: String,
173 /// Optional image title
174 title: Option<String>,
175 /// Alternative text, containing inline elements
176 alt: Vec<Node>,
177 },
178
179 // Autolinks
180 /// Autolink (URI or email wrapped in < and >)
181 Autolink {
182 /// Link URL
183 url: String,
184 /// Whether this is an email autolink
185 is_email: bool,
186 },
187
188 /// GFM Extended Autolink (without angle brackets, automatically detected)
189 ExtendedAutolink(String),
190
191 // Raw HTML
192 /// HTML inline element
193 HtmlElement(HtmlElement),
194
195 // Hard line breaks
196 /// Hard break (two spaces followed by a line break, or backslash followed by a line break)
197 HardBreak,
198
199 // Soft line breaks
200 /// Soft break (single line break)
201 SoftBreak,
202
203 // Textual content
204 /// Plain text
205 Text(String),
206
207 /// Custom node that allows users to implement their own writing behavior
208 Custom(Box<dyn CustomNode>),
209}
210
211impl Default for Node {
212 fn default() -> Self {
213 Node::Document(vec![])
214 }
215}
216
217/// List item type
218#[derive(Debug, Clone, PartialEq)]
219pub enum ListItem {
220 /// Unordered list item
221 Unordered {
222 /// List item content, containing one or more block-level elements
223 content: Vec<Node>,
224 },
225 /// Ordered list item
226 Ordered {
227 /// Optional item number for ordered lists, allowing manual numbering
228 number: Option<u32>,
229 /// List item content, containing one or more block-level elements
230 content: Vec<Node>,
231 },
232 /// Task list item (GFM extension)
233 #[cfg(feature = "gfm")]
234 Task {
235 /// Task completion status
236 status: TaskListStatus,
237 /// List item content, containing one or more block-level elements
238 content: Vec<Node>,
239 },
240}
241
242impl Node {
243 /// Check if a node is a block-level node
244 pub fn is_block(&self) -> bool {
245 matches!(
246 self,
247 Node::Document(_)
248 // Leaf blocks
249 | Node::ThematicBreak
250 | Node::Heading { .. }
251 | Node::CodeBlock { .. }
252 | Node::HtmlBlock(_)
253 | Node::LinkReferenceDefinition { .. }
254 | Node::Paragraph(_)
255 // Container blocks
256 | Node::BlockQuote(_)
257 | Node::OrderedList { .. }
258 | Node::UnorderedList(_)
259 | Node::Table { .. }
260
261 | Node::Custom(_)
262 )
263 }
264
265 /// Check if a node is an inline node
266 pub fn is_inline(&self) -> bool {
267 matches!(
268 self,
269 // Inlines
270 // Code spans
271 Node::InlineCode(_)
272 // Emphasis and strong emphasis
273 | Node::Emphasis(_)
274 | Node::Strong(_)
275 | Node::Strikethrough(_)
276 // Links
277 | Node::Link { .. }
278 | Node::ReferenceLink { .. }
279 // Images
280 | Node::Image { .. }
281 // Autolinks
282 | Node::Autolink { .. }
283 | Node::ExtendedAutolink(_)
284 // Raw HTML
285 | Node::HtmlElement(_)
286 // Hard line breaks
287 | Node::HardBreak
288 // Soft line breaks
289 | Node::SoftBreak
290 // Textual content
291 | Node::Text(_)
292
293 | Node::Custom(_)
294 )
295 }
296 /// Create a heading node
297 ///
298 /// # Arguments
299 /// * `level` - Heading level (1-6)
300 /// * `content` - Heading content
301 ///
302 /// # Returns
303 /// A new heading node, default ATX type
304 pub fn heading(level: u8, content: Vec<Node>) -> Self {
305 Node::Heading {
306 level,
307 content,
308 heading_type: HeadingType::default(),
309 }
310 }
311
312 /// Create a code block node
313 ///
314 /// # Arguments
315 /// * `language` - Optional language identifier
316 /// * `content` - Code content
317 ///
318 /// # Returns
319 /// A new code block node, default Fenced type
320 pub fn code_block(language: Option<String>, content: String) -> Self {
321 Node::CodeBlock {
322 language,
323 content,
324 block_type: CodeBlockType::default(),
325 }
326 }
327
328 /// Create a strikethrough node
329 ///
330 /// # Arguments
331 /// * `content` - Content to be struck through
332 ///
333 /// # Returns
334 /// A new strikethrough node
335 pub fn strikethrough(content: Vec<Node>) -> Self {
336 Node::Strikethrough(content)
337 }
338
339 /// Create a task list item
340 ///
341 /// # Arguments
342 /// * `status` - Task completion status
343 /// * `content` - Task content
344 ///
345 /// # Returns
346 /// A new task list item
347 #[cfg(feature = "gfm")]
348 pub fn task_list_item(status: TaskListStatus, content: Vec<Node>) -> Self {
349 Node::UnorderedList(vec![ListItem::Task { status, content }])
350 }
351
352 /// Create a table with alignment
353 ///
354 /// # Arguments
355 /// * `headers` - Table header cells
356 /// * `alignments` - Column alignments
357 /// * `rows` - Table rows
358 ///
359 /// # Returns
360 /// A new table node with alignment information
361 #[cfg(feature = "gfm")]
362 pub fn table_with_alignment(
363 headers: Vec<Node>,
364 alignments: Vec<TableAlignment>,
365 rows: Vec<Vec<Node>>,
366 ) -> Self {
367 Node::Table {
368 headers,
369 alignments,
370 rows,
371 }
372 }
373 /// Check if a custom node is of a specific type, and return a reference to that type
374 pub fn as_custom_type<T: CustomNode + 'static>(&self) -> Option<&T> {
375 if let Node::Custom(node) = self {
376 node.as_any().downcast_ref::<T>()
377 } else {
378 None
379 }
380 }
381
382 /// Check if a node is a custom node of a specific type
383 pub fn is_custom_type<T: CustomNode + 'static>(&self) -> bool {
384 self.as_custom_type::<T>().is_some()
385 }
386}