Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- _Experimental_: Add `user-valid` and `user-invalid` variants ([#12370](https://github.com/tailwindlabs/tailwindcss/pull/12370))
- _Experimental_: Add `wrap-anywhere`, `wrap-break-word`, and `wrap-normal` utilities ([#12128](https://github.com/tailwindlabs/tailwindcss/pull/12128))

### Fixed

- Fix `haml` pre-processing ([#17051](https://github.com/tailwindlabs/tailwindcss/pull/17051))

## [4.0.12] - 2025-03-07

### Fixed
Expand All @@ -26,7 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure utilities are sorted based on their actual property order ([#16995](https://github.com/tailwindlabs/tailwindcss/pull/16995))
- Ensure strings in Pug and Slim templates are handled correctly ([#17000](https://github.com/tailwindlabs/tailwindcss/pull/17000))
- Ensure classes between `}` and `{` are properly extracted ([#17001](https://github.com/tailwindlabs/tailwindcss/pull/17001))
- Add `razor`/`cshtml` pre processing ([#17027](https://github.com/tailwindlabs/tailwindcss/pull/17027))
- Fix `razor`/`cshtml` pre-processing ([#17027](https://github.com/tailwindlabs/tailwindcss/pull/17027))
- Ensure extracting candidates from JS embedded in a PHP string works as expected ([#17031](https://github.com/tailwindlabs/tailwindcss/pull/17031))

## [4.0.11] - 2025-03-06
Expand Down
50 changes: 50 additions & 0 deletions crates/oxide/src/extractor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,55 @@ mod tests {
);
}

// https://github.com/tailwindlabs/tailwindcss/issues/17050
#[test]
fn test_haml_syntax() {
for (input, expected) in [
// Element with classes
(
"%body.flex.flex-col.items-center.justify-center",
vec!["flex", "flex-col", "items-center", "justify-center"],
),
// Plain element
(
".text-slate-500.xl:text-gray-500",
vec!["text-slate-500", "xl:text-gray-500"],
),
// Element with hash attributes
(
".text-black.xl:text-red-500{ data: { tailwind: 'css' } }",
vec!["text-black", "xl:text-red-500"],
),
// Element with a boolean attribute
(
".text-green-500.xl:text-blue-500(data-sidebar)",
vec!["text-green-500", "xl:text-blue-500"],
),
// Element with interpreted content
(
".text-yellow-500.xl:text-purple-500= 'Element with interpreted content'",
vec!["text-yellow-500", "xl:text-purple-500"],
),
// Element with a hash at the end and an extra class.
(
".text-orange-500.xl:text-pink-500{ class: 'bg-slate-100' }",
vec!["text-orange-500", "xl:text-pink-500", "bg-slate-100"],
),
// Object reference
(
".text-teal-500.xl:text-indigo-500[@user, :greeting]",
vec!["text-teal-500", "xl:text-indigo-500"],
),
// Element with an ID
(
".text-lime-500.xl:text-emerald-500#root",
vec!["text-lime-500", "xl:text-emerald-500"],
),
] {
assert_extract_candidates_contains(&pre_process_input(input, "haml"), expected);
}
}

// https://github.com/tailwindlabs/tailwindcss/issues/16982
#[test]
fn test_arbitrary_container_queries_syntax() {
Expand All @@ -888,6 +937,7 @@ mod tests {
);
}

// https://github.com/tailwindlabs/tailwindcss/issues/17023
#[test]
fn test_js_embedded_in_php_syntax() {
// Escaped single quotes
Expand Down
123 changes: 123 additions & 0 deletions crates/oxide/src/extractor/pre_processors/haml.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
use crate::cursor;
use crate::extractor::bracket_stack::BracketStack;
use crate::extractor::pre_processors::pre_processor::PreProcessor;

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

impl PreProcessor for Haml {
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 bracket_stack = BracketStack::default();

while cursor.pos < len {
match cursor.curr {
// Consume strings as-is
b'\'' | b'"' => {
let len = cursor.input.len();
let end_char = cursor.curr;

cursor.advance();

while cursor.pos < len {
match cursor.curr {
// Escaped character, skip ahead to the next character
b'\\' => cursor.advance_twice(),

// End of the string
b'\'' | b'"' if cursor.curr == end_char => break,

// Everything else is valid
_ => cursor.advance(),
};
}
}

// Replace following characters with spaces if they are not inside of brackets
b'.' | b'#' | b'=' if bracket_stack.is_empty() => {
result[cursor.pos] = b' ';
}

b'(' | b'[' | b'{' => {
// Replace first bracket with a space
if bracket_stack.is_empty() {
result[cursor.pos] = b' ';
}
bracket_stack.push(cursor.curr);
}

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

// Replace closing bracket with a space
if bracket_stack.is_empty() {
result[cursor.pos] = b' ';
}
}

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

cursor.advance();
}

result
}
}

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

#[test]
fn test_haml_pre_processor() {
for (input, expected) in [
// Element with classes
(
"%body.flex.flex-col.items-center.justify-center",
"%body flex flex-col items-center justify-center",
),
// Plain element
(
".text-slate-500.xl:text-gray-500",
" text-slate-500 xl:text-gray-500",
),
// Element with hash attributes
(
".text-black.xl:text-red-500{ data: { tailwind: 'css' } }",
" text-black xl:text-red-500 data: { tailwind: 'css' } ",
),
// Element with a boolean attribute
(
".text-green-500.xl:text-blue-500(data-sidebar)",
" text-green-500 xl:text-blue-500 data-sidebar ",
),
// Element with interpreted content
(
".text-yellow-500.xl:text-purple-500= 'Element with interpreted content'",
" text-yellow-500 xl:text-purple-500 'Element with interpreted content'",
),
// Element with a hash at the end and an extra class.
(
".text-orange-500.xl:text-pink-500{ class: 'bg-slate-100' }",
" text-orange-500 xl:text-pink-500 class: 'bg-slate-100' ",
),
// Object reference
(
".text-teal-500.xl:text-indigo-500[@user, :greeting]",
" text-teal-500 xl:text-indigo-500 @user, :greeting ",
),
// Element with an ID
(
".text-lime-500.xl:text-emerald-500#root",
" text-lime-500 xl:text-emerald-500 root",
),
] {
Haml::test(input, expected);
}
}
}
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,10 +1,12 @@
pub mod haml;
pub mod pre_processor;
pub mod pug;
pub mod razor;
pub mod ruby;
pub mod slim;
pub mod svelte;

pub use haml::*;
pub use pre_processor::*;
pub use pug::*;
pub use razor::*;
Expand Down
1 change: 1 addition & 0 deletions crates/oxide/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ pub fn pre_process_input(content: &[u8], extension: &str) -> Vec<u8> {

match extension {
"cshtml" | "razor" => Razor.process(content),
"haml" => Haml.process(content),
"pug" => Pug.process(content),
"rb" | "erb" => Ruby.process(content),
"slim" => Slim.process(content),
Expand Down