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}