Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 65 additions & 7 deletions bin/mustache_cli.ml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ let load_file f =
close_in ic;
(Bytes.to_string s)

let locate_template search_path filename =
if Filename.is_relative filename then
search_path
|> List.map (fun path -> Filename.concat path filename)
|> List.find_opt Sys.file_exists
else if Sys.file_exists filename then
Some filename
else None

let load_template template_filename =
let template_data = load_file template_filename in
let lexbuf = Lexing.from_string template_data in
Expand All @@ -27,14 +36,18 @@ let load_template template_filename =
let load_json json_filename =
Ezjsonm.from_string (load_file json_filename)

let run json_filename template_filename =
let run search_path json_filename template_filename =
let env = load_json json_filename in
let tmpl = load_template template_filename in
let partials name =
let path = Printf.sprintf "%s.mustache" name in
if not (Sys.file_exists path) then None
else Some (load_template path) in
try Mustache.render ~partials tmpl env |> print_endline
let file = Printf.sprintf "%s.mustache" name in
let path = locate_template search_path file in
Option.map load_template path
in
try
let output = Mustache.render ~partials tmpl env in
print_string output;
flush stdout
with Mustache.Render_error err ->
Format.eprintf "Template render error:@\n%a@."
Mustache.pp_render_error err;
Expand Down Expand Up @@ -64,6 +77,17 @@ let run_command =
`P "The $(i,ocaml-mustache) implementation is tested against
the Mustache specification testsuite.
All features are supported, except for lambdas and setting delimiter tags.";
`S Manpage.s_options;
`S "PARTIALS";
`P "The $(i,ocaml-mustache) library gives programmatic control over the meaning of partials {{>foo}}.
For the $(tname) tool, partials are interpreted as template file inclusion: '{{>foo}}' includes
the template file 'foo.mustache'.";
`P "Included files are resolved in a search path, which contains the current working directory
(unless the $(b,--no-working-dir) option is used)
and include directories passed through $(b,-I DIR) options.";
`P "If a file exists in several directories of the search path, the directory included first
(leftmost $(b,-I) option) has precedence, and the current working directory has precedence
over include directories.";
`S Manpage.s_examples;
`Pre
{|
Expand All @@ -83,6 +107,25 @@ Hello OCaml!
Mustache is:
- simple
- fun


\$ cat page.mustache
<html>
<body>
{{>hello}}
</body>
</html>

\$ $(tname) data.json page.mustache
<html>
<body>
Hello OCaml!
Mustache is:
- simple
- fun
</body>
</html>

|};
`S Manpage.s_bugs;
`P "Report bugs on https://github.com/rgrinberg/ocaml-mustache/issues";
Expand All @@ -96,10 +139,25 @@ Mustache is:
let doc = "mustache template" in
Arg.(required & pos 1 (some file) None & info [] ~docv:"TEMPLATE.mustache" ~doc)
in
Term.(const run $ json_file $ template_file),
let search_path =
let includes =
let doc = "Adds the directory $(docv) to the search path for partials." in
Arg.(value & opt_all dir [] & info ["I"] ~docv:"DIR" ~doc)
in
let no_working_dir =
let doc = "Disable the implicit inclusion of the working directory
in the search path for partials." in
Arg.(value & flag & info ["no-working-dir"] ~doc)
in
let search_path includes no_working_dir =
if no_working_dir then includes
else Filename.current_dir_name :: includes
in
Term.(const search_path $ includes $ no_working_dir)
in
Term.(const run $ search_path $ json_file $ template_file),
Term.info "mustache" ~doc ~man


let () =
let open Cmdliner in
Term.exit @@ Term.eval run_command
2 changes: 1 addition & 1 deletion bin/test/errors/parsing-errors.t
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Delimiter problems:
$ echo "{{>" > $PROBLEM
$ mustache foo.json $PROBLEM
Template parse error:
File "eof-before-partial.mustache", line 2, character 0: ident expected.
File "eof-before-partial.mustache", line 2, character 0: '}}' expected.
[3]

$ PROBLEM=eof-in-comment.mustache
Expand Down
61 changes: 59 additions & 2 deletions bin/test/partials.t/run.t
Original file line number Diff line number Diff line change
@@ -1,5 +1,62 @@
Simple test:

$ mustache data.json foo.mustache
Inside the include is "Foo Bar !"


Include in child or parent directory:
$ mkdir subdir
$ echo "Test from {{src}}" > subdir/test.mustache
$ echo "{{> subdir/test }}" > from_parent.mustache
$ echo '{ "src": "parent" }' > from_parent.json
$ mustache from_parent.json from_parent.mustache
Test from parent

$ mkdir subdir/child
$ echo "{{> ../test }}" > subdir/child/from_child.mustache
$ echo '{ "src": "child" }' > subdir/child/from_child.json
$ (cd subdir/child; mustache from_child.json from_child.mustache)
Test from child

When working with templates from outside the current directory,
we need to set the search path to locate their included partials.

This fails:
$ (cd subdir; mustache ../data.json ../foo.mustache)
Template render error:
File "../foo.mustache", line 2, characters 23-31:
the partial 'bar' is missing.
[2]

This works with the "-I .." option:
$ (cd subdir; mustache -I .. ../data.json ../foo.mustache)
Inside the include is "Foo Bar !"

Note that the include directory is *not* used to locate the template
(or data) argument. This fails:
$ (cd subdir; mustache -I .. ../data.json foo.mustache)
mustache: TEMPLATE.mustache argument: no `foo.mustache' file or directory
Usage: mustache [OPTION]... DATA.json TEMPLATE.mustache
Try `mustache --help' for more information.
[124]

Search path precedence order.
$ mkdir precedence
$ mkdir precedence/first
$ mkdir precedence/last
$ echo "First" > precedence/first/include.mustache
$ echo "Last" > precedence/last/include.mustache
$ echo "{{>include}}" > precedence/template.mustache
$ echo "{}" > precedence/data.json

The include directory added first (left -I option) has precedence
over the include directories added after:
$ (cd precedence; mustache -I first -I last data.json template.mustache)
First

The working directory has precedence over the include directories:
$ echo "Working" > precedence/include.mustache
$ (cd precedence; mustache -I first -I last data.json template.mustache)
Working

... unless --no-working-dir is used:
$ (cd precedence; mustache --no-working-dir -I first -I last data.json template.mustache)
First
24 changes: 23 additions & 1 deletion lib/mustache_lexer.mll
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,25 @@ let raw = [^ '{' '}' '\n']*
let id = ['a'-'z' 'A'-'Z' '-' '_' '/'] ['a'-'z' 'A'-'Z' '0'-'9' '-' '_' '/']*
let ident = ('.' | id ('.' id)*)

(* The grammar of partials is very relaxed compared to normal
identifiers: we want to allow dots anywhere to express relative
paths such as ../foo (this is consistent with other implementations
such as the 'mustache' binary provided by the Ruby implementation),
and in general we don't know what is going to be used, given that
partials are controlled programmatically.

We forbid spaces, to ensure that the behavior of trimming spaces
around the partial name is consistent with the other tag, and we
forbid newlines and mustaches to avoid simple delimiter mistakes
({{> foo } ... {{bar}}) being parsed as valid partial names.

(Note: if one wishes to interpret partials using lambdas placed
within the data (foo.bar interpreted as looking up 'foo' then 'bar'
in the input data and hoping to find a user-decided representation
of a function, it is of course possible to restrict the valid names
and split on dots on the user side.) *)
let partial_name = [^ ' ' '\t' '\n' '{' '}']*

rule space = parse
| blank newline { new_line lexbuf; space lexbuf }
| blank { () }
Expand All @@ -63,6 +82,9 @@ and ident = parse
| ident { lexeme lexbuf }
| "" { raise (Error "ident expected") }

and partial_name = parse
| partial_name { lexeme lexbuf }

and end_on expected = parse
| ("}}" | "}}}" | "") as lexed { check_mustaches ~expected ~lexed }

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