panache_parser/syntax/
definitions.rs1use super::ast::{AstChildren, support};
4use super::{AstNode, PanacheLanguage, SyntaxKind, SyntaxNode};
5
6pub struct DefinitionList(SyntaxNode);
7
8impl AstNode for DefinitionList {
9 type Language = PanacheLanguage;
10
11 fn can_cast(kind: SyntaxKind) -> bool {
12 kind == SyntaxKind::DEFINITION_LIST
13 }
14
15 fn cast(syntax: SyntaxNode) -> Option<Self> {
16 if Self::can_cast(syntax.kind()) {
17 Some(Self(syntax))
18 } else {
19 None
20 }
21 }
22
23 fn syntax(&self) -> &SyntaxNode {
24 &self.0
25 }
26}
27
28impl DefinitionList {
29 pub fn items(&self) -> AstChildren<DefinitionItem> {
30 support::children(&self.0)
31 }
32}
33
34pub struct DefinitionItem(SyntaxNode);
35
36impl AstNode for DefinitionItem {
37 type Language = PanacheLanguage;
38
39 fn can_cast(kind: SyntaxKind) -> bool {
40 kind == SyntaxKind::DEFINITION_ITEM
41 }
42
43 fn cast(syntax: SyntaxNode) -> Option<Self> {
44 if Self::can_cast(syntax.kind()) {
45 Some(Self(syntax))
46 } else {
47 None
48 }
49 }
50
51 fn syntax(&self) -> &SyntaxNode {
52 &self.0
53 }
54}
55
56impl DefinitionItem {
57 pub fn definitions(&self) -> AstChildren<Definition> {
58 support::children(&self.0)
59 }
60
61 pub fn is_compact(&self) -> bool {
62 let definitions: Vec<_> = self.definitions().collect();
63 if definitions.is_empty() {
64 return true;
65 }
66
67 definitions.into_iter().all(|definition| {
68 let blocks: Vec<_> = definition
69 .syntax()
70 .children()
71 .filter(|child| child.kind() != SyntaxKind::BLANK_LINE)
72 .collect();
73
74 if blocks.len() != 1 {
75 return false;
76 }
77
78 match blocks[0].kind() {
79 SyntaxKind::PLAIN | SyntaxKind::PARAGRAPH => {
80 !has_leading_atx_heading_with_remainder(&blocks[0].text().to_string())
81 }
82 SyntaxKind::CODE_BLOCK => false,
83 _ => false,
84 }
85 })
86 }
87
88 pub fn is_loose(&self) -> bool {
89 !self.is_compact()
90 }
91}
92
93fn has_leading_atx_heading_with_remainder(text: &str) -> bool {
94 let mut lines = text.lines();
95 let Some(first_line) = lines.next() else {
96 return false;
97 };
98
99 if !looks_like_atx_heading(first_line) {
100 return false;
101 }
102
103 lines.flat_map(str::split_whitespace).next().is_some()
104}
105
106fn looks_like_atx_heading(line: &str) -> bool {
107 let trimmed = line.trim_start_matches([' ', '\t']);
108 let level = trimmed.chars().take_while(|ch| *ch == '#').count();
109 if !(1..=6).contains(&level) {
110 return false;
111 }
112
113 match trimmed.chars().nth(level) {
114 Some(ch) => ch == ' ' || ch == '\t',
115 None => true,
116 }
117}
118
119pub struct Definition(SyntaxNode);
120
121impl AstNode for Definition {
122 type Language = PanacheLanguage;
123
124 fn can_cast(kind: SyntaxKind) -> bool {
125 kind == SyntaxKind::DEFINITION
126 }
127
128 fn cast(syntax: SyntaxNode) -> Option<Self> {
129 if Self::can_cast(syntax.kind()) {
130 Some(Self(syntax))
131 } else {
132 None
133 }
134 }
135
136 fn syntax(&self) -> &SyntaxNode {
137 &self.0
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use crate::parse;
145
146 #[test]
147 fn definition_item_compact_single_plain_block() {
148 let tree = parse("Term\n: Def\n", None);
149 let item = tree
150 .descendants()
151 .find_map(DefinitionItem::cast)
152 .expect("definition item");
153 assert!(item.is_compact());
154 assert!(!item.is_loose());
155 }
156
157 #[test]
158 fn definition_item_loose_single_code_block() {
159 let tree = parse("Term\n: ```r\n a <- 1\n ```\n", None);
160 let item = tree
161 .descendants()
162 .find_map(DefinitionItem::cast)
163 .expect("definition item");
164 assert!(item.is_loose());
165 }
166
167 #[test]
168 fn definition_item_loose_when_definition_is_multiblock() {
169 let tree = parse("Term\n: # Heading\n\n Text\n", None);
170 let item = tree
171 .descendants()
172 .find_map(DefinitionItem::cast)
173 .expect("definition item");
174 assert!(item.is_loose());
175 }
176
177 #[test]
178 fn definition_item_loose_for_plain_heading_with_remainder() {
179 let tree = parse("Term\n: # Heading\n Some text\n", None);
180 let item = tree
181 .descendants()
182 .find_map(DefinitionItem::cast)
183 .expect("definition item");
184 assert!(item.is_loose());
185 }
186
187 #[test]
188 fn definition_item_compact_for_multiple_simple_definitions() {
189 let tree = parse("Term\n: Def one\n\n: Def two\n", None);
190 let item = tree
191 .descendants()
192 .find_map(DefinitionItem::cast)
193 .expect("definition item");
194 assert!(item.is_compact());
195 }
196}