mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Refactor: use compile time type state pattern (#17083)
This PR implements the state machines using the type state pattern at
compile time (via generic types) instead of a runtime state variable.
There is no runtime check to see what state we are in, instead we
transition to the new state when it's necessary.
This has some nice performance improvements for some of the state
machines, e.g.:
```diff
- ArbitraryVariableMachine: Throughput: 744.92 MB/s
+ ArbitraryVariableMachine: Throughput: 1.21 GB/s
```
We also don't have to store the current state because each machine runs
to completion. It's during execution that we can move to a new state if
necessary.
Unfortunately the diff is a tiny bit annoying to read, but essentially
this is what happened:
### The `enum` is split up in it's individual states as structs:
```rs
enum State {
A,
B,
C,
}
```
Becomes:
```rs
struct A;
struct B;
struct C;
```
### Generics
The current machine will receive a generic `State` that we can default
to the `IdleState`. Then we use `PhantomData` to "use" the type because
the generic type is otherwise not used as a concrete value, it's just a
marker.
```rs
struct MyMachine {}
```
Becomes:
```rs
struct MyMachine<State = Idle> {
_state: std::marker::PhantomData<State>
}
```
### Split
Next, the `next` function used to match on the current state, but now
each match arm is moved to a dedicated implementation instead:
```rs
impl Machine for MyMachine {
fn next(&mut self) -> MachineState {
match self.state {
State::A => { /* … */ },
State::B => { /* … */ },
State::C => { /* … */ },
}
}
}
```
Becomes:
```rs
impl Machine for MyMachine<A> {
fn next(&mut self) -> MachineState {
/* … */
}
}
impl Machine for MyMachine<B> {
fn next(&mut self) -> MachineState {
/* … */
}
}
impl Machine for MyMachine<C> {
fn next(&mut self) -> MachineState {
/* … */
}
}
```
It's a bit more verbose, but now each state is implemented in its own
block. This also removes 2 levels of nesting which is a nice benefit.
This commit is contained in:
parent
cc3e852791
commit
de145c5b06
@ -4,6 +4,31 @@ use crate::extractor::machine::{Machine, MachineState};
|
||||
use crate::extractor::string_machine::StringMachine;
|
||||
use crate::extractor::CssVariableMachine;
|
||||
use classification_macros::ClassifyBytes;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct IdleState;
|
||||
|
||||
/// Parsing the property, e.g.:
|
||||
///
|
||||
/// ```text
|
||||
/// [color:red]
|
||||
/// ^^^^^
|
||||
///
|
||||
/// [--my-color:red]
|
||||
/// ^^^^^^^^^^
|
||||
/// ```
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ParsingPropertyState;
|
||||
|
||||
/// Parsing the value, e.g.:
|
||||
///
|
||||
/// ```text
|
||||
/// [color:red]
|
||||
/// ^^^
|
||||
/// ```
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ParsingValueState;
|
||||
|
||||
/// Extracts arbitrary properties from the input, including the brackets.
|
||||
///
|
||||
@ -17,182 +42,98 @@ use classification_macros::ClassifyBytes;
|
||||
/// ^^^^^^^^^^^^^^^^
|
||||
/// ```
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ArbitraryPropertyMachine {
|
||||
pub struct ArbitraryPropertyMachine<State = IdleState> {
|
||||
/// Start position of the arbitrary value
|
||||
start_pos: usize,
|
||||
|
||||
/// Track brackets to ensure they are balanced
|
||||
bracket_stack: BracketStack,
|
||||
|
||||
/// Current state of the machine
|
||||
state: State,
|
||||
|
||||
css_variable_machine: CssVariableMachine,
|
||||
string_machine: StringMachine,
|
||||
|
||||
_state: PhantomData<State>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
enum State {
|
||||
#[default]
|
||||
Idle,
|
||||
|
||||
/// Parsing the property, e.g.:
|
||||
///
|
||||
/// ```text
|
||||
/// [color:red]
|
||||
/// ^^^^^
|
||||
///
|
||||
/// [--my-color:red]
|
||||
/// ^^^^^^^^^^
|
||||
/// ```
|
||||
ParsingProperty,
|
||||
|
||||
/// Parsing the value, e.g.:
|
||||
///
|
||||
/// ```text
|
||||
/// [color:red]
|
||||
/// ^^^
|
||||
/// ```
|
||||
ParsingValue,
|
||||
}
|
||||
|
||||
impl Machine for ArbitraryPropertyMachine {
|
||||
impl<State> ArbitraryPropertyMachine<State> {
|
||||
#[inline(always)]
|
||||
fn reset(&mut self) {
|
||||
self.start_pos = 0;
|
||||
self.state = State::Idle;
|
||||
self.bracket_stack.reset();
|
||||
fn transition<NextState>(&self) -> ArbitraryPropertyMachine<NextState> {
|
||||
ArbitraryPropertyMachine {
|
||||
start_pos: self.start_pos,
|
||||
bracket_stack: Default::default(),
|
||||
css_variable_machine: Default::default(),
|
||||
string_machine: Default::default(),
|
||||
_state: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Machine for ArbitraryPropertyMachine<IdleState> {
|
||||
#[inline(always)]
|
||||
fn reset(&mut self) {}
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.curr.into() {
|
||||
// Start of an arbitrary property
|
||||
Class::OpenBracket => {
|
||||
self.start_pos = cursor.pos;
|
||||
cursor.advance();
|
||||
self.transition::<ParsingPropertyState>().next(cursor)
|
||||
}
|
||||
|
||||
// Anything else is not a valid start of an arbitrary value
|
||||
_ => MachineState::Idle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Machine for ArbitraryPropertyMachine<ParsingPropertyState> {
|
||||
#[inline(always)]
|
||||
fn reset(&mut self) {}
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
let len = cursor.input.len();
|
||||
|
||||
match self.state {
|
||||
State::Idle => match cursor.curr.into() {
|
||||
// Start of an arbitrary property
|
||||
Class::OpenBracket => {
|
||||
self.start_pos = cursor.pos;
|
||||
self.state = State::ParsingProperty;
|
||||
while cursor.pos < len {
|
||||
match cursor.curr.into() {
|
||||
Class::Dash => match cursor.next.into() {
|
||||
// Start of a CSS variable
|
||||
//
|
||||
// E.g.: `[--my-color:red]`
|
||||
// ^^
|
||||
Class::Dash => return self.parse_property_variable(cursor),
|
||||
|
||||
// Dashes are allowed in the property name
|
||||
//
|
||||
// E.g.: `[background-color:red]`
|
||||
// ^
|
||||
_ => cursor.advance(),
|
||||
},
|
||||
|
||||
// Alpha characters are allowed in the property name
|
||||
//
|
||||
// E.g.: `[color:red]`
|
||||
// ^^^^^
|
||||
Class::AlphaLower => cursor.advance(),
|
||||
|
||||
// End of the property name, but there must be at least a single character
|
||||
Class::Colon if cursor.pos > self.start_pos + 1 => {
|
||||
cursor.advance();
|
||||
self.next(cursor)
|
||||
return self.transition::<ParsingValueState>().next(cursor);
|
||||
}
|
||||
|
||||
// Anything else is not a valid start of an arbitrary value
|
||||
_ => MachineState::Idle,
|
||||
},
|
||||
|
||||
State::ParsingProperty => {
|
||||
while cursor.pos < len {
|
||||
match cursor.curr.into() {
|
||||
Class::Dash => match cursor.next.into() {
|
||||
// Start of a CSS variable
|
||||
//
|
||||
// E.g.: `[--my-color:red]`
|
||||
// ^^
|
||||
Class::Dash => return self.parse_property_variable(cursor),
|
||||
|
||||
// Dashes are allowed in the property name
|
||||
//
|
||||
// E.g.: `[background-color:red]`
|
||||
// ^
|
||||
_ => cursor.advance(),
|
||||
},
|
||||
|
||||
// Alpha characters are allowed in the property name
|
||||
//
|
||||
// E.g.: `[color:red]`
|
||||
// ^^^^^
|
||||
Class::AlphaLower => cursor.advance(),
|
||||
|
||||
// End of the property name, but there must be at least a single character
|
||||
Class::Colon if cursor.pos > self.start_pos + 1 => {
|
||||
self.state = State::ParsingValue;
|
||||
cursor.advance();
|
||||
return self.next(cursor);
|
||||
}
|
||||
|
||||
// Anything else is not a valid property character
|
||||
_ => return self.restart(),
|
||||
}
|
||||
}
|
||||
|
||||
self.restart()
|
||||
}
|
||||
|
||||
State::ParsingValue => {
|
||||
while cursor.pos < len {
|
||||
match cursor.curr.into() {
|
||||
Class::Escape => match cursor.next.into() {
|
||||
// An escaped whitespace character is not allowed
|
||||
//
|
||||
// E.g.: `[color:var(--my-\ color)]`
|
||||
// ^
|
||||
Class::Whitespace => return self.restart(),
|
||||
|
||||
// An escaped character, skip the next character, resume after
|
||||
//
|
||||
// E.g.: `[color:var(--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 value
|
||||
//
|
||||
// 1. All brackets must be balanced
|
||||
// 2. There must be at least a single character inside the brackets
|
||||
Class::CloseBracket
|
||||
if self.start_pos + 1 != cursor.pos
|
||||
&& self.bracket_stack.is_empty() =>
|
||||
{
|
||||
return self.done(self.start_pos, cursor)
|
||||
}
|
||||
|
||||
// Start of a string
|
||||
Class::Quote => return self.parse_string(cursor),
|
||||
|
||||
// Another `:` inside of an arbitrary property is only valid inside of a string or
|
||||
// inside of brackets. Everywhere else, it's invalid.
|
||||
//
|
||||
// E.g.: `[color:red:blue]`
|
||||
// ^ Not valid
|
||||
// E.g.: `[background:url(https://example.com)]`
|
||||
// ^ Valid
|
||||
// E.g.: `[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(),
|
||||
|
||||
// Everything else is valid
|
||||
_ => cursor.advance(),
|
||||
};
|
||||
}
|
||||
|
||||
self.restart()
|
||||
// Anything else is not a valid property character
|
||||
_ => return self.restart(),
|
||||
}
|
||||
}
|
||||
|
||||
self.restart()
|
||||
}
|
||||
}
|
||||
|
||||
impl ArbitraryPropertyMachine {
|
||||
impl ArbitraryPropertyMachine<ParsingPropertyState> {
|
||||
fn parse_property_variable(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match self.css_variable_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
@ -202,9 +143,8 @@ impl ArbitraryPropertyMachine {
|
||||
// E.g.: `[--my-color:red]`
|
||||
// ^
|
||||
Class::Colon => {
|
||||
self.state = State::ParsingValue;
|
||||
cursor.advance_twice();
|
||||
self.next(cursor)
|
||||
self.transition::<ParsingValueState>().next(cursor)
|
||||
}
|
||||
|
||||
// Invalid arbitrary property
|
||||
@ -212,7 +152,86 @@ impl ArbitraryPropertyMachine {
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Machine for ArbitraryPropertyMachine<ParsingValueState> {
|
||||
#[inline(always)]
|
||||
fn reset(&mut self) {
|
||||
self.bracket_stack.reset();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
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.: `[color:var(--my-\ color)]`
|
||||
// ^
|
||||
Class::Whitespace => return self.restart(),
|
||||
|
||||
// An escaped character, skip the next character, resume after
|
||||
//
|
||||
// E.g.: `[color:var(--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 value
|
||||
//
|
||||
// 1. All brackets must be balanced
|
||||
// 2. There must be at least a single character inside the brackets
|
||||
Class::CloseBracket
|
||||
if self.start_pos + 1 != cursor.pos && self.bracket_stack.is_empty() =>
|
||||
{
|
||||
return self.done(self.start_pos, cursor)
|
||||
}
|
||||
|
||||
// Start of a string
|
||||
Class::Quote => return self.parse_string(cursor),
|
||||
|
||||
// Another `:` inside of an arbitrary property is only valid inside of a string or
|
||||
// inside of brackets. Everywhere else, it's invalid.
|
||||
//
|
||||
// E.g.: `[color:red:blue]`
|
||||
// ^ Not valid
|
||||
// E.g.: `[background:url(https://example.com)]`
|
||||
// ^ Valid
|
||||
// E.g.: `[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(),
|
||||
|
||||
// Everything else is valid
|
||||
_ => cursor.advance(),
|
||||
};
|
||||
}
|
||||
|
||||
self.restart()
|
||||
}
|
||||
}
|
||||
|
||||
impl ArbitraryPropertyMachine<ParsingValueState> {
|
||||
fn parse_string(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match self.string_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
@ -271,7 +290,7 @@ enum Class {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ArbitraryPropertyMachine;
|
||||
use super::{ArbitraryPropertyMachine, IdleState};
|
||||
use crate::extractor::machine::Machine;
|
||||
|
||||
#[test]
|
||||
@ -279,8 +298,8 @@ mod tests {
|
||||
fn test_arbitrary_property_machine_performance() {
|
||||
let input = r#"<button class="[color:red] [background-color:red] [--my-color:red] [background:url('https://example.com')]">"#.repeat(10);
|
||||
|
||||
ArbitraryPropertyMachine::test_throughput(1_000_000, &input);
|
||||
ArbitraryPropertyMachine::test_duration_once(&input);
|
||||
ArbitraryPropertyMachine::<IdleState>::test_throughput(1_000_000, &input);
|
||||
ArbitraryPropertyMachine::<IdleState>::test_duration_once(&input);
|
||||
|
||||
todo!()
|
||||
}
|
||||
@ -373,7 +392,7 @@ mod tests {
|
||||
r#"let classes = {primary:'{}'};"#,
|
||||
] {
|
||||
let input = wrapper.replace("{}", input);
|
||||
let actual = ArbitraryPropertyMachine::test_extract_all(&input);
|
||||
let actual = ArbitraryPropertyMachine::<IdleState>::test_extract_all(&input);
|
||||
|
||||
if actual != expected {
|
||||
dbg!(&input, &actual, &expected);
|
||||
|
||||
@ -4,6 +4,37 @@ 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.
|
||||
///
|
||||
@ -17,229 +48,219 @@ use classification_macros::ClassifyBytes;
|
||||
/// ^^^^^^^^^^^^^^
|
||||
/// ```
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ArbitraryVariableMachine {
|
||||
pub struct ArbitraryVariableMachine<State = IdleState> {
|
||||
/// Start position of the arbitrary variable
|
||||
start_pos: usize,
|
||||
|
||||
/// Track brackets to ensure they are balanced
|
||||
bracket_stack: BracketStack,
|
||||
|
||||
/// Current state of the machine
|
||||
state: State,
|
||||
|
||||
string_machine: StringMachine,
|
||||
css_variable_machine: CssVariableMachine,
|
||||
|
||||
_state: PhantomData<State>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
enum State {
|
||||
#[default]
|
||||
Idle,
|
||||
|
||||
/// Currently parsing the data type of the arbitrary variable
|
||||
///
|
||||
/// ```text
|
||||
/// (length:--my-opacity)
|
||||
/// ^^^^^^^
|
||||
/// ```
|
||||
ParsingDataType,
|
||||
|
||||
/// Currently parsing the inside of the arbitrary variable
|
||||
///
|
||||
/// ```text
|
||||
/// (--my-opacity)
|
||||
/// ^^^^^^^^^^^^
|
||||
/// ```
|
||||
Parsing,
|
||||
|
||||
/// Currently parsing the fallback of the arbitrary variable
|
||||
///
|
||||
/// ```text
|
||||
/// (--my-opacity,50%)
|
||||
/// ^^^^
|
||||
/// ```
|
||||
ParsingFallback,
|
||||
}
|
||||
|
||||
impl Machine for ArbitraryVariableMachine {
|
||||
impl<State> ArbitraryVariableMachine<State> {
|
||||
#[inline(always)]
|
||||
fn reset(&mut self) {
|
||||
self.start_pos = 0;
|
||||
self.state = State::Idle;
|
||||
self.bracket_stack.reset();
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
impl Machine for ArbitraryVariableMachine<IdleState> {
|
||||
#[inline(always)]
|
||||
fn reset(&mut self) {}
|
||||
|
||||
#[inline(always)]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
let class_curr = cursor.curr.into();
|
||||
let len = cursor.input.len();
|
||||
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)
|
||||
}
|
||||
|
||||
match self.state {
|
||||
State::Idle => match class_curr {
|
||||
// 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;
|
||||
self.state = State::Parsing;
|
||||
cursor.advance();
|
||||
self.next(cursor)
|
||||
}
|
||||
Class::AlphaLower => {
|
||||
self.start_pos = cursor.pos;
|
||||
cursor.advance();
|
||||
self.transition::<ParsingDataTypeState>().next(cursor)
|
||||
}
|
||||
|
||||
Class::AlphaLower => {
|
||||
self.start_pos = cursor.pos;
|
||||
self.state = State::ParsingDataType;
|
||||
cursor.advance();
|
||||
self.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,
|
||||
},
|
||||
|
||||
State::ParsingDataType => {
|
||||
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 => {
|
||||
self.state = State::Parsing;
|
||||
cursor.advance();
|
||||
return self.next(cursor);
|
||||
}
|
||||
|
||||
_ => return self.restart(),
|
||||
},
|
||||
|
||||
// Anything else is not a valid character
|
||||
_ => return self.restart(),
|
||||
};
|
||||
}
|
||||
self.restart()
|
||||
}
|
||||
|
||||
State::Parsing => 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 => {
|
||||
self.state = State::ParsingFallback;
|
||||
cursor.advance_twice(); // Skip the `,`
|
||||
self.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(),
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
State::ParsingFallback => {
|
||||
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(_) => {
|
||||
self.state = State::ParsingFallback;
|
||||
cursor.advance();
|
||||
return self.next(cursor);
|
||||
}
|
||||
},
|
||||
|
||||
// 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(),
|
||||
|
||||
// Everything else is valid
|
||||
_ => cursor.advance(),
|
||||
};
|
||||
}
|
||||
|
||||
self.restart()
|
||||
}
|
||||
// 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(),
|
||||
|
||||
// Everything else is valid
|
||||
_ => cursor.advance(),
|
||||
};
|
||||
}
|
||||
|
||||
self.restart()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, ClassifyBytes)]
|
||||
enum Class {
|
||||
#[bytes_range(b'a'..=b'z')]
|
||||
@ -306,15 +327,15 @@ enum Class {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ArbitraryVariableMachine;
|
||||
use crate::extractor::machine::Machine;
|
||||
use crate::extractor::{arbitrary_variable_machine::IdleState, machine::Machine};
|
||||
|
||||
#[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::test_throughput(100_000, &input);
|
||||
ArbitraryVariableMachine::test_duration_once(&input);
|
||||
ArbitraryVariableMachine::<IdleState>::test_throughput(100_000, &input);
|
||||
ArbitraryVariableMachine::<IdleState>::test_duration_once(&input);
|
||||
|
||||
todo!()
|
||||
}
|
||||
@ -353,7 +374,10 @@ mod tests {
|
||||
(r"(-)", vec![]),
|
||||
(r"(-my-color)", vec![]),
|
||||
] {
|
||||
assert_eq!(ArbitraryVariableMachine::test_extract_all(input), expected);
|
||||
assert_eq!(
|
||||
ArbitraryVariableMachine::<IdleState>::test_extract_all(input),
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,13 @@ use crate::extractor::arbitrary_variable_machine::ArbitraryVariableMachine;
|
||||
use crate::extractor::boundary::is_valid_after_boundary;
|
||||
use crate::extractor::machine::{Machine, MachineState};
|
||||
use classification_macros::ClassifyBytes;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct IdleState;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ParsingState;
|
||||
|
||||
/// Extracts named utilities from an input.
|
||||
///
|
||||
@ -17,305 +24,304 @@ use classification_macros::ClassifyBytes;
|
||||
/// ^^^^^^^^^^
|
||||
/// ```
|
||||
#[derive(Debug, Default)]
|
||||
pub struct NamedUtilityMachine {
|
||||
pub struct NamedUtilityMachine<State = IdleState> {
|
||||
/// Start position of the utility
|
||||
start_pos: usize,
|
||||
|
||||
/// Current state of the machine
|
||||
state: State,
|
||||
|
||||
arbitrary_variable_machine: ArbitraryVariableMachine,
|
||||
arbitrary_value_machine: ArbitraryValueMachine,
|
||||
|
||||
_state: PhantomData<State>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
enum State {
|
||||
#[default]
|
||||
Idle,
|
||||
|
||||
/// Parsing a utility
|
||||
Parsing,
|
||||
impl<State> NamedUtilityMachine<State> {
|
||||
#[inline(always)]
|
||||
fn transition<NextState>(&self) -> NamedUtilityMachine<NextState> {
|
||||
NamedUtilityMachine {
|
||||
start_pos: self.start_pos,
|
||||
arbitrary_variable_machine: Default::default(),
|
||||
arbitrary_value_machine: Default::default(),
|
||||
_state: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Machine for NamedUtilityMachine {
|
||||
impl Machine for NamedUtilityMachine<IdleState> {
|
||||
#[inline(always)]
|
||||
fn reset(&mut self) {}
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.curr.into() {
|
||||
Class::AlphaLower => match cursor.next.into() {
|
||||
// Valid single character utility in between quotes
|
||||
//
|
||||
// E.g.: `<div class="a"></div>`
|
||||
// ^
|
||||
// E.g.: `<div class="a "></div>`
|
||||
// ^
|
||||
// E.g.: `<div class=" a"></div>`
|
||||
// ^
|
||||
Class::Whitespace | Class::Quote | Class::End => self.done(cursor.pos, cursor),
|
||||
|
||||
// Valid start characters
|
||||
//
|
||||
// E.g.: `flex`
|
||||
// ^
|
||||
_ => {
|
||||
self.start_pos = cursor.pos;
|
||||
cursor.advance();
|
||||
self.transition::<ParsingState>().next(cursor)
|
||||
}
|
||||
},
|
||||
|
||||
// Valid start characters
|
||||
//
|
||||
// E.g.: `@container`
|
||||
// ^
|
||||
Class::At => {
|
||||
self.start_pos = cursor.pos;
|
||||
cursor.advance();
|
||||
self.transition::<ParsingState>().next(cursor)
|
||||
}
|
||||
|
||||
// Valid start of a negative utility, if followed by another set of valid
|
||||
// characters. `@` as a second character is invalid.
|
||||
//
|
||||
// E.g.: `-mx-2.5`
|
||||
// ^^
|
||||
Class::Dash => match cursor.next.into() {
|
||||
Class::AlphaLower => {
|
||||
self.start_pos = cursor.pos;
|
||||
cursor.advance();
|
||||
self.transition::<ParsingState>().next(cursor)
|
||||
}
|
||||
|
||||
// A dash should not be followed by anything else
|
||||
_ => MachineState::Idle,
|
||||
},
|
||||
|
||||
// Everything else, is not a valid start of the utility.
|
||||
_ => MachineState::Idle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Machine for NamedUtilityMachine<ParsingState> {
|
||||
#[inline(always)]
|
||||
fn reset(&mut self) {
|
||||
self.start_pos = 0;
|
||||
self.state = State::Idle;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
let len = cursor.input.len();
|
||||
|
||||
match self.state {
|
||||
State::Idle => match cursor.curr.into() {
|
||||
Class::AlphaLower => match cursor.next.into() {
|
||||
// Valid single character utility in between quotes
|
||||
//
|
||||
// E.g.: `<div class="a"></div>`
|
||||
// ^
|
||||
// E.g.: `<div class="a "></div>`
|
||||
// ^
|
||||
// E.g.: `<div class=" a"></div>`
|
||||
// ^
|
||||
Class::Whitespace | Class::Quote | Class::End => self.done(cursor.pos, cursor),
|
||||
|
||||
// Valid start characters
|
||||
//
|
||||
// E.g.: `flex`
|
||||
// ^
|
||||
_ => {
|
||||
self.start_pos = cursor.pos;
|
||||
self.state = State::Parsing;
|
||||
cursor.advance();
|
||||
self.next(cursor)
|
||||
}
|
||||
},
|
||||
|
||||
// Valid start characters
|
||||
while cursor.pos < len {
|
||||
match cursor.curr.into() {
|
||||
// Followed by a boundary character, we are at the end of the utility.
|
||||
//
|
||||
// E.g.: `@container`
|
||||
// ^
|
||||
Class::At => {
|
||||
self.start_pos = cursor.pos;
|
||||
self.state = State::Parsing;
|
||||
cursor.advance();
|
||||
self.next(cursor)
|
||||
// E.g.: `'flex'`
|
||||
// ^
|
||||
// E.g.: `<div class="flex items-center">`
|
||||
// ^
|
||||
// E.g.: `[flex]` (Angular syntax)
|
||||
// ^
|
||||
// E.g.: `[class.flex.items-center]` (Angular syntax)
|
||||
// ^
|
||||
// E.g.: `:div="{ flex: true }"` (JavaScript object syntax)
|
||||
// ^
|
||||
Class::AlphaLower | Class::AlphaUpper => {
|
||||
if is_valid_after_boundary(&cursor.next) || {
|
||||
// Or any of these characters
|
||||
//
|
||||
// - `:`, because of JS object keys
|
||||
// - `/`, because of modifiers
|
||||
// - `!`, because of important
|
||||
matches!(
|
||||
cursor.next.into(),
|
||||
Class::Colon | Class::Slash | Class::Exclamation
|
||||
)
|
||||
} {
|
||||
return self.done(self.start_pos, cursor);
|
||||
}
|
||||
|
||||
// Still valid characters
|
||||
cursor.advance()
|
||||
}
|
||||
|
||||
// Valid start of a negative utility, if followed by another set of valid
|
||||
// characters. `@` as a second character is invalid.
|
||||
//
|
||||
// E.g.: `-mx-2.5`
|
||||
// ^^
|
||||
Class::Dash => match cursor.next.into() {
|
||||
Class::AlphaLower => {
|
||||
self.start_pos = cursor.pos;
|
||||
self.state = State::Parsing;
|
||||
// Start of an arbitrary value
|
||||
//
|
||||
// E.g.: `bg-[#0088cc]`
|
||||
// ^^
|
||||
Class::OpenBracket => {
|
||||
cursor.advance();
|
||||
self.next(cursor)
|
||||
return match self.arbitrary_value_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => self.done(self.start_pos, cursor),
|
||||
};
|
||||
}
|
||||
|
||||
// A dash should not be followed by anything else
|
||||
_ => MachineState::Idle,
|
||||
// Start of an arbitrary variable
|
||||
//
|
||||
// E.g.: `bg-(--my-color)`
|
||||
// ^^
|
||||
Class::OpenParen => {
|
||||
cursor.advance();
|
||||
return match self.arbitrary_variable_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => self.done(self.start_pos, cursor),
|
||||
};
|
||||
}
|
||||
|
||||
// A dash is a valid character if it is followed by another valid
|
||||
// character.
|
||||
//
|
||||
// E.g.: `flex-`
|
||||
// ^ Invalid
|
||||
// E.g.: `flex-!`
|
||||
// ^ Invalid
|
||||
// E.g.: `flex-/`
|
||||
// ^ Invalid
|
||||
// E.g.: `flex-2`
|
||||
// ^ Valid
|
||||
// E.g.: `foo--bar`
|
||||
// ^ Valid
|
||||
Class::AlphaLower | Class::AlphaUpper | Class::Number | Class::Dash => {
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
// Everything else is invalid
|
||||
_ => return self.restart(),
|
||||
},
|
||||
|
||||
// Everything else, is not a valid start of the utility.
|
||||
_ => MachineState::Idle,
|
||||
},
|
||||
Class::Underscore => match cursor.next.into() {
|
||||
// Valid characters _if_ followed by another valid character. These characters are
|
||||
// only valid inside of the utility but not at the end of the utility.
|
||||
//
|
||||
// E.g.: `custom_`
|
||||
// ^ Invalid
|
||||
// E.g.: `custom_!`
|
||||
// ^ Invalid
|
||||
// E.g.: `custom_/`
|
||||
// ^ Invalid
|
||||
// E.g.: `custom_2`
|
||||
// ^ Valid
|
||||
//
|
||||
Class::AlphaLower | Class::AlphaUpper | Class::Number | Class::Underscore => {
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
State::Parsing => {
|
||||
while cursor.pos < len {
|
||||
match cursor.curr.into() {
|
||||
// Followed by a boundary character, we are at the end of the utility.
|
||||
// Followed by a boundary character, we are at the end of the utility.
|
||||
//
|
||||
// E.g.: `'flex'`
|
||||
// ^
|
||||
// E.g.: `<div class="flex items-center">`
|
||||
// ^
|
||||
// E.g.: `[flex]` (Angular syntax)
|
||||
// ^
|
||||
// E.g.: `[class.flex.items-center]` (Angular syntax)
|
||||
// ^
|
||||
// E.g.: `:div="{ flex: true }"` (JavaScript object syntax)
|
||||
// ^
|
||||
_ if is_valid_after_boundary(&cursor.next) || {
|
||||
// Or any of these characters
|
||||
//
|
||||
// E.g.: `'flex'`
|
||||
// ^
|
||||
// E.g.: `<div class="flex items-center">`
|
||||
// ^
|
||||
// E.g.: `[flex]` (Angular syntax)
|
||||
// ^
|
||||
// E.g.: `[class.flex.items-center]` (Angular syntax)
|
||||
// ^
|
||||
// E.g.: `:div="{ flex: true }"` (JavaScript object syntax)
|
||||
// ^
|
||||
Class::AlphaLower | Class::AlphaUpper => {
|
||||
if is_valid_after_boundary(&cursor.next) || {
|
||||
// Or any of these characters
|
||||
//
|
||||
// - `:`, because of JS object keys
|
||||
// - `/`, because of modifiers
|
||||
// - `!`, because of important
|
||||
matches!(
|
||||
cursor.next.into(),
|
||||
Class::Colon | Class::Slash | Class::Exclamation
|
||||
)
|
||||
} {
|
||||
return self.done(self.start_pos, cursor);
|
||||
}
|
||||
// - `:`, because of JS object keys
|
||||
// - `/`, because of modifiers
|
||||
// - `!`, because of important
|
||||
matches!(
|
||||
cursor.next.into(),
|
||||
Class::Colon | Class::Slash | Class::Exclamation
|
||||
)
|
||||
} =>
|
||||
{
|
||||
return self.done(self.start_pos, cursor)
|
||||
}
|
||||
|
||||
// Still valid characters
|
||||
cursor.advance()
|
||||
}
|
||||
// Everything else is invalid
|
||||
_ => return self.restart(),
|
||||
},
|
||||
|
||||
Class::Dash => match cursor.next.into() {
|
||||
// Start of an arbitrary value
|
||||
//
|
||||
// E.g.: `bg-[#0088cc]`
|
||||
// ^^
|
||||
Class::OpenBracket => {
|
||||
cursor.advance();
|
||||
return match self.arbitrary_value_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => self.done(self.start_pos, cursor),
|
||||
};
|
||||
}
|
||||
// A dot must be surrounded by numbers
|
||||
//
|
||||
// E.g.: `px-2.5`
|
||||
// ^^^
|
||||
Class::Dot => {
|
||||
if !matches!(cursor.prev.into(), Class::Number) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
// Start of an arbitrary variable
|
||||
//
|
||||
// E.g.: `bg-(--my-color)`
|
||||
// ^^
|
||||
Class::OpenParen => {
|
||||
cursor.advance();
|
||||
return match self.arbitrary_variable_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => self.done(self.start_pos, cursor),
|
||||
};
|
||||
}
|
||||
if !matches!(cursor.next.into(), Class::Number) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
// A dash is a valid character if it is followed by another valid
|
||||
// character.
|
||||
//
|
||||
// E.g.: `flex-`
|
||||
// ^ Invalid
|
||||
// E.g.: `flex-!`
|
||||
// ^ Invalid
|
||||
// E.g.: `flex-/`
|
||||
// ^ Invalid
|
||||
// E.g.: `flex-2`
|
||||
// ^ Valid
|
||||
// E.g.: `foo--bar`
|
||||
// ^ Valid
|
||||
Class::AlphaLower | Class::AlphaUpper | Class::Number | Class::Dash => {
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
// Everything else is invalid
|
||||
_ => return self.restart(),
|
||||
},
|
||||
|
||||
Class::Underscore => match cursor.next.into() {
|
||||
// Valid characters _if_ followed by another valid character. These characters are
|
||||
// only valid inside of the utility but not at the end of the utility.
|
||||
//
|
||||
// E.g.: `custom_`
|
||||
// ^ Invalid
|
||||
// E.g.: `custom_!`
|
||||
// ^ Invalid
|
||||
// E.g.: `custom_/`
|
||||
// ^ Invalid
|
||||
// E.g.: `custom_2`
|
||||
// ^ Valid
|
||||
//
|
||||
Class::AlphaLower
|
||||
| Class::AlphaUpper
|
||||
| Class::Number
|
||||
| Class::Underscore => {
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
// Followed by a boundary character, we are at the end of the utility.
|
||||
//
|
||||
// E.g.: `'flex'`
|
||||
// ^
|
||||
// E.g.: `<div class="flex items-center">`
|
||||
// ^
|
||||
// E.g.: `[flex]` (Angular syntax)
|
||||
// ^
|
||||
// E.g.: `[class.flex.items-center]` (Angular syntax)
|
||||
// ^
|
||||
// E.g.: `:div="{ flex: true }"` (JavaScript object syntax)
|
||||
// ^
|
||||
_ if is_valid_after_boundary(&cursor.next) || {
|
||||
// Or any of these characters
|
||||
//
|
||||
// - `:`, because of JS object keys
|
||||
// - `/`, because of modifiers
|
||||
// - `!`, because of important
|
||||
matches!(
|
||||
cursor.next.into(),
|
||||
Class::Colon | Class::Slash | Class::Exclamation
|
||||
)
|
||||
} =>
|
||||
{
|
||||
return self.done(self.start_pos, cursor)
|
||||
}
|
||||
|
||||
// Everything else is invalid
|
||||
_ => return self.restart(),
|
||||
},
|
||||
|
||||
// A dot must be surrounded by numbers
|
||||
//
|
||||
// E.g.: `px-2.5`
|
||||
// ^^^
|
||||
Class::Dot => {
|
||||
if !matches!(cursor.prev.into(), Class::Number) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
if !matches!(cursor.next.into(), Class::Number) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
// A number must be preceded by a `-`, `.` or another alphanumeric
|
||||
// character, and can be followed by a `.` or an alphanumeric character or
|
||||
// dash or underscore.
|
||||
//
|
||||
// E.g.: `text-2xs`
|
||||
// ^^
|
||||
// `p-2.5`
|
||||
// ^^
|
||||
// `bg-red-500`
|
||||
// ^^
|
||||
// It can also be followed by a %, but that will be considered the end of
|
||||
// the candidate.
|
||||
//
|
||||
// E.g.: `from-15%`
|
||||
// ^
|
||||
//
|
||||
Class::Number => {
|
||||
if !matches!(
|
||||
cursor.prev.into(),
|
||||
Class::Dash | Class::Dot | Class::Number | Class::AlphaLower
|
||||
) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
if !matches!(
|
||||
cursor.next.into(),
|
||||
Class::Dot
|
||||
| Class::Number
|
||||
| Class::AlphaLower
|
||||
| Class::AlphaUpper
|
||||
| Class::Percent
|
||||
| Class::Underscore
|
||||
| Class::Dash
|
||||
) {
|
||||
return self.done(self.start_pos, cursor);
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
// A percent sign must be preceded by a number.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// ```
|
||||
// from-15%
|
||||
// ^^
|
||||
// ```
|
||||
Class::Percent => {
|
||||
if !matches!(cursor.prev.into(), Class::Number) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
return self.done(self.start_pos, cursor);
|
||||
}
|
||||
|
||||
// Everything else is invalid
|
||||
_ => return self.restart(),
|
||||
};
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
self.restart()
|
||||
}
|
||||
// A number must be preceded by a `-`, `.` or another alphanumeric
|
||||
// character, and can be followed by a `.` or an alphanumeric character or
|
||||
// dash or underscore.
|
||||
//
|
||||
// E.g.: `text-2xs`
|
||||
// ^^
|
||||
// `p-2.5`
|
||||
// ^^
|
||||
// `bg-red-500`
|
||||
// ^^
|
||||
// It can also be followed by a %, but that will be considered the end of
|
||||
// the candidate.
|
||||
//
|
||||
// E.g.: `from-15%`
|
||||
// ^
|
||||
//
|
||||
Class::Number => {
|
||||
if !matches!(
|
||||
cursor.prev.into(),
|
||||
Class::Dash | Class::Dot | Class::Number | Class::AlphaLower
|
||||
) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
if !matches!(
|
||||
cursor.next.into(),
|
||||
Class::Dot
|
||||
| Class::Number
|
||||
| Class::AlphaLower
|
||||
| Class::AlphaUpper
|
||||
| Class::Percent
|
||||
| Class::Underscore
|
||||
| Class::Dash
|
||||
) {
|
||||
return self.done(self.start_pos, cursor);
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
// A percent sign must be preceded by a number.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// ```
|
||||
// from-15%
|
||||
// ^^
|
||||
// ```
|
||||
Class::Percent => {
|
||||
if !matches!(cursor.prev.into(), Class::Number) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
return self.done(self.start_pos, cursor);
|
||||
}
|
||||
|
||||
// Everything else is invalid
|
||||
_ => return self.restart(),
|
||||
};
|
||||
}
|
||||
|
||||
self.restart()
|
||||
}
|
||||
}
|
||||
|
||||
@ -378,7 +384,7 @@ enum Class {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::NamedUtilityMachine;
|
||||
use super::{IdleState, NamedUtilityMachine};
|
||||
use crate::extractor::machine::Machine;
|
||||
|
||||
#[test]
|
||||
@ -386,8 +392,8 @@ mod tests {
|
||||
fn test_named_utility_machine_performance() {
|
||||
let input = r#"<button class="flex items-center px-2.5 -inset-x-2 bg-[#0088cc] text-(--my-color)">"#;
|
||||
|
||||
NamedUtilityMachine::test_throughput(1_000_000, input);
|
||||
NamedUtilityMachine::test_duration_once(input);
|
||||
NamedUtilityMachine::<IdleState>::test_throughput(1_000_000, input);
|
||||
NamedUtilityMachine::<IdleState>::test_duration_once(input);
|
||||
|
||||
todo!()
|
||||
}
|
||||
@ -505,7 +511,7 @@ mod tests {
|
||||
expected.extend(additional);
|
||||
expected.sort();
|
||||
|
||||
let mut actual = NamedUtilityMachine::test_extract_all(&input);
|
||||
let mut actual = NamedUtilityMachine::<IdleState>::test_extract_all(&input);
|
||||
actual.sort();
|
||||
|
||||
if actual != expected {
|
||||
|
||||
@ -4,6 +4,37 @@ use crate::extractor::arbitrary_variable_machine::ArbitraryVariableMachine;
|
||||
use crate::extractor::machine::{Machine, MachineState};
|
||||
use crate::extractor::modifier_machine::ModifierMachine;
|
||||
use classification_macros::ClassifyBytes;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct IdleState;
|
||||
|
||||
/// Parsing a variant
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ParsingState;
|
||||
|
||||
/// Parsing a modifier
|
||||
///
|
||||
/// E.g.:
|
||||
///
|
||||
/// ```text
|
||||
/// group-hover/name:
|
||||
/// ^^^^^
|
||||
/// ```
|
||||
///
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ParsingModifierState;
|
||||
|
||||
/// Parsing the end of a variant
|
||||
///
|
||||
/// E.g.:
|
||||
///
|
||||
/// ```text
|
||||
/// hover:
|
||||
/// ^
|
||||
/// ```
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ParsingEndState;
|
||||
|
||||
/// Extract named variants from an input including the `:`.
|
||||
///
|
||||
@ -20,244 +51,233 @@ use classification_macros::ClassifyBytes;
|
||||
/// ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
/// ```
|
||||
#[derive(Debug, Default)]
|
||||
pub struct NamedVariantMachine {
|
||||
pub struct NamedVariantMachine<State = IdleState> {
|
||||
/// Start position of the variant
|
||||
start_pos: usize,
|
||||
|
||||
/// Current state of the machine
|
||||
state: State,
|
||||
|
||||
arbitrary_variable_machine: ArbitraryVariableMachine,
|
||||
arbitrary_value_machine: ArbitraryValueMachine,
|
||||
modifier_machine: ModifierMachine,
|
||||
|
||||
_state: PhantomData<State>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
enum State {
|
||||
#[default]
|
||||
Idle,
|
||||
|
||||
/// Parsing a variant
|
||||
Parsing,
|
||||
|
||||
/// Parsing a modifier
|
||||
///
|
||||
/// E.g.:
|
||||
///
|
||||
/// ```text
|
||||
/// group-hover/name:
|
||||
/// ^^^^^
|
||||
/// ```
|
||||
///
|
||||
ParsingModifier,
|
||||
|
||||
/// Parsing the end of a variant
|
||||
///
|
||||
/// E.g.:
|
||||
///
|
||||
/// ```text
|
||||
/// hover:
|
||||
/// ^
|
||||
/// ```
|
||||
ParseEnd,
|
||||
}
|
||||
|
||||
impl Machine for NamedVariantMachine {
|
||||
impl<State> NamedVariantMachine<State> {
|
||||
#[inline(always)]
|
||||
fn reset(&mut self) {
|
||||
self.start_pos = 0;
|
||||
self.state = State::Idle;
|
||||
fn transition<NextState>(&self) -> NamedVariantMachine<NextState> {
|
||||
NamedVariantMachine {
|
||||
start_pos: self.start_pos,
|
||||
arbitrary_variable_machine: Default::default(),
|
||||
arbitrary_value_machine: Default::default(),
|
||||
modifier_machine: Default::default(),
|
||||
_state: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
impl Machine for NamedVariantMachine<IdleState> {
|
||||
#[inline(always)]
|
||||
fn reset(&mut self) {}
|
||||
|
||||
#[inline(always)]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
let len = cursor.input.len();
|
||||
|
||||
match self.state {
|
||||
State::Idle => match cursor.curr.into() {
|
||||
Class::AlphaLower | Class::Star => match cursor.next.into() {
|
||||
// Valid single character variant, must be followed by a `:`
|
||||
//
|
||||
// E.g.: `<div class="x:flex"></div>`
|
||||
// ^^
|
||||
// E.g.: `*:`
|
||||
// ^^
|
||||
Class::Colon => {
|
||||
self.state = State::ParseEnd;
|
||||
cursor.advance();
|
||||
self.next(cursor)
|
||||
}
|
||||
|
||||
// Valid start characters
|
||||
//
|
||||
// E.g.: `hover:`
|
||||
// ^
|
||||
// E.g.: `**:`
|
||||
// ^
|
||||
_ => {
|
||||
self.start_pos = cursor.pos;
|
||||
self.state = State::Parsing;
|
||||
cursor.advance();
|
||||
self.next(cursor)
|
||||
}
|
||||
},
|
||||
match cursor.curr.into() {
|
||||
Class::AlphaLower | Class::Star => match cursor.next.into() {
|
||||
// Valid single character variant, must be followed by a `:`
|
||||
//
|
||||
// E.g.: `<div class="x:flex"></div>`
|
||||
// ^^
|
||||
// E.g.: `*:`
|
||||
// ^^
|
||||
Class::Colon => {
|
||||
cursor.advance();
|
||||
self.transition::<ParsingEndState>().next(cursor)
|
||||
}
|
||||
|
||||
// Valid start characters
|
||||
//
|
||||
// E.g.: `2xl:`
|
||||
// E.g.: `hover:`
|
||||
// ^
|
||||
// E.g.: `@md:`
|
||||
// E.g.: `**:`
|
||||
// ^
|
||||
Class::Number | Class::At => {
|
||||
_ => {
|
||||
self.start_pos = cursor.pos;
|
||||
self.state = State::Parsing;
|
||||
cursor.advance();
|
||||
self.next(cursor)
|
||||
self.transition::<ParsingState>().next(cursor)
|
||||
}
|
||||
|
||||
// Everything else, is not a valid start of the variant.
|
||||
_ => MachineState::Idle,
|
||||
},
|
||||
|
||||
State::Parsing => {
|
||||
while cursor.pos < len {
|
||||
match cursor.curr.into() {
|
||||
Class::Dash => match cursor.next.into() {
|
||||
// Start of an arbitrary value
|
||||
//
|
||||
// E.g.: `data-[state=pending]:`.
|
||||
// ^^
|
||||
Class::OpenBracket => {
|
||||
cursor.advance();
|
||||
// Valid start characters
|
||||
//
|
||||
// E.g.: `2xl:`
|
||||
// ^
|
||||
// E.g.: `@md:`
|
||||
// ^
|
||||
Class::Number | Class::At => {
|
||||
self.start_pos = cursor.pos;
|
||||
cursor.advance();
|
||||
self.transition::<ParsingState>().next(cursor)
|
||||
}
|
||||
|
||||
return match self.arbitrary_value_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => self.parse_arbitrary_end(cursor),
|
||||
};
|
||||
}
|
||||
// Everything else, is not a valid start of the variant.
|
||||
_ => MachineState::Idle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start of an arbitrary variable
|
||||
//
|
||||
// E.g.: `supports-(--my-color):`.
|
||||
// ^^
|
||||
Class::OpenParen => {
|
||||
cursor.advance();
|
||||
return match self.arbitrary_variable_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => self.parse_arbitrary_end(cursor),
|
||||
};
|
||||
}
|
||||
impl Machine for NamedVariantMachine<ParsingState> {
|
||||
#[inline(always)]
|
||||
fn reset(&mut self) {}
|
||||
|
||||
// Valid characters _if_ followed by another valid character. These characters are
|
||||
// only valid inside of the variant but not at the end of the variant.
|
||||
//
|
||||
// E.g.: `hover-`
|
||||
// ^ Invalid
|
||||
// E.g.: `hover-!`
|
||||
// ^ Invalid
|
||||
// E.g.: `hover-/`
|
||||
// ^ Invalid
|
||||
// E.g.: `flex-1`
|
||||
// ^ Valid
|
||||
Class::Dash
|
||||
| Class::Underscore
|
||||
| Class::AlphaLower
|
||||
| Class::AlphaUpper
|
||||
| Class::Number => cursor.advance(),
|
||||
#[inline(always)]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
let len = cursor.input.len();
|
||||
|
||||
// Everything else is invalid
|
||||
_ => return self.restart(),
|
||||
},
|
||||
while cursor.pos < len {
|
||||
match cursor.curr.into() {
|
||||
Class::Dash => match cursor.next.into() {
|
||||
// Start of an arbitrary value
|
||||
//
|
||||
// E.g.: `data-[state=pending]:`.
|
||||
// ^^
|
||||
Class::OpenBracket => {
|
||||
cursor.advance();
|
||||
return match self.arbitrary_value_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => self.parse_arbitrary_end(cursor),
|
||||
};
|
||||
}
|
||||
|
||||
// Start of an arbitrary value
|
||||
//
|
||||
// E.g.: `@[state=pending]:`.
|
||||
// ^
|
||||
Class::OpenBracket => {
|
||||
return match self.arbitrary_value_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => self.parse_arbitrary_end(cursor),
|
||||
};
|
||||
}
|
||||
// Start of an arbitrary variable
|
||||
//
|
||||
// E.g.: `supports-(--my-color):`.
|
||||
// ^^
|
||||
Class::OpenParen => {
|
||||
cursor.advance();
|
||||
return match self.arbitrary_variable_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => self.parse_arbitrary_end(cursor),
|
||||
};
|
||||
}
|
||||
|
||||
Class::Underscore => match cursor.next.into() {
|
||||
// Valid characters _if_ followed by another valid character. These characters are
|
||||
// only valid inside of the variant but not at the end of the variant.
|
||||
//
|
||||
// E.g.: `hover_`
|
||||
// ^ Invalid
|
||||
// E.g.: `hover_!`
|
||||
// ^ Invalid
|
||||
// E.g.: `hover_/`
|
||||
// ^ Invalid
|
||||
// E.g.: `custom_1`
|
||||
// ^ Valid
|
||||
Class::Dash
|
||||
| Class::Underscore
|
||||
| Class::AlphaLower
|
||||
| Class::AlphaUpper
|
||||
| Class::Number => cursor.advance(),
|
||||
// Valid characters _if_ followed by another valid character. These characters are
|
||||
// only valid inside of the variant but not at the end of the variant.
|
||||
//
|
||||
// E.g.: `hover-`
|
||||
// ^ Invalid
|
||||
// E.g.: `hover-!`
|
||||
// ^ Invalid
|
||||
// E.g.: `hover-/`
|
||||
// ^ Invalid
|
||||
// E.g.: `flex-1`
|
||||
// ^ Valid
|
||||
Class::Dash
|
||||
| Class::Underscore
|
||||
| Class::AlphaLower
|
||||
| Class::AlphaUpper
|
||||
| Class::Number => cursor.advance(),
|
||||
|
||||
// Everything else is invalid
|
||||
_ => return self.restart(),
|
||||
},
|
||||
// Everything else is invalid
|
||||
_ => return self.restart(),
|
||||
},
|
||||
|
||||
// Still valid characters
|
||||
Class::AlphaLower | Class::AlphaUpper | Class::Number | Class::Star => {
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
// A `/` means we are at the end of the variant, but there might be a modifier
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// ```
|
||||
// group-hover/name:
|
||||
// ^
|
||||
// ```
|
||||
Class::Slash => {
|
||||
self.state = State::ParsingModifier;
|
||||
return self.next(cursor);
|
||||
}
|
||||
|
||||
// A `:` means we are at the end of the variant
|
||||
//
|
||||
// E.g.: `hover:`
|
||||
// ^
|
||||
Class::Colon => return self.done(self.start_pos, cursor),
|
||||
|
||||
// Everything else is invalid
|
||||
_ => return self.restart(),
|
||||
// Start of an arbitrary value
|
||||
//
|
||||
// E.g.: `@[state=pending]:`.
|
||||
// ^
|
||||
Class::OpenBracket => {
|
||||
return match self.arbitrary_value_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => self.parse_arbitrary_end(cursor),
|
||||
};
|
||||
}
|
||||
|
||||
self.restart()
|
||||
}
|
||||
|
||||
State::ParsingModifier => match self.modifier_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => match cursor.next.into() {
|
||||
// Modifier must be followed by a `:`
|
||||
Class::Underscore => match cursor.next.into() {
|
||||
// Valid characters _if_ followed by another valid character. These characters are
|
||||
// only valid inside of the variant but not at the end of the variant.
|
||||
//
|
||||
// E.g.: `group-hover/name:`
|
||||
// ^
|
||||
Class::Colon => {
|
||||
self.state = State::ParseEnd;
|
||||
cursor.advance();
|
||||
self.next(cursor)
|
||||
}
|
||||
// E.g.: `hover_`
|
||||
// ^ Invalid
|
||||
// E.g.: `hover_!`
|
||||
// ^ Invalid
|
||||
// E.g.: `hover_/`
|
||||
// ^ Invalid
|
||||
// E.g.: `custom_1`
|
||||
// ^ Valid
|
||||
Class::Dash
|
||||
| Class::Underscore
|
||||
| Class::AlphaLower
|
||||
| Class::AlphaUpper
|
||||
| Class::Number => cursor.advance(),
|
||||
|
||||
// Everything else is invalid
|
||||
_ => self.restart(),
|
||||
_ => return self.restart(),
|
||||
},
|
||||
},
|
||||
|
||||
State::ParseEnd => match cursor.curr.into() {
|
||||
// The end of a variant must be the `:`
|
||||
// Still valid characters
|
||||
Class::AlphaLower | Class::AlphaUpper | Class::Number | Class::Star => {
|
||||
cursor.advance()
|
||||
}
|
||||
|
||||
// A `/` means we are at the end of the variant, but there might be a modifier
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// ```
|
||||
// group-hover/name:
|
||||
// ^
|
||||
// ```
|
||||
Class::Slash => return self.transition::<ParsingModifierState>().next(cursor),
|
||||
|
||||
// A `:` means we are at the end of the variant
|
||||
//
|
||||
// E.g.: `hover:`
|
||||
// ^
|
||||
Class::Colon => self.done(self.start_pos, cursor),
|
||||
Class::Colon => return self.done(self.start_pos, cursor),
|
||||
|
||||
// Everything else is invalid
|
||||
_ => return self.restart(),
|
||||
};
|
||||
}
|
||||
|
||||
self.restart()
|
||||
}
|
||||
}
|
||||
|
||||
impl NamedVariantMachine<ParsingState> {
|
||||
#[inline(always)]
|
||||
fn parse_arbitrary_end(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.next.into() {
|
||||
Class::Slash => {
|
||||
cursor.advance();
|
||||
self.transition::<ParsingModifierState>().next(cursor)
|
||||
}
|
||||
Class::Colon => {
|
||||
cursor.advance();
|
||||
self.transition::<ParsingEndState>().next(cursor)
|
||||
}
|
||||
_ => self.restart(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Machine for NamedVariantMachine<ParsingModifierState> {
|
||||
#[inline(always)]
|
||||
fn reset(&mut self) {}
|
||||
|
||||
#[inline(always)]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match self.modifier_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => match cursor.next.into() {
|
||||
// Modifier must be followed by a `:`
|
||||
//
|
||||
// E.g.: `group-hover/name:`
|
||||
// ^
|
||||
Class::Colon => {
|
||||
cursor.advance();
|
||||
self.transition::<ParsingEndState>().next(cursor)
|
||||
}
|
||||
|
||||
// Everything else is invalid
|
||||
_ => self.restart(),
|
||||
@ -266,20 +286,20 @@ impl Machine for NamedVariantMachine {
|
||||
}
|
||||
}
|
||||
|
||||
impl NamedVariantMachine {
|
||||
impl Machine for NamedVariantMachine<ParsingEndState> {
|
||||
#[inline(always)]
|
||||
fn parse_arbitrary_end(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.next.into() {
|
||||
Class::Slash => {
|
||||
self.state = State::ParsingModifier;
|
||||
cursor.advance();
|
||||
self.next(cursor)
|
||||
}
|
||||
Class::Colon => {
|
||||
self.state = State::ParseEnd;
|
||||
cursor.advance();
|
||||
self.next(cursor)
|
||||
}
|
||||
fn reset(&mut self) {}
|
||||
|
||||
#[inline(always)]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.curr.into() {
|
||||
// The end of a variant must be the `:`
|
||||
//
|
||||
// E.g.: `hover:`
|
||||
// ^
|
||||
Class::Colon => self.done(self.start_pos, cursor),
|
||||
|
||||
// Everything else is invalid
|
||||
_ => self.restart(),
|
||||
}
|
||||
}
|
||||
@ -341,7 +361,7 @@ enum Class {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::NamedVariantMachine;
|
||||
use super::{IdleState, NamedVariantMachine};
|
||||
use crate::extractor::machine::Machine;
|
||||
|
||||
#[test]
|
||||
@ -349,8 +369,8 @@ mod tests {
|
||||
fn test_named_variant_machine_performance() {
|
||||
let input = r#"<button class="hover:focus:flex data-[state=pending]:flex supports-(--my-variable):flex group-hover/named:not-has-peer-data-disabled:flex">"#;
|
||||
|
||||
NamedVariantMachine::test_throughput(1_000_000, input);
|
||||
NamedVariantMachine::test_duration_once(input);
|
||||
NamedVariantMachine::<IdleState>::test_throughput(1_000_000, input);
|
||||
NamedVariantMachine::<IdleState>::test_duration_once(input);
|
||||
|
||||
todo!()
|
||||
}
|
||||
@ -385,7 +405,7 @@ mod tests {
|
||||
// Single letter variant with uppercase letter is invalid
|
||||
("A:", vec![]),
|
||||
] {
|
||||
let actual = NamedVariantMachine::test_extract_all(input);
|
||||
let actual = NamedVariantMachine::<IdleState>::test_extract_all(input);
|
||||
if actual != expected {
|
||||
dbg!(&input, &actual, &expected);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user