use crate::tokens::{Input, Token, TokenKind};
use crate::utils::is_cfm_mode;
use detached_str::StrSlice;
use nom::{IResult, branch::alt, error::ParseError};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
struct NotFoundError;
const NOT_FOUND: nom::Err<NotFoundError> = nom::Err::Error(NotFoundError);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Diagnostic {
Valid,
InvalidNumber(StrSlice),
IllegalChar(StrSlice),
NotTokenized(StrSlice),
UnterminatedString(StrSlice),
}
impl<I> ParseError<I> for NotFoundError {
fn from_error_kind(_: I, _: nom::error::ErrorKind) -> Self {
NotFoundError
}
fn append(_: I, _: nom::error::ErrorKind, other: Self) -> Self {
other
}
}
type TokenizationResult<'a, T = StrSlice> = IResult<Input<'a>, T, NotFoundError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Ctx {
Number, Letter, Start, Space, Word, Open, }
impl Ctx {
#[inline]
fn after_token(token: &Token, original: &str) -> Self {
let last_char = token.range.to_str(original).chars().next_back();
match token.kind {
TokenKind::Whitespace | TokenKind::Comment => Ctx::Space,
TokenKind::LineBreak => Ctx::Start,
TokenKind::IntegerLiteral | TokenKind::FloatLiteral => Ctx::Number,
_ => match last_char {
Some(c) if c.is_ascii_alphanumeric() => Ctx::Letter,
Some(')' | ']' | '}' | '\'' | '"' | '`' | '_') => Ctx::Word,
Some('(' | '[' | '{' | '|') => Ctx::Start,
_ => Ctx::Open,
},
}
}
}
fn parse_token_dispatch(
input: Input<'_>,
ctx: Ctx,
last_ctx: Ctx,
is_cfm: bool,
) -> TokenizationResult<'_, (Token, Diagnostic)> {
let first = match input.chars().next() {
Some(c) => c,
None => return Err(NOT_FOUND),
};
macro_rules! m {
($p:expr, $k:expr) => {
map_valid_token($p, $k)(input)
};
}
match first {
' ' | '\t' | '\0' => m!(whitespace, TokenKind::Whitespace),
';' => m!(punctuation_tag(";"), TokenKind::LineBreak),
'\r' => alt((
map_valid_token(punctuation_tag("\r\n"), TokenKind::LineBreak), map_valid_token(punctuation_tag("\r"), TokenKind::Whitespace), ))(input),
'\n' => m!(punctuation_tag("\n"), TokenKind::LineBreak),
'\\' => {
if let Ok(r) = m!(line_continuation, TokenKind::Whitespace) {
return Ok(r);
}
m!(punctuation_tag("\\"), TokenKind::Symbol)
}
'#' => m!(comment, TokenKind::Comment),
'"' | '\'' | '`' => string_literal(input),
'.' => dot_dispatch(input, ctx),
'-' => minus_dispatch(input, ctx, is_cfm),
'(' | ')' | '[' | ']' | '{' | '}' | ',' => paren_dispatch(input, ctx, first),
'%' => percent_dispatch(input, ctx),
'!' => bang_dispatch(input, ctx),
'?' => question_dispatch(input, ctx),
'$' => alt((
map_valid_token(prefix_tag("$"), TokenKind::OperatorPrefix), map_valid_token(punctuation_tag("$"), TokenKind::Symbol), ))(input),
'^' => circum_dispatch(input, ctx),
'&' => and_dispatch(input, ctx),
'|' => m!(pipe_parser, TokenKind::Operator),
'=' => m!(equal_parser, TokenKind::Operator),
'<' => m!(less_parser, TokenKind::Operator),
'>' => m!(greater_parser, TokenKind::Operator),
'+' => plus_dispatch(input, ctx, is_cfm),
'*' => star_dispatch(input, ctx),
'/' => slash_dispatch(input, ctx),
'~' => tiled_dispatch(input, ctx),
':' => colon_dispatch(input, ctx),
'@' => at_dispatch(input, ctx),
'_' => underscore_dispatch(input, ctx),
'0' => radix_literal(input),
'1'..='9' => number_literal(input),
'a'..='z' | 'A'..='Z' => alpha_dispatch(input, ctx, last_ctx, first, is_cfm),
c if !c.is_ascii() => m!(non_ascii, TokenKind::StringRaw),
_ => {
let (rest, range) = input.split_at(first.len_utf8());
Ok((
rest,
(
Token::new(TokenKind::Symbol, range),
Diagnostic::IllegalChar(range),
),
))
}
}
}
#[inline]
fn map_valid_token(
mut parser: impl FnMut(Input<'_>) -> TokenizationResult<'_>,
kind: TokenKind,
) -> impl FnMut(Input<'_>) -> TokenizationResult<'_, (Token, Diagnostic)> {
move |input| {
let (input, s) = parser(input)?;
Ok((input, (Token::new(kind, s), Diagnostic::Valid)))
}
}
fn paren_dispatch(
input: Input<'_>,
ctx: Ctx,
first: char,
) -> TokenizationResult<'_, (Token, Diagnostic)> {
if matches!(ctx, Ctx::Letter | Ctx::Word) && matches!(first, '(' | '[') {
map_valid_token(
punctuation_tag(&first.to_string()),
TokenKind::OperatorPostfix,
)(input)
} else {
map_valid_token(punctuation_tag(&first.to_string()), TokenKind::Punctuation)(input)
}
}
fn equal_parser(input: Input<'_>) -> TokenizationResult<'_> {
alt((
space_brace_followed_tag("=>"),
punctuation_tag("==="),
punctuation_tag("=="),
punctuation_tag("="),
))(input)
}
fn less_parser(input: Input<'_>) -> TokenizationResult<'_> {
alt((
punctuation_tag("<="),
punctuation_tag("<<"),
punctuation_tag("<"),
))(input)
}
fn greater_parser(input: Input<'_>) -> TokenizationResult<'_> {
alt((
punctuation_tag(">="),
punctuation_tag(">>"),
punctuation_tag(">!"),
punctuation_tag(">"),
))(input)
}
fn plus_dispatch(
input: Input<'_>,
ctx: Ctx,
is_cfm: bool,
) -> TokenizationResult<'_, (Token, Diagnostic)> {
match ctx {
Ctx::Space if is_cfm => alt((
map_valid_token(punctuation_tag("+="), TokenKind::Operator),
map_valid_token(postfix_break_tag("+"), TokenKind::Operator), map_valid_token(whole_word("+"), TokenKind::Symbol), ))(input),
Ctx::Space => alt((
map_valid_token(punctuation_tag("+="), TokenKind::Operator),
map_valid_token(postfix_break_tag("+"), TokenKind::Operator), map_valid_token(operator_tag("+"), TokenKind::OperatorPrefix), ))(input),
_ => alt((
map_valid_token(punctuation_tag("+="), TokenKind::Operator),
map_valid_token(punctuation_tag("+"), TokenKind::Operator),
))(input),
}
}
fn pipe_parser(input: Input<'_>) -> TokenizationResult<'_> {
alt((
operator_tag("||"),
punctuation_tag("|>"),
punctuation_tag("|^"),
punctuation_tag("|"),
))(input)
}
fn and_dispatch(input: Input<'_>, ctx: Ctx) -> TokenizationResult<'_, (Token, Diagnostic)> {
match ctx {
Ctx::Word | Ctx::Number | Ctx::Letter => {
alt((map_valid_token(punctuation_tag("&"), TokenKind::Symbol),))(input)
} Ctx::Space | Ctx::Open => alt((
map_valid_token(space_brace_followed_tag("&:"), TokenKind::Operator), map_valid_token(operator_tag("&&"), TokenKind::Operator),
map_valid_token(postfix_break_tag("&+"), TokenKind::StringRaw),
map_valid_token(postfix_break_tag("&-"), TokenKind::StringRaw),
map_valid_token(postfix_break_tag("&?"), TokenKind::StringRaw),
map_valid_token(postfix_break_tag("&."), TokenKind::StringRaw),
map_valid_token(postfix_break_tag("&"), TokenKind::StringRaw),
))(input),
Ctx::Start => alt((map_valid_token(punctuation_tag("&"), TokenKind::Symbol),))(input),
}
}
fn star_dispatch(input: Input<'_>, ctx: Ctx) -> TokenizationResult<'_, (Token, Diagnostic)> {
match ctx {
Ctx::Letter | Ctx::Word | Ctx::Number => {
map_valid_token(operator_tag("*"), TokenKind::Operator)(input)
}
Ctx::Start | Ctx::Space | Ctx::Open => alt((
map_valid_token(operator_tag("*="), TokenKind::Operator),
map_valid_token(path_tag("**/", true), TokenKind::Symbol), map_valid_token(path_tag("*/", true), TokenKind::Symbol), map_valid_token(path_tag("*.", true), TokenKind::Symbol), map_valid_token(operator_tag("*"), TokenKind::Operator),
map_valid_token(last_path_tag("*"), TokenKind::Symbol), ))(input),
}
}
fn tiled_dispatch(input: Input<'_>, ctx: Ctx) -> TokenizationResult<'_, (Token, Diagnostic)> {
match ctx {
Ctx::Letter | Ctx::Word | Ctx::Number => {
map_valid_token(punctuation_tag("~"), TokenKind::Symbol)(input) } Ctx::Start | Ctx::Space | Ctx::Open => alt((
map_valid_token(operator_tag("~:"), TokenKind::Operator),
map_valid_token(path_tag("~/", true), TokenKind::Symbol), map_valid_token(last_path_tag("~"), TokenKind::StringRaw), ))(input),
}
}
fn slash_dispatch(input: Input<'_>, ctx: Ctx) -> TokenizationResult<'_, (Token, Diagnostic)> {
match ctx {
Ctx::Letter | Ctx::Word | Ctx::Number => {
alt((map_valid_token(punctuation_tag("/"), TokenKind::Operator),))(input)
} Ctx::Start | Ctx::Space | Ctx::Open => alt((
map_valid_token(punctuation_tag("/="), TokenKind::Operator),
map_valid_token(path_tag("/", false), TokenKind::Symbol), map_valid_token(last_path_tag("/"), TokenKind::StringRaw), map_valid_token(postfix_break_tag("/"), TokenKind::Operator), ))(input),
}
}
fn dot_dispatch(input: Input<'_>, ctx: Ctx) -> TokenizationResult<'_, (Token, Diagnostic)> {
match ctx {
Ctx::Letter | Ctx::Number | Ctx::Word => alt((
map_valid_token(prefix_range_tag("...="), TokenKind::OperatorInfix),
map_valid_token(prefix_range_tag("..."), TokenKind::OperatorInfix),
map_valid_token(prefix_range_tag("..="), TokenKind::OperatorInfix),
map_valid_token(prefix_range_tag(".."), TokenKind::OperatorInfix), map_valid_token(postfix_range_tag(".."), TokenKind::OperatorPostfix), map_valid_token(alpha_followed_tag("."), TokenKind::OperatorPostfix), ))(input),
Ctx::Start | Ctx::Space | Ctx::Open => alt((
map_valid_token(prefix_range_tag("..="), TokenKind::OperatorPrefix), map_valid_token(prefix_range_tag(".."), TokenKind::OperatorPrefix), number_literal, map_valid_token(alpha_followed_tag("."), TokenKind::OperatorPrefix), map_valid_token(path_tag("../", true), TokenKind::Symbol), map_valid_token(path_tag("./", true), TokenKind::Symbol),
map_valid_token(punct_seq_tag(".."), TokenKind::Operator), map_valid_token(postfix_break_tag(".."), TokenKind::StringRaw), map_valid_token(postfix_break_tag("."), TokenKind::StringRaw), ))(input),
}
}
fn minus_dispatch(
input: Input<'_>,
ctx: Ctx,
is_cfm: bool,
) -> TokenizationResult<'_, (Token, Diagnostic)> {
match ctx {
Ctx::Letter | Ctx::Word | Ctx::Number => alt((
map_valid_token(punctuation_tag("-="), TokenKind::Operator),
map_valid_token(punctuation_tag("->"), TokenKind::Operator),
map_valid_token(punctuation_tag("-"), TokenKind::Operator), ))(input),
Ctx::Start => alt((
map_valid_token(prefix_tag("-"), TokenKind::OperatorPrefix),
map_valid_token(punctuation_tag("-"), TokenKind::Symbol), ))(input),
Ctx::Space if is_cfm => alt((
map_valid_token(punctuation_tag("-="), TokenKind::Operator),
map_valid_token(punctuation_tag("->"), TokenKind::Operator),
map_valid_token(space_followed_tag("-"), TokenKind::Operator),
map_valid_token(postfix_break_tag("-"), TokenKind::StringRaw), map_valid_token(whole_word("-"), TokenKind::StringRaw),
map_valid_token(punctuation_tag("-"), TokenKind::Symbol), ))(input),
Ctx::Space => alt((
map_valid_token(punctuation_tag("-="), TokenKind::Operator),
map_valid_token(punctuation_tag("->"), TokenKind::Operator),
map_valid_token(whole_word("--"), TokenKind::StringRaw),
map_valid_token(prefix_tag("-"), TokenKind::OperatorPrefix),
map_valid_token(space_followed_tag("-"), TokenKind::Operator),
map_valid_token(postfix_break_tag("-"), TokenKind::StringRaw), map_valid_token(punctuation_tag("-"), TokenKind::Symbol), ))(input),
Ctx::Open => alt((
map_valid_token(prefix_tag("-"), TokenKind::OperatorPrefix),
map_valid_token(space_followed_tag("-"), TokenKind::Operator), map_valid_token(punctuation_tag("-"), TokenKind::Symbol), ))(input),
}
}
#[inline]
fn prefix_range_tag(prefix: &str) -> impl '_ + Fn(Input<'_>) -> TokenizationResult<'_> {
move |input: Input<'_>| {
input
.strip_prefix(prefix)
.filter(|(rest, _)| {
rest.starts_with(|c: char| {
c.is_ascii_alphanumeric() || matches!(c, '(' | '-' | '_')
})
})
.ok_or(NOT_FOUND)
}
}
#[inline]
fn postfix_range_tag(prefix: &str) -> impl '_ + Fn(Input<'_>) -> TokenizationResult<'_> {
move |input: Input<'_>| {
input
.strip_prefix(prefix)
.filter(|(rest, _)| {
rest.is_empty()
|| rest.starts_with(|c: char| is_path_delimiter(c) || matches!(c, ':'))
})
.ok_or(NOT_FOUND)
}
}
fn circum_dispatch(input: Input<'_>, ctx: Ctx) -> TokenizationResult<'_, (Token, Diagnostic)> {
match ctx {
Ctx::Letter | Ctx::Word => alt((
(map_valid_token(postfix_break_tag("^"), TokenKind::OperatorPostfix)), (map_valid_token(punctuation_tag("^"), TokenKind::Operator)),
))(input),
Ctx::Start | Ctx::Space | Ctx::Open | Ctx::Number => {
map_valid_token(punctuation_tag("^"), TokenKind::Operator)(input)
}
}
}
fn percent_dispatch(input: Input<'_>, ctx: Ctx) -> TokenizationResult<'_, (Token, Diagnostic)> {
match ctx {
Ctx::Number => alt((map_valid_token(
punctuation_tag("%"),
TokenKind::OperatorPostfix,
),))(input), Ctx::Letter | Ctx::Word => alt((
map_valid_token(punctuation_tag("%{"), TokenKind::Punctuation),
map_valid_token(punctuation_tag("%"), TokenKind::Operator),
))(input),
Ctx::Start | Ctx::Space | Ctx::Open => alt((
map_valid_token(punctuation_tag("%{"), TokenKind::Punctuation),
map_valid_token(punctuation_tag("%"), TokenKind::Operator),
))(input),
}
}
fn bang_dispatch(input: Input<'_>, ctx: Ctx) -> TokenizationResult<'_, (Token, Diagnostic)> {
match ctx {
Ctx::Letter | Ctx::Word | Ctx::Number => alt((
map_valid_token(punctuation_tag("!=="), TokenKind::Operator),
map_valid_token(punctuation_tag("!="), TokenKind::Operator),
map_valid_token(operator_tag("!~:"), TokenKind::Operator),
map_valid_token(postfix_break_tag("!"), TokenKind::OperatorPostfix),
map_valid_token(punctuation_tag("!"), TokenKind::Punctuation),
))(input),
Ctx::Start | Ctx::Space | Ctx::Open => alt((
map_valid_token(punctuation_tag("!=="), TokenKind::Operator),
map_valid_token(punctuation_tag("!="), TokenKind::Operator),
map_valid_token(operator_tag("!~:"), TokenKind::Operator),
map_valid_token(prefix_tag("!"), TokenKind::OperatorPrefix),
map_valid_token(punctuation_tag("!"), TokenKind::Punctuation),
))(input),
}
}
fn question_dispatch(input: Input<'_>, _ctx: Ctx) -> TokenizationResult<'_, (Token, Diagnostic)> {
alt((
map_valid_token(question_operator, TokenKind::Operator),
map_valid_token(operator_tag("?"), TokenKind::Operator),
map_valid_token(punctuation_tag("?"), TokenKind::Symbol),
))(input)
}
fn underscore_dispatch(input: Input<'_>, ctx: Ctx) -> TokenizationResult<'_, (Token, Diagnostic)> {
match ctx {
Ctx::Letter | Ctx::Word | Ctx::Number => alt((
map_valid_token(punctuation_tag("_"), TokenKind::Symbol),
))(input),
_ => alt((
map_valid_token(space_brace_followed_tag("_:"), TokenKind::Operator), map_valid_token(space_brace_followed_tag("_!"), TokenKind::Operator), map_valid_token(punct_seq_tag("__"), TokenKind::Operator), map_valid_token(
|input| {
input
.strip_prefix("_")
.filter(|(rest, _)| {
rest.is_empty()
|| rest.starts_with(&[' ', '\n', ')', ']', '}', ':', ';'])
|| rest.starts_with("..")
})
.ok_or(NOT_FOUND)
},
TokenKind::ValueSymbol,
),
map_valid_token(|input| symbol(input, false, ctx, ctx), TokenKind::Symbol), ))(input),
}
}
fn alpha_dispatch(
input: Input<'_>,
ctx: Ctx,
last_ctx: Ctx,
first: char,
is_cfm: bool,
) -> TokenizationResult<'_, (Token, Diagnostic)> {
if ctx == Ctx::Number && matches!(&first, 'B' | 'K' | 'M' | 'G' | 'T' | 'P') {
return map_valid_token(
postfix_break_tag(&first.to_string()),
TokenKind::OperatorPostfix,
)(input);
}
match input.chars().nth(1) {
Some(second) => {
if second == '{'
&& matches!(ctx, Ctx::Space | Ctx::Start)
&& matches!(&first, 'H' | 'M' | 'S')
{
return map_valid_token(
punctuation_tag(&format!("{first}{{")),
TokenKind::Punctuation,
)(input);
}
if matches!(second, '\'' | '"') {
return match first {
'r' => hashed_literal(&'r')(input),
'g' => parse_prefixed_string(input, first, second, TokenKind::Regex),
't' => parse_prefixed_string(input, first, second, TokenKind::Time),
's' => parse_prefixed_string(input, first, second, TokenKind::StringSafe),
'b' => parse_prefixed_string(input, first, second, TokenKind::Bytes),
_ => Err(NOT_FOUND),
};
}
if second == '#' {
if matches!(first, 'r' | 'g' | 't' | 's' | 'b') {
let result = hashed_literal(&first)(input);
if let Ok(hs) = result {
return Ok(hs);
}
}
}
#[cfg(windows)]
if let Ok(r) = map_valid_token(win_abpath_tag, TokenKind::StringRaw)(input) {
return Ok(r);
}
if ctx == Ctx::Start || ctx == Ctx::Space {
if let Ok(r) = map_valid_token(any_keyword, TokenKind::Keyword)(input) {
return Ok(r);
}
}
alt((
map_valid_token(value_symbol, TokenKind::ValueSymbol),
map_valid_token(protocols, TokenKind::StringRaw),
symbol_literal(ctx, last_ctx, is_cfm),
))(input)
}
None => {
return symbol_literal(ctx, last_ctx, is_cfm)(input);
}
}
}
fn hash_quote_prefix(input: Input<'_>) -> Option<(usize, char)> {
let hashes = input.chars().take_while(|&c| c == '#').count();
let quote = input.chars().nth(hashes)?;
matches!(quote, '\'' | '"' | '`').then_some((hashes, quote))
}
fn hashed_literal(
prefix: &char,
) -> impl FnMut(Input<'_>) -> TokenizationResult<'_, (Token, Diagnostic)> {
move |input: Input<'_>| {
if let Some((after_r, _)) = input.strip_prefix(&prefix.to_string()) {
if let Some((hashes, quote)) = hash_quote_prefix(after_r) {
let kind = match (prefix, quote) {
('r', '\'') => TokenKind::StringRaw,
('r', '"') => TokenKind::StringLiteral,
('r', '`') => TokenKind::StringTemplate,
('g', _) => TokenKind::Regex,
('t', _) => TokenKind::Time,
('s', _) => TokenKind::StringSafe,
('b', _) => TokenKind::Bytes,
_ => return Err(NOT_FOUND),
};
return parse_hashed_string(input, hashes, quote, kind);
}
}
Err(NOT_FOUND)
}
}
fn parse_hashed_string<'a>(
input: Input<'a>,
hashes: usize,
quote: char,
kind: TokenKind,
) -> TokenizationResult<'a, (Token, Diagnostic)> {
let open_len = 1 + hashes + 1;
let close_delim: String = std::iter::once(quote)
.chain(std::iter::repeat('#').take(hashes))
.collect();
let src = input.as_ref();
match src[open_len..].find(close_delim.as_str()) {
Some(pos) => {
let total_len = open_len + pos + close_delim.len();
let (input, full_range) = input.split_at(total_len);
Ok((
input,
(
Token::new_quoted(kind, full_range, open_len as u8, close_delim.len() as u8),
Diagnostic::Valid,
),
))
}
None => {
let (input, full_range) = input.split_at(input.len());
Ok((
input,
(
Token::new_quoted(kind, full_range, open_len as u8, 0),
Diagnostic::UnterminatedString(full_range),
),
))
}
}
}
fn symbol_literal(
ctx: Ctx,
last_ctx: Ctx,
is_cfm: bool,
) -> impl FnMut(Input<'_>) -> TokenizationResult<'_, (Token, Diagnostic)> {
move |input: Input<'_>| {
map_valid_token(
|input| symbol(input, is_cfm, ctx, last_ctx),
TokenKind::Symbol,
)(input)
}
}
fn colon_dispatch(input: Input<'_>, ctx: Ctx) -> TokenizationResult<'_, (Token, Diagnostic)> {
match ctx {
Ctx::Letter => alt((
map_valid_token(alpha_followed_tag("::"), TokenKind::OperatorInfix),
map_valid_token(punctuation_tag(":="), TokenKind::Operator),
map_valid_token(punctuation_tag(":"), TokenKind::Operator), ))(input),
Ctx::Start => alt((
map_valid_token(punctuation_tag(":="), TokenKind::Operator),
map_valid_token(operator_tag(":"), TokenKind::OperatorPrefix), ))(input),
_ => alt((
map_valid_token(punctuation_tag(":="), TokenKind::Operator),
map_valid_token(punctuation_tag(":"), TokenKind::Operator),
))(input),
}
}
fn at_dispatch(input: Input<'_>, ctx: Ctx) -> TokenizationResult<'_, (Token, Diagnostic)> {
match ctx {
Ctx::Start | Ctx::Space => alt((
map_valid_token(prefix_tag("@"), TokenKind::OperatorPrefix), ))(input),
_ => {
map_valid_token(punctuation_tag("@"), TokenKind::Symbol)(input)
}
}
}
fn question_operator(input: Input<'_>) -> TokenizationResult<'_> {
alt((
postfix_break_tag("?+"),
postfix_break_tag("?."),
postfix_break_tag("??"),
postfix_break_tag("?>"),
postfix_break_tag("?!"),
space_brace_followed_tag("?:"),
postfix_break_tag("?~"),
))(input)
}
fn any_keyword(input: Input<'_>) -> TokenizationResult<'_> {
alt((
space_followed_tag("let"),
space_followed_tag("set"),
space_followed_tag("alias"),
space_followed_tag("export"),
space_brace_followed_tag("if"),
space_brace_followed_tag("else"),
space_followed_tag("fn"),
space_brace_followed_tag("match"),
space_followed_tag("for"),
space_followed_tag("in"),
space_brace_followed_tag("while"),
space_brace_followed_tag("loop"),
postfix_break_tag("break"),
postfix_break_tag("continue"),
postfix_break_tag("return"),
postfix_break_tag("shift"),
space_followed_tag("del"),
space_followed_tag("use"),
))(input)
}
#[inline]
fn punct_seq_tag(punct: &str) -> impl '_ + Fn(Input<'_>) -> TokenizationResult<'_> {
move |input: Input<'_>| {
if input.starts_with(punct) {
let places = input.chars().take_while(char::is_ascii_punctuation).count();
if places > punct.len() {
return Ok(input.split_at(places));
}
}
Err(NOT_FOUND)
}
}
fn path_tag(punct: &str, alone_ok: bool) -> impl '_ + Fn(Input<'_>) -> TokenizationResult<'_> {
move |input: Input<'_>| {
if !input.starts_with(punct) {
return Err(NOT_FOUND);
}
let bytes = input.as_ref().as_bytes();
let prefix_len = punct.len();
let mut i = prefix_len;
while i < bytes.len() {
let b = bytes[i];
if b == b'\\' {
i += 1; if i < bytes.len() {
let char_len = match bytes[i] {
0x00..=0x7F => 1,
0xC0..=0xDF => 2,
0xE0..=0xEF => 3, _ => 4,
};
i += char_len; continue;
}
break;
}
if b < 0x80 && is_path_delimiter(b as char) {
break;
}
let char_len = match b {
0x00..=0x7F => 1,
0xC0..=0xDF => 2,
0xE0..=0xEF => 3, _ => 4,
};
i += char_len;
}
if alone_ok || i > prefix_len {
Ok(input.split_at(i))
} else {
Err(NOT_FOUND)
}
}
}
fn last_path_tag(punct: &str) -> impl '_ + Fn(Input<'_>) -> TokenizationResult<'_> {
move |input: Input<'_>| {
if !input.starts_with(punct) {
return Err(NOT_FOUND);
}
let bytes = input.as_ref().as_bytes();
let prefix_len = punct.len();
let mut i = prefix_len;
while i < bytes.len() {
let b = bytes[i];
if !matches!(b, b' ' | b'\t') {
if is_path_delimiter(b as char) {
return Ok(input.split_at(i));
}
return Err(NOT_FOUND);
}
i += (b as char).len_utf8();
}
return Ok(input.split_at(i));
}
}
#[inline]
fn is_path_delimiter(c: char) -> bool {
c.is_ascii_whitespace() || matches!(c, ';' | '`' | ')' | ']' | '}' | '|' | '>' | ',')
}
#[cfg(windows)]
fn win_abpath_tag(input: Input<'_>) -> TokenizationResult<'_> {
let mut it = input.chars();
if input.len() > 1
&& it.next().map_or(false, |c| c.is_ascii_uppercase())
&& it.next().map_or(false, |c| c == ':')
{
let byte_len = input
.chars()
.take_while(|&c| !is_path_delimiter(c))
.map(char::len_utf8)
.sum::<usize>();
Ok(input.split_at(byte_len))
} else {
Err(NOT_FOUND)
}
}
fn protocols(input: Input<'_>) -> TokenizationResult<'_> {
alt((
whole_word("https://"),
whole_word("http://"),
whole_word("ftps://"),
whole_word("ftp://"),
whole_word("file://"),
))(input)
}
fn string_literal(input: Input<'_>) -> TokenizationResult<'_, (Token, Diagnostic)> {
match input.chars().next() {
Some('"') => parse_string(input, '"', TokenKind::StringLiteral),
Some('\'') => parse_string(input, '\'', TokenKind::StringRaw),
Some('`') => parse_string(input, '`', TokenKind::StringTemplate),
_ => Err(NOT_FOUND),
}
}
fn parse_string(
input: Input<'_>,
quote: char,
kind: TokenKind,
) -> TokenizationResult<'_, (Token, Diagnostic)> {
let quote_str = quote.to_string();
let (inner, _) = input.strip_prefix("e_str).ok_or(NOT_FOUND)?;
let (rest_after_content, diagnostic) = parse_string_inner(inner, quote)?;
let (rest, content) = finish_string(input, rest_after_content, quote);
let token = Token::new_quoted(kind, content, 1, 1);
Ok((rest, (token, diagnostic)))
}
fn parse_prefixed_string<'a>(
input: Input<'a>,
leading: char,
quote: char,
kind: TokenKind,
) -> TokenizationResult<'a, (Token, Diagnostic)> {
let mut prefix = String::with_capacity(2);
prefix.push(leading);
prefix.push(quote);
let (inner, _prefix) = input.strip_prefix(&prefix).ok_or(NOT_FOUND)?;
let (rest_after_content, diagnostic) = parse_string_inner(inner, quote)?;
let (rest, content) = finish_string(input, rest_after_content, quote);
let token = Token::new_quoted(kind, content, 2, 1);
Ok((rest, (token, diagnostic)))
}
fn finish_string<'a>(
input: Input<'a>,
rest_after_content: Input<'a>,
quote: char,
) -> (Input<'a>, StrSlice) {
let quote_str = quote.to_string();
match rest_after_content.strip_prefix("e_str) {
Some((after_close, _)) => {
let (_, content) = input.split_until(after_close);
(after_close, content)
}
None => (rest_after_content, input.split_until(rest_after_content).1),
}
}
fn ip_literal(input: Input<'_>) -> TokenizationResult<'_, (Token, Diagnostic)> {
let mut parts = 0;
let mut i = 0;
let bytes = input.as_ref().as_bytes();
while i < bytes.len() {
let start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
if i == start {
break;
}
parts += 1;
if parts == 4 {
break;
}
if i < bytes.len() && bytes[i] == b'.' {
i += 1;
} else {
break;
}
}
if parts == 4 {
let (rest, range) = input.split_at(i);
Ok((
rest,
(Token::new(TokenKind::StringRaw, range), Diagnostic::Valid),
))
} else {
Err(NOT_FOUND)
}
}
fn radix_literal(input: Input<'_>) -> TokenizationResult<'_, (Token, Diagnostic)> {
let radix_specs: [(&str, TokenKind, fn(char) -> bool); 3] = [
("0b", TokenKind::Radix2, |c: char| c == '0' || c == '1'),
("0o", TokenKind::Radix8, |c: char| c.is_digit(8)),
("0x", TokenKind::Radix16, |c: char| c.is_ascii_hexdigit()),
];
for (prefix, kind, is_digit) in radix_specs {
if let Some((after_prefix, _)) = input.strip_prefix(prefix) {
let places = after_prefix
.chars()
.take_while(|&c| is_digit(c) || c == '_')
.count();
let (remain, _) = after_prefix.split_at(places);
let (remain, number) = input.split_until(remain);
if places == 0 {
return Ok((
remain,
(
Token::new_quoted(kind, number, 2, 0),
Diagnostic::InvalidNumber(number),
),
));
}
return Ok((
remain,
(
Token::new_quoted(kind, number, 2, 0), Diagnostic::Valid,
),
));
}
}
number_literal(input)
}
fn number_literal(input: Input<'_>) -> TokenizationResult<'_, (Token, Diagnostic)> {
if let Ok(res) = ip_literal(input) {
return Ok(res);
}
let bytes = input.as_ref().as_bytes();
let mut i = 0;
if bytes.get(0) == Some(&b'.') {
i += 1;
}
let digit_start = i;
while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'_') {
i += 1;
}
if i == digit_start {
return Err(NOT_FOUND);
}
let has_tailing_dot = bytes.get(i) == Some(&b'.');
if has_tailing_dot && bytes.get(i + 1) == Some(&b'.') {
let (rest, range) = input.split_at(i);
return Ok((
rest,
(
Token::new(TokenKind::IntegerLiteral, range),
Diagnostic::Valid,
),
));
}
if has_tailing_dot {
i += 1;
let frac_start = i;
while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'_') {
i += 1;
}
if i == frac_start {
let (rest, range) = input.split_at(i);
return Ok((
rest,
(
Token::new(TokenKind::FloatLiteral, range),
Diagnostic::InvalidNumber(range),
),
));
}
}
let (rest, range) = input.split_at(i);
let kind = if digit_start > 0 || has_tailing_dot {
TokenKind::FloatLiteral
} else {
TokenKind::IntegerLiteral
};
Ok((rest, (Token::new(kind, range), Diagnostic::Valid)))
}
fn value_symbol(input: Input<'_>) -> TokenizationResult<'_> {
alt((
postfix_break_tag("true"),
postfix_break_tag("false"),
postfix_break_tag("none"),
space_punc_followed_tag("_"),
))(input)
}
fn symbol(input: Input<'_>, is_cfm: bool, ctx: Ctx, last_ctx: Ctx) -> TokenizationResult<'_> {
let len = if is_cfm {
let is_param_ctx = ctx == Ctx::Space && last_ctx == Ctx::Letter;
let is_cmd_ctx = ctx == Ctx::Start || last_ctx == Ctx::Start && ctx == Ctx::Space;
input
.chars()
.take_while(|&c| is_symbol_char_cfm(c, is_param_ctx, is_cmd_ctx))
.count()
} else {
input.chars().take_while(|&c| is_symbol_char(c)).count()
};
if len == 0 {
return Err(NOT_FOUND);
}
Ok(input.split_at(len))
}
fn whitespace(input: Input<'_>) -> TokenizationResult<'_> {
let ws_chars = input
.chars()
.take_while(|c| matches!(c, ' ' | '\t' | '\0'))
.count();
if ws_chars == 0 {
return Err(NOT_FOUND);
}
Ok(input.split_at(ws_chars))
}
fn non_ascii(input: Input<'_>) -> TokenizationResult<'_> {
let len = input
.chars()
.take_while(|&c| !c.is_ascii())
.map(char::len_utf8)
.sum();
if len == 0 {
return Err(NOT_FOUND);
}
Ok(input.split_at(len))
}
fn line_continuation(input: Input<'_>) -> TokenizationResult<'_> {
if let Some((rest, matched)) = input.strip_prefix("\\\n") {
Ok((rest, matched))
} else {
#[cfg(windows)]
if let Some((rest, matched)) = input.strip_prefix("\\\r\n") {
return Ok((rest, matched));
}
Err(NOT_FOUND)
}
}
fn comment(input: Input<'_>) -> TokenizationResult<'_> {
let len = input
.chars()
.take_while(|&c| !matches!(c, '\n' | '\r'))
.map(char::len_utf8)
.sum();
Ok(input.split_at(len))
}
fn parse_string_inner(input: Input<'_>, quote_char: char) -> TokenizationResult<'_, Diagnostic> {
let start_range = input.as_str_slice();
let quote_byte = quote_char as u8;
let bytes = input.as_ref().as_bytes();
let mut pos = 0;
while pos < bytes.len() {
let b = bytes[pos];
if b == quote_byte {
return Ok((input.split_at(pos).0, Diagnostic::Valid));
} else {
pos += 1;
if b == b'\\' && pos + 1 < bytes.len() {
pos += 1;
}
}
}
Ok((
input.split_at(pos).0,
Diagnostic::UnterminatedString(start_range),
))
}
#[inline]
fn punctuation_tag(punct: &str) -> impl '_ + Fn(Input<'_>) -> TokenizationResult<'_> {
move |input: Input<'_>| input.strip_prefix(punct).ok_or(NOT_FOUND)
}
#[inline]
fn space_followed_tag(keyword: &str) -> impl '_ + Fn(Input<'_>) -> TokenizationResult<'_> {
move |input: Input<'_>| {
input
.strip_prefix(keyword)
.filter(|(rest, _)| rest.starts_with(char::is_whitespace))
.ok_or(NOT_FOUND)
}
}
#[inline]
fn alpha_followed_tag(keyword: &str) -> impl '_ + Fn(Input<'_>) -> TokenizationResult<'_> {
move |input: Input<'_>| {
input
.strip_prefix(keyword)
.filter(|(rest, _)| rest.starts_with(|x| char::is_ascii_alphabetic(&x)))
.ok_or(NOT_FOUND)
}
}
#[inline]
fn space_brace_followed_tag(keyword: &str) -> impl '_ + Fn(Input<'_>) -> TokenizationResult<'_> {
move |input: Input<'_>| {
input
.strip_prefix(keyword)
.filter(|(rest, _)| {
rest.starts_with(char::is_whitespace)
|| rest.starts_with('{')
|| rest.starts_with('(')
})
.ok_or(NOT_FOUND)
}
}
#[inline]
fn operator_tag(keyword: &str) -> impl '_ + Fn(Input<'_>) -> TokenizationResult<'_> {
move |input: Input<'_>| {
input
.strip_prefix(keyword)
.filter(|(rest, _)| {
rest.starts_with(|c: char| c.is_whitespace() || !c.is_ascii_punctuation())
})
.ok_or(NOT_FOUND)
}
}
#[inline]
fn whole_word(prefix: &str) -> impl '_ + Fn(Input<'_>) -> TokenizationResult<'_> {
move |input: Input<'_>| {
if input.starts_with(prefix) {
let len = input
.chars()
.take_while(|c| !matches!(c, ' ' | ';' | '\n' | '\t' | '\r' | ')' | ']' | '}'))
.map(char::len_utf8)
.sum();
Ok(input.split_at(len))
} else {
Err(NOT_FOUND)
}
}
}
#[inline]
fn space_punc_followed_tag(keyword: &str) -> impl '_ + Fn(Input<'_>) -> TokenizationResult<'_> {
move |input: Input<'_>| {
input
.strip_prefix(keyword)
.filter(|(rest, _)| {
rest.is_empty()
|| rest.starts_with(|c: char| c.is_whitespace() || c.is_ascii_punctuation())
})
.ok_or(NOT_FOUND)
}
}
#[inline]
fn prefix_tag(keyword: &str) -> impl '_ + Fn(Input<'_>) -> TokenizationResult<'_> {
move |input: Input<'_>| {
input
.strip_prefix(keyword)
.filter(|(rest, _)| {
rest.starts_with(|c: char| {
c.is_ascii_alphanumeric() || matches!(&c, '(' | '[' | '{' | '$' | '.')
})
})
.ok_or(NOT_FOUND)
}
}
#[inline]
fn postfix_break_tag(keyword: &str) -> impl '_ + Fn(Input<'_>) -> TokenizationResult<'_> {
move |input: Input<'_>| {
input
.strip_prefix(keyword)
.filter(|(rest, _)| rest.is_empty() || rest.starts_with(|c: char| is_path_delimiter(c)))
.ok_or(NOT_FOUND)
}
}
#[inline]
fn is_symbol_char(c: char) -> bool {
if c.is_ascii_whitespace() {
return false;
}
matches!(
c,
'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '~' | '?' | '&' | '#' | '$' | '@' | '-' | '/' | '\\'
)
}
#[inline]
fn is_symbol_char_cfm(c: char, is_param_ctx: bool, is_cmd_ctx: bool) -> bool {
if c.is_ascii_whitespace() {
return false;
}
if is_cmd_ctx {
return matches!(
c,
'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '~' | '?' | '&' | '#' | '$' | '@' | '-' | '/' | '\\'
);
}
if is_param_ctx {
return matches!(
c,
'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '~' | '?' | '&' | '#' | '$' | '@' | '-' | '/' | '\\' | '=' | '+' | '.' | ':'
);
}
return matches!(
c,
'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '~' | '?' | '&' | '#' | '$' | '@' | '-' | '/' | '\\'
);
}
pub(crate) fn parse_tokens(input: Input<'_>) -> (Vec<Token>, Vec<Diagnostic>) {
let is_cfm = is_cfm_mode(input.as_original_str());
let leading_char = if is_cfm { ">" } else { ":" };
let mut tokens = Vec::new();
let mut diagnostics = Vec::new();
let mut ctx = Ctx::Start;
let mut last_ctx = Ctx::Start;
let mut input = input;
if let Ok((new_input, (token, diagnostic))) =
map_valid_token(punctuation_tag(leading_char), TokenKind::ModeTip)(input)
{
input = new_input;
tokens.push(token);
diagnostics.push(diagnostic);
}
loop {
match parse_token_dispatch(input, ctx, last_ctx, is_cfm) {
Err(_) => break,
Ok((new_input, (token, diagnostic))) => {
last_ctx = ctx;
ctx = Ctx::after_token(&token, input.as_original_str());
input = new_input;
tokens.push(token);
diagnostics.push(diagnostic);
}
}
}
if !input.is_empty() {
diagnostics.push(Diagnostic::NotTokenized(input.as_str_slice()))
}
(tokens, diagnostics)
}
pub fn tokenize(input: &str) -> (Vec<Token>, Vec<Diagnostic>) {
let str = input.into();
let input = Input::new(&str);
parse_tokens(input)
}