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
260
261
262
263
264
265
266
267
268
269
open Ast_mapper
open Parsetree
open Asttypes

open Tyxml_syntax

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

(* When dropping support for 4.02, this module can simply be deleted. *)
module String = struct
  include String
  let lowercase_ascii = String.lowercase [@ocaml.warning "-3"]
end
module Char = struct
  include Char
  let lowercase_ascii = Char.lowercase [@ocaml.warning "-3"]
end

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

let to_kebab_case name =
  let length = String.length name in
  if length > 5 then
    let first = String.sub name 0 4 in
    match first with
    | "aria"
    | "data" ->
      first ^ "-" ^ lowercase_lead (String.sub name 4 (length - 4))
    | _ -> name
  else
    name

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
  Ast_helper.Exp.apply ~loc txt [Common.Label.nolabel, arg]

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

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

let extract_children mapper args =
  match
    List.find
      (function Labelled "children", _ -> true | _ -> false)
      args
  with
  | _, children -> extract_element_list mapper 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 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
  Ast_helper.Exp.apply ~loc f args
  
type config = {
  mutable lang : Common.lang option ;
  mutable enabled : bool ;
}

let expr_mapper c mapper e =
  if not (is_jsx e) || not c.enabled then default_mapper.expr mapper e
  else
    let loc = e.pexp_loc in
    match e with
    (* matches <> ... </>; *)
    | [%expr []]
    | [%expr [%e? _] :: [%e? _]] ->
      let l = extract_element_list mapper 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 c.lang with
        | Some l -> l | None -> Common.Html
      in
      let attributes = filter_map (extract_attr ~lang) args in
      let children = extract_children mapper 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 hint_lang = c.lang in
      let parent_lang, name = classify_name ~loc hint_lang txt in
      let lang = fst name in
      c.lang <- Some lang;
      let attributes = filter_map (extract_attr ~lang) args in
      let children = extract_children mapper args in
      let e = Element.parse ~loc
          ~parent_lang
          ~name
          ~attributes
          children
      in
      c.lang <- hint_lang ;
      e
    | _ -> default_mapper.expr mapper e

let stri_mapper c mapper 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] -> c.enabled <- true
      | PStr [%str false] -> c.enabled <- false
      | _ ->
        Common.error
          attr_loc
          "Unexpected payload for %s. A boolean is expected." s
    end ;
    stri
  | _ -> default_mapper.structure_item mapper stri

let mapper _ _ =
  let c = { lang = None; enabled = true } in
  { default_mapper with
    expr = expr_mapper c ;
    structure_item = stri_mapper c ;
  }

let () =
  Driver.register
    ~name:"tyxml-jsx" Versions.ocaml_408
    mapper
OCaml

Innovation. Community. Security.