Skip to content

Commit

Permalink
Add Haml pre processor (#17051)
Browse files Browse the repository at this point in the history
This PR ensures we extract candidates from Haml files.

Fixes: #17050
  • Loading branch information
RobinMalfait authored Mar 7, 2025
1 parent 2f28e5f commit 7005ad7
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 1 deletion.
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

0 comments on commit 7005ad7

Please sign in to comment.