package ocamlformat-mlx-lib

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

Source file jsx_helper.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
open Printf
open Asttypes
open Longident
open Parsetree
open Ast_helper

let make_loc (startpos, endpos) =
  {
    Location.loc_start = startpos;
    Location.loc_end = endpos;
    Location.loc_ghost = false;
  }

let mkloc = Location.mkloc
let mkexp ~loc d = Exp.mk ~loc:(make_loc loc) d

let mkjsxexp ~loc:loc' e =
  let e = mkexp ~loc:loc' e in
  let loc = make_loc loc' in
  let pexp_attributes = [ Attr.mk ~loc { txt = "JSX"; loc } (PStr []) ] in
  { e with pexp_attributes }

let rec equal_longindent a b =
  match a, b with
  | Longident.Lident a, Longident.Lident b -> String.equal a b
  | Ldot (pa, a), Ldot (pb, b) ->
      String.equal a.txt b.txt && equal_longindent pa.txt pb.txt
  | Lapply _, _ | _, Lapply _ -> assert false
  | _ -> false

let make_jsx_element ~raise ~loc:_ ~tag ~end_tag ~props ~children () =
  let () =
    match end_tag with
    | None -> ()
    | Some (end_tag, (_, end_loc_e)) ->
        let eq =
          match tag, end_tag with
          | (`Module, _, s), (`Module, _, e) -> equal_longindent s e
          | (`Value, _, s), (`Value, _, e) -> equal_longindent s e
          | _ -> false
        in
        if not eq then
          let _, (end_loc_s, _), _ = end_tag in
          let end_loc = end_loc_s, end_loc_e in
          let _, start_loc, tag = tag in
          let tag = Longident.flatten tag |> String.concat "." in
          raise
            Syntaxerr.(
              Error
                (Unclosed
                   ( make_loc start_loc,
                     sprintf "<%s>" tag,
                     make_loc end_loc,
                     sprintf "</%s>" tag )))
  in
  let tag =
    match tag with
    | `Value, loc, txt ->
        mkexp ~loc (Pexp_ident { loc = make_loc loc; txt })
    | `Module, loc, txt ->
        let txt = Longident.Ldot (mkloc txt (make_loc loc), mkloc "createElement" (make_loc loc)) in
        mkexp ~loc (Pexp_ident { loc = make_loc loc; txt })
  in
  let props =
    let prop_exp ~loc name =
      let id = mkloc (Lident name.txt) (make_loc loc) in
      mkexp ~loc (Pexp_ident id)
    in
    List.map
      (function
        | loc, `Prop_punned name -> Labelled {txt=name.txt;loc = make_loc loc}, prop_exp ~loc name
        | loc, `Prop_opt_punned name -> Optional {txt=name.txt;loc = make_loc loc}, prop_exp ~loc name
        | _loc, `Prop (name, expr) -> Labelled name, expr
        | _loc, `Prop_opt (name, expr) -> Optional name, expr)
      props
  in
  let unit =
    Exp.mk ~loc:Location.none
      (Pexp_construct ({ txt = Lident "()"; loc = Location.none }, None))
  in
  let props = (Labelled {txt="children"; loc=children.pexp_loc}, children) :: props in
  Pexp_apply (tag, (Nolabel, unit) :: props)

(** A [@JSX] application that can be printed with JSX syntax. *)
type element = {
  tag : string;
  tag_loc : Location.t;
  props : (arg_label * expression) list;
  children_loc : Location.t;
  children : expression list;
}

(** Classify a [@JSX] application. JSX syntax can only express applications
    of the exact shape produced by [make_jsx_element]: an identifier tag
    applied to one unlabelled [()] argument, one [~children] argument that
    is a list literal, and labelled or optional props. Hand-written [@JSX]
    applications may have any other shape (see
    ocaml-mlx/ocamlformat-mlx#12), in which case [None] is returned and the
    application must be printed as a regular application. *)
let classify_element ~attrs e0 args =
  let tag =
    let rec ident_of = function
      | Lident name -> Some name
      | Ldot (id, name) ->
        Option.map (fun path -> path ^ "." ^ name.txt) (ident_of id.txt)
      | Lapply _ -> None
    in
    match e0.pexp_desc with
    | Pexp_ident { txt = Lident name; loc } -> Some (name, loc)
    | Pexp_ident { txt = Ldot (id, name); loc = _ } ->
      Option.map
        (fun path ->
          let tag =
            (* [<Foo />] is parsed as [Foo.createElement]. *)
            match name.txt with
            | "createElement" -> path
            | name -> path ^ "." ^ name
          in
          (tag, e0.pexp_loc))
        (ident_of id.txt)
    | _ -> None
  in
  let units, children, props =
    List.fold_right
      (fun arg (units, children, props) ->
        match arg with
        | ( Nolabel,
            { pexp_desc = Pexp_construct ({ txt = Lident "()"; _ }, None);
              pexp_attributes = [];
              _ } ) ->
          (() :: units, children, props)
        | ( Labelled { txt = "children"; _ },
            { pexp_desc = Pexp_list es; pexp_attributes = []; pexp_loc; _ } )
          ->
          (units, (pexp_loc, es) :: children, props)
        | ( Labelled { txt = "children"; _ },
            { pexp_desc = Pexp_construct ({ txt = Lident "[]"; _ }, None);
              pexp_attributes = [];
              pexp_loc;
              _ } ) ->
          (units, (pexp_loc, []) :: children, props)
        | arg -> (units, children, arg :: props))
      args ([], [], [])
  in
  let is_prop = function
    | Labelled { txt = "children"; _ }, _ ->
      false (* punned or not a list literal *)
    | (Labelled _ | Optional _), _ -> true
    | Nolabel, _ -> false
  in
  match (attrs, tag, units, children) with
  | ( [ { attr_name = { txt = "JSX"; _ }; attr_payload = PStr []; _ } ],
      Some (tag, tag_loc),
      [ () ],
      [ (children_loc, children) ] )
    when List.for_all is_prop props ->
    Some { tag; tag_loc; props; children_loc; children }
  | _ -> None