Skip to main content

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}