use crate::{common, ParseError};
use token::*;
use winnow::{
ascii::multispace0,
combinator::{delimited, repeat},
prelude::*,
stream::{AsChar, Compare, FindSlice, SliceLen, Stream, StreamIsPartial},
};
pub use common::lexer::*;
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Token<T> {
BlockOrSetOpen,
BlockOrSetClose,
Identifier(T),
Separator,
Value(Value<T>),
Comment(T),
TupleOpen,
TupleClose,
ElementSeparator,
}
impl<T> From<GenericToken<T>> for Token<T> {
fn from(value: GenericToken<T>) -> Self {
match value {
GenericToken::Identifier(i) => Self::Identifier(i),
GenericToken::Value(v) => Self::Value(v),
GenericToken::Comment(c) => Self::Comment(c),
GenericToken::TupleOpen => Self::TupleOpen,
GenericToken::TupleClose => Self::TupleClose,
GenericToken::SetOpen => Self::BlockOrSetOpen,
GenericToken::SetClose => Self::BlockOrSetClose,
GenericToken::ElementSeparator => Self::ElementSeparator,
}
}
}
impl<T> From<Token<T>> for Option<GenericToken<T>> {
fn from(value: Token<T>) -> Self {
match value {
Token::Identifier(i) => Some(GenericToken::Identifier(i)),
Token::Value(v) => Some(GenericToken::Value(v)),
Token::Comment(c) => Some(GenericToken::Comment(c)),
Token::TupleOpen => Some(GenericToken::TupleOpen),
Token::TupleClose => Some(GenericToken::TupleClose),
Token::BlockOrSetOpen => Some(GenericToken::SetOpen),
Token::BlockOrSetClose => Some(GenericToken::SetClose),
Token::ElementSeparator => Some(GenericToken::ElementSeparator),
_ => None,
}
}
}
pub fn lex<
I: Stream
+ StreamIsPartial
+ Compare<&'static str>
+ FindSlice<&'static str>
+ FindSlice<(char, char)>
+ Copy,
>(
input: I,
) -> Result<Vec<Token<<I as Stream>::Slice>>, ParseError<I>>
where
<I as Stream>::Token: AsChar + Clone,
I::Slice: SliceLen,
{
repeat(0.., delimited(multispace0, token, multispace0)).parse(input)
}
pub mod token {
use super::*;
use winnow::{
combinator::alt,
stream::{AsChar, Compare, FindSlice, SliceLen},
};
pub use common::lexer::token::*;
pub fn block_or_set_open<I: Stream + StreamIsPartial + Compare<&'static str>>(
input: &mut I,
) -> ModalResult<Token<<I as Stream>::Slice>> {
"{".map(|_| Token::BlockOrSetOpen).parse_next(input)
}
pub fn block_or_set_close<I: Stream + StreamIsPartial + Compare<&'static str>>(
input: &mut I,
) -> ModalResult<Token<<I as Stream>::Slice>> {
"}".map(|_| Token::BlockOrSetClose).parse_next(input)
}
pub fn separator<I: Stream + StreamIsPartial + Compare<&'static str>>(
input: &mut I,
) -> ModalResult<Token<<I as Stream>::Slice>> {
":".map(|_| Token::Separator).parse_next(input)
}
pub fn token<
I: Stream
+ StreamIsPartial
+ Compare<&'static str>
+ FindSlice<&'static str>
+ FindSlice<(char, char)>
+ Copy,
>(
input: &mut I,
) -> ModalResult<Token<<I as Stream>::Slice>>
where
<I as Stream>::Token: AsChar + Clone,
I::Slice: SliceLen,
{
alt((
block_or_set_open,
block_or_set_close,
separator,
tuple_open,
tuple_close,
element_separator,
comment,
value,
identifier,
))
.parse_next(input)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::test_utils::byte_offset_to_line_column;
#[test]
pub fn simple_example() {
let input = r#"
property1: "some string"
property2: 1.2
^regex$ /* comment */ : identifier // other comment
block {
named_block "name" {
prop: #c01032
42: true
}
// }
}
"#;
let expected = vec![
Token::Identifier("property1"),
Token::Separator,
Token::Value(Value::String("some string")),
Token::Identifier("property2"),
Token::Separator,
Token::Value(Value::Number("1.2")),
Token::Value(Value::Regex("^regex$")),
Token::Comment(" comment "),
Token::Separator,
Token::Identifier("identifier"),
Token::Comment(" other comment"),
Token::Identifier("block"),
Token::BlockOrSetOpen,
Token::Identifier("named_block"),
Token::Value(Value::String("name")),
Token::BlockOrSetOpen,
Token::Identifier("prop"),
Token::Separator,
Token::Value(Value::Color("c01032")),
Token::Value(Value::Number("42")),
Token::Separator,
Token::Value(Value::Boolean("true")),
Token::BlockOrSetClose,
Token::Comment(" }"),
Token::BlockOrSetClose,
];
let lexed = lex(input).expect("Failed to lex");
assert_eq!(lexed, expected);
}
#[test]
fn error_location_byte_offset_conversion() {
let test_cases = vec![
("property: value", 0, 1, 1), ("property: value", 8, 1, 9), ("property: value", 10, 1, 11), ("name: \"test\"\ncolor: blue", 13, 2, 1), ("name: \"test\"\ncolor: blue", 19, 2, 7), ("a: b\nc: d\ne: f", 5, 2, 1), ("a: b\nc: d\ne: f", 10, 3, 1), ];
for (text, offset, expected_line, expected_column) in test_cases {
let (line, column) = byte_offset_to_line_column(text, offset);
assert_eq!(
(line, column),
(expected_line, expected_column),
"Failed for text '{}' at offset {}: expected line {}, column {}, got line {}, column {}",
text.replace('\n', "\\n"), offset, expected_line, expected_column, line, column
);
}
}
#[test]
fn error_location_config_unicode() {
let test_cases = vec![
("name: \"🚀 test\"", 0, 1, 1), ("name: \"🚀 test\"", 7, 1, 8), ("name: \"🚀 test\"", 11, 1, 9), ("🚀: value\n🔬: data", 12, 2, 1), ];
for (text, offset, expected_line, expected_column) in test_cases {
let (line, column) = byte_offset_to_line_column(text, offset);
assert_eq!(
(line, column),
(expected_line, expected_column),
"Failed for text '{text}' at offset {offset}: expected line {expected_line}, column {expected_column}, got line {line}, column {column}",
);
}
}
#[test]
fn error_location_calculation_with_tabs() {
let test_cases = vec![
("line1\n\tindented", 6, (2, 1)), ("line1\n\tindented", 7, (2, 2)), ("no_tabs", 3, (1, 4)), ("tab\there", 4, (1, 5)), ];
for (input, offset, expected) in test_cases {
let result = byte_offset_to_line_column(input, offset);
assert_eq!(
result,
expected,
"Failed for input '{}' at offset {}: expected {:?}, got {:?}",
input.replace('\n', "\\n").replace('\t', "\\t"),
offset,
expected,
result
);
}
}
#[test]
fn error_location_edge_cases() {
let result = byte_offset_to_line_column("", 0);
assert_eq!(result, (1, 1));
let result = byte_offset_to_line_column("a", 0);
assert_eq!(result, (1, 1));
let result = byte_offset_to_line_column("\n\n\n", 2);
assert_eq!(result, (3, 1));
let input = "hello\nworld";
let result = byte_offset_to_line_column(input, input.len());
assert_eq!(result, (2, 6)); }
}