panache_parser/syntax/
fenced_divs.rs1use 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}