Skip to content

Commit 22d034f

Browse files
committed
Add mustache templates.
1 parent f452fb5 commit 22d034f

File tree

5 files changed

+249
-0
lines changed

5 files changed

+249
-0
lines changed

.merlin

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ S lib
22
S syntax
33
S tools
44
S ppx
5+
S mustache
56

67
B _build/*
78

_oasis

+34
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ Flag ppx
2727
Description: Build the ppx syntax extension.
2828
Default: true
2929

30+
Flag mustache
31+
Description: Build the mustache syntax extension.
32+
Default: true
33+
3034
Library tyxml
3135
FindlibName: tyxml
3236
Path: implem
@@ -161,6 +165,36 @@ Executable ppx_reflect
161165
BuildDepends: ppx_tools.metaquot, tyxml.tools
162166
CompiledObject: best
163167

168+
## Template
169+
170+
Library ppx_mustache
171+
Build$: flag(ppx) && flag(mustache)
172+
Install$: flag(ppx) && flag(mustache)
173+
FindlibName: mustache
174+
FindlibParent: ppx
175+
InternalModules: Ppx_tyxml_empty
176+
Path: ppx
177+
XMETADescription:
178+
Well typed templates for HTML and SVG (ppx)
179+
XMETARequires: tyxml
180+
XMETAExtraLines: ppx = "ppx_tyxml_mustache"
181+
182+
Executable ppx_tyxml_mustache
183+
Build$: flag(ppx) && flag(mustache)
184+
Install$: flag(ppx) && flag(mustache)
185+
Path: mustache
186+
MainIs: ppx_tyxml_mustache.ml
187+
BuildDepends: tyxml.ppx.internal, mustache
188+
CompiledObject: best
189+
190+
Executable tymustache
191+
Build$: flag(ppx) && flag(mustache)
192+
Install$: flag(ppx) && flag(mustache)
193+
Path: mustache
194+
MainIs: tymustache.ml
195+
BuildDepends: tyxml.ppx.internal, mustache
196+
CompiledObject: best
197+
164198
## Tests
165199

166200
Executable emit_big

mustache/.merlin

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
PKG mustache
2+
REC

mustache/ppx_tyxml_mustache.ml

+211
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
2+
open Ast_helper
3+
module AC = Ast_convenience
4+
5+
let mustache_from_file file =
6+
let chan = open_in file in
7+
let lex = Lexing.from_channel chan in
8+
Location.init lex file ;
9+
let t = Mustache.parse_lx lex in
10+
close_in chan ;
11+
t
12+
13+
let mustache_from_string ~loc string =
14+
let lex = Lexing.from_string string in
15+
lex.Lexing.lex_curr_p <- loc ;
16+
Mustache.parse_lx lex
17+
18+
let antiquot_pcdata ~loc ~lang var =
19+
let pcdata = Ppx_common.make ~loc lang "pcdata" in
20+
AC.list [
21+
Exp.apply ~loc pcdata
22+
[Ppx_common.Label.nolabel, AC.evar var]
23+
]
24+
25+
module Var = struct
26+
27+
module Env = Map.Make(String)
28+
29+
type kind =
30+
| Var
31+
| Expr
32+
| Section of kind Env.t
33+
34+
let pp fmt = function
35+
| Var -> Format.pp_print_string fmt "variable"
36+
| Expr -> Format.pp_print_string fmt "unescaped variable"
37+
| Section _ -> Format.pp_print_string fmt "section"
38+
39+
let rec equal k k' = match k, k' with
40+
| Var, Var | Expr, Expr -> true
41+
| Section env, Section env' ->
42+
Env.equal equal env env'
43+
| _, _ -> false
44+
45+
let error s k k' =
46+
Location.error @@ Format.asprintf
47+
"Variable %s is used both as a %a and a %a. This is not allowed."
48+
s pp k' pp k
49+
50+
let add env s k =
51+
if Env.mem s env then
52+
let k' = Env.find s env in
53+
if equal k k then env
54+
else raise @@ Location.Error (error s k k')
55+
else
56+
Env.add s k env
57+
58+
let union =
59+
let f s parentkind kind = match parentkind, kind with
60+
| Some k, Some k' -> raise @@ Location.Error (error s k k')
61+
| Some k, None | None, Some k -> Some k
62+
| None, None -> None
63+
in Env.merge f
64+
65+
let section env s secenv =
66+
let k = Section secenv in
67+
add env s k
68+
69+
end
70+
71+
module Template = struct
72+
73+
type t = desc list
74+
and desc =
75+
| Markup of string
76+
| Pcdata of string
77+
| Expr of string
78+
| Section of section
79+
and section = {
80+
inverted : bool;
81+
name: string;
82+
contents: t;
83+
}
84+
85+
let rec of_mustache resolve =
86+
Mustache.fold
87+
~string:(fun x -> [Markup x])
88+
~section:
89+
(fun ~inverted name contents -> [Section { inverted ; name ; contents }])
90+
~escaped:(fun x -> [Pcdata x])
91+
~unescaped:(fun x -> [Expr x])
92+
~partial:
93+
(fun s -> of_mustache resolve @@ mustache_from_file @@ resolve s)
94+
~comment:(fun _ -> [])
95+
~concat:List.concat
96+
97+
let bindings ~env ~sec_env ~id =
98+
let f s b b' = match b, b' with
99+
| Some k, Some k' ->
100+
if Var.equal k k' then None
101+
else raise @@ Location.Error (Var.error s k k')
102+
| None, Some k' -> Some k'
103+
| _, None -> None
104+
in
105+
let env = Var.Env.merge f env sec_env in
106+
let make_binding k _ l =
107+
Vb.mk (AC.pvar k) (Exp.send id k) :: l
108+
in
109+
Exp.let_ Asttypes.Nonrecursive @@ Var.Env.fold make_binding env []
110+
111+
let rec desc_to_expr ~loc ~lang env t =
112+
Ast_helper.default_loc := loc ;
113+
match (t : desc) with
114+
| Markup s -> env, AC.str s
115+
| Pcdata s ->
116+
Var.add env s Var, antiquot_pcdata ~loc ~lang s
117+
| Expr s ->
118+
Var.add env s Expr, AC.evar s
119+
| Section { inverted ; name ; contents } ->
120+
let sec_env, e =
121+
to_expr ~simplify:false ~loc ~lang Var.Env.empty contents
122+
in
123+
let env = Var.section env name sec_env in
124+
let id = AC.evar name in
125+
let pid = AC.pvar name in
126+
if inverted then
127+
env, [%expr if [%e id] = [] then [] else [%e e]]
128+
else
129+
let e = bindings ~env ~sec_env ~id e in
130+
env, [%expr List.concat (List.map (fun [%p pid] -> [%e e]) [%e id])]
131+
132+
and to_expr ~simplify ~loc ~lang env l =
133+
let f (env, acc) t =
134+
let env, expr = desc_to_expr ~loc ~lang env t in
135+
env, expr::acc
136+
in
137+
let env, l = List.fold_left f (env, []) l in
138+
env, Ppx_tyxml.markup_to_expr ~simplify lang loc @@ List.rev l
139+
140+
let make_function env e =
141+
let f s _k e =
142+
Exp.fun_ (AC.Label.labelled s) None (AC.pvar s) e
143+
in
144+
Var.Env.fold f env e
145+
146+
end
147+
148+
let list_as_app = function
149+
| [] -> AC.unit ()
150+
| h :: t -> Exp.apply h (List.map (fun x -> AC.Label.nolabel, x) t)
151+
152+
let expr_of_mustache ~loc ~lang t =
153+
let env, e =
154+
Template.to_expr ~simplify:true ~loc ~lang Var.Env.empty @@
155+
Template.of_mustache (fun _s -> assert false) @@
156+
t
157+
in
158+
Template.make_function env e
159+
160+
let expr_of_string ~loc ~lang s =
161+
expr_of_mustache ~loc ~lang @@
162+
mustache_from_string ~loc:loc.loc_start s
163+
164+
165+
(** Mappers *)
166+
167+
open Parsetree
168+
169+
let error loc =
170+
Ppx_common.error loc "Invalid payload for [%%template]."
171+
172+
let extract_str loc str = match AC.get_str str with
173+
| None -> error loc
174+
| Some s -> s
175+
176+
let expr mapper e =
177+
let loc = e.pexp_loc in
178+
match e.pexp_desc with
179+
| Pexp_extension ({ txt = ("template" | "tyxml.template")}, payload) ->
180+
begin match payload with
181+
| PStr [[%stri let [%p? var] = [%e? str] in [%e? e]]] ->
182+
let s = extract_str loc str in
183+
Exp.let_ Asttypes.Nonrecursive
184+
[Vb.mk var @@ expr_of_string ~loc:str.pexp_loc ~lang:Html s]
185+
e
186+
187+
| PStr [{pstr_desc = Pstr_eval (str, _)}] ->
188+
let s = extract_str loc str in
189+
expr_of_string ~loc:str.pexp_loc ~lang:Html s
190+
191+
| _ -> error loc
192+
end
193+
| _ -> Ast_mapper.default_mapper.expr mapper e
194+
195+
let structure_item mapper stri =
196+
let loc = stri.pstr_loc in
197+
match stri.pstr_desc with
198+
| Pstr_extension (({ txt = ("template" | "tyxml.template")}, payload), _) ->
199+
begin match payload with
200+
| PStr [[%stri let [%p? var] = [%e? str]]] ->
201+
let s = extract_str loc str in
202+
Str.value Asttypes.Nonrecursive
203+
[Vb.mk var @@ expr_of_string ~loc:str.pexp_loc ~lang:Html s]
204+
| _ -> error loc
205+
end
206+
| _ -> Ast_mapper.default_mapper.structure_item mapper stri
207+
208+
let mapper _ =
209+
{ Ast_mapper. default_mapper with expr ; structure_item }
210+
211+
let () = Ast_mapper.register "tyxml.template" mapper

mustache/tymustache.ml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

0 commit comments

Comments
 (0)