///|
/// CSS Tokenizer based on CSS Syntax Level 3
/// https://www.w3.org/TR/css-syntax-3/#tokenization
///|
/// Tokenizer state
priv struct Tokenizer {
input : String
mut pos : Int
len : Int
}
///|
fn Tokenizer::new(input : String) -> Tokenizer {
{ input, pos: 0, len: input.length() }
}
///|
/// Peek current character without consuming
fn Tokenizer::peek(self : Tokenizer) -> Char? {
if self.pos >= self.len {
None
} else {
Some(self.input[self.pos].to_int().unsafe_to_char())
}
}
///|
/// Peek character at offset from current position
fn Tokenizer::peek_at(self : Tokenizer, offset : Int) -> Char? {
let idx = self.pos + offset
if idx >= self.len || idx < 0 {
None
} else {
Some(self.input[idx].to_int().unsafe_to_char())
}
}
///|
/// Consume and return current character
fn Tokenizer::consume(self : Tokenizer) -> Char? {
if self.pos >= self.len {
None
} else {
let c = self.input[self.pos].to_int().unsafe_to_char()
self.pos += 1
Some(c)
}
}
///|
/// Check if at end of input
fn Tokenizer::is_eof(self : Tokenizer) -> Bool {
self.pos >= self.len
}
///|
/// Check if character is whitespace (space, tab, newline)
fn is_whitespace(c : Char) -> Bool {
c == ' ' || c == '\t' || c == '\n' || c == '\r'
}
///|
/// Check if character is a digit
fn is_digit(c : Char) -> Bool {
c >= '0' && c <= '9'
}
///|
/// Check if character is a letter
fn is_letter(c : Char) -> Bool {
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
///|
/// Check if character is a name-start character
fn is_name_start(c : Char) -> Bool {
is_letter(c) || c == '_' || c.to_uint() > 0x7F
}
///|
/// Check if character is a name character
fn is_name_char(c : Char) -> Bool {
is_name_start(c) || is_digit(c) || c == '-'
}
///|
/// Consume whitespace
fn Tokenizer::consume_whitespace(self : Tokenizer) -> Unit {
while not(self.is_eof()) {
match self.peek() {
Some(c) if is_whitespace(c) => {
let _ = self.consume()
}
_ => break
}
}
}
///|
/// Consume an identifier/name
fn Tokenizer::consume_name(self : Tokenizer) -> String {
let buf = StringBuilder::new()
while not(self.is_eof()) {
match self.peek() {
Some(c) if is_name_char(c) => {
buf.write_char(c)
let _ = self.consume()
}
Some('\\') => {
// Escape sequence - simplified handling
let _ = self.consume()
match self.consume() {
Some(escaped) => buf.write_char(escaped)
None => ()
}
}
_ => break
}
}
buf.to_string()
}
///|
/// Consume a number
fn Tokenizer::consume_number(self : Tokenizer) -> (Double, NumType) {
let buf = StringBuilder::new()
let mut is_integer = true
// Optional sign
match self.peek() {
Some('+') | Some('-') =>
match self.consume() {
Some(c) => buf.write_char(c)
None => ()
}
_ => ()
}
// Integer part
while not(self.is_eof()) {
match self.peek() {
Some(c) if is_digit(c) => {
buf.write_char(c)
let _ = self.consume()
}
_ => break
}
}
// Decimal part
match (self.peek(), self.peek_at(1)) {
(Some('.'), Some(c)) if is_digit(c) => {
is_integer = false
buf.write_char('.')
let _ = self.consume()
while not(self.is_eof()) {
match self.peek() {
Some(c) if is_digit(c) => {
buf.write_char(c)
let _ = self.consume()
}
_ => break
}
}
}
_ => ()
}
// Exponent part
match self.peek() {
Some('e') | Some('E') =>
match self.peek_at(1) {
Some(c) if is_digit(c) || c == '+' || c == '-' => {
is_integer = false
match self.consume() {
Some(e) => buf.write_char(e)
None => ()
}
match self.peek() {
Some('+') | Some('-') =>
match self.consume() {
Some(sign) => buf.write_char(sign)
None => ()
}
_ => ()
}
while not(self.is_eof()) {
match self.peek() {
Some(c) if is_digit(c) => {
buf.write_char(c)
let _ = self.consume()
}
_ => break
}
}
}
_ => ()
}
_ => ()
}
let num_str = buf.to_string()
let value = @strconv.parse_double(num_str) catch { _ => 0.0 }
let num_type : NumType = if is_integer { Integer } else { NumType::Number }
(value, num_type)
}
///|
/// Consume a string token
fn Tokenizer::consume_string(self : Tokenizer, quote : Char) -> Token {
let buf = StringBuilder::new()
while not(self.is_eof()) {
match self.consume() {
Some(c) if c == quote => return Token::String(buf.to_string())
Some('\n') | Some('\r') => return BadString
Some('\\') =>
match self.peek() {
Some('\n') => {
let _ = self.consume()
// Line continuation
}
Some(c) => {
let _ = self.consume()
buf.write_char(c)
}
None => ()
}
Some(c) => buf.write_char(c)
None => return Token::String(buf.to_string())
}
}
Token::String(buf.to_string())
}
///|
/// Check if the next characters would start a number
fn Tokenizer::would_start_number(self : Tokenizer) -> Bool {
match self.peek() {
Some('+') | Some('-') =>
match self.peek_at(1) {
Some(c) if is_digit(c) => true
Some('.') =>
match self.peek_at(2) {
Some(c) if is_digit(c) => true
_ => false
}
_ => false
}
Some('.') =>
match self.peek_at(1) {
Some(c) if is_digit(c) => true
_ => false
}
Some(c) if is_digit(c) => true
_ => false
}
}
///|
/// Check if the next characters would start an identifier
fn Tokenizer::would_start_ident(self : Tokenizer) -> Bool {
match self.peek() {
Some('-') =>
match self.peek_at(1) {
Some('-') => true
Some(c) if is_name_start(c) => true
Some('\\') => true
_ => false
}
Some(c) if is_name_start(c) => true
Some('\\') => true
_ => false
}
}
///|
/// Consume the next token
fn Tokenizer::next_token(self : Tokenizer) -> Token {
if self.is_eof() {
return EOF
}
match self.peek() {
// Whitespace
Some(c) if is_whitespace(c) => {
self.consume_whitespace()
Whitespace
}
// Strings
Some('"') => {
let _ = self.consume()
self.consume_string('"')
}
Some('\'') => {
let _ = self.consume()
self.consume_string('\'')
}
// Hash
Some('#') => {
let _ = self.consume()
if self.would_start_ident() ||
(match self.peek() {
Some(c) => is_name_char(c)
None => false
}) {
let name = self.consume_name()
let hash_type : HashType = if self.would_start_ident() {
Id
} else {
Unrestricted
}
Hash(name, hash_type)
} else {
Delim('#')
}
}
// Parentheses and brackets
Some('(') => {
let _ = self.consume()
LeftParen
}
Some(')') => {
let _ = self.consume()
RightParen
}
Some('[') => {
let _ = self.consume()
LeftBracket
}
Some(']') => {
let _ = self.consume()
RightBracket
}
Some('{') => {
let _ = self.consume()
LeftBrace
}
Some('}') => {
let _ = self.consume()
RightBrace
}
// Punctuation
Some(':') => {
let _ = self.consume()
Colon
}
Some(';') => {
let _ = self.consume()
Semicolon
}
Some(',') => {
let _ = self.consume()
Comma
}
// Numbers and signs
Some('+') =>
if self.would_start_number() {
let (value, num_type) = self.consume_number()
self.finish_numeric_token(value, num_type)
} else {
let _ = self.consume()
Delim('+')
}
Some('-') =>
if self.would_start_number() {
let (value, num_type) = self.consume_number()
self.finish_numeric_token(value, num_type)
} else if self.would_start_ident() {
let name = self.consume_name()
self.finish_ident_token(name)
} else {
match (self.peek_at(1), self.peek_at(2)) {
(Some('-'), Some('>')) => {
let _ = self.consume()
let _ = self.consume()
let _ = self.consume()
CDC
}
_ => {
let _ = self.consume()
Delim('-')
}
}
}
Some('.') =>
if self.would_start_number() {
let (value, num_type) = self.consume_number()
self.finish_numeric_token(value, num_type)
} else {
let _ = self.consume()
Delim('.')
}
// Numbers
Some(c) if is_digit(c) => {
let (value, num_type) = self.consume_number()
self.finish_numeric_token(value, num_type)
}
// At-keyword
Some('@') => {
let _ = self.consume()
if self.would_start_ident() {
let name = self.consume_name()
AtKeyword(name)
} else {
Delim('@')
}
}
// Identifiers and functions
Some(c) if is_name_start(c) => {
let name = self.consume_name()
self.finish_ident_token(name)
}
// Comments (simplified - skip them)
Some('/') =>
match self.peek_at(1) {
Some('*') => {
let _ = self.consume()
let _ = self.consume()
// Skip until */
while not(self.is_eof()) {
match (self.peek(), self.peek_at(1)) {
(Some('*'), Some('/')) => {
let _ = self.consume()
let _ = self.consume()
break
}
_ => {
let _ = self.consume()
}
}
}
// Recursively get next token after comment
self.next_token()
}
_ => {
let _ = self.consume()
Delim('/')
}
}
// CDO