Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Ensure classes containing `--` are extracted correctly ([#16972](https://github.com/tailwindlabs/tailwindcss/pull/16972))
- Ensure classes ending in `[` are extracted in Slim templating language ([#16985](https://github.com/tailwindlabs/tailwindcss/pull/16985))

## [4.0.10] - 2025-03-05

Expand Down
2 changes: 2 additions & 0 deletions crates/oxide/src/extractor/pre_processors/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
pub mod pre_processor;
pub mod pug;
pub mod ruby;
pub mod slim;
pub mod svelte;

pub use pre_processor::*;
pub use pug::*;
pub use ruby::*;
pub use slim::*;
pub use svelte::*;
17 changes: 16 additions & 1 deletion crates/oxide/src/extractor/pre_processors/pug.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::cursor;
use crate::extractor::bracket_stack::BracketStack;
use crate::extractor::machine::Machine;
use crate::extractor::pre_processors::pre_processor::PreProcessor;
use crate::StringMachine;
Expand All @@ -12,6 +13,7 @@ impl PreProcessor for Pug {
let mut result = content.to_vec();
let mut cursor = cursor::Cursor::new(content);
let mut string_machine = StringMachine;
let mut bracket_stack = BracketStack::default();

while cursor.pos < len {
match cursor.curr {
Expand All @@ -21,10 +23,18 @@ impl PreProcessor for Pug {
}

// Replace dots with spaces
b'.' => {
b'.' if bracket_stack.is_empty() => {
result[cursor.pos] = b' ';
}

b'(' | b'[' | b'{' => {
bracket_stack.push(cursor.curr);
}

b')' | b']' | b'}' if !bracket_stack.is_empty() => {
bracket_stack.pop(cursor.curr);
}

// Consume everything else
_ => {}
};
Expand All @@ -49,6 +59,11 @@ mod tests {
(".flex.bg-red-500", " flex bg-red-500"),
// Keep dots in strings
(r#"div(class="px-2.5")"#, r#"div(class="px-2.5")"#),
// Nested brackets
(
"bg-[url(https://example.com/?q=[1,2])]",
"bg-[url(https://example.com/?q=[1,2])]",
),
] {
Pug::test(input, expected);
}
Expand Down
108 changes: 108 additions & 0 deletions crates/oxide/src/extractor/pre_processors/slim.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use crate::cursor;
use crate::extractor::bracket_stack::BracketStack;
use crate::extractor::machine::Machine;
use crate::extractor::pre_processors::pre_processor::PreProcessor;
use crate::StringMachine;

#[derive(Debug, Default)]
pub struct Slim;

impl PreProcessor for Slim {
fn process(&self, content: &[u8]) -> Vec<u8> {
let len = content.len();
let mut result = content.to_vec();
let mut cursor = cursor::Cursor::new(content);
let mut string_machine = StringMachine;
let mut bracket_stack = BracketStack::default();

while cursor.pos < len {
match cursor.curr {
// Consume strings as-is
b'\'' | b'"' => {
string_machine.next(&mut cursor);
}

// Replace dots with spaces
b'.' if bracket_stack.is_empty() => {
result[cursor.pos] = b' ';
}

// Any `[` preceded by an alphanumeric value will not be part of a candidate.
//
// E.g.:
//
// ```
// .text-xl.text-red-600[
// ^ not part of the `text-red-600` candidate
// data-foo="bar"
// ]
// | This line should be red
// ```
//
// We know that `-[` is valid for an arbitrary value and that `:[` is valid as a
// variant. However `[color:red]` is also valid, in this case `[` will be preceded
// by nothing or a boundary character.
// Instead of listing all boundary characters, let's list the characters we know
// will be invalid instead.
b'[' if bracket_stack.is_empty()
&& matches!(cursor.prev, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9') =>
{
result[cursor.pos] = b' ';
bracket_stack.push(cursor.curr);
}

b'(' | b'[' | b'{' => {
bracket_stack.push(cursor.curr);
}

b')' | b']' | b'}' if !bracket_stack.is_empty() => {
bracket_stack.pop(cursor.curr);
}

// Consume everything else
_ => {}
};

cursor.advance();
}

result
}
}

#[cfg(test)]
mod tests {
use super::Slim;
use crate::extractor::pre_processors::pre_processor::PreProcessor;

#[test]
fn test_slim_pre_processor() {
for (input, expected) in [
// Convert dots to spaces
("div.flex.bg-red-500", "div flex bg-red-500"),
(".flex.bg-red-500", " flex bg-red-500"),
// Keep dots in strings
(r#"div(class="px-2.5")"#, r#"div(class="px-2.5")"#),
// Replace top-level `(a-z0-9)[` with `$1 `. E.g.: `.flex[x]` -> `.flex x]`
(".text-xl.text-red-600[", " text-xl text-red-600 "),
// But keep important brackets:
(".text-[#0088cc]", " text-[#0088cc]"),
// Arbitrary value and arbitrary modifier
(
".text-[#0088cc].bg-[#0088cc]/[20%]",
" text-[#0088cc] bg-[#0088cc]/[20%]",
),
// Start of arbitrary property
("[color:red]", "[color:red]"),
// Nested brackets
(
"bg-[url(https://example.com/?q=[1,2])]",
"bg-[url(https://example.com/?q=[1,2])]",
),
// Nested brackets, with "invalid" syntax but valid due to nesting
("content-['50[]']", "content-['50[]']"),
] {
Slim::test(input, expected);
}
}
}
3 changes: 2 additions & 1 deletion crates/oxide/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,8 +469,9 @@ pub fn pre_process_input(content: &[u8], extension: &str) -> Vec<u8> {
use crate::extractor::pre_processors::*;

match extension {
"pug" => Pug.process(content),
"rb" | "erb" => Ruby.process(content),
"slim" | "pug" => Pug.process(content),
"slim" => Slim.process(content),
"svelte" => Svelte.process(content),
_ => content.to_vec(),
}
Expand Down