├── .gitignore ├── .ocamlformat ├── .ocamlinit ├── .ocp-indent ├── .travis.yml ├── CHANGES.md ├── LICENSE.md ├── Makefile ├── README.md ├── dune-project ├── mustache-cli.opam ├── mustache-cli ├── dune ├── mustache_cli.ml └── test │ ├── dune │ ├── errors │ ├── dune │ ├── json-errors.t │ ├── parsing-errors.t │ ├── render-errors.t │ │ ├── invalid-dotted-name-1.json │ │ ├── invalid-dotted-name-1.mustache │ │ ├── invalid-dotted-name-2.json │ │ ├── invalid-dotted-name-2.mustache │ │ ├── missing-section.json │ │ ├── missing-section.mustache │ │ ├── missing-variable.json │ │ ├── missing-variable.mustache │ │ ├── non-scalar.json │ │ ├── non-scalar.mustache │ │ ├── reference.json │ │ ├── reference.mustache │ │ ├── run.t │ │ └── z-missing-partial.mustache │ └── sys-errors.t │ ├── inheritance.t │ ├── base.mustache │ ├── header.mustache │ ├── invalid-partial-usage.mustache │ ├── mypage.mustache │ ├── run.t │ ├── test-indent-less.mustache │ ├── test-indent-more.mustache │ └── test-indentation.mustache │ ├── manpage-examples.t │ ├── data.json │ ├── hello.mustache │ ├── new-post.json │ ├── page-layout.mustache │ ├── page.mustache │ ├── post.mustache │ └── run.t │ └── partials.t │ ├── bar.mustache │ ├── data.json │ ├── foo.mustache │ └── run.t ├── mustache.opam └── mustache ├── examples ├── dune └── mustache_example.ml ├── lib ├── dune ├── mustache.ml ├── mustache.mli ├── mustache_lexer.mll ├── mustache_parser.mly └── mustache_types.ml ├── lib_test ├── compat │ ├── dune │ ├── mustache_v200.ml │ ├── mustache_v200.mli │ ├── user_program.expected │ └── user_program.ml ├── dune ├── spec_mustache.ml └── test_mustache.ml ├── specs ├── VERSION ├── comments.json ├── comments.yml ├── inheritance.json ├── inheritance.yml ├── interpolation.json ├── interpolation.yml ├── inverted.json ├── inverted.yml ├── partials.json ├── partials.yml ├── sections.json └── sections.yml └── test ├── args.json └── templ1.html /.gitignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | TAGS 3 | *.install 4 | .merlin 5 | .*.swp 6 | -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- 1 | version=0.15.0 2 | break-separators=before 3 | dock-collection-brackets=false 4 | break-sequences=true 5 | doc-comments=before 6 | field-space=loose 7 | let-and=sparse 8 | sequence-style=terminator 9 | type-decl=sparse 10 | wrap-comments=true 11 | if-then-else=k-r 12 | let-and=sparse 13 | space-around-records 14 | space-around-lists 15 | space-around-arrays 16 | cases-exp-indent=2 17 | break-cases=all 18 | indicate-nested-or-patterns=unsafe-no 19 | parse-docstrings=true 20 | -------------------------------------------------------------------------------- /.ocamlinit: -------------------------------------------------------------------------------- 1 | #use "topfind";; 2 | #require "menhirLib";; 3 | #directory "_build/lib";; 4 | #load "mustache.cma";; -------------------------------------------------------------------------------- /.ocp-indent: -------------------------------------------------------------------------------- 1 | JaneStreet 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | sudo: false 3 | services: 4 | - docker 5 | install: wget https://raw.githubusercontent.com/ocaml/ocaml-travisci-skeleton/master/.travis-docker.sh 6 | script: bash -ex ./.travis-docker.sh 7 | env: 8 | global: 9 | - PACKAGE="mustache" 10 | - PINS="mustache:." 11 | matrix: 12 | - DISTRO="ubuntu-lts" OCAML_VERSION="4.08" 13 | - DISTRO="ubuntu-lts" OCAML_VERSION="4.09" 14 | - DISTRO="ubuntu-lts" OCAML_VERSION="4.10" 15 | - DISTRO="ubuntu-lts" OCAML_VERSION="4.11" 16 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ### 3.3.0 2 | * Rename the CLI tool to `mustache-ocaml`. It's now part of the new opam 3 | package `mustache-cli` (@psafont, #71) 4 | 5 | ### 3.2.0 6 | 7 | * Remove the AST without locations: now all functions build an AST with locations; 8 | in particular, parsing always provide located error messages. 9 | To ease backward-compatibility, the smart constructors still use the 10 | same interface, using dummy locations by default, with 11 | a With_locations module for users who wish to explicitly provide 12 | locations. 13 | (@gasche, #65) 14 | * Support for "template inheritance" (partials with parameters) 15 | `{{foo/bar}}" will include 21 | "foo/bar.mustache", relative to the current working directory. 22 | * Improve error messages (@gasche, #47, #51, #56) 23 | Note: the exceptions raised by Mustache have changed, this breaks 24 | compatibility for users that would catch and deconstruct existing 25 | exceptions. 26 | * Add `render_buf` to render templates directly to buffers (@gasche, #48) 27 | * When a lookup fails in the current context, lookup in parents contexts. 28 | This should fix errors when using "{{#foo}}" for a scalar variable 29 | 'foo' to check that the variable exists. 30 | (@gasche, #49) 31 | 32 | ### 3.1.0 33 | 34 | * Install `mustache` command line utility (@avsm, @anton-trunov) 35 | * Update opam metadata to 2.0 format (@avsm) 36 | * Port build to Dune (@avsm) 37 | * Fix ocamldoc syntax to be compatible with odoc (@avsm) 38 | * Test OCaml 4.06 and 4.07 as well (@avsm) 39 | 40 | ### 3.0.2 (08-05-2017) 41 | 42 | * Add .descr file to repository 43 | 44 | ### 3.0.1 (06-09-2017) 45 | 46 | * Switch to jbuilder (#30) 47 | 48 | ### 3.0.0 49 | 50 | * Proper handling of partials (#28) 51 | * Handle standalone elements (#27) 52 | * Support for dotted names (#26) 53 | * Switch parser to menhir (#24) 54 | * keep track of locations in AST (#21) 55 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Rudi Grinberg, Armaël Guéneau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | dune build 3 | 4 | test: 5 | dune runtest 6 | 7 | check: test 8 | 9 | clean: 10 | @dune clean 11 | 12 | doc: 13 | dune build @doc 14 | 15 | fmt: 16 | dune build @fmt --auto-promote 17 | 18 | .PHONY: check test all clean doc fmt 19 | 20 | .PHONY: release 21 | release: ## Release on Opam 22 | dune-release distrib --skip-build --skip-lint --skip-tests 23 | # See https://github.com/ocamllabs/dune-release/issues/206 24 | DUNE_RELEASE_DELEGATE=github-dune-release-delegate dune-release publish distrib --verbose 25 | dune-release opam pkg 26 | dune-release opam submit 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ocaml-mustache 2 | ============== 3 | 4 | mustache.js logic-less templates in OCaml 5 | 6 | 7 | Example usage 8 | ------------- 9 | 10 | ```ocaml 11 | let tmpl = 12 | try 13 | Mustache.of_string "Hello {{name}}\n\ 14 | Mustache is:\n\ 15 | {{#qualities}}\ 16 | * {{name}}\n\ 17 | {{/qualities}}" 18 | with Mustache.Parse_error err -> 19 | Format.eprintf "%a@." 20 | Mustache.pp_template_parse_error err; 21 | exit 3 22 | 23 | let json = 24 | `O [ "name", `String "OCaml" 25 | ; "qualities", `A [ `O ["name", `String "awesome"] 26 | ; `O ["name", `String "simple"] 27 | ; `O ["name", `String "fun"] 28 | ] 29 | ] 30 | 31 | let rendered = 32 | try Mustache.render tmpl json 33 | with Mustache.Render_error err -> 34 | Format.eprintf "%a@." 35 | Mustache.pp_render_error err; 36 | exit 2 37 | ``` 38 | 39 | Supported template language 40 | --------------------------- 41 | 42 | ocaml-mustache accepts the whole Mustache template language, except: 43 | - it does not support setting delimiter tags to something else than '{{' and '}}'. 44 | - it does not support lambdas inside the provided data 45 | 46 | It is automatically tested against the latest 47 | [mustache specification testsuite](https://github.com/mustache/spec/tree/v1.1.3). 48 | 49 | ocaml-mustache also supports template inheritance / partials with parameters, 50 | tested against the [semi-official specification](https://github.com/mustache/spec/pull/75). 51 | 52 | Todo/Wish List 53 | ----------- 54 | * Support for ropes 55 | 56 | 57 | http://mustache.github.io/ 58 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 2.8) 2 | (name mustache) 3 | (using menhir 2.0) 4 | (cram enable) 5 | 6 | (license MIT) 7 | (implicit_transitive_deps false) 8 | 9 | (maintainers "Rudi Grinberg ") 10 | (authors 11 | "Rudi Grinberg " 12 | "Armaël Guéneau " 13 | "Gabriel Scherer ") 14 | 15 | (source (github rgrinberg/ocaml-mustache)) 16 | 17 | 18 | 19 | (generate_opam_files true) 20 | 21 | (package 22 | (name mustache) 23 | (synopsis "Mustache logic-less templates in OCaml") 24 | (description " 25 | Read and write mustache templates, and render them. 26 | ") 27 | (depends 28 | (ounit2 :with-test) 29 | (ezjsonm :with-test) 30 | (menhir (>= 20180703)) 31 | (ocaml (>= 4.08)))) 32 | 33 | (package 34 | (name mustache-cli) 35 | (synopsis "CLI for Mustache logic-less templates") 36 | (description " 37 | Command line utility `mustache-ocaml` for driving logic-less templates. 38 | Read and write mustache templates, and render them by providing a json object. 39 | ") 40 | (depends 41 | (jsonm (>= 1.0.1)) 42 | (mustache (= :version)) 43 | (cmdliner (>= 1.1.0)) 44 | (ocaml (>= 4.08)))) 45 | -------------------------------------------------------------------------------- /mustache-cli.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "CLI for Mustache logic-less templates" 4 | description: """ 5 | 6 | Command line utility `mustache-ocaml` for driving logic-less templates. 7 | Read and write mustache templates, and render them by providing a json object. 8 | """ 9 | maintainer: ["Rudi Grinberg "] 10 | authors: [ 11 | "Rudi Grinberg " 12 | "Armaël Guéneau " 13 | "Gabriel Scherer " 14 | ] 15 | license: "MIT" 16 | homepage: "https://github.com/rgrinberg/ocaml-mustache" 17 | bug-reports: "https://github.com/rgrinberg/ocaml-mustache/issues" 18 | depends: [ 19 | "dune" {>= "2.8"} 20 | "jsonm" {>= "1.0.1"} 21 | "mustache" {= version} 22 | "cmdliner" {>= "1.1.0"} 23 | "ocaml" {>= "4.08"} 24 | "odoc" {with-doc} 25 | ] 26 | build: [ 27 | ["dune" "subst"] {dev} 28 | [ 29 | "dune" 30 | "build" 31 | "-p" 32 | name 33 | "-j" 34 | jobs 35 | "@install" 36 | "@runtest" {with-test} 37 | "@doc" {with-doc} 38 | ] 39 | ] 40 | dev-repo: "git+https://github.com/rgrinberg/ocaml-mustache.git" 41 | -------------------------------------------------------------------------------- /mustache-cli/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name mustache_cli) 3 | (public_name mustache-ocaml) 4 | (package mustache-cli) 5 | (libraries mustache jsonm cmdliner)) 6 | 7 | (rule 8 | (deps 9 | (:bin mustache_cli.exe)) 10 | (action 11 | (with-stdout-to 12 | mustache.1 13 | (run %{bin} --help=groff)))) 14 | 15 | (install 16 | (section man) 17 | (package mustache-cli) 18 | (files mustache.1)) 19 | -------------------------------------------------------------------------------- /mustache-cli/mustache_cli.ml: -------------------------------------------------------------------------------- 1 | let load_file f = 2 | let ic = open_in f in 3 | let n = in_channel_length ic in 4 | let s = Bytes.create n in 5 | really_input ic s 0 n; 6 | close_in ic; 7 | Bytes.to_string s 8 | 9 | let locate_template search_path filename = 10 | if Filename.is_relative filename then 11 | search_path 12 | |> List.map (fun path -> Filename.concat path filename) 13 | |> List.find_opt Sys.file_exists 14 | else if Sys.file_exists filename then 15 | Some filename 16 | else 17 | None 18 | 19 | let load_template template_filename = 20 | let template_data = load_file template_filename in 21 | let lexbuf = Lexing.from_string template_data in 22 | let () = 23 | let open Lexing in 24 | lexbuf.lex_curr_p <- 25 | { lexbuf.lex_curr_p with pos_fname = template_filename } 26 | in 27 | try Mustache.parse_lx lexbuf 28 | with Mustache.Parse_error err -> 29 | Format.eprintf "%a@." Mustache.pp_template_parse_error err; 30 | exit 3 31 | 32 | module Json = struct 33 | type error = 34 | | Jsonm of Jsonm.error 35 | | Incomplete_input 36 | | Parse_error of string 37 | 38 | type jsonm_loc = (int * int) * (int * int) 39 | 40 | exception Error of jsonm_loc * error 41 | 42 | let json_of_decoder (d : Jsonm.decoder) = 43 | (* inspired from the Jsonm documentation example *) 44 | let raise_err err = raise (Error (Jsonm.decoded_range d, err)) in 45 | let dec d = 46 | match Jsonm.decode d with 47 | | `Lexeme l -> l 48 | | `Error e -> raise_err (Jsonm e) 49 | | `End 50 | | `Await -> 51 | raise_err Incomplete_input 52 | in 53 | let wrap k = (k : Mustache.Json.value -> 'r :> Mustache.Json.t -> 'r) in 54 | let rec value v (k : Mustache.Json.value -> _) d = 55 | match v with 56 | | `Os -> obj [] (wrap k) d 57 | | `As -> arr [] (wrap k) d 58 | | (`Null | `Bool _ | `String _ | `Float _) as v -> k v d 59 | | _ -> raise_err (Parse_error "value fields expected") 60 | and arr vs (k : Mustache.Json.t -> _) d = 61 | match dec d with 62 | | `Ae -> k (`A (List.rev vs)) d 63 | | v -> value v (fun v -> arr (v :: vs) k) d 64 | and obj ms (k : Mustache.Json.t -> _) d = 65 | match dec d with 66 | | `Oe -> k (`O (List.rev ms)) d 67 | | `Name n -> value (dec d) (fun v -> obj ((n, v) :: ms) k) d 68 | | _ -> raise_err (Parse_error "object fields expected") 69 | in 70 | let t v (k : Mustache.Json.t -> _) d = 71 | match v with 72 | | `Os -> obj [] k d 73 | | `As -> arr [] k d 74 | | _ -> raise_err (Parse_error "Json.t expected") 75 | in 76 | t (dec d) (fun v _ -> v) d 77 | 78 | let pp_error ppf (fname, jsonm_loc, error) = 79 | let (start_line, start_col), (end_line, end_col) = jsonm_loc in 80 | let lexpos line col : Lexing.position = 81 | { pos_fname = fname; pos_lnum = line; pos_bol = 0; pos_cnum = col } 82 | in 83 | let loc : Mustache.loc = 84 | { loc_start = lexpos start_line start_col 85 | ; loc_end = lexpos end_line end_col 86 | } 87 | in 88 | Format.fprintf ppf "%a:@ %t" Mustache.pp_loc loc (fun ppf -> 89 | match error with 90 | | Jsonm e -> Jsonm.pp_error ppf e 91 | | Incomplete_input -> Format.fprintf ppf "Incomplete input." 92 | | Parse_error s -> Format.fprintf ppf "Parse error: %s." s) 93 | end 94 | 95 | let load_json json_filename = 96 | let input = load_file json_filename in 97 | let decoder = Jsonm.decoder (`String input) in 98 | try Json.json_of_decoder decoder 99 | with Json.Error (jsonm_loc, error) -> 100 | Format.eprintf "%a@." Json.pp_error (json_filename, jsonm_loc, error); 101 | exit 4 102 | 103 | let run search_path json_filename template_filename = 104 | let env = load_json json_filename in 105 | let tmpl = load_template template_filename in 106 | let partials name = 107 | let file = Printf.sprintf "%s.mustache" name in 108 | let path = locate_template search_path file in 109 | Option.map load_template path 110 | in 111 | try 112 | let output = Mustache.render ~partials tmpl env in 113 | print_string output; 114 | flush stdout 115 | with Mustache.Render_error err -> 116 | Format.eprintf "%a@." Mustache.pp_render_error err; 117 | exit 2 118 | 119 | let manpage = 120 | Cmdliner. 121 | [ `S Manpage.s_description 122 | ; `P 123 | "$(tname) is a command-line tool coming with the $(i,ocaml-mustache) \ 124 | library,\n\ 125 | \ an OCaml implementation of the Mustache template format.\n\ 126 | \ $(tname) takes a data file and a template file as command-line \ 127 | parameters;\n\ 128 | \ it renders the populated template on standard output." 129 | ; `P 130 | "Mustache is a simple and popular template format,\n\ 131 | \ with library implementations in many programming languages.\n\ 132 | \ It is named from its {{..}} delimiters." 133 | ; `I ("Mustache website", "https://mustache.github.io/") 134 | ; `I 135 | ( "Mustache templates documentation" 136 | , "https://mustache.github.io/mustache.5.html" ) 137 | ; `I 138 | ( "$(i,ocaml-mustache) website:" 139 | , "https://github.com/rgrinberg/ocaml-mustache" ) 140 | ; `S Manpage.s_options 141 | ; (* The content of this section is filled by Cmdliner; it is used here to 142 | enforce the placement of the non-standard sections below. *) 143 | `S "PARTIALS" 144 | ; `P 145 | "The $(i,ocaml-mustache) library gives programmatic control over the \ 146 | meaning of partials {{>foo}}.\n\ 147 | \ For the $(tname) tool, partials are interpreted as template \ 148 | file inclusion: '{{>foo}}' includes\n\ 149 | \ the template file 'foo.mustache'." 150 | ; `P 151 | "Included files are resolved in a search path, which contains the \ 152 | current working directory\n\ 153 | \ (unless the $(b,--no-working-dir) option is used)\n\ 154 | \ and include directories passed through $(b,-I DIR) options." 155 | ; `P 156 | "If a file exists in several directories of the search path, the \ 157 | directory included first\n\ 158 | \ (leftmost $(b,-I) option) has precedence, and the current \ 159 | working directory has precedence\n\ 160 | \ over include directories." 161 | ; `S "TEMPLATE INHERITANCE / PARTIALS WITH PARAMETERS" 162 | ; `P 163 | "$(i,ocaml-mustache) supports a common extension to the original \ 164 | Mustache specification,\n\ 165 | \ called 'template inheritance' or 'parent partials', or here \ 166 | 'partials with parameters'.\n\ 167 | \ In addition to usual partials '{{>foo}}', which include a \ 168 | partial template, one can use\n\ 169 | \ the syntax '{{ 207 | 208 | {{>hello}} 209 | 210 | 211 | 212 | \$ $(tname) data.json page.mustache 213 | 214 | 215 | Hello OCaml! 216 | Mustache is: 217 | - simple 218 | - fun 219 | 220 | 221 | 222 | 223 | ## Including a layount around a page; see $(b,PARTIALS WITH PARAMETERS). 224 | 225 | \$ cat new-post.json 226 | { 227 | "title": "New Post", 228 | "authors": "Foo and Bar", 229 | "date": "today", 230 | "content": "Shiny new content." 231 | } 232 | 233 | \$ cat post.mustache 234 | {{{{title}} 238 |

{{content}}

239 | {{/content}} 240 | {{/post-layout}} 241 | 242 | \$ cat post-layout.mustache 243 | 244 | 245 | {{\$page-title}}Default Title{{/page-title}} 246 | 247 | 248 | {{\$content}}{{/content}} 249 | 250 | 251 | 252 | \$ $(tname) new-post.json post.mustache 253 | 254 | 255 | Post: New Post 256 | 257 | 258 |

New Post

259 |

Shiny new content.

260 | 261 | |} 262 | ; `S "CONFORMING TO" 263 | ; `P 264 | "The $(i,ocaml-mustache) implementation is tested against\n\ 265 | \ the Mustache specification testsuite.\n\ 266 | \ All features are supported, except for lambdas and setting \ 267 | delimiter tags." 268 | ; `I ("Mustache specification testsuite", "https://github.com/mustache/spec") 269 | ; `I 270 | ( "Semi-official specification of PARTIALS WITH PARAMETERS" 271 | , "https://github.com/mustache/spec/pull/75" ) 272 | ; `S "REPORTING BUGS" 273 | ; `P "Report bugs on https://github.com/rgrinberg/ocaml-mustache/issues" 274 | ] 275 | 276 | let run_command = 277 | let open Cmdliner in 278 | let doc = "renders Mustache template from JSON data files" in 279 | let json_file = 280 | let doc = "data file in JSON format" in 281 | Arg.(required & pos 0 (some file) None & info [] ~docv:"DATA.json" ~doc) 282 | in 283 | let template_file = 284 | let doc = "mustache template" in 285 | Arg.( 286 | required & pos 1 (some file) None & info [] ~docv:"TEMPLATE.mustache" ~doc) 287 | in 288 | let search_path = 289 | let includes = 290 | let doc = "Adds the directory $(docv) to the search path for partials." in 291 | Arg.(value & opt_all dir [] & info [ "I" ] ~docv:"DIR" ~doc) 292 | in 293 | let no_working_dir = 294 | let doc = 295 | "Disable the implicit inclusion of the working directory\n\ 296 | \ in the search path for partials." 297 | in 298 | Arg.(value & flag & info [ "no-working-dir" ] ~doc) 299 | in 300 | let search_path includes no_working_dir = 301 | if no_working_dir then 302 | includes 303 | else 304 | Filename.current_dir_name :: includes 305 | in 306 | Term.(const search_path $ includes $ no_working_dir) 307 | in 308 | Cmd.v 309 | (Cmd.info "mustache" ~doc ~man:manpage) 310 | Term.(const run $ search_path $ json_file $ template_file) 311 | 312 | let () = exit @@ Cmdliner.Cmd.eval run_command 313 | -------------------------------------------------------------------------------- /mustache-cli/test/dune: -------------------------------------------------------------------------------- 1 | (cram 2 | (package mustache-cli) 3 | (deps %{bin:mustache-ocaml})) 4 | -------------------------------------------------------------------------------- /mustache-cli/test/errors/dune: -------------------------------------------------------------------------------- 1 | (cram 2 | (package mustache-cli) 3 | (deps %{bin:mustache-ocaml})) 4 | -------------------------------------------------------------------------------- /mustache-cli/test/errors/json-errors.t: -------------------------------------------------------------------------------- 1 | $ touch empty.json 2 | $ echo '{ "foo": "Foo"; "bar": "Bar" }' > invalid.json 3 | $ echo '{{foo}}' > foo.mustache 4 | 5 | Empty json file: 6 | $ mustache-ocaml empty.json foo.mustache 7 | File "empty.json", line 1, character 0: expected JSON text (JSON value) 8 | [4] 9 | 10 | Invalid json file: 11 | $ mustache-ocaml invalid.json foo.mustache 12 | File "invalid.json", line 1, characters 15-29: 13 | expected value separator or object end (',' or '}') 14 | [4] 15 | -------------------------------------------------------------------------------- /mustache-cli/test/errors/parsing-errors.t: -------------------------------------------------------------------------------- 1 | $ echo '{ "foo": "Foo"}' > foo.json 2 | 3 | Delimiter problems: 4 | $ PROBLEM=no-closing-mustache.mustache 5 | $ echo "{{foo" > $PROBLEM 6 | $ mustache-ocaml foo.json $PROBLEM 7 | File "no-closing-mustache.mustache", line 2, character 0: '}}' expected. 8 | [3] 9 | 10 | $ PROBLEM=one-closing-mustache.mustache 11 | $ echo "{{foo}" > $PROBLEM 12 | $ mustache-ocaml foo.json $PROBLEM 13 | File "one-closing-mustache.mustache", line 1, character 5: '}}' expected. 14 | [3] 15 | 16 | $ PROBLEM=eof-before-variable.mustache 17 | $ echo "{{" > $PROBLEM 18 | $ mustache-ocaml foo.json $PROBLEM 19 | File "eof-before-variable.mustache", line 2, character 0: ident expected. 20 | [3] 21 | 22 | $ PROBLEM=eof-before-section.mustache 23 | $ echo "{{#" > $PROBLEM 24 | $ mustache-ocaml foo.json $PROBLEM 25 | File "eof-before-section.mustache", line 2, character 0: ident expected. 26 | [3] 27 | 28 | $ PROBLEM=eof-before-section-end.mustache 29 | $ echo "{{#foo}} {{.}} {{/" > $PROBLEM 30 | $ mustache-ocaml foo.json $PROBLEM 31 | File "eof-before-section-end.mustache", line 2, character 0: '}}' expected. 32 | [3] 33 | 34 | $ PROBLEM=eof-before-inverted-section.mustache 35 | $ echo "{{^" > $PROBLEM 36 | $ mustache-ocaml foo.json $PROBLEM 37 | File "eof-before-inverted-section.mustache", line 2, character 0: 38 | ident expected. 39 | [3] 40 | 41 | $ PROBLEM=eof-before-unescape.mustache 42 | $ echo "{{{" > $PROBLEM 43 | $ mustache-ocaml foo.json $PROBLEM 44 | File "eof-before-unescape.mustache", line 2, character 0: ident expected. 45 | [3] 46 | 47 | $ PROBLEM=eof-before-unescape.mustache 48 | $ echo "{{&" > $PROBLEM 49 | $ mustache-ocaml foo.json $PROBLEM 50 | File "eof-before-unescape.mustache", line 2, character 0: ident expected. 51 | [3] 52 | 53 | $ PROBLEM=eof-before-partial.mustache 54 | $ echo "{{>" > $PROBLEM 55 | $ mustache-ocaml foo.json $PROBLEM 56 | File "eof-before-partial.mustache", line 2, character 0: '}}' expected. 57 | [3] 58 | 59 | $ PROBLEM=eof-in-comment.mustache 60 | $ echo "{{! non-terminated comment" > $PROBLEM 61 | $ mustache-ocaml foo.json $PROBLEM 62 | File "eof-in-comment.mustache", line 2, character 0: non-terminated comment. 63 | [3] 64 | 65 | 66 | Mismatches between opening and closing mustaches: 67 | 68 | $ PROBLEM=two-three.mustache 69 | $ echo "{{ foo }}}" > $PROBLEM 70 | $ mustache-ocaml foo.json $PROBLEM 71 | File "two-three.mustache", line 1, characters 7-10: '}}' expected. 72 | [3] 73 | 74 | $ PROBLEM=three-two.mustache 75 | $ echo "{{{ foo }}" > $PROBLEM 76 | $ mustache-ocaml foo.json $PROBLEM 77 | File "three-two.mustache", line 1, characters 8-10: '}}}' expected. 78 | [3] 79 | 80 | 81 | Mismatch between section-start and section-end: 82 | 83 | $ PROBLEM=foo-bar.mustache 84 | $ echo "{{#foo}} {{.}} {{/bar}}" > $PROBLEM 85 | $ mustache-ocaml foo.json $PROBLEM 86 | File "foo-bar.mustache", line 1, characters 0-23: 87 | Open/close tag mismatch: {{# foo }} is closed by {{/ bar }}. 88 | [3] 89 | 90 | $ PROBLEM=foo-not-closed.mustache 91 | $ echo "{{#foo}} {{.}} {{foo}}" > $PROBLEM 92 | $ mustache-ocaml foo.json $PROBLEM 93 | File "foo-not-closed.mustache", line 2, character 0: syntax error. 94 | [3] 95 | 96 | $ PROBLEM=wrong-nesting.mustache 97 | $ echo "{{#bar}} {{#foo}} {{.}} {{/bar}} {{/foo}}" > $PROBLEM 98 | $ mustache-ocaml foo.json $PROBLEM 99 | File "wrong-nesting.mustache", line 1, characters 9-32: 100 | Open/close tag mismatch: {{# foo }} is closed by {{/ bar }}. 101 | [3] 102 | 103 | $ PROBLEM=wrong-nesting-variable.mustache 104 | $ echo '{{#bar}} {{$foo}} {{.}} {{/bar}} {{/foo}}' > $PROBLEM 105 | $ mustache-ocaml foo.json $PROBLEM 106 | File "wrong-nesting-variable.mustache", line 1, characters 9-32: 107 | Open/close tag mismatch: {{$ foo }} is closed by {{/ bar }}. 108 | [3] 109 | 110 | $ PROBLEM=wrong-nesting-partial.mustache 111 | $ echo "{{#foo}} {{ $PROBLEM 112 | $ mustache-ocaml foo.json $PROBLEM 113 | File "wrong-nesting-partial.mustache", line 1, characters 9-30: 114 | Open/close tag mismatch: {{< foo-bar }} is closed by {{/ foo }}. 115 | [3] 116 | 117 | 118 | 119 | Weird cases that may confuse our lexer or parser: 120 | 121 | $ PROBLEM=weird-tag-name.mustache 122 | $ echo "{{.weird}} foo bar" > $PROBLEM 123 | $ mustache-ocaml foo.json $PROBLEM 124 | File "weird-tag-name.mustache", line 1, character 3: '}}' expected. 125 | [3] 126 | -------------------------------------------------------------------------------- /mustache-cli/test/errors/render-errors.t/invalid-dotted-name-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Some Title", 3 | "list": [ 4 | {"data": "foo"}, 5 | {"data": "bar"}, 6 | {"data": "baz"} 7 | ], 8 | "gro": { 9 | "first": "First", 10 | "second": "Second" 11 | }, 12 | "name": "Some Name" 13 | } 14 | -------------------------------------------------------------------------------- /mustache-cli/test/errors/render-errors.t/invalid-dotted-name-1.mustache: -------------------------------------------------------------------------------- 1 | Title: {{title}} 2 | 3 | List: 4 | {{#list}} 5 | - {{data}} 6 | {{/list}} 7 | 8 | Group: 9 | {{#group}} 10 | {{gro.first}} 11 | {{second}} 12 | {{/group}} 13 | 14 | {{#name}}The variable "name" has value "{{name}}".{{/name}} 15 | {{^name}}The variable "name" is not set. {{/name}} 16 | 17 | Last line. -------------------------------------------------------------------------------- /mustache-cli/test/errors/render-errors.t/invalid-dotted-name-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Some Title", 3 | "list": [ 4 | {"data": "foo"}, 5 | {"data": "bar"}, 6 | {"data": "baz"} 7 | ], 8 | "group": { 9 | "fir": "First", 10 | "second": "Second" 11 | }, 12 | "name": "Some Name" 13 | } 14 | -------------------------------------------------------------------------------- /mustache-cli/test/errors/render-errors.t/invalid-dotted-name-2.mustache: -------------------------------------------------------------------------------- 1 | Title: {{title}} 2 | 3 | List: 4 | {{#list}} 5 | - {{data}} 6 | {{/list}} 7 | 8 | Group: 9 | {{#group}} 10 | {{group.fir}} 11 | {{second}} 12 | {{/group}} 13 | 14 | {{#name}}The variable "name" has value "{{name}}".{{/name}} 15 | {{^name}}The variable "name" is not set. {{/name}} 16 | 17 | Last line. -------------------------------------------------------------------------------- /mustache-cli/test/errors/render-errors.t/missing-section.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Some Title", 3 | "list": [ 4 | {"data": "foo"}, 5 | {"data": "bar"}, 6 | {"data": "baz"} 7 | ], 8 | "grop": { 9 | "first": "First", 10 | "second": "Second" 11 | }, 12 | "name": "Some Name" 13 | } 14 | -------------------------------------------------------------------------------- /mustache-cli/test/errors/render-errors.t/missing-section.mustache: -------------------------------------------------------------------------------- 1 | Title: {{title}} 2 | 3 | List: 4 | {{#list}} 5 | - {{data}} 6 | {{/list}} 7 | 8 | Group: 9 | {{#group}} 10 | {{group.first}} 11 | {{second}} 12 | {{/group}} 13 | 14 | {{#na}}The variable "name" has value "{{name}}".{{/na}} 15 | {{^name}}The variable "name" is not set. {{/name}} 16 | 17 | Last line. -------------------------------------------------------------------------------- /mustache-cli/test/errors/render-errors.t/missing-variable.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Some Title", 3 | "list": [ 4 | {"data": "foo"}, 5 | {"datum": "bar"}, 6 | {"data": "baz"} 7 | ], 8 | "group": { 9 | "first": "First", 10 | "second": "Second" 11 | }, 12 | "name": "Some Name" 13 | } 14 | -------------------------------------------------------------------------------- /mustache-cli/test/errors/render-errors.t/missing-variable.mustache: -------------------------------------------------------------------------------- 1 | Title: {{title}} 2 | 3 | List: 4 | {{#list}} 5 | - {{data}} 6 | {{/list}} 7 | 8 | Group: 9 | {{#group}} 10 | {{group.first}} 11 | {{second}} 12 | {{/group}} 13 | 14 | {{#name}}The variable "name" has value "{{na}}".{{/name}} 15 | {{^name}}The variable "name" is not set. {{/name}} 16 | 17 | Last line. -------------------------------------------------------------------------------- /mustache-cli/test/errors/render-errors.t/non-scalar.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": { "text": "Some Title" }, 3 | "list": [ 4 | {"data": "foo"}, 5 | {"data": "bar"}, 6 | {"data": "baz"} 7 | ], 8 | "group": { 9 | "first": "First", 10 | "second": "Second" 11 | }, 12 | "name": "Some Name" 13 | } 14 | -------------------------------------------------------------------------------- /mustache-cli/test/errors/render-errors.t/non-scalar.mustache: -------------------------------------------------------------------------------- 1 | Title: {{title}} 2 | 3 | List: 4 | {{list}} 5 | - {{data}} 6 | 7 | Group: 8 | {{#group}} 9 | {{group.first}} 10 | {{second}} 11 | {{/group}} 12 | 13 | {{#name}}The variable "name" has value "{{name}}".{{/name}} 14 | {{^name}}The variable "name" is not set. {{/name}} 15 | 16 | Last line. -------------------------------------------------------------------------------- /mustache-cli/test/errors/render-errors.t/reference.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Some Title", 3 | "list": [ 4 | {"data": "foo"}, 5 | {"data": "bar"}, 6 | {"data": "baz"} 7 | ], 8 | "group": { 9 | "first": "First", 10 | "second": "Second" 11 | }, 12 | "name": "Some Name" 13 | } 14 | -------------------------------------------------------------------------------- /mustache-cli/test/errors/render-errors.t/reference.mustache: -------------------------------------------------------------------------------- 1 | Title: {{title}} 2 | 3 | List: 4 | {{#list}} 5 | - {{data}} 6 | {{/list}} 7 | 8 | Group: 9 | {{#group}} 10 | {{group.first}} 11 | {{second}} 12 | {{/group}} 13 | 14 | {{#name}}The variable "name" has value "{{name}}".{{/name}} 15 | {{^name}}The variable "name" is not set. {{/name}} 16 | 17 | Last line. -------------------------------------------------------------------------------- /mustache-cli/test/errors/render-errors.t/run.t: -------------------------------------------------------------------------------- 1 | reference.json and reference.mustache work well together, there is no error. 2 | $ mustache-ocaml reference.json reference.mustache 3 | Title: Some Title 4 | 5 | List: 6 | - foo 7 | - bar 8 | - baz 9 | 10 | Group: 11 | First 12 | Second 13 | 14 | The variable "name" has value "Some Name". 15 | 16 | 17 | Last line. 18 | 19 | 20 | Error cases. In many cases, there are two ways to get a given 21 | rendering error, one is by making a mistake in the template 22 | (compared to the reference version), the other is to make a mistake in 23 | the JSON file. We exercise both ways in the tests, to see if the 24 | context given by error messages makes it easy for users to understand 25 | one possible source of error, or both, or none. 26 | 27 | Invalid variable name: 28 | 29 | $ mustache-ocaml reference.json missing-variable.mustache 30 | File "missing-variable.mustache", line 14, characters 40-46: 31 | the variable 'na' is missing. 32 | [2] 33 | 34 | $ mustache-ocaml missing-variable.json reference.mustache 35 | File "reference.mustache", line 5, characters 4-12: 36 | the variable 'data' is missing. 37 | [2] 38 | 39 | Invalid section name: 40 | 41 | $ mustache-ocaml reference.json missing-section.mustache 42 | File "missing-section.mustache", line 14, characters 0-55: 43 | the section 'na' is missing. 44 | [2] 45 | 46 | $ mustache-ocaml missing-section.json reference.mustache 47 | File "reference.mustache", lines 9-12, characters 0-10: 48 | the section 'group' is missing. 49 | [2] 50 | 51 | Error in a dotted path foo.bar (one case for the first component, the other in the second). 52 | 53 | $ mustache-ocaml reference.json invalid-dotted-name-1.mustache 54 | File "invalid-dotted-name-1.mustache", line 10, characters 2-15: 55 | the variable 'gro' is missing. 56 | [2] 57 | 58 | $ mustache-ocaml invalid-dotted-name-1.json reference.mustache 59 | File "reference.mustache", lines 9-12, characters 0-10: 60 | the section 'group' is missing. 61 | [2] 62 | 63 | $ mustache-ocaml reference.json invalid-dotted-name-2.mustache 64 | File "invalid-dotted-name-2.mustache", line 10, characters 2-15: 65 | the variable 'group.fir' is missing. 66 | [2] 67 | 68 | $ mustache-ocaml invalid-dotted-name-2.json reference.mustache 69 | File "reference.mustache", line 10, characters 2-17: 70 | the variable 'group.first' is missing. 71 | [2] 72 | 73 | Non-scalar used as a scalar: 74 | 75 | $ mustache-ocaml reference.json non-scalar.mustache 76 | File "non-scalar.mustache", line 4, characters 0-8: 77 | the value of 'list' is not a valid scalar. 78 | [2] 79 | 80 | $ mustache-ocaml non-scalar.json reference.mustache 81 | File "reference.mustache", line 1, characters 7-16: 82 | the value of 'title' is not a valid scalar. 83 | [2] 84 | 85 | Missing partial (currently the CLI does not support any partial anyway): 86 | (this file has a z- prefix so that the files do come in pairs 87 | (this one does not) are all before in the alphabetic order, resulting 88 | in better `ls` output). 89 | 90 | $ mustache-ocaml reference.json z-missing-partial.mustache 91 | File "z-missing-partial.mustache", line 11, characters 2-13: 92 | the partial 'second' is missing. 93 | [2] 94 | -------------------------------------------------------------------------------- /mustache-cli/test/errors/render-errors.t/z-missing-partial.mustache: -------------------------------------------------------------------------------- 1 | Title: {{title}} 2 | 3 | List: 4 | {{#list}} 5 | - {{data}} 6 | {{/list}} 7 | 8 | Group: 9 | {{#group}} 10 | {{group.first}} 11 | {{>second}} 12 | {{/group}} 13 | 14 | {{#name}}The variable "name" has value "{{name}}".{{/name}} 15 | {{^name}}The variable "name" is not set. {{/name}} 16 | 17 | Last line. -------------------------------------------------------------------------------- /mustache-cli/test/errors/sys-errors.t: -------------------------------------------------------------------------------- 1 | $ echo "{}" > foo.json 2 | $ echo "" > foo.mustache 3 | 4 | Nonexistent json file: 5 | $ mustache-ocaml nonexistent.json foo.mustache 6 | mustache: DATA.json argument: no 'nonexistent.json' file or directory 7 | Usage: mustache [-I DIR] [--no-working-dir] [OPTION]… DATA.json TEMPLATE.mustache 8 | Try 'mustache --help' for more information. 9 | [124] 10 | 11 | Nonexistent template file: 12 | $ mustache-ocaml foo.json nonexistent.mustache 13 | mustache: TEMPLATE.mustache argument: no 'nonexistent.mustache' file or 14 | directory 15 | Usage: mustache [-I DIR] [--no-working-dir] [OPTION]… DATA.json TEMPLATE.mustache 16 | Try 'mustache --help' for more information. 17 | [124] 18 | -------------------------------------------------------------------------------- /mustache-cli/test/inheritance.t/base.mustache: -------------------------------------------------------------------------------- 1 | 2 | {{$header}}{{/header}} 3 | 4 | {{$content}}{{/content}} 5 | 6 | 7 | -------------------------------------------------------------------------------- /mustache-cli/test/inheritance.t/header.mustache: -------------------------------------------------------------------------------- 1 | 2 | {{$title}}Default title{{/title}} 3 | 4 | -------------------------------------------------------------------------------- /mustache-cli/test/inheritance.t/invalid-partial-usage.mustache: -------------------------------------------------------------------------------- 1 | {{Hello world 9 | {{/content}} 10 | {{/base}} 11 | -------------------------------------------------------------------------------- /mustache-cli/test/inheritance.t/run.t: -------------------------------------------------------------------------------- 1 | $ echo "{}" > data.json 2 | 3 | # Reference example 4 | 5 | This test is the reference example from the template-inheritance 6 | specification: https://github.com/mustache/spec/pull/75 7 | 8 | $ cat data.json # the json data 9 | {} 10 | 11 | $ cat mypage.mustache # the page the user would write 12 | {{Hello world 20 | {{/content}} 21 | {{/base}} 22 | 23 | $ cat base.mustache # the base layout 24 | 25 | {{$header}}{{/header}} 26 | 27 | {{$content}}{{/content}} 28 | 29 | 30 | 31 | $ cat header.mustache # this is slightly overkill... 32 | 33 | {{$title}}Default title{{/title}} 34 | 35 | 36 | $ mustache-ocaml data.json mypage.mustache 37 | 38 | 39 | My page title 40 | 41 | 42 |

Hello world

43 | 44 | 45 | 46 | 47 | # Indentation of parameter blocks 48 | 49 | $ cat test-indentation.mustache 50 |

51 | The test below should be indented in the same way as this line. 52 | {{$indented-block}}{{/indented-block}} 53 |

54 | 55 | $ cat test-indent-more.mustache 56 | {{ 65 | The test below should be indented in the same way as this line. 66 | This text is not indented in the source, 67 | it should be indented naturally in the output. 68 |

69 | 70 | $ cat test-indent-less.mustache 71 | {{ 80 | The test below should be indented in the same way as this line. 81 | This text is very indented in the source, 82 | it should be indented naturally in the output. 83 |

84 | 85 | 86 | # Errors on invalid partial block content 87 | 88 | Inside a partial block, we expect parameter blocks. The specification 89 | mandates that text be accepted and ignored, but we error on other tags. 90 | 91 | $ cat invalid-partial-usage.mustache 92 | {{ 2 | The test below should be indented in the same way as this line. 3 | {{$indented-block}}{{/indented-block}} 4 |

5 | -------------------------------------------------------------------------------- /mustache-cli/test/manpage-examples.t/data.json: -------------------------------------------------------------------------------- 1 | { "name": "OCaml", 2 | "qualities": [{"name": "simple"}, {"name": "fun"}] } 3 | -------------------------------------------------------------------------------- /mustache-cli/test/manpage-examples.t/hello.mustache: -------------------------------------------------------------------------------- 1 | Hello {{name}}! 2 | Mustache is: 3 | {{#qualities}} 4 | - {{name}} 5 | {{/qualities}} 6 | -------------------------------------------------------------------------------- /mustache-cli/test/manpage-examples.t/new-post.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "New Post", 3 | "content": "Shiny new content." 4 | } 5 | -------------------------------------------------------------------------------- /mustache-cli/test/manpage-examples.t/page-layout.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{$page-title}}Default Title{{/page-title}} 4 | 5 | 6 | {{$content}}{{/content}} 7 | 8 | 9 | -------------------------------------------------------------------------------- /mustache-cli/test/manpage-examples.t/page.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{>hello}} 4 | 5 | 6 | -------------------------------------------------------------------------------- /mustache-cli/test/manpage-examples.t/post.mustache: -------------------------------------------------------------------------------- 1 | {{{{title}} 5 |

{{content}}

6 | {{/content}} 7 | {{/page-layout}} 8 | -------------------------------------------------------------------------------- /mustache-cli/test/manpage-examples.t/run.t: -------------------------------------------------------------------------------- 1 | Simple usage: 2 | 3 | $ cat data.json 4 | { "name": "OCaml", 5 | "qualities": [{"name": "simple"}, {"name": "fun"}] } 6 | 7 | $ cat hello.mustache 8 | Hello {{name}}! 9 | Mustache is: 10 | {{#qualities}} 11 | - {{name}} 12 | {{/qualities}} 13 | 14 | $ mustache-ocaml data.json hello.mustache 15 | Hello OCaml! 16 | Mustache is: 17 | - simple 18 | - fun 19 | 20 | 21 | Using a partial to include a subpage: 22 | 23 | $ cat page.mustache 24 | 25 | 26 | {{>hello}} 27 | 28 | 29 | 30 | $ mustache-ocaml data.json page.mustache 31 | 32 | 33 | Hello OCaml! 34 | Mustache is: 35 | - simple 36 | - fun 37 | 38 | 39 | 40 | 41 | Using a partial with parameters to include a layout around a page: 42 | 43 | $ cat new-post.json 44 | { 45 | "title": "New Post", 46 | "content": "Shiny new content." 47 | } 48 | 49 | $ cat post.mustache 50 | {{{{title}} 54 |

{{content}}

55 | {{/content}} 56 | {{/page-layout}} 57 | 58 | $ cat page-layout.mustache 59 | 60 | 61 | {{$page-title}}Default Title{{/page-title}} 62 | 63 | 64 | {{$content}}{{/content}} 65 | 66 | 67 | 68 | $ mustache-ocaml new-post.json post.mustache 69 | 70 | 71 | Post: New Post 72 | 73 | 74 |

New Post

75 |

Shiny new content.

76 | 77 | 78 | -------------------------------------------------------------------------------- /mustache-cli/test/partials.t/bar.mustache: -------------------------------------------------------------------------------- 1 | {{foo}} {{bar}} ! -------------------------------------------------------------------------------- /mustache-cli/test/partials.t/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "Foo", 3 | "data": { "bar": "Bar" } 4 | } 5 | -------------------------------------------------------------------------------- /mustache-cli/test/partials.t/foo.mustache: -------------------------------------------------------------------------------- 1 | {{#data}} 2 | Inside the include is "{{>bar}}" 3 | {{/data}} 4 | -------------------------------------------------------------------------------- /mustache-cli/test/partials.t/run.t: -------------------------------------------------------------------------------- 1 | Simple test: 2 | $ mustache-ocaml data.json foo.mustache 3 | Inside the include is "Foo Bar !" 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-ocaml 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-ocaml 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-ocaml ../data.json ../foo.mustache) 24 | File "../foo.mustache", line 2, characters 23-31: 25 | the partial 'bar' is missing. 26 | [2] 27 | 28 | This works with the "-I .." option: 29 | $ (cd subdir; mustache-ocaml -I .. ../data.json ../foo.mustache) 30 | Inside the include is "Foo Bar !" 31 | 32 | Note that the include directory is *not* used to locate the template 33 | (or data) argument. This fails: 34 | $ (cd subdir; mustache-ocaml -I .. ../data.json foo.mustache) 35 | mustache: TEMPLATE.mustache argument: no 'foo.mustache' file or directory 36 | Usage: mustache [-I DIR] [--no-working-dir] [OPTION]… DATA.json TEMPLATE.mustache 37 | Try 'mustache --help' for more information. 38 | [124] 39 | 40 | Search path precedence order. 41 | $ mkdir precedence 42 | $ mkdir precedence/first 43 | $ mkdir precedence/last 44 | $ echo "First" > precedence/first/include.mustache 45 | $ echo "Last" > precedence/last/include.mustache 46 | $ echo "{{>include}}" > precedence/template.mustache 47 | $ echo "{}" > precedence/data.json 48 | 49 | The include directory added first (left -I option) has precedence 50 | over the include directories added after: 51 | $ (cd precedence; mustache-ocaml -I first -I last data.json template.mustache) 52 | First 53 | 54 | The working directory has precedence over the include directories: 55 | $ echo "Working" > precedence/include.mustache 56 | $ (cd precedence; mustache-ocaml -I first -I last data.json template.mustache) 57 | Working 58 | 59 | ... unless --no-working-dir is used: 60 | $ (cd precedence; mustache-ocaml --no-working-dir -I first -I last data.json template.mustache) 61 | First 62 | -------------------------------------------------------------------------------- /mustache.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "Mustache logic-less templates in OCaml" 4 | description: """ 5 | 6 | Read and write mustache templates, and render them. 7 | """ 8 | maintainer: ["Rudi Grinberg "] 9 | authors: [ 10 | "Rudi Grinberg " 11 | "Armaël Guéneau " 12 | "Gabriel Scherer " 13 | ] 14 | license: "MIT" 15 | homepage: "https://github.com/rgrinberg/ocaml-mustache" 16 | bug-reports: "https://github.com/rgrinberg/ocaml-mustache/issues" 17 | depends: [ 18 | "dune" {>= "2.8"} 19 | "ounit2" {with-test} 20 | "ezjsonm" {with-test} 21 | "menhir" {>= "20180703"} 22 | "ocaml" {>= "4.08"} 23 | "odoc" {with-doc} 24 | ] 25 | build: [ 26 | ["dune" "subst"] {dev} 27 | [ 28 | "dune" 29 | "build" 30 | "-p" 31 | name 32 | "-j" 33 | jobs 34 | "@install" 35 | "@runtest" {with-test} 36 | "@doc" {with-doc} 37 | ] 38 | ] 39 | dev-repo: "git+https://github.com/rgrinberg/ocaml-mustache.git" 40 | -------------------------------------------------------------------------------- /mustache/examples/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name mustache_example) 3 | (libraries mustache ezjsonm)) 4 | -------------------------------------------------------------------------------- /mustache/examples/mustache_example.ml: -------------------------------------------------------------------------------- 1 | let tmpl = 2 | Mustache.of_string 3 | "Hello {{name}}\nMustache is:\n{{#qualities}}* {{name}}\n{{/qualities}}" 4 | 5 | let json = 6 | `O 7 | [ ("name", `String "OCaml") 8 | ; ( "qualities" 9 | , `A 10 | [ `O [ ("name", `String "awesome") ] 11 | ; `O [ ("name", `String "simple") ] 12 | ; `O [ ("name", `String "fun") ] 13 | ] ) 14 | ] 15 | 16 | let rendered = Mustache.render tmpl json 17 | -------------------------------------------------------------------------------- /mustache/lib/dune: -------------------------------------------------------------------------------- 1 | (ocamllex mustache_lexer) 2 | 3 | (menhir 4 | (modules mustache_parser)) 5 | 6 | (library 7 | (name mustache) 8 | (public_name mustache) 9 | (synopsis "Mustache.js templates in OCaml") 10 | (libraries menhirLib) 11 | (wrapped false)) 12 | -------------------------------------------------------------------------------- /mustache/lib/mustache.ml: -------------------------------------------------------------------------------- 1 | (*{{{ The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Rudi Grinberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. }}}*) 22 | 23 | [@@@warning "-6"] 24 | 25 | include Mustache_types 26 | module List = ListLabels 27 | module String = StringLabels 28 | 29 | module Json = struct 30 | type value = 31 | [ `Null 32 | | `Bool of bool 33 | | `Float of float 34 | | `String of string 35 | | `A of value list 36 | | `O of (string * value) list 37 | ] 38 | 39 | type t = 40 | [ `A of value list 41 | | `O of (string * value) list 42 | ] 43 | 44 | let value : t -> value = fun t -> (t :> value) 45 | end 46 | 47 | let escape_html s = 48 | let b = Buffer.create (String.length s) in 49 | String.iter 50 | (function 51 | | '&' -> Buffer.add_string b "&" 52 | | '"' -> Buffer.add_string b """ 53 | | '\'' -> Buffer.add_string b "'" 54 | | '>' -> Buffer.add_string b ">" 55 | | '<' -> Buffer.add_string b "<" 56 | | c -> Buffer.add_char b c) 57 | s; 58 | Buffer.contents b 59 | 60 | (* Utility functions that allow converting between the ast with locations and 61 | without locations. *) 62 | 63 | let dummy_loc = { loc_start = Lexing.dummy_pos; loc_end = Lexing.dummy_pos } 64 | 65 | (* [erase_locs] and [dummy_locs] are not so useful now that we only have one AST 66 | (with locations). 67 | 68 | They are kept for backwards compatibility; they can also be useful to compare 69 | templates while ignoring location information. *) 70 | let erase_locs t = 71 | let open Ast in 72 | let rec erase_locs (t : t) : t = 73 | { loc = dummy_loc; desc = erase_locs_desc t.desc } 74 | and erase_locs_desc = function 75 | | String s -> String s 76 | | Escaped s -> Escaped s 77 | | Section s -> Section (erase_locs_section s) 78 | | Unescaped s -> Unescaped s 79 | | Partial p -> Partial (erase_locs_partial p) 80 | | Param pa -> Param (erase_locs_param pa) 81 | | Inverted_section s -> Inverted_section (erase_locs_section s) 82 | | Concat l -> Concat (List.map erase_locs l) 83 | | Comment s -> Comment s 84 | and erase_locs_section (s : section) : section = 85 | { name = s.name; contents = erase_locs s.contents } 86 | and erase_locs_partial (p : partial) : partial = 87 | { indent = p.indent 88 | ; name = p.name 89 | ; params = Option.map (List.map ~f:erase_locs_param) p.params 90 | ; contents = lazy (Option.map erase_locs (Lazy.force p.contents)) 91 | } 92 | and erase_locs_param (pa : param) : param = 93 | { indent = pa.indent; name = pa.name; contents = erase_locs pa.contents } 94 | in 95 | erase_locs t 96 | 97 | let add_dummy_locs t = erase_locs t 98 | 99 | (* Printing: defined on the ast without locations. *) 100 | 101 | let rec pp fmt m = 102 | let open Ast in 103 | match m.desc with 104 | | String s -> Format.pp_print_string fmt s 105 | | Escaped s -> Format.fprintf fmt "{{ %a }}" pp_dotted_name s 106 | | Unescaped s -> Format.fprintf fmt "{{& %a }}" pp_dotted_name s 107 | | Inverted_section s -> 108 | Format.fprintf fmt "{{^%a}}%a{{/%a}}" pp_dotted_name s.name pp s.contents 109 | pp_dotted_name s.name 110 | | Section s -> 111 | Format.fprintf fmt "{{#%a}}%a{{/%a}}" pp_dotted_name s.name pp s.contents 112 | pp_dotted_name s.name 113 | | Partial p -> ( 114 | match p.params with 115 | | None -> Format.fprintf fmt "{{> %s }}" p.name 116 | | Some params -> 117 | Format.fprintf fmt "{{< %s }}%a{{/ %s }}" p.name 118 | (Format.pp_print_list pp_param) 119 | params p.name ) 120 | | Param pa -> Format.fprintf fmt "%a" pp_param pa 121 | | Comment s -> Format.fprintf fmt "{{! %s }}" s 122 | | Concat s -> List.iter (pp fmt) s 123 | 124 | and pp_param fmt pa = 125 | Format.fprintf fmt "{{$%s}}%a{{/%s}}" pa.name pp pa.contents pa.name 126 | 127 | let to_string x = 128 | let b = Buffer.create 0 in 129 | let fmt = Format.formatter_of_buffer b in 130 | pp fmt x; 131 | Format.pp_print_flush fmt (); 132 | Buffer.contents b 133 | 134 | (* Parsing: produces an ast with locations. *) 135 | type template_parse_error = 136 | { loc : loc 137 | ; kind : template_parse_error_kind 138 | } 139 | 140 | and template_parse_error_kind = 141 | | Lexing of string 142 | | Parsing 143 | | Mismatched_names of name_mismatch_error 144 | | Invalid_as_partial_parameter of name * Ast.t 145 | 146 | exception Parse_error of template_parse_error 147 | 148 | let parse_lx (lexbuf : Lexing.lexbuf) : Ast.t = 149 | let loc_of lexbuf = 150 | let open Lexing in 151 | { loc_start = lexbuf.lex_start_p; loc_end = lexbuf.lex_curr_p } 152 | in 153 | let raise_err loc kind = raise (Parse_error { loc; kind }) in 154 | try 155 | MenhirLib.Convert.Simplified.traditional2revised Mustache_parser.mustache 156 | Mustache_lexer.(handle_standalone mustache lexbuf) 157 | with 158 | | Mustache_lexer.Error msg -> raise_err (loc_of lexbuf) (Lexing msg) 159 | | Mustache_parser.Error -> raise_err (loc_of lexbuf) Parsing 160 | | Mismatched_names (loc, { name_kind; start_name; end_name }) -> 161 | raise_err loc (Mismatched_names { name_kind; start_name; end_name }) 162 | | Invalid_as_partial_parameter (name, elt) -> 163 | raise_err elt.loc (Invalid_as_partial_parameter (name, elt)) 164 | 165 | let of_string s = parse_lx (Lexing.from_string s) 166 | 167 | let pp_loc ppf loc = 168 | let open Lexing in 169 | let fname = loc.loc_start.pos_fname in 170 | let is_dummy_pos pos = pos.pos_lnum < 0 || pos.pos_cnum < 0 in 171 | let extract pos = (pos.pos_lnum, pos.pos_cnum - pos.pos_bol) in 172 | let loc_start, loc_end = 173 | let loc_start = loc.loc_start in 174 | let loc_end = loc.loc_end in 175 | let orelse p1 p2 = 176 | if not (is_dummy_pos p1) then 177 | p1 178 | else 179 | p2 180 | in 181 | (orelse loc_start loc_end, orelse loc_end loc_start) 182 | in 183 | let p ppf = Format.fprintf ppf in 184 | if is_dummy_pos loc_start && is_dummy_pos loc_end then 185 | p ppf "(At unknown location)" 186 | else 187 | let start_line, start_col = extract loc.loc_start in 188 | let end_line, end_col = extract loc.loc_end in 189 | let pp_range ppf (start, end_) = 190 | if start = end_ then 191 | p ppf " %d" start 192 | else 193 | p ppf "s %d-%d" start end_ 194 | in 195 | p ppf "@["; 196 | if fname <> "" then 197 | p ppf "File %S,@ l" fname 198 | else 199 | p ppf "L"; 200 | p ppf "ine%a,@ character%a" pp_range (start_line, end_line) pp_range 201 | (start_col, end_col) 202 | 203 | let pp_template_parse_error ppf ({ loc; kind } : template_parse_error) = 204 | let p ppf = Format.fprintf ppf in 205 | p ppf "@[%a:@ " pp_loc loc; 206 | ( match kind with 207 | | Lexing msg -> p ppf "%s" msg 208 | | Parsing -> p ppf "syntax error" 209 | | Mismatched_names { name_kind; start_name; end_name } -> 210 | p ppf "Open/close tag mismatch: {{%c %s }} is closed by {{/ %s }}" 211 | ( match name_kind with 212 | | Section_name -> '#' 213 | | Inverted_section_name -> '^' 214 | | Partial_with_params_name -> '<' 215 | | Param_name -> '$' ) 216 | start_name end_name 217 | | Invalid_as_partial_parameter (name, _t) -> 218 | p ppf "Inside the partial block@ {{< %s }}...{{/ %s }},@ \ 219 | we expect parameter blocks@ {{$foo}...{{/foo}}@ \ 220 | but no other sorts of tags" 221 | name name 222 | ); 223 | p ppf ".@]" 224 | 225 | type render_error_kind = 226 | | Invalid_param of 227 | { name : dotted_name 228 | ; expected_form : string 229 | } 230 | | Missing_variable of { name : dotted_name } 231 | | Missing_section of { name : dotted_name } 232 | | Missing_partial of { name : name } 233 | 234 | type render_error = 235 | { loc : loc 236 | ; kind : render_error_kind 237 | } 238 | 239 | exception Render_error of render_error 240 | 241 | let pp_render_error ppf ({ loc; kind } : render_error) = 242 | let p ppf = Format.fprintf ppf in 243 | p ppf "@[%a:@ " pp_loc loc; 244 | ( match kind with 245 | | Invalid_param { name; expected_form } -> 246 | p ppf "the value of '%a' is not a valid %s" pp_dotted_name name 247 | expected_form 248 | | Missing_variable { name } -> 249 | p ppf "the variable '%a' is missing" pp_dotted_name name 250 | | Missing_section { name } -> 251 | p ppf "the section '%a' is missing" pp_dotted_name name 252 | | Missing_partial { name } -> p ppf "the partial '%s' is missing" name ); 253 | p ppf ".@]" 254 | 255 | let () = 256 | let pretty_print exn_name pp_error err = 257 | let buf = Buffer.create 42 in 258 | Format.fprintf 259 | (Format.formatter_of_buffer buf) 260 | "Mustache.%s:@\n%a@?" exn_name pp_error err; 261 | Buffer.contents buf 262 | in 263 | Printexc.register_printer (function 264 | | Parse_error err -> 265 | Some (pretty_print "Parse_error" pp_template_parse_error err) 266 | | Render_error err -> Some (pretty_print "Render_error" pp_render_error err) 267 | | _ -> None) 268 | 269 | (* Utility modules, that help looking up values in the json data during the 270 | rendering phase. *) 271 | 272 | module Contexts : sig 273 | type t 274 | 275 | val start : Json.value -> t 276 | 277 | val top : t -> Json.value 278 | 279 | val add : t -> Json.value -> t 280 | 281 | val find_name : t -> string -> Json.value option 282 | 283 | val add_param : t -> Ast.param -> t 284 | 285 | val find_param : t -> string -> Ast.param option 286 | end = struct 287 | type t = 288 | { (* nonempty stack of contexts, most recent first *) 289 | stack : Json.value * Json.value list 290 | ; (* an associative list of partial parameters that have been defined *) 291 | params : Ast.param list 292 | } 293 | 294 | let start js = { stack = (js, []); params = [] } 295 | 296 | let top { stack = js, _rest; _ } = js 297 | 298 | let add ctxs ctx = 299 | let top, rest = ctxs.stack in 300 | { ctxs with stack = (ctx, top :: rest) } 301 | 302 | let rec find_name ctxs name = 303 | let top, _ = ctxs.stack in 304 | match top with 305 | | `Null 306 | | `Bool _ 307 | | `Float _ 308 | | `String _ 309 | | `A _ -> 310 | find_in_rest ctxs name 311 | | `O dict -> ( 312 | match List.assoc name dict with 313 | | exception Not_found -> find_in_rest ctxs name 314 | | v -> Some v ) 315 | 316 | and find_in_rest ctxs name = 317 | let _, rest = ctxs.stack in 318 | match rest with 319 | | [] -> None 320 | | top :: rest -> find_name { ctxs with stack = (top, rest) } name 321 | 322 | let param_has_name name (p : Ast.param) = String.equal p.name name 323 | 324 | (* Note: the template-inheritance specification for Mustache 325 | (https://github.com/mustache/spec/pull/75) mandates that in case of 326 | multi-level inclusion, the "topmost" definition of the parameter wins. In 327 | other terms, when traversing the template during rendering, the value 328 | defined first for this parameter has precedence over later definitions. 329 | 330 | This is not a natural choice for our partial-with-arguments view, where we 331 | would expect the parameter binding closest to the use-site to win. This 332 | corresponds to an object-oriented view where applying a 333 | partial-with-parameters is seen as "inheriting" the parent/partial 334 | template, overriding a method for each parameter. Multi-level inclusions 335 | correspond to inheritance hierarchies (the parent template itself inherits 336 | from a grandparent), and then late-binding mandates that the definition 337 | "last" in the inheritance chain (so closest to the start of the rendering) 338 | wins.*) 339 | let add_param ctxs (param : Ast.param) = 340 | if List.exists (param_has_name param.name) ctxs.params then 341 | (* if the parameter is already bound, the existing binding has precedence *) 342 | ctxs 343 | else 344 | { ctxs with params = param :: ctxs.params } 345 | 346 | let find_param ctxs name = List.find_opt (param_has_name name) ctxs.params 347 | end 348 | 349 | let raise_err loc kind = raise (Render_error { loc; kind }) 350 | 351 | module Lookup = struct 352 | let scalar ?(strict = true) ~loc ~name = function 353 | | `Null -> 354 | if strict then 355 | "null" 356 | else 357 | "" 358 | | `Bool true -> "true" 359 | | `Bool false -> "false" 360 | | `Float f -> Printf.sprintf "%.12g" f 361 | | `String s -> s 362 | | `A _ 363 | | `O _ -> 364 | raise_err loc (Invalid_param { name; expected_form = "scalar" }) 365 | 366 | let simple_name ?(strict = true) ctxs ~loc n = 367 | match Contexts.find_name ctxs n with 368 | | None -> 369 | if strict then raise_err loc (Missing_variable { name = [ n ] }); 370 | None 371 | | Some _ as result -> result 372 | 373 | let dotted_name ?(strict = true) ctxs ~loc ~key = 374 | let rec lookup acc (js : Json.value) ~key = 375 | match key with 376 | | [] -> Some js 377 | | n :: ns -> ( 378 | match js with 379 | | `Null 380 | | `Float _ 381 | | `Bool _ 382 | | `String _ 383 | | `A _ -> 384 | raise_err loc 385 | (Invalid_param { name = List.rev acc; expected_form = "object" }) 386 | | `O dict -> ( 387 | match List.assoc n dict with 388 | | exception Not_found -> 389 | if strict then 390 | raise_err loc (Missing_variable { name = List.rev (n :: acc) }); 391 | None 392 | | js -> lookup (n :: acc) js ns ) ) 393 | in 394 | match key with 395 | | [] -> Some (Contexts.top ctxs) 396 | | n :: ns -> ( 397 | match simple_name ~strict ctxs ~loc n with 398 | | None -> None 399 | | Some js -> lookup [ n ] js ns ) 400 | 401 | let str ?(strict = true) ctxs ~loc ~key = 402 | match dotted_name ~strict ctxs ~loc ~key with 403 | | None -> "" 404 | | Some js -> scalar ~strict ~loc ~name:key js 405 | 406 | let section ?(strict = true) ctxs ~loc ~key = 407 | match dotted_name ~strict:false ctxs ~loc ~key with 408 | | None -> 409 | if strict then raise_err loc (Missing_section { name = key }); 410 | `Bool false 411 | | Some js -> ( 412 | match js with 413 | (* php casting *) 414 | | `Null 415 | | `Float _ 416 | | `Bool false 417 | | `String "" -> 418 | `Bool false 419 | | (`A _ | `O _) as js -> js 420 | | _ -> js ) 421 | 422 | let inverted ctxs ~loc ~key = 423 | match dotted_name ~strict:false ctxs ~loc ~key with 424 | | None -> true 425 | | Some (`A [] | `Bool false | `Null) -> true 426 | | _ -> false 427 | 428 | let param ctxs ~loc:_ ~key = Contexts.find_param ctxs key 429 | end 430 | 431 | 432 | module Render = struct 433 | (* Rendering is defined on the ast without locations. *) 434 | 435 | open Ast 436 | 437 | (* Render a template whose partials have already been expanded. 438 | 439 | Note: the reason we expand partials once before rendering, instead of 440 | expanding on the fly during rendering, is to avoid expanding many times the 441 | partials that are inside a list. However, this as the consequence that some 442 | partials that may not be used in a given rendering may be expanded, and 443 | that partial expansion cannot have access to the specific context of each 444 | partial usage -- some other Mustache APIs pass this context information to 445 | the partial-resolution function. *) 446 | let render_expanded ?(strict = true) (buf : Buffer.t) (m : Ast.t) 447 | (js : Json.t) = 448 | let beginning_of_line = ref true in 449 | 450 | let print_indented buf indent line = 451 | assert (indent >= 0); 452 | if String.equal line "" then 453 | () 454 | else ( 455 | for _i = 1 to indent do 456 | Buffer.add_char buf ' ' 457 | done; 458 | Buffer.add_string buf line; 459 | beginning_of_line := false 460 | ) 461 | in 462 | 463 | let print_dedented buf dedent line = 464 | assert (dedent >= 0); 465 | let rec print_from i = 466 | if i = String.length line then 467 | () 468 | else if 469 | i < dedent 470 | && 471 | match line.[i] with 472 | | ' ' 473 | | '\t' -> 474 | true 475 | | _ -> false 476 | then 477 | print_from (i + 1) 478 | else ( 479 | Buffer.add_substring buf line i (String.length line - i); 480 | beginning_of_line := false 481 | ) 482 | in 483 | print_from 0 484 | in 485 | 486 | let print_line indent line = 487 | if not !beginning_of_line then 488 | Buffer.add_string buf line 489 | else if indent >= 0 then 490 | print_indented buf indent line 491 | else 492 | print_dedented buf (-indent) line 493 | in 494 | 495 | let print_newline buf = 496 | Buffer.add_char buf '\n'; 497 | beginning_of_line := true 498 | in 499 | 500 | let print_indented_string indent s = 501 | let lines = String.split_on_char '\n' s in 502 | print_line indent (List.hd lines); 503 | List.iter 504 | (fun line -> 505 | print_newline buf; 506 | print_line indent line) 507 | (List.tl lines) 508 | in 509 | 510 | let print_interpolated indent data = 511 | (* per the specification, interpolated data should be spliced into the 512 | document, with further lines *not* indented specifically; this effect 513 | is obtained by calling print_line on the (possibly multiline) data. *) 514 | print_line indent data 515 | in 516 | 517 | let rec render indent m (ctxs : Contexts.t) = 518 | let loc = m.loc in 519 | match m.desc with 520 | | String s -> print_indented_string indent s 521 | | Escaped name -> 522 | print_interpolated indent 523 | (escape_html (Lookup.str ~strict ~loc ~key:name ctxs)) 524 | | Unescaped name -> 525 | print_interpolated indent (Lookup.str ~strict ~loc ~key:name ctxs) 526 | | Inverted_section s -> 527 | if Lookup.inverted ctxs ~loc ~key:s.name then 528 | render indent s.contents ctxs 529 | | Section s -> ( 530 | let enter ctx = render indent s.contents (Contexts.add ctxs ctx) in 531 | match Lookup.section ~strict ctxs ~loc ~key:s.name with 532 | | `Bool false -> () 533 | | `A elems -> List.iter enter elems 534 | | elem -> enter elem ) 535 | | Partial { indent = partial_indent; name; params; contents } -> ( 536 | let partial = Lazy.force contents in 537 | let ctxs = 538 | match params with 539 | | None -> ctxs 540 | | Some params -> 541 | List.fold_left ~f:Contexts.add_param ~init:ctxs params 542 | in 543 | match partial with 544 | | None -> if strict then raise_err loc (Missing_partial { name }) 545 | | Some partial -> render (indent + partial_indent) partial ctxs ) 546 | | Param default_param -> 547 | let param = 548 | match Lookup.param ctxs ~loc ~key:default_param.name with 549 | | Some passed_param -> passed_param 550 | | None -> default_param 551 | in 552 | render 553 | (indent + default_param.indent - param.indent) 554 | param.contents ctxs 555 | | Comment _c -> () 556 | | Concat templates -> List.iter (fun x -> render indent x ctxs) templates 557 | in 558 | 559 | render 0 m (Contexts.start (Json.value js)) 560 | end 561 | 562 | (* Packing up everything in two modules of similar signature: 563 | [Without_locations] and [With_locations]. In the toplevel signature, only 564 | [With_locations] appears, and [Without_locations] contents are directly 565 | included at the toplevel. *) 566 | 567 | module With_locations = struct 568 | include Ast 569 | 570 | (* re-exported here for backward-compatibility *) 571 | type nonrec loc = loc = 572 | { loc_start : Lexing.position 573 | ; loc_end : Lexing.position 574 | } 575 | 576 | let dummy_loc = dummy_loc 577 | 578 | let parse_lx = parse_lx 579 | 580 | let of_string = of_string 581 | 582 | let pp fmt x = pp fmt x 583 | 584 | let to_formatter = pp 585 | 586 | let to_string x = to_string x 587 | 588 | let rec fold ~string ~section ~escaped ~unescaped ~partial ~param ~comment 589 | ~concat t = 590 | let go = 591 | fold ~string ~section ~escaped ~unescaped ~partial ~param ~comment ~concat 592 | in 593 | let { desc; loc } = t in 594 | match desc with 595 | | String s -> string ~loc s 596 | | Escaped s -> escaped ~loc s 597 | | Unescaped s -> unescaped ~loc s 598 | | Comment s -> comment ~loc s 599 | | Section { name; contents } -> 600 | section ~loc ~inverted:false name (go contents) 601 | | Inverted_section { name; contents } -> 602 | section ~loc ~inverted:true name (go contents) 603 | | Concat ms -> concat ~loc (List.map ms ~f:go) 604 | | Partial p -> 605 | let params = 606 | Option.map 607 | (List.map ~f:(fun { indent; name; contents } -> 608 | (indent, name, go contents))) 609 | p.params 610 | in 611 | partial ~loc ?indent:(Some p.indent) p.name ?params p.contents 612 | | Param { indent; name; contents } -> 613 | param ~loc ?indent:(Some indent) name (go contents) 614 | 615 | module Infix = struct 616 | let ( ^ ) t1 t2 = { desc = Concat [ t1; t2 ]; loc = dummy_loc } 617 | end 618 | 619 | let raw ~loc s = { desc = String s; loc } 620 | 621 | let escaped ~loc s = { desc = Escaped s; loc } 622 | 623 | let unescaped ~loc s = { desc = Unescaped s; loc } 624 | 625 | let section ~loc n c = { desc = Section { name = n; contents = c }; loc } 626 | 627 | let inverted_section ~loc n c = 628 | { desc = Inverted_section { name = n; contents = c }; loc } 629 | 630 | let partial ~loc ?(indent = 0) n ?params c = 631 | let params = 632 | Option.map 633 | (List.map ~f:(fun (indent, name, contents) -> 634 | { indent; name; contents })) 635 | params 636 | in 637 | { desc = Partial { indent; name = n; params; contents = c }; loc } 638 | 639 | let concat ~loc t = { desc = Concat t; loc } 640 | 641 | let comment ~loc s = { desc = Comment s; loc } 642 | 643 | let param ~loc ?(indent = 0) n c = 644 | { desc = Param { indent; name = n; contents = c }; loc } 645 | 646 | let rec expand_partials (partials : name -> t option) : t -> t = 647 | let section ~loc ~inverted = 648 | if inverted then 649 | inverted_section ~loc 650 | else 651 | section ~loc 652 | in 653 | let partial ~loc ?indent name ?params contents = 654 | let contents' = 655 | lazy 656 | ( match Lazy.force contents with 657 | | None -> Option.map (expand_partials partials) (partials name) 658 | | Some t_opt -> Some t_opt ) 659 | in 660 | partial ~loc ?indent name ?params contents' 661 | in 662 | fold ~string:raw ~section ~escaped ~unescaped ~partial ~param ~comment 663 | ~concat 664 | 665 | let render_buf ?strict ?(partials = fun _ -> None) buf (m : t) (js : Json.t) = 666 | let m = expand_partials partials m in 667 | Render.render_expanded buf ?strict m js 668 | 669 | let render ?strict ?partials (m : t) (js : Json.t) = 670 | let buf = Buffer.create 0 in 671 | render_buf ?strict ?partials buf m js; 672 | Buffer.contents buf 673 | 674 | let render_fmt ?strict ?partials fmt m js = 675 | let str = render ?strict ?partials m js in 676 | Format.pp_print_string fmt str; 677 | Format.pp_print_flush fmt () 678 | end 679 | 680 | module Without_locations = struct 681 | include Ast 682 | include With_locations 683 | 684 | let noloc f x = f ~loc:dummy_loc x 685 | 686 | let raw = noloc raw 687 | 688 | let escaped = noloc escaped 689 | 690 | let unescaped = noloc unescaped 691 | 692 | let section = noloc section 693 | 694 | let inverted_section = noloc inverted_section 695 | 696 | let partial ?indent = noloc (partial ?indent) 697 | 698 | let param ?indent = noloc (param ?indent) 699 | 700 | let concat = noloc concat 701 | 702 | let comment = noloc comment 703 | 704 | let fold ~string ~section ~escaped ~unescaped ~partial ~param ~comment ~concat 705 | = 706 | let igloc f ~loc:_ = f in 707 | let string = igloc string 708 | and section = igloc section 709 | and escaped = igloc escaped 710 | and unescaped = igloc unescaped 711 | and partial = igloc partial 712 | and param = igloc param 713 | and comment = igloc comment 714 | and concat = igloc concat in 715 | fold ~string ~section ~escaped ~unescaped ~partial ~param ~comment ~concat 716 | end 717 | 718 | (* Include [Without_locations] at the toplevel, to preserve backwards 719 | compatibility of the API. *) 720 | 721 | include Without_locations 722 | -------------------------------------------------------------------------------- /mustache/lib/mustache.mli: -------------------------------------------------------------------------------- 1 | (** A module for creating and rendering mustache templates in OCaml. *) 2 | 3 | [@@@warning "-30"] 4 | 5 | module Json : sig 6 | (** Compatible with Ezjsonm *) 7 | type value = 8 | [ `Null 9 | | `Bool of bool 10 | | `Float of float 11 | | `String of string 12 | | `A of value list 13 | | `O of (string * value) list 14 | ] 15 | 16 | type t = 17 | [ `A of value list 18 | | `O of (string * value) list 19 | ] 20 | end 21 | 22 | type loc = 23 | { loc_start : Lexing.position 24 | ; loc_end : Lexing.position 25 | } 26 | 27 | type name = string 28 | 29 | type dotted_name = string list 30 | 31 | type t = 32 | { loc : loc 33 | ; desc : desc 34 | } 35 | 36 | and desc = 37 | | String of string 38 | | Escaped of dotted_name 39 | | Unescaped of dotted_name 40 | | Section of section 41 | | Inverted_section of section 42 | | Partial of partial 43 | | Param of param 44 | | Concat of t list 45 | | Comment of string 46 | 47 | and section = 48 | { name : dotted_name 49 | ; contents : t 50 | } 51 | 52 | and partial = 53 | { indent : int 54 | ; name : name 55 | ; params : param list option 56 | ; contents : t option Lazy.t 57 | } 58 | 59 | and param = 60 | { indent : int 61 | ; name : name 62 | ; contents : t 63 | } 64 | 65 | val pp_loc : Format.formatter -> loc -> unit 66 | 67 | (** Read template files; those function may raise [Parse_error]. *) 68 | type template_parse_error 69 | 70 | exception Parse_error of template_parse_error 71 | 72 | val parse_lx : Lexing.lexbuf -> t 73 | 74 | val of_string : string -> t 75 | 76 | (** [pp_template_parse_error fmt err] prints a human-readable description of a 77 | template parse error on the given formatter. The function does not flush the 78 | formatter (in case you want to use it within boxes), so you should remember 79 | to do it yourself. 80 | 81 | {[ try ignore (Mustache.of_string "{{!") with Mustache.Parse_error err -> 82 | Format.eprintf "%a@." Mustache.pp_template_parse_error err ]} *) 83 | val pp_template_parse_error : Format.formatter -> template_parse_error -> unit 84 | 85 | (** [pp fmt template] print a template as raw mustache to the formatter [fmt]. *) 86 | val pp : Format.formatter -> t -> unit 87 | 88 | (** Alias for compatibility *) 89 | val to_formatter : Format.formatter -> t -> unit 90 | 91 | (** [to_string template] uses [to_formatter] in order to return a string 92 | representing the template as raw mustache. *) 93 | val to_string : t -> string 94 | 95 | (** Render templates; those functions may raise [Render_error]. *) 96 | type render_error_kind = 97 | | Invalid_param of 98 | { name : dotted_name 99 | ; expected_form : string 100 | } 101 | | Missing_variable of { name : dotted_name } 102 | | Missing_section of { name : dotted_name } 103 | | Missing_partial of { name : name } 104 | 105 | type render_error = 106 | { loc : loc 107 | ; kind : render_error_kind 108 | } 109 | 110 | exception Render_error of render_error 111 | 112 | val pp_render_error : Format.formatter -> render_error -> unit 113 | 114 | (** [render_fmt fmt template json] renders [template], filling it with data from 115 | [json], printing it to formatter [fmt]. 116 | 117 | For each partial [p], if [partials p] is [Some t] then the partial is 118 | substituted by [t]. Otherwise, the partial is substituted by the empty 119 | string is [strict] is [false]. If [strict] is [true], a {!Missing_partial} 120 | error is raised. 121 | 122 | @raise Render_error when there is a mismatch between the template and the 123 | data. The [Missing_*] cases are only raised in strict mode, when [strict] is 124 | true. *) 125 | val render_fmt : 126 | ?strict:bool 127 | -> ?partials:(name -> t option) 128 | -> Format.formatter 129 | -> t 130 | -> Json.t 131 | -> unit 132 | 133 | (** [render_buf buf template json] renders [template], filling it with data from 134 | [json], printing it to the buffer [buf]. See {!render_fmt}. *) 135 | val render_buf : 136 | ?strict:bool 137 | -> ?partials:(name -> t option) 138 | -> Buffer.t 139 | -> t 140 | -> Json.t 141 | -> unit 142 | 143 | (** [render template json] renders [template], filling it with data from [json], 144 | and returns the resulting string. See {!render_fmt}. *) 145 | val render : 146 | ?strict:bool -> ?partials:(name -> t option) -> t -> Json.t -> string 147 | 148 | (** [fold template] is the composition of [f] over parts of [template], called 149 | in order of occurrence, where each [f] is one of the labelled arguments 150 | applied to the corresponding part. The default for [f] is the identity 151 | function. 152 | 153 | @param string Applied to each literal part of the template. 154 | @param escaped Applied to ["name"] for occurrences of [{{name}}]. 155 | @param unescaped Applied to ["name"] for occurrences of [{{{name}}}]. 156 | @param partial Applied to ["box"] for occurrences of [{{> box}}] or 157 | [{{< box}}]. 158 | @param params Applied to ["param"] for occurrences of [{{$ param}}]. 159 | @param comment Applied to ["comment"] for occurrences of [{{! comment}}]. *) 160 | val fold : 161 | string:(string -> 'a) 162 | -> section:(inverted:bool -> dotted_name -> 'a -> 'a) 163 | -> escaped:(dotted_name -> 'a) 164 | -> unescaped:(dotted_name -> 'a) 165 | -> partial: 166 | ( ?indent:int 167 | -> name 168 | -> ?params:(int * name * 'a) list 169 | -> t option Lazy.t 170 | -> 'a) 171 | -> param:(?indent:int -> name -> 'a -> 'a) 172 | -> comment:(string -> 'a) 173 | -> concat:('a list -> 'a) 174 | -> t 175 | -> 'a 176 | 177 | (** [expand_partials f template] is [template] where for each [Partial p] node, 178 | [p.contents] now evaluates to [f p.name] if they were evaluating to [None]. 179 | Note that no lazy is forced at this point, and calls to [f] are delayed 180 | until [p.contents] is forced. *) 181 | val expand_partials : (name -> t option) -> t -> t 182 | 183 | (** Shortcut for concatening two templates pieces. *) 184 | module Infix : sig 185 | val ( ^ ) : t -> t -> t 186 | end 187 | 188 | (** Escape [&], ["\""], ['], [<] and [>] character for html rendering. *) 189 | val escape_html : string -> string 190 | 191 | (** [

This is raw text.

] *) 192 | val raw : string -> t 193 | 194 | (** [{{name}}] *) 195 | val escaped : dotted_name -> t 196 | 197 | (** [{{{name}}}] *) 198 | val unescaped : dotted_name -> t 199 | 200 | (** [{{^person}} {{/person}}] *) 201 | val inverted_section : dotted_name -> t -> t 202 | 203 | (** [{{#person}} {{/person}}] *) 204 | val section : dotted_name -> t -> t 205 | 206 | (** [{{> box}}] or 207 | 208 | {[ 209 | {{< box}} 210 | {{$param1}} default value for param1 {{/param1}} 211 | {{$param2}} default value for param1 {{/param2}} 212 | {{/box}} 213 | ]} *) 214 | val partial : 215 | ?indent:int -> name -> ?params:(int * name * t) list -> t option Lazy.t -> t 216 | 217 | (** [{{$foo}} {{/foo}}] *) 218 | val param : ?indent:int -> name -> t -> t 219 | 220 | (** [{{! this is a comment}}] *) 221 | val comment : string -> t 222 | 223 | (** Group a [t list] as a single [t]. *) 224 | val concat : t list -> t 225 | 226 | (** Variant of the [t] mustache datatype which includes source-file locations, 227 | and associated functions. *) 228 | module With_locations : sig 229 | (* these types have been moved out, keep an alias for backward-compatibility *) 230 | type nonrec loc = loc = 231 | { loc_start : Lexing.position 232 | ; loc_end : Lexing.position 233 | } 234 | 235 | and desc = desc = 236 | | String of string 237 | | Escaped of dotted_name 238 | | Unescaped of dotted_name 239 | | Section of section 240 | | Inverted_section of section 241 | | Partial of partial 242 | | Param of param 243 | | Concat of t list 244 | | Comment of string 245 | 246 | and section = section = 247 | { name : dotted_name 248 | ; contents : t 249 | } 250 | 251 | and partial = partial = 252 | { indent : int 253 | ; name : name 254 | ; params : param list option 255 | ; contents : t option Lazy.t 256 | } 257 | 258 | and param = param = 259 | { indent : int 260 | ; name : name 261 | ; contents : t 262 | } 263 | 264 | and t = t = 265 | { loc : loc 266 | ; desc : desc 267 | } 268 | 269 | (** A value of type [loc], guaranteed to be different from any valid location. *) 270 | val dummy_loc : loc 271 | 272 | (** Read *) 273 | val parse_lx : Lexing.lexbuf -> t 274 | 275 | val of_string : string -> t 276 | 277 | (** [pp fmt template] print a template as raw mustache to the formatter [fmt]. *) 278 | val pp : Format.formatter -> t -> unit 279 | 280 | (** Alias for compatibility *) 281 | val to_formatter : Format.formatter -> t -> unit 282 | 283 | (** [to_string template] uses [to_formatter] in order to return a string 284 | representing the template as raw mustache. *) 285 | val to_string : t -> string 286 | 287 | (** [render_fmt fmt template json] renders [template], filling it with data 288 | from [json], printing it to formatter [fmt]. 289 | 290 | For each partial [p], if [partials p] is [Some t] then the partial is 291 | substituted by [t]. Otherwise, the partial is substituted by the empty 292 | string is [strict] is [false]. If [strict] is [true], a {!Missing_partial} 293 | error is raised. 294 | 295 | @raise Render_error when there is a mismatch between the template and the 296 | data. The [Missing_*] cases are only raised in strict mode, when [strict] 297 | is true. *) 298 | val render_fmt : 299 | ?strict:bool 300 | -> ?partials:(name -> t option) 301 | -> Format.formatter 302 | -> t 303 | -> Json.t 304 | -> unit 305 | 306 | (** [render_buf buf template json] renders [template], filling it with data 307 | from [json], printing it to the buffer [buf]. See {!render_fmt}. *) 308 | val render_buf : 309 | ?strict:bool 310 | -> ?partials:(name -> t option) 311 | -> Buffer.t 312 | -> t 313 | -> Json.t 314 | -> unit 315 | 316 | (** [render template json] renders [template], filling it with data from 317 | [json], and returns the resulting string. See {!render_fmt}. *) 318 | val render : 319 | ?strict:bool -> ?partials:(name -> t option) -> t -> Json.t -> string 320 | 321 | (** [fold template] is the composition of [f] over parts of [template], called 322 | in order of occurrence, where each [f] is one of the labelled arguments 323 | applied to the corresponding part. The default for [f] is the identity 324 | function. 325 | 326 | @param string Applied to each literal part of the template. 327 | @param escaped Applied to ["name"] for occurrences of [{{name}}]. 328 | @param unescaped Applied to ["name"] for occurrences of [{{{name}}}]. 329 | @param partial Applied to ["box"] for occurrences of [{{> box}}] or 330 | [{{< box}}]. 331 | @param params Applied to ["param"] for occurrences of [{{$ param}}]. 332 | @param comment Applied to ["comment"] for occurrences of [{{! comment}}]. *) 333 | val fold : 334 | string:(loc:loc -> string -> 'a) 335 | -> section:(loc:loc -> inverted:bool -> dotted_name -> 'a -> 'a) 336 | -> escaped:(loc:loc -> dotted_name -> 'a) 337 | -> unescaped:(loc:loc -> dotted_name -> 'a) 338 | -> partial: 339 | ( loc:loc 340 | -> ?indent:int 341 | -> name 342 | -> ?params:(int * name * 'a) list 343 | -> t option Lazy.t 344 | -> 'a) 345 | -> param:(loc:loc -> ?indent:int -> name -> 'a -> 'a) 346 | -> comment:(loc:loc -> string -> 'a) 347 | -> concat:(loc:loc -> 'a list -> 'a) 348 | -> t 349 | -> 'a 350 | 351 | (** [expand_partials f template] is [template] where for each [Partial p] 352 | node, [p.contents] now evaluates to [f p.name] if they were evaluating to 353 | [None]. Note that no lazy is forced at this point, and calls to [f] are 354 | delayed until [p.contents] is forced. *) 355 | val expand_partials : (name -> t option) -> t -> t 356 | 357 | (** Shortcut for concatening two templates pieces. *) 358 | module Infix : sig 359 | (** The location of the created [Concat] node has location [dummy_loc]. Use 360 | [concat] to provide a location. *) 361 | val ( ^ ) : t -> t -> t 362 | end 363 | 364 | (** [

This is raw text.

] *) 365 | val raw : loc:loc -> string -> t 366 | 367 | (** [{{name}}] *) 368 | val escaped : loc:loc -> dotted_name -> t 369 | 370 | (** [{{{name}}}] *) 371 | val unescaped : loc:loc -> dotted_name -> t 372 | 373 | (** [{{^person}} {{/person}}] *) 374 | val inverted_section : loc:loc -> dotted_name -> t -> t 375 | 376 | (** [{{#person}} {{/person}}] *) 377 | val section : loc:loc -> dotted_name -> t -> t 378 | 379 | (** [{{> box}}] or 380 | 381 | {[ 382 | {{< box}} 383 | {{$param1}} default value for param1 {{/param1}} 384 | {{$param2}} default value for param1 {{/param2}} 385 | {{/box}} 386 | ]} *) 387 | val partial : 388 | loc:loc 389 | -> ?indent:int 390 | -> name 391 | -> ?params:(int * name * t) list 392 | -> t option Lazy.t 393 | -> t 394 | 395 | (** [{{$foo}} {{/foo}}] *) 396 | val param : loc:loc -> ?indent:int -> name -> t -> t 397 | 398 | (** [{{! this is a comment}}] *) 399 | val comment : loc:loc -> string -> t 400 | 401 | (** Group a [t list] as a single [t]. *) 402 | val concat : loc:loc -> t list -> t 403 | end 404 | 405 | (** Erase locations from a mustache value of type [With_locations.t]. *) 406 | val erase_locs : With_locations.t -> t 407 | 408 | (** Add the [dummy_loc] location to each node of a mustache value of type [t]. *) 409 | val add_dummy_locs : t -> With_locations.t 410 | -------------------------------------------------------------------------------- /mustache/lib/mustache_lexer.mll: -------------------------------------------------------------------------------- 1 | (*{{{ The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Rudi Grinberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to 7 | deal in the Software without restriction, including without limitation the 8 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | sell copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. }}}*) 22 | { 23 | open Lexing 24 | open Mustache_parser 25 | 26 | exception Error of string 27 | 28 | let tok_arg lexbuf f = 29 | let start_p = lexbuf.Lexing.lex_start_p in 30 | let x = f lexbuf in 31 | lexbuf.Lexing.lex_start_p <- start_p; 32 | x 33 | 34 | let lex_tag lexbuf space ident tag_end = 35 | tok_arg lexbuf (fun lexbuf -> 36 | let () = space lexbuf in 37 | let name = ident lexbuf in 38 | let () = space lexbuf in 39 | let () = tag_end lexbuf in 40 | name 41 | ) 42 | 43 | let split_ident ident = 44 | if ident = "." then [] 45 | else String.split_on_char '.' ident 46 | 47 | let check_mustaches ~expected ~lexed = 48 | if expected <> lexed then 49 | raise (Error (Printf.sprintf "'%s' expected" expected)) 50 | } 51 | 52 | let blank = [' ' '\t']* 53 | let newline = ('\n' | "\r\n") 54 | let raw = [^ '{' '}' '\n']* 55 | let id = ['a'-'z' 'A'-'Z' '-' '_' '/'] ['a'-'z' 'A'-'Z' '0'-'9' '-' '_' '/']* 56 | let ident = ('.' | id ('.' id)*) 57 | 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 | 77 | rule space = parse 78 | | blank newline { new_line lexbuf; space lexbuf } 79 | | blank { () } 80 | 81 | and ident = parse 82 | | ident { lexeme lexbuf } 83 | | "" { raise (Error "ident expected") } 84 | 85 | and partial_name = parse 86 | | partial_name { lexeme lexbuf } 87 | 88 | and end_on expected = parse 89 | | ("}}" | "}}}" | "") as lexed { check_mustaches ~expected ~lexed } 90 | 91 | and comment acc = parse 92 | | "}}" { String.concat "" (List.rev acc) } 93 | | raw newline { new_line lexbuf; comment ((lexeme lexbuf) :: acc) lexbuf } 94 | | raw { comment ((lexeme lexbuf) :: acc) lexbuf } 95 | | ['{' '}'] { comment ((lexeme lexbuf) :: acc) lexbuf } 96 | | eof { raise (Error "non-terminated comment") } 97 | 98 | and mustache = parse 99 | | "{{" { ESCAPE (lex_tag lexbuf space ident (end_on "}}") |> split_ident) } 100 | | "{{{" { UNESCAPE (lex_tag lexbuf space ident (end_on "}}}") |> split_ident) } 101 | | "{{&" { UNESCAPE (lex_tag lexbuf space ident (end_on "}}") |> split_ident) } 102 | | "{{#" { OPEN_SECTION (lex_tag lexbuf space ident (end_on "}}") |> split_ident) } 103 | | "{{^" { OPEN_INVERTED_SECTION (lex_tag lexbuf space ident (end_on "}}") |> split_ident) } 104 | | "{{/" { CLOSE (lex_tag lexbuf space partial_name (end_on "}}")) } 105 | | "{{>" { PARTIAL (0, lex_tag lexbuf space partial_name (end_on "}}")) } 106 | | "{{<" { OPEN_PARTIAL_WITH_PARAMS (0, lex_tag lexbuf space partial_name (end_on "}}")) } 107 | | "{{$" { OPEN_PARAM (0, lex_tag lexbuf space ident (end_on "}}")) } 108 | | "{{!" { COMMENT (tok_arg lexbuf (comment [])) } 109 | | raw newline { new_line lexbuf; RAW (lexeme lexbuf) } 110 | | raw { RAW (lexeme lexbuf) } 111 | | ['{' '}'] { RAW (lexeme lexbuf) } 112 | | eof { EOF } 113 | 114 | { 115 | (* Trim whitespace around standalone tags. 116 | 117 | The Mustache specification is careful with its treatment of 118 | whitespace. In particular, tags that do not themselves expand to 119 | visible content are defined as "standalone", with the 120 | requirement that if one or several standalone tags "stand alone" 121 | in a line (there is nothing else but whitespace), the whitespace 122 | of this line should be ommitted. 123 | 124 | For example, this means that: 125 | {{#foo}} 126 | I can access {{var}} inside the section. 127 | {{/foo} 128 | takes, once rendered, only 1 line instead of 3: the newlines 129 | after {{#foo}} and {{/foo}} are part of the "standalone 130 | whitespace", so they are not included in the output. 131 | 132 | Note: if a line contains only whitespace, no standalone tag, 133 | then the whitespace is preserved. 134 | 135 | We implement this by a post-processing past on the lexer token 136 | stream. We split the token stream, one sub-stream per line, and 137 | then for each token line we determine if satisfies the 138 | standalone criterion. 139 | 140 | Another information collected at the same time, as it is also 141 | part of whitespace processing, is the "indentation" of partials: 142 | if a partial expands to multi-line content, and if it is 143 | intended at the use-site (it is at a non-zero column with only 144 | whitespace before it on the line), then the specification 145 | mandates that all its lines should be indented by the same 146 | amount. We collect this information during the whitespace 147 | postprocessing of tokens, and store it in the Partial 148 | constructor as the first parameter. 149 | *) 150 | let handle_standalone lexer lexbuf = 151 | let ends_with_newline s = 152 | String.length s > 0 && 153 | s.[String.length s - 1] = '\n' 154 | in 155 | let get_loc () = lexbuf.Lexing.lex_curr_p in 156 | let get_tok () = 157 | let loc_start = get_loc () in 158 | let tok = lexer lexbuf in 159 | let loc_end = get_loc () in 160 | (tok, loc_start, loc_end) 161 | in 162 | let slurp_line lookahead = 163 | let rec start = function 164 | | None -> loop [] 165 | | Some lookahead -> continue [] lookahead 166 | and loop acc = 167 | continue acc (get_tok ()) 168 | and continue acc tok = 169 | match tok with 170 | | EOF, _, _ -> (List.rev (tok :: acc), None) 171 | | RAW s, _, _ when ends_with_newline s -> 172 | let lookahead = get_tok () in 173 | (List.rev (tok :: acc), Some lookahead) 174 | | _ -> loop (tok :: acc) 175 | in 176 | start lookahead 177 | in 178 | let count_indentation s = 179 | let i = ref 0 in 180 | let len = String.length s in 181 | while (!i < len 182 | && match s.[!i] with ' ' | '\t' | '\r' | '\n' -> true | _ -> false) 183 | do 184 | incr i 185 | done; 186 | !i 187 | in 188 | let is_blank s = 189 | count_indentation s = String.length s 190 | in 191 | let skip_blanks l = 192 | let rec loop skipped = function 193 | | (RAW s, _, _) :: toks when is_blank s -> 194 | loop (skipped + String.length s) toks 195 | | toks -> (skipped, toks) 196 | in 197 | loop 0 l 198 | in 199 | let trim_standalone toks lookahead = 200 | let toks = 201 | (* if the line starts with a partial, 202 | turn the skipped blank into partial indentation *) 203 | let (skipped, toks_after_blank) = skip_blanks toks in 204 | match toks_after_blank with 205 | | (PARTIAL (_ , name), loc1, loc2) :: rest -> 206 | (PARTIAL (skipped, name), loc1, loc2) :: rest 207 | | (OPEN_PARTIAL_WITH_PARAMS (_ , name), loc1, loc2) :: rest -> 208 | (OPEN_PARTIAL_WITH_PARAMS (skipped, name), loc1, loc2) :: rest 209 | | (OPEN_PARAM (_ , name), loc1, loc2) :: rest -> 210 | (* we want to count the indentation of 211 | {{$param}} 212 | blah blah 213 | {{/param}} 214 | as the indentation of 'blah blah', not the indentation 215 | of '{{$param}}' itself: using the parameter tag instead of the content 216 | as indentation would result in the content being over-indented at each occurrence. 217 | *) 218 | let skipped = 219 | match rest, lookahead with 220 | | ((RAW end_of_line, _, _) :: _), 221 | Some (RAW start_of_next_line, _, _) when ends_with_newline end_of_line -> 222 | count_indentation start_of_next_line 223 | | _ -> skipped 224 | in 225 | (OPEN_PARAM (skipped, name), loc1, loc2) :: rest 226 | | _ -> toks 227 | in 228 | let toks = 229 | (* if the line only contains whitespace and at least one standalone tags, 230 | remove all whitespace *) 231 | let rec standalone acc = function 232 | | (RAW s, _, _) :: rest when is_blank s -> 233 | (* omit whitespace *) 234 | standalone acc rest 235 | | ((OPEN_SECTION _ 236 | | OPEN_INVERTED_SECTION _ 237 | | CLOSE _ 238 | | PARTIAL _ 239 | | OPEN_PARTIAL_WITH_PARAMS _ 240 | | OPEN_PARAM _ 241 | | COMMENT _), _, _) as tok :: rest -> 242 | (* collect standalone tags *) 243 | standalone (tok :: acc) rest 244 | | [] | (EOF, _, _) :: _ -> 245 | (* end of line *) 246 | if (acc = []) then 247 | (* if acc is empty, the line only contains whitespace, 248 | which should be kept *) 249 | None 250 | else 251 | Some (List.rev acc) 252 | | _non_blank :: _rest -> 253 | (* non-blank, non-standalone token *) 254 | None 255 | in 256 | match standalone [] toks with 257 | | None -> toks 258 | | Some standalone_toks -> standalone_toks 259 | in 260 | assert (toks <> []); 261 | toks 262 | in 263 | let line_rest = ref [] in 264 | let lookahead = ref None in 265 | fun () -> 266 | match !line_rest with 267 | | next :: rest -> 268 | line_rest := rest; 269 | next 270 | | [] -> 271 | let next_line, next_lookahead = slurp_line !lookahead in 272 | let next_line = trim_standalone next_line next_lookahead in 273 | line_rest := List.tl next_line; 274 | lookahead := next_lookahead; 275 | List.hd next_line 276 | } 277 | -------------------------------------------------------------------------------- /mustache/lib/mustache_parser.mly: -------------------------------------------------------------------------------- 1 | /*{{{ The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Rudi Grinberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to 7 | deal in the Software without restriction, including without limitation the 8 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | sell copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. *) 22 | }}}*/ 23 | %{ 24 | open Mustache_types 25 | open Mustache_types.Ast 26 | 27 | let mkloc (start_pos, end_pos) = 28 | { loc_start = start_pos; 29 | loc_end = end_pos } 30 | 31 | let check_matching loc name_kind start_name end_name = 32 | if start_name <> end_name then 33 | raise (Mismatched_names (mkloc loc, { name_kind; start_name; end_name })) 34 | 35 | let dotted name = 36 | string_of_dotted_name name 37 | 38 | let with_loc loc desc = 39 | { loc = mkloc loc; desc } 40 | 41 | 42 | (* The template-inheritance specification describes partial-with-params 43 | application of the form: 44 | 45 | {{foo}} partial if it itself 59 | expands to parameter blocks, but this is not specified in the implementation, 60 | slightly more work to implement (we have to defer parameter-filtering to 61 | the rendering step), and of dubious utility in practice, so for now we do 62 | not support this. 63 | *) 64 | let partial_parameters partial_name partial_block = 65 | let rec collect params_rev elt = 66 | match elt.desc with 67 | | Param param -> param :: params_rev 68 | | Comment _ | String _ -> 69 | (* the specification mandates that raw strings are inogred *) 70 | params_rev 71 | | (Escaped _ | Unescaped _ | Section _ | Inverted_section _ | Partial _) -> 72 | raise (Invalid_as_partial_parameter (partial_name, elt)) 73 | | Concat elts -> 74 | List.fold_left collect params_rev elts 75 | in 76 | collect [] partial_block 77 | |> List.rev 78 | 79 | %} 80 | 81 | %token EOF 82 | %token ESCAPE 83 | %token UNESCAPE 84 | %token OPEN_INVERTED_SECTION 85 | %token OPEN_SECTION 86 | %token PARTIAL 87 | %token OPEN_PARTIAL_WITH_PARAMS 88 | %token OPEN_PARAM 89 | %token CLOSE 90 | %token COMMENT 91 | 92 | %token RAW 93 | 94 | %start mustache 95 | %type mustache 96 | 97 | %% 98 | 99 | mustache_element: 100 | | elt = ESCAPE { with_loc $sloc (Escaped elt) } 101 | | elt = UNESCAPE { with_loc $sloc (Unescaped elt) } 102 | | start_name = OPEN_SECTION 103 | contents = mustache_expr 104 | end_name = CLOSE { 105 | check_matching $sloc Section_name (dotted start_name) end_name; 106 | with_loc $sloc 107 | (Section { name = start_name; contents }) 108 | } 109 | | start_name = OPEN_INVERTED_SECTION 110 | contents = mustache_expr 111 | end_name = CLOSE { 112 | check_matching $sloc Inverted_section_name (dotted start_name) end_name; 113 | with_loc $sloc 114 | (Inverted_section { name = start_name; contents }) 115 | } 116 | | partial = PARTIAL { 117 | let (indent, name) = partial in 118 | with_loc $sloc 119 | (Partial { indent; name; params = None; 120 | contents = lazy None }) 121 | } 122 | | partial = OPEN_PARTIAL_WITH_PARAMS 123 | partial_block = mustache_expr 124 | end_name = CLOSE { 125 | let (indent, start_name) = partial in 126 | check_matching $sloc Partial_with_params_name start_name end_name; 127 | let params = partial_parameters start_name partial_block in 128 | with_loc $sloc 129 | (Partial { indent; name = start_name; params = Some params; 130 | contents = lazy None }) 131 | } 132 | | param = OPEN_PARAM 133 | contents = mustache_expr 134 | end_name = CLOSE { 135 | let (indent, start_name) = param in 136 | check_matching $sloc Param_name start_name end_name; 137 | with_loc $sloc 138 | (Param { indent; name = start_name; contents }) 139 | } 140 | | s = COMMENT { with_loc $sloc (Comment s) } 141 | | s = RAW { with_loc $sloc (String s) } 142 | 143 | mustache_expr: 144 | | elts = list(mustache_element) { 145 | match elts with 146 | | [] -> with_loc $sloc (String "") 147 | | [x] -> x 148 | | xs -> with_loc $sloc (Concat xs) 149 | } 150 | 151 | 152 | mustache: 153 | | mexpr = mustache_expr EOF { mexpr } 154 | 155 | %% 156 | -------------------------------------------------------------------------------- /mustache/lib/mustache_types.ml: -------------------------------------------------------------------------------- 1 | (*{{{ The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Rudi Grinberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. }}}*) 22 | 23 | type name = string 24 | 25 | type dotted_name = string list 26 | 27 | type loc = 28 | { loc_start : Lexing.position 29 | ; loc_end : Lexing.position 30 | } 31 | 32 | let pp_dotted_name fmt = function 33 | | [] -> Format.fprintf fmt "." 34 | | n :: ns -> 35 | Format.fprintf fmt "%s" n; 36 | List.iter (fun n -> Format.fprintf fmt ".%s" n) ns 37 | 38 | let string_of_dotted_name n = Format.asprintf "%a" pp_dotted_name n 39 | 40 | module Ast = struct 41 | [@@@warning "-30"] 42 | 43 | type t = 44 | { loc : loc 45 | ; desc : desc 46 | } 47 | 48 | and desc = 49 | | String of string 50 | | Escaped of dotted_name 51 | | Unescaped of dotted_name 52 | | Section of section 53 | | Inverted_section of section 54 | | Partial of partial 55 | | Param of param 56 | | Concat of t list 57 | | Comment of string 58 | 59 | and section = 60 | { name : dotted_name 61 | ; contents : t 62 | } 63 | 64 | and partial = 65 | { indent : int 66 | ; name : name 67 | ; params : param list option 68 | ; contents : t option Lazy.t 69 | } 70 | 71 | and param = 72 | { indent : int 73 | ; name : name 74 | ; contents : t 75 | } 76 | end 77 | 78 | type name_kind = 79 | | Section_name 80 | | Inverted_section_name 81 | | Partial_with_params_name 82 | | Param_name 83 | 84 | type name_mismatch_error = 85 | { name_kind : name_kind 86 | ; start_name : name 87 | ; end_name : name 88 | } 89 | 90 | (* these exceptions are used internally in the parser, never exposed to users *) 91 | exception Mismatched_names of loc * name_mismatch_error 92 | exception Invalid_as_partial_parameter of name * Ast.t 93 | -------------------------------------------------------------------------------- /mustache/lib_test/compat/dune: -------------------------------------------------------------------------------- 1 | (tests 2 | (libraries mustache ezjsonm) 3 | (package mustache) 4 | (names user_program)) 5 | -------------------------------------------------------------------------------- /mustache/lib_test/compat/mustache_v200.ml: -------------------------------------------------------------------------------- 1 | (** This test is designed to get a sense of the backward-compatibility impact of 2 | changes to the ocaml-mustache library. 3 | 4 | mustache_v200.mli is exactly a copy of the Mustache interface as it existed 5 | in the version v2.0.0, and it should not change. 6 | 7 | mustache_v200.ml is a reimplementation of this interface using the current 8 | ocaml-mustache library. If the library changes, this reimplementation will 9 | have to be fixed as well; the invasiveness of this fix can be used to 10 | estimate the invasiveness of the change for end-users. 11 | 12 | v2.0.0 was chosen because it does not contain an explicit AST definition or 13 | "fold" function -- whose compatibility breaks for basically any new language 14 | feature or representation change (for example when adding locations in the 15 | AST). *) 16 | 17 | include Mustache 18 | 19 | (** The exceptions below are not used anymore. *) 20 | exception Invalid_param of string 21 | 22 | exception Invalid_template of string 23 | 24 | let iter_var = Mustache.escaped [] 25 | 26 | let render_fmt = Mustache.render_fmt ?strict:None ?partials:None 27 | 28 | let render = Mustache.render ?strict:None ?partials:None 29 | 30 | open struct 31 | let dotted name = 32 | String.split_on_char '.' name |> List.filter (fun n -> n <> "") 33 | end 34 | 35 | let escaped name = escaped (dotted name) 36 | 37 | let unescaped name = unescaped (dotted name) 38 | 39 | let inverted_section name = inverted_section (dotted name) 40 | 41 | let section name = section (dotted name) 42 | 43 | let partial name = partial ?indent:None ?params:None name (lazy None) 44 | -------------------------------------------------------------------------------- /mustache/lib_test/compat/mustache_v200.mli: -------------------------------------------------------------------------------- 1 | (** A module for creating and rendering mustache templates in OCaml. *) 2 | exception Invalid_param of string 3 | 4 | exception Invalid_template of string 5 | 6 | type t 7 | 8 | (** Read *) 9 | val parse_lx : Lexing.lexbuf -> t 10 | 11 | val of_string : string -> t 12 | 13 | (** [to_formatter fmt template] print a template as raw mustache to the 14 | formatter [fmt]. *) 15 | val to_formatter : Format.formatter -> t -> unit 16 | 17 | (** [to_string template] uses [to_formatter] in order to return a string 18 | representing the template as raw mustache. *) 19 | val to_string : t -> string 20 | 21 | (** [render_fmt fmt template json] render [template], filling it with data from 22 | [json], printing it to formatter [fmt]. *) 23 | val render_fmt : Format.formatter -> t -> Ezjsonm.t -> unit 24 | 25 | (** [render template json] use [render_fmt] to render [template] with data from 26 | [json] and returns the resulting string. *) 27 | val render : t -> Ezjsonm.t -> string 28 | 29 | (** Shortcut for concatening two templates pieces. *) 30 | module Infix : sig 31 | val ( ^ ) : t -> t -> t 32 | end 33 | 34 | (** Escape [&], ["\""], ['], [<] and [>] character for html rendering. *) 35 | val escape_html : string -> string 36 | 37 | (** [{{.}}] *) 38 | val iter_var : t 39 | 40 | (** [

This is raw text.

] *) 41 | val raw : string -> t 42 | 43 | (** [{{name}}] *) 44 | val escaped : string -> t 45 | 46 | (** [{{{name}}}] *) 47 | val unescaped : string -> t 48 | 49 | (** [{{^person}} {{/person}}] *) 50 | val inverted_section : string -> t -> t 51 | 52 | (** [{{#person}} {{/person}}] *) 53 | val section : string -> t -> t 54 | 55 | (** [{{> box}}] *) 56 | val partial : string -> t 57 | 58 | (** [{{! this is a comment}}] *) 59 | val comment : string -> t 60 | 61 | (** Group a [t list] as a single [t]. *) 62 | val concat : t list -> t 63 | -------------------------------------------------------------------------------- /mustache/lib_test/compat/user_program.expected: -------------------------------------------------------------------------------- 1 | 2 | # Parsed template 3 | 4 | ## parsed 5 | --- 6 | Hello {{ name }} 7 | Mustache is: 8 | {{#qualities}}* {{ name }} 9 | {{/qualities}} 10 | --- 11 | 12 | ## rendered 13 | --- 14 | Hello OCaml 15 | Mustache is: 16 | * awesome 17 | * simple 18 | * fun 19 | 20 | --- 21 | 22 | # Programmed template 23 | 24 | ## parsed 25 | --- 26 | Hello {{ name }} 27 | Mustache is: 28 | {{#qualities}}* {{ name }} 29 | {{/qualities}} 30 | --- 31 | 32 | ## rendered 33 | --- 34 | Hello OCaml 35 | Mustache is: 36 | * awesome 37 | * simple 38 | * fun 39 | 40 | --- 41 | 42 | # Output comparison 43 | Outputs match as expected. 44 | -------------------------------------------------------------------------------- /mustache/lib_test/compat/user_program.ml: -------------------------------------------------------------------------------- 1 | module Mustache = Mustache_v200 2 | 3 | (* A simple user program that could have been written against ocaml-mustache 4 | 2.0.0; we check that it keeps working as expected. *) 5 | 6 | let json = 7 | `O 8 | [ ("name", `String "OCaml") 9 | ; ( "qualities" 10 | , `A 11 | [ `O [ ("name", `String "awesome") ] 12 | ; `O [ ("name", `String "simple") ] 13 | ; `O [ ("name", `String "fun") ] 14 | ] ) 15 | ] 16 | 17 | let section heading = 18 | print_newline (); 19 | print_string "# "; 20 | print_endline heading 21 | 22 | let subsection heading = 23 | print_newline (); 24 | print_string "## "; 25 | print_endline heading 26 | 27 | let test tmpl = 28 | subsection "parsed"; 29 | print_endline "---"; 30 | print_endline (Mustache.to_string tmpl); 31 | print_endline "---"; 32 | 33 | subsection "rendered"; 34 | print_endline "---"; 35 | print_endline (Mustache.render tmpl json); 36 | print_endline "---"; 37 | () 38 | 39 | let () = section "Parsed template" 40 | 41 | let parsed_template = 42 | Mustache.of_string 43 | "Hello {{name}}\nMustache is:\n{{#qualities}}\n* {{name}}\n{{/qualities}}\n" 44 | 45 | let () = test parsed_template 46 | 47 | let () = section "Programmed template" 48 | 49 | let programmed_template = 50 | let open Mustache in 51 | concat 52 | [ raw "Hello " 53 | ; escaped "name" 54 | ; raw "\n" 55 | ; raw "Mustache is:" 56 | ; raw "\n" 57 | ; section "qualities" @@ concat [ raw "* "; escaped "name"; raw "\n" ] 58 | ] 59 | 60 | let () = test programmed_template 61 | 62 | let () = section "Output comparison" 63 | 64 | let () = 65 | let parsed_output = Mustache.render parsed_template json in 66 | let programmed_output = Mustache.render programmed_template json in 67 | if String.equal parsed_output programmed_output then 68 | print_endline "Outputs match as expected." 69 | else 70 | print_endline "Outputs DO NOT match, this is suspcious." 71 | -------------------------------------------------------------------------------- /mustache/lib_test/dune: -------------------------------------------------------------------------------- 1 | (tests 2 | (libraries mustache ounit2 ezjsonm) 3 | (names test_mustache spec_mustache) 4 | (package mustache) 5 | (deps 6 | test_mustache.exe 7 | (glob_files ../specs/*.json))) 8 | -------------------------------------------------------------------------------- /mustache/lib_test/spec_mustache.ml: -------------------------------------------------------------------------------- 1 | open OUnit2 2 | module J = Ezjsonm 3 | 4 | let ( ^/ ) = Filename.concat 5 | 6 | let specs_directory = Filename.parent_dir_name ^/ "specs" 7 | 8 | type test = 9 | { from_file : string 10 | ; name : string 11 | ; data : J.t 12 | ; partials : (Mustache.name * Mustache.t) list 13 | ; template : string 14 | ; expected : string 15 | ; desc : string 16 | } 17 | 18 | let apply_mustache test = 19 | let tmpl = 20 | try Mustache.of_string test.template 21 | with e -> 22 | Printf.eprintf "Parsing of test %s from %s failed.\n" test.name 23 | test.from_file; 24 | raise e 25 | in 26 | let partials name = 27 | try Some (List.assoc name test.partials) with Not_found -> None 28 | in 29 | let rendered = 30 | try Mustache.render ~strict:false ~partials tmpl test.data 31 | with e -> 32 | Printf.eprintf "Rendering of test %s from %s failed.\n" test.name 33 | test.from_file; 34 | raise e 35 | in 36 | rendered 37 | 38 | let load_file f = 39 | let ic = open_in f in 40 | let n = in_channel_length ic in 41 | let s = Bytes.create n in 42 | really_input ic s 0 n; 43 | close_in ic; 44 | s 45 | 46 | let j_of_data : J.value -> J.t = function 47 | | `O l -> `O l 48 | | _ -> failwith "Incorrect json data" 49 | 50 | let load_test_file f = 51 | let test_j = 52 | load_file (specs_directory ^/ f) 53 | |> Bytes.to_string |> J.from_string |> J.value 54 | in 55 | 56 | let test_of_json j = 57 | let test_name = J.find j [ "name" ] |> J.get_string in 58 | let parse_partials j = 59 | if not (J.mem j [ "partials" ]) then 60 | [] 61 | else 62 | J.find j [ "partials" ] 63 | |> J.get_dict 64 | |> List.map (fun (name, tmpl) -> 65 | try (name, Mustache.of_string (J.get_string tmpl)) 66 | with e -> 67 | Printf.eprintf "Parsing of partial %s of test %s failed\n" name 68 | test_name; 69 | raise e) 70 | in 71 | { from_file = f 72 | ; name = test_name 73 | ; data = J.find j [ "data" ] |> j_of_data 74 | ; partials = parse_partials j 75 | ; template = J.find j [ "template" ] |> J.get_string 76 | ; expected = J.find j [ "expected" ] |> J.get_string 77 | ; desc = J.find j [ "desc" ] |> J.get_string 78 | } 79 | in 80 | J.find test_j [ "tests" ] |> J.get_list test_of_json 81 | 82 | let assert_equal x y _ = assert_equal ~printer:(fun s -> s) x y 83 | 84 | let mktest test = test.name >:: assert_equal test.expected (apply_mustache test) 85 | 86 | let specs = 87 | [ "comments.json" 88 | ; "inheritance.json" 89 | ; "interpolation.json" 90 | ; "inverted.json" 91 | ; "partials.json" 92 | ; "sections.json" 93 | ] 94 | 95 | let tests = 96 | "Mustache specs test suite" 97 | >::: List.map 98 | (fun specfile -> 99 | "Test suite from " ^ specfile 100 | >::: (load_test_file specfile |> List.map mktest)) 101 | specs 102 | 103 | let () = run_test_tt_main tests 104 | -------------------------------------------------------------------------------- /mustache/lib_test/test_mustache.ml: -------------------------------------------------------------------------------- 1 | [@@@warning "-6"] 2 | 3 | open OUnit2 4 | open Mustache 5 | module List = ListLabels 6 | module String = StringLabels 7 | 8 | (* [ ( Parser input, expected parser output, 9 | * [ (json data, expected rendering) ] ) ] *) 10 | 11 | let tests = 12 | [ ( "Hello {{ name }}!" 13 | , concat [ raw "Hello "; escaped [ "name" ]; raw "!" ] 14 | , [ (`O [ ("name", `String "testing") ], "Hello testing!") ] ) 15 | ; ( "{{#bool}}there{{/bool}}" 16 | , section [ "bool" ] (raw "there") 17 | , [ (`O [ ("bool", `Bool true) ], "there") 18 | ; (`O [ ("bool", `Bool false) ], "") 19 | ] ) 20 | ; ( "{{#implicit}}{{.}}.{{/implicit}}" 21 | , section [ "implicit" ] (concat [ escaped []; raw "." ]) 22 | , [ ( `O [ ("implicit", `A [ `String "1"; `String "2"; `String "3" ]) ] 23 | , "1.2.3." ) 24 | ; (`O [ ("implicit", `A []) ], "") 25 | ] ) 26 | ; ( "{{#things}}{{v1}}-{{v2}}{{/things}}" 27 | , section [ "things" ] 28 | (concat [ escaped [ "v1" ]; raw "-"; escaped [ "v2" ] ]) 29 | , [ ( `O 30 | [ ( "things" 31 | , `A 32 | [ `O [ ("v1", `String "1"); ("v2", `String "one") ] 33 | ; `O [ ("v1", `String "2"); ("v2", `String "two") ] 34 | ] ) 35 | ] 36 | , "1-one2-two" ) 37 | ] ) 38 | ; ( "" 39 | , concat 40 | [ raw "" 47 | ] 48 | , [ ( `O [ ("border", `String "solid") ] 49 | , "" ) 50 | ] ) 51 | ; ( "

Today{{! ignore me }}.

" 52 | , concat [ raw "

Today"; comment " ignore me "; raw ".

" ] 53 | , [ (`O [], "

Today.

") ] ) 54 | ; ( "{{ foo }}{{{ foo }}}{{& foo }}" 55 | , concat [ escaped [ "foo" ]; unescaped [ "foo" ]; unescaped [ "foo" ] ] 56 | , [ ( `O [ ("foo", `String "bar") ] 57 | , "<b>bar</b>barbar" ) 58 | ] ) 59 | ; ( "Hello {{ /foo }}!" 60 | , concat [ raw "Hello "; escaped [ "/foo" ]; raw "!" ] 61 | , [ (`O [ ("/foo", `String "World") ], "Hello World!") ] ) 62 | ; ( "{{& deep/path/string }}" 63 | , unescaped [ "deep/path/string" ] 64 | , [ ( `O [ ("deep/path/string", `String "

Test content

") ] 65 | , "

Test content

" ) 66 | ] ) 67 | ; ( "{{#things/with/slashes}}{{v/1/}}-{{v/2/}}{{/things/with/slashes}}" 68 | , section [ "things/with/slashes" ] 69 | (concat [ escaped [ "v/1/" ]; raw "-"; escaped [ "v/2/" ] ]) 70 | , [ ( `O 71 | [ ( "things/with/slashes" 72 | , `A 73 | [ `O [ ("v/1/", `String "1"); ("v/2/", `String "one") ] 74 | ; `O [ ("v/1/", `String "2"); ("v/2/", `String "two") ] 75 | ] ) 76 | ] 77 | , "1-one2-two" ) 78 | ] ) 79 | ; ( "{{#a}}{{x}}{{/a}}" 80 | , section [ "a" ] (escaped [ "x" ]) 81 | , [ ( `O [ ("a", `A [ `O [ ("x", `Float 1.) ]; `O [ ("x", `Float 2.) ] ]) ] 82 | , "12" ) 83 | ] ) 84 | ; ( "{{#a}}{{.}}{{/a}}" 85 | , section [ "a" ] (escaped []) 86 | , [ (`O [ ("a", `String "foo") ], "foo") ] ) 87 | ; ( "{{#a}}{{a}}{{/a}}" 88 | , section [ "a" ] (escaped [ "a" ]) 89 | , [ (`O [ ("a", `String "foo") ], "foo") ] ) 90 | ; ( (* check that a whitespace line is omitted if it contains (several) 91 | standalone tokens *) 92 | "Begin\n{{#foo}} {{#bar}}\nMiddle\n{{/bar}} {{/foo}}\nEnd\n" 93 | , concat 94 | [ raw "Begin\n" 95 | ; section [ "foo" ] (section [ "bar" ] (raw "Middle\n")) 96 | ; raw "End\n" 97 | ] 98 | , [ (`O [ ("foo", `O []); ("bar", `O []) ], "Begin\nMiddle\nEnd\n") ] ) 99 | ] 100 | 101 | let mkloc (lnum_s, bol_s, cnum_s, lnum_e, bol_e, cnum_e) = 102 | { With_locations.loc_start = 103 | { Lexing.pos_fname = "" 104 | ; Lexing.pos_lnum = lnum_s 105 | ; Lexing.pos_bol = bol_s 106 | ; Lexing.pos_cnum = cnum_s 107 | } 108 | ; With_locations.loc_end = 109 | { Lexing.pos_fname = "" 110 | ; Lexing.pos_lnum = lnum_e 111 | ; Lexing.pos_bol = bol_e 112 | ; Lexing.pos_cnum = cnum_e 113 | } 114 | } 115 | 116 | let tests_with_locs = 117 | With_locations. 118 | [ ( " {{# a\n }} {{ \n x }}\n {{/a}}" 119 | , concat 120 | ~loc:(mkloc (1, 0, 0, 4, 24, 31)) 121 | [ raw ~loc:(mkloc (1, 0, 0, 1, 0, 1)) " " 122 | ; section 123 | ~loc:(mkloc (1, 0, 1, 4, 24, 31)) 124 | [ "a" ] 125 | (concat 126 | ~loc:(mkloc (2, 8, 12, 4, 24, 24)) 127 | [ raw ~loc:(mkloc (2, 8, 12, 2, 8, 13)) " " 128 | ; escaped ~loc:(mkloc (2, 8, 13, 3, 17, 23)) [ "x" ] 129 | ; raw ~loc:(mkloc (3, 17, 23, 4, 24, 24)) "\n" 130 | (* raw ~loc:(mkloc (4, 24, 24, 4, 24, 25)) " " *) 131 | ]) 132 | ] 133 | , [ (`O [ ("a", `O [ ("x", `String "foo") ]) ], " foo\n") ] ) 134 | ; ("{{!x}}", comment ~loc:(mkloc (1, 0, 0, 1, 0, 6)) "x", [ (`A [], "") ]) 135 | ; ( "{{!x\ny}}" 136 | , comment ~loc:(mkloc (1, 0, 0, 2, 5, 8)) "x\ny" 137 | , [ (`A [], "") ] ) 138 | ; ( " {{!x}} " 139 | , comment ~loc:(mkloc (1, 0, 2, 1, 0, 8)) "x" 140 | , [ (`A [], "") ] ) 141 | ] 142 | 143 | let normalize : t -> t = fun t -> erase_locs (add_dummy_locs t) 144 | 145 | let () = 146 | let assert_equal ?(printer = fun _ -> "") a b _ = assert_equal ~printer a b in 147 | 148 | "Mustache test suite" 149 | >::: ( List.mapi 150 | (fun i (input, expected_parsing, rendering_tests) -> 151 | let template = 152 | try Mustache.of_string input 153 | with exn -> 154 | failwith 155 | (Printf.sprintf "Parsing of test %d failed: %s" i 156 | (Printexc.to_string exn)) 157 | in 158 | ( Printf.sprintf "%d - parsing %S" i input 159 | >:: assert_equal (normalize expected_parsing) (normalize template) 160 | ) 161 | :: List.mapi 162 | (fun j (data, expected) -> 163 | let rendered = 164 | try Mustache.render template data 165 | with exn -> 166 | failwith 167 | (Printf.sprintf "Rendering %d of test %d failed: %s" j 168 | i (Printexc.to_string exn)) 169 | in 170 | Printf.sprintf "%d - rendering (%d)" i j 171 | >:: assert_equal ~printer:(fun x -> x) expected rendered) 172 | rendering_tests) 173 | tests 174 | @ List.mapi 175 | (fun i (input, expected_parsing, rendering_tests) -> 176 | let template = 177 | try Mustache.With_locations.of_string input 178 | with exn -> 179 | failwith 180 | (Printf.sprintf 181 | "Parsing of test with locations %d failed: %s" i 182 | (Printexc.to_string exn)) 183 | in 184 | ( Printf.sprintf "%d with locations - parsing" i 185 | >:: assert_equal expected_parsing template ) 186 | :: List.mapi 187 | (fun j (data, expected) -> 188 | let rendered = 189 | try Mustache.With_locations.render template data 190 | with exn -> 191 | failwith 192 | (Printf.sprintf 193 | "Rendering %d of test with locations %d failed: \ 194 | %s" 195 | j i (Printexc.to_string exn)) 196 | in 197 | Printf.sprintf "%d with locations - rendering (%d)" i j 198 | >:: assert_equal ~printer:(fun x -> x) expected rendered) 199 | rendering_tests) 200 | tests_with_locs 201 | |> List.flatten ) 202 | |> run_test_tt_main 203 | -------------------------------------------------------------------------------- /mustache/specs/VERSION: -------------------------------------------------------------------------------- 1 | v1.1.3 -------------------------------------------------------------------------------- /mustache/specs/comments.json: -------------------------------------------------------------------------------- 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Comment tags represent content that should never appear in the resulting\noutput.\n\nThe tag's content may contain any substring (including newlines) EXCEPT the\nclosing delimiter.\n\nComment tags SHOULD be treated as standalone when appropriate.\n","tests":[{"name":"Inline","data":{},"expected":"1234567890","template":"12345{{! Comment Block! }}67890","desc":"Comment blocks should be removed from the template."},{"name":"Multiline","data":{},"expected":"1234567890\n","template":"12345{{!\n This is a\n multi-line comment...\n}}67890\n","desc":"Multiline comments should be permitted."},{"name":"Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n{{! Comment Block! }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n {{! Indented Comment Block! }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Standalone Line Endings","data":{},"expected":"|\r\n|","template":"|\r\n{{! Standalone Comment }}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{},"expected":"!","template":" {{! I'm Still Standalone }}\n!","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{},"expected":"!\n","template":"!\n {{! I'm Still Standalone }}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Multiline Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n{{!\nSomething's going on here...\n}}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Multiline Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n {{!\n Something's going on here...\n }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Inline","data":{},"expected":" 12 \n","template":" 12 {{! 34 }}\n","desc":"Inline comments should not strip whitespace"},{"name":"Surrounding Whitespace","data":{},"expected":"12345 67890","template":"12345 {{! Comment Block! }} 67890","desc":"Comment removal should preserve surrounding whitespace."}]} -------------------------------------------------------------------------------- /mustache/specs/comments.yml: -------------------------------------------------------------------------------- 1 | overview: | 2 | Comment tags represent content that should never appear in the resulting 3 | output. 4 | 5 | The tag's content may contain any substring (including newlines) EXCEPT the 6 | closing delimiter. 7 | 8 | Comment tags SHOULD be treated as standalone when appropriate. 9 | tests: 10 | - name: Inline 11 | desc: Comment blocks should be removed from the template. 12 | data: { } 13 | template: '12345{{! Comment Block! }}67890' 14 | expected: '1234567890' 15 | 16 | - name: Multiline 17 | desc: Multiline comments should be permitted. 18 | data: { } 19 | template: | 20 | 12345{{! 21 | This is a 22 | multi-line comment... 23 | }}67890 24 | expected: | 25 | 1234567890 26 | 27 | - name: Standalone 28 | desc: All standalone comment lines should be removed. 29 | data: { } 30 | template: | 31 | Begin. 32 | {{! Comment Block! }} 33 | End. 34 | expected: | 35 | Begin. 36 | End. 37 | 38 | - name: Indented Standalone 39 | desc: All standalone comment lines should be removed. 40 | data: { } 41 | template: | 42 | Begin. 43 | {{! Indented Comment Block! }} 44 | End. 45 | expected: | 46 | Begin. 47 | End. 48 | 49 | - name: Standalone Line Endings 50 | desc: '"\r\n" should be considered a newline for standalone tags.' 51 | data: { } 52 | template: "|\r\n{{! Standalone Comment }}\r\n|" 53 | expected: "|\r\n|" 54 | 55 | - name: Standalone Without Previous Line 56 | desc: Standalone tags should not require a newline to precede them. 57 | data: { } 58 | template: " {{! I'm Still Standalone }}\n!" 59 | expected: "!" 60 | 61 | - name: Standalone Without Newline 62 | desc: Standalone tags should not require a newline to follow them. 63 | data: { } 64 | template: "!\n {{! I'm Still Standalone }}" 65 | expected: "!\n" 66 | 67 | - name: Multiline Standalone 68 | desc: All standalone comment lines should be removed. 69 | data: { } 70 | template: | 71 | Begin. 72 | {{! 73 | Something's going on here... 74 | }} 75 | End. 76 | expected: | 77 | Begin. 78 | End. 79 | 80 | - name: Indented Multiline Standalone 81 | desc: All standalone comment lines should be removed. 82 | data: { } 83 | template: | 84 | Begin. 85 | {{! 86 | Something's going on here... 87 | }} 88 | End. 89 | expected: | 90 | Begin. 91 | End. 92 | 93 | - name: Indented Inline 94 | desc: Inline comments should not strip whitespace 95 | data: { } 96 | template: " 12 {{! 34 }}\n" 97 | expected: " 12 \n" 98 | 99 | - name: Surrounding Whitespace 100 | desc: Comment removal should preserve surrounding whitespace. 101 | data: { } 102 | template: '12345 {{! Comment Block! }} 67890' 103 | expected: '12345 67890' 104 | -------------------------------------------------------------------------------- /mustache/specs/inheritance.json: -------------------------------------------------------------------------------- 1 | {"overview":"Parent tags are used to expand an external template into the current template,\nwith optional parameters delimited by block tags.\n\nThese tags' content MUST be a non-whitespace character sequence NOT containing\nthe current closing delimiter; each Parent tag MUST be followed by an End\nSection tag with the same content within the matching parent tag.\n\nBlock tags are used inside of parent tags to assign data onto the context stack \nprior to rendering the parent template. Outside of parent tags, block tags are\nused to indicate where value set in the parent tag should be placed. If no value\nis set then the content in between the block tags, if any, is rendered.\n","tests":[{"name":"Default","desc":"Default content should be rendered if the block isn't overridden","data":{},"template":"{{$title}}Default title{{/title}}\n","expected":"Default title\n"},{"name":"Variable","desc":"Default content renders variables","data":{"bar":"baz"},"template":"{{$foo}}default {{bar}} content{{/foo}}\n","expected":"default baz content\n"},{"name":"Triple Mustache","desc":"Default content renders triple mustache variables","data":{"bar":""},"template":"{{$foo}}default {{{bar}}} content{{/foo}}\n","expected":"default content\n"},{"name":"Sections","desc":"Default content renders sections","data":{"bar":{"baz":"qux"}},"template":"{{$foo}}default {{#bar}}{{baz}}{{/bar}} content{{/foo}}\n","expected":"default qux content\n"},{"name":"Negative Sections","desc":"Default content renders negative sections","data":{"baz":"three"},"template":"{{$foo}}default {{^bar}}{{baz}}{{/bar}} content{{/foo}}\n","expected":"default three content\n"},{"name":"Mustache Injection","desc":"Mustache injection in default content","data":{"bar":{"baz":"{{qux}}"}},"template":"{{$foo}}default {{#bar}}{{baz}}{{/bar}} content{{/foo}}\n","expected":"default {{qux}} content\n"},{"name":"Inherit","desc":"Default content rendered inside included templates","data":{},"template":"{{include}}|{{' } 33 | template: | 34 | {{$foo}}default {{{bar}}} content{{/foo}} 35 | expected: | 36 | default content 37 | 38 | - name: Sections 39 | desc: Default content renders sections 40 | data: { bar: {baz: 'qux'} } 41 | template: | 42 | {{$foo}}default {{#bar}}{{baz}}{{/bar}} content{{/foo}} 43 | expected: | 44 | default qux content 45 | 46 | - name: Negative Sections 47 | desc: Default content renders negative sections 48 | data: { baz: 'three' } 49 | template: | 50 | {{$foo}}default {{^bar}}{{baz}}{{/bar}} content{{/foo}} 51 | expected: | 52 | default three content 53 | 54 | - name: Mustache Injection 55 | desc: Mustache injection in default content 56 | data: {bar: {baz: '{{qux}}'} } 57 | template: | 58 | {{$foo}}default {{#bar}}{{baz}}{{/bar}} content{{/foo}} 59 | expected: | 60 | default {{qux}} content 61 | 62 | - name: Inherit 63 | desc: Default content rendered inside included templates 64 | data: { } 65 | template: | 66 | {{include}}|{{"},"expected":"These characters should be HTML escaped: & " < >\n","template":"These characters should be HTML escaped: {{forbidden}}\n","desc":"Basic interpolation should be HTML escaped."},{"name":"Triple Mustache","data":{"forbidden":"& \" < >"},"expected":"These characters should not be HTML escaped: & \" < >\n","template":"These characters should not be HTML escaped: {{{forbidden}}}\n","desc":"Triple mustaches should interpolate without HTML escaping."},{"name":"Ampersand","data":{"forbidden":"& \" < >"},"expected":"These characters should not be HTML escaped: & \" < >\n","template":"These characters should not be HTML escaped: {{&forbidden}}\n","desc":"Ampersand should interpolate without HTML escaping."},{"name":"Basic Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{mph}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Triple Mustache Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{{mph}}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Ampersand Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{&mph}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Basic Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{power}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Triple Mustache Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{{power}}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Ampersand Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{&power}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Basic Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{cannot}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Triple Mustache Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{{cannot}}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Ampersand Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{&cannot}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Dotted Names - Basic Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{person.name}}\" == \"{{#person}}{{name}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Triple Mustache Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{{person.name}}}\" == \"{{#person}}{{{name}}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Ampersand Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{&person.name}}\" == \"{{#person}}{{&name}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Arbitrary Depth","data":{"a":{"b":{"c":{"d":{"e":{"name":"Phil"}}}}}},"expected":"\"Phil\" == \"Phil\"","template":"\"{{a.b.c.d.e.name}}\" == \"Phil\"","desc":"Dotted names should be functional to any level of nesting."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"\" == \"\"","template":"\"{{a.b.c}}\" == \"\"","desc":"Any falsey value prior to the last part of the name should yield ''."},{"name":"Dotted Names - Broken Chain Resolution","data":{"a":{"b":{}},"c":{"name":"Jim"}},"expected":"\"\" == \"\"","template":"\"{{a.b.c.name}}\" == \"\"","desc":"Each part of a dotted name should resolve only against its parent."},{"name":"Dotted Names - Initial Resolution","data":{"a":{"b":{"c":{"d":{"e":{"name":"Phil"}}}}},"b":{"c":{"d":{"e":{"name":"Wrong"}}}}},"expected":"\"Phil\" == \"Phil\"","template":"\"{{#a}}{{b.c.d.e.name}}{{/a}}\" == \"Phil\"","desc":"The first part of a dotted name should resolve as any other name."},{"name":"Interpolation - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{string}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Triple Mustache - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{{string}}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Ampersand - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{&string}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Interpolation - Standalone","data":{"string":"---"},"expected":" ---\n","template":" {{string}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Triple Mustache - Standalone","data":{"string":"---"},"expected":" ---\n","template":" {{{string}}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Ampersand - Standalone","data":{"string":"---"},"expected":" ---\n","template":" {{&string}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Interpolation With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{ string }}|","desc":"Superfluous in-tag whitespace should be ignored."},{"name":"Triple Mustache With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{{ string }}}|","desc":"Superfluous in-tag whitespace should be ignored."},{"name":"Ampersand With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{& string }}|","desc":"Superfluous in-tag whitespace should be ignored."}]} -------------------------------------------------------------------------------- /mustache/specs/interpolation.yml: -------------------------------------------------------------------------------- 1 | overview: | 2 | Interpolation tags are used to integrate dynamic content into the template. 3 | 4 | The tag's content MUST be a non-whitespace character sequence NOT containing 5 | the current closing delimiter. 6 | 7 | This tag's content names the data to replace the tag. A single period (`.`) 8 | indicates that the item currently sitting atop the context stack should be 9 | used; otherwise, name resolution is as follows: 10 | 1) Split the name on periods; the first part is the name to resolve, any 11 | remaining parts should be retained. 12 | 2) Walk the context stack from top to bottom, finding the first context 13 | that is a) a hash containing the name as a key OR b) an object responding 14 | to a method with the given name. 15 | 3) If the context is a hash, the data is the value associated with the 16 | name. 17 | 4) If the context is an object, the data is the value returned by the 18 | method with the given name. 19 | 5) If any name parts were retained in step 1, each should be resolved 20 | against a context stack containing only the result from the former 21 | resolution. If any part fails resolution, the result should be considered 22 | falsey, and should interpolate as the empty string. 23 | Data should be coerced into a string (and escaped, if appropriate) before 24 | interpolation. 25 | 26 | The Interpolation tags MUST NOT be treated as standalone. 27 | tests: 28 | - name: No Interpolation 29 | desc: Mustache-free templates should render as-is. 30 | data: { } 31 | template: | 32 | Hello from {Mustache}! 33 | expected: | 34 | Hello from {Mustache}! 35 | 36 | - name: Basic Interpolation 37 | desc: Unadorned tags should interpolate content into the template. 38 | data: { subject: "world" } 39 | template: | 40 | Hello, {{subject}}! 41 | expected: | 42 | Hello, world! 43 | 44 | - name: HTML Escaping 45 | desc: Basic interpolation should be HTML escaped. 46 | data: { forbidden: '& " < >' } 47 | template: | 48 | These characters should be HTML escaped: {{forbidden}} 49 | expected: | 50 | These characters should be HTML escaped: & " < > 51 | 52 | - name: Triple Mustache 53 | desc: Triple mustaches should interpolate without HTML escaping. 54 | data: { forbidden: '& " < >' } 55 | template: | 56 | These characters should not be HTML escaped: {{{forbidden}}} 57 | expected: | 58 | These characters should not be HTML escaped: & " < > 59 | 60 | - name: Ampersand 61 | desc: Ampersand should interpolate without HTML escaping. 62 | data: { forbidden: '& " < >' } 63 | template: | 64 | These characters should not be HTML escaped: {{&forbidden}} 65 | expected: | 66 | These characters should not be HTML escaped: & " < > 67 | 68 | - name: Basic Integer Interpolation 69 | desc: Integers should interpolate seamlessly. 70 | data: { mph: 85 } 71 | template: '"{{mph}} miles an hour!"' 72 | expected: '"85 miles an hour!"' 73 | 74 | - name: Triple Mustache Integer Interpolation 75 | desc: Integers should interpolate seamlessly. 76 | data: { mph: 85 } 77 | template: '"{{{mph}}} miles an hour!"' 78 | expected: '"85 miles an hour!"' 79 | 80 | - name: Ampersand Integer Interpolation 81 | desc: Integers should interpolate seamlessly. 82 | data: { mph: 85 } 83 | template: '"{{&mph}} miles an hour!"' 84 | expected: '"85 miles an hour!"' 85 | 86 | - name: Basic Decimal Interpolation 87 | desc: Decimals should interpolate seamlessly with proper significance. 88 | data: { power: 1.210 } 89 | template: '"{{power}} jiggawatts!"' 90 | expected: '"1.21 jiggawatts!"' 91 | 92 | - name: Triple Mustache Decimal Interpolation 93 | desc: Decimals should interpolate seamlessly with proper significance. 94 | data: { power: 1.210 } 95 | template: '"{{{power}}} jiggawatts!"' 96 | expected: '"1.21 jiggawatts!"' 97 | 98 | - name: Ampersand Decimal Interpolation 99 | desc: Decimals should interpolate seamlessly with proper significance. 100 | data: { power: 1.210 } 101 | template: '"{{&power}} jiggawatts!"' 102 | expected: '"1.21 jiggawatts!"' 103 | 104 | # Context Misses 105 | 106 | - name: Basic Context Miss Interpolation 107 | desc: Failed context lookups should default to empty strings. 108 | data: { } 109 | template: "I ({{cannot}}) be seen!" 110 | expected: "I () be seen!" 111 | 112 | - name: Triple Mustache Context Miss Interpolation 113 | desc: Failed context lookups should default to empty strings. 114 | data: { } 115 | template: "I ({{{cannot}}}) be seen!" 116 | expected: "I () be seen!" 117 | 118 | - name: Ampersand Context Miss Interpolation 119 | desc: Failed context lookups should default to empty strings. 120 | data: { } 121 | template: "I ({{&cannot}}) be seen!" 122 | expected: "I () be seen!" 123 | 124 | # Dotted Names 125 | 126 | - name: Dotted Names - Basic Interpolation 127 | desc: Dotted names should be considered a form of shorthand for sections. 128 | data: { person: { name: 'Joe' } } 129 | template: '"{{person.name}}" == "{{#person}}{{name}}{{/person}}"' 130 | expected: '"Joe" == "Joe"' 131 | 132 | - name: Dotted Names - Triple Mustache Interpolation 133 | desc: Dotted names should be considered a form of shorthand for sections. 134 | data: { person: { name: 'Joe' } } 135 | template: '"{{{person.name}}}" == "{{#person}}{{{name}}}{{/person}}"' 136 | expected: '"Joe" == "Joe"' 137 | 138 | - name: Dotted Names - Ampersand Interpolation 139 | desc: Dotted names should be considered a form of shorthand for sections. 140 | data: { person: { name: 'Joe' } } 141 | template: '"{{&person.name}}" == "{{#person}}{{&name}}{{/person}}"' 142 | expected: '"Joe" == "Joe"' 143 | 144 | - name: Dotted Names - Arbitrary Depth 145 | desc: Dotted names should be functional to any level of nesting. 146 | data: 147 | a: { b: { c: { d: { e: { name: 'Phil' } } } } } 148 | template: '"{{a.b.c.d.e.name}}" == "Phil"' 149 | expected: '"Phil" == "Phil"' 150 | 151 | - name: Dotted Names - Broken Chains 152 | desc: Any falsey value prior to the last part of the name should yield ''. 153 | data: 154 | a: { } 155 | template: '"{{a.b.c}}" == ""' 156 | expected: '"" == ""' 157 | 158 | - name: Dotted Names - Broken Chain Resolution 159 | desc: Each part of a dotted name should resolve only against its parent. 160 | data: 161 | a: { b: { } } 162 | c: { name: 'Jim' } 163 | template: '"{{a.b.c.name}}" == ""' 164 | expected: '"" == ""' 165 | 166 | - name: Dotted Names - Initial Resolution 167 | desc: The first part of a dotted name should resolve as any other name. 168 | data: 169 | a: { b: { c: { d: { e: { name: 'Phil' } } } } } 170 | b: { c: { d: { e: { name: 'Wrong' } } } } 171 | template: '"{{#a}}{{b.c.d.e.name}}{{/a}}" == "Phil"' 172 | expected: '"Phil" == "Phil"' 173 | 174 | - name: Dotted Names - Context Precedence 175 | desc: Dotted names should be resolved against former resolutions. 176 | data: 177 | a: { b: { } } 178 | b: { c: 'ERROR' } 179 | template: '{{#a}}{{b.c}}{{/a}}' 180 | expected: '' 181 | 182 | # Whitespace Sensitivity 183 | 184 | - name: Interpolation - Surrounding Whitespace 185 | desc: Interpolation should not alter surrounding whitespace. 186 | data: { string: '---' } 187 | template: '| {{string}} |' 188 | expected: '| --- |' 189 | 190 | - name: Triple Mustache - Surrounding Whitespace 191 | desc: Interpolation should not alter surrounding whitespace. 192 | data: { string: '---' } 193 | template: '| {{{string}}} |' 194 | expected: '| --- |' 195 | 196 | - name: Ampersand - Surrounding Whitespace 197 | desc: Interpolation should not alter surrounding whitespace. 198 | data: { string: '---' } 199 | template: '| {{&string}} |' 200 | expected: '| --- |' 201 | 202 | - name: Interpolation - Standalone 203 | desc: Standalone interpolation should not alter surrounding whitespace. 204 | data: { string: '---' } 205 | template: " {{string}}\n" 206 | expected: " ---\n" 207 | 208 | - name: Triple Mustache - Standalone 209 | desc: Standalone interpolation should not alter surrounding whitespace. 210 | data: { string: '---' } 211 | template: " {{{string}}}\n" 212 | expected: " ---\n" 213 | 214 | - name: Ampersand - Standalone 215 | desc: Standalone interpolation should not alter surrounding whitespace. 216 | data: { string: '---' } 217 | template: " {{&string}}\n" 218 | expected: " ---\n" 219 | 220 | # Whitespace Insensitivity 221 | 222 | - name: Interpolation With Padding 223 | desc: Superfluous in-tag whitespace should be ignored. 224 | data: { string: "---" } 225 | template: '|{{ string }}|' 226 | expected: '|---|' 227 | 228 | - name: Triple Mustache With Padding 229 | desc: Superfluous in-tag whitespace should be ignored. 230 | data: { string: "---" } 231 | template: '|{{{ string }}}|' 232 | expected: '|---|' 233 | 234 | - name: Ampersand With Padding 235 | desc: Superfluous in-tag whitespace should be ignored. 236 | data: { string: "---" } 237 | template: '|{{& string }}|' 238 | expected: '|---|' 239 | -------------------------------------------------------------------------------- /mustache/specs/inverted.json: -------------------------------------------------------------------------------- 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Inverted Section tags and End Section tags are used in combination to wrap a\nsection of the template.\n\nThese tags' content MUST be a non-whitespace character sequence NOT\ncontaining the current closing delimiter; each Inverted Section tag MUST be\nfollowed by an End Section tag with the same content within the same\nsection.\n\nThis tag's content names the data to replace the tag. Name resolution is as\nfollows:\n 1) Split the name on periods; the first part is the name to resolve, any\n remaining parts should be retained.\n 2) Walk the context stack from top to bottom, finding the first context\n that is a) a hash containing the name as a key OR b) an object responding\n to a method with the given name.\n 3) If the context is a hash, the data is the value associated with the\n name.\n 4) If the context is an object and the method with the given name has an\n arity of 1, the method SHOULD be called with a String containing the\n unprocessed contents of the sections; the data is the value returned.\n 5) Otherwise, the data is the value returned by calling the method with\n the given name.\n 6) If any name parts were retained in step 1, each should be resolved\n against a context stack containing only the result from the former\n resolution. If any part fails resolution, the result should be considered\n falsey, and should interpolate as the empty string.\nIf the data is not of a list type, it is coerced into a list as follows: if\nthe data is truthy (e.g. `!!data == true`), use a single-element list\ncontaining the data, otherwise use an empty list.\n\nThis section MUST NOT be rendered unless the data list is empty.\n\nInverted Section and End Section tags SHOULD be treated as standalone when\nappropriate.\n","tests":[{"name":"Falsey","data":{"boolean":false},"expected":"\"This should be rendered.\"","template":"\"{{^boolean}}This should be rendered.{{/boolean}}\"","desc":"Falsey sections should have their contents rendered."},{"name":"Truthy","data":{"boolean":true},"expected":"\"\"","template":"\"{{^boolean}}This should not be rendered.{{/boolean}}\"","desc":"Truthy sections should have their contents omitted."},{"name":"Context","data":{"context":{"name":"Joe"}},"expected":"\"\"","template":"\"{{^context}}Hi {{name}}.{{/context}}\"","desc":"Objects and hashes should behave like truthy values."},{"name":"List","data":{"list":[{"n":1},{"n":2},{"n":3}]},"expected":"\"\"","template":"\"{{^list}}{{n}}{{/list}}\"","desc":"Lists should behave like truthy values."},{"name":"Empty List","data":{"list":[]},"expected":"\"Yay lists!\"","template":"\"{{^list}}Yay lists!{{/list}}\"","desc":"Empty lists should behave like falsey values."},{"name":"Doubled","data":{"two":"second","bool":false},"expected":"* first\n* second\n* third\n","template":"{{^bool}}\n* first\n{{/bool}}\n* {{two}}\n{{^bool}}\n* third\n{{/bool}}\n","desc":"Multiple inverted sections per template should be permitted."},{"name":"Nested (Falsey)","data":{"bool":false},"expected":"| A B C D E |","template":"| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested falsey sections should have their contents rendered."},{"name":"Nested (Truthy)","data":{"bool":true},"expected":"| A E |","template":"| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested truthy sections should be omitted."},{"name":"Context Misses","data":{},"expected":"[Cannot find key 'missing'!]","template":"[{{^missing}}Cannot find key 'missing'!{{/missing}}]","desc":"Failed context lookups should be considered falsey."},{"name":"Dotted Names - Truthy","data":{"a":{"b":{"c":true}}},"expected":"\"\" == \"\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"\"","desc":"Dotted names should be valid for Inverted Section tags."},{"name":"Dotted Names - Falsey","data":{"a":{"b":{"c":false}}},"expected":"\"Not Here\" == \"Not Here\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"Not Here\"","desc":"Dotted names should be valid for Inverted Section tags."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"Not Here\" == \"Not Here\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"Not Here\"","desc":"Dotted names that cannot be resolved should be considered falsey."},{"name":"Surrounding Whitespace","data":{"boolean":false},"expected":" | \t|\t | \n","template":" | {{^boolean}}\t|\t{{/boolean}} | \n","desc":"Inverted sections should not alter surrounding whitespace."},{"name":"Internal Whitespace","data":{"boolean":false},"expected":" | \n | \n","template":" | {{^boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n","desc":"Inverted should not alter internal whitespace."},{"name":"Indented Inline Sections","data":{"boolean":false},"expected":" NO\n WAY\n","template":" {{^boolean}}NO{{/boolean}}\n {{^boolean}}WAY{{/boolean}}\n","desc":"Single-line sections should not alter surrounding whitespace."},{"name":"Standalone Lines","data":{"boolean":false},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n{{^boolean}}\n|\n{{/boolean}}\n| A Line\n","desc":"Standalone lines should be removed from the template."},{"name":"Standalone Indented Lines","data":{"boolean":false},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n {{^boolean}}\n|\n {{/boolean}}\n| A Line\n","desc":"Standalone indented lines should be removed from the template."},{"name":"Standalone Line Endings","data":{"boolean":false},"expected":"|\r\n|","template":"|\r\n{{^boolean}}\r\n{{/boolean}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{"boolean":false},"expected":"^\n/","template":" {{^boolean}}\n^{{/boolean}}\n/","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{"boolean":false},"expected":"^\n/\n","template":"^{{^boolean}}\n/\n {{/boolean}}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Padding","data":{"boolean":false},"expected":"|=|","template":"|{{^ boolean }}={{/ boolean }}|","desc":"Superfluous in-tag whitespace should be ignored."}]} -------------------------------------------------------------------------------- /mustache/specs/inverted.yml: -------------------------------------------------------------------------------- 1 | overview: | 2 | Inverted Section tags and End Section tags are used in combination to wrap a 3 | section of the template. 4 | 5 | These tags' content MUST be a non-whitespace character sequence NOT 6 | containing the current closing delimiter; each Inverted Section tag MUST be 7 | followed by an End Section tag with the same content within the same 8 | section. 9 | 10 | This tag's content names the data to replace the tag. Name resolution is as 11 | follows: 12 | 1) Split the name on periods; the first part is the name to resolve, any 13 | remaining parts should be retained. 14 | 2) Walk the context stack from top to bottom, finding the first context 15 | that is a) a hash containing the name as a key OR b) an object responding 16 | to a method with the given name. 17 | 3) If the context is a hash, the data is the value associated with the 18 | name. 19 | 4) If the context is an object and the method with the given name has an 20 | arity of 1, the method SHOULD be called with a String containing the 21 | unprocessed contents of the sections; the data is the value returned. 22 | 5) Otherwise, the data is the value returned by calling the method with 23 | the given name. 24 | 6) If any name parts were retained in step 1, each should be resolved 25 | against a context stack containing only the result from the former 26 | resolution. If any part fails resolution, the result should be considered 27 | falsey, and should interpolate as the empty string. 28 | If the data is not of a list type, it is coerced into a list as follows: if 29 | the data is truthy (e.g. `!!data == true`), use a single-element list 30 | containing the data, otherwise use an empty list. 31 | 32 | This section MUST NOT be rendered unless the data list is empty. 33 | 34 | Inverted Section and End Section tags SHOULD be treated as standalone when 35 | appropriate. 36 | tests: 37 | - name: Falsey 38 | desc: Falsey sections should have their contents rendered. 39 | data: { boolean: false } 40 | template: '"{{^boolean}}This should be rendered.{{/boolean}}"' 41 | expected: '"This should be rendered."' 42 | 43 | - name: Truthy 44 | desc: Truthy sections should have their contents omitted. 45 | data: { boolean: true } 46 | template: '"{{^boolean}}This should not be rendered.{{/boolean}}"' 47 | expected: '""' 48 | 49 | - name: Context 50 | desc: Objects and hashes should behave like truthy values. 51 | data: { context: { name: 'Joe' } } 52 | template: '"{{^context}}Hi {{name}}.{{/context}}"' 53 | expected: '""' 54 | 55 | - name: List 56 | desc: Lists should behave like truthy values. 57 | data: { list: [ { n: 1 }, { n: 2 }, { n: 3 } ] } 58 | template: '"{{^list}}{{n}}{{/list}}"' 59 | expected: '""' 60 | 61 | - name: Empty List 62 | desc: Empty lists should behave like falsey values. 63 | data: { list: [ ] } 64 | template: '"{{^list}}Yay lists!{{/list}}"' 65 | expected: '"Yay lists!"' 66 | 67 | - name: Doubled 68 | desc: Multiple inverted sections per template should be permitted. 69 | data: { bool: false, two: 'second' } 70 | template: | 71 | {{^bool}} 72 | * first 73 | {{/bool}} 74 | * {{two}} 75 | {{^bool}} 76 | * third 77 | {{/bool}} 78 | expected: | 79 | * first 80 | * second 81 | * third 82 | 83 | - name: Nested (Falsey) 84 | desc: Nested falsey sections should have their contents rendered. 85 | data: { bool: false } 86 | template: "| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |" 87 | expected: "| A B C D E |" 88 | 89 | - name: Nested (Truthy) 90 | desc: Nested truthy sections should be omitted. 91 | data: { bool: true } 92 | template: "| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |" 93 | expected: "| A E |" 94 | 95 | - name: Context Misses 96 | desc: Failed context lookups should be considered falsey. 97 | data: { } 98 | template: "[{{^missing}}Cannot find key 'missing'!{{/missing}}]" 99 | expected: "[Cannot find key 'missing'!]" 100 | 101 | # Dotted Names 102 | 103 | - name: Dotted Names - Truthy 104 | desc: Dotted names should be valid for Inverted Section tags. 105 | data: { a: { b: { c: true } } } 106 | template: '"{{^a.b.c}}Not Here{{/a.b.c}}" == ""' 107 | expected: '"" == ""' 108 | 109 | - name: Dotted Names - Falsey 110 | desc: Dotted names should be valid for Inverted Section tags. 111 | data: { a: { b: { c: false } } } 112 | template: '"{{^a.b.c}}Not Here{{/a.b.c}}" == "Not Here"' 113 | expected: '"Not Here" == "Not Here"' 114 | 115 | - name: Dotted Names - Broken Chains 116 | desc: Dotted names that cannot be resolved should be considered falsey. 117 | data: { a: { } } 118 | template: '"{{^a.b.c}}Not Here{{/a.b.c}}" == "Not Here"' 119 | expected: '"Not Here" == "Not Here"' 120 | 121 | # Whitespace Sensitivity 122 | 123 | - name: Surrounding Whitespace 124 | desc: Inverted sections should not alter surrounding whitespace. 125 | data: { boolean: false } 126 | template: " | {{^boolean}}\t|\t{{/boolean}} | \n" 127 | expected: " | \t|\t | \n" 128 | 129 | - name: Internal Whitespace 130 | desc: Inverted should not alter internal whitespace. 131 | data: { boolean: false } 132 | template: " | {{^boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n" 133 | expected: " | \n | \n" 134 | 135 | - name: Indented Inline Sections 136 | desc: Single-line sections should not alter surrounding whitespace. 137 | data: { boolean: false } 138 | template: " {{^boolean}}NO{{/boolean}}\n {{^boolean}}WAY{{/boolean}}\n" 139 | expected: " NO\n WAY\n" 140 | 141 | - name: Standalone Lines 142 | desc: Standalone lines should be removed from the template. 143 | data: { boolean: false } 144 | template: | 145 | | This Is 146 | {{^boolean}} 147 | | 148 | {{/boolean}} 149 | | A Line 150 | expected: | 151 | | This Is 152 | | 153 | | A Line 154 | 155 | - name: Standalone Indented Lines 156 | desc: Standalone indented lines should be removed from the template. 157 | data: { boolean: false } 158 | template: | 159 | | This Is 160 | {{^boolean}} 161 | | 162 | {{/boolean}} 163 | | A Line 164 | expected: | 165 | | This Is 166 | | 167 | | A Line 168 | 169 | - name: Standalone Line Endings 170 | desc: '"\r\n" should be considered a newline for standalone tags.' 171 | data: { boolean: false } 172 | template: "|\r\n{{^boolean}}\r\n{{/boolean}}\r\n|" 173 | expected: "|\r\n|" 174 | 175 | - name: Standalone Without Previous Line 176 | desc: Standalone tags should not require a newline to precede them. 177 | data: { boolean: false } 178 | template: " {{^boolean}}\n^{{/boolean}}\n/" 179 | expected: "^\n/" 180 | 181 | - name: Standalone Without Newline 182 | desc: Standalone tags should not require a newline to follow them. 183 | data: { boolean: false } 184 | template: "^{{^boolean}}\n/\n {{/boolean}}" 185 | expected: "^\n/\n" 186 | 187 | # Whitespace Insensitivity 188 | 189 | - name: Padding 190 | desc: Superfluous in-tag whitespace should be ignored. 191 | data: { boolean: false } 192 | template: '|{{^ boolean }}={{/ boolean }}|' 193 | expected: '|=|' 194 | -------------------------------------------------------------------------------- /mustache/specs/partials.json: -------------------------------------------------------------------------------- 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Partial tags are used to expand an external template into the current\ntemplate.\n\nThe tag's content MUST be a non-whitespace character sequence NOT containing\nthe current closing delimiter.\n\nThis tag's content names the partial to inject. Set Delimiter tags MUST NOT\naffect the parsing of a partial. The partial MUST be rendered against the\ncontext stack local to the tag. If the named partial cannot be found, the\nempty string SHOULD be used instead, as in interpolations.\n\nPartial tags SHOULD be treated as standalone when appropriate. If this tag\nis used standalone, any whitespace preceding the tag should treated as\nindentation, and prepended to each line of the partial before rendering.\n","tests":[{"name":"Basic Behavior","data":{},"expected":"\"from partial\"","template":"\"{{>text}}\"","desc":"The greater-than operator should expand to the named partial.","partials":{"text":"from partial"}},{"name":"Failed Lookup","data":{},"expected":"\"\"","template":"\"{{>text}}\"","desc":"The empty string should be used when the named partial is not found.","partials":{}},{"name":"Context","data":{"text":"content"},"expected":"\"*content*\"","template":"\"{{>partial}}\"","desc":"The greater-than operator should operate within the current context.","partials":{"partial":"*{{text}}*"}},{"name":"Recursion","data":{"content":"X","nodes":[{"content":"Y","nodes":[]}]},"expected":"X>","template":"{{>node}}","desc":"The greater-than operator should properly recurse.","partials":{"node":"{{content}}<{{#nodes}}{{>node}}{{/nodes}}>"}},{"name":"Surrounding Whitespace","data":{},"expected":"| \t|\t |","template":"| {{>partial}} |","desc":"The greater-than operator should not alter surrounding whitespace.","partials":{"partial":"\t|\t"}},{"name":"Inline Indentation","data":{"data":"|"},"expected":" | >\n>\n","template":" {{data}} {{> partial}}\n","desc":"Whitespace should be left untouched.","partials":{"partial":">\n>"}},{"name":"Standalone Line Endings","data":{},"expected":"|\r\n>|","template":"|\r\n{{>partial}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags.","partials":{"partial":">"}},{"name":"Standalone Without Previous Line","data":{},"expected":" >\n >>","template":" {{>partial}}\n>","desc":"Standalone tags should not require a newline to precede them.","partials":{"partial":">\n>"}},{"name":"Standalone Without Newline","data":{},"expected":">\n >\n >","template":">\n {{>partial}}","desc":"Standalone tags should not require a newline to follow them.","partials":{"partial":">\n>"}},{"name":"Standalone Indentation","data":{"content":"<\n->"},"expected":"\\\n |\n <\n->\n |\n/\n","template":"\\\n {{>partial}}\n/\n","desc":"Each line of the partial should be indented before rendering.","partials":{"partial":"|\n{{{content}}}\n|\n"}},{"name":"Padding Whitespace","data":{"boolean":true},"expected":"|[]|","template":"|{{> partial }}|","desc":"Superfluous in-tag whitespace should be ignored.","partials":{"partial":"[]"}}]} -------------------------------------------------------------------------------- /mustache/specs/partials.yml: -------------------------------------------------------------------------------- 1 | overview: | 2 | Partial tags are used to expand an external template into the current 3 | template. 4 | 5 | The tag's content MUST be a non-whitespace character sequence NOT containing 6 | the current closing delimiter. 7 | 8 | This tag's content names the partial to inject. Set Delimiter tags MUST NOT 9 | affect the parsing of a partial. The partial MUST be rendered against the 10 | context stack local to the tag. If the named partial cannot be found, the 11 | empty string SHOULD be used instead, as in interpolations. 12 | 13 | Partial tags SHOULD be treated as standalone when appropriate. If this tag 14 | is used standalone, any whitespace preceding the tag should treated as 15 | indentation, and prepended to each line of the partial before rendering. 16 | tests: 17 | - name: Basic Behavior 18 | desc: The greater-than operator should expand to the named partial. 19 | data: { } 20 | template: '"{{>text}}"' 21 | partials: { text: 'from partial' } 22 | expected: '"from partial"' 23 | 24 | - name: Failed Lookup 25 | desc: The empty string should be used when the named partial is not found. 26 | data: { } 27 | template: '"{{>text}}"' 28 | partials: { } 29 | expected: '""' 30 | 31 | - name: Context 32 | desc: The greater-than operator should operate within the current context. 33 | data: { text: 'content' } 34 | template: '"{{>partial}}"' 35 | partials: { partial: '*{{text}}*' } 36 | expected: '"*content*"' 37 | 38 | - name: Recursion 39 | desc: The greater-than operator should properly recurse. 40 | data: { content: "X", nodes: [ { content: "Y", nodes: [] } ] } 41 | template: '{{>node}}' 42 | partials: { node: '{{content}}<{{#nodes}}{{>node}}{{/nodes}}>' } 43 | expected: 'X>' 44 | 45 | # Whitespace Sensitivity 46 | 47 | - name: Surrounding Whitespace 48 | desc: The greater-than operator should not alter surrounding whitespace. 49 | data: { } 50 | template: '| {{>partial}} |' 51 | partials: { partial: "\t|\t" } 52 | expected: "| \t|\t |" 53 | 54 | - name: Inline Indentation 55 | desc: Whitespace should be left untouched. 56 | data: { data: '|' } 57 | template: " {{data}} {{> partial}}\n" 58 | partials: { partial: ">\n>" } 59 | expected: " | >\n>\n" 60 | 61 | - name: Standalone Line Endings 62 | desc: '"\r\n" should be considered a newline for standalone tags.' 63 | data: { } 64 | template: "|\r\n{{>partial}}\r\n|" 65 | partials: { partial: ">" } 66 | expected: "|\r\n>|" 67 | 68 | - name: Standalone Without Previous Line 69 | desc: Standalone tags should not require a newline to precede them. 70 | data: { } 71 | template: " {{>partial}}\n>" 72 | partials: { partial: ">\n>"} 73 | expected: " >\n >>" 74 | 75 | - name: Standalone Without Newline 76 | desc: Standalone tags should not require a newline to follow them. 77 | data: { } 78 | template: ">\n {{>partial}}" 79 | partials: { partial: ">\n>" } 80 | expected: ">\n >\n >" 81 | 82 | - name: Standalone Indentation 83 | desc: Each line of the partial should be indented before rendering. 84 | data: { content: "<\n->" } 85 | template: | 86 | \ 87 | {{>partial}} 88 | / 89 | partials: 90 | partial: | 91 | | 92 | {{{content}}} 93 | | 94 | expected: | 95 | \ 96 | | 97 | < 98 | -> 99 | | 100 | / 101 | 102 | # Whitespace Insensitivity 103 | 104 | - name: Padding Whitespace 105 | desc: Superfluous in-tag whitespace should be ignored. 106 | data: { boolean: true } 107 | template: "|{{> partial }}|" 108 | partials: { partial: "[]" } 109 | expected: '|[]|' 110 | -------------------------------------------------------------------------------- /mustache/specs/sections.json: -------------------------------------------------------------------------------- 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Section tags and End Section tags are used in combination to wrap a section\nof the template for iteration\n\nThese tags' content MUST be a non-whitespace character sequence NOT\ncontaining the current closing delimiter; each Section tag MUST be followed\nby an End Section tag with the same content within the same section.\n\nThis tag's content names the data to replace the tag. Name resolution is as\nfollows:\n 1) Split the name on periods; the first part is the name to resolve, any\n remaining parts should be retained.\n 2) Walk the context stack from top to bottom, finding the first context\n that is a) a hash containing the name as a key OR b) an object responding\n to a method with the given name.\n 3) If the context is a hash, the data is the value associated with the\n name.\n 4) If the context is an object and the method with the given name has an\n arity of 1, the method SHOULD be called with a String containing the\n unprocessed contents of the sections; the data is the value returned.\n 5) Otherwise, the data is the value returned by calling the method with\n the given name.\n 6) If any name parts were retained in step 1, each should be resolved\n against a context stack containing only the result from the former\n resolution. If any part fails resolution, the result should be considered\n falsey, and should interpolate as the empty string.\nIf the data is not of a list type, it is coerced into a list as follows: if\nthe data is truthy (e.g. `!!data == true`), use a single-element list\ncontaining the data, otherwise use an empty list.\n\nFor each element in the data list, the element MUST be pushed onto the\ncontext stack, the section MUST be rendered, and the element MUST be popped\noff the context stack.\n\nSection and End Section tags SHOULD be treated as standalone when\nappropriate.\n","tests":[{"name":"Truthy","data":{"boolean":true},"expected":"\"This should be rendered.\"","template":"\"{{#boolean}}This should be rendered.{{/boolean}}\"","desc":"Truthy sections should have their contents rendered."},{"name":"Falsey","data":{"boolean":false},"expected":"\"\"","template":"\"{{#boolean}}This should not be rendered.{{/boolean}}\"","desc":"Falsey sections should have their contents omitted."},{"name":"Context","data":{"context":{"name":"Joe"}},"expected":"\"Hi Joe.\"","template":"\"{{#context}}Hi {{name}}.{{/context}}\"","desc":"Objects and hashes should be pushed onto the context stack."},{"name":"Deeply Nested Contexts","data":{"a":{"one":1},"b":{"two":2},"c":{"three":3},"d":{"four":4},"e":{"five":5}},"expected":"1\n121\n12321\n1234321\n123454321\n1234321\n12321\n121\n1\n","template":"{{#a}}\n{{one}}\n{{#b}}\n{{one}}{{two}}{{one}}\n{{#c}}\n{{one}}{{two}}{{three}}{{two}}{{one}}\n{{#d}}\n{{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}\n{{#e}}\n{{one}}{{two}}{{three}}{{four}}{{five}}{{four}}{{three}}{{two}}{{one}}\n{{/e}}\n{{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}\n{{/d}}\n{{one}}{{two}}{{three}}{{two}}{{one}}\n{{/c}}\n{{one}}{{two}}{{one}}\n{{/b}}\n{{one}}\n{{/a}}\n","desc":"All elements on the context stack should be accessible."},{"name":"List","data":{"list":[{"item":1},{"item":2},{"item":3}]},"expected":"\"123\"","template":"\"{{#list}}{{item}}{{/list}}\"","desc":"Lists should be iterated; list items should visit the context stack."},{"name":"Empty List","data":{"list":[]},"expected":"\"\"","template":"\"{{#list}}Yay lists!{{/list}}\"","desc":"Empty lists should behave like falsey values."},{"name":"Doubled","data":{"two":"second","bool":true},"expected":"* first\n* second\n* third\n","template":"{{#bool}}\n* first\n{{/bool}}\n* {{two}}\n{{#bool}}\n* third\n{{/bool}}\n","desc":"Multiple sections per template should be permitted."},{"name":"Nested (Truthy)","data":{"bool":true},"expected":"| A B C D E |","template":"| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested truthy sections should have their contents rendered."},{"name":"Nested (Falsey)","data":{"bool":false},"expected":"| A E |","template":"| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested falsey sections should be omitted."},{"name":"Context Misses","data":{},"expected":"[]","template":"[{{#missing}}Found key 'missing'!{{/missing}}]","desc":"Failed context lookups should be considered falsey."},{"name":"Implicit Iterator - String","data":{"list":["a","b","c","d","e"]},"expected":"\"(a)(b)(c)(d)(e)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should directly interpolate strings."},{"name":"Implicit Iterator - Integer","data":{"list":[1,2,3,4,5]},"expected":"\"(1)(2)(3)(4)(5)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should cast integers to strings and interpolate."},{"name":"Implicit Iterator - Decimal","data":{"list":[1.1,2.2,3.3,4.4,5.5]},"expected":"\"(1.1)(2.2)(3.3)(4.4)(5.5)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should cast decimals to strings and interpolate."},{"name":"Implicit Iterator - Array","desc":"Implicit iterators should allow iterating over nested arrays.","data":{"list":[[1,2,3],["a","b","c"]]},"template":"\"{{#list}}({{#.}}{{.}}{{/.}}){{/list}}\"","expected":"\"(123)(abc)\""},{"name":"Dotted Names - Truthy","data":{"a":{"b":{"c":true}}},"expected":"\"Here\" == \"Here\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"Here\"","desc":"Dotted names should be valid for Section tags."},{"name":"Dotted Names - Falsey","data":{"a":{"b":{"c":false}}},"expected":"\"\" == \"\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"\"","desc":"Dotted names should be valid for Section tags."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"\" == \"\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"\"","desc":"Dotted names that cannot be resolved should be considered falsey."},{"name":"Surrounding Whitespace","data":{"boolean":true},"expected":" | \t|\t | \n","template":" | {{#boolean}}\t|\t{{/boolean}} | \n","desc":"Sections should not alter surrounding whitespace."},{"name":"Internal Whitespace","data":{"boolean":true},"expected":" | \n | \n","template":" | {{#boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n","desc":"Sections should not alter internal whitespace."},{"name":"Indented Inline Sections","data":{"boolean":true},"expected":" YES\n GOOD\n","template":" {{#boolean}}YES{{/boolean}}\n {{#boolean}}GOOD{{/boolean}}\n","desc":"Single-line sections should not alter surrounding whitespace."},{"name":"Standalone Lines","data":{"boolean":true},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n{{#boolean}}\n|\n{{/boolean}}\n| A Line\n","desc":"Standalone lines should be removed from the template."},{"name":"Indented Standalone Lines","data":{"boolean":true},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n {{#boolean}}\n|\n {{/boolean}}\n| A Line\n","desc":"Indented standalone lines should be removed from the template."},{"name":"Standalone Line Endings","data":{"boolean":true},"expected":"|\r\n|","template":"|\r\n{{#boolean}}\r\n{{/boolean}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{"boolean":true},"expected":"#\n/","template":" {{#boolean}}\n#{{/boolean}}\n/","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{"boolean":true},"expected":"#\n/\n","template":"#{{#boolean}}\n/\n {{/boolean}}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Padding","data":{"boolean":true},"expected":"|=|","template":"|{{# boolean }}={{/ boolean }}|","desc":"Superfluous in-tag whitespace should be ignored."}]} -------------------------------------------------------------------------------- /mustache/specs/sections.yml: -------------------------------------------------------------------------------- 1 | overview: | 2 | Section tags and End Section tags are used in combination to wrap a section 3 | of the template for iteration 4 | 5 | These tags' content MUST be a non-whitespace character sequence NOT 6 | containing the current closing delimiter; each Section tag MUST be followed 7 | by an End Section tag with the same content within the same section. 8 | 9 | This tag's content names the data to replace the tag. Name resolution is as 10 | follows: 11 | 1) Split the name on periods; the first part is the name to resolve, any 12 | remaining parts should be retained. 13 | 2) Walk the context stack from top to bottom, finding the first context 14 | that is a) a hash containing the name as a key OR b) an object responding 15 | to a method with the given name. 16 | 3) If the context is a hash, the data is the value associated with the 17 | name. 18 | 4) If the context is an object and the method with the given name has an 19 | arity of 1, the method SHOULD be called with a String containing the 20 | unprocessed contents of the sections; the data is the value returned. 21 | 5) Otherwise, the data is the value returned by calling the method with 22 | the given name. 23 | 6) If any name parts were retained in step 1, each should be resolved 24 | against a context stack containing only the result from the former 25 | resolution. If any part fails resolution, the result should be considered 26 | falsey, and should interpolate as the empty string. 27 | If the data is not of a list type, it is coerced into a list as follows: if 28 | the data is truthy (e.g. `!!data == true`), use a single-element list 29 | containing the data, otherwise use an empty list. 30 | 31 | For each element in the data list, the element MUST be pushed onto the 32 | context stack, the section MUST be rendered, and the element MUST be popped 33 | off the context stack. 34 | 35 | Section and End Section tags SHOULD be treated as standalone when 36 | appropriate. 37 | tests: 38 | - name: Truthy 39 | desc: Truthy sections should have their contents rendered. 40 | data: { boolean: true } 41 | template: '"{{#boolean}}This should be rendered.{{/boolean}}"' 42 | expected: '"This should be rendered."' 43 | 44 | - name: Falsey 45 | desc: Falsey sections should have their contents omitted. 46 | data: { boolean: false } 47 | template: '"{{#boolean}}This should not be rendered.{{/boolean}}"' 48 | expected: '""' 49 | 50 | - name: Context 51 | desc: Objects and hashes should be pushed onto the context stack. 52 | data: { context: { name: 'Joe' } } 53 | template: '"{{#context}}Hi {{name}}.{{/context}}"' 54 | expected: '"Hi Joe."' 55 | 56 | - name: Deeply Nested Contexts 57 | desc: All elements on the context stack should be accessible. 58 | data: 59 | a: { one: 1 } 60 | b: { two: 2 } 61 | c: { three: 3 } 62 | d: { four: 4 } 63 | e: { five: 5 } 64 | template: | 65 | {{#a}} 66 | {{one}} 67 | {{#b}} 68 | {{one}}{{two}}{{one}} 69 | {{#c}} 70 | {{one}}{{two}}{{three}}{{two}}{{one}} 71 | {{#d}} 72 | {{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}} 73 | {{#e}} 74 | {{one}}{{two}}{{three}}{{four}}{{five}}{{four}}{{three}}{{two}}{{one}} 75 | {{/e}} 76 | {{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}} 77 | {{/d}} 78 | {{one}}{{two}}{{three}}{{two}}{{one}} 79 | {{/c}} 80 | {{one}}{{two}}{{one}} 81 | {{/b}} 82 | {{one}} 83 | {{/a}} 84 | expected: | 85 | 1 86 | 121 87 | 12321 88 | 1234321 89 | 123454321 90 | 1234321 91 | 12321 92 | 121 93 | 1 94 | 95 | - name: List 96 | desc: Lists should be iterated; list items should visit the context stack. 97 | data: { list: [ { item: 1 }, { item: 2 }, { item: 3 } ] } 98 | template: '"{{#list}}{{item}}{{/list}}"' 99 | expected: '"123"' 100 | 101 | - name: Empty List 102 | desc: Empty lists should behave like falsey values. 103 | data: { list: [ ] } 104 | template: '"{{#list}}Yay lists!{{/list}}"' 105 | expected: '""' 106 | 107 | - name: Doubled 108 | desc: Multiple sections per template should be permitted. 109 | data: { bool: true, two: 'second' } 110 | template: | 111 | {{#bool}} 112 | * first 113 | {{/bool}} 114 | * {{two}} 115 | {{#bool}} 116 | * third 117 | {{/bool}} 118 | expected: | 119 | * first 120 | * second 121 | * third 122 | 123 | - name: Nested (Truthy) 124 | desc: Nested truthy sections should have their contents rendered. 125 | data: { bool: true } 126 | template: "| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |" 127 | expected: "| A B C D E |" 128 | 129 | - name: Nested (Falsey) 130 | desc: Nested falsey sections should be omitted. 131 | data: { bool: false } 132 | template: "| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |" 133 | expected: "| A E |" 134 | 135 | - name: Context Misses 136 | desc: Failed context lookups should be considered falsey. 137 | data: { } 138 | template: "[{{#missing}}Found key 'missing'!{{/missing}}]" 139 | expected: "[]" 140 | 141 | # Implicit Iterators 142 | 143 | - name: Implicit Iterator - String 144 | desc: Implicit iterators should directly interpolate strings. 145 | data: 146 | list: [ 'a', 'b', 'c', 'd', 'e' ] 147 | template: '"{{#list}}({{.}}){{/list}}"' 148 | expected: '"(a)(b)(c)(d)(e)"' 149 | 150 | - name: Implicit Iterator - Integer 151 | desc: Implicit iterators should cast integers to strings and interpolate. 152 | data: 153 | list: [ 1, 2, 3, 4, 5 ] 154 | template: '"{{#list}}({{.}}){{/list}}"' 155 | expected: '"(1)(2)(3)(4)(5)"' 156 | 157 | - name: Implicit Iterator - Decimal 158 | desc: Implicit iterators should cast decimals to strings and interpolate. 159 | data: 160 | list: [ 1.10, 2.20, 3.30, 4.40, 5.50 ] 161 | template: '"{{#list}}({{.}}){{/list}}"' 162 | expected: '"(1.1)(2.2)(3.3)(4.4)(5.5)"' 163 | 164 | - name: Implicit Iterator - Array 165 | desc: Implicit iterators should allow iterating over nested arrays. 166 | data: 167 | list: [ [1, 2, 3], ['a', 'b', 'c'] ] 168 | template: '"{{#list}}({{#.}}{{.}}{{/.}}){{/list}}"' 169 | expected: '"(123)(abc)"' 170 | 171 | # Dotted Names 172 | 173 | - name: Dotted Names - Truthy 174 | desc: Dotted names should be valid for Section tags. 175 | data: { a: { b: { c: true } } } 176 | template: '"{{#a.b.c}}Here{{/a.b.c}}" == "Here"' 177 | expected: '"Here" == "Here"' 178 | 179 | - name: Dotted Names - Falsey 180 | desc: Dotted names should be valid for Section tags. 181 | data: { a: { b: { c: false } } } 182 | template: '"{{#a.b.c}}Here{{/a.b.c}}" == ""' 183 | expected: '"" == ""' 184 | 185 | - name: Dotted Names - Broken Chains 186 | desc: Dotted names that cannot be resolved should be considered falsey. 187 | data: { a: { } } 188 | template: '"{{#a.b.c}}Here{{/a.b.c}}" == ""' 189 | expected: '"" == ""' 190 | 191 | # Whitespace Sensitivity 192 | 193 | - name: Surrounding Whitespace 194 | desc: Sections should not alter surrounding whitespace. 195 | data: { boolean: true } 196 | template: " | {{#boolean}}\t|\t{{/boolean}} | \n" 197 | expected: " | \t|\t | \n" 198 | 199 | - name: Internal Whitespace 200 | desc: Sections should not alter internal whitespace. 201 | data: { boolean: true } 202 | template: " | {{#boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n" 203 | expected: " | \n | \n" 204 | 205 | - name: Indented Inline Sections 206 | desc: Single-line sections should not alter surrounding whitespace. 207 | data: { boolean: true } 208 | template: " {{#boolean}}YES{{/boolean}}\n {{#boolean}}GOOD{{/boolean}}\n" 209 | expected: " YES\n GOOD\n" 210 | 211 | - name: Standalone Lines 212 | desc: Standalone lines should be removed from the template. 213 | data: { boolean: true } 214 | template: | 215 | | This Is 216 | {{#boolean}} 217 | | 218 | {{/boolean}} 219 | | A Line 220 | expected: | 221 | | This Is 222 | | 223 | | A Line 224 | 225 | - name: Indented Standalone Lines 226 | desc: Indented standalone lines should be removed from the template. 227 | data: { boolean: true } 228 | template: | 229 | | This Is 230 | {{#boolean}} 231 | | 232 | {{/boolean}} 233 | | A Line 234 | expected: | 235 | | This Is 236 | | 237 | | A Line 238 | 239 | - name: Standalone Line Endings 240 | desc: '"\r\n" should be considered a newline for standalone tags.' 241 | data: { boolean: true } 242 | template: "|\r\n{{#boolean}}\r\n{{/boolean}}\r\n|" 243 | expected: "|\r\n|" 244 | 245 | - name: Standalone Without Previous Line 246 | desc: Standalone tags should not require a newline to precede them. 247 | data: { boolean: true } 248 | template: " {{#boolean}}\n#{{/boolean}}\n/" 249 | expected: "#\n/" 250 | 251 | - name: Standalone Without Newline 252 | desc: Standalone tags should not require a newline to follow them. 253 | data: { boolean: true } 254 | template: "#{{#boolean}}\n/\n {{/boolean}}" 255 | expected: "#\n/\n" 256 | 257 | # Whitespace Insensitivity 258 | 259 | - name: Padding 260 | desc: Superfluous in-tag whitespace should be ignored. 261 | data: { boolean: true } 262 | template: '|{{# boolean }}={{/ boolean }}|' 263 | expected: '|=|' 264 | -------------------------------------------------------------------------------- /mustache/test/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Chris", 3 | "value": 1000, 4 | "taxed_value": "6000", 5 | "in_ca": true 6 | } 7 | -------------------------------------------------------------------------------- /mustache/test/templ1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello {{ name }} 5 | You have just won {{ value }} dollars! 6 | {{#in_ca}} 7 | Well, {{taxed_value}} dollars, after taxes. 8 | {{/in_ca}} 9 | 10 | 11 | 12 | --------------------------------------------------------------------------------