diff --git a/bin/mustache_cli.ml b/bin/mustache_cli.ml index f193bfa..6df18f8 100644 --- a/bin/mustache_cli.ml +++ b/bin/mustache_cli.ml @@ -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 @@ -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; @@ -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 {| @@ -83,6 +107,25 @@ Hello OCaml! Mustache is: - simple - fun + + +\$ cat page.mustache + + + {{>hello}} + + + +\$ $(tname) data.json page.mustache + + + Hello OCaml! + Mustache is: + - simple + - fun + + + |}; `S Manpage.s_bugs; `P "Report bugs on https://github.com/rgrinberg/ocaml-mustache/issues"; @@ -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 diff --git a/bin/test/errors/parsing-errors.t b/bin/test/errors/parsing-errors.t index 4faf5f7..36c66f1 100644 --- a/bin/test/errors/parsing-errors.t +++ b/bin/test/errors/parsing-errors.t @@ -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 diff --git a/bin/test/partials.t/run.t b/bin/test/partials.t/run.t index 48d8b99..5c9aa7c 100644 --- a/bin/test/partials.t/run.t +++ b/bin/test/partials.t/run.t @@ -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 diff --git a/lib/mustache_lexer.mll b/lib/mustache_lexer.mll index 27fda7b..d32e462 100644 --- a/lib/mustache_lexer.mll +++ b/lib/mustache_lexer.mll @@ -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 { () } @@ -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 } @@ -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) }