diff --git a/CHANGELOG.md b/CHANGELOG.md index 19f17362e561..be2c8117a14f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - _Experimental_: Add `@source inline(…)` ([#17147](https://github.com/tailwindlabs/tailwindcss/pull/17147)) - _Experimental_: Add `@source not` ([#17255](https://github.com/tailwindlabs/tailwindcss/pull/17255)) +### Fixed + +- Fix an issue causing the CLI to hang when processing Ruby files ([#17383](https://github.com/tailwindlabs/tailwindcss/pull/17383)) + ## [4.0.16] - 2025-03-25 ### Added diff --git a/Cargo.lock b/Cargo.lock index ee6a465fcb69..3d28efbfe35f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,21 +27,6 @@ dependencies = [ "nom", ] -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - [[package]] name = "bitflags" version = "2.6.0" @@ -171,17 +156,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "fancy-regex" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" -dependencies = [ - "bit-set", - "regex-automata 0.4.8", - "regex-syntax 0.8.5", -] - [[package]] name = "fast-glob" version = "0.4.3" @@ -581,7 +555,6 @@ dependencies = [ "classification-macros", "crossbeam", "dunce", - "fancy-regex", "fast-glob", "globwalk", "ignore 0.4.23", diff --git a/crates/oxide/Cargo.toml b/crates/oxide/Cargo.toml index 448fc5c9f227..c2e0b7155572 100644 --- a/crates/oxide/Cargo.toml +++ b/crates/oxide/Cargo.toml @@ -19,8 +19,6 @@ fast-glob = "0.4.3" classification-macros = { path = "../classification-macros" } ignore = { path = "../ignore" } regex = "1.11.1" -fancy-regex = "0.14.0" [dev-dependencies] tempfile = "3.13.0" - diff --git a/crates/oxide/src/extractor/pre_processors/ruby.rs b/crates/oxide/src/extractor/pre_processors/ruby.rs index 121af8e5cd3d..7db1f08f6f59 100644 --- a/crates/oxide/src/extractor/pre_processors/ruby.rs +++ b/crates/oxide/src/extractor/pre_processors/ruby.rs @@ -4,12 +4,22 @@ use crate::cursor; use crate::extractor::bracket_stack; use crate::extractor::pre_processors::pre_processor::PreProcessor; use crate::scanner::pre_process_input; -use bstr::ByteSlice; -use fancy_regex::Regex; +use bstr::ByteVec; +use regex::{Regex, RegexBuilder}; use std::sync; -static TEMPLATE_REGEX: sync::LazyLock = sync::LazyLock::new(|| { - Regex::new(r#"\s*(.*?)_template\s*<<[-~]?([A-Z]+?)\n([\s\S]*?)\2"#).unwrap() +static TEMPLATE_START_REGEX: sync::LazyLock = sync::LazyLock::new(|| { + RegexBuilder::new(r#"\s*([a-z0-9_-]+)_template\s*<<[-~]?([A-Z]+)$"#) + .multi_line(true) + .build() + .unwrap() +}); + +static TEMPLATE_END_REGEX: sync::LazyLock = sync::LazyLock::new(|| { + RegexBuilder::new(r#"^\s*([A-Z]+)"#) + .multi_line(true) + .build() + .unwrap() }); #[derive(Debug, Default)] @@ -25,14 +35,40 @@ impl PreProcessor for Ruby { // Extract embedded template languages // https://viewcomponent.org/guide/templates.html#interpolations let content_as_str = std::str::from_utf8(content).unwrap(); - for capture in TEMPLATE_REGEX - .captures_iter(content_as_str) - .filter_map(Result::ok) - { - let lang = capture.get(1).unwrap().as_str(); - let body = capture.get(3).unwrap().as_str(); - let replaced = pre_process_input(body.as_bytes(), lang); - result = result.replace(body, replaced); + + let starts = TEMPLATE_START_REGEX.captures_iter(content_as_str).collect::>(); + let ends = TEMPLATE_END_REGEX.captures_iter(content_as_str).collect::>(); + + for start in starts.iter() { + // The language for this block + let lang = start.get(1).unwrap().as_str(); + + // The HEREDOC delimiter + let delimiter_start = start.get(2).unwrap().as_str(); + + // Where the "body" starts for the HEREDOC block + let body_start = start.get(0).unwrap().end(); + + // Look through all of the ends to find a matching language + for end in ends.iter() { + // 1. This must appear after the start + let body_end = end.get(0).unwrap().start(); + if body_end < body_start { + continue; + } + + // The languages must match otherwise we haven't found the end + let delimiter_end = end.get(1).unwrap().as_str(); + if delimiter_end != delimiter_start { + continue; + } + + let body = &content_as_str[body_start..body_end]; + let replaced = pre_process_input(body.as_bytes(), &lang.to_ascii_lowercase()); + + result.replace_range(body_start..body_end, replaced); + break; + } } // Ruby extraction