panache_parser/parser/inlines/
shortcodes.rs1use crate::syntax::SyntaxKind;
8use rowan::GreenNodeBuilder;
9
10pub(crate) fn try_parse_shortcode(text: &str) -> Option<(usize, String, bool)> {
18 let bytes = text.as_bytes();
19
20 if bytes.len() < 4 {
22 return None;
23 }
24
25 let (is_escaped, marker_len) = if bytes.len() >= 4
27 && bytes[0] == b'{'
28 && bytes[1] == b'{'
29 && bytes[2] == b'{'
30 && bytes[3] == b'<'
31 {
32 (true, 4)
33 } else if bytes[0] == b'{' && bytes[1] == b'{' && bytes[2] == b'<' {
34 (false, 3)
35 } else {
36 return None;
37 };
38
39 let close_marker = if is_escaped { ">}}}" } else { ">}}" };
41 let close_marker_bytes = close_marker.as_bytes();
42 let close_marker_len = close_marker_bytes.len();
43
44 let mut pos = marker_len;
46 let mut brace_depth: i32 = 0; while pos < text.len() {
49 if pos + close_marker_len <= bytes.len()
50 && &bytes[pos..pos + close_marker_len] == close_marker_bytes
51 && brace_depth == 0
52 {
53 let content = &text[marker_len..pos];
55 let total_len = pos + close_marker_len;
56 return Some((total_len, content.to_string(), is_escaped));
57 }
58
59 match bytes[pos] {
61 b'{' => brace_depth += 1,
62 b'}' => brace_depth = brace_depth.saturating_sub(1),
63 _ => {}
64 }
65
66 pos += 1;
67 }
68
69 None
71}
72
73pub(crate) fn emit_shortcode(builder: &mut GreenNodeBuilder, content: &str, is_escaped: bool) {
75 builder.start_node(SyntaxKind::SHORTCODE.into());
76
77 let open_marker = if is_escaped { "{{{<" } else { "{{<" };
79 builder.token(SyntaxKind::SHORTCODE_MARKER_OPEN.into(), open_marker);
80
81 builder.start_node(SyntaxKind::SHORTCODE_CONTENT.into());
83
84 if !content.is_empty() {
86 builder.token(SyntaxKind::TEXT.into(), content);
87 }
88
89 builder.finish_node(); let close_marker = if is_escaped { ">}}}" } else { ">}}" };
93 builder.token(SyntaxKind::SHORTCODE_MARKER_CLOSE.into(), close_marker);
94
95 builder.finish_node(); }
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn parses_simple_shortcode() {
104 let result = try_parse_shortcode("{{< meta title >}}");
105 assert!(result.is_some());
106 let (len, content, is_escaped) = result.unwrap();
107 assert_eq!(len, 18);
108 assert_eq!(content, " meta title ");
109 assert!(!is_escaped);
110 }
111
112 #[test]
113 fn parses_shortcode_without_spaces() {
114 let result = try_parse_shortcode("{{<meta title>}}");
115 assert!(result.is_some());
116 let (len, content, is_escaped) = result.unwrap();
117 assert_eq!(len, 16);
118 assert_eq!(content, "meta title");
119 assert!(!is_escaped);
120 }
121
122 #[test]
123 fn parses_shortcode_with_extra_spaces() {
124 let result = try_parse_shortcode("{{< meta title >}}");
125 assert!(result.is_some());
126 let (len, content, _) = result.unwrap();
127 assert_eq!(len, 21);
128 assert_eq!(content, " meta title ");
129 }
130
131 #[test]
132 fn parses_shortcode_with_arguments() {
133 let result = try_parse_shortcode("{{< video src=\"url\" >}}");
134 assert!(result.is_some());
135 let (len, content, _) = result.unwrap();
136 assert_eq!(len, 23);
137 assert_eq!(content, " video src=\"url\" ");
138 }
139
140 #[test]
141 fn parses_shortcode_with_unicode_quotes() {
142 let result = try_parse_shortcode("{{< video “https://www.youtube.com/watch?v=test” >}}");
143 assert!(result.is_some());
144 let (len, content, is_escaped) = result.unwrap();
145 assert_eq!(
146 len,
147 "{{< video “https://www.youtube.com/watch?v=test” >}}".len()
148 );
149 assert_eq!(content, " video “https://www.youtube.com/watch?v=test” ");
150 assert!(!is_escaped);
151 }
152
153 #[test]
154 fn parses_shortcode_with_multiple_arguments() {
155 let result = try_parse_shortcode("{{< env VAR \"default\" >}}");
156 assert!(result.is_some());
157 let (len, content, _) = result.unwrap();
158 assert_eq!(len, 25);
159 assert_eq!(content, " env VAR \"default\" ");
160 }
161
162 #[test]
163 fn parses_escaped_shortcode() {
164 let result = try_parse_shortcode("{{{< var version >}}}");
165 assert!(result.is_some());
166 let (len, content, is_escaped) = result.unwrap();
167 assert_eq!(len, 21);
168 assert_eq!(content, " var version ");
169 assert!(is_escaped);
170 }
171
172 #[test]
173 fn parses_shortcode_with_nested_braces() {
174 let result = try_parse_shortcode("{{< meta key={nested} >}}");
175 assert!(result.is_some());
176 let (len, content, _) = result.unwrap();
177 assert_eq!(len, 25);
178 assert_eq!(content, " meta key={nested} ");
179 }
180
181 #[test]
182 fn parses_shortcode_with_dot_notation() {
183 let result = try_parse_shortcode("{{< meta author.1 >}}");
184 assert!(result.is_some());
185 let (len, content, _) = result.unwrap();
186 assert_eq!(len, 21);
187 assert_eq!(content, " meta author.1 ");
188 }
189
190 #[test]
191 fn parses_shortcode_with_escaped_dots() {
192 let result = try_parse_shortcode(r"{{< meta field\\.with\\.dots >}}");
193 assert!(result.is_some());
194 let (len, content, _) = result.unwrap();
195 assert_eq!(len, 32);
196 assert_eq!(content, r" meta field\\.with\\.dots ");
197 }
198
199 #[test]
200 fn parses_empty_shortcode() {
201 let result = try_parse_shortcode("{{< >}}");
202 assert!(result.is_some());
203 let (len, content, _) = result.unwrap();
204 assert_eq!(len, 7);
205 assert_eq!(content, " ");
206 }
207
208 #[test]
209 fn fails_on_unclosed_shortcode() {
210 let result = try_parse_shortcode("{{< meta title");
211 assert!(result.is_none());
212 }
213
214 #[test]
215 fn fails_on_mismatched_braces() {
216 let result = try_parse_shortcode("{{< meta >}");
217 assert!(result.is_none());
218 }
219
220 #[test]
221 fn fails_on_mismatched_escape_braces() {
222 let result = try_parse_shortcode("{{{< meta >}}");
223 assert!(result.is_none());
224 }
225
226 #[test]
227 fn does_not_parse_regular_braces() {
228 let result = try_parse_shortcode("{{not a shortcode}}");
229 assert!(result.is_none());
230 }
231
232 #[test]
233 fn handles_shortcode_with_key_value_pairs() {
234 let result = try_parse_shortcode("{{< video src=\"url\" width=\"100%\" >}}");
235 assert!(result.is_some());
236 let (len, content, _) = result.unwrap();
237 assert_eq!(len, 36);
238 assert_eq!(content, " video src=\"url\" width=\"100%\" ");
239 }
240}