diff --git a/CHANGELOG.md b/CHANGELOG.md index bf75d28e3..5adb9f7f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow whitespace around `@source inline()` argument ([#19461](https://github.com/tailwindlabs/tailwindcss/pull/19461)) - CLI: Emit comment when source maps are saved to files ([#19447](https://github.com/tailwindlabs/tailwindcss/pull/19447)) - Detect utilities when containing capital letters followed by numbers ([#19465](https://github.com/tailwindlabs/tailwindcss/pull/19465)) +- Fix class extraction for Rails' strict locals ([#19525](https://github.com/tailwindlabs/tailwindcss/pull/19525)) ### Added diff --git a/crates/oxide/src/extractor/pre_processors/pre_processor.rs b/crates/oxide/src/extractor/pre_processors/pre_processor.rs index 8d42e744a..8e02b8f69 100644 --- a/crates/oxide/src/extractor/pre_processors/pre_processor.rs +++ b/crates/oxide/src/extractor/pre_processors/pre_processor.rs @@ -25,7 +25,33 @@ pub trait PreProcessor: Sized + Default { } #[cfg(test)] - fn test_extract_contains(input: &str, items: Vec<&str>) { + fn test_extract_exact(input: &str, expected: 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::>(); + + if candidates != expected { + dbg!(&candidates, &expected); + panic!("Extracted candidates do not match expected candidates"); + } + } + + #[cfg(test)] + fn test_extract_contains(input: &str, expected: Vec<&str>) { use crate::extractor::{Extracted, Extractor}; let input = input.as_bytes(); @@ -46,7 +72,7 @@ pub trait PreProcessor: Sized + Default { // Ensure all items are present in the candidates. let mut missing = vec![]; - for item in &items { + for item in &expected { if !candidates.contains(item) { missing.push(item); } diff --git a/crates/oxide/src/extractor/pre_processors/ruby.rs b/crates/oxide/src/extractor/pre_processors/ruby.rs index c94bd945f..89ac1ee7b 100644 --- a/crates/oxide/src/extractor/pre_processors/ruby.rs +++ b/crates/oxide/src/extractor/pre_processors/ruby.rs @@ -119,7 +119,11 @@ impl PreProcessor for Ruby { } // Replace comments in Ruby files - b'#' => { + // + // Except for strict locals, these are defined in a `<%# locals: … %>`. Checking if + // the comment is preceded by a `%` should be enough without having to perform more + // parsing logic. Worst case we _do_ scan a few comments. + b'#' if !matches!(cursor.prev, b'%') => { result[cursor.pos] = b' '; cursor.advance(); @@ -382,4 +386,45 @@ mod tests { "#; Ruby::test_extract_contains(input, vec!["z-1", "z-2", "z-3"]); } + + // https://github.com/tailwindlabs/tailwindcss/issues/19239 + #[test] + fn test_skip_comments() { + let input = r#" + # From activerecord-8.1.1/lib/active_record/errors.rb:147 + # Rails uses RDoc cross-reference syntax in inline documentation: + # {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] + "#; + + // Nothing should be extracted from comments, so expect an empty array. + Ruby::test_extract_exact(input, vec![]); + } + + // https://github.com/tailwindlabs/tailwindcss/issues/19481 + #[test] + fn test_strict_locals() { + // Strict locals are defined in a `<%# locals: … %>`, but the `#` looks like a comment + // which we should not ignore in this case. + let input = r#" + <%# locals: (css: "text-amber-600") %> + <% more_css = "text-sky-500" %> + +

+ In a partial +

+ +

+ In a partial using explicit local variables +

+ +

+ In a partial using explicit local variables +

+ "#; + + Ruby::test_extract_contains( + input, + vec!["text-amber-600", "text-sky-500", "text-green-500"], + ); + } }