diff --git a/CHANGELOG.md b/CHANGELOG.md index 093c2aae4..2c3ea1823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - 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)) ## [4.0.11] - 2025-03-06 diff --git a/crates/oxide/src/extractor/pre_processors/pre_processor.rs b/crates/oxide/src/extractor/pre_processors/pre_processor.rs index 4bbbbe6d1..8c807c8c3 100644 --- a/crates/oxide/src/extractor/pre_processors/pre_processor.rs +++ b/crates/oxide/src/extractor/pre_processors/pre_processor.rs @@ -25,4 +25,38 @@ pub trait PreProcessor: Sized + Default { assert_eq!(actual, expected); } + + #[cfg(test)] + fn test_extract_contains(input: &str, items: Vec<&str>) { + use crate::extractor::{Extracted, Extractor}; + + let input = input.as_bytes(); + + let processor = Self::default(); + let transformed = processor.process(input); + + let extracted = Extractor::new(&transformed).extract(); + + // Extract all candidates and css variables. + let candidates = extracted + .iter() + .filter_map(|x| match x { + Extracted::Candidate(bytes) => std::str::from_utf8(bytes).ok(), + Extracted::CssVariable(bytes) => std::str::from_utf8(bytes).ok(), + }) + .collect::>(); + + // Ensure all items are present in the candidates. + let mut missing = vec![]; + for item in &items { + if !candidates.contains(item) { + missing.push(item); + } + } + + if !missing.is_empty() { + dbg!(&candidates, &missing); + panic!("Missing some items"); + } + } } diff --git a/crates/oxide/src/extractor/pre_processors/pug.rs b/crates/oxide/src/extractor/pre_processors/pug.rs index d7433ae31..eca30e3e8 100644 --- a/crates/oxide/src/extractor/pre_processors/pug.rs +++ b/crates/oxide/src/extractor/pre_processors/pug.rs @@ -1,8 +1,6 @@ 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 Pug; @@ -12,14 +10,29 @@ impl PreProcessor for Pug { 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); + 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 dots with spaces diff --git a/crates/oxide/src/extractor/pre_processors/slim.rs b/crates/oxide/src/extractor/pre_processors/slim.rs index a5c21cdf0..8cceb0536 100644 --- a/crates/oxide/src/extractor/pre_processors/slim.rs +++ b/crates/oxide/src/extractor/pre_processors/slim.rs @@ -1,8 +1,6 @@ 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; @@ -12,14 +10,29 @@ impl PreProcessor for Slim { 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); + 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 dots with spaces @@ -103,8 +116,42 @@ mod tests { ), // Nested brackets, with "invalid" syntax but valid due to nesting ("content-['50[]']", "content-['50[]']"), + // Escaped string + ("content-['a\'b\'c\'']", "content-['a\'b\'c\'']"), ] { Slim::test(input, expected); } } + + #[test] + fn test_nested_slim_syntax() { + let input = r#" + .text-black[ + data-controller= ['foo', ('bar' if rand.positive?)].join(' ') + ] + .bg-green-300 + | BLACK on GREEN - OK + + .bg-red-300[ + data-foo= 42 + ] + | Should be BLACK on RED - FAIL + "#; + + let expected = r#" + text-black + data-controller= ['foo', ('bar' if rand.positive?)].join(' ') + ] + bg-green-300 + | BLACK on GREEN - OK + + bg-red-300 + data-foo= 42 + ] + | Should be BLACK on RED - FAIL + "#; + + Slim::test(input, expected); + Slim::test_extract_contains(input, vec!["text-black", "bg-green-300", "bg-red-300"]); + } }