panache_parser/syntax/
attributes.rs1use crate::parser::utils::attributes::{parse_html_attribute_list, try_parse_trailing_attributes};
2use crate::syntax::{AstNode, PanacheLanguage, SyntaxKind, SyntaxNode};
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5pub struct AttributeNode(SyntaxNode);
6
7impl AstNode for AttributeNode {
8 type Language = PanacheLanguage;
9
10 fn can_cast(kind: SyntaxKind) -> bool {
11 matches!(
12 kind,
13 SyntaxKind::ATTRIBUTE | SyntaxKind::DIV_INFO | SyntaxKind::HTML_ATTRS
14 )
15 }
16
17 fn cast(node: SyntaxNode) -> Option<Self> {
18 Self::can_cast(node.kind()).then(|| AttributeNode(node))
19 }
20
21 fn syntax(&self) -> &SyntaxNode {
22 &self.0
23 }
24}
25
26impl AttributeNode {
27 pub fn id(&self) -> Option<String> {
28 let text = self.0.text().to_string();
29 match self.0.kind() {
30 SyntaxKind::HTML_ATTRS => parse_html_attribute_list(&text)
31 .and_then(|attrs| attrs.identifier)
32 .filter(|id| !id.is_empty()),
33 _ => try_parse_trailing_attributes(&text)
34 .and_then(|(attrs, _)| attrs.identifier)
35 .filter(|id| !id.is_empty()),
36 }
37 }
38
39 pub fn id_value_range(&self) -> Option<rowan::TextRange> {
40 let id = self.id()?;
41 let text = self.0.text().to_string();
42 let node_start: usize = self.0.text_range().start().into();
43 match self.0.kind() {
44 SyntaxKind::HTML_ATTRS => {
45 let marker = text.find("id")?;
50 let after_id = &text[marker + 2..];
51 let eq_off = after_id.bytes().position(|b| b == b'=')?;
52 let after_eq = &after_id[eq_off + 1..];
53 let (val_offset_in_after_eq, val_len) = match after_eq.bytes().next() {
54 Some(b'"') | Some(b'\'') => (1, id.len()),
55 _ => (0, id.len()),
56 };
57 let value_start_in_text = marker + 2 + eq_off + 1 + val_offset_in_after_eq;
58 let start = rowan::TextSize::from((node_start + value_start_in_text) as u32);
59 let end =
60 rowan::TextSize::from((node_start + value_start_in_text + val_len) as u32);
61 Some(rowan::TextRange::new(start, end))
62 }
63 _ => {
64 let marker = text.find(&format!("#{}", id))?;
65 let start = rowan::TextSize::from((node_start + marker + 1) as u32);
66 let end = rowan::TextSize::from((node_start + marker + 1 + id.len()) as u32);
67 Some(rowan::TextRange::new(start, end))
68 }
69 }
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn attribute_node_extracts_div_info_id_and_range() {
79 let config = crate::ParserOptions {
80 flavor: crate::options::Flavor::RMarkdown,
81 ..Default::default()
82 };
83 let tree = crate::parse("::: {#mu .exercise}\ntext\n:::\n", Some(config));
84 let node = tree
85 .descendants()
86 .find_map(AttributeNode::cast)
87 .expect("attribute node");
88 assert_eq!(node.id().as_deref(), Some("mu"));
89
90 let range = node.id_value_range().expect("id range");
91 let start: usize = range.start().into();
92 let end: usize = range.end().into();
93 assert_eq!(&tree.text().to_string()[start..end], "mu");
94 }
95}