use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Token {
Text(String),
EscapedExpr(String),
RawExpr(String),
Directive { name: String, args: Option<String> },
ComponentOpen {
name: String,
attrs: Vec<(String, String)>,
self_closing: bool,
},
ComponentClose { name: String },
}
impl fmt::Display for Token {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Token::Text(s) => write!(f, "{s}"),
Token::EscapedExpr(e) => write!(f, "{{{{ {e} }}}}"),
Token::RawExpr(e) => write!(f, "{{!! {e} !!}}"),
Token::Directive { name, args: Some(a) } => write!(f, "@{name}({a})"),
Token::Directive { name, args: None } => write!(f, "@{name}"),
Token::ComponentOpen { name, attrs, self_closing } => {
write!(f, "<x-{name}")?;
for (k, v) in attrs {
write!(f, " {k}=\"{v}\"")?;
}
if *self_closing {
write!(f, " />")
} else {
write!(f, ">")
}
}
Token::ComponentClose { name } => write!(f, "</x-{name}>"),
}
}
}
pub fn tokenize(input: &str) -> Vec<Token> {
let mut tokens = Vec::new();
let bytes = input.as_bytes();
let mut i = 0;
let mut text_start = 0;
while i < bytes.len() {
if i + 1 < bytes.len() && bytes[i] == b'{' && bytes[i + 1] == b'{' && !(i + 2 < bytes.len() && bytes[i + 2] == b'-' ) {
flush_text(input, text_start, i, &mut tokens);
if let Some(end) = find_close(&input[i + 2..], "}}") {
let expr = input[i + 2..i + 2 + end].trim().to_string();
tokens.push(Token::EscapedExpr(expr));
i += 2 + end + 2;
text_start = i;
continue;
}
}
if i + 2 < bytes.len() && bytes[i] == b'{' && bytes[i + 1] == b'!' && bytes[i + 2] == b'!' {
flush_text(input, text_start, i, &mut tokens);
if let Some(end) = find_close(&input[i + 3..], "!!}") {
let expr = input[i + 3..i + 3 + end].trim().to_string();
tokens.push(Token::RawExpr(expr));
i += 3 + end + 3;
text_start = i;
continue;
}
}
if bytes[i] == b'@' && i + 1 < bytes.len() && (bytes[i + 1].is_ascii_alphabetic() || bytes[i + 1] == b'_') {
if i + 1 < bytes.len() && bytes[i + 1] == b'@' {
}
flush_text(input, text_start, i, &mut tokens);
let dir_start = i + 1;
let mut dir_end = dir_start;
while dir_end < bytes.len() && (bytes[dir_end].is_ascii_alphanumeric() || bytes[dir_end] == b'_') {
dir_end += 1;
}
let name = input[dir_start..dir_end].to_string();
let mut args = None;
let mut new_i = dir_end;
if dir_end < bytes.len() && bytes[dir_end] == b'(' {
if let Some(close_offset) = find_matching_paren(&input[dir_end..]) {
args = Some(input[dir_end + 1..dir_end + close_offset].to_string());
new_i = dir_end + close_offset + 1;
}
}
tokens.push(Token::Directive { name, args });
i = new_i;
text_start = i;
continue;
}
if bytes[i] == b'<' && i + 2 < bytes.len() && bytes[i + 1] == b'x' && bytes[i + 2] == b'-' {
flush_text(input, text_start, i, &mut tokens);
let after = &input[i + 3..];
let name_end = after
.find(|c: char| c.is_whitespace() || c == '>' || c == '/')
.unwrap_or(after.len());
let name = after[..name_end].to_string();
let rest_start = i + 3 + name_end;
let close_offset = input[rest_start..]
.find('>')
.unwrap_or(input.len() - rest_start);
let tag_inner = &input[rest_start..rest_start + close_offset];
let self_closing = tag_inner.ends_with('/');
let attrs = parse_attrs(tag_inner.trim_end_matches('/'));
tokens.push(Token::ComponentOpen {
name,
attrs,
self_closing,
});
i = rest_start + close_offset + 1;
text_start = i;
continue;
}
if bytes[i] == b'<' && i + 3 < bytes.len() && bytes[i + 1] == b'/' && bytes[i + 2] == b'x' && bytes[i + 3] == b'-' {
flush_text(input, text_start, i, &mut tokens);
let after = &input[i + 4..];
let name_end = after.find('>').unwrap_or(after.len());
let name = after[..name_end].trim().to_string();
tokens.push(Token::ComponentClose { name });
i += 4 + name_end + 1;
text_start = i;
continue;
}
i += 1;
}
flush_text(input, text_start, bytes.len(), &mut tokens);
tokens
}
fn flush_text(input: &str, start: usize, end: usize, tokens: &mut Vec<Token>) {
if end > start {
tokens.push(Token::Text(input[start..end].to_string()));
}
}
fn find_close(s: &str, needle: &str) -> Option<usize> {
s.find(needle)
}
fn find_matching_paren(s: &str) -> Option<usize> {
let bytes = s.as_bytes();
if bytes.is_empty() || bytes[0] != b'(' {
return None;
}
let mut depth = 1;
let mut in_string = None::<u8>;
for (i, &b) in bytes.iter().enumerate().skip(1) {
if let Some(quote) = in_string {
if b == quote && bytes.get(i - 1) != Some(&b'\\') {
in_string = None;
}
continue;
}
match b {
b'"' | b'\'' => in_string = Some(b),
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
}
None
}
fn parse_attrs(s: &str) -> Vec<(String, String)> {
let mut attrs = Vec::new();
let mut chars = s.char_indices().peekable();
while let Some((_, ch)) = chars.peek() {
if ch.is_whitespace() {
chars.next();
continue;
}
let mut name_end = 0;
let mut name = String::new();
let mut found_eq = false;
while let Some(&(idx, c)) = chars.peek() {
if c == '=' {
found_eq = true;
name_end = idx;
chars.next();
break;
}
if c.is_whitespace() {
name_end = idx;
break;
}
name.push(c);
chars.next();
}
let _ = name_end;
if !found_eq {
attrs.push((name, String::new()));
continue;
}
if let Some(&(_, q)) = chars.peek() {
if q == '"' || q == '\'' {
chars.next();
let mut val = String::new();
while let Some(&(_, c)) = chars.peek() {
chars.next();
if c == q {
break;
}
val.push(c);
}
attrs.push((name, val));
continue;
}
}
let mut val = String::new();
while let Some(&(_, c)) = chars.peek() {
if c.is_whitespace() {
break;
}
val.push(c);
chars.next();
}
attrs.push((name, val));
}
attrs
}