package tw

  1. Overview
  2. Docs
Type-safe Tailwind CSS v4 in OCaml

Install

dune-project
 Dependency

Authors

Maintainers

Sources

tw-1.1.0.tbz
sha256=48754ab34d0a97c37f5f2dbf50ce46747ec0ca6d483f5adbb7305fc247fb5315
sha512=f43621b49e77adc23fab3c968e5041188e428228d1930b89c307fc8916c428f1943a5d74c21467219077247021f0ba83fda9234b0dd119dfd14b7f9746332bf7

doc/src/tw/masks.ml.html

Source file masks.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
(** Mask utilities for CSS masking.

    Provides utilities for mask-image, mask-composite, mask-mode, mask-type,
    mask-size, mask-position, mask-repeat, mask-clip, and mask-origin. *)

module Css = Cascade.Css

module Handler = struct
  open Style
  open Css

  (* The position keywords [mask-<position>] accepts. *)
  module Keyword = struct
    type t =
      | Bottom
      | Bottom_left
      | Bottom_right
      | Center
      | Left
      | Right
      | Top
      | Top_left
      | Top_right
  end

  (* The longhands an arbitrary mask bracket is written into, each beside the
     [-webkit-] spelling this family writes with it. *)
  module Longhand = struct
    type t = Image | Position | Size

    let properties = function
      | Image -> [ "-webkit-mask-image"; "mask-image" ]
      | Position -> [ "-webkit-mask-position"; "mask-position" ]
      | Size -> [ "-webkit-mask-size"; "mask-size" ]

    (* The slot Tailwind's property table gives each one. *)
    let rank = function Image -> 209 | Size -> 260 | Position -> 262
  end

  (* A bracket no reader here takes. Tailwind writes it into the longhand the
     class names and leaves the value spelled as the author wrote it, so whether
     it means anything is the browser's answer rather than tw's. *)
  type opaque = {
    spelling : string;  (** the class as written, which prints back *)
    longhand : Longhand.t;
    value : string;  (** the bracket's contents, decoded but not read *)
  }

  type t =
    | No_mask
    | Add
    | Exclude
    | Intersect
    | Subtract
    | Alpha
    | Luminance
    | Match
    | Type_alpha
    | Type_luminance
    | Auto
    | Contain
    | Cover
    | Position of Keyword.t
    | No_repeat
    | Repeat
    | Repeat_round
    | Repeat_space
    | Repeat_x
    | Repeat_y
    | Clip_border
    | Clip_padding
    | Clip_content
    | Clip_fill
    | Clip_stroke
    | Clip_view
    | No_clip
    | Origin_border
    | Origin_padding
    | Origin_content
    | Origin_fill
    | Origin_stroke
    | Origin_view
    (* Bracket notation *)
    | Bracket_contain
    | Bracket_cover
    | Bracket_size of string
    | Bracket_length of string
    | Bracket_position of string * Css.position_value list
    | Bracket_typed_position of string * Css.position_value list
    | Bracket_image_var of string
    | Bracket_url of string
    | Bracket_url_var of string
    | Bracket_var of string
    | Bracket_image of string
    (* Sub-property bracket notation: mask-position-[...], mask-size-[...] *)
    | Position_bracket of string * Css.position_value list
    | Position_bracket_var of string
    | Size_bracket of string
    | Size_bracket_var of string
    | Bracket_opaque of opaque

  let name = "masks"

  (* After the backgrounds and the mask-gradient utilities, before fill and
     stroke and before padding - Tailwind's own order. *)
  let priority _ = 21

  (* The composite and mode utilities write their [-webkit-] longhand twice:
     that is what Tailwind's minified sheet carries for them, and the site
     comparison reads the two sheets byte for byte. *)

  let mask_none =
    style [ Css.webkit_mask_image Css.None; Css.mask_image Css.None ]

  let mask_add =
    style
      [
        Css.webkit_mask_composite Source_over;
        Css.webkit_mask_composite Source_over;
        Css.mask_composite Css.Add;
      ]

  let mask_exclude =
    style
      [
        Css.webkit_mask_composite Xor;
        Css.webkit_mask_composite Xor;
        Css.mask_composite Css.Exclude;
      ]

  let mask_intersect =
    style
      [
        Css.webkit_mask_composite Source_in;
        Css.webkit_mask_composite Source_in;
        Css.mask_composite Css.Intersect;
      ]

  let mask_subtract =
    style
      [
        Css.webkit_mask_composite Source_out;
        Css.webkit_mask_composite Source_out;
        Css.mask_composite Css.Subtract;
      ]

  let mask_alpha =
    style
      [
        Css.webkit_mask_source_type Css.Alpha;
        Css.webkit_mask_source_type Css.Alpha;
        Css.mask_mode Css.Alpha;
      ]

  let mask_luminance =
    style
      [
        Css.webkit_mask_source_type Css.Luminance;
        Css.webkit_mask_source_type Css.Luminance;
        Css.mask_mode Css.Luminance;
      ]

  let mask_match =
    style
      [
        Css.webkit_mask_source_type Css.Auto;
        Css.webkit_mask_source_type Css.Auto;
        Css.mask_mode Match_source;
      ]

  let mask_type_alpha = style [ Css.mask_type Css.Alpha ]
  let mask_type_luminance = style [ Css.mask_type Css.Luminance ]

  (* mask-size *)
  let mask_auto =
    style [ Css.webkit_mask_size Css.Auto; Css.mask_size Css.Auto ]

  let mask_contain =
    style [ Css.webkit_mask_size Css.Contain; Css.mask_size Css.Contain ]

  let mask_cover =
    style [ Css.webkit_mask_size Css.Cover; Css.mask_size Css.Cover ]

  (* mask-position *)
  let mask_position' pos =
    let pos_val : Css.position_value list =
      match pos with
      | Keyword.Bottom -> [ Center_bottom ]
      | Keyword.Bottom_left -> [ XY (Pct 0., Pct 100.) ]
      | Keyword.Bottom_right -> [ XY (Pct 100., Pct 100.) ]
      | Keyword.Center -> [ Center ]
      | Keyword.Left -> [ Single (Pct 0.) ]
      | Keyword.Right -> [ Single (Pct 100.) ]
      | Keyword.Top -> [ Center_top ]
      | Keyword.Top_left -> [ XY (Pct 0., Pct 0.) ]
      | Keyword.Top_right -> [ XY (Pct 100., Pct 0.) ]
    in
    style [ Css.webkit_mask_position pos_val; Css.mask_position pos_val ]

  (* mask-repeat *)
  let mask_no_repeat' =
    style
      [ Css.webkit_mask_repeat Css.No_repeat; Css.mask_repeat Css.No_repeat ]

  let mask_repeat' =
    style [ Css.webkit_mask_repeat Css.Repeat; Css.mask_repeat Css.Repeat ]

  let mask_repeat_round' =
    style [ Css.webkit_mask_repeat Round; Css.mask_repeat Round ]

  let mask_repeat_space' =
    style [ Css.webkit_mask_repeat Space; Css.mask_repeat Space ]

  let mask_repeat_x' =
    style [ Css.webkit_mask_repeat Css.Repeat_x; Css.mask_repeat Css.Repeat_x ]

  let mask_repeat_y' =
    style [ Css.webkit_mask_repeat Css.Repeat_y; Css.mask_repeat Css.Repeat_y ]

  (* mask-clip and mask-origin utilities. Tailwind writes a [-webkit-] twin for
     every box, but WebKit's own grammar only has the three CSS boxes:
     [fill-box], [stroke-box], [view-box] and [no-clip] are dropped by every
     browser on the prefixed property, and [Css.webkit_mask_box] has no
     constructor for them, so those utilities carry the unprefixed declaration
     alone. *)
  let mask_clip_border =
    style [ Css.webkit_mask_clip Border_box; Css.mask_clip Border_box ]

  let mask_clip_padding =
    style [ Css.webkit_mask_clip Padding_box; Css.mask_clip Padding_box ]

  let mask_clip_content =
    style [ Css.webkit_mask_clip Content_box; Css.mask_clip Content_box ]

  let mask_clip_fill = style [ Css.mask_clip Fill_box ]
  let mask_clip_stroke = style [ Css.mask_clip Stroke_box ]
  let mask_clip_view = style [ Css.mask_clip View_box ]
  let mask_no_clip = style [ Css.mask_clip Css.No_clip ]

  let mask_origin_border =
    style [ Css.webkit_mask_origin Border_box; Css.mask_origin Border_box ]

  let mask_origin_padding =
    style [ Css.webkit_mask_origin Padding_box; Css.mask_origin Padding_box ]

  let mask_origin_content =
    style [ Css.webkit_mask_origin Content_box; Css.mask_origin Content_box ]

  let mask_origin_fill = style [ Css.mask_origin Fill_box ]
  let mask_origin_stroke = style [ Css.mask_origin Stroke_box ]
  let mask_origin_view = style [ Css.mask_origin View_box ]

  (* Bracket notation helpers *)

  (* [mask-size-[...]], [mask-[size:...]] and [mask-[length:...]] take any CSS
     length, so a token is read with the value parser rather than a hand-picked
     unit table. *)
  let parse_bracket_len s = Parse.arbitrary_length s

  let parse_bracket_size inner =
    let parts =
      String.split_on_char '_' inner |> List.filter (fun s -> s <> "")
    in
    match parts with
    | [ w; h ] -> (
        match (parse_bracket_len w, parse_bracket_len h) with
        | Some wl, Some hl ->
            Some
              [
                Css.webkit_mask_size (Size (wl, hl));
                Css.mask_size (Size (wl, hl));
              ]
        | _ -> None)
    | [ v ] ->
        Option.map
          (fun l ->
            let size : Css.background_size = Length l in
            [ Css.webkit_mask_size size; Css.mask_size size ])
          (parse_bracket_len v)
    | _ -> None

  (* A bracket mask-position: the whole [<position>] grammar, one position per
     mask layer, comma-separated. [None] means the bracket is not a position,
     which [of_class] rejects: [mask-[position:top]] used to fall through the
     hand-rolled reading to a plausible-looking [center]. *)
  let parse_bracket_position inner : Css.position_value list option =
    let one entry : Css.position_value option =
      let cursor = Cascade.Cursor.of_string (Parse.decode_underscores entry) in
      match
        Cascade.Cursor.try_parse_full_err Css.Properties.read_position_value
          cursor
      with
      | Ok pos -> Some pos
      | Error _ -> None
    in
    let entries = String.split_on_char ',' inner |> List.map String.trim in
    let positions = List.map one entries in
    if List.exists Option.is_none positions then None
    else Some (List.filter_map Fun.id positions)

  let mask_position_style positions =
    style [ Css.webkit_mask_position positions; Css.mask_position positions ]

  let to_style _theme = function
    | No_mask -> mask_none
    | Add -> mask_add
    | Exclude -> mask_exclude
    | Intersect -> mask_intersect
    | Subtract -> mask_subtract
    | Alpha -> mask_alpha
    | Luminance -> mask_luminance
    | Match -> mask_match
    | Type_alpha -> mask_type_alpha
    | Type_luminance -> mask_type_luminance
    | Auto -> mask_auto
    | Contain -> mask_contain
    | Cover -> mask_cover
    | Position pos -> mask_position' pos
    | No_repeat -> mask_no_repeat'
    | Repeat -> mask_repeat'
    | Repeat_round -> mask_repeat_round'
    | Repeat_space -> mask_repeat_space'
    | Repeat_x -> mask_repeat_x'
    | Repeat_y -> mask_repeat_y'
    | Clip_border -> mask_clip_border
    | Clip_padding -> mask_clip_padding
    | Clip_content -> mask_clip_content
    | Clip_fill -> mask_clip_fill
    | Clip_stroke -> mask_clip_stroke
    | Clip_view -> mask_clip_view
    | No_clip -> mask_no_clip
    | Origin_border -> mask_origin_border
    | Origin_padding -> mask_origin_padding
    | Origin_content -> mask_origin_content
    | Origin_fill -> mask_origin_fill
    | Origin_stroke -> mask_origin_stroke
    | Origin_view -> mask_origin_view
    (* Bracket notation *)
    | Bracket_contain ->
        style [ Css.webkit_mask_size Css.Contain; Css.mask_size Css.Contain ]
    | Bracket_cover ->
        style [ Css.webkit_mask_size Css.Cover; Css.mask_size Css.Cover ]
    | Bracket_size inner -> (
        match parse_bracket_size inner with
        | Some decls -> style decls
        | None ->
            style [ Css.webkit_mask_size Css.Auto; Css.mask_size Css.Auto ])
    | Bracket_length inner -> (
        match parse_bracket_size inner with
        | Some decls -> style decls
        | None ->
            style [ Css.webkit_mask_size Css.Auto; Css.mask_size Css.Auto ])
    | Bracket_position (_, positions) -> mask_position_style positions
    | Bracket_typed_position (_, positions) -> mask_position_style positions
    (* A hint says how to read the value written after it; only a var()
       reference there names a custom property. *)
    | (Bracket_image_var v | Bracket_url_var v) when not (Parse.is_var v) -> (
        match Css.parse_background_image (Parse.decode_underscores v) with
        | Some (img :: _) ->
            style [ Css.webkit_mask_image img; Css.mask_image img ]
        | _ -> (
            match Parse.url_token (Parse.decode_arbitrary_value v) with
            | Some url ->
                style
                  [ Css.webkit_mask_image (Url url); Css.mask_image (Url url) ]
            | None -> style []))
    | Bracket_image_var v ->
        let bare = Parse.extract_var_name v in
        let var_ref : Css.background_image Css.var = Var.bracket bare in
        style
          [
            Css.webkit_mask_image (Var var_ref);
            Css.webkit_mask_image (Var var_ref);
            Css.mask_image (Var var_ref);
          ]
    (* The whole [url()] is kept, so cascade reads the token rather than masks
       slicing the file name out of it: the quotes are the tokeniser's, and an
       escape in it stands for one character of the URL. [of_class] refuses a
       value the token reader will not take, so [None] names nothing. *)
    | Bracket_url v -> (
        match Parse.url_token (Parse.decode_arbitrary_value v) with
        | Some url ->
            style [ Css.webkit_mask_image (Url url); Css.mask_image (Url url) ]
        | None -> style [])
    | Bracket_url_var v ->
        let bare = Parse.extract_var_name v in
        let var_ref : Css.background_image Css.var = Var.bracket bare in
        style
          [
            Css.webkit_mask_image (Var var_ref);
            Css.webkit_mask_image (Var var_ref);
            Css.mask_image (Var var_ref);
          ]
    | Bracket_var v ->
        let bare = Parse.extract_var_name v in
        let var_ref : Css.background_image Css.var = Var.bracket bare in
        style
          [
            Css.webkit_mask_image (Var var_ref);
            Css.webkit_mask_image (Var var_ref);
            Css.mask_image (Var var_ref);
          ]
    | Bracket_image v -> (
        match Css.parse_background_image (Parse.decode_underscores v) with
        | Some (img :: _) ->
            style [ Css.webkit_mask_image img; Css.mask_image img ]
        | _ ->
            invalid_arg ("mask-[" ^ v ^ "]: not a valid background-image value")
        )
    (* Sub-property bracket notation *)
    | Position_bracket (_, positions) -> mask_position_style positions
    | Position_bracket_var v ->
        let bare = Parse.extract_var_name v in
        let var_ref : Css.position_value Css.var = Var.bracket bare in
        style
          [
            Css.webkit_mask_position [ Var var_ref ];
            Css.webkit_mask_position [ Var var_ref ];
            Css.mask_position [ Var var_ref ];
          ]
    | Size_bracket inner -> (
        match parse_bracket_size inner with
        | Some decls -> style decls
        | None ->
            style [ Css.webkit_mask_size Css.Auto; Css.mask_size Css.Auto ])
    | Size_bracket_var v ->
        let bare = Parse.extract_var_name v in
        let var_ref : Css.background_size Css.var = Var.bracket bare in
        style
          [
            Css.webkit_mask_size (Var var_ref);
            Css.webkit_mask_size (Var var_ref);
            Css.mask_size (Var var_ref);
          ]
    | Bracket_opaque { longhand; value; _ } ->
        style
          (List.filter_map
             (fun property -> Parse.opaque_declaration property value)
             (Longhand.properties longhand))

  (* Tailwind sorts by the property a utility sets, at the rank its property
     table gives it: mask-image (209), then mask-composite (257) through
     mask-origin (264). Utilities that set the same property share a slot, where
     the class name breaks the tie. *)
  let property_rank = function
    | Bracket_image_var _ | Bracket_image _ | Bracket_url _ | Bracket_url_var _
    | Bracket_var _ | No_mask ->
        209
    | Add | Exclude | Intersect | Subtract -> 257
    | Alpha | Luminance | Match -> 258
    | Type_alpha | Type_luminance -> 259
    | Bracket_contain | Bracket_cover | Bracket_length _ | Bracket_size _ | Auto
    | Contain | Cover | Size_bracket _ | Size_bracket_var _ ->
        260
    | Clip_border | Clip_content | Clip_fill | Clip_padding | Clip_stroke
    | Clip_view | No_clip ->
        261
    | Bracket_position _ | Bracket_typed_position _ | Position _
    | Position_bracket _ | Position_bracket_var _ ->
        262
    | No_repeat | Repeat | Repeat_round | Repeat_space | Repeat_x | Repeat_y ->
        263
    | Origin_border | Origin_content | Origin_fill | Origin_padding
    | Origin_stroke | Origin_view ->
        264
    | Bracket_opaque { longhand; _ } -> Longhand.rank longhand

  (* Each of these writes one property and stops there, so it closes that
     property's slot: the mask-gradient utilities that write mask-image and
     carry on sort inside mask-image's, and the background sizing utilities that
     share this priority take the slots between the two families. *)
  let suborder t = Utility.Property_order.last (property_rank t)

  (* [mask-[<image>]] takes any background-image value, so what makes one is
     whether the value parser accepts it, not which gradient function it
     names. *)
  let is_image_value inner =
    match Css.parse_background_image (Parse.decode_underscores inner) with
    | Some (_ :: _) -> true
    | _ -> false

  (* What the value of an [image:]/[url:] hint may be: a var() reference, a
     [url()] token, or a literal image such as a gradient. *)
  let is_mask_image_value v =
    Parse.is_var v || is_image_value v
    || Parse.url_token (Parse.decode_arbitrary_value v) <> None

  (* A bracket no reader takes is still a utility: it is written into [longhand]
     as the author spelled it, less the data-type hint that chose the longhand.
     Take the whole bracket here and let [arbitrary_declaration_value] peel it,
     so the hint comes off once however this family routed the class. A value
     that would end the declaration or swallow what follows it is not one, and
     neither is a bracket opening with a [:]. *)
  let opaque spelling longhand inner =
    match Parse.arbitrary_declaration_value inner with
    | Some value -> Ok (Bracket_opaque { spelling; longhand; value })
    | None -> Error (`Msg ("Unknown mask bracket value: " ^ inner))

  (* Tailwind reads a bracket holding a math function as a length, and a length
     is a position component, so such a value goes to mask-position however the
     rest of it reads: [mask-[foo_calc(1+2)]] is a position where
     [mask-[foo_10px]] is an image. *)
  let holds_math_function inner =
    let len = String.length inner in
    let is_name_char c =
      (c >= 'a' && c <= 'z')
      || (c >= 'A' && c <= 'Z')
      || (c >= '0' && c <= '9')
      || c = '-'
    in
    let rec name_start i =
      if i > 0 && is_name_char inner.[i - 1] then name_start (i - 1) else i
    in
    let rec scan i =
      i < len
      && ((inner.[i] = '('
          &&
          let s = name_start i in
          Css.Properties.is_math_function
            (String.lowercase_ascii (String.sub inner s (i - s))))
         || scan (i + 1))
    in
    scan 0

  let of_class _theme class_name =
    let parts = Parse.split_class class_name in
    match parts with
    | [ "mask"; "none" ] -> Ok No_mask
    | [ "mask"; "add" ] -> Ok Add
    | [ "mask"; "exclude" ] -> Ok Exclude
    | [ "mask"; "intersect" ] -> Ok Intersect
    | [ "mask"; "subtract" ] -> Ok Subtract
    | [ "mask"; "alpha" ] -> Ok Alpha
    | [ "mask"; "luminance" ] -> Ok Luminance
    | [ "mask"; "match" ] -> Ok Match
    | [ "mask"; "type"; "alpha" ] -> Ok Type_alpha
    | [ "mask"; "type"; "luminance" ] -> Ok Type_luminance
    | [ "mask"; "auto" ] -> Ok Auto
    | [ "mask"; "contain" ] -> Ok Contain
    | [ "mask"; "cover" ] -> Ok Cover
    (* mask-position *)
    | [ "mask"; "bottom" ] -> Ok (Position Keyword.Bottom)
    | [ "mask"; "bottom"; "left" ] -> Ok (Position Keyword.Bottom_left)
    | [ "mask"; "bottom"; "right" ] -> Ok (Position Keyword.Bottom_right)
    | [ "mask"; "center" ] -> Ok (Position Keyword.Center)
    | [ "mask"; "left" ] -> Ok (Position Keyword.Left)
    | [ "mask"; "right" ] -> Ok (Position Keyword.Right)
    | [ "mask"; "top" ] -> Ok (Position Keyword.Top)
    | [ "mask"; "top"; "left" ] -> Ok (Position Keyword.Top_left)
    | [ "mask"; "top"; "right" ] -> Ok (Position Keyword.Top_right)
    (* mask-repeat *)
    | [ "mask"; "no"; "repeat" ] -> Ok No_repeat
    | [ "mask"; "repeat" ] -> Ok Repeat
    | [ "mask"; "repeat"; "round" ] -> Ok Repeat_round
    | [ "mask"; "repeat"; "space" ] -> Ok Repeat_space
    | [ "mask"; "repeat"; "x" ] -> Ok Repeat_x
    | [ "mask"; "repeat"; "y" ] -> Ok Repeat_y
    (* mask-clip *)
    | [ "mask"; "clip"; "border" ] -> Ok Clip_border
    | [ "mask"; "clip"; "padding" ] -> Ok Clip_padding
    | [ "mask"; "clip"; "content" ] -> Ok Clip_content
    | [ "mask"; "clip"; "fill" ] -> Ok Clip_fill
    | [ "mask"; "clip"; "stroke" ] -> Ok Clip_stroke
    | [ "mask"; "clip"; "view" ] -> Ok Clip_view
    | [ "mask"; "no"; "clip" ] -> Ok No_clip
    (* mask-origin *)
    | [ "mask"; "origin"; "border" ] -> Ok Origin_border
    | [ "mask"; "origin"; "padding" ] -> Ok Origin_padding
    | [ "mask"; "origin"; "content" ] -> Ok Origin_content
    | [ "mask"; "origin"; "fill" ] -> Ok Origin_fill
    | [ "mask"; "origin"; "stroke" ] -> Ok Origin_stroke
    | [ "mask"; "origin"; "view" ] -> Ok Origin_view
    (* Sub-property bracket notation: mask-position-[...], mask-size-[...] *)
    | [ "mask"; "position"; bracket ] when Parse.is_bracket_value bracket -> (
        let inner = Parse.bracket_inner bracket in
        (* This class names its longhand outright, so a hint in front of the
           value says nothing more than where the value starts. *)
        match Parse.data_type_hint inner with
        | Some _ -> opaque class_name Longhand.Position inner
        | None -> (
            if Parse.is_var inner then Ok (Position_bracket_var inner)
            else
              match parse_bracket_position inner with
              | Some positions -> Ok (Position_bracket (inner, positions))
              | None -> opaque class_name Longhand.Position inner))
    | [ "mask"; "size"; bracket ] when Parse.is_bracket_value bracket -> (
        let inner = Parse.bracket_inner bracket in
        match Parse.data_type_hint inner with
        | Some _ -> opaque class_name Longhand.Size inner
        | None ->
            if Parse.is_var inner then Ok (Size_bracket_var inner)
            else if parse_bracket_size inner = None then
              opaque class_name Longhand.Size inner
            else Ok (Size_bracket inner))
    (* Bracket notation: mask-[...] *)
    | [ "mask"; bracket ] when Parse.is_bracket_value bracket -> (
        let inner = Parse.bracket_inner bracket in
        (* A hint with nothing after it names no value: it is read below as a
           hint Tailwind does not know. *)
        let hint =
          Option.bind (Parse.data_type_hint inner) (fun (h, v) ->
              if v = "" then Option.none else Option.some (h, v))
        in
        match (inner, hint) with
        | "contain", _ -> Ok Bracket_contain
        | "cover", _ -> Ok Bracket_cover
        | _, Some ("length", v) ->
            (* The [length:] hint forces a mask-size, and says nothing about
               whether the value is one: a size the grammar cannot take is
               written out as the author spelled it. *)
            if parse_bracket_size v = None then
              opaque class_name Longhand.Size inner
            else Ok (Bracket_length v)
        | _, Some ("size", v) ->
            if parse_bracket_size v = None then
              opaque class_name Longhand.Size inner
            else Ok (Bracket_size v)
        | _, Some ("position", v) -> (
            (* The [position:] hint forces a mask-position the same way. *)
            match parse_bracket_position v with
            | Some positions -> Ok (Bracket_typed_position (v, positions))
            | None -> opaque class_name Longhand.Position inner)
        (* An [image:]/[url:] hint says how to read the value written after it;
           only a var() reference there names a custom property. *)
        | _, Some ("image", v) ->
            if is_mask_image_value v then Ok (Bracket_image_var v)
            else opaque class_name Longhand.Image inner
        | _, Some ("url", v) ->
            if is_mask_image_value v then Ok (Bracket_url_var v)
            else opaque class_name Longhand.Image inner
        (* Before the [url(...)] reading below, which takes one whole token and
           so has no answer for the comma of a layer list. *)
        | _ when is_image_value inner -> Ok (Bracket_image inner)
        | _ when String.starts_with ~prefix:"url(" inner -> (
            match Parse.url_token (Parse.decode_arbitrary_value inner) with
            | Some _ -> Ok (Bracket_url inner)
            | None -> opaque class_name Longhand.Image inner)
        | _ when Parse.is_var inner -> Ok (Bracket_var inner)
        (* A hint Tailwind does not know here settles the longhand all the same:
           it takes the last resort, and nothing is read from the value that
           follows it. The name is a run of [a-z] and [-], so [10px:2em] and
           [FOO:2em] carry no hint and are values whole. *)
        | _ when Parse.data_type_hint inner <> None ->
            opaque class_name Longhand.Image inner
        | _ -> (
            match parse_bracket_position inner with
            | Some positions -> Ok (Bracket_position (inner, positions))
            | None ->
                let longhand =
                  if holds_math_function inner then Longhand.Position
                  else Longhand.Image
                in
                opaque class_name longhand inner))
    | _ -> Error (`Msg "Not a mask utility")

  let to_class = function
    | No_mask -> "mask-none"
    | Add -> "mask-add"
    | Exclude -> "mask-exclude"
    | Intersect -> "mask-intersect"
    | Subtract -> "mask-subtract"
    | Alpha -> "mask-alpha"
    | Luminance -> "mask-luminance"
    | Match -> "mask-match"
    | Type_alpha -> "mask-type-alpha"
    | Type_luminance -> "mask-type-luminance"
    | Auto -> "mask-auto"
    | Contain -> "mask-contain"
    | Cover -> "mask-cover"
    | Position Keyword.Bottom -> "mask-bottom"
    | Position Keyword.Bottom_left -> "mask-bottom-left"
    | Position Keyword.Bottom_right -> "mask-bottom-right"
    | Position Keyword.Center -> "mask-center"
    | Position Keyword.Left -> "mask-left"
    | Position Keyword.Right -> "mask-right"
    | Position Keyword.Top -> "mask-top"
    | Position Keyword.Top_left -> "mask-top-left"
    | Position Keyword.Top_right -> "mask-top-right"
    | No_repeat -> "mask-no-repeat"
    | Repeat -> "mask-repeat"
    | Repeat_round -> "mask-repeat-round"
    | Repeat_space -> "mask-repeat-space"
    | Repeat_x -> "mask-repeat-x"
    | Repeat_y -> "mask-repeat-y"
    | Clip_border -> "mask-clip-border"
    | Clip_padding -> "mask-clip-padding"
    | Clip_content -> "mask-clip-content"
    | Clip_fill -> "mask-clip-fill"
    | Clip_stroke -> "mask-clip-stroke"
    | Clip_view -> "mask-clip-view"
    | No_clip -> "mask-no-clip"
    | Origin_border -> "mask-origin-border"
    | Origin_padding -> "mask-origin-padding"
    | Origin_content -> "mask-origin-content"
    | Origin_fill -> "mask-origin-fill"
    | Origin_stroke -> "mask-origin-stroke"
    | Origin_view -> "mask-origin-view"
    | Bracket_contain -> "mask-[contain]"
    | Bracket_cover -> "mask-[cover]"
    | Bracket_size v -> "mask-[size:" ^ v ^ "]"
    | Bracket_length v -> "mask-[length:" ^ v ^ "]"
    | Bracket_position (v, _) -> "mask-[" ^ v ^ "]"
    | Bracket_typed_position (v, _) -> "mask-[position:" ^ v ^ "]"
    | Bracket_image_var v -> "mask-[image:" ^ v ^ "]"
    | Bracket_url v -> "mask-[" ^ v ^ "]"
    | Bracket_url_var v -> "mask-[url:" ^ v ^ "]"
    | Bracket_var v -> "mask-[" ^ v ^ "]"
    | Bracket_image v -> "mask-[" ^ v ^ "]"
    | Position_bracket (v, _) -> "mask-position-[" ^ v ^ "]"
    | Position_bracket_var v -> "mask-position-[" ^ v ^ "]"
    | Size_bracket v -> "mask-size-[" ^ v ^ "]"
    | Size_bracket_var v -> "mask-size-[" ^ v ^ "]"
    | Bracket_opaque { spelling; _ } -> spelling

  let examples =
    [ No_mask; Clip_border; Origin_border; Repeat; Type_alpha; Add; Alpha ]
end

open Handler
module Utility_factory = Utility.Make (Handler)

let utility = Utility_factory.v
let mask_none = utility No_mask
let mask_add = utility Add
let mask_exclude = utility Exclude
let mask_intersect = utility Intersect
let mask_subtract = utility Subtract
let mask_alpha = utility Alpha
let mask_luminance = utility Luminance
let mask_match = utility Match
let mask_type_alpha = utility Type_alpha
let mask_type_luminance = utility Type_luminance
let mask_auto = utility Auto
let mask_contain = utility Contain
let mask_cover = utility Cover
let mask_bottom = utility (Position Keyword.Bottom)
let mask_bottom_left = utility (Position Keyword.Bottom_left)
let mask_bottom_right = utility (Position Keyword.Bottom_right)
let mask_center = utility (Position Keyword.Center)
let mask_left = utility (Position Keyword.Left)
let mask_right = utility (Position Keyword.Right)
let mask_top = utility (Position Keyword.Top)
let mask_top_left = utility (Position Keyword.Top_left)
let mask_top_right = utility (Position Keyword.Top_right)
let mask_no_repeat = utility No_repeat
let mask_repeat = utility Repeat
let mask_repeat_round = utility Repeat_round
let mask_repeat_space = utility Repeat_space
let mask_repeat_x = utility Repeat_x
let mask_repeat_y = utility Repeat_y
let mask_clip_border = utility Clip_border
let mask_clip_padding = utility Clip_padding
let mask_clip_content = utility Clip_content
let mask_clip_fill = utility Clip_fill
let mask_clip_stroke = utility Clip_stroke
let mask_clip_view = utility Clip_view
let mask_no_clip = utility No_clip
let mask_origin_border = utility Origin_border
let mask_origin_padding = utility Origin_padding
let mask_origin_content = utility Origin_content
let mask_origin_fill = utility Origin_fill
let mask_origin_stroke = utility Origin_stroke
let mask_origin_view = utility Origin_view