1use crate::token::{ChoiceOption, SnippetBody, SnippetToken, TransformTarget};
40
41#[derive(Debug, thiserror::Error)]
45pub enum ParseError {
46 #[error("internal parser error: {0}")]
47 Internal(&'static str),
48}
49
50pub fn parse(body: &str) -> Result<SnippetBody, ParseError> {
52 let mut p = Parser::new(body);
53 let tokens = p.parse_body(None)?;
54 Ok(SnippetBody::new(merge_literals(tokens)))
55}
56
57fn merge_literals(tokens: Vec<SnippetToken>) -> Vec<SnippetToken> {
61 let mut out: Vec<SnippetToken> = Vec::with_capacity(tokens.len());
62 for tok in tokens {
63 if let SnippetToken::Literal(new) = &tok
64 && let Some(SnippetToken::Literal(prev)) = out.last_mut()
65 {
66 prev.push_str(new);
67 continue;
68 }
69 out.push(tok);
70 }
71 out
72}
73
74struct Parser<'a> {
75 src: &'a [u8],
76 pos: usize,
77}
78
79impl<'a> Parser<'a> {
80 fn new(s: &'a str) -> Self {
81 Self {
82 src: s.as_bytes(),
83 pos: 0,
84 }
85 }
86
87 fn peek(&self) -> Option<u8> {
88 self.src.get(self.pos).copied()
89 }
90
91 fn bump(&mut self) -> Option<u8> {
92 let b = self.peek();
93 if b.is_some() {
94 self.pos += 1;
95 }
96 b
97 }
98
99 #[allow(dead_code)]
100 fn starts_with(&self, lit: &[u8]) -> bool {
101 self.src
102 .get(self.pos..self.pos + lit.len())
103 .map(|s| s == lit)
104 .unwrap_or(false)
105 }
106
107 fn parse_body(&mut self, stop: Option<&[u8]>) -> Result<Vec<SnippetToken>, ParseError> {
112 let mut tokens = Vec::new();
113 while let Some(b) = self.peek() {
114 if let Some(stop) = stop
115 && stop.contains(&b)
116 {
117 break;
118 }
119 match b {
120 b'\\' => {
121 self.bump();
122 if let Some(next) = self.peek() {
123 match next {
124 b'$' | b'\\' | b'}' => {
125 self.bump();
126 tokens.push(SnippetToken::Literal(char::from(next).to_string()));
127 }
128 _ => {
131 tokens.push(SnippetToken::Literal("\\".to_string()));
132 }
133 }
134 } else {
135 tokens.push(SnippetToken::Literal("\\".to_string()));
136 }
137 }
138 b'$' => {
139 let saved = self.pos;
140 self.bump();
141 match self.try_parse_dollar() {
142 Some(tok) => tokens.push(tok),
143 None => {
144 self.pos = saved + 1;
146 tokens.push(SnippetToken::Literal("$".to_string()));
147 }
148 }
149 }
150 _ => {
151 let ch = self.peek_char_at(self.pos);
157 self.pos += ch.len_utf8();
158 tokens.push(SnippetToken::Literal(ch.to_string()));
159 }
160 }
161 }
162 Ok(tokens)
163 }
164
165 fn peek_char_at(&self, pos: usize) -> char {
170 let s = std::str::from_utf8(&self.src[pos..])
171 .ok()
172 .and_then(|s| s.chars().next());
173 s.unwrap_or('?')
174 }
175
176 fn try_parse_dollar(&mut self) -> Option<SnippetToken> {
180 match self.peek()? {
181 b'0'..=b'9' => {
183 let n = self.parse_uint()?;
184 Some(SnippetToken::Tabstop(n))
185 }
186 b'A'..=b'Z' | b'a'..=b'z' | b'_' => {
188 let name = self.parse_name()?;
189 Some(SnippetToken::Variable {
190 name,
191 default: None,
192 })
193 }
194 b'{' => {
196 self.bump();
197 self.parse_block()
198 }
199 _ => None,
200 }
201 }
202
203 fn parse_uint(&mut self) -> Option<u32> {
204 let start = self.pos;
205 while let Some(b) = self.peek() {
206 if b.is_ascii_digit() {
207 self.bump();
208 } else {
209 break;
210 }
211 }
212 if start == self.pos {
213 return None;
214 }
215 let s = std::str::from_utf8(&self.src[start..self.pos]).ok()?;
216 s.parse().ok()
217 }
218
219 fn parse_name(&mut self) -> Option<String> {
220 let start = self.pos;
221 match self.peek()? {
223 b'A'..=b'Z' | b'a'..=b'z' | b'_' => {
224 self.bump();
225 }
226 _ => return None,
227 }
228 while let Some(b) = self.peek() {
230 if matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_') {
231 self.bump();
232 } else {
233 break;
234 }
235 }
236 std::str::from_utf8(&self.src[start..self.pos])
237 .ok()
238 .map(|s| s.to_string())
239 }
240
241 fn parse_block(&mut self) -> Option<SnippetToken> {
245 match self.peek()? {
246 b'0'..=b'9' => {
247 let idx = self.parse_uint()?;
248 match self.peek()? {
249 b'}' => {
250 self.bump();
251 Some(SnippetToken::Tabstop(idx))
252 }
253 b':' => {
254 self.bump();
255 let inner = self.parse_body(Some(b"}")).ok()?;
257 if self.peek()? != b'}' {
258 return None;
259 }
260 self.bump();
261 Some(SnippetToken::Placeholder {
262 idx,
263 default: SnippetBody::new(merge_literals(inner)),
264 })
265 }
266 b'|' => {
267 self.bump();
268 let options = self.parse_choices()?;
269 if self.peek()? != b'}' {
270 return None;
271 }
272 self.bump();
273 Some(SnippetToken::Choice { idx, options })
274 }
275 b'/' => {
276 self.bump();
277 let (pattern, replacement, flags) = self.parse_transform()?;
278 Some(SnippetToken::Transform {
279 target: TransformTarget::Tabstop(idx),
280 pattern,
281 replacement,
282 flags,
283 })
284 }
285 _ => None,
286 }
287 }
288 b'A'..=b'Z' | b'a'..=b'z' | b'_' => {
289 let name = self.parse_name()?;
290 match self.peek()? {
291 b'}' => {
292 self.bump();
293 Some(SnippetToken::Variable {
294 name,
295 default: None,
296 })
297 }
298 b':' => {
299 self.bump();
300 let inner = self.parse_body(Some(b"}")).ok()?;
301 if self.peek()? != b'}' {
302 return None;
303 }
304 self.bump();
305 Some(SnippetToken::Variable {
306 name,
307 default: Some(SnippetBody::new(merge_literals(inner))),
308 })
309 }
310 b'/' => {
311 self.bump();
312 let (pattern, replacement, flags) = self.parse_transform()?;
313 Some(SnippetToken::Transform {
314 target: TransformTarget::Variable(name),
315 pattern,
316 replacement,
317 flags,
318 })
319 }
320 _ => None,
321 }
322 }
323 _ => None,
324 }
325 }
326
327 fn parse_choices(&mut self) -> Option<Vec<ChoiceOption>> {
328 let mut options = Vec::new();
329 let mut current = String::new();
330 loop {
331 match self.peek()? {
332 b'|' => {
333 self.bump();
334 options.push(ChoiceOption {
335 text: std::mem::take(&mut current),
336 });
337 return Some(options);
338 }
339 b',' => {
340 self.bump();
341 options.push(ChoiceOption {
342 text: std::mem::take(&mut current),
343 });
344 }
345 b'\\' => {
346 self.bump();
347 if let Some(esc) = self.peek() {
348 match esc {
349 b',' | b'|' | b'\\' => {
350 self.bump();
351 current.push(char::from(esc));
352 }
353 _ => current.push('\\'),
354 }
355 } else {
356 current.push('\\');
357 }
358 }
359 b => {
360 self.bump();
361 current.push(char::from(b));
362 }
363 }
364 }
365 }
366
367 fn parse_transform(&mut self) -> Option<(String, String, String)> {
368 let pattern = self.parse_until_slash()?;
370 if self.peek()? != b'/' {
371 return None;
372 }
373 self.bump();
374 let replacement = self.parse_until_slash()?;
375 if self.peek()? != b'/' {
376 return None;
377 }
378 self.bump();
379 let mut flags = String::new();
381 while let Some(b) = self.peek() {
382 if b == b'}' {
383 self.bump();
384 return Some((pattern, replacement, flags));
385 }
386 self.bump();
387 flags.push(char::from(b));
388 }
389 None
390 }
391
392 fn parse_until_slash(&mut self) -> Option<String> {
393 let mut out = String::new();
394 while let Some(b) = self.peek() {
395 match b {
396 b'/' => return Some(out),
397 b'\\' => {
398 self.bump();
399 if let Some(esc) = self.peek() {
400 self.bump();
401 match esc {
402 b'/' | b'\\' => out.push(char::from(esc)),
403 other => {
404 out.push('\\');
408 out.push(char::from(other));
409 }
410 }
411 } else {
412 out.push('\\');
413 }
414 }
415 b'}' => return Some(out),
416 _ => {
417 self.bump();
418 out.push(char::from(b));
419 }
420 }
421 }
422 Some(out)
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 fn parse_ok(s: &str) -> SnippetBody {
431 parse(s).expect("parse succeeds (parser is total)")
432 }
433
434 #[test]
435 fn pure_literal_parses_to_one_literal_token() {
436 let b = parse_ok("hello world");
437 assert_eq!(b.tokens, vec![SnippetToken::Literal("hello world".into())]);
438 }
439
440 #[test]
441 fn bare_tabstop_parses() {
442 let b = parse_ok("$1");
443 assert_eq!(b.tokens, vec![SnippetToken::Tabstop(1)]);
444 }
445
446 #[test]
447 fn final_tabstop_parses() {
448 let b = parse_ok("$0");
449 assert_eq!(b.tokens, vec![SnippetToken::Tabstop(0)]);
450 }
451
452 #[test]
453 fn block_tabstop_parses() {
454 let b = parse_ok("${42}");
455 assert_eq!(b.tokens, vec![SnippetToken::Tabstop(42)]);
456 }
457
458 #[test]
459 fn placeholder_with_default_parses() {
460 let b = parse_ok("${1:foo}");
461 match &b.tokens[0] {
462 SnippetToken::Placeholder { idx, default } => {
463 assert_eq!(*idx, 1);
464 assert_eq!(default.tokens, vec![SnippetToken::Literal("foo".into())]);
465 }
466 other => panic!("expected Placeholder, got {other:?}"),
467 }
468 }
469
470 #[test]
471 fn nested_placeholder_inside_default_parses() {
472 let b = parse_ok("${1:outer ${2:inner} more}");
473 match &b.tokens[0] {
474 SnippetToken::Placeholder { idx, default } => {
475 assert_eq!(*idx, 1);
476 assert_eq!(
477 default.tokens,
478 vec![
479 SnippetToken::Literal("outer ".into()),
480 SnippetToken::Placeholder {
481 idx: 2,
482 default: SnippetBody::new(vec![SnippetToken::Literal("inner".into())]),
483 },
484 SnippetToken::Literal(" more".into()),
485 ]
486 );
487 }
488 other => panic!("expected Placeholder, got {other:?}"),
489 }
490 }
491
492 #[test]
493 fn choice_placeholder_parses() {
494 let b = parse_ok("${1|alpha,beta,gamma|}");
495 match &b.tokens[0] {
496 SnippetToken::Choice { idx, options } => {
497 assert_eq!(*idx, 1);
498 let texts: Vec<&str> = options.iter().map(|c| c.text.as_str()).collect();
499 assert_eq!(texts, vec!["alpha", "beta", "gamma"]);
500 }
501 other => panic!("expected Choice, got {other:?}"),
502 }
503 }
504
505 #[test]
506 fn bare_variable_parses() {
507 let b = parse_ok("$TM_FILENAME");
508 match &b.tokens[0] {
509 SnippetToken::Variable { name, default } => {
510 assert_eq!(name, "TM_FILENAME");
511 assert!(default.is_none());
512 }
513 other => panic!("expected Variable, got {other:?}"),
514 }
515 }
516
517 #[test]
518 fn block_variable_parses() {
519 let b = parse_ok("${TM_FILENAME}");
520 match &b.tokens[0] {
521 SnippetToken::Variable { name, default } => {
522 assert_eq!(name, "TM_FILENAME");
523 assert!(default.is_none());
524 }
525 other => panic!("expected Variable, got {other:?}"),
526 }
527 }
528
529 #[test]
530 fn variable_with_fallback_parses() {
531 let b = parse_ok("${TM_FILENAME:default.txt}");
532 match &b.tokens[0] {
533 SnippetToken::Variable { name, default } => {
534 assert_eq!(name, "TM_FILENAME");
535 let d = default.as_ref().expect("has default");
536 assert_eq!(d.tokens, vec![SnippetToken::Literal("default.txt".into())]);
537 }
538 other => panic!("expected Variable, got {other:?}"),
539 }
540 }
541
542 #[test]
543 fn escapes_dollar_brace_backslash() {
544 let b = parse_ok("\\$1 \\\\ \\}");
545 assert_eq!(b.tokens, vec![SnippetToken::Literal("$1 \\ }".into())]);
547 }
548
549 #[test]
550 fn malformed_block_falls_back_to_literal_dollar() {
551 let b = parse_ok("$ no-block");
555 assert_eq!(b.tokens, vec![SnippetToken::Literal("$ no-block".into())]);
556 }
557
558 #[test]
559 fn transform_token_parses_pattern_replacement_flags() {
560 let b = parse_ok("${1/foo/bar/g}");
561 match &b.tokens[0] {
562 SnippetToken::Transform {
563 target,
564 pattern,
565 replacement,
566 flags,
567 } => {
568 assert!(matches!(target, TransformTarget::Tabstop(1)));
569 assert_eq!(pattern, "foo");
570 assert_eq!(replacement, "bar");
571 assert_eq!(flags, "g");
572 }
573 other => panic!("expected Transform, got {other:?}"),
574 }
575 }
576
577 #[test]
578 fn complex_body_round_trips_to_tokens() {
579 let b = parse_ok("for ${1:i} in ${2:iter} {\n\t$0\n}");
581 assert_eq!(b.tokens.len(), 7);
585 assert!(matches!(b.tokens[0], SnippetToken::Literal(ref s) if s == "for "));
586 assert!(matches!(
587 b.tokens[1],
588 SnippetToken::Placeholder { idx: 1, .. }
589 ));
590 assert!(matches!(b.tokens[2], SnippetToken::Literal(ref s) if s == " in "));
591 assert!(matches!(
592 b.tokens[3],
593 SnippetToken::Placeholder { idx: 2, .. }
594 ));
595 assert!(matches!(b.tokens[4], SnippetToken::Literal(ref s) if s.starts_with(" {")));
596 assert!(matches!(b.tokens[5], SnippetToken::Tabstop(0)));
597 assert!(matches!(b.tokens[6], SnippetToken::Literal(ref s) if s.contains("}")));
598 }
599
600 #[test]
601 fn unicode_literals_round_trip() {
602 let b = parse_ok("héllo wörld");
603 assert_eq!(b.tokens, vec![SnippetToken::Literal("héllo wörld".into())]);
604 }
605}