M .gitignore +1 -0
@@ 2,4 2,5 @@
pds.mk
Ocamlrules.mk.in
build/
+.merlin
M LICENSE +2 -2
@@ 1,4 1,4 @@
-Copyright (c) 2013, orbitz.
+Copyright (c) 2017, Applied Computer Science Laboratory.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
@@ 25,4 25,4 @@ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIA
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
No newline at end of file
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
M README.org +278 -9
@@ 1,11 1,280 @@
* About
-To use:
-#+BEGIN_SRC
-# -n: required, name
-# -r: optional, will push branch to the remote if provided
-# -d: optional, description for generated README.org.
-# Can be multi-line string.
-# Additional deps are optional.
+Logic-less template system. Snabela (pronounced snob-el-aw) is based on
+Mustache with an intent to fix the issues the author found with it.
+
+Specifically:
+
+- Mustache is geared very specifically at web development, by default everything
+ is HTML escaped. Snabela does not escape anything by default and allows
+ extensible escaping.
+- Mustache has no direct way to escape template replacement but has a convoluted
+ mechanism based on changing the syntax of the delimiter on the fly. Snabela
+ does not allow changing the syntax of the template delimiter on the fly and
+ has a simple mechanism for escaping template replacement.
+- Mustache supports changing the syntax of the template delimiter on the fly,
+ Snabela does not support this.
+- Mustache has many implicit conversions, for example iterating a list and
+ testing boolean values use the same operator. Snabela has specific operators
+ for each type.
+- Mustache does not allow arbitrary conversion of a template to a defined
+ format. For example if you want to represent a number as money, you have to
+ either convert it to a string before the replacement or use whatever numeric
+ representation the template engine does. Similarly, in some context a string
+ should be a title or capitalized/lowercased. In Mustache that value must have
+ each version represented.
+
+* Description
+A typical Snabela template:
+
+#+BEGIN_EXAMPLE
+Hello @name@
+You have just won @value@ dollars!
+@?in_ca-@
+Well, @taxed_value | money@ dollars, after taxes.
+@/in_ca-@
+
+Email me at foo@@bar.com for more details.
+#+END_EXAMPLE
+
+Given the following value:
+
+#+BEGIN_EXAMPLE
+{
+ "name": "Chris",
+ "value": 10000,
+ "taxed_value": 10000 - (10000 * 0.4),
+ "in_ca": true
+}
+#+END_EXAMPLE
+
+Will produce the following, assuming there is a transformer named ~money~:
+
+#+BEGIN_EXAMPLE
+Hello Chris
+You have just won 10000 dollars!
+Well, 6000.00 dollars, after taxes
+
+Email me at foo@bar.com for more details.
+#+END_EXAMPLE
+
+Snabela takes a Unicode template string and a key-value object and replaces keys
+referenced in the template string with the value in the object. Keys are
+separated by the ~@~ symbol. The ~@~ can be escaped with two ~@~ symbols. The
+key-value object can contain UTF-8 encoded strings, integers, floats, lists, and
+key-value objects. Symbols directly after the opening ~@~ allow the key-value
+object to be traversed in different ways.
+
+** Restrictions on key identifiers
+A key identifier must correspond to the regexp: ~[a-zA-Z_][a-zA-Z0-9_]*~.
+
+** A note on strictness
+A conforming Snabela implementation must, by default, error if a key is accessed
+in a template but does not exist in the key-value object. Likewise, it must
+error if a transformer is accessed but does not exist in the context. The
+implementation must also error if a test operator is used on a value of the
+incorrect type.
+
+** Template replacements
+Replacing a key with a value is called template replacement. Template
+replacement is began with a ~@~ and ended with a ~@~. Modifiers may directly
+follow the ~@~, such as ~?~, ~!~, and ~#~. After those may be any number of
+white space characters followed by a key identifier, followed by any number of
+white space characters, followed by a closing ~@~. By default, all white space
+is preserved in the template, however this can be modified with the ~-~
+modifier, which is always the inner-most modifier.
+
+** Types of template replacements
+*** Removing white space
+White space is removed up to the preceding or following new line using the ~-~
+modifier. A ~-~ in the opening of a template replacement trims any white space
+up to the new line. A ~-~ in the closing template replacement trims any white
+space up to and including the first new line.
+
+Template:
+
+#+BEGIN_EXAMPLE
+ @-them@
+My name is @me-@
+and I have a business proposition for you.
+#+END_EXAMPLE
+
+Key-value object:
+
+#+BEGIN_EXAMPLE
+{
+ "them": "Sir/Madam",
+ "me": "Arthur Digby Sellers"
+}
+#+END_EXAMPLE
+
+Output:
+
+#+BEGIN_EXAMPLE
+Sir/Madam
+My name is Arthur Digby Sellers and I have a business proposition for you.
+#+END_EXAMPLE
+
+*** Sections
+Like Mustache, Snabela has sections. The identifier to enter a section depends
+on the type of section however each section is exited with ~@/~.
+
+*** Boolean testing
+A boolean value in the template can be tested for ~true~ with ~@?~ and ~false~
+with ~@!~.
+
+Template:
+
+#+BEGIN_EXAMPLE
+Shown.
+@?person-@
+ Never shown!
+@/person@
+@!person-@
+ Always shown!
+@/person-@
+#+END_EXAMPLE
+
+Key-value object:
-$ scripts/genesis -n <project name> -r <git remote> -d <description> [dep1 [dep2 ... depn]]
-#+END_SRC
+#+BEGIN_EXAMPLE
+{
+ "person": false
+}
+#+END_EXAMPLE
+
+Output:
+
+#+BEGIN_EXAMPLE
+Shown.
+Always shown!
+#+END_EXAMPLE
+
+*** Iterating lists
+The key-value object can have keys which correspond to a list of objects.
+Iterating is done with ~@#~. Each object in the list creates a new, inner,
+context which includes the outer contexts but shadows keys with the same name.
+
+Template:
+
+#+BEGIN_EXAMPLE
+@#parties-@
+@name@ has a minimum age of @min_age@.
+ Guest list:
+ @#guest_list-@
+ @name@
+ @/guest_list-@
+@/parties-@
+#+END_EXAMPLE
+
+Hash:
+
+#+BEGIN_EXAMPLE
+{
+ "min_age": 18,
+ "guest_list": [],
+ "parties": [
+ { "name": "End of the world party", "guest_list": [{"name": "me"}, {"name": "myself"}, {"name: "i"}] },
+ { "name": "End of the world party party", "min_age": 21 },
+ ]
+}
+#+END_EXAMPLE
+
+Output:
+
+#+BEGIN_EXAMPLE
+End of the world party has a minimum age of 18.
+ Guest list:
+ me
+ myself
+ i
+End of the world party party has a minimum age of 21.
+ Guest list:
+#+END_EXAMPLE
+
+*** Testing lists for empty/non-empty
+A list can be tested for if it is empty or not. The value must be a list.
+Testing if a list is empty or not is done with ~@#?~ and ~@#!~ respectively.
+This does not create a new context inside the list with shadowed variables.
+
+The previous example done such it does not give an empty "Guest list" section
+would look like:
+
+Template:
+
+#+BEGIN_EXAMPLE
+@#parties-@
+@name@ has a minimum age of @min_age@.
+@#?guest_list-@
+ Guest list:
+ @#-guest_list-@
+ @name@
+ @-/guest_list-@
+@/guest_list-@
+@#!guest_list-@
+ No guests have signed up.
+@/guest_list-@
+@/parties-@
+#+END_EXAMPLE
+
+Hash:
+
+#+BEGIN_EXAMPLE
+{
+ "min_age": 18,
+ "guest_list": [],
+ "parties": [
+ { "name": "End of the world party", "guest_list": [{"name": "me"}, {"name": "myself"}, {"name: "i"}] },
+ { "name": "End of the world party party", "min_age": 21 },
+ ]
+}
+#+END_EXAMPLE
+
+Output:
+
+#+BEGIN_EXAMPLE
+End of the world party has a minimum age of 18.
+ Guest list:
+ me
+ myself
+ i
+End of the world party party has a minimum age of 21.
+ No guests have signed up.
+#+END_EXAMPLE
+
+*** Transformers
+Any template replacement may include one or more transformers. A transformer is
+a function which takes the value of a template and converts can perform any
+operation on it. It may also throw an error. Transformers can be used to
+encode a value to ensure it is safe to output or ensure a value has a particular
+structure. Transformers come after the name of the key, with optional white
+space, separated by the ~|~ symbol. Transformers are only valid for key
+replacements, not testing a value or iterating a list. An implementation is
+allowed to execute a template with a default transformer on all template
+replacements. For example, in a web context the template execution might put
+all values through an transformer which HTML escapes.
+
+Template:
+
+#+BEGIN_EXAMPLE
+- @name@
+- @company | html@
+- @company@
+#+END_EXAMPLE
+
+Hash:
+
+#+BEGIN_EXAMPLE
+{
+ "name": "Chris",
+ "company": "<b>GitHub</b>"
+}
+#+END_EXAMPLE
+
+Output, presuming a transformer called ~html~ exists which takes any value end turns it
+into an HTML-escaped string, the following:
+
+#+BEGIN_EXAMPLE
+- Chris
+- <b>GitHub</b>
+- <b>GitHub</b>
+#+END_EXAMPLE
M hll.conf +11 -8
@@ 1,14 1,17 @@
pds = {major_version = 5}
-desc = ""
-maintainer = ""
-authors = [ ]
-homepage = ""
-bug_reports = ""
-dev_repo = ""
+desc = "Logic-less template system"
+maintainer = "mmatalka@gmail.com"
+authors = [ "mmatalka@gmail.com" ]
+homepage = "https://bitbucket.org/acslab/snabela"
+bug_reports = "https://bitbucket.org/acslab/snabela/issues"
+dev_repo = "git@bitbucket.org:acslab/snabela.git"
-url_template = "git@foo:repo.git#{tag}"
+url_template = "https://bitbucket.org/acslab/snabela/get/{tag}.tar.gz"
url_pattern = "{tag}"
-url_protocol = "git"
+url_protocol = "http"
build_deps = ["merlin-of-pds"]
+
+[deps_map]
+ppx_deriving = ["ppx_deriving.show", "ppx_deriving.eq"]
No newline at end of file
A => pds.conf +20 -0
@@ 0,0 1,20 @@
+[global.release]
+extra_compiler_opts = "-strict-sequence -strict-formats -safe-string -noassert"
+
+[global.debug]
+extra_compiler_opts = "-g -strict-sequence -strict-formats -safe-string -w '@d@f@p@u@s@40'"
+
+[src.snabela]
+install = true
+extra_ocamldep_opts = "-package sedlex"
+deps = [ "process", "containers", "sedlex", "ppx_deriving", "ppx_deriving.show", "ppx_deriving.eq" ]
+
+[src.snabela_cli]
+install = true
+type = "exec"
+install_cmd = "cp -vf snabela_cli.native $(PREFIX)/bin/snabela"
+remove_cmd = "rm -vf $(PREFIX)/bin/snabela"
+deps = ["cmdliner", "containers", "process", "toml", "snabela"]
+
+[tests.snabela]
+deps = [ "containers", "oth", "snabela" ]
R scripts/advertise => +0 -25
@@ 1,25 0,0 @@
-#! /usr/bin/env bash
-
-set -e
-
-if [[ "$#" -gt "1" ]]; then
- cat <<EOF
-Usage: $0 <project name> <dep1> ... <depN>
-EOF
- exit 1
-fi
-
-REMOTE=$1
-
-git remote rm origin
-git checkout -b initial
-git add pds.conf README.org LICENSE
-git add src tests
-git rm -rf scripts
-git commit -m "Initial commit. Generated from templater."
-
-if [[ -n "$REMOTE" ]]; then
- git remote add origin $REMOTE
- # push master and initial
- git push --set-upstream --all origin
-fi
R scripts/differentiate => +0 -55
@@ 1,55 0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-# Quote list of strings and comma separate them.
-function format_dep_list {
- if [[ "$#" -gt "0" ]]; then
- echo $* | xargs -n1 -I{} echo -n '"{}"' | sed 's/""/", "/g'
- else
- echo ""
- fi
-}
-
-if [[ "$#" -eq "0" ]]; then
- cat <<EOF
-Usage: $0 <project name> <dep1> ... <depN>
-EOF
- exit 1
-fi
-
-NAME="$(echo "$1" | tr '-' '_')"
-shift
-DEPS=$@
-
-# Create Dirs
-mkdir -p "src/$NAME"
-mkdir -p "tests/$NAME"
-
-# Output pds.conf
-cat <<EOF > pds.conf
-[global.release]
-extra_compiler_opts = "-strict-sequence -strict-formats -safe-string -noassert"
-
-[global.debug]
-extra_compiler_opts = "-g -strict-sequence -strict-formats -safe-string -w '@d@f@p@u@s@40'"
-
-[src.$NAME]
-install = true
-deps = [ $(format_dep_list $DEPS) ]
-
-[tests.$NAME]
-deps = [ $(format_dep_list $NAME) ]
-EOF
-
-# Output dummy src file.
-cat <<EOF > "src/$NAME/$NAME.ml"
-(* How you doin'. *)
-print_endline "Hello, Beautiful!";;
-EOF
-
-# Output dummy test file.
-cat <<EOF > "tests/$NAME/test.ml"
-(* Be kind. Test. *)
-exit 1;;
-EOF
R scripts/enlighten => +0 -44
@@ 1,44 0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-DESCRIPTION="$1"
-YEAR=$(date "+%Y")
-
-LICENSE_ENTITY="$(git config user.license.entity || git config user.name)"
-
-cat <<EOF > LICENSE
-Copyright (c) $YEAR, $LICENSE_ENTITY.
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without modification,
-are permitted provided that the following conditions are met:
-
-
- 1. Redistributions of source code must retain the above copyright notice,
- this list of conditions and the following disclaimer.
-
- 2. Redistributions in binary form must reproduce the above copyright
- notice, this list of conditions and the following disclaimer in the
- documentation and/or other materials provided with the distribution.
-
- 3. Neither the name of riakc or ocaml-riakc nor the names of its contributors may be
- used to endorse or promote products derived from this software without
- specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
-ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
-ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-EOF
-
-cat <<EOF > README.org
-* About
-$DESCRIPTION
-EOF
R scripts/genesis => +0 -39
@@ 1,39 0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-function usage {
- exit 1
-}
-
-# n: required project name
-# d: optional description
-# r: optional remote
-while getopts "n:r:d:" opt; do
- case "$opt" in
- n)
- NAME=$OPTARG
- ;;
- r)
- REMOTE=$OPTARG
- ;;
- d)
- DESCRIPTION="$OPTARG"
- ;;
- *)
- usage
- ;;
- esac
-done
-
-if [[ -z "$NAME" ]]; then
- usage
-fi
-
-shift $((OPTIND-1))
-
-SCRIPT_DIR="$(dirname $0)"
-
-"$SCRIPT_DIR/differentiate" $NAME $*
-"$SCRIPT_DIR/enlighten" "$DESCRIPTION"
-"$SCRIPT_DIR/advertise" $REMOTE
R scripts/tests/genesis_test => +0 -60
@@ 1,60 0,0 @@
-#!/usr/bin/env bash
-
-# Regression test for genesis script.
-# Assumes it is being executed from the root of the git repo.
-# So ROOT/scripts/tests/<this file>.
-# Relatively destructive, so best if run from a clean happy place
-# after you've committed important changes or from your
-# test automation in a clean environment.
-
-set -e
-set -x
-
-HEAD_REF=$(git symbolic-ref --short HEAD)
-
-REMOTE_DIR=$(mktemp -d)
-
-# Constants
-TEST_DESCRIPTION="Test Baz"
-TEST_DEPS="core"
-TEST_REMOTE_SUFFIX="testremote.git"
-TEST_PROJECT_NAME="footest"
-TEST_GIT_BRANCH="initial"
-
-# Create test remote
-pushd $REMOTE_DIR
-git init --bare $TEST_REMOTE_SUFFIX
-popd
-
-scripts/genesis -n $TEST_PROJECT_NAME \
- -r "file://$REMOTE_DIR/$TEST_REMOTE_SUFFIX" \
- -d "$TEST_DESCRIPTION" $TEST_DEPS
-
-# Verify we're on the initial branch.
-git symbolic-ref --short HEAD | grep $TEST_GIT_BRANCH
-
-# Verify origin is testremote.git
-git remote -vv | grep origin | grep $TEST_REMOTE_SUFFIX
-
-# Verify we've cleaned up the scripts after we're done.
-if [[ -d scripts ]]; then
- exit 1
-fi
-
-# Make should succeed
-gmake
-
-# Test has some fail code in it, so don't run it.
-grep "exit 1" tests/$TEST_PROJECT_NAME/$TEST_PROJECT_NAME.ml
-
-# License should have the correct year in it.
-grep $(date "+%Y") LICENSE
-
-# We put the right stuff in the README.
-grep "$TEST_DESCRIPTION" README.org
-
-# Cleanup
-rm -rf $REMOTE_DIR
-git checkout $HEAD_REF
-
-echo "Success!"
R scripts/tests/initial_state => +0 -12
@@ 1,12 0,0 @@
-#!/usr/bin/env bash
-
-# Some helper code to put the directory back in a testable state
-# after a bogo test run. Meant to make human debugging of the
-# tests a bit easier.
-
-set -e
-
-git clean -f -d
-git reset HEAD --hard
-git branch -D initial || true
-git remote add origin foo || true
A => src/snabela/snabela.ml +291 -0
@@ 0,0 1,291 @@
+module Kv = struct
+ module Map = CCMap.Make(String)
+
+ type scalar =
+ | I of int
+ | F of float
+ | S of string
+ | B of bool
+ [@@deriving show,eq]
+
+ type t =
+ | V of scalar
+ | L of t Map.t list [@printer CCList.pp (Map.pp CCString.print pp)]
+ [@@deriving show,eq]
+
+ let list m = L m
+ let int i = V (I i)
+ let float f = V (F f)
+ let string s = V (S s)
+ let bool b = V (B b)
+end
+
+module Transformer = struct
+ type t = (string * (Kv.scalar -> Kv.scalar))
+end
+
+module Template = struct
+ type err = [ `Exn of exn | Snabela_lexer.err ]
+
+ type t = Snabela_lexer.Token.t
+
+ let is_space = function
+ | ' ' | '\012' | '\r' | '\t' -> true
+ | _ -> false
+
+ let trim_trailing_ws s =
+ if s = "" then
+ s
+ else begin
+ let i = ref (String.length s - 1) in
+ while !i >= 0 && is_space (String.get s !i) do
+ i := !i - 1
+ done;
+ StringLabels.sub s ~pos:0 ~len:(!i + 1)
+ end
+
+ let trim_leading_ws s =
+ if s = "" then
+ s
+ else begin
+ let i = ref 0 in
+ while !i < String.length s && is_space (String.get s !i) do
+ i := !i + 1
+ done;
+ if !i < String.length s && String.get s !i = '\n' then
+ i := !i + 1;
+ StringLabels.sub s ~pos:!i ~len:(CCString.length s - !i)
+ end
+
+ let apply_trims tokens =
+ let open Snabela_lexer.Token in
+ let rec at acc = function
+ | String s::At ln::List::Test::Left_trim::xs ->
+ (* @#?- ... *)
+ at (Test::List::At ln::String (trim_trailing_ws s)::acc) xs
+ | String s::At ln::List::Left_trim::xs ->
+ (* @#- ... *)
+ at (List::At ln::String (trim_trailing_ws s)::acc) xs
+ | String s::At ln::Test::Left_trim::xs ->
+ (* @?- ... *)
+ at (Test::At ln::String (trim_trailing_ws s)::acc) xs
+ | String s::At ln::Neg_test::Left_trim::xs ->
+ (* @!- ... *)
+ at (Neg_test::At ln::String (trim_trailing_ws s)::acc) xs
+ | String s::At ln::Left_trim::xs ->
+ (* @- ... *)
+ at (At ln::String (trim_trailing_ws s)::acc) xs
+ | Left_trim::xs ->
+ (* ... - ... *)
+ at acc xs
+ | Right_trim::At ln::String s::xs ->
+ (* ... -@ ... *)
+ at (At ln::acc) (String (trim_leading_ws s)::xs)
+ | Right_trim::xs ->
+ (** ... -@ ... *)
+ at acc xs
+ | x::xs ->
+ at (x::acc) xs
+ | [] ->
+ acc
+ in
+ List.rev (at [] tokens)
+
+ let of_utf8_string s =
+ let open CCResult.Infix in
+ try
+ let lexbuf = Sedlexing.Utf8.from_string s in
+ Snabela_lexer.tokenize lexbuf
+ >>= fun tokens ->
+ Ok (apply_trims tokens)
+ with
+ | exn ->
+ Error (`Exn exn)
+end
+
+module TMap = CCMap.Make(String)
+
+type line_number = int [@@deriving show]
+
+type err = [ `Missing_key of (string * line_number)
+ | `Expected_boolean of (string * line_number)
+ | `Expected_list of (string * line_number)
+ | `Missing_transformer of (string * line_number)
+ | `Non_scalar_key of (string * line_number)
+ | `Premature_eof
+ | `Missing_closing_section of string
+ ]
+[@@deriving show]
+
+exception Apply_error of err
+
+type trans_func = (Kv.scalar -> Kv.scalar)
+
+type t = { template : Template.t
+ ; transformers : trans_func TMap.t
+ ; append_transformers : trans_func list
+ }
+
+let of_template ?(append_transformers = []) t tr =
+ let transformers = TMap.of_list tr in
+ { template = t; transformers; append_transformers }
+
+let string_of_scalar = function
+ | Kv.I i -> Printf.sprintf "%d" i
+ | Kv.F f -> Printf.sprintf "%f" f
+ | Kv.S s -> s
+ | Kv.B true -> "true"
+ | Kv.B false -> "false"
+
+let rec skip_section key = let open Snabela_lexer.Token in function
+ | At _::List::Key k::At _::ts
+ | At _::List::Test::Key k::At _::ts
+ | At _::List::Neg_test::Key k::At _::ts
+ | At _::Test::Key k::At _::ts
+ | At _::Neg_test::Key k::At _::ts ->
+ skip_section key (skip_section k ts)
+ | At _::End_section::Key k::At _::ts when key = k ->
+ (* @/key@ *)
+ ts
+ | [] ->
+ raise (Apply_error (`Missing_closing_section key))
+ | _::ts ->
+ skip_section key ts
+
+let apply_transformers v trs =
+ ListLabels.fold_left
+ ~f:(fun v f -> f v)
+ ~init:v
+ trs
+
+let rec eval_template buf t kv template section =
+ let open Snabela_lexer.Token in
+ match template with
+ | [] when section = "" ->
+ []
+ | [] when section <> ""->
+ raise (Apply_error (`Missing_closing_section section))
+ | [] ->
+ raise (Apply_error `Premature_eof)
+ | Escaped_at::ts ->
+ (* @@ *)
+ Buffer.add_char buf '@';
+ eval_template buf t kv ts section
+ | String s::ts ->
+ (* Foo *)
+ Buffer.add_string buf s;
+ eval_template buf t kv ts section
+ | At ln::Key k::ts ->
+ (* @key [ | t1 | t2 ... ] @ *)
+ let t_test = function | Snabela_lexer.Token.Transformer _ -> true | _ -> false in
+ let t_map = function
+ | Snabela_lexer.Token.Transformer name ->
+ begin match TMap.get name t.transformers with
+ | Some f -> f
+ | None -> raise (Apply_error (`Missing_transformer (name, ln)))
+ end
+ | _ ->
+ assert false
+ in
+ let trans = CCList.take_while t_test ts in
+ let ts = CCList.drop (List.length trans + 1) ts in
+ let v =
+ match Kv.Map.get k kv with
+ | Some (Kv.V v) ->
+ begin match v with
+ | Kv.I _
+ | Kv.F _
+ | Kv.S _
+ | Kv.B _ as scalar -> scalar
+ end
+ | Some (Kv.L _) ->
+ raise (Apply_error (`Non_scalar_key (k, ln)))
+ | None ->
+ raise (Apply_error (`Missing_key (k, ln)))
+ in
+ let transformed_k =
+ apply_transformers
+ (apply_transformers v (ListLabels.map ~f:t_map trans))
+ t.append_transformers
+ in
+ Buffer.add_string buf (string_of_scalar transformed_k);
+ eval_template buf t kv ts section
+ | At ln::Test::Key k::At _::ts ->
+ (* @?key@ ... @/key@ *)
+ eval_bool_section ln buf t kv ts section k true
+ | At ln::Neg_test::Key k::At _::ts ->
+ (* @!key@ ... @/key@ *)
+ eval_bool_section ln buf t kv ts section k false
+ | At ln::List::Key k::At _::ts ->
+ (* @#key@ ... @/key@ *)
+ eval_list_section ln buf t kv ts section k
+ | At ln::List::Test::Key k::At _::ts ->
+ (* @#?key@ ... @/key@ *)
+ eval_list_test_section ln buf t kv ts section k `Not_empty
+ | At ln::List::Neg_test::Key k::At _::ts ->
+ (* @#!key@ ... @/key@ *)
+ eval_list_test_section ln buf t kv ts section k `Empty
+ | At _::End_section::Key k::At _::ts when k = section ->
+ (* @/key@ *)
+ ts
+ | _ ->
+ assert false
+and eval_bool_section ln buf t kv ts section key b =
+ match Kv.Map.get key kv with
+ | Some (Kv.V (Kv.B v)) when v = b ->
+ let ts = eval_template buf t kv ts key in
+ eval_template buf t kv ts section
+ | Some (Kv.V (Kv.B _)) ->
+ let ts = skip_section key ts in
+ eval_template buf t kv ts section
+ | Some _ ->
+ raise (Apply_error (`Expected_boolean (key, ln)))
+ | None ->
+ raise (Apply_error (`Missing_key (key, ln)))
+and eval_list_section ln buf t kv ts section key =
+ match Kv.Map.get key kv with
+ | Some (Kv.L []) ->
+ let ts = skip_section key ts in
+ eval_template buf t kv ts section
+ | Some (Kv.L ls) ->
+ ListLabels.iter
+ ~f:(fun kv' ->
+ let kv =
+ Kv.Map.union
+ (fun _ _ r -> Some r)
+ kv
+ kv'
+ in
+ ignore (eval_template buf t kv ts key))
+ ls;
+ let ts = skip_section key ts in
+ eval_template buf t kv ts section
+ | Some _ ->
+ raise (Apply_error (`Expected_list (key, ln)))
+ | None ->
+ raise (Apply_error (`Missing_key (key, ln)))
+and eval_list_test_section ln buf t kv ts section key empty =
+ match Kv.Map.get key kv with
+ | Some (Kv.L []) when empty = `Empty ->
+ let ts = eval_template buf t kv ts key in
+ eval_template buf t kv ts section
+ | Some (Kv.L (_::_)) when empty = `Not_empty ->
+ let ts = eval_template buf t kv ts key in
+ eval_template buf t kv ts section
+ | Some (Kv.L _) ->
+ let ts = skip_section key ts in
+ eval_template buf t kv ts section
+ | Some _ ->
+ raise (Apply_error (`Expected_list (key, ln)))
+ | None ->
+ raise (Apply_error (`Missing_key (key, ln)))
+
+let apply t kv =
+ let buf = Buffer.create 100 in
+ try
+ let ret = eval_template buf t kv t.template "" in
+ assert (ret = []);
+ Ok (Buffer.contents buf)
+ with
+ | Apply_error err ->
+ Error (err : err :> [> err ])
A => src/snabela/snabela.mli +83 -0
@@ 0,0 1,83 @@
+(** Logic-less template replacement. *)
+
+(** [Kv] represents the values to be used in during replacement in a template.
+ There are two classes: scalars and lists. Only scalars can be used in
+ replacement. Lists can be iterated.
+
+ Convenience function are provided for constructing a value of type [t] as
+ well as pretty printing. *)
+module Kv : sig
+ module Map : CCMap.S with type key = string
+
+ type scalar =
+ | I of int
+ | F of float
+ | S of string
+ | B of bool
+
+ type t =
+ | V of scalar
+ | L of t Map.t list
+
+ val list : t Map.t list -> t
+ val int : int -> t
+ val float : float -> t
+ val string : string -> t
+ val bool : bool -> t
+
+ val pp : Format.formatter -> t -> unit
+ val show : t -> string
+ val equal : t -> t -> bool
+end
+
+module Transformer : sig
+ type t = (string * (Kv.scalar -> Kv.scalar))
+end
+
+module Template : sig
+ type err = [ `Exn of exn | Snabela_lexer.err ]
+
+ type t
+
+ (** Parse a template string that is UTF-8 encoded. *)
+ val of_utf8_string : string -> (t, [> err ]) result
+end
+
+type line_number = int
+
+(** Error type when applying a key-value to a template. When necessary, the
+ line number in the template is included in the error. *)
+type err = [ `Missing_key of (string * line_number) (** Key was missing on the line *)
+ | `Expected_boolean of (string * line_number) (** Expected a boolean in a section. *)
+ | `Expected_list of (string * line_number) (** Expected a list in a section. *)
+ | `Missing_transformer of (string * line_number) (** Transformer was not found. *)
+ | `Non_scalar_key of (string * line_number) (** Non-scalar value used in replacement *)
+ | `Premature_eof (** Reached an unexpected EOF. *)
+ | `Missing_closing_section of string (** Failed to provide a close for the section. *)
+ ]
+
+(** A compiled representation of a parsed template and transformers. *)
+type t
+
+(** Standard function to convert a scalar into a string. *)
+val string_of_scalar : Kv.scalar -> string
+
+(** Turn a parsed template and transformers into a compiled representation that
+ can be applied to a kv.
+
+ The [append_transformers] parameter contains transformers that will be
+ applied to every replacement. *)
+val of_template :
+ ?append_transformers:(Kv.scalar -> Kv.scalar) list ->
+ Template.t ->
+ Transformer.t list ->
+ t
+
+(** Apply a key-value to a template turning it into a string or an error. *)
+val apply : t -> Kv.t Kv.Map.t -> (string, [> err ]) result
+
+(** Pretty print an error. *)
+val pp_err : Format.formatter -> err -> unit
+
+(** Turn an error into a string. *)
+val show_err : err -> string
A => src/snabela/snabela_lexer.ml +134 -0
@@ 0,0 1,134 @@
+module Token = struct
+ type token =
+ | Escaped_at
+ | At of int
+ | List
+ | Test
+ | Neg_test
+ | Left_trim
+ | Right_trim
+ | Key of string
+ | Transformer of string
+ | String of string
+ | End_section
+ [@@deriving show,eq]
+
+ type t = token list [@@deriving show,eq]
+end
+
+(* Token builder. *)
+module Tb : sig
+ type t
+ val create : unit -> t
+ val add : Token.token -> t -> t
+ val add_l : Token.t -> t -> t
+ val build : t -> Token.t
+end = struct
+ type t = Token.t
+ let create () = []
+ let add v t = v::t
+ let add_l vs t = (List.rev vs) @ t
+ let build t = List.rev t
+end
+
+type err = [ `Premature_eof
+ | `Invalid_replacement of int
+ | `Invalid_transformer of int
+ ]
+[@@deriving show,eq]
+
+exception Tokenize_error of err
+
+open Token
+
+let key = [%sedlex.regexp? ('a'..'z' | 'A'..'Z'), Star ('a'..'z' | 'A'..'Z' | '0'..'9' | '_')]
+
+let rec token ln bldr buf =
+ match%sedlex buf with
+ | "@@" ->
+ token ln (Tb.add Escaped_at bldr) buf
+ | "@#?-" ->
+ replacement ln (Tb.add_l [At ln; List; Test; Left_trim] bldr) buf
+ | "@#-" ->
+ replacement ln (Tb.add_l [At ln; List; Left_trim] bldr) buf
+ | "@#?" ->
+ replacement ln (Tb.add_l [At ln; List; Test] bldr) buf
+ | "@#!" ->
+ replacement ln (Tb.add_l [At ln; List; Neg_test] bldr) buf
+ | "@#" ->
+ replacement ln (Tb.add_l [At ln; List] bldr) buf
+ | "@?-" ->
+ replacement ln (Tb.add_l [At ln; Test; Left_trim] bldr) buf
+ | "@!-" ->
+ replacement ln (Tb.add_l [At ln; Neg_test; Left_trim] bldr) buf
+ | "@?" ->
+ replacement ln (Tb.add_l [At ln; Test] bldr) buf
+ | "@!" ->
+ replacement ln (Tb.add_l [At ln; Neg_test] bldr) buf
+ | "@-" ->
+ replacement ln (Tb.add_l [At ln; Left_trim] bldr) buf
+ | "@" ->
+ replacement ln (Tb.add (At ln) bldr) buf
+ | Star (Sub (any, "@")) ->
+ let str = Sedlexing.Utf8.lexeme buf in
+ let n = List.length (CCString.find_all_l ~sub:"\n" str) in
+ token (ln + n) (Tb.add (String str) bldr) buf
+ | eof ->
+ Tb.build bldr
+ | _ ->
+ assert false
+and replacement ln bldr buf =
+ match%sedlex buf with
+ | "\n" ->
+ replacement (ln + 1) bldr buf
+ | white_space ->
+ replacement ln bldr buf
+ | "/", key ->
+ let key = Sedlexing.Utf8.sub_lexeme buf 1 (Sedlexing.lexeme_length buf - 1) in
+ replacement_close ln (Tb.add_l [End_section; Key key] bldr) buf
+ | "@" ->
+ token ln (Tb.add (At ln) bldr) buf
+ | key ->
+ let key = Sedlexing.Utf8.lexeme buf in
+ transformer ln (Tb.add (Key key) bldr) buf
+ | _ ->
+ raise (Tokenize_error (`Invalid_replacement ln))
+and transformer ln bldr buf =
+ match%sedlex buf with
+ | "\n" ->
+ transformer (ln + 1) bldr buf
+ | white_space ->
+ transformer ln bldr buf
+ | "|" ->
+ transformer_key ln bldr buf
+ | eof ->
+ raise (Tokenize_error `Premature_eof)
+ | _ ->
+ Sedlexing.rollback buf;
+ replacement_close ln bldr buf
+and transformer_key ln bldr buf =
+ match%sedlex buf with
+ | "\n" ->
+ transformer_key (ln + 1) bldr buf
+ | white_space ->
+ transformer_key ln bldr buf
+ | key ->
+ let key = Sedlexing.Utf8.lexeme buf in
+ transformer ln (Tb.add (Transformer key) bldr) buf
+ | _ ->
+ raise (Tokenize_error (`Invalid_transformer ln))
+and replacement_close ln bldr buf =
+ match%sedlex buf with
+ | "-@" ->
+ token ln (Tb.add_l [Right_trim; At ln] bldr) buf
+ | "@" ->
+ token ln (Tb.add (At ln) bldr) buf
+ | _ ->
+ raise (Tokenize_error (`Invalid_replacement ln))
+
+let tokenize s =
+ try
+ Ok (token 1 (Tb.create ()) s)
+ with
+ | Tokenize_error err ->
+ Error (err : err :> [> err ])
A => src/snabela_cli/snabela_cli.ml +159 -0
@@ 0,0 1,159 @@
+module SMap = CCMap.Make(String)
+
+module Cmdline = struct
+ module C = Cmdliner
+
+ let kv =
+ let doc = "TOML file of variables." in
+ C.Arg.(required & opt (some file) None & info ["kv"] ~docv:"FILE" ~doc)
+
+ let transformers =
+ let doc = "Directory containing transformers." in
+ C.Arg.(value & opt_all dir [] & info ["td"] ~docv:"DIR" ~doc)
+
+ let append_transformers =
+ let doc = "Append these transformers to all replacements." in
+ C.Arg.(value & opt_all dir [] & info ["at"] ~docv:"DIR" ~doc)
+end
+
+exception Invalid_type
+exception Transformer_error of string
+
+let result_of_toml_result = function
+ | `Ok v -> Ok v
+ | `Error e -> Error (`Toml_parse_error e)
+
+let rec kv_of_toml_table_exn tbl =
+ let module Kv = Snabela.Kv in
+ let open TomlTypes in
+ Table.fold
+ (fun k v acc ->
+ let k = Table.Key.to_string k in
+ match v with
+ | TBool b ->
+ Kv.Map.add k (Kv.bool b) acc
+ | TInt i ->
+ Kv.Map.add k (Kv.int i) acc
+ | TFloat f ->
+ Kv.Map.add k (Kv.float f) acc
+ | TString s ->
+ Kv.Map.add k (Kv.string s) acc
+ | TArray arr ->
+ Kv.Map.add k (Kv.list (kv_of_toml_array_exn arr)) acc
+ | TDate _
+ | TTable _ ->
+ raise Invalid_type)
+ tbl
+ (Kv.Map.empty)
+and kv_of_toml_array_exn arr =
+ let open TomlTypes in
+ match arr with
+ | NodeEmpty ->
+ []
+ | NodeTable tbls ->
+ ListLabels.map ~f:kv_of_toml_table_exn tbls
+ | NodeBool _
+ | NodeInt _
+ | NodeFloat _
+ | NodeString _
+ | NodeDate _
+ | NodeArray _ ->
+ raise Invalid_type
+
+let kv_of_toml_table tbl =
+ try
+ Ok (kv_of_toml_table_exn tbl)
+ with
+ | Invalid_type ->
+ Error `Invalid_type
+
+let run_transformer exec v =
+ let b = Bytes.of_string (Snabela.string_of_scalar v) in
+ try
+ Snabela.Kv.S (String.concat "" (Process.read_stdout ~stdin:b exec [||]))
+ with
+ | _ ->
+ raise
+ (Transformer_error
+ (Printf.sprintf "Failed to execute transformer %s" (Filename.basename exec)))
+
+let transformer_of_dir d =
+ let files = Array.to_list (Sys.readdir d) in
+ ListLabels.map
+ ~f:(fun f -> (f, run_transformer (Filename.concat d f)))
+ files
+
+let load_transformers ds =
+ ListLabels.fold_left
+ ~f:(fun acc d ->
+ let ts = transformer_of_dir d in
+ let m = SMap.of_list ts in
+ SMap.union (fun _ l _ -> Some l) acc m)
+ ~init:SMap.empty
+ ds
+
+let snabela_apply kv_file transformers append_transformers =
+ let open CCResult.Infix in
+ let template_str = CCIO.read_all stdin in
+ Snabela.Template.of_utf8_string template_str
+ >>= fun template ->
+ result_of_toml_result (Toml.Parser.from_filename kv_file)
+ >>= fun toml ->
+ kv_of_toml_table toml
+ >>= fun kv ->
+ let transformers = load_transformers transformers in
+ let at =
+ ListLabels.map
+ ~f:(fun tname ->
+ match SMap.get tname transformers with
+ | Some tr -> tr
+ | None -> failwith "nyi")
+ append_transformers
+ in
+ let cache = Snabela.of_template ~append_transformers:at template (SMap.to_list transformers) in
+ Snabela.apply cache kv
+ >>= fun applied ->
+ output_string stdout applied;
+ Ok ()
+
+let snabela kv_file transformers append_transformers =
+ match snabela_apply kv_file transformers append_transformers with
+ | Ok _ ->
+ `Ok ()
+ | Error err ->
+ let err_str =
+ match err with
+ | `Missing_key (key, ln) ->
+ Printf.sprintf "Missing key %s in replacement on line %d" key ln
+ | `Expected_boolean (key, ln) ->
+ Printf.sprintf "Expected key %s to be a boolean in replacement on line %d" key ln
+ | `Expected_list (key, ln) ->
+ Printf.sprintf "Expected key %s to be a list in replacement on line %d" key ln
+ | `Missing_transformer (tr, ln) ->
+ Printf.sprintf "Missing transformer %s in replacement on line %d" tr ln
+ | `Non_scalar_key (key, ln) ->
+ Printf.sprintf "Key %s must be a scalar in replacement on line %d" key ln
+ | `Premature_eof ->
+ "Template ended prematurely"
+ | `Missing_closing_section section ->
+ Printf.sprintf "Section named %s was not closed before end of file" section
+ | `Exn exn ->
+ Printf.sprintf "Failed with exception %s" (Printexc.to_string exn)
+ | `Invalid_replacement ln ->
+ Printf.sprintf "Malformed replacement on line %d" ln
+ | `Invalid_transformer ln ->
+ Printf.sprintf "Malformed transformer in replacement on line %d" ln
+ | `Invalid_type ->
+ "TOML file cannot be converted to a key-value"
+ | `Toml_parse_error (s, _) ->
+ Printf.sprintf "TOML parse error %s" (String.trim s)
+ in
+ `Error (false, err_str)
+
+let cmd =
+ let doc = "Execute replacements in a template." in
+ let exits = Cmdliner.Term.default_exits in
+ Cmdliner.Term.(ret Cmdline.(const snabela $ kv $ transformers $ append_transformers),
+ info "snabela" ~version:"1.0" ~doc ~exits)
+
+let () = Cmdliner.Term.(exit @@ eval cmd)
A => test_data/foo.tmpl +12 -0
@@ 0,0 1,12 @@
+@#parties-@
+@name@ has a minimum age of @min_age@ and has a $@cost | money@ cover charge.
+@#?guest_list-@
+ Guest list:
+ @#-guest_list-@
+ @name@
+ @-/guest_list-@
+@/guest_list-@
+@#!guest_list-@
+ No guests have signed up.
+@/guest_list-@
+@/parties-@
A => test_data/foo.toml +20 -0
@@ 0,0 1,20 @@
+min_age = 18
+guest_list = []
+
+[[parties]]
+name = "End of the world party"
+cost = 20.0
+
+[[parties.guest_list]]
+name = "me"
+
+[[parties.guest_list]]
+name = "myself"
+
+[[parties.guest_list]]
+name = "i"
+
+[[parties]]
+name = "End of the world party party"
+min_age = 21
+cost = 40.0
A => test_data/transformers/money +5 -0
@@ 0,0 1,5 @@
+#! /bin/sh
+
+read line
+
+printf "%0.2f" $line
A => tests/snabela/test.ml +471 -0
@@ 0,0 1,471 @@
+let test_tokenizer1 =
+ Oth.test
+ ~desc:"Basic tokenizer"
+ ~name:"Tokenizer: Simple replace"
+ (fun _ ->
+ let template = "@name@" in
+ let lexbuf = Sedlexing.Utf8.from_string template in
+ let tokens = CCResult.get_exn (Snabela_lexer.tokenize lexbuf) in
+ assert Snabela_lexer.Token.(equal tokens [At 1; Key "name"; At 1]))
+
+let test_tokenizer2 =
+ Oth.test
+ ~desc:"Tokenize transformer"
+ ~name:"Tokenizer: Transformer"
+ (fun _ ->
+ let template = "@name | foo@" in
+ let lexbuf = Sedlexing.Utf8.from_string template in
+ let tokens = CCResult.get_exn (Snabela_lexer.tokenize lexbuf) in
+ assert Snabela_lexer.Token.(equal tokens [At 1; Key "name"; Transformer "foo"; At 1]))
+
+let test_tokenizer3 =
+ Oth.test
+ ~desc:"Just a string"
+ ~name:"Tokenizer: String"
+ (fun _ ->
+ let template = "Hello" in
+ let lexbuf = Sedlexing.Utf8.from_string template in
+ let tokens = CCResult.get_exn (Snabela_lexer.tokenize lexbuf) in
+ assert Snabela_lexer.Token.(equal tokens [String "Hello"]))
+
+let test_tokenizer4 =
+ Oth.test
+ ~desc:"Empty input"
+ ~name:"Tokenizer: Empty"
+ (fun _ ->
+ let template = "" in
+ let lexbuf = Sedlexing.Utf8.from_string template in
+ let tokens = CCResult.get_exn (Snabela_lexer.tokenize lexbuf) in
+ assert Snabela_lexer.Token.(equal tokens []))
+
+let test_tokenizer5 =
+ Oth.test
+ ~desc:"Left right trim"
+ ~name:"Tokenizer: Trim"
+ (fun _ ->
+ let template = "@- test -@" in
+ let lexbuf = Sedlexing.Utf8.from_string template in
+ let tokens = CCResult.get_exn (Snabela_lexer.tokenize lexbuf) in
+ assert Snabela_lexer.Token.(equal tokens [At 1; Left_trim; Key "test"; Right_trim; At 1]))
+
+let test_tokenizer6 =
+ Oth.test
+ ~name:"Tokenizer: Invalid"
+ (fun _ ->
+ try
+ let template = "@- te st -@" in
+ let lexbuf = Sedlexing.Utf8.from_string template in
+ ignore (Snabela_lexer.tokenize lexbuf);
+ assert false
+ with
+ | _ ->
+ ())
+
+let test_tokenizer7 =
+ Oth.test
+ ~name:"Tokenizer: Basic"
+ (fun _ ->
+ let template = "Hello, @name@" in
+ let lexbuf = Sedlexing.Utf8.from_string template in
+ let tokens = CCResult.get_exn (Snabela_lexer.tokenize lexbuf) in
+ assert
+ Snabela_lexer.Token.(
+ equal
+ tokens
+ [String "Hello, "; At 1; Key "name"; At 1]))
+
+let test_tokenizer8 =
+ Oth.test
+ ~name:"Tokenizer: README"
+ (fun _ ->
+ let template = "@#parties-@
+@name@ has a minimum age of @min_age@.
+@#?guest_list-@
+ Guest list:
+ @#-guest_list-@
+ @name@
+ @-/guest_list-@
+@/guest_list-@
+@#!guest_list-@
+ No guests have signed up.
+@/guest_list-@
+@/parties-@"
+ in
+ let lexbuf = Sedlexing.Utf8.from_string template in
+ let tokens = CCResult.get_exn (Snabela_lexer.tokenize lexbuf) in
+ assert Snabela_lexer.Token.(
+ equal
+ tokens
+ [ At 1; List; Key "parties"; Right_trim; At 1
+ ; String "\n"
+ ; At 2; Key "name"; At 2
+ ; String " has a minimum age of "
+ ; At 2; Key "min_age"; At 2
+ ; String ".\n"
+ ; At 3; List; Test; Key "guest_list"; Right_trim; At 3
+ ; String "\n Guest list:\n "
+ ; At 5; List; Left_trim; Key "guest_list"; Right_trim; At 5
+ ; String "\n "
+ ; At 6; Key "name"; At 6
+ ; String "\n "
+ ; At 7; Left_trim; End_section; Key "guest_list"; Right_trim; At 7
+ ; String "\n"
+ ; At 8; End_section; Key "guest_list"; Right_trim; At 8
+ ; String "\n"
+ ; At 9; List; Neg_test; Key "guest_list"; Right_trim; At 9
+ ; String "\n No guests have signed up.\n"
+ ; At 11; End_section; Key "guest_list"; Right_trim; At 11
+ ; String "\n"
+ ; At 12; End_section; Key "parties"; Right_trim; At 12
+ ]))
+
+let test_apply1 =
+ Oth.test
+ ~name:"Apply: Simple"
+ (fun _ ->
+ let template = "Hello, @name@" in
+ let kv = Snabela.Kv.(Map.of_list [("name", string "foo")]) in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let compile = Snabela.of_template t [] in
+ let applied = CCResult.get_exn (Snabela.apply compile kv) in
+ assert ("Hello, foo" = applied))
+
+let test_apply2 =
+ Oth.test
+ ~name:"Apply: Boolean true"
+ (fun _ ->
+ let template = "Hello, @?personalize-@ @name@ @-/personalize@" in
+ let kv = Snabela.Kv.(Map.of_list [("name", string "foo"); ("personalize", bool true)]) in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let compile = Snabela.of_template t [] in
+ let applied = CCResult.get_exn (Snabela.apply compile kv) in
+ assert ("Hello, foo" = applied))
+
+let test_apply3 =
+ Oth.test
+ ~name:"Apply: Boolean false"
+ (fun _ ->
+ let template = "Hello, @?personalize-@ @name@ @-/personalize@" in
+ let kv = Snabela.Kv.(Map.of_list [("name", string "foo"); ("personalize", bool false)]) in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let compile = Snabela.of_template t [] in
+ let applied = CCResult.get_exn (Snabela.apply compile kv) in
+ assert ("Hello, " = applied))
+
+let test_apply4 =
+ Oth.test
+ ~name:"Apply: Boolean not true"
+ (fun _ ->
+ let template = "Hello, @!personalize-@ @name@ @-/personalize@" in
+ let kv = Snabela.Kv.(Map.of_list [("name", string "foo"); ("personalize", bool false)]) in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let compile = Snabela.of_template t [] in
+ let applied = CCResult.get_exn (Snabela.apply compile kv) in
+ assert ("Hello, foo" = applied))
+
+let test_apply5 =
+ Oth.test
+ ~name:"Apply: Boolean not false"
+ (fun _ ->
+ let template = "Hello, @!personalize-@ @name@ @-/personalize@" in
+ let kv = Snabela.Kv.(Map.of_list [("name", string "foo"); ("personalize", bool true)]) in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let compile = Snabela.of_template t [] in
+ let applied = CCResult.get_exn (Snabela.apply compile kv) in
+ assert ("Hello, " = applied))
+
+let test_apply6 =
+ Oth.test
+ ~name:"Apply: List non-empty iter"
+ (fun _ ->
+ let template = "Hello,\n@#names-@\n@name@\n@-/names@" in
+ let kv = Snabela.Kv.(
+ Map.of_list
+ [("names",
+ list
+ [ Map.of_list [("name", string "foo")]
+ ; Map.of_list [("name", string "bar")]
+ ])])
+ in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let compile = Snabela.of_template t [] in
+ let applied = CCResult.get_exn (Snabela.apply compile kv) in
+ assert ("Hello,\nfoo\nbar\n" = applied))
+
+let test_apply7 =
+ Oth.test
+ ~name:"Apply: List non-empty test"
+ (fun _ ->
+ let template = "Hello, @#?names-@ everyone @-/names@" in
+ let kv = Snabela.Kv.(
+ Map.of_list
+ [("names",
+ list
+ [ Map.of_list [("name", string "foo")]
+ ; Map.of_list [("name", string "bar")]
+ ])])
+ in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let compile = Snabela.of_template t [] in
+ let applied = CCResult.get_exn (Snabela.apply compile kv) in
+ assert ("Hello, everyone" = applied))
+
+let test_apply8 =
+ Oth.test
+ ~name:"Apply: List empty test"
+ (fun _ ->
+ let template = "Hello, @#!names-@ everyone @-/names@" in
+ let kv = Snabela.Kv.(
+ Map.of_list
+ [("names",
+ list
+ [ Map.of_list [("name", string "foo")]
+ ; Map.of_list [("name", string "bar")]
+ ])])
+ in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let compile = Snabela.of_template t [] in
+ let applied = CCResult.get_exn (Snabela.apply compile kv) in
+ assert ("Hello, " = applied))
+
+let test_apply9 =
+ Oth.test
+ ~name:"Apply: README Test"
+ (fun _ ->
+ let template = "@#parties-@
+@name@ has a minimum age of @min_age@.
+@#?guest_list-@
+ Guest list:
+ @#-guest_list-@
+ @name@
+ @-/guest_list-@
+@/guest_list-@
+@#!guest_list-@
+ No guests have signed up.
+@/guest_list-@
+@/parties-@"
+ in
+ let kv = Snabela.Kv.(
+ Map.of_list
+ [ ("min_age", int 18)
+ ; ("guest_list", list [])
+ ; ("parties",
+ list
+ [ Map.of_list
+ [ ("name", string "End of the world party")
+ ; ("guest_list",
+ list
+ [ Map.of_list [("name", string "me")]
+ ; Map.of_list [("name", string "myself")]
+ ; Map.of_list [("name", string "i")]
+ ])
+ ]
+ ; Map.of_list
+ [ ("name", string "End of the world party party")
+ ; ("min_age", int 21)
+ ]
+ ])
+ ])
+ in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let compile = Snabela.of_template t [] in
+ let applied = CCResult.get_exn (Snabela.apply compile kv) in
+ let expected =
+ "End of the world party has a minimum age of 18.
+ Guest list:
+ me
+ myself
+ i
+End of the world party party has a minimum age of 21.
+ No guests have signed up.
+"
+ in
+ assert (expected = applied))
+
+let test_apply10 =
+ Oth.test
+ ~name:"Apply: List non-empty test on empty list"
+ (fun _ ->
+ let template = "Hello, @#?names-@ everyone @-/names@" in
+ let kv = Snabela.Kv.(Map.of_list [("names", list [])]) in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let compile = Snabela.of_template t [] in
+ let applied = CCResult.get_exn (Snabela.apply compile kv) in
+ assert ("Hello, " = applied))
+
+let test_apply11 =
+ Oth.test
+ ~name:"Apply: Default transformer"
+ (fun _ ->
+ let template = "Hello, @name@" in
+ let kv = Snabela.Kv.(Map.of_list [("name", string "joe")]) in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let capitalize = function
+ | Snabela.Kv.S s -> Snabela.Kv.S (CCString.capitalize_ascii s)
+ | _ -> raise (Invalid_argument "not a string")
+ in
+ let compile = Snabela.of_template ~append_transformers:[capitalize] t [] in
+ let applied = CCResult.get_exn (Snabela.apply compile kv) in
+ assert ("Hello, Joe" = applied))
+
+let test_apply_fail1 =
+ Oth.test
+ ~name:"Apply Fail: Missing key"
+ (fun _ ->
+ let template = "Hello, @name@" in
+ let kv = Snabela.Kv.Map.empty in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let compile = Snabela.of_template t [] in
+ let ret = Snabela.apply compile kv in
+ assert (ret = Error (`Missing_key ("name", 1))))
+
+let test_apply_fail2 =
+ Oth.test
+ ~name:"Apply Fail: Expected boolean"
+ (fun _ ->
+ let template = "@?greet-@ hello @-/greet@" in
+ let kv = Snabela.Kv.(Map.of_list [("greet", int 2)]) in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let compile = Snabela.of_template t [] in
+ let ret = Snabela.apply compile kv in
+ assert (ret = Error (`Expected_boolean ("greet", 1))))
+
+let test_apply_fail3 =
+ Oth.test
+ ~name:"Apply Fail: Expected list"
+ (fun _ ->
+ let template = "@#?greet-@ hello @-/greet@" in
+ let kv = Snabela.Kv.(Map.of_list [("greet", int 2)]) in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let compile = Snabela.of_template t [] in
+ let ret = Snabela.apply compile kv in
+ assert (ret = Error (`Expected_list ("greet", 1))))
+
+let test_apply_fail4 =
+ Oth.test
+ ~name:"Apply Fail: Missing transformer"
+ (fun _ ->
+ let template = "Hello, @name | test@" in
+ let kv = Snabela.Kv.(Map.of_list [("name", string "Joe")]) in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let compile = Snabela.of_template t [] in
+ let ret = Snabela.apply compile kv in
+ assert (ret = Error (`Missing_transformer ("test", 1))))
+
+let test_apply_fail5 =
+ Oth.test
+ ~name:"Apply Fail: Non scalar key"
+ (fun _ ->
+ let template = "Hello, @name | test@" in
+ let kv = Snabela.Kv.(Map.of_list [("name", list [])]) in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let compile = Snabela.of_template t [] in
+ let ret = Snabela.apply compile kv in
+ assert (ret = Error (`Non_scalar_key ("name", 1))))
+
+let test_apply_fail6 =
+ Oth.test
+ ~name:"Apply Fail: Missing closing section"
+ (fun _ ->
+ let template = "@?foo@ hi" in
+ let kv = Snabela.Kv.(Map.of_list [("foo", bool true)]) in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let compile = Snabela.of_template t [] in
+ let ret = Snabela.apply compile kv in
+ assert (ret = Error (`Missing_closing_section "foo")))
+
+let test_apply_fail7 =
+ Oth.test
+ ~name:"Apply Fail: Missing key not line 1"
+ (fun _ ->
+ let template = "Hello,\n@name@" in
+ let kv = Snabela.Kv.Map.empty in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let compile = Snabela.of_template t [] in
+ let ret = Snabela.apply compile kv in
+ assert (ret = Error (`Missing_key ("name", 2))))
+
+let test_apply_fail8 =
+ Oth.test
+ ~name:"Apply Fail: New lines in replacement"
+ (fun _ ->
+ let template = "Hello,\n@\n\nname\n\n@\n@name1@" in
+ let kv = Snabela.Kv.(Map.of_list [("name", string "foo")]) in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let compile = Snabela.of_template t [] in
+ let ret = Snabela.apply compile kv in
+ assert (ret = Error (`Missing_key ("name1", 7))))
+
+let test_transformer1 =
+ Oth.test
+ ~name:"Transformer: Capitalize"
+ (fun _ ->
+ let template = "Hello, @name | capitalize@" in
+ let kv = Snabela.Kv.(Map.of_list [("name", string "foo")]) in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let capitalize = function
+ | Snabela.Kv.S s -> Snabela.Kv.S (CCString.capitalize_ascii s)
+ | _ -> raise (Invalid_argument "not a string")
+ in
+ let compile = Snabela.of_template t [("capitalize", capitalize)] in
+ let applied = CCResult.get_exn (Snabela.apply compile kv) in
+ assert ("Hello, Foo" = applied))
+
+let test_transformer2 =
+ Oth.test
+ ~name:"Transformer: Money"
+ (fun _ ->
+ let template = "You owe me @ amount | money@@currency@" in
+ let kv =
+ Snabela.Kv.(
+ Map.of_list
+ [ ("currency", string "USD")
+ ; ("amount", float 1.25)
+ ])
+ in
+ let t = CCResult.get_exn (Snabela.Template.of_utf8_string template) in
+ let money = function
+ | Snabela.Kv.F f -> Snabela.Kv.S (Printf.sprintf "%0.2f" f)
+ | _ -> raise (Invalid_argument "not a float")
+ in
+ let compile = Snabela.of_template t [("money", money)] in
+ let applied = CCResult.get_exn (Snabela.apply compile kv) in
+ assert ("You owe me 1.25USD" = applied))
+
+let test =
+ Oth.parallel
+ [ test_tokenizer1
+ ; test_tokenizer2
+ ; test_tokenizer3
+ ; test_tokenizer4
+ ; test_tokenizer5
+ ; test_tokenizer6
+ ; test_tokenizer7
+ ; test_tokenizer8
+ ; test_apply1
+ ; test_apply2
+ ; test_apply3
+ ; test_apply4
+ ; test_apply5
+ ; test_apply6
+ ; test_apply7
+ ; test_apply8
+ ; test_apply9
+ ; test_apply10
+ ; test_apply11
+ ; test_apply_fail1
+ ; test_apply_fail2
+ ; test_apply_fail3
+ ; test_apply_fail4
+ ; test_apply_fail5
+ ; test_apply_fail6
+ ; test_apply_fail7
+ ; test_apply_fail8
+ ; test_transformer1
+ ; test_transformer2
+ ]
+
+let () =
+ Random.self_init ();
+ Oth.run test
+