use crate::cursor;
use crate::extractor::bracket_stack::BracketStack;
use crate::extractor::machine::{Machine, MachineState};
use crate::extractor::string_machine::StringMachine;
use classification_macros::ClassifyBytes;
#[derive(Debug, Default)]
pub struct ArbitraryValueMachine {
bracket_stack: BracketStack,
string_machine: StringMachine,
}
impl Machine for ArbitraryValueMachine {
#[inline(always)]
fn reset(&mut self) {
self.bracket_stack.reset();
}
#[inline]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
if Class::OpenBracket != cursor.curr.into() {
return MachineState::Idle;
}
let start_pos = cursor.pos;
cursor.advance();
let len = cursor.input.len();
while cursor.pos < len {
match cursor.curr.into() {
Class::Escape => match cursor.next.into() {
Class::Whitespace => {
cursor.advance_twice();
return self.restart();
}
_ => 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();
}
Class::CloseBracket
if start_pos + 1 != cursor.pos && self.bracket_stack.is_empty() =>
{
return self.done(start_pos, cursor);
}
Class::Quote => match self.string_machine.next(cursor) {
MachineState::Idle => return self.restart(),
MachineState::Done(_) => cursor.advance(),
},
Class::Whitespace => return self.restart(),
Class::Dollar if matches!(cursor.next.into(), Class::OpenCurly) => {
return self.restart()
}
_ => cursor.advance(),
};
}
self.restart()
}
}
#[derive(Clone, Copy, PartialEq, ClassifyBytes)]
enum Class {
#[bytes(b'\\')]
Escape,
#[bytes(b'(')]
OpenParen,
#[bytes(b')')]
CloseParen,
#[bytes(b'[')]
OpenBracket,
#[bytes(b']')]
CloseBracket,
#[bytes(b'{')]
OpenCurly,
#[bytes(b'}')]
CloseCurly,
#[bytes(b'"', b'\'', b'`')]
Quote,
#[bytes(b' ', b'\t', b'\n', b'\r', b'\x0C')]
Whitespace,
#[bytes(b'$')]
Dollar,
#[fallback]
Other,
}
#[cfg(test)]
mod tests {
use super::ArbitraryValueMachine;
use crate::extractor::machine::Machine;
use pretty_assertions::assert_eq;
#[test]
#[ignore]
fn test_arbitrary_value_machine_performance() {
let input = r#"<div class="[color:red] [[data-foo]] [url('https://tailwindcss.com')] [url(https://tailwindcss.com)]"></div>"#.repeat(100);
ArbitraryValueMachine::test_throughput(100_000, &input);
ArbitraryValueMachine::test_duration_once(&input);
todo!()
}
#[test]
fn test_arbitrary_value_machine_extraction() {
for (input, expected) in [
// Simple variable
("[#0088cc]", vec!["[#0088cc]"]),
// With parentheses
(
"[url(https:
vec!["[url(https://tailwindcss.com)]"],
),
("['[({])}']", vec!["['[({])}']"]),
(
"[url('https://tailwindcss.com?[{]}')]",
vec!["[url('https://tailwindcss.com?[{]}')]"],
),
("[[data-foo]]", vec!["[[data-foo]]"]),
(
"[&>[data-slot=icon]:last-child]",
vec!["[&>[data-slot=icon]:last-child]"],
),
("[length:32rem]", vec!["[length:32rem]"]),
("[ #0088cc ]", vec![]),
("[foo[bar]", vec![]),
("[]", vec![]),
] {
assert_eq!(ArbitraryValueMachine::test_extract_all(input), expected);
}
}
#[test]
fn test_exceptions() {
for (input, expected) in [
("[${x}]", vec![]),
("[url(${x})]", vec![]),
("[url('${x}')]", vec!["[url('${x}')]"]),
] {
assert_eq!(ArbitraryValueMachine::test_extract_all(input), expected);
}
}
}