use std::ops::Range;
use logos::{Lexer, Logos};
use crate::parse::ParseError;
use crate::quantity::Calendar;
#[derive(Logos, Debug, Clone, PartialEq, Eq)]
#[logos(skip r"[ \t\r\n]+")]
#[logos(skip(r"//[^\r\n]*", allow_greedy = true))]
#[logos(skip r"/\*([^*]|\*+[^*/])*\*+/")]
pub(crate) enum Token {
#[token(".")]
Dot,
#[token("[")]
OpenBracket,
#[token("]")]
CloseBracket,
#[token("(")]
OpenParen,
#[token(")")]
CloseParen,
#[token("{")]
OpenBrace,
#[token("}")]
CloseBrace,
#[token(",")]
Comma,
#[token("+")]
Plus,
#[token("-")]
Minus,
#[token("*")]
Star,
#[token("/")]
Slash,
#[token("&")]
Ampersand,
#[token("|")]
Pipe,
#[token("=")]
Equal,
#[token("~")]
Tilde,
#[token("!=")]
NotEqual,
#[token("!~")]
NotTilde,
#[token("<")]
Less,
#[token("<=")]
LessOrEqual,
#[token(">")]
Greater,
#[token(">=")]
GreaterOrEqual,
#[token("%")]
Percent,
#[token("div")]
Div,
#[token("mod")]
Mod,
#[token("and")]
And,
#[token("or")]
Or,
#[token("xor")]
Xor,
#[token("implies")]
Implies,
#[token("is")]
Is,
#[token("as")]
As,
#[token("in")]
In,
#[token("contains")]
Contains,
#[token("true")]
True,
#[token("false")]
False,
#[token("$this")]
This,
#[token("$index")]
Index,
#[token("$total")]
Total,
#[regex(
"years?|months?|weeks?|days?|hours?|minutes?|seconds?|milliseconds?",
|lexer| Calendar::from_keyword(lexer.slice())
)]
Calendar(Calendar),
#[regex("[A-Za-z_][A-Za-z0-9_]*", |lexer| String::from(lexer.slice()))]
Identifier(String),
#[regex(r"`([^`\\]|\\[\x00-\u{10FFFF}])*`", |lexer| unescape(lexer.slice()))]
Delimited(String),
#[regex(r"'([^'\\]|\\[\x00-\u{10FFFF}])*'", |lexer| unescape(lexer.slice()))]
Text(String),
#[regex("[0-9]", number)]
Number(String),
#[token("@", temporal)]
Temporal(String),
}
fn unescape(slice: &str) -> String {
let mut chars = slice.chars();
chars.next();
chars.next_back();
let mut text = String::with_capacity(slice.len());
while let Some(c) = chars.next() {
if c != '\\' {
text.push(c);
continue;
}
match chars.next() {
Some('r') => text.push('\r'),
Some('n') => text.push('\n'),
Some('t') => text.push('\t'),
Some('f') => text.push('\u{c}'),
Some('u') => {
let hex: String = chars.clone().take(4).collect();
match (hex.len() == 4)
.then(|| u32::from_str_radix(&hex, 16).ok())
.flatten()
.and_then(char::from_u32)
{
Some(decoded) => {
text.push(decoded);
for _ in 0..4 {
chars.next();
}
}
None => text.push('u'),
}
}
Some(other) => text.push(other),
None => {}
}
}
text
}
fn number(lexer: &mut Lexer<'_, Token>) -> String {
let rest = lexer.remainder().as_bytes();
let mut length = digits(rest, 0);
if rest.get(length) == Some(&b'.')
&& rest
.get(length.saturating_add(1))
.is_some_and(u8::is_ascii_digit)
{
length = digits(rest, length.saturating_add(1));
}
lexer.bump(length);
String::from(lexer.slice())
}
fn digits(bytes: &[u8], from: usize) -> usize {
let mut at = from;
while bytes.get(at).is_some_and(u8::is_ascii_digit) {
at = at.saturating_add(1);
}
at
}
fn temporal(lexer: &mut Lexer<'_, Token>) -> Option<String> {
let rest = lexer.remainder().as_bytes();
let length = if rest.first() == Some(&b'T') {
time_format(rest, 1)?
} else {
let date = date_format(rest)?;
if rest.get(date) == Some(&b'T') {
let after = date.saturating_add(1);
match time_format(rest, after) {
Some(time) => offset(rest, time),
None => after,
}
} else {
date
}
};
lexer.bump(length);
lexer.slice().strip_prefix('@').map(String::from)
}
fn fixed(bytes: &[u8], at: usize, n: usize) -> Option<usize> {
let end = at.checked_add(n)?;
bytes
.get(at..end)
.filter(|run| run.iter().all(u8::is_ascii_digit))
.map(|_| end)
}
fn dashed(bytes: &[u8], at: usize, separator: u8) -> Option<usize> {
(bytes.get(at) == Some(&separator))
.then(|| fixed(bytes, at.saturating_add(1), 2))
.flatten()
}
fn date_format(bytes: &[u8]) -> Option<usize> {
let year = fixed(bytes, 0, 4)?;
let Some(month) = dashed(bytes, year, b'-') else {
return Some(year);
};
Some(dashed(bytes, month, b'-').unwrap_or(month))
}
fn time_format(bytes: &[u8], at: usize) -> Option<usize> {
let hour = fixed(bytes, at, 2)?;
let Some(minute) = dashed(bytes, hour, b':') else {
return Some(hour);
};
let Some(second) = dashed(bytes, minute, b':') else {
return Some(minute);
};
if bytes.get(second) == Some(&b'.')
&& bytes
.get(second.saturating_add(1))
.is_some_and(u8::is_ascii_digit)
{
return Some(digits(bytes, second.saturating_add(1)));
}
Some(second)
}
fn offset(bytes: &[u8], at: usize) -> usize {
match bytes.get(at) {
Some(b'Z') => at.saturating_add(1),
Some(b'+' | b'-') => fixed(bytes, at.saturating_add(1), 2)
.and_then(|hours| dashed(bytes, hours, b':'))
.unwrap_or(at),
_ => at,
}
}
pub(crate) fn lex(text: &str) -> Result<Vec<(Token, Range<usize>)>, ParseError> {
let mut tokens = Vec::new();
let mut lexer = Token::lexer(text);
while let Some(token) = lexer.next() {
match token {
Ok(token) => tokens.push((token, lexer.span())),
Err(()) => {
return Err(ParseError::Lex {
offset: lexer.span().start,
text: String::from(lexer.slice()),
});
}
}
}
Ok(tokens)
}
#[cfg(test)]
mod tests {
use super::{Token, lex};
use crate::quantity::Calendar;
fn tokens(text: &str) -> Vec<Token> {
lex(text)
.expect("the text should lex")
.into_iter()
.map(|(token, _)| token)
.collect()
}
#[test]
fn an_integer_before_an_invocation_stops_at_the_point() {
assert_eq!(
tokens("1.toString()"),
vec![
Token::Number(String::from("1")),
Token::Dot,
Token::Identifier(String::from("toString")),
Token::OpenParen,
Token::CloseParen,
]
);
}
#[test]
fn a_decimal_keeps_its_fraction() {
assert_eq!(tokens("1.50"), vec![Token::Number(String::from("1.50"))]);
}
#[test]
fn a_datetime_stops_before_an_invocation() {
assert_eq!(
tokens("@2015-02-04T14:34:28.123.is"),
vec![
Token::Temporal(String::from("2015-02-04T14:34:28.123")),
Token::Dot,
Token::Is,
]
);
assert_eq!(
tokens("@2015T"),
vec![Token::Temporal(String::from("2015T"))]
);
assert_eq!(
tokens("@2017-11-05T01:30:00.0-04:00"),
vec![Token::Temporal(String::from("2017-11-05T01:30:00.0-04:00"))]
);
}
#[test]
fn a_time_takes_no_offset() {
assert_eq!(
tokens("@T14:34:28Z"),
vec![
Token::Temporal(String::from("T14:34:28")),
Token::Identifier(String::from("Z")),
]
);
}
#[test]
fn escapes_resolve_and_unknown_ones_keep_their_character() {
assert_eq!(
tokens(r#"'\\\/\f\r\n\t\"\`\'*\p'"#),
vec![Token::Text(String::from("\\/\u{c}\r\n\t\"`'*p"))]
);
}
#[test]
fn comments_are_skipped_and_an_open_one_is_left_as_operators() {
assert_eq!(
tokens("2 /* x */ // y\n"),
vec![Token::Number(String::from("2"))]
);
assert_eq!(
tokens("2 /* open")[..3],
[Token::Number(String::from("2")), Token::Slash, Token::Star]
);
assert!(crate::parse::parse("2 /* open").is_err());
}
#[test]
fn keywords_win_over_identifiers_of_the_same_length() {
assert_eq!(
tokens("div days divs"),
vec![
Token::Div,
Token::Calendar(Calendar::Day),
Token::Identifier(String::from("divs")),
]
);
}
}