panache_parser/syntax/
shortcodes.rs1use super::{AstNode, PanacheLanguage, SyntaxKind, SyntaxNode};
4
5pub struct Shortcode(SyntaxNode);
6
7impl AstNode for Shortcode {
8 type Language = PanacheLanguage;
9
10 fn can_cast(kind: SyntaxKind) -> bool {
11 kind == SyntaxKind::SHORTCODE
12 }
13
14 fn cast(syntax: SyntaxNode) -> Option<Self> {
15 if Self::can_cast(syntax.kind()) {
16 Some(Self(syntax))
17 } else {
18 None
19 }
20 }
21
22 fn syntax(&self) -> &SyntaxNode {
23 &self.0
24 }
25}
26
27impl Shortcode {
28 pub fn is_escaped(&self) -> bool {
30 self.0.children_with_tokens().any(|child| match child {
31 rowan::NodeOrToken::Token(token) => {
32 token.kind() == SyntaxKind::SHORTCODE_MARKER_OPEN && token.text() == "{{{<"
33 }
34 _ => false,
35 })
36 }
37
38 pub fn content(&self) -> Option<String> {
40 self.0.children().find_map(|child| {
41 if child.kind() == SyntaxKind::SHORTCODE_CONTENT {
42 Some(child.text().to_string())
43 } else {
44 None
45 }
46 })
47 }
48
49 pub fn name(&self) -> Option<String> {
51 self.args().first().cloned()
52 }
53
54 pub fn args(&self) -> Vec<String> {
56 let Some(content) = self.content() else {
57 return Vec::new();
58 };
59
60 split_shortcode_args(&content)
61 }
62}
63
64pub fn split_shortcode_args(content: &str) -> Vec<String> {
65 let mut args = Vec::new();
66 let mut current = String::new();
67 let mut in_quotes = false;
68 let mut quote_char = None;
69
70 for ch in content.trim().chars() {
71 match ch {
72 '"' | '\'' if !in_quotes => {
73 in_quotes = true;
74 quote_char = Some(ch);
75 }
76 c if Some(c) == quote_char && in_quotes => {
77 in_quotes = false;
78 quote_char = None;
79 }
80 c if c.is_whitespace() && !in_quotes => {
81 if !current.is_empty() {
82 args.push(current.clone());
83 current.clear();
84 }
85 }
86 c => current.push(c),
87 }
88 }
89
90 if !current.is_empty() {
91 args.push(current);
92 }
93
94 args
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::{Config, parser::parse};
101
102 #[test]
103 fn shortcode_wrapper_extracts_name_and_args() {
104 let tree = parse(
105 "{{< include \"chapters/part 1.qmd\" >}}",
106 Some(Config::default()),
107 );
108 let shortcode = tree
109 .descendants()
110 .find_map(Shortcode::cast)
111 .expect("shortcode");
112
113 assert_eq!(shortcode.name().as_deref(), Some("include"));
114 assert_eq!(
115 shortcode.args(),
116 vec!["include".to_string(), "chapters/part 1.qmd".to_string()]
117 );
118 }
119
120 #[test]
121 fn shortcode_wrapper_detects_escaped_shortcode() {
122 let tree = parse("{{{< include child.qmd >}}}", Some(Config::default()));
123 let shortcode = tree
124 .descendants()
125 .find_map(Shortcode::cast)
126 .expect("shortcode");
127
128 assert!(shortcode.is_escaped());
129 }
130}