Skip to main content

kode_markdown/
nodes.rs

1/// Classification of markdown node types from the tree-sitter CST.
2///
3/// These represent the semantic meaning of each node, useful for
4/// rendering decisions and command dispatch.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6#[non_exhaustive]
7pub enum NodeKind {
8    // Block-level
9    Document,
10    Section,
11    Paragraph,
12    Heading { level: u8 },
13    FencedCodeBlock,
14    IndentedCodeBlock,
15    BlockQuote,
16    BulletList,
17    OrderedList,
18    ListItem,
19    ThematicBreak,
20    HtmlBlock,
21    Table,
22    TableRow,
23    TableHeader,
24    TableDelimiterRow,
25    LinkReferenceDefinition,
26    FrontMatter,
27
28    // Inline
29    Emphasis,       // *italic* or _italic_
30    StrongEmphasis, // **bold** or __bold__
31    InlineCode,     // `code`
32    Link,           // [text](url)
33    Image,          // ![alt](url)
34    Strikethrough,  // ~~text~~
35    HardLineBreak,
36    SoftLineBreak,
37
38    // Code block parts
39    InfoString,       // language identifier in fenced code block
40    CodeFenceContent, // the code inside a fenced block
41
42    // Other
43    Inline, // inline container
44    Text,   // plain text
45    Unknown,
46}
47
48impl NodeKind {
49    /// Classify a tree-sitter node kind string.
50    pub fn from_ts_kind(kind: &str) -> Self {
51        match kind {
52            "document" => NodeKind::Document,
53            "section" => NodeKind::Section,
54            "paragraph" => NodeKind::Paragraph,
55            "atx_heading" | "setext_heading" => NodeKind::Heading { level: 0 }, // level set separately
56            "fenced_code_block" => NodeKind::FencedCodeBlock,
57            "indented_code_block" => NodeKind::IndentedCodeBlock,
58            "block_quote" => NodeKind::BlockQuote,
59            "thematic_break" => NodeKind::ThematicBreak,
60            "html_block" => NodeKind::HtmlBlock,
61            "list" => NodeKind::BulletList, // caller refines to ordered/bullet
62            "list_item" => NodeKind::ListItem,
63            "pipe_table" => NodeKind::Table,
64            "pipe_table_row" => NodeKind::TableRow,
65            "pipe_table_header" => NodeKind::TableHeader,
66            "pipe_table_delimiter_row" => NodeKind::TableDelimiterRow,
67            "link_reference_definition" => NodeKind::LinkReferenceDefinition,
68            "minus_metadata" | "plus_metadata" => NodeKind::FrontMatter,
69
70            "emphasis" => NodeKind::Emphasis,
71            "strong_emphasis" => NodeKind::StrongEmphasis,
72            "code_span" => NodeKind::InlineCode,
73            "link" | "full_reference_link" | "collapsed_reference_link"
74            | "shortcut_link" | "autolink" | "uri_autolink" => NodeKind::Link,
75            "image" => NodeKind::Image,
76            "strikethrough" => NodeKind::Strikethrough,
77            "hard_line_break" => NodeKind::HardLineBreak,
78            "soft_line_break" => NodeKind::SoftLineBreak,
79
80            "info_string" => NodeKind::InfoString,
81            "code_fence_content" => NodeKind::CodeFenceContent,
82
83            "inline" => NodeKind::Inline,
84
85            _ => NodeKind::Unknown,
86        }
87    }
88
89    /// True if this is a block-level element.
90    pub fn is_block(&self) -> bool {
91        matches!(
92            self,
93            NodeKind::Document
94                | NodeKind::Section
95                | NodeKind::Paragraph
96                | NodeKind::Heading { .. }
97                | NodeKind::FencedCodeBlock
98                | NodeKind::IndentedCodeBlock
99                | NodeKind::BlockQuote
100                | NodeKind::BulletList
101                | NodeKind::OrderedList
102                | NodeKind::ListItem
103                | NodeKind::ThematicBreak
104                | NodeKind::HtmlBlock
105                | NodeKind::Table
106                | NodeKind::LinkReferenceDefinition
107                | NodeKind::FrontMatter
108        )
109    }
110
111    /// True if this is an inline element.
112    pub fn is_inline(&self) -> bool {
113        matches!(
114            self,
115            NodeKind::Emphasis
116                | NodeKind::StrongEmphasis
117                | NodeKind::InlineCode
118                | NodeKind::Link
119                | NodeKind::Image
120                | NodeKind::Strikethrough
121                | NodeKind::HardLineBreak
122                | NodeKind::SoftLineBreak
123                | NodeKind::Inline
124                | NodeKind::Text
125        )
126    }
127
128    /// True if this node can contain other blocks (is a container).
129    pub fn is_container(&self) -> bool {
130        matches!(
131            self,
132            NodeKind::Document
133                | NodeKind::Section
134                | NodeKind::BlockQuote
135                | NodeKind::BulletList
136                | NodeKind::OrderedList
137                | NodeKind::ListItem
138        )
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn classify_block_nodes() {
148        assert!(NodeKind::from_ts_kind("paragraph").is_block());
149        assert!(NodeKind::from_ts_kind("atx_heading").is_block());
150        assert!(NodeKind::from_ts_kind("fenced_code_block").is_block());
151        assert!(NodeKind::from_ts_kind("list").is_block());
152    }
153
154    #[test]
155    fn classify_inline_nodes() {
156        assert!(NodeKind::from_ts_kind("emphasis").is_inline());
157        assert!(NodeKind::from_ts_kind("strong_emphasis").is_inline());
158        assert!(NodeKind::from_ts_kind("code_span").is_inline());
159        assert!(NodeKind::from_ts_kind("link").is_inline());
160    }
161
162    #[test]
163    fn containers() {
164        assert!(NodeKind::from_ts_kind("block_quote").is_container());
165        assert!(NodeKind::from_ts_kind("list").is_container());
166        assert!(!NodeKind::from_ts_kind("paragraph").is_container());
167    }
168}