package tyxml-jsx

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

Source file tyxml_jsx.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
open Ppxlib.Parsetree
open Ppxlib.Asttypes
open Tyxml_syntax

let is_jsx e =
  let f = function
    | { attr_name = {txt = "JSX"}} -> true
    | _ -> false
  in
  List.exists f e.pexp_attributes

let lowercase_lead s =
  String.mapi (fun i c -> if i = 0 then Char.lowercase_ascii c else c) s

let to_kebab_case =
  let open Re in
  let camelPat = Posix.compile_pat "[A-Z]" in
  let underscore = compile @@ char '_' in
  let prefixes = Perl.compile_pat {|^(data_?|aria_?)(.+)|} in
  fun name ->
    let kebab string =
      replace camelPat ~f:(fun g -> "-" ^ Group.get g 0) string
      |> String.lowercase_ascii
      |> replace_string underscore ~by:"-" in
    match exec_opt prefixes name with
    | None -> 
      if name.[0] == '_'
      (* need to keep the leading underscore, as that's what the syntax support keys
         off of to know to use Unsafe.string_attrib *)
      then "_" ^ kebab @@ String.sub name 1 (String.length name - 1)
      else name
    | Some g ->
      let prefix = String.sub name 0 4 in
      let suffix = kebab @@ Group.get g 2 in
      prefix ^ (if suffix.[0] == '-' then "" else "-") ^ suffix

let make_attr_name name =
  let name =
    match name with
    | "className" -> "class"
    | "htmlFor" -> "for"
    | "class_" -> "class"
    | "for_" -> "for"
    | "type_" -> "type"
    | "to_" -> "to"
    | "open_" -> "open"
    | "begin_" -> "begin"
    | "end_" -> "end"
    | "in_" -> "in"
    | "method_" -> "method"
    | name -> to_kebab_case name
  in
  name

open Common

let rec filter_map f = function
  | [] -> []
  | a :: q ->
  match f a with
  | None -> filter_map f q
  | Some a -> a :: filter_map f q

(** Children *)


let make_txt ~loc ~lang s =
  let txt = Common.make ~loc lang "txt" in
  let arg = Common.wrap lang loc @@ Common.string loc s in
  Ppxlib.Ast_helper.Exp.apply ~loc txt [Nolabel, arg]

let element_mapper transform_expr e =
  match e with
  (* Convert string constant into Html.txt "constant" for convenience *)
  | { pexp_desc = Pexp_constant (Pconst_string (str, loc, _)); _ } ->
    make_txt ~loc ~lang:Html str
  | _ ->
    transform_expr e

let extract_element_list transform_expr elements =
  let rec map acc e =
    match e with
    | [%expr []] -> List.rev acc
    | [%expr [%e? child] :: [%e? rest]] ->
      let child = Common.value (element_mapper transform_expr child) in
      map (child :: acc) rest
    | e ->
      List.rev (Common.antiquot (element_mapper transform_expr e) :: acc)
  in
  map [] elements

let extract_children transform_expr args =
  match
    List.find
      (function Labelled "children", _ -> true | _ -> false)
      args
  with
  | _, children -> extract_element_list transform_expr children
  | exception Not_found -> []

(** Attributes *)

type attr = {
  a_name: Common.name;
  a_value : string value;
  a_loc: Location.t;
}

let rec extract_attr_value ~lang a_name a_value =
  let a_name = make_attr_name a_name in
  match a_value with
  | { pexp_desc = Pexp_constant (Pconst_string (attr_value, _, _)); _ } ->
    ((lang, a_name), Common.value attr_value)
  | e ->
    ((lang, a_name), Common.antiquot e)

and extract_attr ~lang = function
  (* Ignore last unit argument as tyxml api is pure *)
  | Nolabel, [%expr ()] -> None
  | Labelled "children", _ -> None
  | Labelled name, value ->
    Some (extract_attr_value ~lang name value)
  | Nolabel, e ->
    error e.pexp_loc "Unexpected unlabeled jsx attribute"
  | Optional name, e ->
    error e.pexp_loc "Unexpected optional jsx attribute %s" name



let classify_name ~loc hint_lang lid =
  let annotated_lang, name = match lid with
    | Longident.Ldot (Ldot (Lident s, name), "createElement")
      when String.lowercase_ascii s = "html"
      -> Some Html, lowercase_lead name
    | Longident.Ldot (Lident s, name)
      when String.lowercase_ascii s = "html"
      -> Some Html, lowercase_lead name
    | Ldot (Ldot (Lident s, name), "createElement")
      when String.lowercase_ascii s = "svg"
      -> Some Svg, lowercase_lead name
    | Longident.Ldot (Lident s, name)
      when String.lowercase_ascii s = "svg"
      -> Some Svg, lowercase_lead name
    | Lident name ->
      hint_lang, name
    | _ ->
      Common.error loc "Invalid Tyxml tag %s"
        (String.concat "." (Longident.flatten_exn lid))
  in
  let parent_lang, elt =
    match Element.find_assembler (Html, name),
          Element.find_assembler (Svg, name),
          annotated_lang
    with
    | _, Some ("svg", _), Some l -> l, (Svg, name)
    | _, Some ("svg", _), None -> Svg, (Svg, name)
    | Some _, None, _ -> Html, (Html, name)
    | None, Some _, _ -> Svg, (Svg, name)
    | Some _, Some _, Some lang -> lang, (lang, name)
    | Some _, Some _, None ->
      (* In case of doubt, use Html *)
      Html, (Html, name)
    | None, None, _ ->
      Common.error loc "Unknown namespace for the element %s" name
  in
  parent_lang, elt

let is_homemade_component lid = match lid with
  | Longident.Ldot (( Lident s | Ldot (_, s)), "createElement") ->
    String.lowercase_ascii s <> "svg"
    && String.lowercase_ascii s <> "Html"
    && let c = s.[0] in 'A' <= c && c <= 'Z'
  | _ -> false

let mk_component ~lang ~loc f attrs children =
  let children = match children with
    | [] -> []
    | l -> [Labelled "children",  Common.list_wrap_value lang loc l]
  in
  let mk_attr ((_ns, name), v) =
    Labelled name, match v with
    | Common.Val s -> Common.string loc s
    | Common.Antiquot e -> e
  in
  let attrs = List.map mk_attr attrs in
  let args = attrs @ children @ [Nolabel,[%expr ()]] in
  Ppxlib.Ast_helper.Exp.apply ~loc f args
  
let traverse = object(self)
  inherit [Common.lang option] Ppxlib.Ast_traverse.map_with_context as super

  val mutable enabled = true

  method! structure_item hint_lang stri = match stri.pstr_desc with
    | Pstr_attribute
        { attr_name = { txt = ("tyxml.jsx" | "tyxml.jsx.enable") as s } ;
          attr_payload ; attr_loc ;
        }
      ->
      begin match attr_payload with
        | PStr [%str true] -> enabled <- true
        | PStr [%str false] -> enabled <- false
        | _ ->
          Common.error
            attr_loc
            "Unexpected payload for %s. A boolean is expected." s
      end ;
      stri
    | _ -> super#structure_item hint_lang stri

  method! expression hint_lang e =
    if not (is_jsx e) || not enabled then super#expression hint_lang e
    else
      let loc = e.pexp_loc in
      match e with
      (* matches <> ... </>; *)
      | [%expr []]
      | [%expr [%e? _] :: [%e? _]] ->
        let l = extract_element_list (self#expression hint_lang) e in
        Common.list_wrap_value Common.Html loc l
      (* matches <Component foo={bar}> child1 child2 </div>; *)
      | {pexp_desc = Pexp_apply
             ({ pexp_desc = Pexp_ident { txt }; _ } as f_expr, args )}
        when is_homemade_component txt
        ->
        let lang = match hint_lang with
          | Some l -> l | None -> Common.Html
        in
        let attributes = filter_map (extract_attr ~lang) args in
        let children = extract_children (self#expression hint_lang) args in
        let e =
          mk_component ~loc ~lang f_expr attributes children
        in
        e
      (* matches <div foo={bar}> child1 child2 </div>; *)
      | {pexp_desc = Pexp_apply
             ({ pexp_desc = Pexp_ident { txt }; _ }, args )}
        ->
        let parent_lang, name = classify_name ~loc hint_lang txt in
        let lang = fst name in
        let attributes = filter_map (extract_attr ~lang) args in
        let children =
          extract_children (self#expression @@ Some lang) args
        in
        let e = Element.parse ~loc
            ~parent_lang
            ~name
            ~attributes
            children
        in
        e
      | _ -> super#expression hint_lang e

end

let () =
  Ppxlib.Driver.register_transformation
    ~impl:(traverse#structure None)
    "tyxml-jsx"
OCaml

Innovation. Community. Security.