Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
1bde46d
start of Oxide scanner improvements
RobinMalfait Feb 6, 2025
d9369ea
bump Rust version
RobinMalfait Feb 24, 2025
157ef40
bump Rust dependencies
RobinMalfait Feb 24, 2025
85341d7
apply `cargo clippy`
RobinMalfait Feb 24, 2025
8208f1d
make `move_to` less branchy
RobinMalfait Feb 24, 2025
15aaae6
add `advance()` shorthand
RobinMalfait Feb 24, 2025
529c92c
add throughput helper for benchmarks
RobinMalfait Feb 26, 2025
6dd19f7
add example fixture file
RobinMalfait Feb 26, 2025
a945f51
adjust internal `ChangedContent` use an enum instead
RobinMalfait Feb 26, 2025
8deb2ab
add `Machine`
RobinMalfait Feb 26, 2025
49885ce
add `BracketStack`
RobinMalfait Feb 26, 2025
4a78d4c
add `StringMachine`
RobinMalfait Feb 26, 2025
d9aa816
add `CssVariableMachine`
RobinMalfait Feb 26, 2025
bfe611d
add `ArbitraryValueMachine`
RobinMalfait Feb 26, 2025
38748b4
add `ArbitraryVariableMachine`
RobinMalfait Feb 26, 2025
dc66379
add `ArbitraryPropertyMachine`
RobinMalfait Feb 26, 2025
4763d3f
add `ModifierMachine`
RobinMalfait Feb 26, 2025
4f1f668
add `NamedVariantMachine`
RobinMalfait Feb 26, 2025
68c44fa
add `NamedUtilityMachine`
RobinMalfait Feb 26, 2025
ef366d0
add `UtilityMachine`
RobinMalfait Feb 26, 2025
e61fd30
add `VariantMachine`
RobinMalfait Feb 26, 2025
97eb783
add `CandidateMachine`
RobinMalfait Feb 26, 2025
b1c2be4
add new extractor
RobinMalfait Feb 26, 2025
b7027f9
remove old parser
RobinMalfait Feb 26, 2025
bdf2e81
wire up new Extractor
RobinMalfait Feb 26, 2025
f911de9
adjust test from the scanner
RobinMalfait Feb 26, 2025
cc11fb5
update changelog
RobinMalfait Feb 26, 2025
60e5f67
add `advance_twice` method
RobinMalfait Feb 26, 2025
3ab7a52
use `cursor.advance_twice()`
RobinMalfait Feb 26, 2025
6bbf003
simplify ArbitraryValueMachine
RobinMalfait Feb 26, 2025
21fe019
support emoji in CSS Variables
RobinMalfait Feb 26, 2025
f083c9d
remove `cursor.rewind_by`
RobinMalfait Feb 26, 2025
36905b5
Merge branch 'main' into feat/improve-oxide-scanner
RobinMalfait Feb 27, 2025
8cf78f0
add dedicated pre-processors for certain file extensions
RobinMalfait Feb 27, 2025
869e557
remove tests when we already have dedicated tests for them
RobinMalfait Feb 27, 2025
b00d370
add more angular binding attribute tests
RobinMalfait Feb 27, 2025
a11c471
adjust tests with better boundaries
RobinMalfait Feb 27, 2025
b0968b6
add a few more CSS Variable tests
RobinMalfait Feb 27, 2025
3d8c41a
improve comments and fix typos
RobinMalfait Feb 27, 2025
9564525
drop `Class::AlphaUpper`
RobinMalfait Feb 27, 2025
fb235a2
use a set_range!
RobinMalfait Feb 27, 2025
df51fba
Merge branch 'main' into feat/improve-oxide-scanner
RobinMalfait Feb 27, 2025
072d8d2
Merge branch 'main' into feat/improve-oxide-scanner
RobinMalfait Mar 2, 2025
995948d
Merge branch 'main' into feat/improve-oxide-scanner
RobinMalfait Mar 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions crates/oxide/src/extractor/modifier_machine.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
use crate::cursor;
use crate::extractor::arbitrary_value_machine::ArbitraryValueMachine;
use crate::extractor::arbitrary_variable_machine::ArbitraryVariableMachine;
use crate::extractor::machine::{Machine, MachineState};

/// Extract modifiers from an input including the `/`.
///
/// E.g.:
///
/// ```text
/// bg-red-500/20
/// ^^^
///
/// bg-red-500/[20%]
/// ^^^^^^
///
/// bg-red-500/(--my-opacity)
/// ^^^^^^^^^^^^^^^
/// ```
#[derive(Debug, Default)]
pub struct ModifierMachine {
arbitrary_value_machine: ArbitraryValueMachine,
arbitrary_variable_machine: ArbitraryVariableMachine,
}

impl Machine for ModifierMachine {
#[inline(always)]
fn reset(&mut self) {}

#[inline]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
// A modifier must start with a `/`, everything else is not a valid start of a modifier
if CLASS_TABLE[cursor.curr as usize] != Class::Slash {
return MachineState::Idle;
}

let start_pos = cursor.pos;
cursor.advance();

match CLASS_TABLE[cursor.curr as usize] {
// Start of an arbitrary value:
//
// ```
// bg-red-500/[20%]
// ^^^^^
// ```
Class::OpenBracket => match self.arbitrary_value_machine.next(cursor) {
MachineState::Idle => self.restart(),
MachineState::Done(_) => self.done(start_pos, cursor),
},

// Start of an arbitrary variable:
//
// ```
// bg-red-500/[20%]
// ^^^^^
// ```
Class::OpenParen => match self.arbitrary_variable_machine.next(cursor) {
MachineState::Idle => self.restart(),
MachineState::Done(_) => self.done(start_pos, cursor),
},

// Start of a named modifier:
//
// ```
// bg-red-500/20
// ^^
// ```
Class::ValidStart => {
let len = cursor.input.len();
while cursor.pos < len {
match CLASS_TABLE[cursor.curr as usize] {
Class::ValidStart | Class::ValidInside => {
match CLASS_TABLE[cursor.next as usize] {
// Only valid characters are allowed, if followed by another valid character
Class::ValidStart | Class::ValidInside => cursor.advance(),

// Valid character, but at the end of the modifier, this ends the
// modifier
_ => return self.done(start_pos, cursor),
}
}

// Anything else is invalid, end of the modifier
_ => return self.restart(),
}
}

MachineState::Idle
}

// Anything else is not a valid start of a modifier
_ => MachineState::Idle,
}
}
}

#[derive(Debug, Clone, Copy, PartialEq)]
enum Class {
/// `'a'..='z' | 'A'..='Z' | '0'..='9'`
ValidStart,

/// `-`, `_`, `.`
ValidInside,

/// `[`
OpenBracket,

/// `(`
OpenParen,

/// `/`
Slash,

Other,
}

const CLASS_TABLE: [Class; 256] = {
let mut table = [Class::Other; 256];

macro_rules! set {
($class:expr, $($byte:expr),+ $(,)?) => {
$(table[$byte as usize] = $class;)+
};
}

macro_rules! set_range {
($class:expr, $start:literal ..= $end:literal) => {
let mut i = $start;
while i <= $end {
table[i as usize] = $class;
i += 1;
}
};
}

set_range!(Class::ValidStart, b'a'..=b'z');
set_range!(Class::ValidStart, b'A'..=b'Z');
set_range!(Class::ValidStart, b'0'..=b'9');

set!(Class::OpenBracket, b'[');
set!(Class::OpenParen, b'(');

set!(Class::Slash, b'/');

set!(Class::ValidInside, b'-', b'_', b'.');

table
};

#[cfg(test)]
mod tests {
use super::ModifierMachine;
use crate::extractor::machine::Machine;

#[test]
#[ignore]
fn test_modifier_machine_performance() {
let input = r#"<button class="group-hover/name:flex bg-red-500/20 text-black/[20%] border-white/(--my-opacity)">"#;

ModifierMachine::test_throughput(1_000_000, input);
ModifierMachine::test_duration_once(input);

todo!()
}

#[test]
fn test_modifier_extraction() {
for (input, expected) in [
// Simple modifier
("foo/bar", vec!["/bar"]),
("foo/bar-baz", vec!["/bar-baz"]),
// Simple modifier with numbers
("foo/20", vec!["/20"]),
// Simple modifier with numbers
("foo/20", vec!["/20"]),
// Arbitrary value
("foo/[20]", vec!["/[20]"]),
// Arbitrary value with CSS variable shorthand
("foo/(--x)", vec!["/(--x)"]),
("foo/(--foo-bar)", vec!["/(--foo-bar)"]),
// --------------------------------------------------------

// Empty arbitrary value is not allowed
("foo/[]", vec![]),
// Empty arbitrary value shorthand is not allowed
("foo/()", vec![]),
// A CSS variable must start with `--` and must have at least a single character
("foo/(-)", vec![]),
("foo/(--)", vec![]),
// Arbitrary value shorthand should be a valid CSS variable
("foo/(--my#color)", vec![]),
] {
assert_eq!(ModifierMachine::test_extract_all(input), expected);
}
}
}