use std::error::Error;
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub enum Token {
Pipeline,
Class, Id, Tag, Attr,
Text, Src, Href, Pound,
Function(String), Comma,
Colon,
String(String),
Float(f64),
Number(usize),
Bool(bool),
Nil,
Dot,
DotDot,
Minus,
Tilde,
LeftParen,
RightParen,
LeftBracket,
RightBracket,
Union,
Intersection,
Difference,
EOF,
}
impl fmt::Display for Token {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Token::Pipeline => write!(f, ">"),
Token::Class => write!(f, "class"),
Token::Id => write!(f, "id"),
Token::Tag => write!(f, "tag"),
Token::Attr => write!(f, "attr"),
Token::Text => write!(f, "text"),
Token::Src => write!(f, "src"),
Token::Href => write!(f, "href"),
Token::Tilde => write!(f, "~"),
Token::Function(func) => write!(f, "@{}", func),
Token::Comma => write!(f, ","),
Token::Colon => write!(f, ":"),
Token::Number(n) => write!(f, "{}", n),
Token::String(s) => write!(f, "{}", s),
Token::Float(n) => write!(f, "{}", n),
Token::Bool(b) => write!(f, "{}", b),
Token::Minus => write!(f, "-"),
Token::LeftParen => write!(f, "("),
Token::RightParen => write!(f, ")"),
Token::LeftBracket => write!(f, "["),
Token::RightBracket => write!(f, "]"),
Token::Union => write!(f, "|"),
Token::Intersection => write!(f, "&"),
Token::Difference => write!(f, "^"),
Token::Dot => write!(f, "."),
Token::DotDot => write!(f, ".."),
Token::Nil => write!(f, "nil"),
Token::EOF => write!(f, "EOF"),
Token::Pound => write!(f, "#"),
}
}
}
#[derive(Debug)]
pub struct LexerError {
pub message: String,
pub line: usize,
pub column: usize,
}
impl fmt::Display for LexerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Lexical error(line {}, column {}): {}",
self.line, self.column, self.message
)
}
}
impl Error for LexerError {}
pub struct Lexer {
chars: Vec<char>,
position: usize,
read_position: usize,
current_char: Option<char>,
line: usize,
column: usize,
}
impl Lexer {
pub fn new(input: &str) -> Self {
let estimated_capacity = input.len() + 1;
let mut chars = Vec::with_capacity(estimated_capacity);
chars.extend(input.chars());
let mut lexer = Lexer {
chars,
position: 0,
read_position: 0,
current_char: None,
line: 1,
column: 0,
};
lexer.read_char();
lexer
}
fn read_char(&mut self) {
if self.read_position >= self.chars.len() {
self.current_char = None;
} else {
self.current_char = Some(self.chars[self.read_position]);
}
self.position = self.read_position;
self.read_position += 1;
if let Some('\n') = self.current_char {
self.line += 1;
self.column = 0;
} else {
self.column += 1;
}
}
fn skip_whitespace(&mut self) {
while let Some(c) = self.current_char {
if c.is_whitespace() {
self.read_char();
} else {
break;
}
}
}
fn is_identifier_start(&self, c: char) -> bool {
c.is_alphabetic() || c == '_' || Self::is_unicode_identifier_part(c)
}
fn is_identifier_part(&self, c: char) -> bool {
c.is_alphanumeric() || c == '_' || Self::is_unicode_identifier_part(c)
}
fn is_unicode_identifier_part(c: char) -> bool {
(c >= '\u{4E00}' && c <= '\u{9FFF}')
|| (c >= '\u{3040}' && c <= '\u{309F}')
|| (c >= '\u{30A0}' && c <= '\u{30FF}')
|| (c >= '\u{AC00}' && c <= '\u{D7AF}')
|| (c >= '\u{1F600}' && c <= '\u{1F64F}')
|| (c >= '\u{1F300}' && c <= '\u{1F5FF}')
|| (c >= '\u{1F680}' && c <= '\u{1F6FF}')
|| (c >= '\u{2600}' && c <= '\u{26FF}')
}
fn is_function_name_start(&self, c: char) -> bool {
c.is_ascii_alphabetic()
}
fn is_function_name_part(&self, c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
pub fn next_token(&mut self) -> Result<Token, LexerError> {
self.skip_whitespace();
if self.current_char.is_none() {
return Ok(Token::EOF);
}
match self.current_char.unwrap() {
'>' => {
self.read_char();
Ok(Token::Pipeline)
}
',' => {
self.read_char();
Ok(Token::Comma)
}
':' => {
self.read_char();
Ok(Token::Colon)
}
'|' => {
self.read_char();
Ok(Token::Union)
}
'^' => {
self.read_char();
Ok(Token::Difference)
}
'&' => {
self.read_char();
Ok(Token::Intersection)
}
'@' => self.read_function(),
'"' => self.read_quoted_string(),
'~' => {
self.read_char();
Ok(Token::Tilde)
}
'(' => {
self.read_char();
Ok(Token::LeftParen)
}
')' => {
self.read_char();
Ok(Token::RightParen)
}
'[' => {
self.read_char();
Ok(Token::LeftBracket)
}
']' => {
self.read_char();
Ok(Token::RightBracket)
}
'.' => {
if self.chars[self.read_position].is_ascii_digit() {
self.read_number(true)
} else {
self.read_char();
Ok(Token::Dot)
}
}
'-' => {
self.read_char();
Ok(Token::Minus)
}
'0'..='9' => self.read_number(false),
'#' => {
self.read_char();
Ok(Token::Pound)
}
_ => self.read_string(),
}
}
fn read_number(&mut self, has_dot: bool) -> Result<Token, LexerError> {
let start_position = self.position;
let mut has_dot_ = has_dot;
let mut end = 0;
if has_dot_ {
self.read_char();
}
while let Some(c) = self.current_char {
if c.is_ascii_digit() {
self.read_char();
end = self.position;
} else if c == '.' {
if !has_dot_ {
has_dot_ = true;
self.read_char();
end = self.position;
} else {
self.read_char();
while let Some(c1) = self.current_char {
if c1.is_ascii_digit() {
self.read_char();
} else {
break;
}
}
}
} else {
break;
}
}
if has_dot_ {
let number_str: String = self.chars[start_position..end].iter().collect();
match number_str.parse::<f64>() {
Ok(float) => Ok(Token::Float(float)),
Err(_) => Err(LexerError {
message: format!("Unable to resolve the float: {}", number_str),
line: self.line,
column: self.column,
}),
}
} else {
let number_str: String = self.chars[start_position..self.position].iter().collect();
match number_str.parse::<usize>() {
Ok(number) => Ok(Token::Number(number)),
Err(_) => Err(LexerError {
message: format!("Unable to resolve the number: {}", number_str),
line: self.line,
column: self.column,
}),
}
}
}
#[deprecated(note = "Keyword analysis has been added to the function for reading argument.")]
#[allow(dead_code)]
fn read_identifier(&mut self) -> Result<Token, LexerError> {
let start_position = self.position;
while let Some(c) = self.current_char {
if self.is_identifier_part(c) {
self.read_char();
} else {
break;
}
}
let identifier: String = self.chars[start_position..self.position].iter().collect();
match identifier.as_str() {
"class" => Ok(Token::Class),
"id" => Ok(Token::Id),
"tag" => Ok(Token::Tag),
"attr" => Ok(Token::Attr),
"text" => Ok(Token::Text),
"src" => Ok(Token::Src),
"href" => Ok(Token::Href),
_ => Err(LexerError {
message: "Illegal identifier".to_string(),
line: self.line,
column: self.column,
}),
}
}
fn read_function(&mut self) -> Result<Token, LexerError> {
self.read_char();
let start_position = self.position;
if let Some(c) = self.current_char {
if !self.is_function_name_start(c) {
return Err(LexerError {
message: "Function names must start with a letter.".to_string(),
line: self.line,
column: self.column,
});
}
} else {
return Err(LexerError {
message: "Function name cannot be empty.".to_string(),
line: self.line,
column: self.column,
});
}
while let Some(c) = self.current_char {
if self.is_function_name_part(c) {
self.read_char();
} else {
break;
}
}
let function_name: String = self.chars[start_position..self.position].iter().collect();
Ok(Token::Function(function_name))
}
fn read_quoted_string(&mut self) -> Result<Token, LexerError> {
self.read_char();
let mut value = String::new();
let mut escaped = false;
while let Some(c) = self.current_char {
if escaped {
match c {
'"' => value.push('"'),
'\\' => value.push('\\'),
'n' => value.push('\n'),
't' => value.push('\t'),
'r' => value.push('\r'),
'u' => {
let mut unicode_value = String::new();
for _ in 0..4 {
self.read_char();
if let Some(hex_char) = self.current_char {
if hex_char.is_ascii_hexdigit() {
unicode_value.push(hex_char);
} else {
return Err(LexerError {
message: format!(
"Invalid Unicode escape sequence: \\u{}",
unicode_value
),
line: self.line,
column: self.column,
});
}
} else {
return Err(LexerError {
message: "Unfinished Unicode escape sequence.".to_string(),
line: self.line,
column: self.column,
});
}
}
if let Ok(code_point) = u32::from_str_radix(&unicode_value, 16) {
if let Some(unicode_char) = std::char::from_u32(code_point) {
value.push(unicode_char);
} else {
return Err(LexerError {
message: format!(
"Invalid Unicode code point: U+{}",
unicode_value
),
line: self.line,
column: self.column,
});
}
} else {
return Err(LexerError {
message: format!(
"Unable to resolve Unicode escape sequence: \\u{}",
unicode_value
),
line: self.line,
column: self.column,
});
}
}
_ => value.push(c),
}
escaped = false;
self.read_char();
} else if c == '\\' {
escaped = true;
self.read_char();
} else if c == '"' {
self.read_char();
return Ok(Token::String(value));
} else {
value.push(c);
self.read_char();
}
}
Err(LexerError {
message: "Unterminated string.".to_string(),
line: self.line,
column: self.column,
})
}
fn read_string(&mut self) -> Result<Token, LexerError> {
let start_position = self.position;
while let Some(c) = self.current_char {
if c.is_whitespace() || c == '>' || c == ',' || c == '"' || c == '@' || c == ':' {
break;
}
self.read_char();
}
let argument: String = self.chars[start_position..self.position].iter().collect();
if argument.is_empty() {
return Err(LexerError {
message: format!("Unrecognized characters: {:?}", self.current_char),
line: self.line,
column: self.column,
});
}
match argument.as_str() {
"class" => Ok(Token::Class),
"id" => Ok(Token::Id),
"tag" => Ok(Token::Tag),
"attr" => Ok(Token::Attr),
"text" => Ok(Token::Text),
"src" => Ok(Token::Src),
"href" => Ok(Token::Href),
"true" => Ok(Token::Bool(true)),
"false" => Ok(Token::Bool(false)),
"nil" => Ok(Token::Nil),
_ => Ok(Token::String(argument)),
}
}
fn recover_from_error(&mut self) {
while let Some(c) = self.current_char {
if c == '>' || c == ',' || c == '"' || c == '@' || self.is_identifier_start(c) {
break;
}
self.read_char();
}
}
}
pub fn tokenize(input: &str) -> Vec<(Token, usize, usize)> {
let mut lexer = Lexer::new(input);
let estimated_tokens = (input.len() / 4).max(8);
let mut tokens_with_pos = Vec::with_capacity(estimated_tokens);
loop {
let line = lexer.line;
let column = lexer.column;
match lexer.next_token() {
Ok(Token::EOF) => {
tokens_with_pos.push((Token::EOF, line, column));
break;
}
Ok(token) => tokens_with_pos.push((token, line, column)),
Err(_) => lexer.recover_from_error(),
}
}
tokens_with_pos
}