Was working on another issue and noticed that I wanted these but they aren't related to any of the issues. So opening a separate PR to do some internal cleanup.
214 lines
7 KiB
Rust
214 lines
7 KiB
Rust
use crate::cursor;
|
|
use crate::extractor::machine::{Machine, MachineState};
|
|
use classification_macros::ClassifyBytes;
|
|
|
|
/// Extract CSS variables from an input.
|
|
///
|
|
/// E.g.:
|
|
///
|
|
/// ```text
|
|
/// var(--my-variable)
|
|
/// ^^^^^^^^^^^^^
|
|
/// ```
|
|
#[derive(Debug, Default)]
|
|
pub struct CssVariableMachine;
|
|
|
|
impl Machine for CssVariableMachine {
|
|
#[inline(always)]
|
|
fn reset(&mut self) {}
|
|
|
|
#[inline]
|
|
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
|
// CSS Variables must start with `--`
|
|
if Class::Dash != cursor.curr.into() || Class::Dash != cursor.next.into() {
|
|
return MachineState::Idle;
|
|
}
|
|
|
|
let start_pos = cursor.pos;
|
|
let len = cursor.input.len();
|
|
|
|
cursor.advance_twice();
|
|
|
|
while cursor.pos < len {
|
|
match cursor.curr.into() {
|
|
// https://drafts.csswg.org/css-syntax-3/#ident-token-diagram
|
|
//
|
|
Class::AllowedCharacter | Class::Dash => {
|
|
match cursor.next.into() {
|
|
// Valid character followed by a valid character or an escape character
|
|
//
|
|
// E.g.: `--my-variable`
|
|
// ^^
|
|
// E.g.: `--my-\#variable`
|
|
// ^^
|
|
Class::AllowedCharacter | Class::Dash | Class::Escape => cursor.advance(),
|
|
|
|
// Valid character followed by anything else means the variable is done
|
|
//
|
|
// E.g.: `'--my-variable'`
|
|
// ^
|
|
_ => {
|
|
// There must be at least 1 character after the `--`
|
|
if cursor.pos - start_pos < 2 {
|
|
return self.restart();
|
|
} else {
|
|
return self.done(start_pos, cursor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Class::Escape => match cursor.next.into() {
|
|
// An escaped whitespace character is not allowed
|
|
//
|
|
// In CSS it is allowed, but in the context of a class it's not because then we
|
|
// would have spaces in the class.
|
|
//
|
|
// E.g.: `bg-(--my-\ color)`
|
|
// ^
|
|
Class::Whitespace => return self.restart(),
|
|
|
|
// An escape at the end of the class is not allowed
|
|
Class::End => return self.restart(),
|
|
|
|
// An escaped character, skip the next character, resume after
|
|
//
|
|
// E.g.: `--my-\#variable`
|
|
// ^ We are here
|
|
// ^ Resume here
|
|
_ => cursor.advance_twice(),
|
|
},
|
|
|
|
// Character is not valid anymore
|
|
_ => return self.restart(),
|
|
}
|
|
}
|
|
|
|
MachineState::Idle
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, ClassifyBytes)]
|
|
enum Class {
|
|
#[bytes(b'-')]
|
|
Dash,
|
|
|
|
#[bytes(b'_')]
|
|
#[bytes_range(b'a'..=b'z', b'A'..=b'Z', b'0'..=b'9')]
|
|
// non-ASCII (such as Emoji): https://drafts.csswg.org/css-syntax-3/#non-ascii-ident-code-point
|
|
#[bytes_range(0x80..=0xff)]
|
|
AllowedCharacter,
|
|
|
|
#[bytes(b'\\')]
|
|
Escape,
|
|
|
|
#[bytes(b' ', b'\t', b'\n', b'\r', b'\x0C')]
|
|
Whitespace,
|
|
|
|
#[bytes(b'\0')]
|
|
End,
|
|
|
|
#[fallback]
|
|
Other,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::CssVariableMachine;
|
|
use crate::extractor::machine::Machine;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn test_css_variable_machine_performance() {
|
|
let input = r#"This sentence will contain a few variables here and there var(--my-variable) --other-variable-1\/2 var(--more-variables-here)"#.repeat(100);
|
|
|
|
CssVariableMachine::test_throughput(100_000, &input);
|
|
CssVariableMachine::test_duration_once(&input);
|
|
|
|
todo!();
|
|
}
|
|
|
|
#[test]
|
|
fn test_css_variable_machine_extraction() {
|
|
for (input, expected) in [
|
|
// Simple variable
|
|
("--foo", vec!["--foo"]),
|
|
("--my-variable", vec!["--my-variable"]),
|
|
// Multiple variables
|
|
(
|
|
"calc(var(--first) + var(--second))",
|
|
vec!["--first", "--second"],
|
|
),
|
|
// Variables with... emojis
|
|
("--😀", vec!["--😀"]),
|
|
("--😀-😁", vec!["--😀-😁"]),
|
|
// Escaped character in the middle, skips the next character
|
|
(r#"--spacing-1\/2"#, vec![r#"--spacing-1\/2"#]),
|
|
// Escaped whitespace is not allowed
|
|
(r#"--my-\ variable"#, vec![]),
|
|
// --------------------------
|
|
//
|
|
// Exceptions
|
|
// Not a valid variable
|
|
("", vec![]),
|
|
("-", vec![]),
|
|
("--", vec![]),
|
|
] {
|
|
for wrapper in [
|
|
// No wrapper
|
|
"{}",
|
|
// With leading spaces
|
|
" {}",
|
|
// With trailing spaces
|
|
"{} ",
|
|
// Surrounded by spaces
|
|
" {} ",
|
|
// Inside a string
|
|
"'{}'",
|
|
// Inside a function call
|
|
"fn({})",
|
|
// Inside nested function calls
|
|
"fn1(fn2({}))",
|
|
// --------------------------
|
|
//
|
|
// HTML
|
|
// Inside a class (on its own)
|
|
r#"<div class="{}"></div>"#,
|
|
// Inside a class (first)
|
|
r#"<div class="{} foo"></div>"#,
|
|
// Inside a class (second)
|
|
r#"<div class="foo {}"></div>"#,
|
|
// Inside a class (surrounded)
|
|
r#"<div class="foo {} bar"></div>"#,
|
|
// Inside an arbitrary property
|
|
r#"<div class="[{}:red]"></div>"#,
|
|
// --------------------------
|
|
//
|
|
// JavaScript
|
|
// Inside a variable
|
|
r#"let classes = '{}';"#,
|
|
// Inside an object (key)
|
|
r#"let classes = { '{}': true };"#,
|
|
// Inside an object (no spaces, key)
|
|
r#"let classes = {'{}':true};"#,
|
|
// Inside an object (value)
|
|
r#"let classes = { primary: '{}' };"#,
|
|
// Inside an object (no spaces, value)
|
|
r#"let classes = {primary:'{}'};"#,
|
|
// Inside an array
|
|
r#"let classes = ['{}'];"#,
|
|
] {
|
|
let input = wrapper.replace("{}", input);
|
|
|
|
let actual = CssVariableMachine::test_extract_all(&input);
|
|
|
|
if actual != expected {
|
|
dbg!(&input);
|
|
}
|
|
|
|
assert_eq!(actual, expected);
|
|
}
|
|
}
|
|
}
|
|
}
|