package tw

  1. Overview
  2. Docs
Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source

Source file parse.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
(* [prefix_loop] is top-level so it captures nothing and allocates no closure -
   unlike [String.starts_with]'s inner [aux], which allocates one per call in a
   non-flambda build. Indices are kept in bounds by [has_prefix]. *)
let rec prefix_loop prefix s i lp =
  i = lp || (prefix.[i] = s.[i] && prefix_loop prefix s (i + 1) lp)

let has_prefix ~prefix s =
  let lp = String.length prefix in
  lp <= String.length s && prefix_loop prefix s 0 lp

let int_any s =
  match int_of_string_opt s with
  | Some n -> Ok n
  | None -> Error (`Msg ("Invalid number: " ^ s))

let nonnegative_int ~name s =
  match int_of_string_opt s with
  | Some n when n >= 0 -> Some (Ok n)
  | Some _ -> Some (Error (`Msg (name ^ " must be non-negative: " ^ s)))
  | None -> None

let int_pos ~name s =
  match nonnegative_int ~name s with
  | Some result -> result
  | None -> Error (`Msg ("Invalid " ^ name ^ " value: " ^ s))

(* Parse decimal values like "0.5", "1.5" for spacing utilities. Valid decimals
   must be multiples of 0.25 (i.e., value * 4 is integer). *)
let decimal_pos ~name s =
  try
    let f = Float.of_string s in
    if f < 0.0 then Error (`Msg (name ^ " must be non-negative: " ^ s))
    else
      let scaled = f *. 4.0 in
      if Float.is_integer scaled then Ok f
      else Error (`Msg ("Invalid " ^ name ^ " value: " ^ s))
  with Failure _ -> Error (`Msg ("Invalid " ^ name ^ " value: " ^ s))

(* Parse spacing values - handles both integers and decimals *)
let spacing_value ~name s =
  match nonnegative_int ~name s with
  | Some (Ok n) -> Ok (float_of_int n)
  | Some (Error _ as error) -> error
  | None -> decimal_pos ~name s

let int_bounded ~name ~min ~max s =
  match int_of_string_opt s with
  | Some n when n >= min && n <= max -> Ok n
  | Some _ ->
      Error
        (`Msg
           ("" ^ name ^ " must be between " ^ string_of_int min ^ " and "
          ^ string_of_int max ^ ": " ^ s))
  | None -> Error (`Msg ("Invalid " ^ name ^ " value: " ^ s))

(* Check if a value is a valid theme variable name. Theme variable names must
   not contain '/' — values with '/' that aren't handled as explicit fractions
   (like 3/4) are invalid class suffixes, not theme references. *)
let is_valid_theme_name s = s <> "" && not (String.contains s '/')
let ( >|= ) r f = Result.map f r

(** Extract the bare variable name from a "var(--name)" string. Returns "name"
    for "var(--name)", or the original string if not a var() reference. *)
let extract_var_name s =
  let len = String.length s in
  if len > 6 && String.sub s 0 6 = "var(--" && s.[len - 1] = ')' then
    String.trim (String.sub s 6 (len - 7))
  else s

(** Check if a string is a bracket-wrapped value like "[...]" *)
let is_bracket_value s =
  String.length s > 2 && s.[0] = '[' && s.[String.length s - 1] = ']'

(** Extract the inner content from a bracket value "[foo]" → "foo" *)
let bracket_inner s =
  if is_bracket_value s then String.sub s 1 (String.length s - 2) else s

let decode_underscores s = String.map (fun c -> if c = '_' then ' ' else c) s

let function_name_before s i =
  let is_name_char = function
    | 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '-' -> true
    | _ -> false
  in
  let j = ref (i - 1) in
  while !j >= 0 && s.[!j] = ' ' do
    decr j
  done;
  let stop = !j + 1 in
  while !j >= 0 && is_name_char s.[!j] do
    decr j
  done;
  if stop > !j + 1 then
    String.lowercase_ascii (String.sub s (!j + 1) (stop - !j - 1))
  else ""

let is_css_math_function = function
  | "abs" | "calc" | "calc-size" | "clamp" | "hypot" | "max" | "min" | "mod"
  | "rem" | "round" | "sign" ->
      true
  | _ -> false

let is_css_non_math_function = function
  | "attr" | "env" | "url" | "var" -> true
  | _ -> false

let normalize_css_math_operators s =
  let len = String.length s in
  let buf = Buffer.create (len + 8) in
  let contexts = ref [] in
  let current_math () =
    match !contexts with math :: _ -> math | [] -> false
  in
  let rec prev_non_space i =
    if i < 0 then None
    else if s.[i] = ' ' then prev_non_space (i - 1)
    else Some s.[i]
  in
  let rec next_non_space i =
    if i >= len then None
    else if s.[i] = ' ' then next_non_space (i + 1)
    else Some s.[i]
  in
  let value_end = function
    | '0' .. '9' | 'a' .. 'z' | 'A' .. 'Z' | '%' | ')' | ']' -> true
    | _ -> false
  in
  let value_start = function
    | '0' .. '9' | 'a' .. 'z' | 'A' .. 'Z' | '.' | '(' -> true
    | _ -> false
  in
  for i = 0 to len - 1 do
    match s.[i] with
    | '(' ->
        let fn = function_name_before s i in
        let ctx =
          if is_css_math_function fn then true
          else if is_css_non_math_function fn then false
          else current_math ()
        in
        contexts := ctx :: !contexts;
        Buffer.add_char buf '('
    | ')' ->
        (match !contexts with _ :: rest -> contexts := rest | [] -> ());
        Buffer.add_char buf ')'
    | ('+' | '-') as op
      when current_math ()
           && (match prev_non_space (i - 1) with
             | Some c -> value_end c
             | None -> false)
           &&
           match next_non_space (i + 1) with
           | Some c -> value_start c
           | None -> false ->
        let last =
          let n = Buffer.length buf in
          if n = 0 then None else Some (Buffer.nth buf (n - 1))
        in
        (match last with
        | Some ' ' | None -> ()
        | Some _ -> Buffer.add_char buf ' ');
        Buffer.add_char buf op;
        Buffer.add_char buf ' '
    | c -> Buffer.add_char buf c
  done;
  Buffer.contents buf

let decode_arbitrary_value s =
  s |> decode_underscores |> normalize_css_math_operators

(** Check if a string starts with "var(" — works on inner bracket content *)
let is_var s = String.length s > 4 && String.sub s 0 4 = "var("

(** Check if a bracket value contains a var() reference *)
let is_bracket_var s =
  if is_bracket_value s then is_var (bracket_inner s) else false

(** Check if a string looks like a CSS color function call (e.g., "rgba(...)",
    "hsl(...)", "oklch(...)"). Returns true for known CSS color function names
    followed by '('. *)
let is_css_color_fn s =
  let starts prefix =
    String.length s >= String.length prefix + 1
    && String.sub s 0 (String.length prefix) = prefix
    && s.[String.length prefix] = '('
  in
  starts "rgb" || starts "rgba" || starts "hsl" || starts "hsla" || starts "hwb"
  || starts "oklch" || starts "oklab" || starts "lch" || starts "lab"
  || starts "color" || starts "color-mix"

(** Check if a string is a bare var reference like "(--name)" *)
let is_bare_var s =
  String.length s > 4
  && s.[0] = '('
  && s.[String.length s - 1] = ')'
  && String.length s > 3
  && s.[1] = '-'
  && s.[2] = '-'

(** Extract the var name from a bare var "(--name)" → "--name" *)
let bare_var_inner s =
  if is_bare_var s then String.sub s 1 (String.length s - 2) else s

(** Split a class name on '-' but treat '[...]' as atomic. E.g.
    "m-[var(--value)]" → ["m"; "[var(--value)]"] E.g. "-m-[var(--value)]" →
    [""; "m"; "[var(--value)]"] *)
let split_class class_name =
  let len = String.length class_name in
  let buf = Buffer.create 16 in
  let parts = ref [] in
  let i = ref 0 in
  while !i < len do
    let c = class_name.[!i] in
    if c = '[' then (
      (* Read until matching ']', including nested brackets *)
      let depth = ref 1 in
      Buffer.add_char buf c;
      incr i;
      while !i < len && !depth > 0 do
        let c = class_name.[!i] in
        Buffer.add_char buf c;
        if c = '[' then incr depth else if c = ']' then decr depth;
        incr i
      done)
    else if c = '(' then (
      (* Read until matching ')', including nested parens *)
      let depth = ref 1 in
      Buffer.add_char buf c;
      incr i;
      while !i < len && !depth > 0 do
        let c = class_name.[!i] in
        Buffer.add_char buf c;
        if c = '(' then incr depth else if c = ')' then decr depth;
        incr i
      done)
    else if c = '-' then (
      parts := Buffer.contents buf :: !parts;
      Buffer.clear buf;
      incr i)
    else (
      Buffer.add_char buf c;
      incr i)
  done;
  parts := Buffer.contents buf :: !parts;
  List.rev !parts