Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Page
Library
Module
Module type
Parameter
Class
Class type
Source
ppx.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
open Ppxlib open Ast_builder.Default module List = ListLabels let repo_url = "https://github.com/davesnx/html_of_jsx" let issues_url = "https://github.com/davesnx/html_of_jsx/issues" (* There's no pexp_list on Ppxlib since is not a constructor of the Parsetree *) let pexp_list ~loc xs = List.fold_left (List.rev xs) ~init:[%expr []] ~f:(fun xs x -> [%expr [%e x] :: [%e xs]]) exception Error of expression let raise_errorf ~loc fmt = let open Ast_builder.Default in Printf.ksprintf (fun msg -> let expr = pexp_extension ~loc (Location.error_extensionf ~loc "[html_of_jsx] %s" msg) in raise (Error expr)) fmt let collect_props visit args = let rec go props = function | [] -> (None, props) | [ (Nolabel, arg) ] -> (Some (visit arg), props) | (Nolabel, prop) :: _ -> let loc = prop.pexp_loc in raise_errorf ~loc "an argument without a label could only be the last one" | (proplab, prop) :: xs -> go ((proplab, visit prop) :: props) xs in go [] args let rec unwrap_children ~f children = function | { pexp_desc = Pexp_construct ({ txt = Lident "[]"; _ }, None); _ } -> List.rev children | { pexp_desc = Pexp_construct ( { txt = Lident "::"; _ }, Some { pexp_desc = Pexp_tuple [ child; next ]; _ } ); _; } -> unwrap_children ~f (f child :: children) next | e -> raise_errorf ~loc:e.pexp_loc "children prop should be a list" let is_jsx = function | { attr_name = { txt = "JSX"; _ }; _ } -> true | _ -> false let has_jsx_attr attrs = List.exists ~f:is_jsx attrs let rewrite_component ~loc tag args children = let component = pexp_ident ~loc tag in let props = match children with | None -> args | Some [ children ] -> (Labelled "children", children) :: args | Some children -> (Labelled "children", [%expr [%e pexp_list ~loc children]]) :: args in pexp_apply ~loc component props let validate_attr ~loc id name = match Ppx_html.findByName id name with | Ok p -> p | Error `ElementNotFound -> raise_errorf ~loc {|HTML tag '%s' doesn't exist. If this is not correct, please open an issue at %s|} id issues_url | Error `AttributeNotFound -> let suggestion = match Ppx_html.find_closest_name name with | Some suggestion -> Printf.sprintf "Hint: Maybe you mean '%s'?\n" suggestion | None -> "" in raise_errorf ~loc {|The attribute '%s' is not valid on a '%s' element. %s If this is not correct, please open an issue at %s.|} name id suggestion issues_url let add_attribute_type_constraint ~loc ~is_optional (type_ : Ppx_attributes.attributeType) value = match (type_, is_optional) with | String, true -> [%expr ([%e value] : string option)] | String, false -> [%expr ([%e value] : string)] | Int, false -> [%expr ([%e value] : int)] | Int, true -> [%expr ([%e value] : int option)] | Bool, false -> [%expr ([%e value] : bool)] | Bool, true -> [%expr ([%e value] : bool option)] | BooleanishString, false -> [%expr ([%e value] : bool)] | BooleanishString, true -> [%expr ([%e value] : bool option)] (* We treat `Style` as string *) | Style, false -> [%expr ([%e value] : string)] | Style, true -> [%expr ([%e value] : string option)] let make_attribute ~loc ~is_optional ~prop attribute_name attribute_value = let open Ppx_attributes in match (prop, is_optional) with | Rich_attribute { type_ = String; _ }, false | Attribute { type_ = String; _ }, false -> [%expr Some (JSX.Attribute.String ([%e attribute_name], [%e attribute_value]))] | Rich_attribute { type_ = String; _ }, true | Attribute { type_ = String; _ }, true -> [%expr Stdlib.Option.map (fun v -> JSX.Attribute.String ([%e attribute_name], v)) [%e attribute_value]] | Rich_attribute { type_ = Int; _ }, false | Attribute { type_ = Int; _ }, false -> [%expr Some (JSX.Attribute.String ([%e attribute_name], string_of_int [%e attribute_value]))] | Rich_attribute { type_ = Int; _ }, true | Attribute { type_ = Int; _ }, true -> [%expr Stdlib.Option.map (fun v -> JSX.Attribute.String ([%e attribute_name], string_of_int v)) [%e attribute_value]] | Rich_attribute { type_ = Bool; _ }, false | Attribute { type_ = Bool; _ }, false -> [%expr Some (JSX.Attribute.Bool ([%e attribute_name], [%e attribute_value]))] | Rich_attribute { type_ = Bool; _ }, true | Attribute { type_ = Bool; _ }, true -> [%expr Stdlib.Option.map (fun v -> JSX.Attribute.Bool ([%e attribute_name], v)) [%e attribute_value]] (* BooleanishString needs to transform bool into string *) | Rich_attribute { type_ = BooleanishString; _ }, false | Attribute { type_ = BooleanishString; _ }, false -> [%expr Some (JSX.Attribute.String ([%e attribute_name], string_of_bool [%e attribute_value]))] | Rich_attribute { type_ = BooleanishString; _ }, true | Attribute { type_ = BooleanishString; _ }, true -> [%expr Stdlib.Option.map (fun v -> JSX.Attribute.String ([%e attribute_name], v)) string_of_bool [%e attribute_value]] | Rich_attribute { type_ = Style; _ }, false | Attribute { type_ = Style; _ }, false -> [%expr Some (JSX.Attribute.Style [%e attribute_value])] | Rich_attribute { type_ = Style; _ }, true | Attribute { type_ = Style; _ }, true -> [%expr Stdlib.Option.map (fun v -> JSX.Attribute.Style v) [%e attribute_value]] | Event _, false -> [%expr Some (JSX.Attribute.Event ([%e attribute_name], [%e attribute_value]))] | Event _, true -> [%expr Stdlib.Option.map (fun v -> JSX.Attribute.Event ([%e attribute_name], v)) [%e attribute_value]] let is_optional = function Optional _ -> true | _ -> false let transform_labelled ~loc:_parentLoc ~tag_name props (prop_label, value) = let loc = props.pexp_loc in match prop_label with | Nolabel -> props | Optional name | Labelled name -> let is_optional = is_optional prop_label in let attribute = validate_attr ~loc tag_name name in let attribute_type = match attribute with | Rich_attribute { type_; _ } -> type_ | Attribute { type_; _ } -> type_ | Event _ -> String in let attribute_name = Ppx_html.getName attribute in let attribute_name_expr = estring ~loc attribute_name in let attribute_value = add_attribute_type_constraint ~loc ~is_optional attribute_type value in let attribute_final = make_attribute ~loc ~is_optional ~prop:attribute attribute_name_expr attribute_value in [%expr [%e attribute_final] :: [%e props]] let transform_attributes ~loc ~tag_name args = match args with | [] -> [%expr []] | attrs -> ( let list_of_attributes = attrs |> List.fold_left ~f:(transform_labelled ~loc ~tag_name) ~init:[%expr []] in match list_of_attributes with | [%expr []] -> [%expr []] | _ -> (* We need to filter attributes since optionals are represented as None *) [%expr List.filter_map Fun.id [%e list_of_attributes]]) let rewrite_node ~loc tag_name args children = let dom_node_name = estring ~loc tag_name in let attributes = transform_attributes ~loc ~tag_name args in match children with | Some children -> let childrens = pexp_list ~loc children in [%expr JSX.node [%e dom_node_name] [%e attributes] [%e childrens]] | None -> [%expr JSX.node [%e dom_node_name] [%e attributes] []] let split_args ~mapper args = let children = ref (Location.none, []) in let rest = List.filter_map args ~f:(function | Labelled "children", children_expression -> let children' = unwrap_children [] ~f:(fun e -> let expression = match e.pexp_desc with | Pexp_constant (Pconst_string _) -> let loc = e.pexp_loc in [%expr JSX.string [%e e]] | _ -> e in mapper expression) children_expression in children := (children_expression.pexp_loc, children'); None | arg_label, expression -> Some (arg_label, mapper expression)) in let children_prop = match !children with _, [] -> None | _loc, children -> Some children in (children_prop, rest) let reverse_pexp_list ~loc expr = let rec go acc = function | [%expr []] -> acc | [%expr [%e? hd] :: [%e? tl]] -> go [%expr [%e hd] :: [%e acc]] tl | expr -> expr in go [%expr []] expr let list_have_tail expr = match expr with | Pexp_construct ({ txt = Lident "::"; _ }, Some { pexp_desc = Pexp_tuple _; _ }) | Pexp_construct ({ txt = Lident "[]"; _ }, None) -> false | _ -> true let transform_items_of_list ~loc ~mapper children = let rec run_mapper children accum = match children with | [%expr []] -> reverse_pexp_list ~loc accum | [%expr [%e? v] :: [%e? acc]] when list_have_tail acc.pexp_desc -> [%expr [%e mapper#expression v]] | [%expr [%e? v] :: [%e? acc]] -> run_mapper acc [%expr [%e mapper#expression v] :: [%e accum]] | notAList -> mapper#expression notAList in run_mapper children [%expr []] let rewrite_jsx = object (self) inherit Ast_traverse.map as super method! expression expr = try match expr.pexp_desc with | Pexp_apply (({ pexp_desc = Pexp_ident _; _ } as tag), args) when has_jsx_attr expr.pexp_attributes -> ( let children, rest_of_args = split_args ~mapper:self#expression args in match tag.pexp_desc with (* div() [@JSX] *) | Pexp_ident { txt = Lident name; loc = name_loc } when Html.is_html_element name || Html.is_svg_element name -> rewrite_node ~loc:name_loc name rest_of_args children (* Reason adds `createElement` as default when an uppercase is found, we change it back to make *) (* Foo.createElement() [@JSX] *) | Pexp_ident { txt = Ldot (modulePath, ("createElement" | "make")); loc } -> let id = { loc; txt = Ldot (modulePath, "make") } in rewrite_component ~loc:tag.pexp_loc id rest_of_args children (* local_function() [@JSX] *) | Pexp_ident id -> rewrite_component ~loc:tag.pexp_loc id rest_of_args children | _ -> assert false) (* div() [@JSX] *) | Pexp_apply (_tag, _props) when has_jsx_attr expr.pexp_attributes -> raise_errorf ~loc:expr.pexp_loc "tag should be an identifier" (* <> </> is represented as a list in the Parsetree with [@JSX] *) | Pexp_construct ({ txt = Lident "::"; loc }, Some { pexp_desc = Pexp_tuple _; _ }) | Pexp_construct ({ txt = Lident "[]"; loc }, None) -> ( let jsx_attr, rest_attributes = List.partition ~f:is_jsx expr.pexp_attributes in match (jsx_attr, rest_attributes) with | [], _ -> super#expression expr | _, _rest_attributes -> let children = transform_items_of_list ~loc ~mapper:self expr in [%expr JSX.fragment [%e children]]) | _ -> super#expression expr with Error err -> [%expr [%e err]] end let () = let driver_args = [ ( "-htmx", Arg.Unit (fun _ -> Ppx_extra_attributes.set_htmx ()), "Enable htmx props" ); (* ( "-custom", Arg.String (fun file -> Static_attributes.extra_properties := Some file), "FILE Load inferred types from server cmo file FILE." ); *) ] in List.iter ~f:(fun (key, spec, doc) -> Driver.add_arg key spec ~doc) driver_args; Driver.register_transformation "html_of_jsx.ppx" ~preprocess_impl:rewrite_jsx#structure