Skip to main content

panache_parser/syntax/
definitions.rs

1//! Definition list AST node wrappers.
2
3use 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}