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(underline) = support::token(&self.0, SyntaxKind::SETEXT_HEADING_UNDERLINE) {
47 if underline.text().starts_with('=') {
49 1
50 } else {
51 2
52 }
53 } else {
54 1 }
56 }
57
58 pub fn content(&self) -> Option<HeadingContent> {
60 support::child(&self.0)
61 }
62
63 pub fn text(&self) -> String {
65 self.content().map(|c| c.text()).unwrap_or_default()
66 }
67
68 pub fn text_range(&self) -> rowan::TextRange {
70 self.0.text_range()
71 }
72
73 pub fn title_or(&self, placeholder: &str) -> String {
75 let text = self.text();
76 if text.is_empty() {
77 placeholder.to_string()
78 } else {
79 text
80 }
81 }
82
83 pub fn atx_marker_range(&self) -> Option<rowan::TextRange> {
85 self.0
86 .children()
87 .find(|child| child.kind() == SyntaxKind::ATX_HEADING_MARKER)
88 .and_then(|marker_node| {
89 marker_node
90 .children_with_tokens()
91 .find_map(|el| el.as_token().map(|token| token.text_range()))
92 })
93 }
94}
95
96pub struct HeadingContent(SyntaxNode);
97
98impl AstNode for HeadingContent {
99 type Language = PanacheLanguage;
100
101 fn can_cast(kind: SyntaxKind) -> bool {
102 kind == SyntaxKind::HEADING_CONTENT
103 }
104
105 fn cast(syntax: SyntaxNode) -> Option<Self> {
106 if Self::can_cast(syntax.kind()) {
107 Some(Self(syntax))
108 } else {
109 None
110 }
111 }
112
113 fn syntax(&self) -> &SyntaxNode {
114 &self.0
115 }
116}
117
118impl HeadingContent {
119 pub fn text(&self) -> String {
121 self.0
122 .descendants_with_tokens()
123 .filter_map(|it| it.into_token())
124 .filter(|token| {
125 matches!(
126 token.kind(),
127 SyntaxKind::TEXT
128 | SyntaxKind::INLINE_CODE_CONTENT
129 | SyntaxKind::INLINE_EXEC_CONTENT
130 )
131 })
132 .map(|token| token.text().to_string())
133 .collect()
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn heading_title_or_returns_placeholder_for_empty_heading() {
143 let tree = crate::parse("# \n", None);
144 let heading = tree.descendants().find_map(Heading::cast).expect("heading");
145 assert_eq!(heading.title_or("(empty)"), "(empty)");
146 }
147
148 #[test]
149 fn heading_atx_marker_range_points_to_hashes() {
150 let input = "### Title\n";
151 let tree = crate::parse(input, None);
152 let heading = tree.descendants().find_map(Heading::cast).expect("heading");
153 let range = heading.atx_marker_range().expect("marker range");
154 let start: usize = range.start().into();
155 let end: usize = range.end().into();
156 assert_eq!(&input[start..end], "###");
157 }
158}