Add Twig pre-processor (#20198)
This PR fixes an issue where `addClass(opacity-50)` and
`removeClass(opacity-50)` in Twig templates isn't extracted properly.
This fixes that by adding a pre-processor for `.twig` files. This is a
simple initial implementation where we drop the `(` and `)` from the
`addClass` and `removeClass` functions. We don't do any special handling
around escaped characters or parenthesis inside of strings. We will add
them when there is a use case for it. Until then, we'll keep it simple.
If the real API would've been `addClass('opacity-50')` then it would've
worked out of the box.
A workaround you can use today is by using spaces `addClass( opacity-50
)` that works as well.
Fixes: #19458
Closes: #20110
## Test plan
1. Added new tests for `twig` extraction
2. Added tests with nested `(` and `)` e.g. `addClass(p-(--value))`
which should properly extarct `p-(--value)`
This commit is contained in:
parent
0b58dd69fd
commit
0612ddc7dc
4 changed files with 183 additions and 0 deletions
|
|
@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Canonicalization: avoid suggesting large spacing-scale values for arbitrary lengths (e.g. `left-[99999px]` → `left-[99999px]`, not `left-24999.75`) ([#20130](https://github.com/tailwindlabs/tailwindcss/pull/20130))
|
||||
- Ensure `@tailwindcss/cli` in `--watch` mode recovers when a tracked dependency is deleted and restored ([#20137](https://github.com/tailwindlabs/tailwindcss/pull/20137))
|
||||
- Ensure standalone `@tailwindcss/cli` binaries are ignored when scanning for class candidates ([#20139](https://github.com/tailwindlabs/tailwindcss/pull/20139))
|
||||
- Ensure class candidates are extracted from Twig `addClass(…)` and `removeClass(…)` calls ([#20198](https://github.com/tailwindlabs/tailwindcss/pull/20198))
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ pub mod ruby;
|
|||
pub mod rust;
|
||||
pub mod slim;
|
||||
pub mod svelte;
|
||||
pub mod twig;
|
||||
pub mod vue;
|
||||
|
||||
pub use clojure::*;
|
||||
|
|
@ -24,4 +25,5 @@ pub use ruby::*;
|
|||
pub use rust::*;
|
||||
pub use slim::*;
|
||||
pub use svelte::*;
|
||||
pub use twig::*;
|
||||
pub use vue::*;
|
||||
|
|
|
|||
179
crates/oxide/src/extractor/pre_processors/twig.rs
Normal file
179
crates/oxide/src/extractor/pre_processors/twig.rs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
use crate::cursor;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Twig;
|
||||
|
||||
impl PreProcessor for Twig {
|
||||
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 = vec![];
|
||||
|
||||
const ADD_CLASS: &[u8] = b"addClass";
|
||||
const REMOVE_CLASS: &[u8] = b"removeClass";
|
||||
|
||||
while cursor.pos < len {
|
||||
match (!bracket_stack.is_empty(), cursor.curr()) {
|
||||
// addClass(
|
||||
// ^
|
||||
(false, b'(')
|
||||
if cursor.pos >= ADD_CLASS.len()
|
||||
&& matches!(
|
||||
&content[cursor.pos - ADD_CLASS.len()..cursor.pos],
|
||||
ADD_CLASS
|
||||
) =>
|
||||
{
|
||||
bracket_stack.push(cursor.curr());
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
|
||||
// removeClass(
|
||||
// ^
|
||||
(false, b'(')
|
||||
if cursor.pos >= REMOVE_CLASS.len()
|
||||
&& matches!(
|
||||
&content[cursor.pos - REMOVE_CLASS.len()..cursor.pos],
|
||||
REMOVE_CLASS
|
||||
) =>
|
||||
{
|
||||
bracket_stack.push(cursor.curr());
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
|
||||
(true, b'(') => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
}
|
||||
|
||||
(true, b')') => {
|
||||
bracket_stack.pop();
|
||||
|
||||
if bracket_stack.is_empty() {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Twig;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
|
||||
#[test]
|
||||
fn test_twig_pre_processor() {
|
||||
for (input, expected) in [
|
||||
(
|
||||
// Ensure we don't crash when we encounter an `(`
|
||||
"(p-(--value))",
|
||||
"(p-(--value))",
|
||||
),
|
||||
(
|
||||
// addClass with single argument
|
||||
"addClass(p-(--value))",
|
||||
"addClass p-(--value) ",
|
||||
),
|
||||
(
|
||||
// addClass with single argument
|
||||
"addClass(m-4 p-8 w-full)",
|
||||
"addClass m-4 p-8 w-full ",
|
||||
),
|
||||
(
|
||||
// removeClass with single argument
|
||||
"removeClass(p-(--value))",
|
||||
"removeClass p-(--value) ",
|
||||
),
|
||||
(
|
||||
// removeClass with single argument
|
||||
"removeClass(m-4 p-8 w-full)",
|
||||
"removeClass m-4 p-8 w-full ",
|
||||
),
|
||||
(
|
||||
// Combined with single arguments
|
||||
"addClass(m-(--value))|removeClass(p-(--value))",
|
||||
"addClass m-(--value) |removeClass p-(--value) ",
|
||||
),
|
||||
] {
|
||||
Twig::test(input, expected);
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19458
|
||||
#[test]
|
||||
fn test_extraction_in_add_class_and_remove_class_works() {
|
||||
// Inside normal HTML
|
||||
let input = r#"
|
||||
<div data-loading="addClass(opacity-50)">
|
||||
<!-- -->
|
||||
</div>
|
||||
"#;
|
||||
|
||||
let expected = r#"
|
||||
<div data-loading="addClass opacity-50 ">
|
||||
<!-- -->
|
||||
</div>
|
||||
"#;
|
||||
|
||||
Twig::test(input, expected);
|
||||
Twig::test_extract_contains(input, vec!["opacity-50"]);
|
||||
|
||||
// Inside a component
|
||||
let input = r#"
|
||||
<twig:d:Card
|
||||
{{ ...attributes.defaults({
|
||||
as: "a",
|
||||
class: "cursor-pointer bg-base-200 card-sm md:card-md lg:card-lg",
|
||||
role: "button",
|
||||
tabindex: "0",
|
||||
"data-action": "live#$render",
|
||||
"data-loading": "addAttribute(disabled)",
|
||||
"data-poll": "delay(60000)|$render",
|
||||
"data-loading": "addClass(border border-red-500 opacity-75)",
|
||||
})
|
||||
}}
|
||||
>
|
||||
"#;
|
||||
|
||||
let expected = r#"
|
||||
<twig:d:Card
|
||||
{{ ...attributes.defaults({
|
||||
as: "a",
|
||||
class: "cursor-pointer bg-base-200 card-sm md:card-md lg:card-lg",
|
||||
role: "button",
|
||||
tabindex: "0",
|
||||
"data-action": "live#$render",
|
||||
"data-loading": "addAttribute(disabled)",
|
||||
"data-poll": "delay(60000)|$render",
|
||||
"data-loading": "addClass border border-red-500 opacity-75 ",
|
||||
})
|
||||
}}
|
||||
>
|
||||
"#;
|
||||
|
||||
Twig::test(input, expected);
|
||||
Twig::test_extract_contains(
|
||||
input,
|
||||
vec![
|
||||
// class
|
||||
"cursor-pointer",
|
||||
"bg-base-200",
|
||||
"card-sm",
|
||||
"md:card-md",
|
||||
"lg:card-lg",
|
||||
// data-loading
|
||||
"border",
|
||||
"border-red-500",
|
||||
"opacity-75",
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -481,6 +481,7 @@ pub fn pre_process_input(content: Vec<u8>, extension: &str) -> Vec<u8> {
|
|||
"slim" | "slang" => Slim.process(&content),
|
||||
"svelte" => Svelte.process(&content),
|
||||
"rs" => Rust.process(&content),
|
||||
"twig" => Twig.process(&content),
|
||||
"vue" => Vue.process(&content),
|
||||
_ => content,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue