#![allow(clippy::range_plus_one)]
use crate::{error::AutoErrorOffset, Error, Result};
use indexmap::IndexMap;
use pochoir_common::{Spanned, StreamParser};
use std::{borrow::Cow, path::Path};
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum UnaryOp {
BoolNot,
Negate,
Range,
RangeInclusive,
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum BinaryOp {
Add,
Subtract,
Multiply,
Divide,
Equal,
NotEqual,
Greater,
GreaterOrEqual,
Less,
LessOrEqual,
Pipe,
BoolAnd,
BoolOr,
Semicolon,
Definition,
Range,
RangeInclusive,
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum TernaryOp {
Conditional,
}
#[derive(Debug, PartialEq, Clone)]
pub enum Literal<'a> {
String(String),
Number(f64),
Array(Vec<Vec<Spanned<Token<'a>>>>),
Object(IndexMap<String, Vec<Spanned<Token<'a>>>>),
Bool(bool),
Null,
}
#[derive(Debug, PartialEq, Clone)]
pub enum Token<'a> {
UnaryOperator(UnaryOp, Vec<Spanned<Token<'a>>>),
BinaryOperator(BinaryOp, Vec<Spanned<Token<'a>>>, Vec<Spanned<Token<'a>>>),
TernaryOperator(
TernaryOp,
Vec<Spanned<Token<'a>>>,
Vec<Spanned<Token<'a>>>,
Vec<Spanned<Token<'a>>>,
),
Literal(Literal<'a>),
FunctionCall(Box<Spanned<Token<'a>>>, Vec<Vec<Spanned<Token<'a>>>>),
Variable(Cow<'a, str>),
Index(Vec<Spanned<Token<'a>>>, Box<Spanned<Token<'a>>>),
}
pub fn parse<P: AsRef<Path>>(
file_path: P,
value: &str,
file_offset: usize,
) -> Result<Vec<Spanned<Token<'_>>>> {
let file_path = file_path.as_ref();
let mut parser = StreamParser::new(file_path, value);
let mut tokens = vec![];
while !parser.is_eoi() {
parse_recursive(&mut parser, &mut tokens, 0, file_offset)?;
}
Ok(tokens)
}
fn parse_recursive<'a>(
parser: &mut StreamParser<'a>,
tokens: &mut Vec<Spanned<Token<'a>>>,
min_precedence: usize,
file_offset: usize,
) -> Result<()> {
parser.trim();
let start = parser.index();
let mut left_tokens = vec![];
if parser.take_exact("(").is_ok() {
parse_recursive(parser, &mut left_tokens, 0, file_offset)?;
parser.take_exact(")").map_err(|_e| {
Spanned::new(Error::MismatchedClosingDelimiter(")".to_string()))
.with_span(start + file_offset..start + file_offset + 1)
.with_file_path(parser.file_path())
})?;
} else if let Some((operator, length)) = parse_unary_operator(parser) {
parser.take(length).auto_error_offset(file_offset)?;
let mut tokens = vec![];
parse_recursive(parser, &mut tokens, 1242, file_offset)?;
let end = parser.index();
left_tokens.push(
Spanned::new(Token::UnaryOperator(operator, tokens))
.with_span(start + file_offset..end + file_offset)
.with_file_path(parser.file_path()),
);
} else {
left_tokens.push(parse_token(parser, file_offset)?);
}
loop {
let before_trim_index = parser.index();
parser.trim();
if let Some((operator, length, (left_precedence, right_precedence))) =
parse_binary_operator(parser)
{
if left_precedence < min_precedence {
parser.set_index(before_trim_index);
break;
}
parser.take(length).auto_error_offset(file_offset)?;
parser.trim();
let mut right_tokens = vec![];
if operator == BinaryOp::Range || operator == BinaryOp::RangeInclusive {
if !parser
.next()
.map_or(true, |ch| [')', ']', '}', ';'].contains(&ch))
{
parse_recursive(parser, &mut right_tokens, right_precedence, file_offset)?;
}
} else {
parse_recursive(parser, &mut right_tokens, right_precedence, file_offset)?;
}
let end = parser.index();
left_tokens =
vec![
Spanned::new(Token::BinaryOperator(operator, left_tokens, right_tokens))
.with_span(start + file_offset..end + file_offset)
.with_file_path(parser.file_path()),
];
continue;
}
if let Some((operator, length, (left_precedence, right_precedence))) =
parse_ternary_operator(parser)
{
if left_precedence < min_precedence {
parser.set_index(before_trim_index);
break;
}
parser.take(length).auto_error_offset(file_offset)?;
parser.trim();
let mut middle_tokens = vec![];
parse_recursive(parser, &mut middle_tokens, 0, file_offset)?;
parser.trim();
parser.take_exact(":").auto_error_offset(file_offset)?;
parser.trim();
let mut right_tokens = vec![];
parse_recursive(parser, &mut right_tokens, right_precedence, file_offset)?;
let end = parser.index();
left_tokens = vec![Spanned::new(Token::TernaryOperator(
operator,
left_tokens,
middle_tokens,
right_tokens,
))
.with_span(start + file_offset..end + file_offset)
.with_file_path(parser.file_path())];
continue;
}
parser.set_index(before_trim_index);
break;
}
tokens.extend(left_tokens);
Ok(())
}
#[allow(clippy::too_many_lines)]
fn parse_token<'a>(
parser: &mut StreamParser<'a>,
file_offset: usize,
) -> Result<Spanned<Token<'a>>> {
let token_start = parser.index();
let mut token = None;
parser.trim();
match parser.next().auto_error_offset(file_offset)? {
'"' | '\'' => token = Some(Token::Literal(string(parser, file_offset)?)),
ch if ch.is_ascii_digit() => token = Some(Token::Literal(number(parser)?)),
'[' => token = Some(Token::Literal(array(parser, file_offset)?)),
'{' => token = Some(Token::Literal(object(parser, file_offset)?)),
_ => (),
}
if parser.take_exact("true").is_ok() {
token = Some(Token::Literal(Literal::Bool(true)));
} else if parser.take_exact("false").is_ok() {
token = Some(Token::Literal(Literal::Bool(false)));
} else if parser.take_exact("null").is_ok() {
token = Some(Token::Literal(Literal::Null));
}
if parser
.next()
.is_ok_and(|ch| ch.is_alphabetic() || ch == '_')
{
let identifier_name = parser.take_while(|(_, ch)| ch.is_alphanumeric() || ch == '_');
token = Some(Token::Variable(Cow::Borrowed(identifier_name)));
}
if let Some(mut token) = token {
loop {
loop {
if parser.peek_exact(".") && !parser.peek_early_exact(".", 1) {
parser.take_exact(".").unwrap();
let index_start = parser.index();
let index_name = parser.take_while(|(_, ch)| ch.is_alphanumeric() || ch == '_');
let index_end = parser.index();
token = Token::Index(
vec![
Spanned::new(Token::Literal(Literal::String(index_name.to_string())))
.with_span(index_start + file_offset..index_end + file_offset)
.with_file_path(parser.file_path()),
],
Box::new(
Spanned::new(token)
.with_span(token_start + file_offset..index_start + file_offset - 1)
.with_file_path(parser.file_path()),
),
);
} else if parser.take_exact("[").is_ok() {
let index_start = parser.index();
let mut index_tokens = vec![];
parser.trim();
parse_recursive(parser, &mut index_tokens, 0, file_offset)?;
parser.trim();
parser.take_exact("]").map_err(|_e| {
Spanned::new(Error::MismatchedClosingDelimiter("]".to_string()))
.with_span(index_start + file_offset - 1..index_start + file_offset)
.with_file_path(parser.file_path())
})?;
token = Token::Index(
index_tokens,
Box::new(
Spanned::new(token)
.with_span(token_start + file_offset..index_start + file_offset - 1)
.with_file_path(parser.file_path()),
),
);
} else {
break;
}
}
if parser.take_exact("(").is_ok() {
let function_name_end = parser.index() - 1;
parser.trim();
let mut args = vec![];
loop {
parser.trim();
if parser.peek_exact(")") {
break;
}
let mut tokens_for_arg = vec![];
parse_recursive(parser, &mut tokens_for_arg, 0, file_offset)?;
args.push(tokens_for_arg);
parser.trim();
if parser.take_exact(",").is_err() {
break;
}
}
token = Token::FunctionCall(
Box::new(
Spanned::new(token)
.with_span(token_start + file_offset..function_name_end + file_offset)
.with_file_path(parser.file_path()),
),
args,
);
parser.trim();
parser.take_exact(")").map_err(|_e| {
Spanned::new(Error::MismatchedClosingDelimiter(")".to_string()))
.with_span(
function_name_end + file_offset..function_name_end + file_offset + 1,
)
.with_file_path(parser.file_path())
})?;
} else {
break;
}
}
let token_end = parser.index();
Ok(Spanned::new(token)
.with_span(token_start + file_offset..token_end + file_offset)
.with_file_path(parser.file_path()))
} else {
Err(Spanned::new(Error::ExpectedExpression(
parser.next().auto_error_offset(file_offset)?.to_string(),
))
.with_span(token_start + file_offset..token_start + file_offset + 1)
.with_file_path(parser.file_path()))
}
}
fn parse_unary_operator(parser: &mut StreamParser) -> Option<(UnaryOp, usize)> {
parser.trim();
match parser.next() {
Ok('-') => Some((UnaryOp::Negate, 1)),
Ok('!') => Some((UnaryOp::BoolNot, 1)),
Ok('.') if parser.peek_early(1, 1) == Ok(".") && parser.peek_early(1, 2) == Ok("=") => {
Some((UnaryOp::RangeInclusive, 3))
}
Ok('.') if parser.peek_early(1, 1) == Ok(".") => Some((UnaryOp::Range, 2)),
_ => None,
}
}
fn parse_binary_operator(parser: &mut StreamParser) -> Option<(BinaryOp, usize, (usize, usize))> {
parser.trim();
match parser.next() {
Ok('|') if parser.peek_early(1, 1) == Ok("|") => Some((BinaryOp::BoolOr, 2, (8, 9))),
Ok('&') if parser.peek_early(1, 1) == Ok("&") => Some((BinaryOp::BoolAnd, 2, (10, 11))),
Ok('=') if parser.peek_early(1, 1) == Ok("=") => Some((BinaryOp::Equal, 2, (12, 13))),
Ok('!') if parser.peek_early(1, 1) == Ok("=") => Some((BinaryOp::NotEqual, 2, (12, 13))),
Ok('<') if parser.peek_early(1, 1) == Ok("=") => Some((BinaryOp::LessOrEqual, 2, (14, 15))),
Ok('>') if parser.peek_early(1, 1) == Ok("=") => {
Some((BinaryOp::GreaterOrEqual, 2, (14, 15)))
}
Ok('<') => Some((BinaryOp::Less, 1, (14, 15))),
Ok('>') => Some((BinaryOp::Greater, 1, (14, 15))),
Ok('.') if parser.peek_early(1, 1) == Ok(".") && parser.peek_early(1, 2) == Ok("=") => {
Some((BinaryOp::RangeInclusive, 3, (16, 17)))
}
Ok('.') if parser.peek_early(1, 1) == Ok(".") => Some((BinaryOp::Range, 2, (16, 17))),
Ok('+') => Some((BinaryOp::Add, 1, (18, 19))),
Ok('-') => Some((BinaryOp::Subtract, 1, (18, 19))),
Ok('*') => Some((BinaryOp::Multiply, 1, (20, 21))),
Ok('/') => Some((BinaryOp::Divide, 1, (20, 21))),
Ok(';') => Some((BinaryOp::Semicolon, 1, (1, 2))),
Ok('=') => Some((BinaryOp::Definition, 1, (3, 4))),
Ok('|') if parser.peek_early(1, 1) == Ok(">") => Some((BinaryOp::Pipe, 2, (5, 22))),
_ => None,
}
}
fn parse_ternary_operator(parser: &mut StreamParser) -> Option<(TernaryOp, usize, (usize, usize))> {
parser.trim();
match parser.next() {
Ok('?') => Some((TernaryOp::Conditional, 1, (7, 6))),
_ => None,
}
}
fn string<'a>(parser: &mut StreamParser<'a>, file_offset: usize) -> Result<Literal<'a>> {
let index = file_offset + parser.index();
if parser.take_exact("\"").is_ok() {
let mut is_escaped = false;
let result = parser
.take_while(|(_, ch)| {
if is_escaped {
is_escaped = false;
true
} else if ch == '\\' {
is_escaped = true;
true
} else {
ch != '"'
}
})
.to_string();
parser
.take_exact("\"")
.auto_error_offset(file_offset)
.map_err(|e| match *e {
Error::StreamParserError(pochoir_common::Error::UnexpectedEoi) => {
Spanned::new(Error::UnterminatedString)
.with_span(index..index + 1)
.with_file_path(parser.file_path())
}
_ => e,
})?;
Ok(Literal::String(
result.replace("\\\"", "\"").replace("\\\\", "\\"),
))
} else if parser.take_exact("'").is_ok() {
let mut is_escaped = false;
let result = parser
.take_while(|(_, ch)| {
if is_escaped {
is_escaped = false;
true
} else if ch == '\\' {
is_escaped = true;
true
} else {
ch != '\''
}
})
.to_string();
parser
.take_exact("'")
.auto_error_offset(file_offset)
.map_err(|e| match *e {
Error::StreamParserError(pochoir_common::Error::UnexpectedEoi) => {
Spanned::new(Error::UnterminatedString)
.with_span(index..index + 1)
.with_file_path(parser.file_path())
}
_ => e,
})?;
Ok(Literal::String(
result.replace("\\'", "'").replace("\\\\", "\\"),
))
} else {
unreachable!();
}
}
fn number<'a>(parser: &mut StreamParser<'a>) -> Result<Literal<'a>> {
let result = parser.take_while_peekable(|_, ch, next_ch| {
ch.is_numeric() || (ch == '.' && next_ch != Some('.'))
});
Ok(Literal::Number(
result.parse::<f64>().map_err(|_e| Error::InvalidNumber)?,
))
}
fn array<'a>(parser: &mut StreamParser<'a>, file_offset: usize) -> Result<Literal<'a>> {
let index = file_offset + parser.index();
parser.take_exact("[").auto_error_offset(file_offset)?;
let mut values = vec![];
loop {
parser.trim();
if parser.peek_exact("]") {
break;
}
let mut field_values = vec![];
parse_recursive(parser, &mut field_values, 0, file_offset).map_err(|e| match *e {
Error::StreamParserError(pochoir_common::Error::UnexpectedEoi) => {
Spanned::new(Error::UnterminatedArray)
.with_span(index..index + 1)
.with_file_path(parser.file_path())
}
_ => e,
})?;
values.push(field_values);
if parser.take_exact(",").is_err() {
break;
}
}
parser.trim();
parser.take_exact("]").map_err(|_e| {
Spanned::new(Error::UnterminatedArray)
.with_span(index..index + 1)
.with_file_path(parser.file_path())
})?;
Ok(Literal::Array(values))
}
fn object<'a>(parser: &mut StreamParser<'a>, file_offset: usize) -> Result<Literal<'a>> {
let index = file_offset + parser.index();
parser.take_exact("{").auto_error_offset(file_offset)?;
let mut values = IndexMap::new();
loop {
parser.trim();
if parser.peek_exact("}") {
break;
}
let key = if parser.peek_exact("\"") || parser.peek_exact("'") {
if let Literal::String(string) = string(parser, file_offset)? {
string
} else {
unreachable!();
}
} else {
let mut first = true;
parser
.take_while(|(_, ch)| {
if first {
first = false;
ch.is_alphabetic() || ch == '_'
} else {
ch.is_alphanumeric() || ch == '_'
}
})
.trim()
.to_string()
};
parser
.take_exact(":")
.auto_error_offset(file_offset)
.map_err(|e| match *e {
Error::StreamParserError(pochoir_common::Error::UnexpectedEoi) => {
Spanned::new(Error::UnterminatedObject)
.with_span(index..index + 1)
.with_file_path(parser.file_path())
}
_ => e,
})?;
parser.trim();
let mut field_values = vec![];
parse_recursive(parser, &mut field_values, 0, file_offset).map_err(|e| match *e {
Error::StreamParserError(pochoir_common::Error::UnexpectedEoi) => {
Spanned::new(Error::UnterminatedObject)
.with_span(index..index + 1)
.with_file_path(parser.file_path())
}
_ => e,
})?;
values.insert(key, field_values);
if parser.take_exact(",").is_err() {
break;
}
}
parser.trim();
parser.take_exact("}").map_err(|_e| {
Spanned::new(Error::UnterminatedObject)
.with_span(index..index + 1)
.with_file_path(parser.file_path())
})?;
Ok(Literal::Object(values))
}
#[cfg(test)]
mod tests {
use super::*;
use indexmap::indexmap;
use pretty_assertions::assert_eq;
#[test]
fn parse_number() {
assert_eq!(
parse("index.html", "42", 0),
Ok(vec![Spanned::new(Token::Literal(Literal::Number(42.0)))
.with_span(0..2)
.with_file_path("index.html")])
);
}
#[test]
fn mathematical_priority_test() {
assert_eq!(
parse("index.html", "6/ 2 *(2+1) == 9", 0),
Ok(vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Equal,
vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Multiply,
vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Divide,
vec![Spanned::new(Token::Literal(Literal::Number(6.0)))
.with_span(0..1)
.with_file_path("index.html")],
vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
.with_span(3..4)
.with_file_path("index.html")],
))
.with_span(0..4)
.with_file_path("index.html")],
vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Add,
vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
.with_span(7..8)
.with_file_path("index.html")],
vec![Spanned::new(Token::Literal(Literal::Number(1.0)))
.with_span(9..10)
.with_file_path("index.html")],
))
.with_span(7..10)
.with_file_path("index.html")],
))
.with_span(0..11)
.with_file_path("index.html")],
vec![Spanned::new(Token::Literal(Literal::Number(9.0)))
.with_span(15..16)
.with_file_path("index.html")]
))
.with_span(0..16)
.with_file_path("index.html")]),
);
}
#[test]
fn array_indexing() {
assert_eq!(
parse("index.html", "my_arr[1] == 2.0", 0),
Ok(vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Equal,
vec![Spanned::new(Token::Index(
vec![Spanned::new(Token::Literal(Literal::Number(1.0)))
.with_span(7..8)
.with_file_path("index.html")],
Box::new(
Spanned::new(Token::Variable(Cow::Borrowed("my_arr")))
.with_span(0..6)
.with_file_path("index.html")
),
))
.with_span(0..9)
.with_file_path("index.html")],
vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
.with_span(13..16)
.with_file_path("index.html")]
))
.with_span(0..16)
.with_file_path("index.html")]),
);
}
#[test]
fn function_args_newline() {
assert_eq!(
parse(
"index.html",
r#"test(
"a",
42
)"#,
0
),
Ok(vec![Spanned::new(Token::FunctionCall(
Box::new(
Spanned::new(Token::Variable(Cow::Borrowed("test")))
.with_span(0..4)
.with_file_path("index.html")
),
vec![
vec![
Spanned::new(Token::Literal(Literal::String("a".to_string())))
.with_span(10..13)
.with_file_path("index.html")
],
vec![Spanned::new(Token::Literal(Literal::Number(42.0)))
.with_span(19..21)
.with_file_path("index.html")],
]
))
.with_span(0..23)
.with_file_path("index.html")])
);
}
#[test]
fn function_args_trailing_comma() {
assert_eq!(
parse(
"index.html",
r#"test(
"a",
42,
)"#,
0
),
Ok(vec![Spanned::new(Token::FunctionCall(
Box::new(
Spanned::new(Token::Variable(Cow::Borrowed("test")))
.with_span(0..4)
.with_file_path("index.html")
),
vec![
vec![
Spanned::new(Token::Literal(Literal::String("a".to_string())))
.with_span(10..13)
.with_file_path("index.html")
],
vec![Spanned::new(Token::Literal(Literal::Number(42.0)))
.with_span(19..21)
.with_file_path("index.html")],
]
))
.with_span(0..24)
.with_file_path("index.html")])
);
assert_eq!(
parse("index.html", r#"test("a", 42,)"#, 0),
Ok(vec![Spanned::new(Token::FunctionCall(
Box::new(
Spanned::new(Token::Variable(Cow::Borrowed("test")))
.with_span(0..4)
.with_file_path("index.html")
),
vec![
vec![
Spanned::new(Token::Literal(Literal::String("a".to_string())))
.with_span(5..8)
.with_file_path("index.html")
],
vec![Spanned::new(Token::Literal(Literal::Number(42.0)))
.with_span(10..12)
.with_file_path("index.html")],
]
))
.with_span(0..14)
.with_file_path("index.html")])
);
}
#[test]
fn namespaced_function() {
assert_eq!(
parse(
"index.html",
"Restaurants.get('Killer Pizza from Mars').location == 'Oceanside, CA'",
0
),
Ok(vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Equal,
vec![Spanned::new(Token::Index(
vec![
Spanned::new(Token::Literal(Literal::String("location".to_string())))
.with_span(42..50)
.with_file_path("index.html")
],
Box::new(
Spanned::new(Token::FunctionCall(
Box::new(
Spanned::new(Token::Index(
vec![Spanned::new(Token::Literal(Literal::String(
"get".to_string()
)))
.with_span(12..15)
.with_file_path("index.html")],
Box::new(
Spanned::new(Token::Variable(Cow::Borrowed("Restaurants")))
.with_span(0..11)
.with_file_path("index.html")
)
))
.with_span(0..15)
.with_file_path("index.html")
),
vec![vec![Spanned::new(Token::Literal(Literal::String(
"Killer Pizza from Mars".to_string()
)))
.with_span(16..40)
.with_file_path("index.html")]],
))
.with_span(0..41)
.with_file_path("index.html")
)
))
.with_span(0..50)
.with_file_path("index.html")],
vec![
Spanned::new(Token::Literal(Literal::String("Oceanside, CA".to_string())))
.with_span(54..69)
.with_file_path("index.html")
],
))
.with_span(0..69)
.with_file_path("index.html")]),
);
}
#[test]
fn object_newline() {
assert_eq!(
parse(
"index.html",
r#"{
"a": "b",
"c": 42,
}"#,
0
),
Ok(vec![Spanned::new(Token::Literal(Literal::Object(
indexmap! {
String::from("a") => vec![Spanned::new(Token::Literal(Literal::String("b".to_string())))
.with_span(27..30)
.with_file_path("index.html")
],
String::from("c") => vec![Spanned::new(Token::Literal(Literal::Number(42.0)))
.with_span(57..59)
.with_file_path("index.html")
],
}
)))
.with_span(0..78)
.with_file_path("index.html")])
);
}
#[test]
fn array_newline() {
assert_eq!(
parse(
"index.html",
r#"[
"b",
42,
]"#,
0
),
Ok(vec![Spanned::new(Token::Literal(Literal::Array(vec![
vec![
Spanned::new(Token::Literal(Literal::String("b".to_string())))
.with_span(22..25)
.with_file_path("index.html")
],
vec![Spanned::new(Token::Literal(Literal::Number(42.0)))
.with_span(47..49)
.with_file_path("index.html")],
])))
.with_span(0..68)
.with_file_path("index.html")])
);
}
#[test]
#[allow(clippy::too_many_lines)]
fn range() {
assert_eq!(
parse("index.html", "2..3", 0),
Ok(vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Range,
vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
.with_span(0..1)
.with_file_path("index.html")],
vec![Spanned::new(Token::Literal(Literal::Number(3.0)))
.with_span(3..4)
.with_file_path("index.html")],
))
.with_span(0..4)
.with_file_path("index.html")]),
);
assert_eq!(
parse("index.html", "3..2", 0),
Ok(vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Range,
vec![Spanned::new(Token::Literal(Literal::Number(3.0)))
.with_span(0..1)
.with_file_path("index.html")],
vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
.with_span(3..4)
.with_file_path("index.html")],
))
.with_span(0..4)
.with_file_path("index.html")]),
);
assert_eq!(
parse("index.html", "..3", 0),
Ok(vec![Spanned::new(Token::UnaryOperator(
UnaryOp::Range,
vec![Spanned::new(Token::Literal(Literal::Number(3.0)))
.with_span(2..3)
.with_file_path("index.html")],
))
.with_span(0..3)
.with_file_path("index.html")]),
);
assert_eq!(
parse("index.html", "2..", 0),
Ok(vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Range,
vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
.with_span(0..1)
.with_file_path("index.html")],
vec![],
))
.with_span(0..3)
.with_file_path("index.html")]),
);
assert_eq!(
parse("index.html", "1 + 2..4", 0),
Ok(vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Range,
vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Add,
vec![Spanned::new(Token::Literal(Literal::Number(1.0)))
.with_span(0..1)
.with_file_path("index.html")],
vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
.with_span(4..5)
.with_file_path("index.html")],
))
.with_span(0..5)
.with_file_path("index.html")],
vec![Spanned::new(Token::Literal(Literal::Number(4.0)))
.with_span(7..8)
.with_file_path("index.html")],
))
.with_span(0..8)
.with_file_path("index.html")]),
);
assert_eq!(
parse("index.html", "1..1 + 1", 0),
Ok(vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Range,
vec![Spanned::new(Token::Literal(Literal::Number(1.0)))
.with_span(0..1)
.with_file_path("index.html")],
vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Add,
vec![Spanned::new(Token::Literal(Literal::Number(1.0)))
.with_span(3..4)
.with_file_path("index.html")],
vec![Spanned::new(Token::Literal(Literal::Number(1.0)))
.with_span(7..8)
.with_file_path("index.html")],
))
.with_span(3..8)
.with_file_path("index.html")],
))
.with_span(0..8)
.with_file_path("index.html")]),
);
assert_eq!(
parse("index.html", r#"len("a")..3"#, 0),
Ok(vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Range,
vec![Spanned::new(Token::FunctionCall(
Box::new(
Spanned::new(Token::Variable(Cow::Borrowed("len")))
.with_span(0..3)
.with_file_path("index.html")
),
vec![vec![Spanned::new(Token::Literal(Literal::String(
"a".to_string()
)))
.with_span(4..7)
.with_file_path("index.html")]]
))
.with_span(0..8)
.with_file_path("index.html")],
vec![Spanned::new(Token::Literal(Literal::Number(3.0)))
.with_span(10..11)
.with_file_path("index.html")],
))
.with_span(0..11)
.with_file_path("index.html")]),
);
assert_eq!(
parse("index.html", r#""a"..3"#, 0),
Ok(vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Range,
vec![
Spanned::new(Token::Literal(Literal::String("a".to_string())))
.with_span(0..3)
.with_file_path("index.html")
],
vec![Spanned::new(Token::Literal(Literal::Number(3.0)))
.with_span(5..6)
.with_file_path("index.html")],
))
.with_span(0..6)
.with_file_path("index.html")]),
);
assert_eq!(
parse("index.html", "1..3[2]", 0),
Ok(vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Range,
vec![Spanned::new(Token::Literal(Literal::Number(1.0)))
.with_span(0..1)
.with_file_path("index.html")],
vec![Spanned::new(Token::Index(
vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
.with_span(5..6)
.with_file_path("index.html"),],
Box::new(
Spanned::new(Token::Literal(Literal::Number(3.0)))
.with_span(3..4)
.with_file_path("index.html")
)
))
.with_span(3..7)
.with_file_path("index.html")]
))
.with_span(0..7)
.with_file_path("index.html")]),
);
assert_eq!(
parse("index.html", "1..3.2", 0),
Ok(vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Range,
vec![Spanned::new(Token::Literal(Literal::Number(1.0)))
.with_span(0..1)
.with_file_path("index.html")],
vec![Spanned::new(Token::Literal(Literal::Number(3.2)))
.with_span(3..6)
.with_file_path("index.html")],
))
.with_span(0..6)
.with_file_path("index.html")]),
);
assert_eq!(
parse("index.html", "0..2 == ..=1", 0),
Ok(vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Equal,
vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Range,
vec![Spanned::new(Token::Literal(Literal::Number(0.0)))
.with_span(0..1)
.with_file_path("index.html")],
vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
.with_span(3..4)
.with_file_path("index.html")],
))
.with_span(0..4)
.with_file_path("index.html")],
vec![Spanned::new(Token::UnaryOperator(
UnaryOp::RangeInclusive,
vec![Spanned::new(Token::Literal(Literal::Number(1.0)))
.with_span(11..12)
.with_file_path("index.html")],
))
.with_span(8..12)
.with_file_path("index.html")],
))
.with_span(0..12)
.with_file_path("index.html")]),
);
assert_eq!(
parse("index.html", "my_fn(2..)", 0),
Ok(vec![Spanned::new(Token::FunctionCall(
Box::new(
Spanned::new(Token::Variable(Cow::Borrowed("my_fn")))
.with_span(0..5)
.with_file_path("index.html")
),
vec![vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Range,
vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
.with_span(6..7)
.with_file_path("index.html")],
vec![],
))
.with_span(6..9)
.with_file_path("index.html")]],
))
.with_span(0..10)
.with_file_path("index.html")]),
);
assert_eq!(
parse("index.html", r#""hello"[2..=4] == "ll""#, 0),
Ok(vec![Spanned::new(Token::BinaryOperator(
BinaryOp::Equal,
vec![Spanned::new(Token::Index(
vec![Spanned::new(Token::BinaryOperator(
BinaryOp::RangeInclusive,
vec![Spanned::new(Token::Literal(Literal::Number(2.0)))
.with_span(8..9)
.with_file_path("index.html")],
vec![Spanned::new(Token::Literal(Literal::Number(4.0)))
.with_span(12..13)
.with_file_path("index.html")],
))
.with_span(8..13)
.with_file_path("index.html")],
Box::new(
Spanned::new(Token::Literal(Literal::String("hello".to_string())))
.with_span(0..7)
.with_file_path("index.html")
)
))
.with_span(0..14)
.with_file_path("index.html")],
vec![
Spanned::new(Token::Literal(Literal::String("ll".to_string())))
.with_span(18..22)
.with_file_path("index.html")
],
))
.with_span(0..22)
.with_file_path("index.html")]),
);
}
}