Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@
which used to drop the rule and leave an element carrying the class with
nothing at all. `bg-size-[foo]` no longer writes `background-size: auto`, a
value the class never asked for (#761 through #771).
- The source scanner draws the boundary Tailwind's extractor does: a candidate
whose `/modifier` opens on `-` or `_` is not one, so `tw file.html` no longer
writes rules for `group-hover/-2a:underline` that the author's own toolchain
never produces (#772).
- A negated arbitrary length is `calc(<value> * -1)` on every family and in
every unit, the spelling Tailwind writes. The sign was folded into the number
from a unit table that each of margin, the inset sides and `text-indent` kept
Expand Down
27 changes: 26 additions & 1 deletion lib/tools/source_scan.ml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,28 @@ let read_candidate d start =
in
loop start 0 0 None false

(* Tailwind's extractor refuses a candidate whose [/modifier] opens on [-] or
[_], and refuses the whole candidate rather than truncating it: the CLI emits
nothing at all for [bg-red-500/-2]. Only a [/] the utility itself carries
counts, so one inside a bracket or a paren group - [aspect-[16/9]],
[bg-[url(a/_b)]] - is the value's, not a modifier's. *)
let modifier_opens_badly candidate =
let len = String.length candidate in
let rec loop i bracket paren =
if i >= len then false
else
match candidate.[i] with
| '[' -> loop (i + 1) (bracket + 1) paren
| ']' when bracket > 0 -> loop (i + 1) (bracket - 1) paren
| '(' -> loop (i + 1) bracket (paren + 1)
| ')' when paren > 0 -> loop (i + 1) bracket (paren - 1)
| '/' when bracket = 0 && paren = 0 ->
(i + 1 < len && (candidate.[i + 1] = '-' || candidate.[i + 1] = '_'))
|| loop (i + 1) bracket paren
| _ -> loop (i + 1) bracket paren
in
loop 0 0 0

let candidates source =
let d = decoded_utf_8 source in
let len = Array.length d.chars in
Expand All @@ -161,7 +183,10 @@ let candidates source =
String.sub source byte_start (byte_stop - byte_start)
|> trim_candidate
in
let acc = if candidate = "" then acc else candidate :: acc in
let acc =
if candidate = "" || modifier_opens_badly candidate then acc
else candidate :: acc
in
loop stop acc
else loop (i + 1) acc
else loop (i + 1) acc
Expand Down
36 changes: 36 additions & 0 deletions test/test_source_scan.ml
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,44 @@ const f = fn(arg)|}
Alcotest.(check bool)
"a call is not a candidate" false (List.mem "fn(arg)" found)

(* Tailwind's extractor refuses a candidate whose [/modifier] opens on [-] or
[_], and refuses the whole candidate rather than truncating it: measured
2026-09-13, the CLI emits nothing at all for [bg-red-500/-2], not the
truncated [bg-red-500]. tw's scanner read one where Tailwind reads none, so a
sheet tw built carried rules the author's own toolchain never writes. Only a
[/] the utility itself carries counts; one inside a bracket or a paren group
belongs to the value. *)
let test_scan_modifier_boundary () =
let source =
{|<span class="group-hover/-2a:underline group-hover/_x:underline
group-hover/edit:underline bg-red-500/-2 bg-red-500/50 aspect-[16/9]
bg-[url(a/b.png)] bg-cyan-400/(--alpha) w-1/2">x</span>|}
in
let found = Tw_tools.Source_scan.candidates source in
let refused cls =
Alcotest.(check bool)
(cls ^ " is not a candidate")
false (List.mem cls found)
in
let scanned cls =
Alcotest.(check bool) (cls ^ " is a candidate") true (List.mem cls found)
in
refused "group-hover/-2a:underline";
refused "group-hover/_x:underline";
refused "bg-red-500/-2";
(* A modifier that opens on anything else is a candidate, and so is every [/]
the value carries. *)
scanned "group-hover/edit:underline";
scanned "bg-red-500/50";
scanned "aspect-[16/9]";
scanned "bg-[url(a/b.png)]";
scanned "bg-cyan-400/(--alpha)";
scanned "w-1/2"

let tests =
[
test_case "a modifier opening on - or _ is no candidate" `Quick
test_scan_modifier_boundary;
test_case "unbalanced bracket stops at the newline" `Quick
test_unbalanced_bracket_stops_at_newline;
test_case "split whitespace" `Quick test_split_whitespace;
Expand Down
Loading