1#![allow(clippy::range_plus_one)]
3
4use crate::{error::AutoErrorOffset, Error, Result};
5
6use indexmap::IndexMap;
7use pochoir_common::{Spanned, StreamParser};
8use std::{borrow::Cow, path::Path};
9
10#[derive(Debug, PartialEq, Clone, Copy)]
11pub enum UnaryOp {
12 BoolNot,
13 Negate,
14 Range,
15 RangeInclusive,
16}
17
18#[derive(Debug, PartialEq, Clone, Copy)]
19pub enum BinaryOp {
20 Add,
21 Subtract,
22 Multiply,
23 Divide,
24
25 Equal,
26 NotEqual,
27
28 Greater,
29 GreaterOrEqual,
30 Less,
31 LessOrEqual,
32
33 Pipe,
34
35 BoolAnd,
36 BoolOr,
37
38 Semicolon,
39 Definition,
40 Range,
41 RangeInclusive,
42}
43
44#[derive(Debug, PartialEq, Clone, Copy)]
45pub enum TernaryOp {
46 Conditional,
47}
48
49#[derive(Debug, PartialEq, Clone)]
50pub enum Literal<'a> {
51 String(String),
52 Number(f64),
53 Array(Vec<Vec<Spanned<Token<'a>>>>),
54 Object(IndexMap<String, Vec<Spanned<Token<'a>>>>),
55 Bool(bool),
56 Null,
57}
58
59#[derive(Debug, PartialEq, Clone)]
60pub enum Token<'a> {
61 UnaryOperator(UnaryOp, Vec<Spanned<Token<'a>>>),
62 BinaryOperator(BinaryOp, Vec<Spanned<Token<'a>>>, Vec<Spanned<Token<'a>>>),
63 TernaryOperator(
64 TernaryOp,
65 Vec<Spanned<Token<'a>>>,
66 Vec<Spanned<Token<'a>>>,
67 Vec<Spanned<Token<'a>>>,
68 ),
69 Literal(Literal<'a>),
70 FunctionCall(Box<Spanned<Token<'a>>>, Vec<Vec<Spanned<Token<'a>>>>),
71 Variable(Cow<'a, str>),
72 Index(Vec<Spanned<Token<'a>>>, Box<Spanned<Token<'a>>>),
73}
74
75pub fn parse<P: AsRef<Path>>(
86 file_path: P,
87 value: &str,
88 file_offset: usize,
89) -> Result<Vec<Spanned<Token<'_>>>> {
90 let file_path = file_path.as_ref();
91 let mut parser = StreamParser::new(file_path, value);
92 let mut tokens = vec![];
93
94 while !parser.is_eoi() {
95 parse_recursive(&mut parser, &mut tokens, 0, file_offset)?;
96 }
97
98 Ok(tokens)
99}
100
101fn parse_recursive<'a>(
104 parser: &mut StreamParser<'a>,
105 tokens: &mut Vec<Spanned<Token<'a>>>,
106 min_precedence: usize,
107 file_offset: usize,
108) -> Result<()> {
109 parser.trim();
110
111 let start = parser.index();
112 let mut left_tokens = vec![];
113
114 if parser.take_exact("(").is_ok() {
115 parse_recursive(parser, &mut left_tokens, 0, file_offset)?;
116 parser.take_exact(")").map_err(|_e| {
117 Spanned::new(Error::MismatchedClosingDelimiter(")".to_string()))
118 .with_span(start + file_offset..start + file_offset + 1)
119 .with_file_path(parser.file_path())
120 })?;
121 } else if let Some((operator, length)) = parse_unary_operator(parser) {
122 parser.take(length).auto_error_offset(file_offset)?;
124
125 let mut tokens = vec![];
126 parse_recursive(parser, &mut tokens, 1242, file_offset)?;
127
128 let end = parser.index();
129 left_tokens.push(
130 Spanned::new(Token::UnaryOperator(operator, tokens))
131 .with_span(start + file_offset..end + file_offset)
132 .with_file_path(parser.file_path()),
133 );
134 } else {
135 left_tokens.push(parse_token(parser, file_offset)?);
137 }
138
139 loop {
140 let before_trim_index = parser.index();
141 parser.trim();
142
143 if let Some((operator, length, (left_precedence, right_precedence))) =
145 parse_binary_operator(parser)
146 {
147 if left_precedence < min_precedence {
148 parser.set_index(before_trim_index);
149 break;
150 }
151
152 parser.take(length).auto_error_offset(file_offset)?;
153 parser.trim();
154
155 let mut right_tokens = vec![];
157 if operator == BinaryOp::Range || operator == BinaryOp::RangeInclusive {
158 if !parser
160 .next()
161 .map_or(true, |ch| [')', ']', '}', ';'].contains(&ch))
162 {
163 parse_recursive(parser, &mut right_tokens, right_precedence, file_offset)?;
164 }
165 } else {
166 parse_recursive(parser, &mut right_tokens, right_precedence, file_offset)?;
167 }
168 let end = parser.index();
169
170 left_tokens =
171 vec![
172 Spanned::new(Token::BinaryOperator(operator, left_tokens, right_tokens))
173 .with_span(start + file_offset..end + file_offset)
174 .with_file_path(parser.file_path()),
175 ];
176 continue;
177 }
178
179 if let Some((operator, length, (left_precedence, right_precedence))) =
181 parse_ternary_operator(parser)
182 {
183 if left_precedence < min_precedence {
184 parser.set_index(before_trim_index);
185 break;
186 }
187
188 parser.take(length).auto_error_offset(file_offset)?;
189 parser.trim();
190
191 let mut middle_tokens = vec![];
193 parse_recursive(parser, &mut middle_tokens, 0, file_offset)?;
194
195 parser.trim();
196 parser.take_exact(":").auto_error_offset(file_offset)?;
197 parser.trim();
198
199 let mut right_tokens = vec![];
201 parse_recursive(parser, &mut right_tokens, right_precedence, file_offset)?;
202
203 let end = parser.index();
204
205 left_tokens = vec![Spanned::new(Token::TernaryOperator(
206 operator,
207 left_tokens,
208 middle_tokens,
209 right_tokens,
210 ))
211 .with_span(start + file_offset..end + file_offset)
212 .with_file_path(parser.file_path())];
213
214 continue;
215 }
216
217 parser.set_index(before_trim_index);
218 break;
219 }
220
221 tokens.extend(left_tokens);
222 Ok(())
223}
224
225#[allow(clippy::too_many_lines)]
226fn parse_token<'a>(
227 parser: &mut StreamParser<'a>,
228 file_offset: usize,
229) -> Result<Spanned<Token<'a>>> {
230 let token_start = parser.index();
231 let mut token = None;
232 parser.trim();
233
234 match parser.next().auto_error_offset(file_offset)? {
236 '"' | '\'' => token = Some(Token::Literal(string(parser, file_offset)?)),
237 ch if ch.is_ascii_digit() => token = Some(Token::Literal(number(parser)?)),
238 '[' => token = Some(Token::Literal(array(parser, file_offset)?)),
239 '{' => token = Some(Token::Literal(object(parser, file_offset)?)),
240 _ => (),
241 }
242
243 if parser.take_exact("true").is_ok() {
244 token = Some(Token::Literal(Literal::Bool(true)));
245 } else if parser.take_exact("false").is_ok() {
246 token = Some(Token::Literal(Literal::Bool(false)));
247 } else if parser.take_exact("null").is_ok() {
248 token = Some(Token::Literal(Literal::Null));
249 }
250
251 if parser
253 .next()
254 .is_ok_and(|ch| ch.is_alphabetic() || ch == '_')
255 {
256 let identifier_name = parser.take_while(|(_, ch)| ch.is_alphanumeric() || ch == '_');
257 token = Some(Token::Variable(Cow::Borrowed(identifier_name)));
258 }
259
260 if let Some(mut token) = token {
261 loop {
262 loop {
264 if parser.peek_exact(".") && !parser.peek_early_exact(".", 1) {
266 parser.take_exact(".").unwrap();
267
268 let index_start = parser.index();
269 let index_name = parser.take_while(|(_, ch)| ch.is_alphanumeric() || ch == '_');
270 let index_end = parser.index();
271
272 token = Token::Index(
273 vec![
274 Spanned::new(Token::Literal(Literal::String(index_name.to_string())))
275 .with_span(index_start + file_offset..index_end + file_offset)
276 .with_file_path(parser.file_path()),
277 ],
278 Box::new(
279 Spanned::new(token)
280 .with_span(token_start + file_offset..index_start + file_offset - 1)
281 .with_file_path(parser.file_path()),
282 ),
283 );
284 } else if parser.take_exact("[").is_ok() {
285 let index_start = parser.index();
286 let mut index_tokens = vec![];
287
288 parser.trim();
289 parse_recursive(parser, &mut index_tokens, 0, file_offset)?;
290 parser.trim();
291
292 parser.take_exact("]").map_err(|_e| {
293 Spanned::new(Error::MismatchedClosingDelimiter("]".to_string()))
294 .with_span(index_start + file_offset - 1..index_start + file_offset)
295 .with_file_path(parser.file_path())
296 })?;
297
298 token = Token::Index(
299 index_tokens,
300 Box::new(
301 Spanned::new(token)
302 .with_span(token_start + file_offset..index_start + file_offset - 1)
303 .with_file_path(parser.file_path()),
304 ),
305 );
306 } else {
307 break;
308 }
309 }
310
311 if parser.take_exact("(").is_ok() {
313 let function_name_end = parser.index() - 1;
314 parser.trim();
315 let mut args = vec![];
316
317 loop {
318 parser.trim();
319
320 if parser.peek_exact(")") {
321 break;
322 }
323
324 let mut tokens_for_arg = vec![];
325 parse_recursive(parser, &mut tokens_for_arg, 0, file_offset)?;
326 args.push(tokens_for_arg);
327
328 parser.trim();
329 if parser.take_exact(",").is_err() {
330 break;
331 }
332 }
333
334 token = Token::FunctionCall(
335 Box::new(
336 Spanned::new(token)
337 .with_span(token_start + file_offset..function_name_end + file_offset)
338 .with_file_path(parser.file_path()),
339 ),
340 args,
341 );
342
343 parser.trim();
344 parser.take_exact(")").map_err(|_e| {
345 Spanned::new(Error::MismatchedClosingDelimiter(")".to_string()))
346 .with_span(
347 function_name_end + file_offset..function_name_end + file_offset + 1,
348 )
349 .with_file_path(parser.file_path())
350 })?;
351 } else {
352 break;
353 }
354 }
355
356 let token_end = parser.index();
357 Ok(Spanned::new(token)
358 .with_span(token_start + file_offset..token_end + file_offset)
359 .with_file_path(parser.file_path()))
360 } else {
361 Err(Spanned::new(Error::ExpectedExpression(
362 parser.next().auto_error_offset(file_offset)?.to_string(),
363 ))
364 .with_span(token_start + file_offset..token_start + file_offset + 1)
365 .with_file_path(parser.file_path()))
366 }
367}
368
369fn parse_unary_operator(parser: &mut StreamParser) -> Option<(UnaryOp, usize)> {
370 parser.trim();
371
372 match parser.next() {
374 Ok('-') => Some((UnaryOp::Negate, 1)),
375 Ok('!') => Some((UnaryOp::BoolNot, 1)),
376 Ok('.') if parser.peek_early(1, 1) == Ok(".") && parser.peek_early(1, 2) == Ok("=") => {
377 Some((UnaryOp::RangeInclusive, 3))
378 }
379 Ok('.') if parser.peek_early(1, 1) == Ok(".") => Some((UnaryOp::Range, 2)),
380 _ => None,
381 }
382}
383
384fn parse_binary_operator(parser: &mut StreamParser) -> Option<(BinaryOp, usize, (usize, usize))> {
385 parser.trim();
386
387 match parser.next() {
389 Ok('|') if parser.peek_early(1, 1) == Ok("|") => Some((BinaryOp::BoolOr, 2, (8, 9))),
390 Ok('&') if parser.peek_early(1, 1) == Ok("&") => Some((BinaryOp::BoolAnd, 2, (10, 11))),
391 Ok('=') if parser.peek_early(1, 1) == Ok("=") => Some((BinaryOp::Equal, 2, (12, 13))),
392 Ok('!') if parser.peek_early(1, 1) == Ok("=") => Some((BinaryOp::NotEqual, 2, (12, 13))),
393 Ok('<') if parser.peek_early(1, 1) == Ok("=") => Some((BinaryOp::LessOrEqual, 2, (14, 15))),
394 Ok('>') if parser.peek_early(1, 1) == Ok("=") => {
395 Some((BinaryOp::GreaterOrEqual, 2, (14, 15)))
396 }
397 Ok('<') => Some((BinaryOp::Less, 1, (14, 15))),
398 Ok('>') => Some((BinaryOp::Greater, 1, (14, 15))),
399 Ok('.') if parser.peek_early(1, 1) == Ok(".") && parser.peek_early(1, 2) == Ok("=") => {
400 Some((BinaryOp::RangeInclusive, 3, (16, 17)))
401 }
402 Ok('.') if parser.peek_early(1, 1) == Ok(".") => Some((BinaryOp::Range, 2, (16, 17))),
403 Ok('+') => Some((BinaryOp::Add, 1, (18, 19))),
404 Ok('-') => Some((BinaryOp::Subtract, 1, (18, 19))),
405 Ok('*') => Some((BinaryOp::Multiply, 1, (20, 21))),
406 Ok('/') => Some((BinaryOp::Divide, 1, (20, 21))),
407
408 Ok(';') => Some((BinaryOp::Semicolon, 1, (1, 2))),
409 Ok('=') => Some((BinaryOp::Definition, 1, (3, 4))),
410
411 Ok('|') if parser.peek_early(1, 1) == Ok(">") => Some((BinaryOp::Pipe, 2, (5, 22))),
413 _ => None,
414 }
415}
416
417fn parse_ternary_operator(parser: &mut StreamParser) -> Option<(TernaryOp, usize, (usize, usize))> {
418 parser.trim();
419
420 match parser.next() {
422 Ok('?') => Some((TernaryOp::Conditional, 1, (7, 6))),
423 _ => None,
424 }
425}
426
427fn string<'a>(parser: &mut StreamParser<'a>, file_offset: usize) -> Result<Literal<'a>> {
428 let index = file_offset + parser.index();
429 if parser.take_exact("\"").is_ok() {
430 let mut is_escaped = false;
431 let result = parser
432 .take_while(|(_, ch)| {
433 if is_escaped {
434 is_escaped = false;
435 true
436 } else if ch == '\\' {
437 is_escaped = true;
438 true
439 } else {
440 ch != '"'
441 }
442 })
443 .to_string();
444 parser
445 .take_exact("\"")
446 .auto_error_offset(file_offset)
447 .map_err(|e| match *e {
448 Error::StreamParserError(pochoir_common::Error::UnexpectedEoi) => {
449 Spanned::new(Error::UnterminatedString)
450 .with_span(index..index + 1)
451 .with_file_path(parser.file_path())
452 }
453 _ => e,
454 })?;
455
456 Ok(Literal::String(
457 result.replace("\\\"", "\"").replace("\\\\", "\\"),
458 ))
459 } else if parser.take_exact("'").is_ok() {
460 let mut is_escaped = false;
461 let result = parser
462 .take_while(|(_, ch)| {
463 if is_escaped {
464 is_escaped = false;
465 true
466 } else if ch == '\\' {
467 is_escaped = true;
468 true
469 } else {
470 ch != '\''
471 }
472 })
473 .to_string();
474 parser
475 .take_exact("'")
476 .auto_error_offset(file_offset)
477 .map_err(|e| match *e {
478 Error::StreamParserError(pochoir_common::Error::UnexpectedEoi) => {
479 Spanned::new(Error::UnterminatedString)
480 .with_span(index..index + 1)
481 .with_file_path(parser.file_path())
482 }
483 _ => e,
484 })?;
485
486 Ok(Literal::String(
487 result.replace("\\'", "'").replace("\\\\", "\\"),
488 ))
489 } else {
490 unreachable!();
492 }
493}
494
495fn number<'a>(parser: &mut StreamParser<'a>) -> Result<Literal<'a>> {
496 let result = parser.take_while_peekable(|_, ch, next_ch| {
498 ch.is_numeric() || (ch == '.' && next_ch != Some('.'))
499 });
500
501 Ok(Literal::Number(
502 result.parse::<f64>().map_err(|_e| Error::InvalidNumber)?,
503 ))
504}
505
506fn array<'a>(parser: &mut StreamParser<'a>, file_offset: usize) -> Result<Literal<'a>> {
507 let index = file_offset + parser.index();
508 parser.take_exact("[").auto_error_offset(file_offset)?;
509
510 let mut values = vec![];
511
512 loop {
513 parser.trim();
514
515 if parser.peek_exact("]") {
516 break;
517 }
518
519 let mut field_values = vec![];
520 parse_recursive(parser, &mut field_values, 0, file_offset).map_err(|e| match *e {
521 Error::StreamParserError(pochoir_common::Error::UnexpectedEoi) => {
522 Spanned::new(Error::UnterminatedArray)
523 .with_span(index..index + 1)
524 .with_file_path(parser.file_path())
525 }
526 _ => e,
527 })?;
528 values.push(field_values);
529
530 if parser.take_exact(",").is_err() {
531 break;
532 }
533 }
534
535 parser.trim();
536 parser.take_exact("]").map_err(|_e| {
537 Spanned::new(Error::UnterminatedArray)
538 .with_span(index..index + 1)
539 .with_file_path(parser.file_path())
540 })?;
541 Ok(Literal::Array(values))
542}
543
544fn object<'a>(parser: &mut StreamParser<'a>, file_offset: usize) -> Result<Literal<'a>> {
545 let index = file_offset + parser.index();
546 parser.take_exact("{").auto_error_offset(file_offset)?;
547
548 let mut values = IndexMap::new();
549
550 loop {
551 parser.trim();
552
553 if parser.peek_exact("}") {
554 break;
555 }
556
557 let key = if parser.peek_exact("\"") || parser.peek_exact("'") {
558 if let Literal::String(string) = string(parser, file_offset)? {
559 string
560 } else {
561 unreachable!();
562 }
563 } else {
564 let mut first = true;
565 parser
566 .take_while(|(_, ch)| {
567 if first {
568 first = false;
569 ch.is_alphabetic() || ch == '_'
570 } else {
571 ch.is_alphanumeric() || ch == '_'
572 }
573 })
574 .trim()
575 .to_string()
576 };
577 parser
578 .take_exact(":")
579 .auto_error_offset(file_offset)
580 .map_err(|e| match *e {
581 Error::StreamParserError(pochoir_common::Error::UnexpectedEoi) => {
582 Spanned::new(Error::UnterminatedObject)
583 .with_span(index..index + 1)
584 .with_file_path(parser.file_path())
585 }
586 _ => e,
587 })?;
588 parser.trim();
589
590 let mut field_values = vec![];
591 parse_recursive(parser, &mut field_values, 0, file_offset).map_err(|e| match *e {
592 Error::StreamParserError(pochoir_common::Error::UnexpectedEoi) => {
593 Spanned::new(Error::UnterminatedObject)
594 .with_span(index..index + 1)
595 .with_file_path(parser.file_path())
596 }
597 _ => e,
598 })?;
599 values.insert(key, field_values);
600
601 if parser.take_exact(",").is_err() {
602 break;
603 }
604 }
605
606 parser.trim();
607 parser.take_exact("}").map_err(|_e| {
608 Spanned::new(Error::UnterminatedObject)
609 .with_span(index..index + 1)
610 .with_file_path(parser.file_path())
611 })?;
612 Ok(Literal::Object(values))
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618 use indexmap::indexmap;
619 use pretty_assertions::assert_eq;
620
621 #[test]
622 fn parse_number() {
623 assert_eq!(
624 parse("index.html", "42", 0),
625 Ok(vec![Spanned::new(Token::Literal(Literal::Number(42.0)))
626 .with_span(0..2)
627 .with_file_path("index.html")])
628 );
629 }
630
631 #[test]
632 fn mathematical_priority_test() {
633 assert_eq!(
634 parse("index.html", "6/ 2 *(2+1) == 9", 0),
635 Ok(vec![Spanned::new(Token::BinaryOperator(
636 BinaryOp::Equal,
637 vec![Spanned::new(Token::BinaryOperator(
638 BinaryOp::Multiply,
639 vec![Spanned::new(Token::BinaryOperator(
640 BinaryOp::Divide,
641 vec![Spanned::new(Token::Literal(Literal::Number(6.0)))
642 .with_span(0..1)
643 .with_file_path("index.html")],
644 vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
645 .with_span(3..4)
646 .with_file_path("index.html")],
647 ))
648 .with_span(0..4)
649 .with_file_path("index.html")],
650 vec![Spanned::new(Token::BinaryOperator(
651 BinaryOp::Add,
652 vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
653 .with_span(7..8)
654 .with_file_path("index.html")],
655 vec![Spanned::new(Token::Literal(Literal::Number(1.0)))
656 .with_span(9..10)
657 .with_file_path("index.html")],
658 ))
659 .with_span(7..10)
660 .with_file_path("index.html")],
661 ))
662 .with_span(0..11)
663 .with_file_path("index.html")],
664 vec![Spanned::new(Token::Literal(Literal::Number(9.0)))
665 .with_span(15..16)
666 .with_file_path("index.html")]
667 ))
668 .with_span(0..16)
669 .with_file_path("index.html")]),
670 );
671 }
672
673 #[test]
674 fn array_indexing() {
675 assert_eq!(
676 parse("index.html", "my_arr[1] == 2.0", 0),
677 Ok(vec![Spanned::new(Token::BinaryOperator(
678 BinaryOp::Equal,
679 vec![Spanned::new(Token::Index(
680 vec![Spanned::new(Token::Literal(Literal::Number(1.0)))
681 .with_span(7..8)
682 .with_file_path("index.html")],
683 Box::new(
684 Spanned::new(Token::Variable(Cow::Borrowed("my_arr")))
685 .with_span(0..6)
686 .with_file_path("index.html")
687 ),
688 ))
689 .with_span(0..9)
690 .with_file_path("index.html")],
691 vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
692 .with_span(13..16)
693 .with_file_path("index.html")]
694 ))
695 .with_span(0..16)
696 .with_file_path("index.html")]),
697 );
698 }
699
700 #[test]
701 fn function_args_newline() {
702 assert_eq!(
703 parse(
704 "index.html",
705 r#"test(
706 "a",
707 42
708)"#,
709 0
710 ),
711 Ok(vec![Spanned::new(Token::FunctionCall(
712 Box::new(
713 Spanned::new(Token::Variable(Cow::Borrowed("test")))
714 .with_span(0..4)
715 .with_file_path("index.html")
716 ),
717 vec![
718 vec![
719 Spanned::new(Token::Literal(Literal::String("a".to_string())))
720 .with_span(10..13)
721 .with_file_path("index.html")
722 ],
723 vec![Spanned::new(Token::Literal(Literal::Number(42.0)))
724 .with_span(19..21)
725 .with_file_path("index.html")],
726 ]
727 ))
728 .with_span(0..23)
729 .with_file_path("index.html")])
730 );
731 }
732
733 #[test]
734 fn function_args_trailing_comma() {
735 assert_eq!(
736 parse(
737 "index.html",
738 r#"test(
739 "a",
740 42,
741)"#,
742 0
743 ),
744 Ok(vec![Spanned::new(Token::FunctionCall(
745 Box::new(
746 Spanned::new(Token::Variable(Cow::Borrowed("test")))
747 .with_span(0..4)
748 .with_file_path("index.html")
749 ),
750 vec![
751 vec![
752 Spanned::new(Token::Literal(Literal::String("a".to_string())))
753 .with_span(10..13)
754 .with_file_path("index.html")
755 ],
756 vec![Spanned::new(Token::Literal(Literal::Number(42.0)))
757 .with_span(19..21)
758 .with_file_path("index.html")],
759 ]
760 ))
761 .with_span(0..24)
762 .with_file_path("index.html")])
763 );
764
765 assert_eq!(
766 parse("index.html", r#"test("a", 42,)"#, 0),
767 Ok(vec![Spanned::new(Token::FunctionCall(
768 Box::new(
769 Spanned::new(Token::Variable(Cow::Borrowed("test")))
770 .with_span(0..4)
771 .with_file_path("index.html")
772 ),
773 vec![
774 vec![
775 Spanned::new(Token::Literal(Literal::String("a".to_string())))
776 .with_span(5..8)
777 .with_file_path("index.html")
778 ],
779 vec![Spanned::new(Token::Literal(Literal::Number(42.0)))
780 .with_span(10..12)
781 .with_file_path("index.html")],
782 ]
783 ))
784 .with_span(0..14)
785 .with_file_path("index.html")])
786 );
787 }
788
789 #[test]
790 fn namespaced_function() {
791 assert_eq!(
792 parse(
793 "index.html",
794 "Restaurants.get('Killer Pizza from Mars').location == 'Oceanside, CA'",
795 0
796 ),
797 Ok(vec![Spanned::new(Token::BinaryOperator(
798 BinaryOp::Equal,
799 vec![Spanned::new(Token::Index(
800 vec![
801 Spanned::new(Token::Literal(Literal::String("location".to_string())))
802 .with_span(42..50)
803 .with_file_path("index.html")
804 ],
805 Box::new(
806 Spanned::new(Token::FunctionCall(
807 Box::new(
808 Spanned::new(Token::Index(
809 vec![Spanned::new(Token::Literal(Literal::String(
810 "get".to_string()
811 )))
812 .with_span(12..15)
813 .with_file_path("index.html")],
814 Box::new(
815 Spanned::new(Token::Variable(Cow::Borrowed("Restaurants")))
816 .with_span(0..11)
817 .with_file_path("index.html")
818 )
819 ))
820 .with_span(0..15)
821 .with_file_path("index.html")
822 ),
823 vec![vec![Spanned::new(Token::Literal(Literal::String(
824 "Killer Pizza from Mars".to_string()
825 )))
826 .with_span(16..40)
827 .with_file_path("index.html")]],
828 ))
829 .with_span(0..41)
830 .with_file_path("index.html")
831 )
832 ))
833 .with_span(0..50)
834 .with_file_path("index.html")],
835 vec![
836 Spanned::new(Token::Literal(Literal::String("Oceanside, CA".to_string())))
837 .with_span(54..69)
838 .with_file_path("index.html")
839 ],
840 ))
841 .with_span(0..69)
842 .with_file_path("index.html")]),
843 );
844 }
845
846 #[test]
847 fn object_newline() {
848 assert_eq!(
849 parse(
850 "index.html",
851 r#"{
852 "a": "b",
853 "c": 42,
854 }"#,
855 0
856 ),
857 Ok(vec![Spanned::new(Token::Literal(Literal::Object(
858 indexmap! {
859 String::from("a") => vec![Spanned::new(Token::Literal(Literal::String("b".to_string())))
860 .with_span(27..30)
861 .with_file_path("index.html")
862 ],
863 String::from("c") => vec![Spanned::new(Token::Literal(Literal::Number(42.0)))
864 .with_span(57..59)
865 .with_file_path("index.html")
866 ],
867 }
868 )))
869 .with_span(0..78)
870 .with_file_path("index.html")])
871 );
872 }
873
874 #[test]
875 fn array_newline() {
876 assert_eq!(
877 parse(
878 "index.html",
879 r#"[
880 "b",
881 42,
882 ]"#,
883 0
884 ),
885 Ok(vec![Spanned::new(Token::Literal(Literal::Array(vec![
886 vec![
887 Spanned::new(Token::Literal(Literal::String("b".to_string())))
888 .with_span(22..25)
889 .with_file_path("index.html")
890 ],
891 vec![Spanned::new(Token::Literal(Literal::Number(42.0)))
892 .with_span(47..49)
893 .with_file_path("index.html")],
894 ])))
895 .with_span(0..68)
896 .with_file_path("index.html")])
897 );
898 }
899
900 #[test]
901 #[allow(clippy::too_many_lines)]
902 fn range() {
903 assert_eq!(
904 parse("index.html", "2..3", 0),
905 Ok(vec![Spanned::new(Token::BinaryOperator(
906 BinaryOp::Range,
907 vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
908 .with_span(0..1)
909 .with_file_path("index.html")],
910 vec![Spanned::new(Token::Literal(Literal::Number(3.0)))
911 .with_span(3..4)
912 .with_file_path("index.html")],
913 ))
914 .with_span(0..4)
915 .with_file_path("index.html")]),
916 );
917
918 assert_eq!(
919 parse("index.html", "3..2", 0),
920 Ok(vec![Spanned::new(Token::BinaryOperator(
921 BinaryOp::Range,
922 vec![Spanned::new(Token::Literal(Literal::Number(3.0)))
923 .with_span(0..1)
924 .with_file_path("index.html")],
925 vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
926 .with_span(3..4)
927 .with_file_path("index.html")],
928 ))
929 .with_span(0..4)
930 .with_file_path("index.html")]),
931 );
932
933 assert_eq!(
934 parse("index.html", "..3", 0),
935 Ok(vec![Spanned::new(Token::UnaryOperator(
936 UnaryOp::Range,
937 vec![Spanned::new(Token::Literal(Literal::Number(3.0)))
938 .with_span(2..3)
939 .with_file_path("index.html")],
940 ))
941 .with_span(0..3)
942 .with_file_path("index.html")]),
943 );
944
945 assert_eq!(
946 parse("index.html", "2..", 0),
947 Ok(vec![Spanned::new(Token::BinaryOperator(
948 BinaryOp::Range,
949 vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
950 .with_span(0..1)
951 .with_file_path("index.html")],
952 vec![],
953 ))
954 .with_span(0..3)
955 .with_file_path("index.html")]),
956 );
957
958 assert_eq!(
959 parse("index.html", "1 + 2..4", 0),
960 Ok(vec![Spanned::new(Token::BinaryOperator(
961 BinaryOp::Range,
962 vec![Spanned::new(Token::BinaryOperator(
963 BinaryOp::Add,
964 vec![Spanned::new(Token::Literal(Literal::Number(1.0)))
965 .with_span(0..1)
966 .with_file_path("index.html")],
967 vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
968 .with_span(4..5)
969 .with_file_path("index.html")],
970 ))
971 .with_span(0..5)
972 .with_file_path("index.html")],
973 vec![Spanned::new(Token::Literal(Literal::Number(4.0)))
974 .with_span(7..8)
975 .with_file_path("index.html")],
976 ))
977 .with_span(0..8)
978 .with_file_path("index.html")]),
979 );
980
981 assert_eq!(
982 parse("index.html", "1..1 + 1", 0),
983 Ok(vec![Spanned::new(Token::BinaryOperator(
984 BinaryOp::Range,
985 vec![Spanned::new(Token::Literal(Literal::Number(1.0)))
986 .with_span(0..1)
987 .with_file_path("index.html")],
988 vec![Spanned::new(Token::BinaryOperator(
989 BinaryOp::Add,
990 vec![Spanned::new(Token::Literal(Literal::Number(1.0)))
991 .with_span(3..4)
992 .with_file_path("index.html")],
993 vec![Spanned::new(Token::Literal(Literal::Number(1.0)))
994 .with_span(7..8)
995 .with_file_path("index.html")],
996 ))
997 .with_span(3..8)
998 .with_file_path("index.html")],
999 ))
1000 .with_span(0..8)
1001 .with_file_path("index.html")]),
1002 );
1003
1004 assert_eq!(
1005 parse("index.html", r#"len("a")..3"#, 0),
1006 Ok(vec![Spanned::new(Token::BinaryOperator(
1007 BinaryOp::Range,
1008 vec![Spanned::new(Token::FunctionCall(
1009 Box::new(
1010 Spanned::new(Token::Variable(Cow::Borrowed("len")))
1011 .with_span(0..3)
1012 .with_file_path("index.html")
1013 ),
1014 vec![vec![Spanned::new(Token::Literal(Literal::String(
1015 "a".to_string()
1016 )))
1017 .with_span(4..7)
1018 .with_file_path("index.html")]]
1019 ))
1020 .with_span(0..8)
1021 .with_file_path("index.html")],
1022 vec![Spanned::new(Token::Literal(Literal::Number(3.0)))
1023 .with_span(10..11)
1024 .with_file_path("index.html")],
1025 ))
1026 .with_span(0..11)
1027 .with_file_path("index.html")]),
1028 );
1029
1030 assert_eq!(
1031 parse("index.html", r#""a"..3"#, 0),
1032 Ok(vec![Spanned::new(Token::BinaryOperator(
1033 BinaryOp::Range,
1034 vec![
1035 Spanned::new(Token::Literal(Literal::String("a".to_string())))
1036 .with_span(0..3)
1037 .with_file_path("index.html")
1038 ],
1039 vec![Spanned::new(Token::Literal(Literal::Number(3.0)))
1040 .with_span(5..6)
1041 .with_file_path("index.html")],
1042 ))
1043 .with_span(0..6)
1044 .with_file_path("index.html")]),
1045 );
1046
1047 assert_eq!(
1048 parse("index.html", "1..3[2]", 0),
1049 Ok(vec![Spanned::new(Token::BinaryOperator(
1050 BinaryOp::Range,
1051 vec![Spanned::new(Token::Literal(Literal::Number(1.0)))
1052 .with_span(0..1)
1053 .with_file_path("index.html")],
1054 vec![Spanned::new(Token::Index(
1055 vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
1056 .with_span(5..6)
1057 .with_file_path("index.html"),],
1058 Box::new(
1059 Spanned::new(Token::Literal(Literal::Number(3.0)))
1060 .with_span(3..4)
1061 .with_file_path("index.html")
1062 )
1063 ))
1064 .with_span(3..7)
1065 .with_file_path("index.html")]
1066 ))
1067 .with_span(0..7)
1068 .with_file_path("index.html")]),
1069 );
1070
1071 assert_eq!(
1072 parse("index.html", "1..3.2", 0),
1073 Ok(vec![Spanned::new(Token::BinaryOperator(
1074 BinaryOp::Range,
1075 vec![Spanned::new(Token::Literal(Literal::Number(1.0)))
1076 .with_span(0..1)
1077 .with_file_path("index.html")],
1078 vec![Spanned::new(Token::Literal(Literal::Number(3.2)))
1079 .with_span(3..6)
1080 .with_file_path("index.html")],
1081 ))
1082 .with_span(0..6)
1083 .with_file_path("index.html")]),
1084 );
1085
1086 assert_eq!(
1087 parse("index.html", "0..2 == ..=1", 0),
1088 Ok(vec![Spanned::new(Token::BinaryOperator(
1089 BinaryOp::Equal,
1090 vec![Spanned::new(Token::BinaryOperator(
1091 BinaryOp::Range,
1092 vec![Spanned::new(Token::Literal(Literal::Number(0.0)))
1093 .with_span(0..1)
1094 .with_file_path("index.html")],
1095 vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
1096 .with_span(3..4)
1097 .with_file_path("index.html")],
1098 ))
1099 .with_span(0..4)
1100 .with_file_path("index.html")],
1101 vec![Spanned::new(Token::UnaryOperator(
1102 UnaryOp::RangeInclusive,
1103 vec![Spanned::new(Token::Literal(Literal::Number(1.0)))
1104 .with_span(11..12)
1105 .with_file_path("index.html")],
1106 ))
1107 .with_span(8..12)
1108 .with_file_path("index.html")],
1109 ))
1110 .with_span(0..12)
1111 .with_file_path("index.html")]),
1112 );
1113
1114 assert_eq!(
1115 parse("index.html", "my_fn(2..)", 0),
1116 Ok(vec![Spanned::new(Token::FunctionCall(
1117 Box::new(
1118 Spanned::new(Token::Variable(Cow::Borrowed("my_fn")))
1119 .with_span(0..5)
1120 .with_file_path("index.html")
1121 ),
1122 vec![vec![Spanned::new(Token::BinaryOperator(
1123 BinaryOp::Range,
1124 vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
1125 .with_span(6..7)
1126 .with_file_path("index.html")],
1127 vec![],
1128 ))
1129 .with_span(6..9)
1130 .with_file_path("index.html")]],
1131 ))
1132 .with_span(0..10)
1133 .with_file_path("index.html")]),
1134 );
1135
1136 assert_eq!(
1137 parse("index.html", r#""hello"[2..=4] == "ll""#, 0),
1138 Ok(vec![Spanned::new(Token::BinaryOperator(
1139 BinaryOp::Equal,
1140 vec![Spanned::new(Token::Index(
1141 vec![Spanned::new(Token::BinaryOperator(
1142 BinaryOp::RangeInclusive,
1143 vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
1144 .with_span(8..9)
1145 .with_file_path("index.html")],
1146 vec![Spanned::new(Token::Literal(Literal::Number(4.0)))
1147 .with_span(12..13)
1148 .with_file_path("index.html")],
1149 ))
1150 .with_span(8..13)
1151 .with_file_path("index.html")],
1152 Box::new(
1153 Spanned::new(Token::Literal(Literal::String("hello".to_string())))
1154 .with_span(0..7)
1155 .with_file_path("index.html")
1156 )
1157 ))
1158 .with_span(0..14)
1159 .with_file_path("index.html")],
1160 vec![
1161 Spanned::new(Token::Literal(Literal::String("ll".to_string())))
1162 .with_span(18..22)
1163 .with_file_path("index.html")
1164 ],
1165 ))
1166 .with_span(0..22)
1167 .with_file_path("index.html")]),
1168 );
1169 }
1170}