Skip to content

Commit 750f7f0

Browse files
authored
Merge pull request #60 from gasche/partials-include-in-cli
Partials as includes: support a proper search path
2 parents dc1115b + 9488f65 commit 750f7f0

File tree

4 files changed

+148
-11
lines changed

4 files changed

+148
-11
lines changed

bin/mustache_cli.ml

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ let load_file f =
1111
close_in ic;
1212
(Bytes.to_string s)
1313

14+
let locate_template search_path filename =
15+
if Filename.is_relative filename then
16+
search_path
17+
|> List.map (fun path -> Filename.concat path filename)
18+
|> List.find_opt Sys.file_exists
19+
else if Sys.file_exists filename then
20+
Some filename
21+
else None
22+
1423
let load_template template_filename =
1524
let template_data = load_file template_filename in
1625
let lexbuf = Lexing.from_string template_data in
@@ -27,14 +36,18 @@ let load_template template_filename =
2736
let load_json json_filename =
2837
Ezjsonm.from_string (load_file json_filename)
2938

30-
let run json_filename template_filename =
39+
let run search_path json_filename template_filename =
3140
let env = load_json json_filename in
3241
let tmpl = load_template template_filename in
3342
let partials name =
34-
let path = Printf.sprintf "%s.mustache" name in
35-
if not (Sys.file_exists path) then None
36-
else Some (load_template path) in
37-
try Mustache.render ~partials tmpl env |> print_endline
43+
let file = Printf.sprintf "%s.mustache" name in
44+
let path = locate_template search_path file in
45+
Option.map load_template path
46+
in
47+
try
48+
let output = Mustache.render ~partials tmpl env in
49+
print_string output;
50+
flush stdout
3851
with Mustache.Render_error err ->
3952
Format.eprintf "Template render error:@\n%a@."
4053
Mustache.pp_render_error err;
@@ -64,6 +77,17 @@ let run_command =
6477
`P "The $(i,ocaml-mustache) implementation is tested against
6578
the Mustache specification testsuite.
6679
All features are supported, except for lambdas and setting delimiter tags.";
80+
`S Manpage.s_options;
81+
`S "PARTIALS";
82+
`P "The $(i,ocaml-mustache) library gives programmatic control over the meaning of partials {{>foo}}.
83+
For the $(tname) tool, partials are interpreted as template file inclusion: '{{>foo}}' includes
84+
the template file 'foo.mustache'.";
85+
`P "Included files are resolved in a search path, which contains the current working directory
86+
(unless the $(b,--no-working-dir) option is used)
87+
and include directories passed through $(b,-I DIR) options.";
88+
`P "If a file exists in several directories of the search path, the directory included first
89+
(leftmost $(b,-I) option) has precedence, and the current working directory has precedence
90+
over include directories.";
6791
`S Manpage.s_examples;
6892
`Pre
6993
{|
@@ -83,6 +107,25 @@ Hello OCaml!
83107
Mustache is:
84108
- simple
85109
- fun
110+
111+
112+
\$ cat page.mustache
113+
<html>
114+
<body>
115+
{{>hello}}
116+
</body>
117+
</html>
118+
119+
\$ $(tname) data.json page.mustache
120+
<html>
121+
<body>
122+
Hello OCaml!
123+
Mustache is:
124+
- simple
125+
- fun
126+
</body>
127+
</html>
128+
86129
|};
87130
`S Manpage.s_bugs;
88131
`P "Report bugs on https://github.com/rgrinberg/ocaml-mustache/issues";
@@ -96,10 +139,25 @@ Mustache is:
96139
let doc = "mustache template" in
97140
Arg.(required & pos 1 (some file) None & info [] ~docv:"TEMPLATE.mustache" ~doc)
98141
in
99-
Term.(const run $ json_file $ template_file),
142+
let search_path =
143+
let includes =
144+
let doc = "Adds the directory $(docv) to the search path for partials." in
145+
Arg.(value & opt_all dir [] & info ["I"] ~docv:"DIR" ~doc)
146+
in
147+
let no_working_dir =
148+
let doc = "Disable the implicit inclusion of the working directory
149+
in the search path for partials." in
150+
Arg.(value & flag & info ["no-working-dir"] ~doc)
151+
in
152+
let search_path includes no_working_dir =
153+
if no_working_dir then includes
154+
else Filename.current_dir_name :: includes
155+
in
156+
Term.(const search_path $ includes $ no_working_dir)
157+
in
158+
Term.(const run $ search_path $ json_file $ template_file),
100159
Term.info "mustache" ~doc ~man
101160

102-
103161
let () =
104162
let open Cmdliner in
105163
Term.exit @@ Term.eval run_command

bin/test/errors/parsing-errors.t

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Delimiter problems:
6262
$ echo "{{>" > $PROBLEM
6363
$ mustache foo.json $PROBLEM
6464
Template parse error:
65-
File "eof-before-partial.mustache", line 2, character 0: ident expected.
65+
File "eof-before-partial.mustache", line 2, character 0: '}}' expected.
6666
[3]
6767
6868
$ PROBLEM=eof-in-comment.mustache

bin/test/partials.t/run.t

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,62 @@
11
Simple test:
2-
32
$ mustache data.json foo.mustache
43
Inside the include is "Foo Bar !"
5-
4+
5+
Include in child or parent directory:
6+
$ mkdir subdir
7+
$ echo "Test from {{src}}" > subdir/test.mustache
8+
$ echo "{{> subdir/test }}" > from_parent.mustache
9+
$ echo '{ "src": "parent" }' > from_parent.json
10+
$ mustache from_parent.json from_parent.mustache
11+
Test from parent
12+
13+
$ mkdir subdir/child
14+
$ echo "{{> ../test }}" > subdir/child/from_child.mustache
15+
$ echo '{ "src": "child" }' > subdir/child/from_child.json
16+
$ (cd subdir/child; mustache from_child.json from_child.mustache)
17+
Test from child
18+
19+
When working with templates from outside the current directory,
20+
we need to set the search path to locate their included partials.
21+
22+
This fails:
23+
$ (cd subdir; mustache ../data.json ../foo.mustache)
24+
Template render error:
25+
File "../foo.mustache", line 2, characters 23-31:
26+
the partial 'bar' is missing.
27+
[2]
28+
29+
This works with the "-I .." option:
30+
$ (cd subdir; mustache -I .. ../data.json ../foo.mustache)
31+
Inside the include is "Foo Bar !"
32+
33+
Note that the include directory is *not* used to locate the template
34+
(or data) argument. This fails:
35+
$ (cd subdir; mustache -I .. ../data.json foo.mustache)
36+
mustache: TEMPLATE.mustache argument: no `foo.mustache' file or directory
37+
Usage: mustache [OPTION]... DATA.json TEMPLATE.mustache
38+
Try `mustache --help' for more information.
39+
[124]
40+
41+
Search path precedence order.
42+
$ mkdir precedence
43+
$ mkdir precedence/first
44+
$ mkdir precedence/last
45+
$ echo "First" > precedence/first/include.mustache
46+
$ echo "Last" > precedence/last/include.mustache
47+
$ echo "{{>include}}" > precedence/template.mustache
48+
$ echo "{}" > precedence/data.json
49+
50+
The include directory added first (left -I option) has precedence
51+
over the include directories added after:
52+
$ (cd precedence; mustache -I first -I last data.json template.mustache)
53+
First
54+
55+
The working directory has precedence over the include directories:
56+
$ echo "Working" > precedence/include.mustache
57+
$ (cd precedence; mustache -I first -I last data.json template.mustache)
58+
Working
59+
60+
... unless --no-working-dir is used:
61+
$ (cd precedence; mustache --no-working-dir -I first -I last data.json template.mustache)
62+
First

lib/mustache_lexer.mll

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,25 @@ let raw = [^ '{' '}' '\n']*
5555
let id = ['a'-'z' 'A'-'Z' '-' '_' '/'] ['a'-'z' 'A'-'Z' '0'-'9' '-' '_' '/']*
5656
let ident = ('.' | id ('.' id)*)
5757

58+
(* The grammar of partials is very relaxed compared to normal
59+
identifiers: we want to allow dots anywhere to express relative
60+
paths such as ../foo (this is consistent with other implementations
61+
such as the 'mustache' binary provided by the Ruby implementation),
62+
and in general we don't know what is going to be used, given that
63+
partials are controlled programmatically.
64+
65+
We forbid spaces, to ensure that the behavior of trimming spaces
66+
around the partial name is consistent with the other tag, and we
67+
forbid newlines and mustaches to avoid simple delimiter mistakes
68+
({{> foo } ... {{bar}}) being parsed as valid partial names.
69+
70+
(Note: if one wishes to interpret partials using lambdas placed
71+
within the data (foo.bar interpreted as looking up 'foo' then 'bar'
72+
in the input data and hoping to find a user-decided representation
73+
of a function, it is of course possible to restrict the valid names
74+
and split on dots on the user side.) *)
75+
let partial_name = [^ ' ' '\t' '\n' '{' '}']*
76+
5877
rule space = parse
5978
| blank newline { new_line lexbuf; space lexbuf }
6079
| blank { () }
@@ -63,6 +82,9 @@ and ident = parse
6382
| ident { lexeme lexbuf }
6483
| "" { raise (Error "ident expected") }
6584

85+
and partial_name = parse
86+
| partial_name { lexeme lexbuf }
87+
6688
and end_on expected = parse
6789
| ("}}" | "}}}" | "") as lexed { check_mustaches ~expected ~lexed }
6890

@@ -80,7 +102,7 @@ and mustache = parse
80102
| "{{#" { OPEN_SECTION (lex_tag lexbuf space ident (end_on "}}") |> split_ident) }
81103
| "{{^" { OPEN_INVERTED_SECTION (lex_tag lexbuf space ident (end_on "}}") |> split_ident) }
82104
| "{{/" { CLOSE_SECTION (lex_tag lexbuf space ident (end_on "}}") |> split_ident) }
83-
| "{{>" { PARTIAL (0, lex_tag lexbuf space ident (end_on "}}")) }
105+
| "{{>" { PARTIAL (0, lex_tag lexbuf space partial_name (end_on "}}")) }
84106
| "{{!" { COMMENT (tok_arg lexbuf (comment [])) }
85107
| raw newline { new_line lexbuf; RAW (lexeme lexbuf) }
86108
| raw { RAW (lexeme lexbuf) }

0 commit comments

Comments
 (0)