use std::fmt;
use tsv_lang::Error;
use crate::parser::scan_to_matching_brace;
use crate::whitespace::{brace_interior_start, char_at, is_svelte_ws, skip_svelte_ws};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TokenKind {
LeftAngle, RightAngle, Slash, LeftBrace, RightBrace, BlockOpen, BlockClose, BlockContinue, TagOpen, Equals, String, Identifier, Comment, Eof,
}
impl TokenKind {
pub(crate) const fn starts_with_brace(self) -> bool {
match self {
Self::LeftBrace
| Self::BlockOpen
| Self::BlockClose
| Self::BlockContinue
| Self::TagOpen => true,
Self::LeftAngle
| Self::RightAngle
| Self::Slash
| Self::RightBrace
| Self::Equals
| Self::String
| Self::Identifier
| Self::Comment
| Self::Eof => false,
}
}
}
impl fmt::Display for TokenKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TokenKind::LeftAngle => write!(f, "'<'"),
TokenKind::RightAngle => write!(f, "'>'"),
TokenKind::Slash => write!(f, "'/'"),
TokenKind::LeftBrace => write!(f, "'{{'"),
TokenKind::RightBrace => write!(f, "'}}'"),
TokenKind::BlockOpen => write!(f, "'{{#'"),
TokenKind::BlockClose => write!(f, "'{{/'"),
TokenKind::BlockContinue => write!(f, "'{{:'"),
TokenKind::TagOpen => write!(f, "'{{@'"),
TokenKind::Equals => write!(f, "'='"),
TokenKind::String => write!(f, "string"),
TokenKind::Identifier => write!(f, "identifier"),
TokenKind::Comment => write!(f, "comment"),
TokenKind::Eof => write!(f, "end of input"),
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct Token {
pub kind: TokenKind,
pub start: u32,
pub end: u32,
}
#[cfg(test)]
const _: () = assert!(size_of::<Token>() == 12);
const NAME_RUN_ASCII: [bool; 256] = {
let mut t = [false; 256];
let mut i = 0;
while i < 0x80 {
let b = i as u8;
t[i] = b.is_ascii_alphanumeric() || matches!(b, b'_' | b'$' | b'-' | b':' | b'|' | b'.');
i += 1;
}
t
};
#[inline]
fn special_byte_at_or_after(bytes: &[u8], from: usize) -> usize {
match bytes.get(from) {
None | Some(b'<' | b'{') => from,
Some(_) => tsv_lang::swar::next_byte_of(bytes, from + 1, [b'<', b'{']),
}
}
#[inline]
fn html_comment_end(bytes: &[u8], from: usize) -> Option<usize> {
let mut i = from;
loop {
i = tsv_lang::swar::next_byte_of(bytes, i, [b'-']);
if i >= bytes.len() {
return None;
}
if bytes[i..].starts_with(b"-->") {
return Some(i + b"-->".len());
}
i += 1;
}
}
const SVELTE_WS_ASCII: [bool; 256] = {
let mut t = [false; 256];
let mut i = 0;
while i < 0x80 {
t[i] = is_svelte_ws(i as u8 as char);
i += 1;
}
t
};
#[inline]
fn ascii_svelte_ws_run_end(bytes: &[u8], from: usize) -> usize {
let mut i = from;
while i < bytes.len() && SVELTE_WS_ASCII[usize::from(bytes[i])] {
i += 1;
}
i
}
#[inline]
fn ascii_name_run_end(bytes: &[u8], from: usize) -> usize {
let mut i = from;
while i < bytes.len() && NAME_RUN_ASCII[usize::from(bytes[i])] {
i += 1;
}
i
}
#[cold]
#[inline(never)]
fn name_run_end_from_non_ascii(source: &str, mut i: usize) -> usize {
let bytes = source.as_bytes();
while let Some(&b) = bytes.get(i) {
if NAME_RUN_ASCII[usize::from(b)] {
i += 1;
continue;
}
if b < 0x80 {
break;
}
let Some((ch, width)) = char_at(source, i) else {
break;
};
if is_svelte_ws(ch) {
break;
}
if ch.is_alphanumeric() || crate::html::is_svelte_tag_name_tail_char(ch) {
i += width;
} else {
break;
}
}
i
}
#[cold]
#[inline(never)]
fn quoted_value_end_from_brace(source: &str, brace: usize, quote: u8) -> Option<usize> {
let bytes = source.as_bytes();
let mut i = brace;
let mut sequence_is_invalid = false;
loop {
let b = *bytes.get(i)?;
if b == quote {
return Some(i + 1);
}
if b == b'{' && !sequence_is_invalid {
if BlockOrTagMarker::in_sequence_at(source, i).is_some() {
sequence_is_invalid = true;
} else {
i = scan_to_matching_brace(bytes, i + 1)? + '}'.len_utf8();
continue;
}
}
i += 1;
}
}
pub(crate) struct Lexer<'a> {
source: &'a str,
position: usize,
pub inside_tag: bool, plain_string_start: u32,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum BlockOrTagMarker {
Block,
Tag,
}
impl BlockOrTagMarker {
#[inline]
pub(crate) fn in_sequence_at(source: &str, brace_pos: usize) -> Option<(Self, usize)> {
let marker_pos = brace_interior_start(source, brace_pos);
let marker = match source.as_bytes().get(marker_pos)? {
b'#' => Self::Block,
b'@' => Self::Tag,
_ => return None,
};
Some((marker, marker_pos))
}
pub(crate) const fn sigil(self) -> char {
match self {
Self::Block => '#',
Self::Tag => '@',
}
}
pub(crate) const fn construct(self) -> &'static str {
match self {
Self::Block => "block",
Self::Tag => "tag",
}
}
}
impl<'a> Lexer<'a> {
pub(crate) fn new(source: &'a str) -> Self {
Self {
source,
position: tsv_lang::leading_bom_len(source),
inside_tag: false,
plain_string_start: u32::MAX,
}
}
pub(crate) fn seek(&mut self, pos: usize) {
assert!(
self.source.is_char_boundary(pos),
"seek to {pos}, not a character boundary of the document"
);
self.position = pos;
}
#[inline]
fn cur_byte(&self) -> Option<u8> {
self.source.as_bytes().get(self.position).copied()
}
#[inline]
fn cur_char(&self) -> Option<char> {
char_at(self.source, self.position).map(|(c, _)| c)
}
#[inline]
fn advance(&mut self) {
if let Some((_, width)) = char_at(self.source, self.position) {
self.position += width;
}
}
#[inline]
fn starts_with(&self, needle: &[u8]) -> bool {
self.source.as_bytes()[self.position..].starts_with(needle)
}
#[inline]
pub(crate) fn string_holds_no_brace(&self, start: usize) -> bool {
self.plain_string_start as usize == start
}
fn skip_whitespace(&mut self) {
self.position = self.peek_past_whitespace();
}
#[inline]
fn peek_past_whitespace(&self) -> usize {
let bytes = self.source.as_bytes();
let i = ascii_svelte_ws_run_end(bytes, self.position);
if bytes.get(i).is_some_and(|&b| b >= 0x80) {
skip_svelte_ws(self.source, i)
} else {
i
}
}
fn skip_to_special_char(&mut self) {
self.position = special_byte_at_or_after(self.source.as_bytes(), self.position);
}
fn scan_name_run(&mut self) {
let bytes = self.source.as_bytes();
let i = ascii_name_run_end(bytes, self.position);
self.position = if bytes.get(i).is_some_and(|&b| b >= 0x80) {
name_run_end_from_non_ascii(self.source, i)
} else {
i
};
}
#[inline(never)]
pub(crate) fn next_token_into(&mut self, dst: &mut Token) -> Result<(), Error> {
let bytes = self.source.as_bytes();
let mut i = self.position;
if self.inside_tag {
i = ascii_svelte_ws_run_end(bytes, i);
} else if !matches!(bytes.get(i), None | Some(b'<' | b'{')) {
self.position = i + 1;
return self.text_gap_then_token_into(dst);
}
let start = i;
let Some(&b) = bytes.get(start) else {
self.position = start;
*dst = Token {
kind: TokenKind::Eof,
start: start as u32,
end: start as u32,
};
return Ok(());
};
macro_rules! hand_on {
() => {{
self.position = start;
return self.next_token_dispatch(dst);
}};
}
let kind = match b {
b'<' if bytes.get(start + 1) == Some(&b'!') => hand_on!(),
b'<' => {
self.inside_tag = true; i = start + 1;
TokenKind::LeftAngle
}
b'>' => {
self.inside_tag = false; i = start + 1;
TokenKind::RightAngle
}
b'/' => {
i = start + 1;
TokenKind::Slash
}
b'{' => match bytes.get(start + 1).copied().unwrap_or(0) {
b'#' => {
i = start + 2;
TokenKind::BlockOpen
}
b':' => {
i = start + 2;
TokenKind::BlockContinue
}
b'/' if !matches!(bytes.get(start + 2), Some(b'*' | b'/')) => {
i = start + 2;
TokenKind::BlockClose
}
b'@' => {
i = start + 2;
TokenKind::TagOpen
}
c if c >= 0x80 || is_svelte_ws(c as char) => hand_on!(),
_ => {
i = start + 1;
TokenKind::LeftBrace
}
},
b'}' => {
i = start + 1;
TokenKind::RightBrace
}
b'=' => {
i = start + 1;
TokenKind::Equals
}
b'\'' | b'"' => {
self.position = start;
return self.string_token_into(dst);
}
b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'$' | b'-' | b'!' => {
i = ascii_name_run_end(bytes, start + 1);
if bytes.get(i).is_some_and(|&c| c >= 0x80) {
hand_on!();
}
TokenKind::Identifier
}
b'0'..=b'9' | 0x80..=0xFF => hand_on!(),
_ => {
i = start + 1;
TokenKind::Identifier
}
};
self.position = i;
*dst = Token {
kind,
start: start as u32,
end: i as u32,
};
Ok(())
}
#[inline(never)]
fn text_gap_then_token_into(&mut self, dst: &mut Token) -> Result<(), Error> {
self.position =
tsv_lang::swar::next_byte_of(self.source.as_bytes(), self.position, [b'<', b'{']);
self.next_token_into(dst)
}
#[inline(never)]
fn string_token_into(&mut self, dst: &mut Token) -> Result<(), Error> {
let bytes = self.source.as_bytes();
let start = self.position;
let quote = bytes[start];
debug_assert!(matches!(quote, b'\'' | b'"'));
let from = start + 1; let stop = if quote == b'"' {
tsv_lang::swar::next_byte_of(bytes, from, [b'"', b'{'])
} else {
tsv_lang::swar::next_byte_of(bytes, from, [b'\'', b'{'])
};
let end = match bytes.get(stop) {
Some(b'{') => quoted_value_end_from_brace(self.source, stop, quote),
Some(_) => {
self.plain_string_start = start as u32;
Some(stop + 1)
}
None => None,
};
let Some(end) = end else {
return Err(Error::lex("Unterminated string literal in template", start));
};
self.position = end;
*dst = Token {
kind: TokenKind::String,
start: start as u32,
end: end as u32,
};
Ok(())
}
#[inline(never)]
fn next_token_dispatch(&mut self, dst: &mut Token) -> Result<(), Error> {
if self.inside_tag {
self.skip_whitespace();
} else {
self.skip_to_special_char();
}
let start = self.position;
let kind = match self.cur_byte() {
None => TokenKind::Eof,
Some(b'<') => {
if !self.inside_tag && self.starts_with(b"<!--") {
let Some(end) = html_comment_end(self.source.as_bytes(), start + b"<!--".len())
else {
return Err(Error::lex("Unterminated HTML comment", start));
};
self.position = end;
TokenKind::Comment
} else {
self.inside_tag = true; self.position = start + 1;
TokenKind::LeftAngle
}
}
Some(b'>') => {
self.inside_tag = false; self.position = start + 1;
TokenKind::RightAngle
}
Some(b'/') => {
self.position = start + 1;
TokenKind::Slash
}
Some(b'{') => {
self.position = start + 1;
let marker = self.peek_past_whitespace();
match self.source.as_bytes().get(marker) {
Some(b'#') => {
self.position = marker + 1;
TokenKind::BlockOpen
}
Some(b':') => {
self.position = marker + 1;
TokenKind::BlockContinue
}
Some(b'/')
if !matches!(
self.source.as_bytes().get(marker + 1),
Some(b'*') | Some(b'/')
) =>
{
self.position = marker + 1;
TokenKind::BlockClose
}
Some(b'@') => {
self.position = marker + 1;
TokenKind::TagOpen
}
_ => TokenKind::LeftBrace,
}
}
Some(b'}') => {
self.position = start + 1;
TokenKind::RightBrace
}
Some(b'=') => {
self.position = start + 1;
TokenKind::Equals
}
Some(b'\'' | b'"') => return self.string_token_into(dst),
Some(b) if b.is_ascii_alphabetic() || matches!(b, b'_' | b'$' | b'-' | b'!') => {
self.position += 1;
self.scan_name_run();
TokenKind::Identifier
}
Some(b) if b.is_ascii_digit() => {
let bytes = self.source.as_bytes();
let mut i = self.position;
while let Some(&b) = bytes.get(i) {
if b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-') {
i += 1;
continue;
}
if b < 0x80 {
break;
}
let Some((ch, width)) = char_at(self.source, i) else {
break;
};
if ch.is_alphanumeric() {
i += width;
} else {
break;
}
}
self.position = i;
TokenKind::Identifier
}
Some(b) if b >= 0x80 && self.cur_char().is_some_and(char::is_alphabetic) => {
self.advance();
self.scan_name_run();
TokenKind::Identifier
}
Some(_) => {
self.advance();
TokenKind::Identifier
}
};
*dst = Token {
kind,
start: start as u32,
end: self.position as u32,
};
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{
Error, Lexer, Token, TokenKind, char_at, html_comment_end, is_svelte_ws,
special_byte_at_or_after,
};
#[test]
fn string_holds_no_brace_names_the_last_brace_free_string() {
let source = r#"<a b="x" c="{y}" d='z' e="">"#;
let mut lexer = Lexer::new(source);
let mut token = Token {
kind: TokenKind::Eof,
start: 0,
end: 0,
};
let mut answers = Vec::new();
let mut first_string = None;
loop {
lexer.next_token_into(&mut token).unwrap();
match token.kind {
TokenKind::Eof => break,
TokenKind::String => {
first_string.get_or_insert(token.start as usize);
answers.push(lexer.string_holds_no_brace(token.start as usize));
}
_ => {}
}
}
assert_eq!(answers, [true, false, true, true]);
assert!(!lexer.string_holds_no_brace(first_string.unwrap()));
}
type Step = (TokenKind, u32, u32, usize, bool, u32);
fn lex_all(source: &str, front: bool) -> Vec<Result<Step, Error>> {
let mut lexer = Lexer::new(source);
let mut token = Token {
kind: TokenKind::Eof,
start: 0,
end: 0,
};
let mut steps = Vec::new();
for _ in 0..=source.len() {
let step = if front {
lexer.next_token_into(&mut token)
} else {
lexer.next_token_dispatch(&mut token)
};
let failed = step.is_err();
steps.push(step.map(|()| {
(
token.kind,
token.start,
token.end,
lexer.position,
lexer.inside_tag,
lexer.plain_string_start,
)
}));
if failed {
break;
}
if token.kind == TokenKind::Eof {
break;
}
}
steps
}
#[test]
fn front_matches_the_dispatch_on_every_fragment_sequence() {
let fragments = [
"<",
">",
"/",
"=",
"}",
"{",
"{#",
"{ #",
"{:",
"{\t:",
"{/",
"{/*",
"{//",
"{@",
"{\u{a0}#",
"{\u{feff}",
"{é",
" ",
"\n\t",
"\u{a0}",
"\u{2028}",
"\u{feff}x",
"a",
"div",
"on:click|once",
"a.b",
"!DOCTYPE",
"<!--c-->",
"<!--",
"<!x",
"\"x\"",
"'y'",
"\"{x}\"",
"'{#if c}'",
"\"",
"'",
"\"a",
"12",
"3é",
"é",
"Ωmega",
"a\u{b7}b",
"x-café",
"%",
"[",
"\\",
"\u{1}",
"\0",
"{\0",
"text",
];
for a in fragments {
for b in fragments {
for c in ["", "<", " ", "a", "{", "é", "\""] {
for prefix in ["", "<x "] {
let source = format!("{prefix}{a}{b}{c}");
assert_eq!(
lex_all(&source, true),
lex_all(&source, false),
"{source:?}"
);
}
}
}
}
}
fn name_run_end_model(source: &str, mut i: usize) -> usize {
while let Some((ch, width)) = char_at(source, i) {
let continues = if ch.is_ascii() {
ch.is_ascii_alphanumeric() || matches!(ch, '_' | '$' | '-' | ':' | '|' | '.')
} else {
!is_svelte_ws(ch)
&& (ch.is_alphanumeric() || crate::html::is_svelte_tag_name_tail_char(ch))
};
if !continues {
break;
}
i += width;
}
i
}
fn scan_from(source: &str, start: usize) -> usize {
let mut lexer = Lexer::new(source);
lexer.seek(start);
lexer.scan_name_run();
lexer.position
}
#[test]
fn scan_name_run_matches_the_class_for_every_scalar() {
let mut checked = 0usize;
for cp in 0..=0x10_ffff_u32 {
let Some(c) = char::from_u32(cp) else {
continue;
};
for (prefix, body) in [
("<", format!("{c}")),
("<", format!("a{c}")),
("<", format!("ab-{c}x")),
("<", format!("é{c}")),
("<", format!("\u{e9}a{c}{c}b")),
("< ", format!("{c}=")),
("<x ", format!("on:a|{c}>")),
] {
let source = format!("{prefix}{body}");
let start = prefix.len();
assert_eq!(
scan_from(&source, start),
name_run_end_model(&source, start),
"U+{cp:04X} in {source:?}"
);
checked += 1;
}
}
assert!(checked > 7_000_000, "the scalar walk ran ({checked} cases)");
}
#[test]
fn scan_name_run_named_boundary_characters() {
for (name, tail) in [
("div", "\u{feff}>"),
("div", "\u{a0}x"),
("div", "\u{1680}x"),
("div", "\u{2028}x"),
("div", "\u{2029}x"),
("div", "\u{85}x"),
("e\u{301}t\u{301}e", "="),
("a\u{200c}b\u{200d}c", "/"),
("a\u{b7}b", " "),
("my-\u{1f600}", ">"),
("x-caf\u{e9}", ""),
("on:click|once", "={f}"),
("A.b$c_d", "\"x\""),
("a", "\\b"),
("a", "\u{1}b"),
("a", "\"b"),
] {
let source = format!("<{name}{tail}");
assert_eq!(scan_from(&source, 1), 1 + name.len(), "{source:?}");
assert_eq!(
name_run_end_model(&source, 1),
1 + name.len(),
"model: {source:?}"
);
}
}
fn boundaries(source: &str) -> Vec<usize> {
(0..=source.len())
.filter(|&i| source.is_char_boundary(i))
.collect()
}
fn text_run_sources() -> Vec<String> {
let mut sources = Vec::new();
for background in ["a", "é", "€", "\u{1f600}", "}", ">", "-", "\n", "\0"] {
for len in 0..=18 {
let run = background.repeat(len);
for tail in ["", "<", "{", "<{", "{<", "}"] {
sources.push(format!("{run}{tail}"));
sources.push(format!("{run}{tail}{run}"));
}
}
}
sources
}
#[test]
fn special_byte_at_or_after_matches_a_scalar_scan() {
fn scalar(bytes: &[u8], from: usize) -> usize {
let mut i = from;
while i < bytes.len() && !matches!(bytes[i], b'<' | b'{') {
i += 1;
}
i
}
for source in text_run_sources() {
let bytes = source.as_bytes();
for from in boundaries(&source) {
assert_eq!(
special_byte_at_or_after(bytes, from),
scalar(bytes, from),
"{source:?} from {from}"
);
}
}
}
#[test]
fn template_text_tokens_start_at_the_next_special_byte() {
for source in text_run_sources() {
if source.contains('<') {
continue;
}
let bytes = source.as_bytes();
let mut expected = Vec::new();
let mut i = 0;
loop {
while i < bytes.len() && bytes[i] != b'{' {
i += 1;
}
if i == bytes.len() {
expected.push((TokenKind::Eof, i, i));
break;
}
expected.push((TokenKind::LeftBrace, i, i + 1));
i += 1;
}
for front in [true, false] {
let got: Vec<_> = lex_all(&source, front)
.into_iter()
.map(|step| {
let (kind, start, end, ..) = step.unwrap();
(kind, start as usize, end as usize)
})
.collect();
assert_eq!(got, expected, "{source:?} (front: {front})");
}
}
}
#[test]
fn comment_opener_inside_a_tag_is_a_plain_left_angle() {
let open = "<x ".len() as u32;
for tail in [
"<!--",
"<!-- c -- />",
"<!-- c -->",
"<!--c>y</x><!-- d -->",
"<!-- c -- /><!-- d -->",
] {
let source = format!("<x {tail}");
for front in [true, false] {
let steps = lex_all(&source, front);
let at_opener = steps
.iter()
.find_map(|step| match step {
Ok((kind, start, end, ..)) if *start == open => Some((*kind, *end)),
_ => None,
})
.expect("a token at the opener");
assert_eq!(
at_opener,
(TokenKind::LeftAngle, open + 1),
"{source:?} (front: {front})"
);
let template_comment = source.find("><!--").map(|at| at as u32 + 1);
for step in &steps {
match step {
Ok((TokenKind::Comment, start, ..)) => {
assert_eq!(Some(*start), template_comment, "{source:?}");
}
Ok(_) => {}
Err(err) => panic!("{source:?} (front: {front}): {err:?}"),
}
}
assert_eq!(
steps
.iter()
.any(|step| matches!(step, Ok((TokenKind::Comment, ..)))),
template_comment.is_some(),
"{source:?} (front: {front})"
);
}
}
}
#[test]
fn html_comment_end_matches_a_scalar_search() {
fn scalar(bytes: &[u8], from: usize) -> Option<usize> {
bytes[from..]
.windows(3)
.position(|w| w == b"-->")
.map(|at| from + at + 3)
}
let fragments = [
"",
"-",
"--",
"->",
">",
"-->",
"--->",
"a",
"é",
"<",
"\u{1f600}",
];
let mut bodies = Vec::new();
for pad in [0, 1, 5, 6, 7, 8, 9, 13, 14, 15, 16, 17] {
for padding in ["a", "é", "-"] {
let lead = padding.repeat(pad);
for a in fragments {
for b in fragments {
for c in ["", "-", "-->", "a"] {
bodies.push(format!("{lead}{a}{b}{c}"));
}
}
}
}
}
for body in &bodies {
let bytes = body.as_bytes();
for from in boundaries(body) {
assert_eq!(
html_comment_end(bytes, from),
scalar(bytes, from),
"{body:?} from {from}"
);
}
for prefix in ["", "x", "<p>"] {
let source = format!("{prefix}<!--{body}");
let start = prefix.len();
let expected = scalar(source.as_bytes(), start + 4)
.ok_or_else(|| Error::lex("Unterminated HTML comment", start));
for front in [true, false] {
let steps = lex_all(&source, front);
let got = steps
.iter()
.find_map(|step| match step {
Ok((TokenKind::Comment, s, e, ..)) => {
assert_eq!(*s as usize, start, "{source:?}");
Some(Ok(*e as usize))
}
Err(err) => Some(Err(err.clone())),
Ok(_) => None,
})
.expect("a comment or an error");
assert_eq!(got, expected, "{source:?} (front: {front})");
}
}
}
}
}