Skip to content
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Ensure validation of `source(…)` happens relative to the file it is in ([#19274](https://github.com/tailwindlabs/tailwindcss/pull/19274))
- Include filename and line numbers in CSS parse errors ([#19282](https://github.com/tailwindlabs/tailwindcss/pull/19282))
- Skip comments in Ruby files when checking for class names ([#19243](https://github.com/tailwindlabs/tailwindcss/pull/19243))
- Skip over arbitrary property utilities with a top-level `!` in the value ([#19243](https://github.com/tailwindlabs/tailwindcss/pull/19243))

### Added

Expand Down
12 changes: 12 additions & 0 deletions crates/oxide/src/extractor/arbitrary_property_machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,12 @@ impl Machine for ArbitraryPropertyMachine<ParsingValueState> {
return self.restart()
}

// An `!` at the top-level is invalid. We don't allow things to end with
// `!important` either as we have dedicated syntax for this.
Class::Exclamation if self.bracket_stack.is_empty() => {
return self.restart();
}

// Everything else is valid
_ => cursor.advance(),
};
Expand Down Expand Up @@ -293,6 +299,9 @@ enum Class {
#[bytes(b'/')]
Slash,

#[bytes(b'!')]
Exclamation,

#[bytes(b' ', b'\t', b'\n', b'\r', b'\x0C')]
Whitespace,

Expand Down Expand Up @@ -369,6 +378,9 @@ mod tests {
"[background:url(https://example.com?q={[{[([{[[2]]}])]}]})]",
vec!["[background:url(https://example.com?q={[{[([{[[2]]}])]}]})]"],
),
// A property containing `!` at the top-level is invalid
("[color:red!]", vec![]),
("[color:red!important]", vec![]),
] {
for wrapper in [
// No wrapper
Expand Down
126 changes: 123 additions & 3 deletions crates/oxide/src/extractor/pre_processors/ruby.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,74 @@ impl PreProcessor for Ruby {

// Ruby extraction
while cursor.pos < len {
// Looking for `%w` or `%W`
if cursor.curr != b'%' && !matches!(cursor.next, b'w' | b'W') {
match cursor.curr {
b'"' => {
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'"' => break,

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

cursor.advance();
continue;
}

b'\'' => {
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'\'' => break,

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

cursor.advance();
continue;
}

// Replace comments in Ruby files
b'#' => {
result[cursor.pos] = b' ';
cursor.advance();

while cursor.pos < len {
match cursor.curr {
// End of the comment
b'\n' => break,

// Everything else is part of the comment and replaced
_ => {
result[cursor.pos] = b' ';
cursor.advance();
}
};
}

cursor.advance();
continue;
}

_ => {}
}

// Looking for `%w`, `%W`, or `%p`
if cursor.curr != b'%' || !matches!(cursor.next, b'w' | b'W' | b'p') {
cursor.advance();
continue;
}
Expand All @@ -90,6 +156,8 @@ impl PreProcessor for Ruby {
b'[' => b']',
b'(' => b')',
b'{' => b'}',
b'#' => b'#',
b' ' => b'\n',
_ => {
cursor.advance();
continue;
Expand Down Expand Up @@ -131,7 +199,10 @@ impl PreProcessor for Ruby {

// End of the pattern, replace the boundary character with a space
_ if cursor.curr == boundary => {
result[cursor.pos] = b' ';
if boundary != b'\n' {
result[cursor.pos] = b' ';
}

break;
}

Expand Down Expand Up @@ -173,12 +244,51 @@ mod tests {
"%w(flex data-[state=pending]:bg-(--my-color) flex-col)",
"%w flex data-[state=pending]:bg-(--my-color) flex-col ",
),

// %w …\n
("%w flex px-2.5\n", "%w flex px-2.5\n"),

// Use backslash to embed spaces in the strings.
(r#"%w[foo\ bar baz\ bat]"#, r#"%w foo bar baz bat "#),
(r#"%W[foo\ bar baz\ bat]"#, r#"%W foo bar baz bat "#),

// The nested delimiters evaluated to a flat array of strings
// (not nested array).
(r#"%w[foo[bar baz]qux]"#, r#"%w foo[bar baz]qux "#),

(
"# test\n# test\n# {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!]\n%w[flex px-2.5]",
" \n \n \n%w flex px-2.5 "
),

(r#""foo # bar""#, r#""foo # bar""#),
(r#"'foo # bar'"#, r#"'foo # bar'"#),
(
r#"def call = tag.span "Foo", class: %w[rounded-full h-0.75 w-0.75]"#,
r#"def call = tag.span "Foo", class: %w rounded-full h-0.75 w-0.75 "#
),

(r#"%w[foo ' bar]"#, r#"%w foo ' bar "#),
(r#"%w[foo " bar]"#, r#"%w foo " bar "#),
(r#"%W[foo ' bar]"#, r#"%W foo ' bar "#),
(r#"%W[foo " bar]"#, r#"%W foo " bar "#),

(r#"%p foo ' bar "#, r#"%p foo ' bar "#),
(r#"%p foo " bar "#, r#"%p foo " bar "#),

(
"%p has a ' quote\n# this should be removed\n%p has a ' quote",
"%p has a ' quote\n \n%p has a ' quote"
),
(
"%p has a \" quote\n# this should be removed\n%p has a \" quote",
"%p has a \" quote\n \n%p has a \" quote"
),

(
"%w#this text is kept# # this text is not",
"%w this text is kept ",
),
] {
Ruby::test(input, expected);
}
Expand Down Expand Up @@ -211,6 +321,16 @@ mod tests {
"%w(flex data-[state=pending]:bg-(--my-color) flex-col)",
vec!["flex", "data-[state=pending]:bg-(--my-color)", "flex-col"],
),

(
"# test\n# test\n# {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!]\n%w[flex px-2.5]",
vec!["flex", "px-2.5"],
),

(r#""foo # bar""#, vec!["foo", "bar"]),
(r#"'foo # bar'"#, vec!["foo", "bar"]),

(r#"%w[foo ' bar]"#, vec!["foo", "bar"]),
] {
Ruby::test_extract_contains(input, expected);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
.relative
^^^^^^^^
- # Blurred background star
^^^^^^^^^^ ^^^^
.absolute.left-0.z-0{ class: "-top-[400px] -right-[400px]" }
^^^^^^^^ ^^^^^^ ^^^ ^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^
.flex.justify-end.blur-3xl
Expand Down Expand Up @@ -196,7 +195,6 @@
^^^^^^^^ ^^^^ ^^^ ^^^^ ^^
:escaped
- # app/components/character_component.html.haml
^^^^
= part(:component) do
^^
= part(:head)
Expand Down