panache_parser/syntax/
headings.rs1use super::ast::support;
4use super::{AstNode, PanacheLanguage, SyntaxKind, SyntaxNode};
5
6pub struct Heading(SyntaxNode);
7
8impl AstNode for Heading {
9 type Language = PanacheLanguage;
10
11 fn can_cast(kind: SyntaxKind) -> bool {
12 kind == SyntaxKind::HEADING
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 Heading {
29 pub fn level(&self) -> usize {
31 for child in self.0.children() {
33 if child.kind() == SyntaxKind::ATX_HEADING_MARKER {
34 for token in child.children_with_tokens() {
36 if let Some(t) = token.as_token()
37 && t.kind() == SyntaxKind::ATX_HEADING_MARKER
38 {
39 return t.text().chars().filter(|&c| c == '#').count();
40 }
41 }
42 }
43 }
44
45 if let Some(text) = self
49 .0
50 .descendants_with_tokens()
51 .filter_map(|el| el.into_token())
52 .find(|t| t.kind() == SyntaxKind::SETEXT_HEADING_UNDERLINE)
53 .map(|t| t.text().to_string())
54 {
55 if text.starts_with('=') { 1 } else { 2 }
57 } else {
58 1 }
60 }
61
62 pub fn content(&self) -> Option<HeadingContent> {
64 support::child(&self.0)
65 }
66
67 pub fn text(&self) -> String {
69 self.content().map(|c| c.text()).unwrap_or_default()
70 }
71
72 pub fn text_range(&self) -> rowan::TextRange {
74 self.0.text_range()
75 }
76
77 pub fn title_or(&self, placeholder: &str) -> String {
79 let text = self.text();
80 if text.is_empty() {
81 placeholder.to_string()
82 } else {
83 text
84 }
85 }
86
87 pub fn atx_marker_range(&self) -> Option<rowan::TextRange> {
89 self.0
90 .children()
91 .find(|child| child.kind() == SyntaxKind::ATX_HEADING_MARKER)
92 .and_then(|marker_node| {
93 marker_node
94 .children_with_tokens()
95 .find_map(|el| el.as_token().map(|token| token.text_range()))
96 })
97 }
98}
99
100pub struct HeadingContent(SyntaxNode);
101
102impl AstNode for HeadingContent {
103 type Language = PanacheLanguage;
104
105 fn can_cast(kind: SyntaxKind) -> bool {
106 kind == SyntaxKind::HEADING_CONTENT
107 }
108
109 fn cast(syntax: SyntaxNode) -> Option<Self> {
110 if Self::can_cast(syntax.kind()) {
111 Some(Self(syntax))
112 } else {
113 None
114 }
115 }
116
117 fn syntax(&self) -> &SyntaxNode {
118 &self.0
119 }
120}
121
122impl HeadingContent {
123 pub fn text(&self) -> String {
125 self.0
126 .descendants_with_tokens()
127 .filter_map(|it| it.into_token())
128 .filter(|token| {
129 matches!(
130 token.kind(),
131 SyntaxKind::TEXT
132 | SyntaxKind::INLINE_CODE_CONTENT
133 | SyntaxKind::INLINE_EXEC_CONTENT
134 )
135 })
136 .map(|token| token.text().to_string())
137 .collect()
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn heading_title_or_returns_placeholder_for_empty_heading() {
147 let tree = crate::parse("# \n", None);
148 let heading = tree.descendants().find_map(Heading::cast).expect("heading");
149 assert_eq!(heading.title_or("(empty)"), "(empty)");
150 }
151
152 #[test]
153 fn setext_heading_levels_are_one_and_two() {
154 let input = "Level 1\n=======\n\nLevel 2\n-------\n";
155 let tree = crate::parse(input, None);
156 let levels: Vec<usize> = tree
157 .descendants()
158 .filter_map(Heading::cast)
159 .map(|h| h.level())
160 .collect();
161 assert_eq!(levels, vec![1, 2]);
162 }
163
164 #[test]
165 fn heading_atx_marker_range_points_to_hashes() {
166 let input = "### Title\n";
167 let tree = crate::parse(input, None);
168 let heading = tree.descendants().find_map(Heading::cast).expect("heading");
169 let range = heading.atx_marker_range().expect("marker range");
170 let start: usize = range.start().into();
171 let end: usize = range.end().into();
172 assert_eq!(&input[start..end], "###");
173 }
174}