Skip to main content

panache_parser/syntax/
fenced_divs.rs

1//! Fenced div AST node wrappers.
2
3use super::{AstNode, PanacheLanguage, SyntaxKind, SyntaxNode};
4
5pub struct FencedDiv(SyntaxNode);
6
7impl AstNode for FencedDiv {
8    type Language = PanacheLanguage;
9
10    fn can_cast(kind: SyntaxKind) -> bool {
11        kind == SyntaxKind::FENCED_DIV
12    }
13
14    fn cast(syntax: SyntaxNode) -> Option<Self> {
15        if Self::can_cast(syntax.kind()) {
16            Some(Self(syntax))
17        } else {
18            None
19        }
20    }
21
22    fn syntax(&self) -> &SyntaxNode {
23        &self.0
24    }
25}
26
27impl FencedDiv {
28    pub fn opening_fence(&self) -> Option<DivFenceOpen> {
29        self.0.children().find_map(DivFenceOpen::cast)
30    }
31
32    pub fn closing_fence(&self) -> Option<DivFenceClose> {
33        self.0.children().find_map(DivFenceClose::cast)
34    }
35
36    pub fn info(&self) -> Option<DivInfo> {
37        self.opening_fence().and_then(|fence| fence.info())
38    }
39
40    pub fn info_text(&self) -> Option<String> {
41        self.info().map(|info| info.text())
42    }
43
44    pub fn body_blocks(&self) -> impl Iterator<Item = SyntaxNode> {
45        self.0.children().filter(|child| {
46            !matches!(
47                child.kind(),
48                SyntaxKind::DIV_FENCE_OPEN | SyntaxKind::DIV_FENCE_CLOSE
49            )
50        })
51    }
52
53    pub fn has_closing_fence(&self) -> bool {
54        self.closing_fence().is_some()
55    }
56}
57
58pub struct DivFenceOpen(SyntaxNode);
59
60impl AstNode for DivFenceOpen {
61    type Language = PanacheLanguage;
62
63    fn can_cast(kind: SyntaxKind) -> bool {
64        kind == SyntaxKind::DIV_FENCE_OPEN
65    }
66
67    fn cast(syntax: SyntaxNode) -> Option<Self> {
68        if Self::can_cast(syntax.kind()) {
69            Some(Self(syntax))
70        } else {
71            None
72        }
73    }
74
75    fn syntax(&self) -> &SyntaxNode {
76        &self.0
77    }
78}
79
80impl DivFenceOpen {
81    pub fn info(&self) -> Option<DivInfo> {
82        self.0.children().find_map(DivInfo::cast)
83    }
84
85    pub fn trailing_colons(&self) -> Option<String> {
86        let mut saw_info = false;
87        for child in self.0.children_with_tokens() {
88            match child {
89                rowan::NodeOrToken::Node(node) if node.kind() == SyntaxKind::DIV_INFO => {
90                    saw_info = true;
91                }
92                rowan::NodeOrToken::Token(token) if token.kind() == SyntaxKind::TEXT => {
93                    let text = token.text().trim();
94                    if saw_info && !text.is_empty() && text.chars().all(|c| c == ':') {
95                        return Some(text.to_string());
96                    }
97                }
98                _ => {}
99            }
100        }
101        None
102    }
103}
104
105pub struct DivFenceClose(SyntaxNode);
106
107impl AstNode for DivFenceClose {
108    type Language = PanacheLanguage;
109
110    fn can_cast(kind: SyntaxKind) -> bool {
111        kind == SyntaxKind::DIV_FENCE_CLOSE
112    }
113
114    fn cast(syntax: SyntaxNode) -> Option<Self> {
115        if Self::can_cast(syntax.kind()) {
116            Some(Self(syntax))
117        } else {
118            None
119        }
120    }
121
122    fn syntax(&self) -> &SyntaxNode {
123        &self.0
124    }
125}
126
127pub struct DivInfo(SyntaxNode);
128
129impl AstNode for DivInfo {
130    type Language = PanacheLanguage;
131
132    fn can_cast(kind: SyntaxKind) -> bool {
133        kind == SyntaxKind::DIV_INFO
134    }
135
136    fn cast(syntax: SyntaxNode) -> Option<Self> {
137        if Self::can_cast(syntax.kind()) {
138            Some(Self(syntax))
139        } else {
140            None
141        }
142    }
143
144    fn syntax(&self) -> &SyntaxNode {
145        &self.0
146    }
147}
148
149impl DivInfo {
150    pub fn text(&self) -> String {
151        self.0.text().to_string()
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::parse;
159
160    #[test]
161    fn fenced_div_wrapper_with_braced_attributes() {
162        let tree = parse("::: {.callout-note #tip}\nText\n:::\n", None);
163        let div = tree
164            .descendants()
165            .find_map(FencedDiv::cast)
166            .expect("fenced div");
167
168        assert_eq!(div.info_text().as_deref(), Some("{.callout-note #tip}"));
169        assert!(div.opening_fence().is_some());
170        assert!(div.closing_fence().is_some());
171    }
172
173    #[test]
174    fn fenced_div_body_blocks_excludes_fences() {
175        let tree = parse("::: note\n# Heading\n\nText\n:::\n", None);
176        let div = tree
177            .descendants()
178            .find_map(FencedDiv::cast)
179            .expect("fenced div");
180
181        let kinds: Vec<_> = div.body_blocks().map(|n| n.kind()).collect();
182        assert!(kinds.contains(&SyntaxKind::HEADING));
183        assert!(kinds.contains(&SyntaxKind::PARAGRAPH));
184        assert!(!kinds.contains(&SyntaxKind::DIV_FENCE_OPEN));
185        assert!(!kinds.contains(&SyntaxKind::DIV_FENCE_CLOSE));
186    }
187
188    #[test]
189    fn fenced_div_open_info_node_cast() {
190        let tree = parse("::: warning\nBody\n:::\n", None);
191        let open = tree
192            .descendants()
193            .find_map(DivFenceOpen::cast)
194            .expect("div fence open");
195        let info = open.info().expect("div info");
196        assert_eq!(info.text(), "warning");
197    }
198
199    #[test]
200    fn fenced_div_open_trailing_colons() {
201        let tree = parse("::: note :::\nBody\n:::\n", None);
202        let open = tree
203            .descendants()
204            .find_map(DivFenceOpen::cast)
205            .expect("div fence open");
206        assert_eq!(open.trailing_colons().as_deref(), Some(":::"));
207    }
208}