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
12 changes: 6 additions & 6 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,12 @@
declarations the browser discards rather than being refused outright, which
dropped the selector and left an element carrying the class with nothing at
all. A bare number after the hint is pixels, the spelling Tailwind's minifier
ships. Eight families do the same for a bracket no reader took at all,
writing it into their colour: `text-[notacolour]`, `stroke-[1zz]`,
`border-[abc]` and a malformed hex in any of `text-`, `outline-`, `border-`,
`accent-`, `caret-`, `placeholder-`, `fill-` and `stroke-` reach the sheet
where they used to be refused. `bg-` still refuses them (#761, #762, #763,
#764).
ships. Every colour family does the same for a bracket no reader took at all,
writing it into the longhand the hint named or into the colour when there is
no hint: `text-[notacolour]`, `stroke-[1zz]`, `bg-[image:nope]` and a
malformed hex in any of them reach the sheet where they used to be refused.
`bg-[size:...]` no longer substitutes `background-size: auto` for a value it
cannot read (#761, #762, #763, #764, #765).
- 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
11 changes: 7 additions & 4 deletions docs/token-stream-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,10 +313,13 @@ does not. `text-[…]` and `outline-[…]` now write a bracket
no reader took into their colour, which is each family's last resort, so
`text-[foo:1.25rem]`, `text-[foo:red]`, `text-[notacolour]`,
`outline-[foo:red]` and a malformed hex all reach the sheet. `border-`, `fill-`, `stroke-`, `accent-`,
`caret-` and `placeholder-` followed. `bg-` is the one still refusing: its
bracket runs through a size, a position, an image and a url before any colour
reader sees it, so there is no single point where a refusal falls through, and
the per-side border colours have no last resort either. An opacity modifier over a value that is not a colour
`caret-`, `placeholder-` and `bg-` followed. `bg-` took the most work, for the
reason a family with several readers always will: its bracket runs through a
size, a position, an image and a url, so each reader forwards its own value and
only the unhinted fall-through reaches the colour. The fall-through is gated on
the text being one bracket value, or `bg-[10px][20px]` would be accepted as a
single declaration. What is left is the per-side border colours, a separate
constructor per side. An opacity modifier over a value that is not a colour
stays refused everywhere: the CLI writes a `color-mix()` around the raw token,
and cascade's colour types hold colours, not token streams. The width hints in `lib/typography.ml`,
`lib/borders.ml` and `lib/svg.ml` no longer refuse a value their reader
Expand Down
98 changes: 83 additions & 15 deletions lib/backgrounds.ml
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,14 @@ module Handler = struct
| Bg_opacity of Color.color * int * Color.opacity_modifier
(* bg-[length:...] - explicit length prefix for bracket size *)
| Bg_bracket_length of string
| (* A bracket no reader took. The hint chooses the longhand and the value
is forwarded verbatim, which is Tailwind's token-stream contract; with
no hint, or one this family does not know, the colour is the last
resort. The browser discards the declaration; the selector is what the
class was for. *)
Bg_bracket_raw_color of string * string
| Bg_bracket_raw_image of string * string
| Bg_bracket_raw_position of string * string
(* bg-position-[...] bracket notation *)
| Bg_position_bracket of string * Css.position_value
(* bg-size-[...] bracket notation *)
Expand Down Expand Up @@ -305,6 +313,10 @@ module Handler = struct
| Bg_bracket_cover -> "bg-[cover]"
| Bg_bracket_size v -> "bg-[size:" ^ v ^ "]"
| Bg_bracket_length v -> "bg-[length:" ^ v ^ "]"
| Bg_bracket_raw_color (v, _)
| Bg_bracket_raw_image (v, _)
| Bg_bracket_raw_position (v, _) ->
"bg-[" ^ v ^ "]"
| Bg_bracket_position (v, _) -> "bg-[" ^ v ^ "]"
| Bg_bracket_typed_position (inner, _) -> "bg-[" ^ inner ^ "]"
| Bg_bracket_color_var v -> "bg-[color:" ^ v ^ "]"
Expand Down Expand Up @@ -419,7 +431,7 @@ module Handler = struct
| Bg _ | Bg_opacity _ | Bg_inherit | Bg_current | Bg_current_opacity _
| Bg_transparent | Bg_bracket_color _ | Bg_bracket_color_opacity _
| Bg_bracket_color_var _ | Bg_bracket_color_var_opacity _ | Bg_bracket_var _
| Bg_bracket_var_opacity _ ->
| Bg_bracket_var_opacity _ | Bg_bracket_raw_color _ ->
198
| Bg_gradient_to _ | Bg_linear_to _ | Bg_linear_to_interp _
| Bg_linear_angle _ | Bg_linear_angle_neg _ | Bg_linear_angle_interp _
Expand All @@ -429,7 +441,8 @@ module Handler = struct
| Bg_conic_angle_neg_interp _ | Bg_radial | Bg_radial_interp _
| Bg_conic_bracket _ | Bg_radial_bracket _ | Bg_bracket_image_var _
| Bg_bracket_image _ | Bg_bracket_linear_gradient _ | Bg_bracket_url _
| Bg_bracket_image_url _ | Bg_bracket_url_var _ | Bg_none ->
| Bg_bracket_image_url _ | Bg_bracket_url_var _ | Bg_bracket_raw_image _
| Bg_none ->
199
| Via_none -> 202
| Gradient_color (From, _) -> 203
Expand All @@ -447,7 +460,7 @@ module Handler = struct
| Bg_fixed | Bg_local | Bg_scroll -> 252
| Bg_clip_border | Bg_clip_padding | Bg_clip_content | Bg_clip_text -> 253
| Bg_position _ | Bg_bracket_position _ | Bg_bracket_typed_position _
| Bg_position_bracket _ ->
| Bg_position_bracket _ | Bg_bracket_raw_position _ ->
254
| Bg_repeat | Bg_no_repeat | Bg_repeat_x | Bg_repeat_y | Bg_repeat_round
| Bg_repeat_space ->
Expand Down Expand Up @@ -1516,9 +1529,15 @@ module Handler = struct
| Bg_bracket_contain -> style [ Css.background_size Contain ]
| Bg_bracket_cover -> style [ Css.background_size Cover ]
| Bg_bracket_size inner -> (
(* A value the size grammar declines is forwarded verbatim, not replaced
by [auto]: the hint chose the longhand, and substituting a value the
class never asked for paints something rather than nothing. *)
match parse_bracket_size inner with
| Some decl -> style [ decl ]
| None -> style [ Css.background_size Auto ])
| None ->
style
(Option.to_list
(Parse.opaque_declaration "background-size" inner)))
| Bg_bracket_position (_, pos) -> style [ Css.background_position [ pos ] ]
| Bg_bracket_typed_position (_, pos) ->
style [ Css.background_position [ pos ] ]
Expand Down Expand Up @@ -1590,10 +1609,23 @@ module Handler = struct
| Bg_current_opacity opacity -> Color.bg_current_with_opacity ~theme opacity
| Bg_transparent -> style [ Css.background_color (Css.hex "#0000") ]
| Bg_opacity (color, shade, opacity) -> bg_with_opacity color shade opacity
| Bg_bracket_raw_color (_, value) ->
style
(Option.to_list (Parse.opaque_declaration "background-color" value))
| Bg_bracket_raw_image (_, value) ->
style
(Option.to_list (Parse.opaque_declaration "background-image" value))
| Bg_bracket_raw_position (_, value) ->
style
(Option.to_list
(Parse.opaque_declaration "background-position" value))
| Bg_bracket_length inner -> (
match parse_bracket_size inner with
| Some decl -> style [ decl ]
| None -> style [ Css.background_size Auto ])
| None ->
style
(Option.to_list
(Parse.opaque_declaration "background-size" inner)))
| Bg_position_bracket (_, pos) -> style [ Css.background_position [ pos ] ]
| Bg_size_bracket inner -> (
(* [of_class] refused an empty hint, so the peel succeeds here. *)
Expand Down Expand Up @@ -1929,6 +1961,15 @@ module Handler = struct
if !depth = 0 then close := i)
done;
let parse_opacity s = Color.opacity_of_string ~theme s in
(* The last resort forwards a value verbatim, so it has to be handed one
value: [bg-[10px][20px]] is two brackets glued together, which is no
utility to Tailwind either. Every reader above answers for a shape it
recognises; only the fall-through needs this. *)
let last_resort inner =
if Parse.is_bracket_value bracket_stuff then
Parse.arbitrary_declaration_value inner
else None
in
if !close >= 0 && !close + 1 < len && bracket_stuff.[!close + 1] = '/'
then
(* Bracket with opacity: [color:var(--x)]/50 *)
Expand Down Expand Up @@ -1986,7 +2027,12 @@ module Handler = struct
in
match bracket_position_value v with
| Some pos -> Ok (Bg_bracket_typed_position (inner, pos))
| None -> Error (`Msg ("Unknown bg bracket position: " ^ v)))
| None -> (
(* The hint chose the longhand, so the value goes there
whatever the position grammar makes of it. *)
match last_resort inner with
| Some raw -> Ok (Bg_bracket_raw_position (inner, raw))
| None -> Error (`Msg ("Unknown bg bracket position: " ^ v))))
| _ when has_bracket_color_hint inner -> (
(* The hint says how to read the value written after it; only a
var() reference there names a custom property. *)
Expand All @@ -1995,23 +2041,31 @@ module Handler = struct
else
match Color.parse_bracket_color v with
| Some css_color -> Ok (Bg_bracket_color (inner, css_color))
| None -> Error (`Msg ("Unknown bg bracket color: " ^ v)))
| None -> (
match last_resort inner with
| Some raw -> Ok (Bg_bracket_raw_color (inner, raw))
| None -> Error (`Msg ("Unknown bg bracket color: " ^ v))))
| _ when String.length inner > 6 && String.sub inner 0 6 = "image:"
-> (
(* The [image:] data-type hint forces a background-image. The
value is a [url(...)] literal, a [var(...)] reference, or a
literal image (e.g. a gradient). *)
let v = String.sub inner 6 (String.length inner - 6) in
let raw_image () =
match last_resort inner with
| Some raw -> Ok (Bg_bracket_raw_image (inner, raw))
| None -> Error (`Msg ("Unknown bg bracket image: " ^ v))
in
if String.starts_with ~prefix:"url(" v then
match Parse.url_token (Parse.decode_arbitrary_value v) with
| Some _ -> Ok (Bg_bracket_image_url v)
| None -> Error (`Msg ("Unknown bg bracket image: " ^ v))
| None -> raw_image ()
else if Parse.is_var v then Ok (Bg_bracket_image_var v)
else
match parse_bracket_image v with
| Some img -> Ok (Bg_bracket_image (v, img))
| None -> Error (`Msg ("Unknown bg bracket image: " ^ v)))
| _ when String.length inner > 4 && String.sub inner 0 4 = "url:" ->
| None -> raw_image ())
| _ when String.length inner > 4 && String.sub inner 0 4 = "url:" -> (
(* The [url:] data-type hint forces a background-image the way
[image:] does. Only a var() reference names a custom property;
any other spelling is the image itself. *)
Expand All @@ -2021,11 +2075,19 @@ module Handler = struct
Parse.url_token (Parse.decode_arbitrary_value v) <> None
|| parse_bracket_image v <> None
then Ok (Bg_bracket_url_var v)
else Error (`Msg ("Unknown bg bracket url: " ^ v))
else
match last_resort inner with
| Some raw -> Ok (Bg_bracket_raw_image (inner, raw))
| None -> Error (`Msg ("Unknown bg bracket url: " ^ v)))
| _ when String.starts_with ~prefix:"url(" inner -> (
match Parse.url_token (Parse.decode_arbitrary_value inner) with
| Some _ -> Ok (Bg_bracket_url inner)
| None -> Error (`Msg ("Unknown bg bracket url: " ^ inner)))
| None -> (
(* No hint, so the colour is the last resort even though the
text opens with [url(]. *)
match last_resort inner with
| Some raw -> Ok (Bg_bracket_raw_color (inner, raw))
| None -> Error (`Msg ("Unknown bg bracket url: " ^ inner))))
| _ when Parse.is_var inner -> Ok (Bg_bracket_var inner)
| _ -> (
(* Try parsing as background-image (gradients, urls,
Expand All @@ -2038,9 +2100,15 @@ module Handler = struct
| None -> (
match parse_bracket_position inner with
| Some pos -> Ok (Bg_bracket_position (inner, pos))
| None ->
Error (`Msg ("Unknown bg bracket value: " ^ inner)))))
)
| None -> (
(* The colour is this family's last resort, which an
unknown hint reaches as well as no hint at all. *)
match last_resort inner with
| Some raw -> Ok (Bg_bracket_raw_color (inner, raw))
| None ->
Error
(`Msg ("Unknown bg bracket value: " ^ inner)))))
))
| "bg" :: rest when List.exists has_opacity rest -> (
match Color.shade_and_opacity_of_strings ~theme rest with
| Ok (color, shade, opacity) -> Ok (Bg_opacity (color, shade, opacity))
Expand Down
30 changes: 29 additions & 1 deletion lib/parse.ml
Original file line number Diff line number Diff line change
Expand Up @@ -556,8 +556,36 @@ let declaration_value_of s =

(* Every family reaches its last resort with the bracket as the author wrote it,
so refusing an empty hint here refuses it everywhere. *)
(* A [theme()] or [--theme()] call is resolved before a family's reader sees
the bracket, so one still standing in the text is a lookup that declined.
Tailwind emits nothing for such a class, so it is not a token stream to
forward: [p-[--theme(spacing.4)]] is the v4 spelling over a v3 dot path,
which resolves to nothing and names no utility. *)
let holds_unresolved_theme_call s =
(* [theme(] preceded by anything that cannot continue an identifier, so a
function whose name merely ends in "theme" does not match. *)
let needle = "theme(" in
let n = String.length needle and len = String.length s in
let rec scan i =
match Strings.index ~sub:needle (String.sub s i (len - i)) with
| None -> false
| Some off ->
let at = i + off in
let before = if at = 0 then ' ' else s.[at - 1] in
let opens_a_name =
match before with
| 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '_' -> true
| '-' -> at >= 2 && s.[at - 2] <> '-'
| _ -> false
in
if opens_a_name then scan (at + n) else true
in
scan 0

let arbitrary_declaration_value s =
Option.bind (value_after_hint s) declaration_value_of
match Option.bind (value_after_hint s) declaration_value_of with
| Some value when holds_unresolved_theme_call value -> None
| answer -> answer

let wrap_declaration_value ~before ~after value =
if value = "" || not (is_declaration_value value) then None
Expand Down
7 changes: 6 additions & 1 deletion lib/parse.mli
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,12 @@ val arbitrary_declaration_value : string -> string option
returns its CSS declaration value: the text after any {!data_type_hint},
which has to hold something other than blank space. Values that can
terminate or swallow the declaration are [None], as is a bracket whose hint
is empty. *)
is empty.

A [theme()] or [--theme()] call still standing in the text is also [None]:
such a call is resolved before a family's reader sees the bracket, so one
that survived is a lookup that declined, and Tailwind names no utility for
it. *)

val wrap_declaration_value :
before:string -> after:string -> string -> string option
Expand Down
42 changes: 22 additions & 20 deletions test/test_backgrounds.ml
Original file line number Diff line number Diff line change
Expand Up @@ -189,22 +189,19 @@ let test_bracket_position_grammar () =
writes "bg-[position:120px_120px]" "120px 120px";
writes "bg-position-[center_-100px]" "50% -100px"

(* A bracket value the property cannot take is not a utility. [bg-[image:...]]
used to emit an empty rule and [bg-[position:...]] a plausible-looking
[center]: no CSS the class asked for, and no diagnostic. *)
(* A bracket value the property cannot take goes to that property anyway: the
hint chose the longhand, and the browser discards what it cannot read. What
is ruled out is the two answers that are neither - [bg-[image:...]] used to
emit an empty rule and [bg-[position:...]] a plausible-looking [center], so
the class silently painted something it never asked for. *)
let test_invalid_bracket_value () =
let rejected cls =
match Tw.of_string cls with
| Ok _ -> Alcotest.failf "expected %s to be rejected" cls
| Error _ -> ()
in
let accepted cls =
match Tw.of_string cls with
| Ok _ -> ()
| Error (`Msg m) -> Alcotest.failf "%s: %s" cls m
in
rejected "bg-[image:nope]";
rejected "bg-[position:nope]";
check_declarations "bg-[image:nope]" [ "background-image:nope" ];
check_declarations "bg-[position:nope]" [ "background-position:nope" ];
accepted "bg-[image:radial-gradient(white,black)]";
accepted "bg-[image:var(--x)]";
accepted "bg-[image:url(/a.png)]";
Expand Down Expand Up @@ -621,12 +618,15 @@ let test_bracket_data_type_hint_reads_the_value () =
Alcotest.(check string)
"bg-[color:red] round-trips" "bg-[color:red]"
(Tw.pp (Result.get_ok (Tw.of_string "bg-[color:red]")));
(* A value no colour reader takes is held open, not settled: Tailwind writes
the bracket out whatever it says, so refusing is an intermediate. *)
check_invalid_input
~why:(Diverges "emitted verbatim; tw needs an opaque declaration to match")
(module Tw.Backgrounds.Handler)
"bg-[color:notacolour]"
(* The colour is this family's last resort, so a value no colour reader takes
still reaches [background-color], forwarded verbatim. *)
check_declarations "bg-[color:notacolour]" [ "background-color:notacolour" ];
check_declarations "bg-[notacolour]" [ "background-color:notacolour" ];
(* An unknown hint lands there too, and so does a bracket that opens with
[url(] but holds no url. *)
check_declarations "bg-[foo:50%]" [ "background-color:50%" ];
check_declarations "bg-[url(x.png)_center]"
[ "background-color:url(x.png) center" ]

(* Tailwind knows two spellings for the hint that names a background-position,
and [bg-[…]] routes on the one the author wrote while the class name keeps
Expand All @@ -635,10 +635,12 @@ let test_percentage_hint_names_a_position () =
check_declarations "bg-[percentage:50%]" [ "background-position:50%" ];
check_declarations "bg-[position:50%]" [ "background-position:50%" ];
List.iter check [ "bg-[percentage:50%]"; "bg-[position:50%]" ];
check_invalid_input
~why:(Diverges "emitted verbatim; tw needs an opaque declaration to match")
(module Tw.Backgrounds.Handler)
"bg-[percentage:notaposition]"
(* Either spelling of the hint names the longhand, so a value the position
grammar declines still lands on [background-position]. *)
check_declarations "bg-[percentage:notaposition]"
[ "background-position:notaposition" ];
check_declarations "bg-[position:notaposition]"
[ "background-position:notaposition" ]

let tests =
[
Expand Down
Loading
Loading