From fe17dae39cc3ca05c27de44636316fdf60d218b8 Mon Sep 17 00:00:00 2001 From: Thomas Gazagnaire Date: Sun, 13 Sep 2026 20:17:42 +0200 Subject: [PATCH 1/2] borders, position: write a bracket no length reader took into its longhand The corner radii and the inset sides each name their own longhands, so a bracket no length reader took still names them and the value goes there verbatim - Tailwind's token-stream contract. Refusing dropped the selector. That is 31 more of the families the contract doc inventories. A theme() call still standing in the text is not such a value: it 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. p-[--theme(spacing.4)] is the v4 spelling over a v3 dot path. --- lib/borders.ml | 42 ++++++++++++++++++++++++++++++++++++++--- lib/position.ml | 50 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/lib/borders.ml b/lib/borders.ml index a6662f87..f5332e8e 100644 --- a/lib/borders.ml +++ b/lib/borders.ml @@ -59,6 +59,10 @@ type rounded_size = | Rsz_theme of string (* radius named by a project [--radius-*] token (rounded-blob) *) | Rsz_arbitrary of string * Css.length + | Rsz_raw of string * string +(* rounded-[foo]: a corner names its own longhands, so a bracket no length + reader took still reaches them and the value is forwarded verbatim - + Tailwind's token-stream contract. *) module Handler = struct open Style @@ -412,6 +416,26 @@ module Handler = struct Shared by both the sized and arbitrary radius utilities; [Corner.All] uses the [border-radius] shorthand, all others target the matching corner/logical properties. *) + (* The longhands each corner names, for a value no typed setter can hold. *) + let radius_properties_for_position = function + | Corner.All -> [ "border-radius" ] + | Corner.Top -> [ "border-top-left-radius"; "border-top-right-radius" ] + | Corner.Right -> + [ "border-top-right-radius"; "border-bottom-right-radius" ] + | Corner.Bottom -> + [ "border-bottom-right-radius"; "border-bottom-left-radius" ] + | Corner.Left -> [ "border-top-left-radius"; "border-bottom-left-radius" ] + | Corner.Top_left -> [ "border-top-left-radius" ] + | Corner.Top_right -> [ "border-top-right-radius" ] + | Corner.Bottom_right -> [ "border-bottom-right-radius" ] + | Corner.Bottom_left -> [ "border-bottom-left-radius" ] + | Corner.Start -> [ "border-start-start-radius"; "border-end-start-radius" ] + | Corner.End -> [ "border-start-end-radius"; "border-end-end-radius" ] + | Corner.Start_start -> [ "border-start-start-radius" ] + | Corner.Start_end -> [ "border-start-end-radius" ] + | Corner.End_start -> [ "border-end-start-radius" ] + | Corner.End_end -> [ "border-end-end-radius" ] + let radius_decls_for_position pos (len : Css.length) : Css.declaration list = match pos with | Corner.All -> [ Css.border_radius (radius_value len) ] @@ -499,6 +523,11 @@ module Handler = struct (decl :: radius_decls_for_position pos (Var r : Css.length)) )) | Rsz_arbitrary (_, len) -> style (radius_decls_for_position pos len) + | Rsz_raw (_, value) -> + style + (List.filter_map + (fun property -> Parse.opaque_declaration property value) + (radius_properties_for_position pos)) (* Outline style variable - used by outline utilities that set the style *) let outline_style_var = @@ -961,7 +990,10 @@ module Handler = struct let inner = Parse.bracket_inner v in match parse_length inner with | Some len -> Ok (Rounded (Corner.All, Rsz_arbitrary (inner, len))) - | None -> err_not_utility) + | None -> ( + match Parse.arbitrary_declaration_value inner with + | Some raw -> Ok (Rounded (Corner.All, Rsz_raw (inner, raw))) + | None -> err_not_utility)) | [ "rounded"; tok ] -> ( match rounded_size_of_string tok with | Some size -> Ok (Rounded (Corner.All, size)) @@ -976,7 +1008,11 @@ module Handler = struct let inner = Parse.bracket_inner v in match (corner_of_string pos, parse_length inner) with | Some pos, Some len -> Ok (Rounded (pos, Rsz_arbitrary (inner, len))) - | _ -> err_not_utility) + | Some pos, None -> ( + match Parse.arbitrary_declaration_value inner with + | Some raw -> Ok (Rounded (pos, Rsz_raw (inner, raw))) + | None -> err_not_utility) + | None, _ -> err_not_utility) | [ "rounded"; pos; size ] -> ( match (corner_of_string pos, rounded_size_of_string size) with | Some pos, Some size -> Ok (Rounded (pos, size)) @@ -1137,7 +1173,7 @@ module Handler = struct | Rsz_4xl -> "-4xl" | Rsz_full -> "-full" | Rsz_theme name -> "-" ^ name - | Rsz_arbitrary (raw, _) -> "-[" ^ raw ^ "]" + | Rsz_arbitrary (raw, _) | Rsz_raw (raw, _) -> "-[" ^ raw ^ "]" in "rounded" ^ pos_str ^ size_str | Outline -> "outline" diff --git a/lib/position.ml b/lib/position.ml index 542f224e..f6e9b3e2 100644 --- a/lib/position.ml +++ b/lib/position.ml @@ -40,6 +40,12 @@ let read_paren_calc inner : Css.length Css.calc option = the negative as [calc((a + b) * -1)], where the group is a calc sub-expression; the positive it writes as [top: (a + b)], which is no declaration a browser accepts, so there is nothing to agree with. *) +(* A bracket no length reader took is still one declaration value, which the + side's own longhand takes verbatim. *) +let bracket_token_stream s = + if not (Parse.is_bracket_value s) then None + else Parse.arbitrary_declaration_value (Parse.bracket_inner s) + let parse_bracket_length ?(negate = false) s : Css.length option = if not (Parse.is_bracket_value s) then None else @@ -123,6 +129,20 @@ module Side = struct (* What the side writes. [start] and [inset-s] name one property under two spellings, as [end] and [inset-e] do. *) + (* The longhand each side names, for a value no typed setter can hold. *) + let properties = function + | Top -> [ "top" ] + | Right -> [ "right" ] + | Bottom -> [ "bottom" ] + | Left -> [ "left" ] + | Inset -> [ "inset" ] + | Inset_x -> [ "inset-inline" ] + | Inset_y -> [ "inset-block" ] + | Start | Inset_s -> [ "inset-inline-start" ] + | End | Inset_e -> [ "inset-inline-end" ] + | Inset_bs -> [ "inset-block-start" ] + | Inset_be -> [ "inset-block-end" ] + let declarations side (len : Css.length) = match side with | Top -> [ Css.top len ] @@ -227,6 +247,11 @@ module Handler = struct | Pos_arbitrary of Side.t * string * Css.length (* raw bracket suffix kept for the class name, value already signed *) | Neg_pos_arbitrary of Side.t * string * Css.length + | Pos_raw of Side.t * string * string + (* top-[foo]: an inset side writes one longhand, so a bracket no length + reader took still names it and the value is forwarded verbatim - + Tailwind's token-stream contract. *) + | Neg_pos_raw of Side.t * string * string | Pos_named of Side.t * string | Neg_pos_named of Side.t * string (* theme token reference like inset-shadowned *) @@ -316,6 +341,11 @@ module Handler = struct style (Side.declarations side (Pct (-.frac_pct f))) | Pos_arbitrary (side, _, len) | Neg_pos_arbitrary (side, _, len) -> style (Side.declarations side len) + | Pos_raw (side, _, value) | Neg_pos_raw (side, _, value) -> + style + (List.filter_map + (fun property -> Parse.opaque_declaration property value) + (Side.properties side)) | Pos_named (side, name) -> style (Side.declarations side (named_inset_value theme name)) | Neg_pos_named (side, name) -> @@ -421,6 +451,8 @@ module Handler = struct | Neg_pos_fraction (side, _) | Pos_arbitrary (side, _, _) | Neg_pos_arbitrary (side, _, _) + | Pos_raw (side, _, _) + | Neg_pos_raw (side, _, _) | Pos_named (side, _) | Neg_pos_named (side, _) -> side_slot side @@ -486,14 +518,22 @@ module Handler = struct | Some len -> Ok (Pos_arbitrary (side, n, len)) | None when Parse.is_valid_theme_name n && is_named_inset theme n -> Ok (Pos_named (side, n)) - | None -> Error (`Msg "invalid") + | None -> ( + match bracket_token_stream n with + | Some raw -> Ok (Pos_raw (side, n, raw)) + | None -> Error (`Msg "invalid")) in let neg_arbitrary_or_named side n = match parse_bracket_length ~negate:true n with | Some len -> Ok (Neg_pos_arbitrary (side, n, len)) | None when Parse.is_valid_theme_name n && is_named_inset theme n -> Ok (Neg_pos_named (side, n)) - | None -> Error (`Msg "invalid") + | None -> ( + (* [calc( * -1)] is what a negated arbitrary writes whatever + the unit, and a value no reader took is no exception. *) + match bracket_token_stream n with + | Some raw -> Ok (Neg_pos_raw (side, n, "calc(" ^ raw ^ " * -1)")) + | None -> Error (`Msg "invalid")) in (* A fraction, then the side's own scale, then the shared tail. Every inset side reads all three, so the suffix vocabulary is one function of the @@ -688,8 +728,10 @@ module Handler = struct Side.name side ^ "-" ^ Spacing.pp_spacing_suffix sp | Neg_pos_spacing (side, sp) -> "-" ^ Side.name side ^ "-" ^ Spacing.pp_spacing_suffix sp - | Pos_arbitrary (side, raw, _) -> Side.name side ^ "-" ^ raw - | Neg_pos_arbitrary (side, raw, _) -> "-" ^ Side.name side ^ "-" ^ raw + | Pos_arbitrary (side, raw, _) | Pos_raw (side, raw, _) -> + Side.name side ^ "-" ^ raw + | Neg_pos_arbitrary (side, raw, _) | Neg_pos_raw (side, raw, _) -> + "-" ^ Side.name side ^ "-" ^ raw | Pos_fraction (side, f) -> Side.name side ^ "-" ^ f | Neg_pos_fraction (side, f) -> "-" ^ Side.name side ^ "-" ^ f | Pos_named (side, name) -> Side.name side ^ "-" ^ name From c99204d00e7051c17e0dd6672c3999914ca3ad2f Mon Sep 17 00:00:00 2001 From: Thomas Gazagnaire Date: Sun, 13 Sep 2026 22:02:47 +0200 Subject: [PATCH 2/2] test: pin what a parenthesised group writes, not that it is refused The commit before this one gives the inset sides their last resort, so top-[(var(--a)+var(--b))] is a declaration rather than a refusal. The assertion belongs here, with the change it describes, not two PRs later. --- test/test_position.ml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/test/test_position.ml b/test/test_position.ml index f54094e1..a06fb5e3 100644 --- a/test/test_position.ml +++ b/test/test_position.ml @@ -508,14 +508,11 @@ let negative_named_inset_on_every_side () = accepts *) check_themed theme "-top-[(var(--a)+var(--b))]" [ "top:calc((var(--a) + var(--b))*-1)" ]; - Test_helpers.check_invalid_input - ~why: - (Test_helpers.Diverges - "Tailwind writes the group out unwrapped, as top: (var(--a) + \ - var(--b)); tw refuses the class rather than emit a declaration no \ - browser reads") - (module Tw.Position.Handler) - "top-[(var(--a)+var(--b))]"; + (* Unsigned, the group is no length either, so it goes to the side's own + longhand as the token stream it is - the same text the CLI writes, which no + browser reads and both sides therefore agree on. *) + Test_helpers.check_declarations "top-[(var(--a)+var(--b))]" + [ "top:(var(--a)+var(--b))" ]; (* and a name the theme binds in neither namespace is still no utility *) Test_helpers.check_invalid_input (module Tw.Position.Handler) "-top-level"; Test_helpers.check_invalid_input (module Tw.Position.Handler) "-bottom-right"