/
githubmirror
/
tailwindcss
Обзор
Документация
Войти
/
githubmirror
/
tailwindcss
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
crates/oxide/src/extractor/arbitrary_variable_machine.rs
413 строк
12 KB
Robin Malfait
Improve performance in bigger projects (#19632)
17 фев 2026, 22:15
Не верифицирован
17 фев 2026, 22:15
095ff96
Код
Авторство
О чём код?
use crate::cursor; use crate::extractor::bracket_stack::BracketStack; use crate::extractor::css_variable_machine::CssVariableMachine; use crate::extractor::machine::{Machine, MachineState}; use crate::extractor::string_machine::StringMachine; use classification_macros::ClassifyBytes; use std::marker::PhantomData; #[derive(Debug, Default)] pub struct IdleState; /// Currently parsing the inside of the arbitrary variable /// /// ```text /// (--my-opacity) /// ^^^^^^^^^^^^ /// ``` #[derive(Debug, Default)] pub struct ParsingState; /// Currently parsing the data type of the arbitrary variable /// /// ```text /// (length:--my-opacity) /// ^^^^^^^ /// ``` #[derive(Debug, Default)] pub struct ParsingDataTypeState; /// Currently parsing the fallback of the arbitrary variable /// /// ```text /// (--my-opacity,50%) /// ^^^^ /// ``` #[derive(Debug, Default)] pub struct ParsingFallbackState; /// Extracts arbitrary variables including the parens. /// /// E.g.: /// /// ```text /// (--my-value) /// ^^^^^^^^^^^^ /// /// bg-red-500/(--my-opacity) /// ^^^^^^^^^^^^^^ /// ``` #[derive(Debug, Default)] pub struct ArbitraryVariableMachine<State = IdleState> { /// Start position of the arbitrary variable start_pos: usize, /// Track brackets to ensure they are balanced bracket_stack: BracketStack, string_machine: StringMachine, css_variable_machine: CssVariableMachine, _state: PhantomData<State>, } impl<State> ArbitraryVariableMachine<State> { #[inline(always)] fn transition<NextState>(&self) -> ArbitraryVariableMachine<NextState> { ArbitraryVariableMachine { start_pos: self.start_pos, bracket_stack: Default::default(), string_machine: Default::default(), css_variable_machine: Default::default(), _state: PhantomData, } } } impl Machine for ArbitraryVariableMachine<IdleState> { #[inline(always)] fn reset(&mut self) {} #[inline(always)] fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { match cursor.curr().into() { // Arbitrary variables start with `(` followed by a CSS variable // // E.g.: `(--my-variable)` // ^^ // Class::OpenParen => match cursor.next().into() { Class::Dash => { self.start_pos = cursor.pos; cursor.advance(); self.transition::<ParsingState>().next(cursor) } Class::AlphaLower => { self.start_pos = cursor.pos; cursor.advance(); self.transition::<ParsingDataTypeState>().next(cursor) } _ => MachineState::Idle, }, // Everything else, is not a valid start of the arbitrary variable. But the next // character might be a valid start for a new utility. _ => MachineState::Idle, } } } impl Machine for ArbitraryVariableMachine<ParsingState> { #[inline(always)] fn reset(&mut self) {} #[inline(always)] fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { match self.css_variable_machine.next(cursor) { MachineState::Idle => self.restart(), MachineState::Done(_) => match cursor.next().into() { // A CSS variable followed by a `,` means that there is a fallback // // E.g.: `(--my-color,red)` // ^ Class::Comma => { cursor.advance_twice(); // Skip the `,` self.transition::<ParsingFallbackState>().next(cursor) } // End of the CSS variable // // E.g.: `(--my-color)` // ^ _ => { cursor.advance(); match cursor.curr().into() { // End of an arbitrary variable, must be followed by `)` Class::CloseParen => self.done(self.start_pos, cursor), // Invalid arbitrary variable, not ending at `)` _ => self.restart(), } } }, } } } impl Machine for ArbitraryVariableMachine<ParsingDataTypeState> { #[inline(always)] fn reset(&mut self) {} #[inline(always)] fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { let len = cursor.input.len(); while cursor.pos < len { match cursor.curr().into() { // Valid data type characters // // E.g.: `(length:--my-length)` // ^ Class::AlphaLower | Class::Dash => { cursor.advance(); } // End of the data type // // E.g.: `(length:--my-length)` // ^ Class::Colon => match cursor.next().into() { Class::Dash => { cursor.advance(); return self.transition::<ParsingState>().next(cursor); } _ => return self.restart(), }, // Anything else is not a valid character _ => return self.restart(), }; } self.restart() } } impl Machine for ArbitraryVariableMachine<ParsingFallbackState> { #[inline(always)] fn reset(&mut self) { self.bracket_stack.reset(); } #[inline(always)] fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState { let len = cursor.input.len(); while cursor.pos < len { match cursor.curr().into() { Class::Escape => match cursor.next().into() { // An escaped whitespace character is not allowed // // E.g.: `(--my-\ color)` // ^^ Class::Whitespace => return self.restart(), // An escaped character, skip the next character, resume after // // E.g.: `(--my-\#color)` // ^^ _ => cursor.advance_twice(), }, Class::OpenParen | Class::OpenBracket | Class::OpenCurly => { if !self.bracket_stack.push(cursor.curr()) { return self.restart(); } cursor.advance(); } Class::CloseParen | Class::CloseBracket | Class::CloseCurly if !self.bracket_stack.is_empty() => { if !self.bracket_stack.pop(cursor.curr()) { return self.restart(); } cursor.advance(); } // End of an arbitrary variable Class::CloseParen => return self.done(self.start_pos, cursor), // Start of a string Class::Quote => match self.string_machine.next(cursor) { MachineState::Idle => return self.restart(), MachineState::Done(_) => cursor.advance(), }, // A `:` inside of a fallback value is only valid inside of brackets or inside of a // string. Everywhere else, it's invalid. // // E.g.: `(--foo,bar:baz)` // ^ Not valid // // E.g.: `(--url,url(https://example.com))` // ^ Valid // // E.g.: `(--my-content:'a:b:c:')` // ^ ^ ^ Valid Class::Colon if self.bracket_stack.is_empty() => return self.restart(), // Any kind of whitespace is not allowed Class::Whitespace => return self.restart(), // String interpolation-like syntax is not allowed. E.g.: `[${x}]` Class::Dollar if matches!(cursor.next().into(), Class::OpenCurly) => { return self.restart() } // Everything else is valid _ => cursor.advance(), }; } self.restart() } } #[derive(Clone, Copy, PartialEq, ClassifyBytes)] enum Class { #[bytes_range(b'a'..=b'z')] AlphaLower, #[bytes_range(b'A'..=b'Z')] AlphaUpper, #[bytes(b'@')] At, #[bytes(b':')] Colon, #[bytes(b',')] Comma, #[bytes(b'-')] Dash, #[bytes(b'.')] Dot, #[bytes(b'$')] Dollar, #[bytes(b'\\')] Escape, #[bytes(b'\0')] End, #[bytes_range(b'0'..=b'9')] Number, #[bytes(b'[')] OpenBracket, #[bytes(b']')] CloseBracket, #[bytes(b'(')] OpenParen, #[bytes(b')')] CloseParen, #[bytes(b'{')] OpenCurly, #[bytes(b'}')] CloseCurly, #[bytes(b'"', b'\'', b'`')] Quote, #[bytes(b'_')] Underscore, #[bytes(b' ', b'\t', b'\n', b'\r', b'\x0C')] Whitespace, #[fallback] Other, } #[cfg(test)] mod tests { use super::ArbitraryVariableMachine; use crate::extractor::{arbitrary_variable_machine::IdleState, machine::Machine}; use pretty_assertions::assert_eq; #[test] #[ignore] fn test_arbitrary_variable_machine_performance() { let input = r#"<div class="(--foo) (--my-color,red,blue) (--my-img,url('https://example.com?q=(][)'))"></div>"#.repeat(100); ArbitraryVariableMachine::<IdleState>::test_throughput(100_000, &input); ArbitraryVariableMachine::<IdleState>::test_duration_once(&input); todo!() } #[test] fn test_arbitrary_variable_extraction() { for (input, expected) in [ // Simple utility ("(--foo)", vec!["(--foo)"]), // With dashes ("(--my-color)", vec!["(--my-color)"]), // With a fallback ("(--my-color,red,blue)", vec!["(--my-color,red,blue)"]), // With a fallback containing a string with unbalanced brackets ( "(--my-img,url('https://example.com?q=(][)'))", vec!["(--my-img,url('https://example.com?q=(][)'))"], ), // With a type hint ("(length:--my-length)", vec!["(length:--my-length)"]), // -------------------------------------------------------- // Exceptions: // Arbitrary variable must start with a CSS variable (r"(bar)", vec![]), // Arbitrary variables must be valid CSS variables (r"(--my-\ color)", vec![]), (r"(--my#color)", vec![]), // Fallbacks cannot have spaces (r"(--my-color, red)", vec![]), // Fallbacks cannot have escaped spaces (r"(--my-color,\ red)", vec![]), // Variables must have at least one character after the `--` (r"(--)", vec![]), (r"(--,red)", vec![]), (r"(-)", vec![]), (r"(-my-color)", vec![]), ] { assert_eq!( ArbitraryVariableMachine::<IdleState>::test_extract_all(input), expected ); } } #[test] fn test_exceptions() { for (input, expected) in [ // JS string interpolation // As part of the variable ("(--my-${var})", vec![]), // As the fallback ("(--my-variable,${var})", vec![]), // As the fallback in strings ( "(--my-variable,url('${var}'))", vec!["(--my-variable,url('${var}'))"], ), ] { assert_eq!( ArbitraryVariableMachine::<IdleState>::test_extract_all(input), expected ); } } }