comrak/nodes.rs
1//! The CommonMark AST.
2
3use std::borrow::Cow;
4use std::cell::RefCell;
5use std::convert::TryFrom;
6
7#[cfg(feature = "phoenix_heex")]
8pub use crate::parser::phoenix_heex::{HeexNode, NodeHeexBlock};
9#[cfg(feature = "shortcodes")]
10pub use crate::parser::shortcodes::NodeShortCode;
11use crate::{arena_tree, options::AlertStyleType};
12
13/// Shorthand for checking if a node's value matches the given expression.
14///
15/// Note this will call `node.data()`, which will fail if the node is already
16/// mutably borrowed.
17#[macro_export]
18macro_rules! node_matches {
19 ($node:expr, $( $pat:pat_param )|+) => {{
20 matches!(
21 $node.data().value,
22 $( $pat )|+
23 )
24 }};
25}
26
27/// The core AST node enum.
28#[derive(Debug, Clone, PartialEq, Eq)]
29#[cfg_attr(test, derive(strum::EnumDiscriminants))]
30#[cfg_attr(
31 test,
32 strum_discriminants(vis(pub(crate)), derive(strum::VariantArray, Hash))
33)]
34pub enum NodeValue {
35 /// The root of every CommonMark document. Contains **blocks**.
36 Document,
37
38 /// Non-Markdown front matter. Treated as an opaque blob.
39 FrontMatter(String),
40
41 /// **Block**. A [block quote](https://github.github.com/gfm/#block-quotes). Contains other
42 /// **blocks**.
43 ///
44 /// ```markdown
45 /// > A block quote.
46 /// ```
47 BlockQuote,
48
49 /// **Block**. A [list](https://github.github.com/gfm/#lists). Contains
50 /// [list items](https://github.github.com/gfm/#list-items).
51 ///
52 /// ```markdown
53 /// * An unordered list
54 /// * Another item
55 ///
56 /// 1. An ordered list
57 /// 2. Another item
58 /// ```
59 List(NodeList),
60
61 /// **Block**. A [list item](https://github.github.com/gfm/#list-items). Contains other
62 /// **blocks**.
63 Item(NodeList),
64
65 /// **Block**. A description list, enabled with `ext_description_lists` option. Contains
66 /// description items.
67 ///
68 /// It is required to put a blank line between terms and details.
69 ///
70 /// ```markdown
71 /// Term 1
72 ///
73 /// : Details 1
74 ///
75 /// Term 2
76 ///
77 /// : Details 2
78 /// ```
79 DescriptionList,
80
81 /// *Block**. An item of a description list. Contains a term and one details block.
82 DescriptionItem(NodeDescriptionItem),
83
84 /// **Block**. Term of an item in a definition list.
85 DescriptionTerm,
86
87 /// **Block**. Details of an item in a definition list.
88 DescriptionDetails,
89
90 /// **Block**. A code block; may be [fenced](https://github.github.com/gfm/#fenced-code-blocks)
91 /// or [indented](https://github.github.com/gfm/#indented-code-blocks). Contains raw text
92 /// which is not parsed as Markdown, although is HTML escaped.
93 CodeBlock(Box<NodeCodeBlock>),
94
95 /// **Block**. A [HTML block](https://github.github.com/gfm/#html-blocks). Contains raw text
96 /// which is neither parsed as Markdown nor HTML escaped.
97 HtmlBlock(NodeHtmlBlock),
98
99 #[cfg(feature = "phoenix_heex")]
100 /// **Block**. A [Phoenix HEEx](https://hexdocs.pm/phoenix/components.html) template block. Contains raw Phoenix HEEx text
101 /// which is neither parsed as Markdown nor HTML escaped.
102 HeexBlock(Box<NodeHeexBlock>),
103
104 /// **Block**. A [paragraph](https://github.github.com/gfm/#paragraphs). Contains **inlines**.
105 Paragraph,
106
107 /// **Block**. A heading; may be an [ATX heading](https://github.github.com/gfm/#atx-headings)
108 /// or a [setext heading](https://github.github.com/gfm/#setext-headings). Contains
109 /// **inlines**.
110 Heading(NodeHeading),
111
112 /// **Block**. A [thematic break](https://github.github.com/gfm/#thematic-breaks). Has no
113 /// children.
114 ThematicBreak,
115
116 /// **Block**. A footnote definition. The `String` is the footnote's name.
117 /// Contains other **blocks**.
118 FootnoteDefinition(NodeFootnoteDefinition),
119
120 /// **Block**. A [table](https://github.github.com/gfm/#tables-extension-) per the GFM spec.
121 /// Contains table rows.
122 Table(Box<NodeTable>),
123
124 /// **Block**. A table row. The `bool` represents whether the row is the header row or not.
125 /// Contains table cells.
126 TableRow(bool),
127
128 /// **Block**. A table cell. Contains **inlines**.
129 TableCell,
130
131 /// **Inline**. [Textual content](https://github.github.com/gfm/#textual-content). All text
132 /// in a document will be contained in a `Text` node.
133 Text(Cow<'static, str>),
134
135 /// **Block**. [Task list item](https://github.github.com/gfm/#task-list-items-extension-).
136 /// The value is the symbol that was used in the brackets to mark a task item as checked, or
137 /// `None` if the item is unchecked.
138 TaskItem(NodeTaskItem),
139
140 /// **Inline**. A [soft line break](https://github.github.com/gfm/#soft-line-breaks). If
141 /// the `hardbreaks` option is set in `Options` during formatting, it will be formatted
142 /// as a `LineBreak`.
143 SoftBreak,
144
145 /// **Inline**. A [hard line break](https://github.github.com/gfm/#hard-line-breaks).
146 LineBreak,
147
148 /// **Inline**. A [code span](https://github.github.com/gfm/#code-spans).
149 Code(NodeCode),
150
151 /// **Inline**. [Raw HTML](https://github.github.com/gfm/#raw-html) contained inline.
152 HtmlInline(String),
153
154 #[cfg(feature = "phoenix_heex")]
155 /// **Inline**. A [Phoenix HEEx](https://hexdocs.pm/phoenix/components.html) inline template element. Contains raw Phoenix HEEx text
156 /// which is neither parsed as Markdown nor HTML escaped.
157 HeexInline(String),
158
159 /// **Block/Inline**. A Raw output node. This will be inserted verbatim into CommonMark and
160 /// HTML output. It can only be created programmatically, and is never parsed from input.
161 Raw(String),
162
163 /// **Inline**. [Emphasized](https://github.github.com/gfm/#emphasis-and-strong-emphasis)
164 /// text.
165 Emph,
166
167 /// **Inline**. [Strong](https://github.github.com/gfm/#emphasis-and-strong-emphasis) text.
168 Strong,
169
170 /// **Inline**. [Strikethrough](https://github.github.com/gfm/#strikethrough-extension-) text
171 /// per the GFM spec.
172 Strikethrough,
173
174 /// ***Inline**. [Highlight](https://www.markdownlang.com/extended/highlight.html) / mark text
175 Highlight,
176
177 /// **Inline**. Inserted text, rendered as `<ins>`.
178 Insert,
179
180 /// **Inline**. Superscript. Enabled with `ext_superscript` option.
181 Superscript,
182
183 /// **Inline**. A [link](https://github.github.com/gfm/#links) to some URL, with possible
184 /// title.
185 Link(Box<NodeLink>),
186
187 /// **Inline**. An [image](https://github.github.com/gfm/#images).
188 Image(Box<NodeLink>),
189
190 /// **Inline**. A footnote reference.
191 FootnoteReference(Box<NodeFootnoteReference>),
192
193 #[cfg(feature = "shortcodes")]
194 /// **Inline**. An Emoji character generated from a shortcode. Enable with feature "shortcodes".
195 ShortCode(Box<NodeShortCode>),
196
197 /// **Inline**. A math span. Contains raw text which is not parsed as Markdown.
198 /// Dollar math or code math
199 ///
200 /// Inline math $1 + 2$ and $`1 + 2`$
201 ///
202 /// Display math $$1 + 2$$ and
203 /// $$
204 /// 1 + 2
205 /// $$
206 ///
207 Math(NodeMath),
208
209 /// **Block**. A [multiline block quote](https://github.github.com/gfm/#block-quotes). Spans multiple
210 /// lines and contains other **blocks**.
211 ///
212 /// ```markdown
213 /// >>>
214 /// A paragraph.
215 ///
216 /// - item one
217 /// - item two
218 /// >>>
219 /// ```
220 MultilineBlockQuote(NodeMultilineBlockQuote),
221
222 /// **Inline**. A character that has been [escaped](https://github.github.com/gfm/#backslash-escapes)
223 ///
224 /// Included in the document tree only if the `escaped_char_spans` [parse
225 /// option](crate::options::ParseBuilder::escaped_char_spans) and/or [render
226 /// option](crate::options::RenderBuilder::escaped_char_spans) is enabled.
227 ///
228 /// If the render option is enabled, these are wrapped in distinct `<span>`
229 /// tags in output HTML and XML.
230 Escaped,
231
232 /// **Inline**. A wikilink to some URL.
233 WikiLink(NodeWikiLink),
234
235 /// **Inline**. Underline. Enabled with `underline` option.
236 Underline,
237
238 /// **Inline**. Subscript. Enabled with `subscript` options.
239 Subscript,
240
241 /// **Inline**. Spoilered text. Enabled with `spoiler` option.
242 SpoileredText,
243
244 /// **Inline**. Text surrounded by escaped markup. Enabled with `spoiler` option.
245 /// The `&'static str` is the tag to be escaped.
246 EscapedTag(&'static str),
247
248 /// **Block**. GitHub style alert boxes which uses a modified blockquote syntax.
249 /// Enabled with the `alerts` option.
250 Alert(Box<NodeAlert>),
251
252 /// **Block**. Block scoped subscript that acts similar to a header.
253 /// Enabled with `subtext` option.
254 Subtext,
255
256 /// **Block**. A container directive block which can be used for custom extensions.
257 /// Contains other **blocks**.
258 ///
259 /// ```markdown
260 /// :::warning
261 /// A paragraph.
262 ///
263 /// - item one
264 /// - item two
265 /// :::
266 /// ```
267 BlockDirective(Box<NodeBlockDirective>),
268}
269
270/// Alignment of a single table cell.
271#[derive(Debug, Copy, Clone, PartialEq, Eq)]
272pub enum TableAlignment {
273 /// Cell content is unaligned.
274 None,
275
276 /// Cell content is aligned left.
277 Left,
278
279 /// Cell content is centered.
280 Center,
281
282 /// Cell content is aligned right.
283 Right,
284}
285
286impl TableAlignment {
287 pub(crate) fn xml_name(&self) -> Option<&'static str> {
288 match *self {
289 TableAlignment::None => None,
290 TableAlignment::Left => Some("left"),
291 TableAlignment::Center => Some("center"),
292 TableAlignment::Right => Some("right"),
293 }
294 }
295}
296
297/// The metadata of a table
298#[derive(Debug, Default, Clone, PartialEq, Eq)]
299pub struct NodeTable {
300 /// The table alignments
301 pub alignments: Vec<TableAlignment>,
302
303 /// Number of columns of the table
304 pub num_columns: usize,
305
306 /// Number of rows of the table
307 pub num_rows: usize,
308
309 /// Number of non-empty, non-autocompleted cells
310 pub num_nonempty_cells: usize,
311}
312
313/// A task list item's contents, and where it was found
314#[derive(Debug, Clone, PartialEq, Eq, Copy)]
315pub struct NodeTaskItem {
316 /// The symbol within the brackets used to mark the task item as checked,
317 /// or `None` if unchecked.
318 pub symbol: Option<char>,
319
320 /// The source position of the symbol itself.
321 pub symbol_sourcepos: Sourcepos,
322}
323
324/// An inline [code span](https://github.github.com/gfm/#code-spans).
325#[derive(Default, Debug, Clone, PartialEq, Eq)]
326pub struct NodeCode {
327 /// The number of backticks
328 pub num_backticks: usize,
329
330 /// The content of the inline code span.
331 /// As the contents are not interpreted as Markdown at all,
332 /// they are contained within this structure,
333 /// rather than inserted into a child inline of any kind.
334 pub literal: String,
335}
336
337/// The details of a link's destination, or an image's source.
338#[derive(Default, Debug, Clone, PartialEq, Eq)]
339pub struct NodeLink {
340 /// The URL for the link destination or image source.
341 pub url: String,
342
343 /// The title for the link or image.
344 ///
345 /// Note this field is used for the `title` attribute by the HTML formatter even for images;
346 /// `alt` text is supplied in the image inline text.
347 pub title: String,
348}
349
350/// The details of a wikilink's destination.
351#[derive(Default, Debug, Clone, PartialEq, Eq)]
352pub struct NodeWikiLink {
353 /// The URL for the link destination.
354 pub url: String,
355}
356
357/// The metadata of a list; the kind of list, the delimiter used and so on.
358#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
359pub struct NodeList {
360 /// The kind of list (bullet (unordered) or ordered).
361 pub list_type: ListType,
362
363 /// Number of spaces before the list marker.
364 pub marker_offset: usize,
365
366 /// Number of characters between the start of the list marker and the item text (including the list marker(s)).
367 pub padding: usize,
368
369 /// For ordered lists, the ordinal the list starts at.
370 pub start: usize,
371
372 /// For ordered lists, the delimiter after each number.
373 pub delimiter: ListDelimType,
374
375 /// For bullet lists, the character used for each bullet.
376 pub bullet_char: u8,
377
378 /// Whether the list is [tight](https://github.github.com/gfm/#tight), i.e. whether the
379 /// paragraphs are wrapped in `<p>` tags when formatted as HTML.
380 pub tight: bool,
381
382 /// Whether the list contains tasks (checkbox items)
383 pub is_task_list: bool,
384}
385
386/// The metadata of a description list
387#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
388pub struct NodeDescriptionItem {
389 /// Number of spaces before the list marker.
390 pub marker_offset: usize,
391
392 /// Number of characters between the start of the list marker and the item text (including the list marker(s)).
393 pub padding: usize,
394
395 /// Whether the list is [tight](https://github.github.com/gfm/#tight), i.e. whether the
396 /// paragraphs are wrapped in `<p>` tags when formatted as HTML.
397 pub tight: bool,
398}
399
400/// The type of list.
401#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
402pub enum ListType {
403 /// A bullet list, i.e. an unordered list.
404 #[default]
405 Bullet,
406
407 /// An ordered list.
408 Ordered,
409}
410
411/// The delimiter for ordered lists, i.e. the character which appears after each number.
412#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
413pub enum ListDelimType {
414 /// A period character `.`.
415 #[default]
416 Period,
417
418 /// A paren character `)`.
419 Paren,
420}
421
422impl ListDelimType {
423 pub(crate) fn xml_name(&self) -> &'static str {
424 match *self {
425 ListDelimType::Period => "period",
426 ListDelimType::Paren => "paren",
427 }
428 }
429}
430
431/// The metadata and data of a code block (fenced or indented).
432#[derive(Default, Debug, Clone, PartialEq, Eq)]
433pub struct NodeCodeBlock {
434 /// Whether the code block is fenced.
435 pub fenced: bool,
436
437 /// For fenced code blocks, the fence character itself (`` ` `` or `~`).
438 pub fence_char: u8,
439
440 /// For fenced code blocks, the length of the fence.
441 pub fence_length: usize,
442
443 /// For fenced code blocks, the indentation level of the code within the block.
444 pub fence_offset: usize,
445
446 /// For fenced code blocks, the [info string](https://github.github.com/gfm/#info-string) after
447 /// the opening fence, if any.
448 pub info: String,
449
450 /// The literal contents of the code block. As the contents are not interpreted as Markdown at
451 /// all, they are contained within this structure, rather than inserted into a child inline of
452 /// any kind.
453 pub literal: String,
454
455 /// Whether the code block was explicitly closed by a closing fence. This is only meaningful
456 /// for fenced code blocks; indented code blocks are treated as closed (true).
457 pub closed: bool,
458}
459
460/// The metadata of a heading.
461#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
462pub struct NodeHeading {
463 /// The level of the header; from 1 to 6 for ATX headings, 1 or 2 for setext headings.
464 pub level: u8,
465
466 /// Whether the heading is setext (if not, ATX).
467 pub setext: bool,
468
469 /// Whether this ATX heading had a closing sequence of trailing hashes.
470 /// Only meaningful for ATX headings (i.e. when `setext` is false).
471 pub closed: bool,
472}
473
474/// The metadata of an included HTML block.
475#[derive(Debug, Default, Clone, PartialEq, Eq)]
476pub struct NodeHtmlBlock {
477 /// The HTML block's type
478 pub block_type: u8,
479
480 /// The literal contents of the HTML block. Per NodeCodeBlock, the content is included here
481 /// rather than in any inline.
482 pub literal: String,
483}
484
485/// The metadata of a footnote definition.
486#[derive(Debug, Default, Clone, PartialEq, Eq)]
487pub struct NodeFootnoteDefinition {
488 /// The name of the footnote.
489 pub name: String,
490
491 /// Total number of references to this footnote
492 pub total_references: u32,
493}
494
495/// The metadata of a footnote reference.
496#[derive(Debug, Default, Clone, PartialEq, Eq)]
497pub struct NodeFootnoteReference {
498 /// The name of the footnote.
499 pub name: String,
500
501 /// The original text elements of the footnote, including their source position spans.
502 pub texts: Vec<(String, usize)>,
503
504 /// The index of reference to the same footnote
505 pub ref_num: u32,
506
507 /// The index of the footnote in the document.
508 pub ix: u32,
509}
510
511/// The metadata of a multiline blockquote.
512#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
513pub struct NodeMultilineBlockQuote {
514 /// The length of the fence.
515 pub fence_length: usize,
516
517 /// The indentation level of the fence marker.
518 pub fence_offset: usize,
519}
520
521/// An inline math span
522#[derive(Default, Debug, Clone, PartialEq, Eq)]
523pub struct NodeMath {
524 /// Whether this is dollar math (`$` or `$$`).
525 /// `false` indicates it is code math
526 pub dollar_math: bool,
527
528 /// Whether this is display math (using `$$`)
529 pub display_math: bool,
530
531 /// The literal contents of the math span.
532 /// As the contents are not interpreted as Markdown at all,
533 /// they are contained within this structure,
534 /// rather than inserted into a child inline of any kind.
535 pub literal: String,
536}
537
538/// The metadata of an Alert node.
539#[derive(Debug, Clone, PartialEq, Eq)]
540pub struct NodeAlert {
541 /// Type of alert
542 pub alert_type: AlertType,
543
544 /// Overridden title. If `None`, then use the default title.
545 pub title: Option<String>,
546
547 /// Originated from a multiline blockquote.
548 pub multiline: bool,
549
550 /// The length of the fence (multiline only).
551 pub fence_length: usize,
552
553 /// The indentation level of the fence marker (multiline only)
554 pub fence_offset: usize,
555}
556
557/// The type of alert.
558#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
559pub enum AlertType {
560 /// Useful information that users should know, even when skimming content
561 #[default]
562 Note,
563
564 /// Helpful advice for doing things better or more easily
565 Tip,
566
567 /// Key information users need to know to achieve their goal
568 Important,
569
570 /// Urgent info that needs immediate user attention to avoid problems
571 Warning,
572
573 /// Advises about risks or negative outcomes of certain actions
574 Caution,
575}
576
577impl AlertType {
578 /// Returns the default title for an alert type
579 pub fn default_title(&self) -> &'static str {
580 match *self {
581 AlertType::Note => "Note",
582 AlertType::Tip => "Tip",
583 AlertType::Important => "Important",
584 AlertType::Warning => "Warning",
585 AlertType::Caution => "Caution",
586 }
587 }
588
589 /// Returns the CSS class to use for an alert type
590 pub fn css_class(&self, style: AlertStyleType) -> &'static str {
591 match (*self, style) {
592 (AlertType::Note, AlertStyleType::Specific) => "markdown-alert-note",
593 (AlertType::Note, AlertStyleType::Semantic) => "note",
594 (AlertType::Tip, AlertStyleType::Specific) => "markdown-alert-tip",
595 (AlertType::Tip, AlertStyleType::Semantic) => "tip",
596 (AlertType::Important, AlertStyleType::Specific) => "markdown-alert-important",
597 (AlertType::Important, AlertStyleType::Semantic) => "important",
598 (AlertType::Warning, AlertStyleType::Specific) => "markdown-alert-warning",
599 (AlertType::Warning, AlertStyleType::Semantic) => "warning",
600 (AlertType::Caution, AlertStyleType::Specific) => "markdown-alert-caution",
601 (AlertType::Caution, AlertStyleType::Semantic) => "caution",
602 }
603 }
604}
605
606/// The metadata of a container block directive node.
607#[derive(Default, Debug, Clone, PartialEq, Eq)]
608pub struct NodeBlockDirective {
609 /// The length of the fence.
610 pub fence_length: usize,
611
612 /// The indentation level of the fence marker.
613 pub fence_offset: usize,
614
615 /// The info string after the opening fence, if any.
616 pub info: String,
617}
618
619/// Attributes on the node, if any.
620///
621/// These are somewhat HTML-specific in nature, though none are formatted
622/// automatically to HTML. There may be an `id` (specified like "#xyz" in the
623/// source document; last ID-like attribute wins), zero or more `classes`
624/// (specified like ".abc" in the source document), and zero or more `pairs`
625/// (specified like "a=b" in the source document).
626///
627/// Attribute values may be quoted as follows:
628///
629/// * `#"id with spaces in it"`
630/// * `."class with \"double quotes\" in it"`
631/// * `pair="with spaces and such in the value"`
632///
633/// Pair keys may not be quoted. You can't open quotes late or close them early.
634/// Backslashes escape whatever character follows them within quotes.
635/// Newlines are not permitted in quoted values, but they are permitted around
636/// attribute values when attached to an inline, e.g.:
637///
638/// ```markdown
639/// Some `code`{
640/// a="hello"
641/// b="privyet"
642/// }
643/// ```
644///
645/// Take care that you don't lead such lines with four or more spaces relative to
646/// the block offset, or you'll get an unwanted code block!
647#[cfg(feature = "attributes")]
648#[derive(Default, Debug, Clone, PartialEq, Eq)]
649pub struct Attributes {
650 /// The id attribute, if any; specified like `#id` (last wins).
651 pub id: Option<String>,
652
653 /// Class attributes, if any; specified like `.a .b`.
654 pub classes: Vec<String>,
655
656 /// Attribute pairs, if any; specified like `a=b c=d`.
657 pub pairs: Vec<(String, String)>,
658}
659
660impl NodeValue {
661 /// Indicates whether this node is a block node or inline node.
662 pub fn block(&self) -> bool {
663 match *self {
664 NodeValue::Document
665 | NodeValue::BlockQuote
666 | NodeValue::FootnoteDefinition(_)
667 | NodeValue::List(..)
668 | NodeValue::DescriptionList
669 | NodeValue::DescriptionItem(_)
670 | NodeValue::DescriptionTerm
671 | NodeValue::DescriptionDetails
672 | NodeValue::Item(..)
673 | NodeValue::CodeBlock(..)
674 | NodeValue::HtmlBlock(..)
675 | NodeValue::Paragraph
676 | NodeValue::Heading(..)
677 | NodeValue::ThematicBreak
678 | NodeValue::Table(..)
679 | NodeValue::TableRow(..)
680 | NodeValue::TableCell
681 | NodeValue::TaskItem(..)
682 | NodeValue::MultilineBlockQuote(_)
683 | NodeValue::Alert(_)
684 | NodeValue::Subtext => true,
685 NodeValue::BlockDirective(_) => true,
686 #[cfg(feature = "phoenix_heex")]
687 NodeValue::HeexBlock(..) => true,
688 _ => false,
689 }
690 }
691
692 /// Whether the type the node is of can contain inline nodes.
693 pub fn contains_inlines(&self) -> bool {
694 matches!(
695 *self,
696 NodeValue::Paragraph
697 | NodeValue::Heading(..)
698 | NodeValue::TableCell
699 | NodeValue::Subtext
700 )
701 }
702
703 /// Return a reference to the text of a `Text` inline, if this node is one.
704 ///
705 /// Convenience method.
706 pub fn text(&self) -> Option<&str> {
707 match *self {
708 NodeValue::Text(ref t) => Some(t),
709 _ => None,
710 }
711 }
712
713 /// Return a mutable reference to the text of a `Text` inline, if this node is one.
714 ///
715 /// Convenience method.
716 pub fn text_mut(&mut self) -> Option<&mut Cow<'static, str>> {
717 match *self {
718 NodeValue::Text(ref mut t) => Some(t),
719 _ => None,
720 }
721 }
722
723 /// Returns the name this kind of node gets in an XML document. Follows
724 /// the CommonMark DTD for nodes from the spec, otherwise we just vibe in a
725 /// stable way.
726 pub fn xml_node_name(&self) -> &'static str {
727 match *self {
728 NodeValue::Document => "document",
729 NodeValue::BlockQuote => "block_quote",
730 NodeValue::FootnoteDefinition(_) => "footnote_definition",
731 NodeValue::List(..) => "list",
732 NodeValue::DescriptionList => "description_list",
733 NodeValue::DescriptionItem(_) => "description_item",
734 NodeValue::DescriptionTerm => "description_term",
735 NodeValue::DescriptionDetails => "description_details",
736 NodeValue::Item(..) => "item",
737 NodeValue::CodeBlock(..) => "code_block",
738 NodeValue::HtmlBlock(..) => "html_block",
739 #[cfg(feature = "phoenix_heex")]
740 NodeValue::HeexBlock(..) => "heex_block",
741 NodeValue::Paragraph => "paragraph",
742 NodeValue::Heading(..) => "heading",
743 NodeValue::ThematicBreak => "thematic_break",
744 NodeValue::Table(..) => "table",
745 NodeValue::TableRow(..) => "table_row",
746 NodeValue::TableCell => "table_cell",
747 NodeValue::Text(..) => "text",
748 NodeValue::SoftBreak => "softbreak",
749 NodeValue::LineBreak => "linebreak",
750 NodeValue::Image(..) => "image",
751 NodeValue::Link(..) => "link",
752 NodeValue::Emph => "emph",
753 NodeValue::Strong => "strong",
754 NodeValue::Code(..) => "code",
755 NodeValue::HtmlInline(..) => "html_inline",
756 #[cfg(feature = "phoenix_heex")]
757 NodeValue::HeexInline(..) => "heex_inline",
758 NodeValue::Raw(..) => "raw",
759 NodeValue::Strikethrough => "strikethrough",
760 NodeValue::Highlight => "highlight",
761 NodeValue::Insert => "insert",
762 NodeValue::FrontMatter(_) => "frontmatter",
763 NodeValue::TaskItem { .. } => "taskitem",
764 NodeValue::Superscript => "superscript",
765 NodeValue::FootnoteReference(..) => "footnote_reference",
766 #[cfg(feature = "shortcodes")]
767 NodeValue::ShortCode(_) => "shortcode",
768 NodeValue::MultilineBlockQuote(_) => "multiline_block_quote",
769 NodeValue::Escaped => "escaped",
770 NodeValue::Math(..) => "math",
771 NodeValue::WikiLink(..) => "wikilink",
772 NodeValue::Underline => "underline",
773 NodeValue::Subscript => "subscript",
774 NodeValue::SpoileredText => "spoiler",
775 NodeValue::EscapedTag(_) => "escaped_tag",
776 NodeValue::Alert(_) => "alert",
777 NodeValue::Subtext => "subtext",
778 NodeValue::BlockDirective(_) => "block_directive",
779 }
780 }
781
782 pub(crate) fn accepts_lines(&self) -> bool {
783 match *self {
784 NodeValue::Paragraph
785 | NodeValue::Heading(..)
786 | NodeValue::CodeBlock(..)
787 | NodeValue::Subtext => true,
788 #[cfg(feature = "phoenix_heex")]
789 NodeValue::HeexBlock(..) => true,
790 _ => false,
791 }
792 }
793}
794
795/// A single node in the CommonMark AST.
796///
797/// The struct contains metadata about the node's position in the original document, and the core
798/// enum, `NodeValue`.
799#[derive(Clone, PartialEq, Eq)]
800pub struct Ast {
801 /// The node value itself.
802 pub value: NodeValue,
803
804 /// The positions in the source document this node comes from.
805 pub sourcepos: Sourcepos,
806
807 #[cfg(feature = "attributes")]
808 /// [Attributes] on this node, if any.
809 pub attrs: Option<Box<Attributes>>,
810
811 pub(crate) content: String,
812 pub(crate) open: bool,
813 pub(crate) last_line_blank: bool,
814 pub(crate) table_visited: bool,
815 pub(crate) line_offsets: Vec<usize>,
816}
817
818impl std::fmt::Debug for Ast {
819 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
820 write!(f, "<{:?} ({})>", self.value, self.sourcepos)
821 }
822}
823
824#[allow(dead_code)]
825#[cfg(all(target_pointer_width = "64", not(feature = "attributes")))]
826/// Assert the size of Ast is 128 bytes. It's pretty big; let's stop it getting
827/// bigger.
828const AST_SIZE_ASSERTION: [u8; 128] = [0; std::mem::size_of::<Ast>()];
829
830#[allow(dead_code)]
831#[cfg(all(target_pointer_width = "64", not(feature = "attributes")))]
832/// Assert the total size of what we allocate in the Arena, for reference.
833///
834/// Note that the size adds to Ast:
835/// * 8 bytes for RefCell.
836/// * 40 bytes for arena_tree::Node's 5 pointers.
837const AST_NODE_SIZE_ASSERTION: [u8; 176] = [0; std::mem::size_of::<AstNode<'_>>()];
838
839/// Represents the position in the source Markdown this node was rendered from.
840#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
841pub struct Sourcepos {
842 /// The line and column of the first character of this node.
843 pub start: LineColumn,
844 /// The line and column of the last character of this node.
845 pub end: LineColumn,
846}
847
848impl std::fmt::Debug for Sourcepos {
849 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
850 std::fmt::Display::fmt(self, f)
851 }
852}
853
854impl std::fmt::Display for Sourcepos {
855 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
856 write!(
857 f,
858 "{}:{}-{}:{}",
859 self.start.line, self.start.column, self.end.line, self.end.column,
860 )
861 }
862}
863
864impl From<(LineColumn, LineColumn)> for Sourcepos {
865 fn from((start, end): (LineColumn, LineColumn)) -> Sourcepos {
866 Sourcepos { start, end }
867 }
868}
869
870impl From<(usize, usize, usize, usize)> for Sourcepos {
871 fn from(sp: (usize, usize, usize, usize)) -> Sourcepos {
872 Sourcepos {
873 start: LineColumn {
874 line: sp.0,
875 column: sp.1,
876 },
877 end: LineColumn {
878 line: sp.2,
879 column: sp.3,
880 },
881 }
882 }
883}
884
885/// Represents the 1-based line and column positions of a given character.
886///
887/// By default, the `column` value is measured in UTF-8 byte offsets (1-based),
888/// matching cmark behavior. This means multi-byte UTF-8 characters
889/// (for example, `ö` or `好`) increase the column count by their byte
890/// length rather than by Rust's `char` count.
891///
892/// Enable [`parse.sourcepos_chars`][crate::options::Parse#structfield.sourcepos_chars] to have
893/// column values reported as a Unicode character count instead.
894#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
895pub struct LineColumn {
896 /// The 1-based line number of the character.
897 pub line: usize,
898 /// The 1-based column number of the character.
899 ///
900 /// By default this is counted in UTF-8 bytes (so a 3-byte character
901 /// increments the column by 3). Enable
902 /// [`parse.sourcepos_chars`][crate::options::Parse#structfield.sourcepos_chars] to have
903 /// it reported as a Unicode character count instead.
904 pub column: usize,
905}
906
907impl From<(usize, usize)> for LineColumn {
908 fn from(lc: (usize, usize)) -> LineColumn {
909 LineColumn {
910 line: lc.0,
911 column: lc.1,
912 }
913 }
914}
915
916impl LineColumn {
917 /// Return a new LineColumn based on this one, with the column adjusted by offset.
918 pub fn column_add(&self, offset: isize) -> LineColumn {
919 LineColumn {
920 line: self.line,
921 column: usize::try_from((self.column as isize) + offset).unwrap(),
922 }
923 }
924}
925
926impl Ast {
927 /// Create a new AST node with the given value and starting sourcepos. The
928 /// end column is set to zero; it is expected this will be set manually
929 /// or later in the parse. Use [`Ast::new_with_sourcepos`] if you have full
930 /// sourcepos.
931 pub fn new(value: NodeValue, start: LineColumn) -> Self {
932 Ast {
933 value,
934 content: String::new(),
935 sourcepos: (start.line, start.column, start.line, 0).into(),
936 #[cfg(feature = "attributes")]
937 attrs: None,
938 open: true,
939 last_line_blank: false,
940 table_visited: false,
941 line_offsets: Vec::new(),
942 }
943 }
944
945 /// Create a new AST node with the given value and sourcepos.
946 pub fn new_with_sourcepos(value: NodeValue, sourcepos: Sourcepos) -> Self {
947 Ast {
948 value,
949 content: String::new(),
950 sourcepos,
951 #[cfg(feature = "attributes")]
952 attrs: None,
953 open: true,
954 last_line_blank: false,
955 table_visited: false,
956 line_offsets: Vec::new(),
957 }
958 }
959}
960
961/// The type of a node within the document.
962///
963/// It is bound by the lifetime `'a`, which corresponds to the `Arena` nodes are
964/// allocated in. Child `Ast`s are wrapped in `RefCell` for interior mutability.
965///
966/// You can construct a new `AstNode` from a `NodeValue` using the `From` trait:
967///
968/// ```rust
969/// # use comrak::nodes::{AstNode, NodeValue};
970/// let root = AstNode::from(NodeValue::Document);
971/// ```
972///
973/// Note that no sourcepos information is given to the created node. If you wish
974/// to assign sourcepos information, use the `From` trait to create an `AstNode`
975/// from an `Ast`:
976///
977/// ```rust
978/// # use comrak::nodes::{Ast, AstNode, NodeValue};
979/// let root = AstNode::from(Ast::new_with_sourcepos(
980/// NodeValue::Paragraph,
981/// (4, 1, 4, 10).into(),
982/// ));
983/// ```
984///
985/// For practical use, you'll probably need it allocated in an `Arena`, in which
986/// case you can use `.into()` to simplify creation:
987///
988/// ```rust
989/// # use comrak::{nodes::{AstNode, NodeValue}, Arena};
990/// # let arena = Arena::new();
991/// let node_in_arena = arena.alloc(NodeValue::Document.into());
992/// ```
993pub type AstNode<'a> = arena_tree::Node<'a, RefCell<Ast>>;
994
995/// A reference to a node in an arena. Unless you are manually creating nodes
996/// before the arena, this is the type you will see most often.
997pub type Node<'a> = &'a AstNode<'a>;
998
999impl From<NodeValue> for AstNode<'_> {
1000 /// Create a new AST node with the given value. The sourcepos is set to (0,0)-(0,0).
1001 fn from(value: NodeValue) -> Self {
1002 arena_tree::Node::new(RefCell::new(Ast::new(value, LineColumn::default())))
1003 }
1004}
1005
1006impl From<Ast> for AstNode<'_> {
1007 /// Create a new AST node with the given Ast.
1008 fn from(ast: Ast) -> Self {
1009 arena_tree::Node::new(RefCell::new(ast))
1010 }
1011}
1012
1013/// Validation errors produced by [arena_tree::Node::validate].
1014#[derive(Debug, Clone)]
1015pub enum ValidationError<'a> {
1016 /// The type of a child node is not allowed in the parent node. This can happen when an inline
1017 /// node is found in a block container, a block is found in an inline node, etc.
1018 InvalidChildType {
1019 /// The parent node.
1020 parent: Node<'a>,
1021 /// The child node.
1022 child: Node<'a>,
1023 },
1024}
1025
1026impl<'a> arena_tree::Node<'a, RefCell<Ast>> {
1027 /// Returns true if the given node can contain a node with the given value.
1028 pub fn can_contain_type(&self, child: &NodeValue) -> bool {
1029 match *child {
1030 NodeValue::Raw(_) => {
1031 return true;
1032 }
1033 NodeValue::Document => {
1034 return false;
1035 }
1036 NodeValue::FrontMatter(_) => {
1037 return node_matches!(self, NodeValue::Document);
1038 }
1039 _ => {}
1040 }
1041
1042 match self.data().value {
1043 NodeValue::Document
1044 | NodeValue::BlockQuote
1045 | NodeValue::FootnoteDefinition(_)
1046 | NodeValue::DescriptionTerm
1047 | NodeValue::DescriptionDetails
1048 | NodeValue::Item(..)
1049 | NodeValue::TaskItem(..) => {
1050 child.block() && !matches!(*child, NodeValue::Item(..) | NodeValue::TaskItem(..))
1051 }
1052 NodeValue::List(..) => matches!(*child, NodeValue::Item(..) | NodeValue::TaskItem(..)),
1053 NodeValue::DescriptionList => matches!(*child, NodeValue::DescriptionItem(_)),
1054 NodeValue::DescriptionItem(_) => matches!(
1055 *child,
1056 NodeValue::DescriptionTerm | NodeValue::DescriptionDetails
1057 ),
1058 #[cfg(feature = "shortcodes")]
1059 NodeValue::ShortCode(..) => !child.block(),
1060 NodeValue::Paragraph
1061 | NodeValue::Heading(..)
1062 | NodeValue::Emph
1063 | NodeValue::Strong
1064 | NodeValue::Link(..)
1065 | NodeValue::Image(..)
1066 | NodeValue::WikiLink(..)
1067 | NodeValue::Strikethrough
1068 | NodeValue::Highlight
1069 | NodeValue::Insert
1070 | NodeValue::Superscript
1071 | NodeValue::SpoileredText
1072 | NodeValue::Underline
1073 | NodeValue::Subscript
1074 | NodeValue::Subtext
1075 // XXX: this is quite a hack: the EscapedTag _contains_ whatever was
1076 // possibly going to fall into the spoiler. This should be fixed in
1077 // inlines.
1078 | NodeValue::EscapedTag(_)
1079 => !child.block(),
1080 NodeValue::Table(..) => matches!(*child, NodeValue::TableRow(..)),
1081 NodeValue::TableRow(..) => matches!(*child, NodeValue::TableCell),
1082 NodeValue::TableCell => {
1083 #[cfg(feature = "shortcodes")]
1084 if matches!(*child, NodeValue::ShortCode(..)) {
1085 return true;
1086 }
1087
1088 #[cfg(feature = "phoenix_heex")]
1089 if matches!(*child, NodeValue::HeexInline(_)) {
1090 return true;
1091 }
1092
1093 matches!(
1094 *child,
1095 NodeValue::Text(..)
1096 | NodeValue::Code(..)
1097 | NodeValue::Emph
1098 | NodeValue::Strong
1099 | NodeValue::Link(..)
1100 | NodeValue::Image(..)
1101 | NodeValue::Strikethrough
1102 | NodeValue::Highlight
1103 | NodeValue::Insert
1104 | NodeValue::HtmlInline(..)
1105 | NodeValue::Math(..)
1106 | NodeValue::WikiLink(..)
1107 | NodeValue::FootnoteReference(..)
1108 | NodeValue::Superscript
1109 | NodeValue::SpoileredText
1110 | NodeValue::Underline
1111 | NodeValue::Subscript
1112 | NodeValue::TaskItem(_)
1113 | NodeValue::Escaped
1114 | NodeValue::EscapedTag(_)
1115 )
1116 }
1117 NodeValue::MultilineBlockQuote(_) => {
1118 child.block() && !matches!(*child, NodeValue::Item(..) | NodeValue::TaskItem(..))
1119 }
1120 NodeValue::Alert(_) => {
1121 child.block() && !matches!(*child, NodeValue::Item(..) | NodeValue::TaskItem(..))
1122 }
1123 NodeValue::BlockDirective(_) => {
1124 child.block() && !matches!(*child, NodeValue::Item(..) | NodeValue::TaskItem(..))
1125 }
1126
1127 NodeValue::Escaped => matches!(*child, NodeValue::Text(_)),
1128
1129 NodeValue::FrontMatter(_)
1130 | NodeValue::CodeBlock(_)
1131 | NodeValue::HtmlBlock(_)
1132 | NodeValue::ThematicBreak
1133 | NodeValue::Text(_)
1134 | NodeValue::SoftBreak
1135 | NodeValue::LineBreak
1136 | NodeValue::Code(_)
1137 | NodeValue::HtmlInline(_)
1138 | NodeValue::Raw(_)
1139 | NodeValue::FootnoteReference(_)
1140 | NodeValue::Math(_) => false,
1141
1142 #[cfg(feature = "phoenix_heex")]
1143 NodeValue::HeexBlock(_) | NodeValue::HeexInline(_) => false,
1144 }
1145 }
1146
1147 /// The Comrak representation of a markdown node in Rust isn't strict enough to rule out
1148 /// invalid trees according to the CommonMark specification. One simple example is that block
1149 /// containers, such as lists, should only contain blocks, but it's possible to put naked
1150 /// inline text in a list item. Such invalid trees can lead Comrak to generate incorrect output
1151 /// if rendered.
1152 ///
1153 /// This method performs additional structural checks to ensure that a markdown AST is valid
1154 /// according to the CommonMark specification.
1155 ///
1156 /// Note that those invalid trees can only be generated programmatically. Parsing markdown with
1157 /// Comrak, on the other hand, should always produce a valid tree.
1158 pub fn validate(&'a self) -> Result<(), ValidationError<'a>> {
1159 let mut stack = vec![self];
1160
1161 while let Some(node) = stack.pop() {
1162 // Check that this node type is valid wrt to the type of its parent.
1163 if let Some(parent) = node.parent() {
1164 if !parent.can_contain_type(&node.data().value) {
1165 return Err(ValidationError::InvalidChildType {
1166 parent,
1167 child: node,
1168 });
1169 }
1170 }
1171
1172 stack.extend(node.children());
1173 }
1174
1175 Ok(())
1176 }
1177
1178 /// Recurses through a node and all of its children in depth-first
1179 /// (document) order, returning the concatenated literal contents of text,
1180 /// code and math blocks. Line breaks and soft breaks are represented as a
1181 /// single whitespace character.
1182 pub fn collect_text(&'a self) -> String {
1183 let mut text = String::with_capacity(20);
1184 self.collect_text_append(&mut text);
1185 text
1186 }
1187
1188 /// Recurses through a node and all of its children in depth-first
1189 /// (document) order, appending the literal contents of text, code and math
1190 /// blocks to an output buffer. Line breaks and soft breaks are represented
1191 /// as a single whitespace character.
1192 pub fn collect_text_append(&'a self, output: &mut String) {
1193 match self.data().value {
1194 NodeValue::Text(ref literal) => output.push_str(literal),
1195 NodeValue::Code(NodeCode { ref literal, .. }) => output.push_str(literal),
1196 NodeValue::LineBreak | NodeValue::SoftBreak => output.push(' '),
1197 NodeValue::Math(NodeMath { ref literal, .. }) => output.push_str(literal),
1198 _ => {
1199 for n in self.children() {
1200 n.collect_text_append(output);
1201 }
1202 }
1203 }
1204 }
1205
1206 pub(crate) fn last_child_is_open(&self) -> bool {
1207 self.last_child().is_some_and(|n| n.data().open)
1208 }
1209
1210 pub(crate) fn ends_with_blank_line(&self) -> bool {
1211 let mut it = Some(self);
1212 while let Some(cur) = it {
1213 if cur.data().last_line_blank {
1214 return true;
1215 }
1216 match cur.data().value {
1217 NodeValue::List(..) | NodeValue::Item(..) | NodeValue::TaskItem(..) => {
1218 it = cur.last_child()
1219 }
1220 _ => it = None,
1221 };
1222 }
1223 false
1224 }
1225
1226 pub(crate) fn containing_block(&'a self) -> Option<Node<'a>> {
1227 let mut ch = Some(self);
1228 while let Some(n) = ch {
1229 if n.data().value.block() {
1230 return Some(n);
1231 }
1232 ch = n.parent();
1233 }
1234 None
1235 }
1236}