diff options
Diffstat (limited to 'src/templating')
83 files changed, 7566 insertions, 0 deletions
diff --git a/src/templating/.gitignore b/src/templating/.gitignore new file mode 100644 index 000000000..9ed2f3ff9 --- /dev/null +++ b/src/templating/.gitignore @@ -0,0 +1,3 @@ +test_mustach_jansson +taler-mustach-tool +mustach diff --git a/src/templating/AUTHORS b/src/templating/AUTHORS new file mode 100644 index 000000000..110b36981 --- /dev/null +++ b/src/templating/AUTHORS @@ -0,0 +1,38 @@ +Main author: + José Bollo <jobol@nonadev.net> + +Contributors: + Abhishek Mishra + Atlas + Ben Beasley + Christian Grothoff + Dominik Kummer + Gabriel Zachmann + Harold L Marzan + Kurt Jung + Lailton Fernando Mariano + Lucas Ramage + Paramtamtam + RekGRpth + Ryan Fox + Sami Kerola + Sijmen J. Mulder + Steve-Chavez + Tomasz Sieprawski + +Packagers: + pkgsrc: Sijmen J. Mulder + alpine linux: Lucas Ramage + RPM & DEB: Marcus Hardt + +Thanks to issue submitters: + Dante Torres + @fabbe + Felix von Leitner + Johann Oskarsson + Mark Bucciarelli + Nigel Hathaway + Paul Wisehart + Tarik @tr001 + Thierry Fournier + SASU OKFT diff --git a/src/templating/CHANGELOG.md b/src/templating/CHANGELOG.md new file mode 100644 index 000000000..003652ebf --- /dev/null +++ b/src/templating/CHANGELOG.md @@ -0,0 +1,161 @@ +1.2.7 (2024-03-21) +------------------ + +New: + - fallback to default when mustach_wrap_get_partial + returns MUSTACH_ERROR_PARTIAL_NOT_FOUND + - remove at compile time the load of files for templates + if MUSTACH_LOAD_TEMPLATE is defined as 0 + - add compile time flag MUSTACH_SAFE for enforcing + safety behaviours + +Fix: + - selection of subitem by index (#47) + - get latest iterated key when getting key name (#52) + - allow tests without valgrind + - avoid recursive template expansion (#55) + +1.2.6 (2024-01-08) +------------------ + +Fix: + - improve naming (#42) + - magical spaces in recursive partials (#43) + - installation when tool isn't built + - correct detection of falsey values (#45) + +Minor: + - update to newer reference tests + +1.2.5 (2023-02-18) +------------------ + +Fix: + - Don't override CFLAGS in Makefile + - Use of $(INSTALL) in Makefile for setting options + +Minor: + - Orthograf of 'instantiate' + +1.2.4 (2023-01-02) +------------------ + +Fix: + - Latent SIGSEGV using cJSON + +1.2.3 (2022-12-21) +------------------ + +New: + - Flag Mustach_With_ErrorUndefined (and option --strict for the tool) + for returning a requested tag is not defined + - Test of specifications in separate directory + +Fix: + - Version printing is now okay + - Compiling libraries on Darwin (no soname but install_name) + - Compiling test6 with correct flags + - Update test from specifications + - Better use of valgrind reports + +1.2.2 (2021-10-28) +------------------ + +Fix: + - SONAME of libmustach-json-c.so + +1.2.1 (2021-10-19) +------------------ + +New: + - Add SONAME in libraries. + - Flag Mustach_With_PartialDataFirst to switch the + policy of resolving partials. + +Fix: + - Identification of types in cJSON + +1.2.0 (2021-08-24) +------------------ + +New: + - Add hook 'mustach_wrap_get_partial' for handling partials. + - Add test of mustache specifications https://github.com/mustache/spec. + +Changes: + - Mustach_With_SingleDot is always set. + - Mustach_With_IncPartial is always set. + - Mustach_With_AllExtensions is changed to use currently known extensions. + - Output of tests changed. + - Makefile improved. + - Partials are first searched as file then in current selection. + - Improved management of delimiters. + +Fixes: + - Improved output accordingly to https://github.com/mustache/spec: + - escaping of quote " + - interpolating null with empty string + - removal of empty lines with standalone tag + - don't enter section if null + - indentation of partials + - comment improved for get of mustach_wrap_itf. + +1.1.1 (2021-08-19) +------------------ +Fixes: + - Avoid conflicting with getopt. + - Remove unexpected build artifact. + - Handle correctly a size of 0. + +1.1.0 (2021-05-01) +------------------ +New: + - API refactored to take lengths to ease working with partial or + non-NULL-terminated strings. (ABI break) + +Fixes: + - Use correct int type for jansson (json_int_t instead of int64_t). + - JSON output of different backends is now the same. + +1.0 (2021-04-28, retracted) +--------------------------- +Legal: + - License changed to ISC. + +Fixes: + - Possible data leak in memfile_open() by clearing buffers. + - Fix build on Solaris-likes by including alloca.h. + - Fix Windows build by including malloc.h, using size_t instead of + ssize_t, and using the standard ternary operator syntax. + - Fix JSON in test3 by using double quote characters. + - Fix installation in alternative directories such as + /opt/pkg/lib on macOS by setting install_name. + - Normalise return values in compare() implementations. + +New: + - Support for cJSON and jansson libraries. + - Version info now embedded at build time and shown with mustach(1) + usage. + - Versioned so-names (e.g. libxlsx.so.1.0). + - BINDIR, LIBDIR and INCLUDEDIR variables in Makefile. + - New mustach-wrap.{c,h} to ease implementation new libraries, + extracted and refactored from the existing implementations. + - Makefile now supports 3 modes: single libmustach (default), split + libmustache-core etc, and both. + - Any or all backends (json-c, jansson, etc) can be enabled at compile + time. By default, all available libraries are used. + - mustach(1) can use any JSON backend instead of only json-c. + - MUSTACH_COMPATIBLE_0_99 can be defined for backwards source + compatibility. + - 'No extensions' can now be set Mustach_With_NoExtensions instead of + passing 0. + - pkgconfig (.pc) file for library. + - Manual page for mustach(1). + +Changed: + - Many renames. + - Maximum tag length increased from 1024 to 4096. + - Other headers include json-c.h instead of using forward declarations. + - mustach(1) reads from /dev/stdin instead of fd 0. + - Several structures are now taken as const. + - New/changed Makefile targets. diff --git a/src/templating/LICENSE.txt b/src/templating/LICENSE.txt new file mode 100644 index 000000000..495aeefd5 --- /dev/null +++ b/src/templating/LICENSE.txt @@ -0,0 +1,14 @@ + +Copyright (c) 2017-2020 by José Bollo + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, +OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. diff --git a/src/templating/Makefile.am b/src/templating/Makefile.am new file mode 100644 index 000000000..a79b109d1 --- /dev/null +++ b/src/templating/Makefile.am @@ -0,0 +1,132 @@ +# This Makefile.am is in the public domain +AM_CPPFLAGS = -I$(top_srcdir)/src/include $(LIBGCRYPT_CFLAGS) + +if USE_COVERAGE + AM_CFLAGS = --coverage -O0 + XLIB = -lgcov +endif + +noinst_PROGRAMS = \ + taler-mustach-tool + +taler_mustach_tool_SOURCES = \ + mustach-tool.c \ + mustach-jansson.h +taler_mustach_tool_LDADD = \ + libmustach.la \ + -ljansson +taler_mustach_tool_CFLAGS = \ + -DTOOL=MUSTACH_TOOL_JANSSON \ + -DMUSTACH_SAFE=1 \ + -DMUSTACH_LOAD_TEMPLATE=0 + +lib_LTLIBRARIES = \ + libtalertemplating.la + +noinst_LTLIBRARIES = \ + libmustach.la + +libtalertemplating_la_SOURCES = \ + mustach.c mustach.h \ + mustach-wrap.c mustach-wrap.h \ + mustach-jansson.c mustach-jansson.h \ + templating_api.c +libtalertemplating_la_LIBADD = \ + $(top_builddir)/src/mhd/libtalermhd.la \ + $(top_builddir)/src/util/libtalerutil.la \ + -lmicrohttpd \ + -lgnunetjson \ + -lgnunetutil \ + -ljansson \ + $(XLIB) +libtalertemplating_la_LDFLAGS = \ + -version-info 0:0:0 \ + -no-undefined +libtalertemplating_la_CFLAGS = \ + -DMUSTACH_SAFE=1 \ + -DMUSTACH_LOAD_TEMPLATE=0 + +libmustach_la_SOURCES = \ + mustach.c mustach.h \ + mustach-wrap.c mustach-wrap.h \ + mustach-jansson.c mustach-jansson.h + +test_mustach_jansson_SOURCES = \ + test_mustach_jansson.c +test_mustach_jansson_LDADD = \ + -lgnunetutil \ + -ljansson \ + -lmustach \ + $(XLIB) + +check_PROGRAMS = \ + test_mustach_jansson + +TESTS = $(check_PROGRAMS) + +EXTRA_DIST = \ + $(check_SCRIPTS) \ + mustach-original-Makefile \ + mustach.1.gz \ + mustach.1.scd \ + meson.build \ + LICENSE.txt \ + ORIGIN \ + pkgcfgs \ + README.md \ + dotest.sh \ + AUTHORS \ + CHANGELOG.md \ + mustach-json-c.h \ + mustach-json-c.c \ + mustach-cjson.h \ + mustach-cjson.c \ + test1/json \ + test1/Makefile \ + test1/must \ + test1/resu.ref \ + test1/vg.ref \ + test2/json \ + test2/Makefile \ + test2/must \ + test2/resu.ref \ + test2/vg.ref \ + test3/json \ + test3/Makefile \ + test3/must \ + test3/resu.ref \ + test3/vg.ref \ + test4/json \ + test4/Makefile \ + test4/must \ + test4/resu.ref \ + test4/vg.ref \ + test5/json \ + test5/Makefile \ + test5/must \ + test5/must2 \ + test5/must2.mustache \ + test5/must3.mustache \ + test5/resu.ref \ + test5/vg.ref \ + test6/json \ + test6/Makefile \ + test6/must \ + test6/resu.ref \ + test6/test-custom-write.c \ + test6/vg.ref \ + test7/base.mustache \ + test7/json \ + test7/Makefile \ + test7/node.mustache \ + test7/resu.ref \ + test7/vg.ref \ + test8/json \ + test8/Makefile \ + test8/must \ + test8/resu.ref \ + test8/vg.ref \ + test-specs/test-specs.c \ + test-specs/test-specs-cjson.ref \ + test-specs/test-specs-jansson.ref \ + test-specs/test-specs-json-c.ref diff --git a/src/templating/ORIGIN b/src/templating/ORIGIN new file mode 100644 index 000000000..14902983a --- /dev/null +++ b/src/templating/ORIGIN @@ -0,0 +1,11 @@ +Cloned originally from https://gitlab.com/jobol/mustach/ + +Changes: +======== + +Renamed original Makefile to mustach-original-Makefile +and wrote Makefile.am for us. + +Added run-original-tests.sh shell script as a wrapper around +mustach-original-Makefile to use the original build process for the test +suite. diff --git a/src/templating/README.md b/src/templating/README.md new file mode 100644 index 000000000..6e7a6c956 --- /dev/null +++ b/src/templating/README.md @@ -0,0 +1,320 @@ +# Introduction to Mustach 1.2 + +`mustach` is a C implementation of the [mustache](http://mustache.github.io "main site for mustache") +template specification. + +The main site for `mustach` is on [gitlab](https://gitlab.com/jobol/mustach). + +The simplest way to use mustach is to copy the files **mustach.h** and **mustach.c** +directly into your project and use it. + +If you are using one of the JSON libraries listed below, you can get extended feature +by also including **mustach-wrap.h**, **mustach-wrap.c**, **mustach-XXX.h** and +**mustach-XXX.c** in your project (see below for **XXX**) + +- [json-c](https://github.com/json-c/json-c): use **XXX** = **json-c** +- [jansson](http://www.digip.org/jansson/): use **XXX** = **jansson** +- [cJSON](https://github.com/DaveGamble/cJSON): use **XXX** = **cjson** + +Alternatively, make and meson files are provided for building `mustach` and +`libmustach.so` shared library. + +Since version 1.0, the makefile allows to compile and install different +flavours. See below for details. + +## Distributions offering mustach package + +### Alpine Linux + +```sh +apk add mustach +apk add mustach-lib +apk add mustach-dev +``` + +### NetBSD + +```sh +cd devel/mustach +make +``` + +See http://pkgsrc.se/devel/mustach + +## Known projects using Mustach + +This [wiki page](https://gitlab.com/jobol/mustach/-/wikis/projects-using-mustach) +lists the known project that are using mustach and that kindly told it. + +Don't hesitate to tell us if you are interested to be listed there. + +## Using Mustach from sources + +The file **mustach.h** is the main documentation. Look at it. + +The current source files are: + +- **mustach.c** core implementation of mustache in C +- **mustach.h** header file for core definitions +- **mustach-wrap.c** generic wrapper of mustach for easier integration +- **mustach-wrap.h** header file for using mustach-wrap +- **mustach-json-c.c** tiny json wrapper of mustach using [json-c](https://github.com/json-c/json-c) +- **mustach-json-c.h** header file for using the tiny json-c wrapper +- **mustach-cjson.c** tiny json wrapper of mustach using [cJSON](https://github.com/DaveGamble/cJSON) +- **mustach-cjson.h** header file for using the tiny cJSON wrapper +- **mustach-jansson.c** tiny json wrapper of mustach using [jansson](https://www.digip.org/jansson/) +- **mustach-jansson.h** header file for using the tiny jansson wrapper +- **mustach-tool.c** simple tool for applying template files to one JSON file + +The file **mustach-json-c.c** is the historical example of use of **mustach** and +**mustach-wrap** core and it is also a practical implementation that can be used. +It uses the library json-c. (NOTE for Mac OS: available through homebrew). + +Since version 1.0, the project also provide integration of other JSON libraries: +**cJSON** and **jansson**. + +*If you integrate a new library with* **mustach**, *your contribution will be +welcome here*. + +The tool **mustach** is build using `make`, its usage is: + + mustach json template [template]... + +It then outputs the result of applying the templates files to the JSON file. + +### Portability + +Some system does not provide *open_memstream*. In that case, tell your +preferred compiler to declare the preprocessor symbol **NO_OPEN_MEMSTREAM**. +Example: + + CFLAGS=-DNO_OPEN_MEMSTREAM make + +### Integration + +The files **mustach.h** and **mustach-wrap.h** are the main documentation. Look at it. + +The file **mustach-json-c.c** provides a good example of integration. + +If you intend to use basic HTML/XML escaping and standard C FILE, the callbacks +of the interface **mustach_itf** that you have to implement are: +`enter`, `next`, `leave`, `get`. + +If you intend to use specific escaping and/or specific output, the callbacks +of the interface **mustach_itf** that you have to implement are: +`enter`, `next`, `leave`, `get` and `emit`. + +### Compilation Using Make + +Building and installing can be done using make. + +Example: + + $ make tool=cjson libs=none PREFIX=/usr/local DESTDIR=/ install + $ make tool=jsonc libs=single PREFIX=/ DESTDIR=$HOME/.local install + +The makefile knows following switches (\*: default): + + Switch name | Values | Description + --------------+---------+----------------------------------------------- + jsonc | (unset) | Auto detection of json-c + | no | Don't compile for json-c + | yes | Compile for json-c that must exist + --------------+---------+----------------------------------------------- + cjson | (unset) | Auto detection of cJSON + | no | Don't compile for cJSON + | yes | Compile for cJSON that must exist + --------------+---------+----------------------------------------------- + jansson | (unset) | Auto detection of jansson + | no | Don't compile for jansson + | yes | Compile for jansson that must exist + --------------+---------+----------------------------------------------- + tool | (unset) | Auto detection + | cjson | Use cjson library + | jsonc | Use jsonc library + | jansson | Use jansson library + | none | Don't compile the tool + --------------+---------+---------------------------------------------- + libs | (unset) | Like 'all' + | all | Like 'single' AND 'split' + | single | Only libmustach.so + | split | All the possible libmustach-XXX.so ... + | none | No library is produced + +The libraries that can be produced are: + + Library name | Content + --------------------+-------------------------------------------------------- + libmustach-core | mustach.c mustach-wrap.c + libmustach-cjson | mustach.c mustach-wrap.c mustach-cjson.c + libmustach-jsonc | mustach.c mustach-wrap.c mustach-json-c.c + libmustach-jansson | mustach.c mustach-wrap.c mustach-jansson.c + libmustach | mustach.c mustach-wrap.c mustach-{cjson,json-c,jansson}.c + +There is no dependencies of a library to an other. This is intended and doesn't +hurt today because the code is small. + +### Testing + +The makefile offers the way to execute basic tests. Just type `make test`. + +By default, if valgrind is available, tests are using it. It can be disabled +by typing `make test valgrind=no` or `NOVALGRIND=1 make test`. + +## Extensions + +The current implementation provides extensions to specifications of **mustache**. +This extensions can be activated or deactivated using flags. + +Here is the summary. + + Flag name | Description + -------------------------------+------------------------------------------------ + Mustach_With_Colon | Explicit tag substitution with colon + Mustach_With_EmptyTag | Empty Tag Allowed + -------------------------------+------------------------------------------------ + Mustach_With_Equal | Value Testing Equality + Mustach_With_Compare | Value Comparing + Mustach_With_JsonPointer | Interpret JSON Pointers + Mustach_With_ObjectIter | Iteration On Objects + Mustach_With_EscFirstCmp | Escape First Compare + Mustach_With_ErrorUndefined | Error when a requested tag is undefined + -------------------------------+------------------------------------------------ + Mustach_With_AllExtensions | Activate all known extensions + Mustach_With_NoExtensions | Disable any extension + +For the details, see below. + +### Explicit Tag Substitution With Colon (Mustach_With_Colon) + +In somecases the name of the key used for substitution begins with a +character reserved for mustach: one of `#`, `^`, `/`, `&`, `{`, `>` and `=`. + +This extension introduces the special character `:` to explicitly +tell mustach to just substitute the value. So `:` becomes a new special +character. + +This is a core extension implemented in file **mustach.c**. + +### Empty Tag Allowed (Mustach_With_EmptyTag) + +When an empty tag is found, instead of automatically raising the error +MUSTACH\_ERROR\_EMPTY\_TAG pass it. + +This is a core extension implemented in file **mustach.c**. + +### Value Testing Equality (Mustach_With_Equal) + +This extension allows you to test the value of the selected key. +It allows to write `key=value` (matching test) or `key=!value` +(not matching test) in any query. + +This is a wrap extension implemented in file **mustach-wrap.c**. + +### Value Comparing (Mustach_With_Compare) + +These extension extends the extension for testing equality to also +compare values if greater or lesser. +Its allows to write `key>value` (greater), `key>=value` (greater or equal), +`key<value` (lesser) and `key<=value` (lesser or equal). + +It the comparator sign appears in the first column it is ignored +as if it was escaped. + +This is a wrap extension implemented in file **mustach-wrap.c**. + +### Interpret JSON Pointers (Mustach_With_JsonPointer) + +This extension allows to use JSON pointers as defined in IETF RFC 6901. +If active, any key starting with "/" is a JSON pointer. +This implies to use the colon to introduce JSON keys. + +A special escaping is used for `=`, `<`, `>` signs when +values comparisons are enabled: `~=` gives `=` in the key. + +This is a wrap extension implemented in file **mustach-wrap.c**. + +### Iteration On Objects (Mustach_With_ObjectIter) + +With this extension, using the pattern `{{#X.*}}...{{/X.*}}` +allows to iterate on fields of `X`. + +Example: + +- `{{s.*}} {{*}}:{{.}}{{/s.*}}` applied on `{"s":{"a":1,"b":true}}` produces ` a:1 b:true` + +Here the single star `{{*}}` is replaced by the iterated key +and the single dot `{{.}}` is replaced by its value. + +This is a wrap extension implemented in file **mustach-wrap.c**. + +### Error when a requested tag is undefined (Mustach_With_ErrorUndefined) + +Report the error MUSTACH_ERROR_UNDEFINED_TAG when a requested tag +is not defined. + +This is a wrap extension implemented in file **mustach-wrap.c**. + +### Access To Current Value + +*this was an extension but is now always enforced* + +The value of the current field can be accessed using single dot. + +Examples: + +- `{{#key}}{{.}}{{/key}}` applied to `{"key":3.14}` produces `3.14` +- `{{#array}} {{.}}{{/array}}` applied to `{"array":[1,2]}` produces ` 1 2`. + +This is a wrap extension implemented in file **mustach-wrap.c**. + +### Partial Data First + +*this was an extension but is now always enforced* + +The default resolution for partial pattern like `{{> name}}` +is to search for `name` in the current json context and +as a file named `name` or if not found `name.mustache`. + +By default, the order of the search is (1) as a file, +and if not found, (2) in the current json context. + +When this option is set, the order is reverted and content +of partial is search (1) in the current json context, +and if not found, (2) as a file. + +That option is useful to keep the compatibility with +versions of *mustach* anteriors to 1.2.0. + +This is a wrap extension implemented in file **mustach-wrap.c**. + +### Escape First Compare + +This extension automatically escapes comparisons appears as +first characters. + +This is a wrap extension implemented in file **mustach-wrap.c**. + +## Difference with version 0.99 and previous + +### Extensions + +The extensions can no more be removed at compile time, use +flags to select your required extension on need. + +### Name of functions + +Names of functions were improved. Old names remain but are obsolete +and legacy. Their removal in far future versions is possible. + +The table below summarize the changes. + + legacy name | name since version 1.0.0 + ------------------+----------------------- + fmustach | mustach_file + fdmustach | mustach_fd + mustach | mustach_mem + fmustach_json_c | mustach_json_c_file + fdmustach_json_c | mustach_json_c_fd + mustach_json_c | mustach_json_c_mem + mustach_json_c | mustach_json_c_write diff --git a/src/templating/dotest.sh b/src/templating/dotest.sh new file mode 100755 index 000000000..32f575c2e --- /dev/null +++ b/src/templating/dotest.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +# Exit, with error message (hard failure) +exit_fail() { + echo " FAIL: " "$@" >&2 + exit 1 +} + +mustach="${mustach:-../mustach}" +echo "starting test" +if ! valgrind --version 2> /dev/null +then + $mustach "$@" > resu.last || exit_fail "ERROR! mustach command failed ($?)!" +else + valgrind $mustach "$@" > resu.last 2> vg.last || exit_fail "ERROR! valgrind + mustach command failed ($?)!" + sed -i 's:^==[0-9]*== ::' vg.last + awk '/^ *total heap usage: .* allocs, .* frees,.*/{if($$4-$$6)exit(1)}' vg.last || exit_fail "ERROR! Alloc/Free issue" +fi +if diff -w resu.ref resu.last +then + echo "result ok" +else + exit_fail "ERROR! Result differs" +fi +echo +exit 0 diff --git a/src/templating/meson.build b/src/templating/meson.build new file mode 100644 index 000000000..c7ecc8dfc --- /dev/null +++ b/src/templating/meson.build @@ -0,0 +1,12 @@ +project('mustach', 'c', + version: '1.0.0' +) + +mustach_inc = include_directories('.') +mustach_lib = shared_library('mustach', + 'mustach.c', + include_directories: mustach_inc +) + +mustach_dep = declare_dependency(link_with: mustach_lib, + include_directories: mustach_inc) diff --git a/src/templating/mustach-cjson.c b/src/templating/mustach-cjson.c new file mode 100644 index 000000000..ee65c8038 --- /dev/null +++ b/src/templating/mustach-cjson.c @@ -0,0 +1,258 @@ +/* + Author: José Bollo <jobol@nonadev.net> + + https://gitlab.com/jobol/mustach + + SPDX-License-Identifier: ISC +*/ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> + +#include "mustach.h" +#include "mustach-wrap.h" +#include "mustach-cjson.h" + +struct expl { + cJSON null; + cJSON *root; + cJSON *selection; + int depth; + struct { + cJSON *cont; + cJSON *obj; + cJSON *next; + int is_objiter; + } stack[MUSTACH_MAX_DEPTH]; +}; + +static int start(void *closure) +{ + struct expl *e = closure; + e->depth = 0; + memset(&e->null, 0, sizeof e->null); + e->null.type = cJSON_NULL; + e->selection = &e->null; + e->stack[0].cont = NULL; + e->stack[0].obj = e->root; + return MUSTACH_OK; +} + +static int compare(void *closure, const char *value) +{ + struct expl *e = closure; + cJSON *o = e->selection; + double d; + + if (cJSON_IsNumber(o)) { + d = o->valuedouble - atof(value); + return d < 0 ? -1 : d > 0 ? 1 : 0; + } else if (cJSON_IsString(o)) { + return strcmp(o->valuestring, value); + } else if (cJSON_IsTrue(o)) { + return strcmp("true", value); + } else if (cJSON_IsFalse(o)) { + return strcmp("false", value); + } else if (cJSON_IsNull(o)) { + return strcmp("null", value); + } else { + return 1; + } +} + +static int sel(void *closure, const char *name) +{ + struct expl *e = closure; + cJSON *o; + int i, r; + + if (name == NULL) { + o = e->stack[e->depth].obj; + r = 1; + } else { + i = e->depth; + while (i >= 0 && !(o = cJSON_GetObjectItemCaseSensitive(e->stack[i].obj, name))) + i--; + if (i >= 0) + r = 1; + else { + o = &e->null; + r = 0; + } + } + e->selection = o; + return r; +} + +static int subsel(void *closure, const char *name) +{ + struct expl *e = closure; + cJSON *o = NULL; + int r = 0; + + if (cJSON_IsObject(e->selection)) { + o = cJSON_GetObjectItemCaseSensitive(e->selection, name); + r = o != NULL; + } + else if (cJSON_IsArray(e->selection) && *name) { + char *end; + int idx = (int)strtol(name, &end, 10); + if (!*end && idx >= 0 && idx < cJSON_GetArraySize(e->selection)) { + o = cJSON_GetArrayItem(e->selection, idx); + r = 1; + } + } + if (r) + e->selection = o; + return r; +} + +static int enter(void *closure, int objiter) +{ + struct expl *e = closure; + cJSON *o; + + if (++e->depth >= MUSTACH_MAX_DEPTH) + return MUSTACH_ERROR_TOO_DEEP; + + o = e->selection; + e->stack[e->depth].is_objiter = 0; + if (objiter) { + if (! cJSON_IsObject(o)) + goto not_entering; + if (o->child == NULL) + goto not_entering; + e->stack[e->depth].obj = o->child; + e->stack[e->depth].next = o->child->next; + e->stack[e->depth].cont = o; + e->stack[e->depth].is_objiter = 1; + } else if (cJSON_IsArray(o)) { + if (o->child == NULL) + goto not_entering; + e->stack[e->depth].obj = o->child; + e->stack[e->depth].next = o->child->next; + e->stack[e->depth].cont = o; + } else if ((cJSON_IsObject(o) && o->child != NULL) + || cJSON_IsTrue(o) + || (cJSON_IsString(o) && cJSON_GetStringValue(o)[0] != '\0') + || (cJSON_IsNumber(o) && cJSON_GetNumberValue(o) != 0)) { + e->stack[e->depth].obj = o; + e->stack[e->depth].cont = NULL; + e->stack[e->depth].next = NULL; + } else + goto not_entering; + return 1; + +not_entering: + e->depth--; + return 0; +} + +static int next(void *closure) +{ + struct expl *e = closure; + cJSON *o; + + if (e->depth <= 0) + return MUSTACH_ERROR_CLOSING; + + o = e->stack[e->depth].next; + if (o == NULL) + return 0; + + e->stack[e->depth].obj = o; + e->stack[e->depth].next = o->next; + return 1; +} + +static int leave(void *closure) +{ + struct expl *e = closure; + + if (e->depth <= 0) + return MUSTACH_ERROR_CLOSING; + + e->depth--; + return 0; +} + +static int get(void *closure, struct mustach_sbuf *sbuf, int key) +{ + struct expl *e = closure; + const char *s; + int d; + + if (key) { + s = ""; + for (d = e->depth ; d >= 0 ; d--) + if (e->stack[d].is_objiter) { + s = e->stack[d].obj->string; + break; + } + } + else if (cJSON_IsString(e->selection)) + s = e->selection->valuestring; + else if (cJSON_IsNull(e->selection)) + s = ""; + else { + s = cJSON_PrintUnformatted(e->selection); + if (s == NULL) + return MUSTACH_ERROR_SYSTEM; + sbuf->freecb = cJSON_free; + } + sbuf->value = s; + return 1; +} + +const struct mustach_wrap_itf mustach_cJSON_wrap_itf = { + .start = start, + .stop = NULL, + .compare = compare, + .sel = sel, + .subsel = subsel, + .enter = enter, + .next = next, + .leave = leave, + .get = get +}; + +int mustach_cJSON_file(const char *template, size_t length, cJSON *root, int flags, FILE *file) +{ + struct expl e; + e.root = root; + return mustach_wrap_file(template, length, &mustach_cJSON_wrap_itf, &e, flags, file); +} + +int mustach_cJSON_fd(const char *template, size_t length, cJSON *root, int flags, int fd) +{ + struct expl e; + e.root = root; + return mustach_wrap_fd(template, length, &mustach_cJSON_wrap_itf, &e, flags, fd); +} + +int mustach_cJSON_mem(const char *template, size_t length, cJSON *root, int flags, char **result, size_t *size) +{ + struct expl e; + e.root = root; + return mustach_wrap_mem(template, length, &mustach_cJSON_wrap_itf, &e, flags, result, size); +} + +int mustach_cJSON_write(const char *template, size_t length, cJSON *root, int flags, mustach_write_cb_t *writecb, void *closure) +{ + struct expl e; + e.root = root; + return mustach_wrap_write(template, length, &mustach_cJSON_wrap_itf, &e, flags, writecb, closure); +} + +int mustach_cJSON_emit(const char *template, size_t length, cJSON *root, int flags, mustach_emit_cb_t *emitcb, void *closure) +{ + struct expl e; + e.root = root; + return mustach_wrap_emit(template, length, &mustach_cJSON_wrap_itf, &e, flags, emitcb, closure); +} + diff --git a/src/templating/mustach-cjson.h b/src/templating/mustach-cjson.h new file mode 100644 index 000000000..e049415f8 --- /dev/null +++ b/src/templating/mustach-cjson.h @@ -0,0 +1,96 @@ +/* + Author: José Bollo <jobol@nonadev.net> + + https://gitlab.com/jobol/mustach + + SPDX-License-Identifier: ISC +*/ + +#ifndef _mustach_cJSON_h_included_ +#define _mustach_cJSON_h_included_ + +/* + * mustach-cjson is intended to make integration of cJSON + * library by providing integrated functions. + */ + +#include <cjson/cJSON.h> +#include "mustach-wrap.h" + +/** + * Wrap interface used internally by mustach cJSON functions. + * Can be used for overriding behaviour. + */ +extern const struct mustach_wrap_itf mustach_cJSON_wrap_itf; + +/** + * mustach_cJSON_file - Renders the mustache 'template' in 'file' for 'root'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @root: the root json object to render + * @file: the file where to write the result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_cJSON_file(const char *template, size_t length, cJSON *root, int flags, FILE *file); + +/** + * mustach_cJSON_fd - Renders the mustache 'template' in 'fd' for 'root'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @root: the root json object to render + * @fd: the file descriptor number where to write the result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_cJSON_fd(const char *template, size_t length, cJSON *root, int flags, int fd); + + +/** + * mustach_cJSON_mem - Renders the mustache 'template' in 'result' for 'root'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @root: the root json object to render + * @result: the pointer receiving the result when 0 is returned + * @size: the size of the returned result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_cJSON_mem(const char *template, size_t length, cJSON *root, int flags, char **result, size_t *size); + +/** + * mustach_cJSON_write - Renders the mustache 'template' for 'root' to custom writer 'writecb' with 'closure'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @root: the root json object to render + * @writecb: the function that write values + * @closure: the closure for the write function + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_cJSON_write(const char *template, size_t length, cJSON *root, int flags, mustach_write_cb_t *writecb, void *closure); + +/** + * mustach_cJSON_emit - Renders the mustache 'template' for 'root' to custom emiter 'emitcb' with 'closure'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @root: the root json object to render + * @emitcb: the function that emit values + * @closure: the closure for the write function + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_cJSON_emit(const char *template, size_t length, cJSON *root, int flags, mustach_emit_cb_t *emitcb, void *closure); + +#endif + diff --git a/src/templating/mustach-jansson.c b/src/templating/mustach-jansson.c new file mode 100644 index 000000000..d9b50b57e --- /dev/null +++ b/src/templating/mustach-jansson.c @@ -0,0 +1,271 @@ +/* + Author: José Bollo <jobol@nonadev.net> + + https://gitlab.com/jobol/mustach + + SPDX-License-Identifier: ISC +*/ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include <stdio.h> +#include <string.h> + +#include "mustach.h" +#include "mustach-wrap.h" +#include "mustach-jansson.h" + +struct expl { + json_t *root; + json_t *selection; + int depth; + struct { + json_t *cont; + json_t *obj; + void *iter; + int is_objiter; + size_t index, count; + } stack[MUSTACH_MAX_DEPTH]; +}; + +static int start(void *closure) +{ + struct expl *e = closure; + e->depth = 0; + e->selection = json_null(); + e->stack[0].cont = NULL; + e->stack[0].obj = e->root; + e->stack[0].index = 0; + e->stack[0].count = 1; + return MUSTACH_OK; +} + +static int compare(void *closure, const char *value) +{ + struct expl *e = closure; + json_t *o = e->selection; + double d; + json_int_t i; + + switch (json_typeof(o)) { + case JSON_REAL: + d = json_number_value(o) - atof(value); + return d < 0 ? -1 : d > 0 ? 1 : 0; + case JSON_INTEGER: + i = (json_int_t)json_integer_value(o) - (json_int_t)atoll(value); + return i < 0 ? -1 : i > 0 ? 1 : 0; + case JSON_STRING: + return strcmp(json_string_value(o), value); + case JSON_TRUE: + return strcmp("true", value); + case JSON_FALSE: + return strcmp("false", value); + case JSON_NULL: + return strcmp("null", value); + default: + return 1; + } +} + +static int sel(void *closure, const char *name) +{ + struct expl *e = closure; + json_t *o; + int i, r; + + if (name == NULL) { + o = e->stack[e->depth].obj; + r = 1; + } else { + i = e->depth; + while (i >= 0 && !(o = json_object_get(e->stack[i].obj, name))) + i--; + if (i >= 0) + r = 1; + else { + o = json_null(); + r = 0; + } + } + e->selection = o; + return r; +} + +static int subsel(void *closure, const char *name) +{ + struct expl *e = closure; + json_t *o = NULL; + int r = 0; + + if (json_is_object(e->selection)) { + o = json_object_get(e->selection, name); + r = o != NULL; + } + else if (json_is_array(e->selection)) { + char *end; + size_t idx = (size_t)strtol(name, &end, 10); + if (!*end && idx < json_array_size(e->selection)) { + o = json_array_get(e->selection, idx); + r = 1; + } + } + if (r) + e->selection = o; + return r; +} + +static int enter(void *closure, int objiter) +{ + struct expl *e = closure; + json_t *o; + + if (++e->depth >= MUSTACH_MAX_DEPTH) + return MUSTACH_ERROR_TOO_DEEP; + + o = e->selection; + e->stack[e->depth].is_objiter = 0; + if (objiter) { + if (!json_is_object(o)) + goto not_entering; + e->stack[e->depth].iter = json_object_iter(o); + if (e->stack[e->depth].iter == NULL) + goto not_entering; + e->stack[e->depth].obj = json_object_iter_value(e->stack[e->depth].iter); + e->stack[e->depth].cont = o; + e->stack[e->depth].is_objiter = 1; + } else if (json_is_array(o)) { + e->stack[e->depth].count = json_array_size(o); + if (e->stack[e->depth].count == 0) + goto not_entering; + e->stack[e->depth].cont = o; + e->stack[e->depth].obj = json_array_get(o, 0); + e->stack[e->depth].index = 0; + } else if ((json_is_object(o) && json_object_size(o)) + || json_is_true(o) + || (json_is_string(o) && json_string_length(o) > 0) + || (json_is_integer(o) && json_integer_value(o) != 0) + || (json_is_real(o) && json_real_value(o) != 0)) { + e->stack[e->depth].count = 1; + e->stack[e->depth].cont = NULL; + e->stack[e->depth].obj = o; + e->stack[e->depth].index = 0; + } else + goto not_entering; + return 1; + +not_entering: + e->depth--; + return 0; +} + +static int next(void *closure) +{ + struct expl *e = closure; + + if (e->depth <= 0) + return MUSTACH_ERROR_CLOSING; + + if (e->stack[e->depth].is_objiter) { + e->stack[e->depth].iter = json_object_iter_next(e->stack[e->depth].cont, e->stack[e->depth].iter); + if (e->stack[e->depth].iter == NULL) + return 0; + e->stack[e->depth].obj = json_object_iter_value(e->stack[e->depth].iter); + return 1; + } + + e->stack[e->depth].index++; + if (e->stack[e->depth].index >= e->stack[e->depth].count) + return 0; + + e->stack[e->depth].obj = json_array_get(e->stack[e->depth].cont, e->stack[e->depth].index); + return 1; +} + +static int leave(void *closure) +{ + struct expl *e = closure; + + if (e->depth <= 0) + return MUSTACH_ERROR_CLOSING; + + e->depth--; + return 0; +} + +static int get(void *closure, struct mustach_sbuf *sbuf, int key) +{ + struct expl *e = closure; + const char *s; + int d; + + if (key) { + s = ""; + for (d = e->depth ; d >= 0 ; d--) + if (e->stack[d].is_objiter) { + s = json_object_iter_key(e->stack[d].iter); + break; + } + } + else if (json_is_string(e->selection)) + s = json_string_value(e->selection); + else if (json_is_null(e->selection)) + s = ""; + else { + s = json_dumps(e->selection, JSON_ENCODE_ANY | JSON_COMPACT); + if (s == NULL) + return MUSTACH_ERROR_SYSTEM; + sbuf->freecb = free; + } + sbuf->value = s; + return 1; +} + +const struct mustach_wrap_itf mustach_jansson_wrap_itf = { + .start = start, + .stop = NULL, + .compare = compare, + .sel = sel, + .subsel = subsel, + .enter = enter, + .next = next, + .leave = leave, + .get = get +}; + +int mustach_jansson_file(const char *template, size_t length, json_t *root, int flags, FILE *file) +{ + struct expl e; + e.root = root; + return mustach_wrap_file(template, length, &mustach_jansson_wrap_itf, &e, flags, file); +} + +int mustach_jansson_fd(const char *template, size_t length, json_t *root, int flags, int fd) +{ + struct expl e; + e.root = root; + return mustach_wrap_fd(template, length, &mustach_jansson_wrap_itf, &e, flags, fd); +} + +int mustach_jansson_mem(const char *template, size_t length, json_t *root, int flags, char **result, size_t *size) +{ + struct expl e; + e.root = root; + return mustach_wrap_mem(template, length, &mustach_jansson_wrap_itf, &e, flags, result, size); +} + +int mustach_jansson_write(const char *template, size_t length, json_t *root, int flags, mustach_write_cb_t *writecb, void *closure) +{ + struct expl e; + e.root = root; + return mustach_wrap_write(template, length, &mustach_jansson_wrap_itf, &e, flags, writecb, closure); +} + +int mustach_jansson_emit(const char *template, size_t length, json_t *root, int flags, mustach_emit_cb_t *emitcb, void *closure) +{ + struct expl e; + e.root = root; + return mustach_wrap_emit(template, length, &mustach_jansson_wrap_itf, &e, flags, emitcb, closure); +} + diff --git a/src/templating/mustach-jansson.h b/src/templating/mustach-jansson.h new file mode 100644 index 000000000..8def948e0 --- /dev/null +++ b/src/templating/mustach-jansson.h @@ -0,0 +1,96 @@ +/* + Author: José Bollo <jobol@nonadev.net> + + https://gitlab.com/jobol/mustach + + SPDX-License-Identifier: ISC +*/ + +#ifndef _mustach_jansson_h_included_ +#define _mustach_jansson_h_included_ + +/* + * mustach-jansson is intended to make integration of jansson + * library by providing integrated functions. + */ + +#include <jansson.h> +#include "mustach-wrap.h" + +/** + * Wrap interface used internally by mustach jansson functions. + * Can be used for overriding behaviour. + */ +extern const struct mustach_wrap_itf mustach_jansson_wrap_itf; + +/** + * mustach_jansson_file - Renders the mustache 'template' in 'file' for 'root'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @root: the root json object to render + * @file: the file where to write the result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_jansson_file(const char *template, size_t length, json_t *root, int flags, FILE *file); + +/** + * mustach_jansson_fd - Renders the mustache 'template' in 'fd' for 'root'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @root: the root json object to render + * @fd: the file descriptor number where to write the result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_jansson_fd(const char *template, size_t length, json_t *root, int flags, int fd); + + +/** + * mustach_jansson_mem - Renders the mustache 'template' in 'result' for 'root'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @root: the root json object to render + * @result: the pointer receiving the result when 0 is returned + * @size: the size of the returned result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_jansson_mem(const char *template, size_t length, json_t *root, int flags, char **result, size_t *size); + +/** + * mustach_jansson_write - Renders the mustache 'template' for 'root' to custom writer 'writecb' with 'closure'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @root: the root json object to render + * @writecb: the function that write values + * @closure: the closure for the write function + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_jansson_write(const char *template, size_t length, json_t *root, int flags, mustach_write_cb_t *writecb, void *closure); + +/** + * mustach_jansson_emit - Renders the mustache 'template' for 'root' to custom emiter 'emitcb' with 'closure'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @root: the root json object to render + * @emitcb: the function that emit values + * @closure: the closure for the write function + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_jansson_emit(const char *template, size_t length, json_t *root, int flags, mustach_emit_cb_t *emitcb, void *closure); + +#endif + diff --git a/src/templating/mustach-json-c.c b/src/templating/mustach-json-c.c new file mode 100644 index 000000000..75251c07e --- /dev/null +++ b/src/templating/mustach-json-c.c @@ -0,0 +1,284 @@ +/* + Author: José Bollo <jobol@nonadev.net> + + https://gitlab.com/jobol/mustach + + SPDX-License-Identifier: ISC +*/ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include <stdio.h> +#include <string.h> + +#include "mustach.h" +#include "mustach-wrap.h" +#include "mustach-json-c.h" + +struct expl { + struct json_object *root; + struct json_object *selection; + int depth; + struct { + struct json_object *cont; + struct json_object *obj; + struct json_object_iterator iter; + struct json_object_iterator enditer; + int is_objiter; + int index, count; + } stack[MUSTACH_MAX_DEPTH]; +}; + +static int start(void *closure) +{ + struct expl *e = closure; + e->depth = 0; + e->selection = NULL; + e->stack[0].cont = NULL; + e->stack[0].obj = e->root; + e->stack[0].index = 0; + e->stack[0].count = 1; + return MUSTACH_OK; +} + +static int compare(void *closure, const char *value) +{ + struct expl *e = closure; + struct json_object *o = e->selection; + double d; + int64_t i; + + switch (json_object_get_type(o)) { + case json_type_double: + d = json_object_get_double(o) - atof(value); + return d < 0 ? -1 : d > 0 ? 1 : 0; + case json_type_int: + i = json_object_get_int64(o) - (int64_t)atoll(value); + return i < 0 ? -1 : i > 0 ? 1 : 0; + default: + return strcmp(json_object_get_string(o), value); + } +} + +static int sel(void *closure, const char *name) +{ + struct expl *e = closure; + struct json_object *o; + int i, r; + + if (name == NULL) { + o = e->stack[e->depth].obj; + r = 1; + } else { + i = e->depth; + while (i >= 0 && !json_object_object_get_ex(e->stack[i].obj, name, &o)) + i--; + if (i >= 0) + r = 1; + else { + o = NULL; + r = 0; + } + } + e->selection = o; + return r; +} + +static int subsel(void *closure, const char *name) +{ + struct expl *e = closure; + struct json_object *o = NULL; + int r = 0; + + if (json_object_is_type(e->selection, json_type_object)) + r = json_object_object_get_ex(e->selection, name, &o); + else if (json_object_is_type(e->selection, json_type_array)) { + char *end; + size_t idx = (size_t)strtol(name, &end, 10); + if (!*end && idx < json_object_array_length(e->selection)) { + o = json_object_array_get_idx(e->selection, idx); + r = 1; + } + } + if (r) + e->selection = o; + return r; +} + +static int enter(void *closure, int objiter) +{ + struct expl *e = closure; + struct json_object *o; + + if (++e->depth >= MUSTACH_MAX_DEPTH) + return MUSTACH_ERROR_TOO_DEEP; + + o = e->selection; + e->stack[e->depth].is_objiter = 0; + if (objiter) { + if (!json_object_is_type(o, json_type_object)) + goto not_entering; + + e->stack[e->depth].iter = json_object_iter_begin(o); + e->stack[e->depth].enditer = json_object_iter_end(o); + if (json_object_iter_equal(&e->stack[e->depth].iter, &e->stack[e->depth].enditer)) + goto not_entering; + e->stack[e->depth].obj = json_object_iter_peek_value(&e->stack[e->depth].iter); + e->stack[e->depth].cont = o; + e->stack[e->depth].is_objiter = 1; + } else if (json_object_is_type(o, json_type_array)) { + e->stack[e->depth].count = json_object_array_length(o); + if (e->stack[e->depth].count == 0) + goto not_entering; + e->stack[e->depth].cont = o; + e->stack[e->depth].obj = json_object_array_get_idx(o, 0); + e->stack[e->depth].index = 0; + } else if ((json_object_is_type(o, json_type_object) && json_object_object_length(o) > 0) + || json_object_get_boolean(o)) { + e->stack[e->depth].count = 1; + e->stack[e->depth].cont = NULL; + e->stack[e->depth].obj = o; + e->stack[e->depth].index = 0; + } else + goto not_entering; + return 1; + +not_entering: + e->depth--; + return 0; +} + +static int next(void *closure) +{ + struct expl *e = closure; + + if (e->depth <= 0) + return MUSTACH_ERROR_CLOSING; + + if (e->stack[e->depth].is_objiter) { + json_object_iter_next(&e->stack[e->depth].iter); + if (json_object_iter_equal(&e->stack[e->depth].iter, &e->stack[e->depth].enditer)) + return 0; + e->stack[e->depth].obj = json_object_iter_peek_value(&e->stack[e->depth].iter); + return 1; + } + + e->stack[e->depth].index++; + if (e->stack[e->depth].index >= e->stack[e->depth].count) + return 0; + + e->stack[e->depth].obj = json_object_array_get_idx(e->stack[e->depth].cont, e->stack[e->depth].index); + return 1; +} + +static int leave(void *closure) +{ + struct expl *e = closure; + + if (e->depth <= 0) + return MUSTACH_ERROR_CLOSING; + + e->depth--; + return 0; +} + +static int get(void *closure, struct mustach_sbuf *sbuf, int key) +{ + struct expl *e = closure; + const char *s; + int d; + + if (key) { + s = ""; + for (d = e->depth ; d >= 0 ; d--) + if (e->stack[d].is_objiter) { + s = json_object_iter_peek_name(&e->stack[d].iter); + break; + } + } + else + switch (json_object_get_type(e->selection)) { + case json_type_string: + s = json_object_get_string(e->selection); + break; + case json_type_null: + s = ""; + break; + default: + s = json_object_to_json_string_ext(e->selection, 0); + break; + } + sbuf->value = s; + return 1; +} + +const struct mustach_wrap_itf mustach_json_c_wrap_itf = { + .start = start, + .stop = NULL, + .compare = compare, + .sel = sel, + .subsel = subsel, + .enter = enter, + .next = next, + .leave = leave, + .get = get +}; + +int mustach_json_c_file(const char *template, size_t length, struct json_object *root, int flags, FILE *file) +{ + struct expl e; + e.root = root; + return mustach_wrap_file(template, length, &mustach_json_c_wrap_itf, &e, flags, file); +} + +int mustach_json_c_fd(const char *template, size_t length, struct json_object *root, int flags, int fd) +{ + struct expl e; + e.root = root; + return mustach_wrap_fd(template, length, &mustach_json_c_wrap_itf, &e, flags, fd); +} + +int mustach_json_c_mem(const char *template, size_t length, struct json_object *root, int flags, char **result, size_t *size) +{ + struct expl e; + e.root = root; + return mustach_wrap_mem(template, length, &mustach_json_c_wrap_itf, &e, flags, result, size); +} + +int mustach_json_c_write(const char *template, size_t length, struct json_object *root, int flags, mustach_write_cb_t *writecb, void *closure) +{ + struct expl e; + e.root = root; + return mustach_wrap_write(template, length, &mustach_json_c_wrap_itf, &e, flags, writecb, closure); +} + +int mustach_json_c_emit(const char *template, size_t length, struct json_object *root, int flags, mustach_emit_cb_t *emitcb, void *closure) +{ + struct expl e; + e.root = root; + return mustach_wrap_emit(template, length, &mustach_json_c_wrap_itf, &e, flags, emitcb, closure); +} + +int fmustach_json_c(const char *template, struct json_object *root, FILE *file) +{ + return mustach_json_c_file(template, 0, root, -1, file); +} + +int fdmustach_json_c(const char *template, struct json_object *root, int fd) +{ + return mustach_json_c_fd(template, 0, root, -1, fd); +} + +int mustach_json_c(const char *template, struct json_object *root, char **result, size_t *size) +{ + return mustach_json_c_mem(template, 0, root, -1, result, size); +} + +int umustach_json_c(const char *template, struct json_object *root, mustach_write_cb_t *writecb, void *closure) +{ + return mustach_json_c_write(template, 0, root, -1, writecb, closure); +} + + diff --git a/src/templating/mustach-json-c.h b/src/templating/mustach-json-c.h new file mode 100644 index 000000000..50846c6cb --- /dev/null +++ b/src/templating/mustach-json-c.h @@ -0,0 +1,160 @@ +/* + Author: José Bollo <jobol@nonadev.net> + + https://gitlab.com/jobol/mustach + + SPDX-License-Identifier: ISC +*/ + +#ifndef _mustach_json_c_h_included_ +#define _mustach_json_c_h_included_ + +/* + * mustach-json-c is intended to make integration of json-c + * library by providing integrated functions. + */ + +#include <json-c/json.h> +#include "mustach-wrap.h" + +/** + * Wrap interface used internally by mustach json-c functions. + * Can be used for overriding behaviour. + */ +extern const struct mustach_wrap_itf mustach_json_c_wrap_itf; + +/** + * mustach_json_c_file - Renders the mustache 'template' in 'file' for 'root'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @root: the root json object to render + * @file: the file where to write the result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_json_c_file(const char *template, size_t length, struct json_object *root, int flags, FILE *file); + +/** + * mustach_json_c_fd - Renders the mustache 'template' in 'fd' for 'root'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @root: the root json object to render + * @fd: the file descriptor number where to write the result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_json_c_fd(const char *template, size_t length, struct json_object *root, int flags, int fd); + +/** + * mustach_json_c_mem - Renders the mustache 'template' in 'result' for 'root'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @root: the root json object to render + * @result: the pointer receiving the result when 0 is returned + * @size: the size of the returned result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_json_c_mem(const char *template, size_t length, struct json_object *root, int flags, char **result, size_t *size); + +/** + * mustach_json_c_write - Renders the mustache 'template' for 'root' to custom writer 'writecb' with 'closure'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @root: the root json object to render + * @writecb: the function that write values + * @closure: the closure for the write function + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_json_c_write(const char *template, size_t length, struct json_object *root, int flags, mustach_write_cb_t *writecb, void *closure); + +/** + * mustach_json_c_emit - Renders the mustache 'template' for 'root' to custom emiter 'emitcb' with 'closure'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @root: the root json object to render + * @emitcb: the function that emit values + * @closure: the closure for the write function + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_json_c_emit(const char *template, size_t length, struct json_object *root, int flags, mustach_emit_cb_t *emitcb, void *closure); + +/*************************************************************************** +* compatibility with version before 1.0 +*/ + +/** + * OBSOLETE use mustach_json_c_file + * + * fmustach_json_c - Renders the mustache 'template' in 'file' for 'root'. + * + * @template: the template string to instantiate + * @root: the root json object to render + * @file: the file where to write the result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ + +DEPRECATED_MUSTACH(extern int fmustach_json_c(const char *template, struct json_object *root, FILE *file)); + +/** + * OBSOLETE use mustach_json_c_fd + * + * fdmustach_json_c - Renders the mustache 'template' in 'fd' for 'root'. + * + * @template: the template string to instantiate + * @root: the root json object to render + * @fd: the file descriptor number where to write the result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ + +DEPRECATED_MUSTACH(extern int fdmustach_json_c(const char *template, struct json_object *root, int fd)); + +/** + * OBSOLETE use mustach_json_c_mem + * + * mustach_json_c - Renders the mustache 'template' in 'result' for 'root'. + * + * @template: the template string to instantiate + * @root: the root json object to render + * @result: the pointer receiving the result when 0 is returned + * @size: the size of the returned result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ + +DEPRECATED_MUSTACH(extern int mustach_json_c(const char *template, struct json_object *root, char **result, size_t *size)); + +/** + * OBSOLETE use mustach_json_c_write + * + * umustach_json_c - Renders the mustache 'template' for 'root' to custom writer 'writecb' with 'closure'. + * + * @template: the template string to instantiate + * @root: the root json object to render + * @writecb: the function that write values + * @closure: the closure for the write function + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +typedef mustach_write_cb_t *mustach_json_write_cb; +DEPRECATED_MUSTACH(extern int umustach_json_c(const char *template, struct json_object *root, mustach_write_cb_t *writecb, void *closure)); + +#endif diff --git a/src/templating/mustach-original-Makefile b/src/templating/mustach-original-Makefile new file mode 100644 index 000000000..aee827583 --- /dev/null +++ b/src/templating/mustach-original-Makefile @@ -0,0 +1,305 @@ +# version +MAJOR := 1 +MINOR := 2 +REVIS := 7 + +# installation settings +DESTDIR ?= +PREFIX ?= /usr/local +BINDIR ?= $(PREFIX)/bin +LIBDIR ?= $(PREFIX)/lib +INCLUDEDIR ?= $(PREFIX)/include +MANDIR ?= $(PREFIX)/share/man +PKGDIR ?= $(LIBDIR)/pkgconfig + +# Tools (sed must be GNU sed) +SED ?= sed +INSTALL ?= install + +# initial settings +VERSION := $(MAJOR).$(MINOR).$(REVIS) +SOVER := .$(MAJOR) +SOVEREV := .$(MAJOR).$(MINOR) + +HEADERS := mustach.h mustach-wrap.h +SPLITLIB := libmustach-core.so$(SOVEREV) +SPLITPC := libmustach-core.pc +COREOBJS := mustach.o mustach-wrap.o +SINGLEOBJS := $(COREOBJS) +SINGLEFLAGS := +SINGLELIBS := +TESTSPECS := +ALL := manuals + +# availability of CJSON +ifneq ($(cjson),no) + cjson_cflags := $(shell pkg-config --silence-errors --cflags libcjson) + cjson_libs := $(shell pkg-config --silence-errors --libs libcjson) + ifdef cjson_libs + cjson := yes + tool ?= cjson + HEADERS += mustach-cjson.h + SPLITLIB += libmustach-cjson.so$(SOVEREV) + SPLITPC += libmustach-cjson.pc + SINGLEOBJS += mustach-cjson.o + SINGLEFLAGS += ${cjson_cflags} + SINGLELIBS += ${cjson_libs} + TESTSPECS += test-specs/test-specs-cjson + else + ifeq ($(cjson),yes) + $(error Can't find required library cjson) + endif + cjson := no + endif +endif + +# availability of JSON-C +ifneq ($(jsonc),no) + jsonc_cflags := $(shell pkg-config --silence-errors --cflags json-c) + jsonc_libs := $(shell pkg-config --silence-errors --libs json-c) + ifdef jsonc_libs + jsonc := yes + tool ?= jsonc + HEADERS += mustach-json-c.h + SPLITLIB += libmustach-json-c.so$(SOVEREV) + SPLITPC += libmustach-json-c.pc + SINGLEOBJS += mustach-json-c.o + SINGLEFLAGS += ${jsonc_cflags} + SINGLELIBS += ${jsonc_libs} + TESTSPECS += test-specs/test-specs-json-c + else + ifeq ($(jsonc),yes) + $(error Can't find required library json-c) + endif + jsonc := no + endif +endif + +# availability of JANSSON +ifneq ($(jansson),no) + jansson_cflags := $(shell pkg-config --silence-errors --cflags jansson) + jansson_libs := $(shell pkg-config --silence-errors --libs jansson) + ifdef jansson_libs + jansson := yes + tool ?= jansson + HEADERS += mustach-jansson.h + SPLITLIB += libmustach-jansson.so$(SOVEREV) + SPLITPC += libmustach-jansson.pc + SINGLEOBJS += mustach-jansson.o + SINGLEFLAGS += ${jansson_cflags} + SINGLELIBS += ${jansson_libs} + TESTSPECS += test-specs/test-specs-jansson + else + ifeq ($(jansson),yes) + $(error Can't find required library jansson) + endif + jansson := no + endif +endif + +# tool +TOOLOBJS = mustach-tool.o $(COREOBJS) +tool ?= none +ifneq ($(tool),none) + ifeq ($(tool),cjson) + TOOLOBJS += mustach-cjson.o + TOOLFLAGS := ${cjson_cflags} -DTOOL=MUSTACH_TOOL_CJSON + TOOLLIBS := ${cjson_libs} + TOOLDEP := mustach-cjson.h + else ifeq ($(tool),jsonc) + TOOLOBJS += mustach-json-c.o + TOOLFLAGS := ${jsonc_cflags} -DTOOL=MUSTACH_TOOL_JSON_C + TOOLLIBS := ${jsonc_libs} + TOOLDEP := mustach-json-c.h + else ifeq ($(tool),jansson) + TOOLOBJS += mustach-jansson.o + TOOLFLAGS := ${jansson_cflags} -DTOOL=MUSTACH_TOOL_JANSSON + TOOLLIBS := ${jansson_libs} + TOOLDEP := mustach-jansson.h + else + $(error Unknown library $(tool) for tool) + endif + ifneq ($($(tool)),yes) + $(error No library found for tool $(tool)) + endif + ALL += mustach +endif + +# compute targets +libs ?= all +ifeq (${libs},split) + ALL += ${SPLITLIB} ${SPLITPC} +else ifeq (${libs},single) + ALL += libmustach.so$(SOVEREV) libmustach.pc +else ifeq (${libs},all) + ALL += libmustach.so$(SOVEREV) libmustach.pc ${SPLITLIB} ${SPLITPC} +else ifneq (${libs},none) + $(error Unknown libs $(libs)) +endif + +# display target +$(info tool = ${tool}) +$(info libs = ${libs}) +$(info jsonc = ${jsonc}) +$(info jansson = ${jansson}) +$(info cjson = ${cjson}) + +# settings + +EFLAGS = -fPIC -Wall -Wextra -DVERSION=${VERSION} + +ifeq ($(shell uname),Darwin) + LDFLAGS_single += -install_name $(LIBDIR)/libmustach.so$(SOVEREV) + LDFLAGS_core += -install_name $(LIBDIR)/libmustach-core.so$(SOVEREV) + LDFLAGS_cjson += -install_name $(LIBDIR)/libmustach-cjson.so$(SOVEREV) + LDFLAGS_jsonc += -install_name $(LIBDIR)/libmustach-json-c.so$(SOVEREV) + LDFLAGS_jansson += -install_name $(LIBDIR)/libmustach-jansson.so$(SOVEREV) +else + LDFLAGS_single += -Wl,-soname,libmustach.so$(SOVER) + LDFLAGS_core += -Wl,-soname,libmustach-core.so$(SOVER) + LDFLAGS_cjson += -Wl,-soname,libmustach-cjson.so$(SOVER) + LDFLAGS_jsonc += -Wl,-soname,libmustach-json-c.so$(SOVER) + LDFLAGS_jansson += -Wl,-soname,libmustach-jansson.so$(SOVER) +endif + +# targets + +.PHONY: all +all: ${ALL} + +mustach: $(TOOLOBJS) + $(CC) $(LDFLAGS) $(TOOLFLAGS) -o mustach $(TOOLOBJS) $(TOOLLIBS) + +libmustach.so$(SOVEREV): $(SINGLEOBJS) + $(CC) -shared $(LDFLAGS) $(LDFLAGS_single) -o $@ $^ $(SINGLELIBS) + +libmustach-core.so$(SOVEREV): $(COREOBJS) + $(CC) -shared $(LDFLAGS) $(LDFLAGS_core) -o $@ $(COREOBJS) $(lib_OBJ) + +libmustach-cjson.so$(SOVEREV): $(COREOBJS) mustach-cjson.o + $(CC) -shared $(LDFLAGS) $(LDFLAGS_cjson) -o $@ $^ $(cjson_libs) + +libmustach-json-c.so$(SOVEREV): $(COREOBJS) mustach-json-c.o + $(CC) -shared $(LDFLAGS) $(LDFLAGS_jsonc) -o $@ $^ $(jsonc_libs) + +libmustach-jansson.so$(SOVEREV): $(COREOBJS) mustach-jansson.o + $(CC) -shared $(LDFLAGS) $(LDFLAGS_jansson) -o $@ $^ $(jansson_libs) + +# pkgconfigs + +%.pc: pkgcfgs + $(SED) -E '/^==.*==$$/{h;d};x;/==$@==/{x;s/VERSION/$(VERSION)/;p;d};x;d' $< > $@ + +# objects + +mustach.o: mustach.c mustach.h + $(CC) -c $(EFLAGS) $(CFLAGS) -o $@ $< + +mustach-wrap.o: mustach-wrap.c mustach.h mustach-wrap.h + $(CC) -c $(EFLAGS) $(CFLAGS) -o $@ $< + +mustach-tool.o: mustach-tool.c mustach.h mustach-json-c.h $(TOOLDEP) + $(CC) -c $(EFLAGS) $(CFLAGS) $(TOOLFLAGS) -o $@ $< + +mustach-cjson.o: mustach-cjson.c mustach.h mustach-wrap.h mustach-cjson.h + $(CC) -c $(EFLAGS) $(CFLAGS) $(cjson_cflags) -o $@ $< + +mustach-json-c.o: mustach-json-c.c mustach.h mustach-wrap.h mustach-json-c.h + $(CC) -c $(EFLAGS) $(CFLAGS) $(jsonc_cflags) -o $@ $< + +mustach-jansson.o: mustach-jansson.c mustach.h mustach-wrap.h mustach-jansson.h + $(CC) -c $(EFLAGS) $(CFLAGS) $(jansson_cflags) -o $@ $< + +# installing +.PHONY: install +install: all + $(INSTALL) -d $(DESTDIR)$(BINDIR) + if test "${tool}" != "none"; then \ + $(INSTALL) -m0755 mustach $(DESTDIR)$(BINDIR)/; \ + fi + $(INSTALL) -d $(DESTDIR)$(INCLUDEDIR)/mustach + $(INSTALL) -m0644 $(HEADERS) $(DESTDIR)$(INCLUDEDIR)/mustach + $(INSTALL) -d $(DESTDIR)$(LIBDIR) + for x in libmustach*.so$(SOVEREV); do \ + $(INSTALL) -m0755 $$x $(DESTDIR)$(LIBDIR)/ ;\ + ln -sf $$x $(DESTDIR)$(LIBDIR)/$${x%.so.*}.so$(SOVER) ;\ + ln -sf $$x $(DESTDIR)$(LIBDIR)/$${x%.so.*}.so ;\ + done + $(INSTALL) -d $(DESTDIR)/$(PKGDIR) + $(INSTALL) -m0644 libmustach*.pc $(DESTDIR)/$(PKGDIR) + $(INSTALL) -d $(DESTDIR)/$(MANDIR)/man1 + $(INSTALL) -m0644 mustach.1.gz $(DESTDIR)/$(MANDIR)/man1 + +# deinstalling +.PHONY: uninstall +uninstall: + rm -f $(DESTDIR)$(BINDIR)/mustach + rm -f $(DESTDIR)$(LIBDIR)/libmustach*.so* + rm -rf $(DESTDIR)$(INCLUDEDIR)/mustach + +.PHONY: test test-basic test-specs +test: basic-tests spec-tests + +basic-tests: mustach + @$(MAKE) -C test1 test + @$(MAKE) -C test2 test + @$(MAKE) -C test3 test + @$(MAKE) -C test4 test + @$(MAKE) -C test5 test + @$(MAKE) -C test6 test + @$(MAKE) -C test7 test + @$(MAKE) -C test8 test + +spec-tests: $(TESTSPECS) + +test-specs/test-specs-%: test-specs/%-test-specs test-specs/specs + ./$< test-specs/spec/specs/[a-z]*.json > $@.last || true + diff $@.ref $@.last + +test-specs/cjson-test-specs.o: test-specs/test-specs.c mustach.h mustach-wrap.h mustach-cjson.h + $(CC) -I. -c $(EFLAGS) $(CFLAGS) $(cjson_cflags) -DTEST=TEST_CJSON -o $@ $< + +test-specs/cjson-test-specs: test-specs/cjson-test-specs.o mustach-cjson.o $(COREOBJS) + $(CC) $(LDFLAGS) -o $@ $^ $(cjson_libs) + +test-specs/json-c-test-specs.o: test-specs/test-specs.c mustach.h mustach-wrap.h mustach-json-c.h + $(CC) -I. -c $(EFLAGS) $(CFLAGS) $(jsonc_cflags) -DTEST=TEST_JSON_C -o $@ $< + +test-specs/json-c-test-specs: test-specs/json-c-test-specs.o mustach-json-c.o $(COREOBJS) + $(CC) $(LDFLAGS) -o $@ $^ $(jsonc_libs) + +test-specs/jansson-test-specs.o: test-specs/test-specs.c mustach.h mustach-wrap.h mustach-jansson.h + $(CC) -I. -c $(EFLAGS) $(CFLAGS) $(jansson_cflags) -DTEST=TEST_JANSSON -o $@ $< + +test-specs/jansson-test-specs: test-specs/jansson-test-specs.o mustach-jansson.o $(COREOBJS) + $(CC) $(LDFLAGS) -o $@ $^ $(jansson_libs) + +.PHONY: test-specs/specs +test-specs/specs: + if test -d test-specs/spec; then \ + git -C test-specs/spec pull; \ + else \ + git -C test-specs clone https://github.com/mustache/spec.git; \ + fi + +#cleaning +.PHONY: clean +clean: + rm -f mustach libmustach*.so* *.o *.pc + rm -f test-specs/*-test-specs test-specs/test-specs-*.last + rm -rf *.gcno *.gcda coverage.info gcov-latest + @$(MAKE) -C test1 clean + @$(MAKE) -C test2 clean + @$(MAKE) -C test3 clean + @$(MAKE) -C test4 clean + @$(MAKE) -C test5 clean + @$(MAKE) -C test6 clean + @$(MAKE) -C test7 clean + @$(MAKE) -C test8 clean + +# manpage +.PHONY: manuals +manuals: mustach.1.gz + +mustach.1.gz: mustach.1.scd + if which scdoc >/dev/null 2>&1; then scdoc < mustach.1.scd | gzip > mustach.1.gz; fi diff --git a/src/templating/mustach-tool.c b/src/templating/mustach-tool.c new file mode 100644 index 000000000..5f28c1f58 --- /dev/null +++ b/src/templating/mustach-tool.c @@ -0,0 +1,258 @@ +/* + Author: José Bollo <jobol@nonadev.net> + + https://gitlab.com/jobol/mustach + + SPDX-License-Identifier: ISC +*/ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include <stdlib.h> +#include <stdio.h> +#include <sys/stat.h> +#include <unistd.h> +#include <fcntl.h> +#include <string.h> +#include <libgen.h> + +#include "mustach-wrap.h" + +static const size_t BLOCKSIZE = 8192; + +static const char *errors[] = { + "??? unreferenced ???", + "system", + "unexpected end", + "empty tag", + "tag too long", + "bad separators", + "too depth", + "closing", + "bad unescape tag", + "invalid interface", + "item not found", + "partial not found", + "undefined tag", + "too much template nesting" +}; + +static const char *errmsg = 0; +static int flags = 0; +static FILE *output = 0; + +static void help(char *prog) +{ + char *name = basename(prog); +#define STR_INDIR(x) #x +#define STR(x) STR_INDIR(x) + printf("%s version %s\n", name, STR(VERSION)); +#undef STR +#undef STR_INDIR + printf( + "\n" + "USAGE:\n" + " %s [FLAGS] <json-file> <mustach-templates...>\n" + "\n" + "FLAGS:\n" + " -h, --help Prints help information\n" + " -s, --strict Error when a tag is undefined\n" + "\n" + "ARGS: (if a file is -, read standard input)\n" + " <json-file> JSON file with input data\n" + " <mustach-templates...> Template files to instantiate\n", + name); + exit(0); +} + +static char *readfile(const char *filename, size_t *length) +{ + int f; + struct stat s; + char *result; + size_t size, pos; + ssize_t rc; + + result = NULL; + if (filename[0] == '-' && filename[1] == 0) + f = dup(0); + else + f = open(filename, O_RDONLY); + if (f < 0) { + fprintf(stderr, "Can't open file: %s\n", filename); + exit(1); + } + + fstat(f, &s); + switch (s.st_mode & S_IFMT) { + case S_IFREG: + size = s.st_size; + break; + case S_IFSOCK: + case S_IFIFO: + size = BLOCKSIZE; + break; + default: + fprintf(stderr, "Bad file: %s\n", filename); + exit(1); + } + + pos = 0; + result = malloc(size + 1); + do { + if (result == NULL) { + fprintf(stderr, "Out of memory\n"); + exit(1); + } + rc = read(f, &result[pos], (size - pos) + 1); + if (rc < 0) { + fprintf(stderr, "Error while reading %s\n", filename); + exit(1); + } + if (rc > 0) { + pos += (size_t)rc; + if (pos > size) { + size = pos + BLOCKSIZE; + result = realloc(result, size + 1); + } + } + } while(rc > 0); + + close(f); + if (length != NULL) + *length = pos; + result[pos] = 0; + return result; +} + +static int load_json(const char *filename); +static int process(const char *content, size_t length); +static void close_json(); + +int main(int ac, char **av) +{ + char *t, *f; + char *prog = *av; + int s; + size_t length; + + (void)ac; /* unused */ + flags = Mustach_With_AllExtensions; + output = stdout; + + for( ++av ; av[0] && av[0][0] == '-' && av[0][1] != 0 ; av++) { + if (!strcmp(*av, "-h") || !strcmp(*av, "--help")) + help(prog); + if (!strcmp(*av, "-s") || !strcmp(*av, "--strict")) + flags |= Mustach_With_ErrorUndefined; + } + if (*av) { + f = (av[0][0] == '-' && !av[0][1]) ? "/dev/stdin" : av[0]; + s = load_json(f); + if (s < 0) { + fprintf(stderr, "Can't load json file %s\n", av[0]); + if(errmsg) + fprintf(stderr, " reason: %s\n", errmsg); + exit(1); + } + while(*++av) { + t = readfile(*av, &length); + s = process(t, length); + free(t); + if (s != MUSTACH_OK) { + s = -s; + if (s < 1 || s >= (int)(sizeof errors / sizeof * errors)) + s = 0; + fprintf(stderr, "Template error %s (file %s)\n", errors[s], *av); + } + } + close_json(); + } + return 0; +} + +#define MUSTACH_TOOL_JSON_C 1 +#define MUSTACH_TOOL_JANSSON 2 +#define MUSTACH_TOOL_CJSON 3 + +#if TOOL == MUSTACH_TOOL_JSON_C + +#include "mustach-json-c.h" + +static struct json_object *o; +static int load_json(const char *filename) +{ + o = json_object_from_file(filename); +#if JSON_C_VERSION_NUM >= 0x000D00 + errmsg = json_util_get_last_err(); + if (errmsg != NULL) + return -1; +#endif + if (o == NULL) { + errmsg = "null json"; + return -1; + } + return 0; +} +static int process(const char *content, size_t length) +{ + return mustach_json_c_file(content, length, o, flags, output); +} +static void close_json() +{ + json_object_put(o); +} + +#elif TOOL == MUSTACH_TOOL_JANSSON + +#include "mustach-jansson.h" + +static json_t *o; +static json_error_t e; +static int load_json(const char *filename) +{ + o = json_load_file(filename, JSON_DECODE_ANY, &e); + if (o == NULL) { + errmsg = e.text; + return -1; + } + return 0; +} +static int process(const char *content, size_t length) +{ + return mustach_jansson_file(content, length, o, flags, output); +} +static void close_json() +{ + json_decref(o); +} + +#elif TOOL == MUSTACH_TOOL_CJSON + +#include "mustach-cjson.h" + +static cJSON *o; +static int load_json(const char *filename) +{ + char *t; + size_t length; + + t = readfile(filename, &length); + o = t ? cJSON_ParseWithLength(t, length) : NULL; + free(t); + return -!o; +} +static int process(const char *content, size_t length) +{ + return mustach_cJSON_file(content, length, o, flags, output); +} +static void close_json() +{ + cJSON_Delete(o); +} + +#else +#error "no defined json library" +#endif diff --git a/src/templating/mustach-wrap.c b/src/templating/mustach-wrap.c new file mode 100644 index 000000000..2cd00db12 --- /dev/null +++ b/src/templating/mustach-wrap.c @@ -0,0 +1,482 @@ +/* + Author: José Bollo <jobol@nonadev.net> + + https://gitlab.com/jobol/mustach + + SPDX-License-Identifier: ISC +*/ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#ifdef _WIN32 +#include <malloc.h> +#endif + +#include "mustach.h" +#include "mustach-wrap.h" + +/* +* It was stated that allowing to include files +* through template is not safe when the mustache +* template is open to any value because it could +* create leaks (example: {{>/etc/passwd}}). +*/ +#if MUSTACH_SAFE +# undef MUSTACH_LOAD_TEMPLATE +#elif !defined(MUSTACH_LOAD_TEMPLATE) +# define MUSTACH_LOAD_TEMPLATE 1 +#endif + +#if !defined(INCLUDE_PARTIAL_EXTENSION) +# define INCLUDE_PARTIAL_EXTENSION ".mustache" +#endif + +/* global hook for partials */ +int (*mustach_wrap_get_partial)(const char *name, struct mustach_sbuf *sbuf) = NULL; + +/* internal structure for wrapping */ +struct wrap { + /* original interface */ + const struct mustach_wrap_itf *itf; + + /* original closure */ + void *closure; + + /* flags */ + int flags; + + /* emiter callback */ + mustach_emit_cb_t *emitcb; + + /* write callback */ + mustach_write_cb_t *writecb; +}; + +/* length given by masking with 3 */ +enum comp { + C_no = 0, + C_eq = 1, + C_lt = 5, + C_le = 6, + C_gt = 9, + C_ge = 10 +}; + +enum sel { + S_none = 0, + S_ok = 1, + S_objiter = 2, + S_ok_or_objiter = S_ok | S_objiter +}; + +static enum comp getcomp(char *head, int sflags) +{ + return (head[0] == '=' && (sflags & Mustach_With_Equal)) ? C_eq + : (head[0] == '<' && (sflags & Mustach_With_Compare)) ? (head[1] == '=' ? C_le : C_lt) + : (head[0] == '>' && (sflags & Mustach_With_Compare)) ? (head[1] == '=' ? C_ge : C_gt) + : C_no; +} + +static char *keyval(char *head, int sflags, enum comp *comp) +{ + char *w, car, escaped; + enum comp k; + + k = C_no; + w = head; + car = *head; + escaped = (sflags & Mustach_With_EscFirstCmp) && (getcomp(head, sflags) != C_no); + while (car && (escaped || (k = getcomp(head, sflags)) == C_no)) { + if (escaped) + escaped = 0; + else + escaped = ((sflags & Mustach_With_JsonPointer) ? car == '~' : car == '\\') + && (getcomp(head + 1, sflags) != C_no); + if (!escaped) + *w++ = car; + head++; + car = *head; + } + *w = 0; + *comp = k; + return k == C_no ? NULL : &head[k & 3]; +} + +static char *getkey(char **head, int sflags) +{ + char *result, *iter, *write, car; + + car = *(iter = *head); + if (!car) + result = NULL; + else { + result = write = iter; + if (sflags & Mustach_With_JsonPointer) + { + while (car && car != '/') { + if (car == '~') + switch (iter[1]) { + case '1': car = '/'; /*@fallthrough@*/ + case '0': iter++; + } + *write++ = car; + car = *++iter; + } + *write = 0; + while (car == '/') + car = *++iter; + } + else + { + while (car && car != '.') { + if (car == '\\' && (iter[1] == '.' || iter[1] == '\\')) + car = *++iter; + *write++ = car; + car = *++iter; + } + *write = 0; + while (car == '.') + car = *++iter; + } + *head = iter; + } + return result; +} + +static enum sel sel(struct wrap *w, const char *name) +{ + enum sel result; + int i, j, sflags, scmp; + char *key, *value; + enum comp k; + + /* make a local writeable copy */ + size_t lenname = 1 + strlen(name); + char buffer[lenname]; + char *copy = buffer; + memcpy(copy, name, lenname); + + /* check if matches json pointer selection */ + sflags = w->flags; + if (sflags & Mustach_With_JsonPointer) { + if (copy[0] == '/') + copy++; + else + sflags ^= Mustach_With_JsonPointer; + } + + /* extract the value, translate the key and get the comparator */ + if (sflags & (Mustach_With_Equal | Mustach_With_Compare)) + value = keyval(copy, sflags, &k); + else { + k = C_no; + value = NULL; + } + + /* case of . alone if Mustach_With_SingleDot? */ + if (copy[0] == '.' && copy[1] == 0 /*&& (sflags & Mustach_With_SingleDot)*/) + /* yes, select current */ + result = w->itf->sel(w->closure, NULL) ? S_ok : S_none; + else + { + /* not the single dot, extract the first key */ + key = getkey(©, sflags); + if (key == NULL) + return 0; + + /* select the root item */ + if (w->itf->sel(w->closure, key)) + result = S_ok; + else if (key[0] == '*' + && !key[1] + && !value + && !*copy + && (w->flags & Mustach_With_ObjectIter) + && w->itf->sel(w->closure, NULL)) + result = S_ok_or_objiter; + else + result = S_none; + if (result == S_ok) { + /* iterate the selection of sub items */ + key = getkey(©, sflags); + while(result == S_ok && key) { + if (w->itf->subsel(w->closure, key)) + /* nothing */; + else if (key[0] == '*' + && !key[1] + && !value + && !*copy + && (w->flags & Mustach_With_ObjectIter)) + result = S_objiter; + else + result = S_none; + key = getkey(©, sflags); + } + } + } + /* should it be compared? */ + if (result == S_ok && value) { + if (!w->itf->compare) + result = S_none; + else { + i = value[0] == '!'; + scmp = w->itf->compare(w->closure, &value[i]); + switch (k) { + case C_eq: j = scmp == 0; break; + case C_lt: j = scmp < 0; break; + case C_le: j = scmp <= 0; break; + case C_gt: j = scmp > 0; break; + case C_ge: j = scmp >= 0; break; + default: j = i; break; + } + if (i == j) + result = S_none; + } + } + return result; +} + +static int start_callback(void *closure) +{ + struct wrap *w = closure; + return w->itf->start ? w->itf->start(w->closure) : MUSTACH_OK; +} + +static void stop_callback(void *closure, int status) +{ + struct wrap *w = closure; + if (w->itf->stop) + w->itf->stop(w->closure, status); +} + +static int emit(struct wrap *w, const char *buffer, size_t size, FILE *file) +{ + int r; + + if (w->writecb) + r = w->writecb(file, buffer, size); + else + r = fwrite(buffer, 1, size, file) == size ? MUSTACH_OK : MUSTACH_ERROR_SYSTEM; + return r; +} + +static int emit_callback(void *closure, const char *buffer, size_t size, int escape, FILE *file) +{ + struct wrap *w = closure; + int r; + size_t s, i; + char car; + + if (w->emitcb) + r = w->emitcb(file, buffer, size, escape); + else if (!escape) + r = emit(w, buffer, size, file); + else { + i = 0; + r = MUSTACH_OK; + while(i < size && r == MUSTACH_OK) { + s = i; + while (i < size && (car = buffer[i]) != '<' && car != '>' && car != '&' && car != '"') + i++; + if (i != s) + r = emit(w, &buffer[s], i - s, file); + if (i < size && r == MUSTACH_OK) { + switch(car) { + case '<': r = emit(w, "<", 4, file); break; + case '>': r = emit(w, ">", 4, file); break; + case '&': r = emit(w, "&", 5, file); break; + case '"': r = emit(w, """, 6, file); break; + } + i++; + } + } + } + return r; +} + +static int enter_callback(void *closure, const char *name) +{ + struct wrap *w = closure; + enum sel s = sel(w, name); + return s == S_none ? 0 : w->itf->enter(w->closure, s & S_objiter); +} + +static int next_callback(void *closure) +{ + struct wrap *w = closure; + return w->itf->next(w->closure); +} + +static int leave_callback(void *closure) +{ + struct wrap *w = closure; + return w->itf->leave(w->closure); +} + +static int getoptional(struct wrap *w, const char *name, struct mustach_sbuf *sbuf) +{ + enum sel s = sel(w, name); + if (!(s & S_ok)) + return 0; + return w->itf->get(w->closure, sbuf, s & S_objiter); +} + +static int get_callback(void *closure, const char *name, struct mustach_sbuf *sbuf) +{ + struct wrap *w = closure; + if (getoptional(w, name, sbuf) <= 0) { + if (w->flags & Mustach_With_ErrorUndefined) + return MUSTACH_ERROR_UNDEFINED_TAG; + sbuf->value = ""; + } + return MUSTACH_OK; +} + +#if MUSTACH_LOAD_TEMPLATE +static int get_partial_from_file(const char *name, struct mustach_sbuf *sbuf) +{ + static char extension[] = INCLUDE_PARTIAL_EXTENSION; + size_t s; + long pos; + FILE *file; + char *path, *buffer; + + /* allocate path */ + s = strlen(name); + path = malloc(s + sizeof extension); + if (path == NULL) + return MUSTACH_ERROR_SYSTEM; + + /* try without extension first */ + memcpy(path, name, s + 1); + file = fopen(path, "r"); + if (file == NULL) { + memcpy(&path[s], extension, sizeof extension); + file = fopen(path, "r"); + } + free(path); + + /* if file opened */ + if (file == NULL) + return MUSTACH_ERROR_PARTIAL_NOT_FOUND; + + /* compute file size */ + if (fseek(file, 0, SEEK_END) >= 0 + && (pos = ftell(file)) >= 0 + && fseek(file, 0, SEEK_SET) >= 0) { + /* allocate value */ + s = (size_t)pos; + buffer = malloc(s + 1); + if (buffer != NULL) { + /* read value */ + if (1 == fread(buffer, s, 1, file)) { + /* force zero at end */ + sbuf->value = buffer; + buffer[s] = 0; + sbuf->freecb = free; + fclose(file); + return MUSTACH_OK; + } + free(buffer); + } + } + fclose(file); + return MUSTACH_ERROR_SYSTEM; +} +#endif + +static int partial_callback(void *closure, const char *name, struct mustach_sbuf *sbuf) +{ + struct wrap *w = closure; + int rc; + if (mustach_wrap_get_partial != NULL) { + rc = mustach_wrap_get_partial(name, sbuf); + if (rc != MUSTACH_ERROR_PARTIAL_NOT_FOUND) { + if (rc != MUSTACH_OK) + sbuf->value = ""; + return rc; + } + } +#if MUSTACH_LOAD_TEMPLATE + if (w->flags & Mustach_With_PartialDataFirst) { + if (getoptional(w, name, sbuf) > 0) + rc = MUSTACH_OK; + else + rc = get_partial_from_file(name, sbuf); + } + else { + rc = get_partial_from_file(name, sbuf); + if (rc != MUSTACH_OK && getoptional(w, name, sbuf) > 0) + rc = MUSTACH_OK; + } +#else + rc = getoptional(w, name, sbuf) > 0 ? MUSTACH_OK : MUSTACH_ERROR_PARTIAL_NOT_FOUND; +#endif + if (rc != MUSTACH_OK) + sbuf->value = ""; + return MUSTACH_OK; +} + +const struct mustach_itf mustach_wrap_itf = { + .start = start_callback, + .put = NULL, + .enter = enter_callback, + .next = next_callback, + .leave = leave_callback, + .partial = partial_callback, + .get = get_callback, + .emit = emit_callback, + .stop = stop_callback +}; + +static void wrap_init(struct wrap *wrap, const struct mustach_wrap_itf *itf, void *closure, int flags, mustach_emit_cb_t *emitcb, mustach_write_cb_t *writecb) +{ + if (flags & Mustach_With_Compare) + flags |= Mustach_With_Equal; + wrap->closure = closure; + wrap->itf = itf; + wrap->flags = flags; + wrap->emitcb = emitcb; + wrap->writecb = writecb; +} + +int mustach_wrap_file(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, FILE *file) +{ + struct wrap w; + wrap_init(&w, itf, closure, flags, NULL, NULL); + return mustach_file(template, length, &mustach_wrap_itf, &w, flags, file); +} + +int mustach_wrap_fd(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, int fd) +{ + struct wrap w; + wrap_init(&w, itf, closure, flags, NULL, NULL); + return mustach_fd(template, length, &mustach_wrap_itf, &w, flags, fd); +} + +int mustach_wrap_mem(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, char **result, size_t *size) +{ + struct wrap w; + wrap_init(&w, itf, closure, flags, NULL, NULL); + return mustach_mem(template, length, &mustach_wrap_itf, &w, flags, result, size); +} + +int mustach_wrap_write(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, mustach_write_cb_t *writecb, void *writeclosure) +{ + struct wrap w; + wrap_init(&w, itf, closure, flags, NULL, writecb); + return mustach_file(template, length, &mustach_wrap_itf, &w, flags, writeclosure); +} + +int mustach_wrap_emit(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, mustach_emit_cb_t *emitcb, void *emitclosure) +{ + struct wrap w; + wrap_init(&w, itf, closure, flags, emitcb, NULL); + return mustach_file(template, length, &mustach_wrap_itf, &w, flags, emitclosure); +} + diff --git a/src/templating/mustach-wrap.h b/src/templating/mustach-wrap.h new file mode 100644 index 000000000..fedcb9191 --- /dev/null +++ b/src/templating/mustach-wrap.h @@ -0,0 +1,235 @@ +/* + Author: José Bollo <jobol@nonadev.net> + + https://gitlab.com/jobol/mustach + + SPDX-License-Identifier: ISC +*/ + +#ifndef _mustach_wrap_h_included_ +#define _mustach_wrap_h_included_ + +/* + * mustach-wrap is intended to make integration of JSON + * libraries easier by wrapping mustach extensions in a + * single place. + * + * As before, using mustach and only mustach is possible + * (by using only mustach.h) but does not implement high + * level features coming with extensions implemented by + * this high level wrapper. + */ +#include "mustach.h" +/* + * Definition of the writing callbacks for mustach functions + * producing output to callbacks. + * + * Two callback types are defined: + * + * @mustach_write_cb_t: + * + * callback receiving the escaped data to be written as 3 parameters: + * + * 1. the 'closure', the same given to the wmustach_... function + * 2. a pointer to a 'buffer' containing the characters to be written + * 3. the size in bytes of the data pointed by 'buffer' + * + * @mustach_emit_cb_t: + * + * callback receiving the data to be written and a flag indicating + * if escaping should be done or not as 4 parameters: + * + * 1. the 'closure', the same given to the emustach_... function + * 2. a pointer to a 'buffer' containing the characters to be written + * 3. the size in bytes of the data pointed by 'buffer' + * 4. a boolean indicating if 'escape' should be done + */ +#ifndef _mustach_output_callbacks_defined_ +#define _mustach_output_callbacks_defined_ +typedef int mustach_write_cb_t(void *closure, const char *buffer, size_t size); +typedef int mustach_emit_cb_t(void *closure, const char *buffer, size_t size, int escape); +#endif + +/** + * Flags specific to mustach wrap + */ +#define Mustach_With_SingleDot 4 /* obsolete, always set */ +#define Mustach_With_Equal 8 +#define Mustach_With_Compare 16 +#define Mustach_With_JsonPointer 32 +#define Mustach_With_ObjectIter 64 +#define Mustach_With_IncPartial 128 /* obsolete, always set */ +#define Mustach_With_EscFirstCmp 256 +#define Mustach_With_PartialDataFirst 512 +#define Mustach_With_ErrorUndefined 1024 + +#undef Mustach_With_AllExtensions +#define Mustach_With_AllExtensions 1023 /* don't include ErrorUndefined */ + +/** + * mustach_wrap_itf - high level wrap of mustach - interface for callbacks + * + * The functions sel, subsel, enter and next should return 0 or 1. + * + * All other functions should normally return MUSTACH_OK (zero). + * + * If any function returns a negative value, it means an error that + * stop the processing and that is reported to the caller. Mustach + * also has its own error codes. Using the macros MUSTACH_ERROR_USER + * and MUSTACH_IS_ERROR_USER could help to avoid clashes. + * + * @start: If defined (can be NULL), starts the mustach processing + * of the closure, called at the very beginning before any + * mustach processing occurs. + * + * @stop: If defined (can be NULL), stops the mustach processing + * of the closure, called at the very end after all mustach + * processing occurered. The status returned by the processing + * is passed to the stop. + * + * @compare: If defined (can be NULL), compares the value of the + * currently selected item with the given value and returns + * a negative value if current value is lesser, a positive + * value if the current value is greater or zero when + * values are equals. + * If 'compare' is NULL, any comparison in mustach + * is going to fails. + * + * @sel: Selects the item of the given 'name'. If 'name' is NULL + * Selects the current item. Returns 1 if the selection is + * effective or else 0 if the selection failed. + * + * @subsel: Selects from the currently selected object the value of + * the field of given name. Returns 1 if the selection is + * effective or else 0 if the selection failed. + * + * @enter: Enters the section of 'name' if possible. + * Musts return 1 if entered or 0 if not entered. + * When 1 is returned, the function 'leave' will always be called. + * Conversely 'leave' is never called when enter returns 0 or + * a negative value. + * When 1 is returned, the function must activate the first + * item of the section. + * + * @next: Activates the next item of the section if it exists. + * Musts return 1 when the next item is activated. + * Musts return 0 when there is no item to activate. + * + * @leave: Leaves the last entered section + * + * @get: Returns in 'sbuf' the value of the current selection if 'key' + * is zero. Otherwise, when 'key' is not zero, return in 'sbuf' + * the name of key of the current selection, or if no such key + * exists, the empty string. Must return 1 if possible or + * 0 when not possible or an error code. + */ +struct mustach_wrap_itf { + int (*start)(void *closure); + void (*stop)(void *closure, int status); + int (*compare)(void *closure, const char *value); + int (*sel)(void *closure, const char *name); + int (*subsel)(void *closure, const char *name); + int (*enter)(void *closure, int objiter); + int (*next)(void *closure); + int (*leave)(void *closure); + int (*get)(void *closure, struct mustach_sbuf *sbuf, int key); +}; + +/** + * Mustach interface used internally by mustach wrapper functions. + * Can be used for overriding behaviour. + */ +extern const struct mustach_itf mustach_wrap_itf; + +/** + * Global hook for providing partials. When set to a not NULL value, the pointed + * function replaces the default behaviour and is called to provide the partial + * of the given 'name' in 'sbuf'. + * The function must return MUSTACH_OK when it filled 'sbuf' with value of partial + * or must return an error code if it failed. But if MUSTACH_ERROR_PARTIAL_NOT_FOUND + * is returned, the default behavior is evaluated. + */ +extern int (*mustach_wrap_get_partial)(const char *name, struct mustach_sbuf *sbuf); + +/** + * mustach_wrap_file - Renders the mustache 'template' in 'file' for an abstract + * wrapper of interface 'itf' and 'closure'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @itf: the interface of the abstract wrapper + * @closure: the closure of the abstract wrapper + * @file: the file where to write the result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_wrap_file(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, FILE *file); + +/** + * mustach_wrap_fd - Renders the mustache 'template' in 'fd' for an abstract + * wrapper of interface 'itf' and 'closure'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @itf: the interface of the abstract wrapper + * @closure: the closure of the abstract wrapper + * @fd: the file descriptor number where to write the result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_wrap_fd(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, int fd); + +/** + * mustach_wrap_mem - Renders the mustache 'template' in 'result' for an abstract + * wrapper of interface 'itf' and 'closure'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @itf: the interface of the abstract wrapper + * @closure: the closure of the abstract wrapper + * @result: the pointer receiving the result when 0 is returned + * @size: the size of the returned result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_wrap_mem(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, char **result, size_t *size); + +/** + * mustach_wrap_write - Renders the mustache 'template' for an abstract + * wrapper of interface 'itf' and 'closure' to custom writer + * 'writecb' with 'writeclosure'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @itf: the interface of the abstract wrapper + * @closure: the closure of the abstract wrapper + * @writecb: the function that write values + * @closure: the closure for the write function + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_wrap_write(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, mustach_write_cb_t *writecb, void *writeclosure); + +/** + * mustach_wrap_emit - Renders the mustache 'template' for an abstract + * wrapper of interface 'itf' and 'closure' to custom emiter 'emitcb' + * with 'emitclosure'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @itf: the interface of the abstract wrapper + * @closure: the closure of the abstract wrapper + * @emitcb: the function that emit values + * @closure: the closure for the write function + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_wrap_emit(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, mustach_emit_cb_t *emitcb, void *emitclosure); + +#endif + diff --git a/src/templating/mustach.1.gz b/src/templating/mustach.1.gz Binary files differnew file mode 100644 index 000000000..15b8a9052 --- /dev/null +++ b/src/templating/mustach.1.gz diff --git a/src/templating/mustach.1.scd b/src/templating/mustach.1.scd new file mode 100644 index 000000000..af4f08ef2 --- /dev/null +++ b/src/templating/mustach.1.scd @@ -0,0 +1,60 @@ +mustach(1) + +# NAME + +mustach - Mustache templating command line engine + +# SYNOPSIS + +*mustach* [-s|--strict] JSON TEMPLATE... + +# DESCRIPTION + +Instanciate the TEMPLATE files accordingly to the JSON file. + +If one of the given files is *-*, the standard input is used. + +Option *--strict* make mustach fail if a tag is not found. + +# EXAMPLE + +A typical Mustache template file: *temp.must* + +``` +Hello {{name}} +You have just won {{value}} dollars! +{{#in_ca}} +Well, {{taxed_value}} dollars, after taxes. +{{/in_ca}} +``` + +Given a JSON file: *inst.json* + +``` +{ + "name": "Chris", + "value": 10000, + "taxed_value": 6000, + "in_ca": true +} +``` + +Calling the command *mustach inst.json temp.must* +will produce the following output: + +``` +Hello Chris +You have just won 10000 dollars! +Well, 6000.0 dollars, after taxes. +``` + +# LINK + +Site of *mustach*, the *C* implementation: https://gitlab.com/jobol/mustach + +*Mustache format*: http://mustache.github.io/mustache.5.html + +Main site for *Mustache*: http://mustache.github.io/ + +JSON: https://www.json.org/ + diff --git a/src/templating/mustach.c b/src/templating/mustach.c new file mode 100644 index 000000000..9f5af131c --- /dev/null +++ b/src/templating/mustach.c @@ -0,0 +1,561 @@ +/* + Author: José Bollo <jobol@nonadev.net> + + https://gitlab.com/jobol/mustach + + SPDX-License-Identifier: ISC +*/ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <errno.h> +#include <ctype.h> +#ifdef _WIN32 +#include <malloc.h> +#endif + +#include "mustach.h" + +struct iwrap { + int (*emit)(void *closure, const char *buffer, size_t size, int escape, FILE *file); + void *closure; /* closure for: enter, next, leave, emit, get */ + int (*put)(void *closure, const char *name, int escape, FILE *file); + void *closure_put; /* closure for put */ + int (*enter)(void *closure, const char *name); + int (*next)(void *closure); + int (*leave)(void *closure); + int (*get)(void *closure, const char *name, struct mustach_sbuf *sbuf); + int (*partial)(void *closure, const char *name, struct mustach_sbuf *sbuf); + void *closure_partial; /* closure for partial */ + FILE *file; + int flags; + int nesting; +}; + +struct prefix { + size_t len; + const char *start; + struct prefix *prefix; +}; + +#if !defined(NO_OPEN_MEMSTREAM) +static FILE *memfile_open(char **buffer, size_t *size) +{ + return open_memstream(buffer, size); +} +static void memfile_abort(FILE *file, char **buffer, size_t *size) +{ + fclose(file); + free(*buffer); + *buffer = NULL; + *size = 0; +} +static int memfile_close(FILE *file, char **buffer, size_t *size) +{ + int rc; + + /* adds terminating null */ + rc = fputc(0, file) ? MUSTACH_ERROR_SYSTEM : 0; + fclose(file); + if (rc == 0) + /* removes terminating null of the length */ + (*size)--; + else { + free(*buffer); + *buffer = NULL; + *size = 0; + } + return rc; +} +#else +static FILE *memfile_open(char **buffer, size_t *size) +{ + /* + * We can't provide *buffer and *size as open_memstream does but + * at least clear them so the caller won't get bad data. + */ + *buffer = NULL; + *size = 0; + + return tmpfile(); +} +static void memfile_abort(FILE *file, char **buffer, size_t *size) +{ + fclose(file); + *buffer = NULL; + *size = 0; +} +static int memfile_close(FILE *file, char **buffer, size_t *size) +{ + int rc; + size_t s; + char *b; + + s = (size_t)ftell(file); + b = malloc(s + 1); + if (b == NULL) { + rc = MUSTACH_ERROR_SYSTEM; + errno = ENOMEM; + s = 0; + } else { + rewind(file); + if (1 == fread(b, s, 1, file)) { + rc = 0; + b[s] = 0; + } else { + rc = MUSTACH_ERROR_SYSTEM; + free(b); + b = NULL; + s = 0; + } + } + *buffer = b; + *size = s; + return rc; +} +#endif + +static inline void sbuf_reset(struct mustach_sbuf *sbuf) +{ + sbuf->value = NULL; + sbuf->freecb = NULL; + sbuf->closure = NULL; + sbuf->length = 0; +} + +static inline void sbuf_release(struct mustach_sbuf *sbuf) +{ + if (sbuf->releasecb) + sbuf->releasecb(sbuf->value, sbuf->closure); +} + +static inline size_t sbuf_length(struct mustach_sbuf *sbuf) +{ + size_t length = sbuf->length; + if (length == 0 && sbuf->value != NULL) + length = strlen(sbuf->value); + return length; +} + +static int iwrap_emit(void *closure, const char *buffer, size_t size, int escape, FILE *file) +{ + size_t i, j, r; + + (void)closure; /* unused */ + + if (!escape) + return fwrite(buffer, 1, size, file) != size ? MUSTACH_ERROR_SYSTEM : MUSTACH_OK; + + r = i = 0; + while (i < size) { + j = i; + while (j < size && buffer[j] != '<' && buffer[j] != '>' && buffer[j] != '&' && buffer[j] != '"') + j++; + if (j != i && fwrite(&buffer[i], j - i, 1, file) != 1) + return MUSTACH_ERROR_SYSTEM; + if (j < size) { + switch(buffer[j++]) { + case '<': + r = fwrite("<", 4, 1, file); + break; + case '>': + r = fwrite(">", 4, 1, file); + break; + case '&': + r = fwrite("&", 5, 1, file); + break; + case '"': + r = fwrite(""", 6, 1, file); + break; + } + if (r != 1) + return MUSTACH_ERROR_SYSTEM; + } + i = j; + } + return MUSTACH_OK; +} + +static int iwrap_put(void *closure, const char *name, int escape, FILE *file) +{ + struct iwrap *iwrap = closure; + int rc; + struct mustach_sbuf sbuf; + size_t length; + + sbuf_reset(&sbuf); + rc = iwrap->get(iwrap->closure, name, &sbuf); + if (rc >= 0) { + length = sbuf_length(&sbuf); + if (length) + rc = iwrap->emit(iwrap->closure, sbuf.value, length, escape, file); + sbuf_release(&sbuf); + } + return rc; +} + +static int iwrap_partial(void *closure, const char *name, struct mustach_sbuf *sbuf) +{ + struct iwrap *iwrap = closure; + int rc; + FILE *file; + size_t size; + char *result; + + result = NULL; + file = memfile_open(&result, &size); + if (file == NULL) + rc = MUSTACH_ERROR_SYSTEM; + else { + rc = iwrap->put(iwrap->closure_put, name, 0, file); + if (rc < 0) + memfile_abort(file, &result, &size); + else { + rc = memfile_close(file, &result, &size); + if (rc == 0) { + sbuf->value = result; + sbuf->freecb = free; + sbuf->length = size; + } + } + } + return rc; +} + +static int emitprefix(struct iwrap *iwrap, struct prefix *prefix) +{ + if (prefix->prefix) { + int rc = emitprefix(iwrap, prefix->prefix); + if (rc < 0) + return rc; + } + return prefix->len ? iwrap->emit(iwrap->closure, prefix->start, prefix->len, 0, iwrap->file) : 0; +} + +static int process(const char *template, size_t length, struct iwrap *iwrap, struct prefix *prefix) +{ + struct mustach_sbuf sbuf; + char opstr[MUSTACH_MAX_DELIM_LENGTH], clstr[MUSTACH_MAX_DELIM_LENGTH]; + char name[MUSTACH_MAX_LENGTH + 1], c; + const char *beg, *term, *end; + struct { const char *name, *again; size_t length; unsigned enabled: 1, entered: 1; } stack[MUSTACH_MAX_DEPTH]; + size_t oplen, cllen, len, l; + int depth, rc, enabled, stdalone; + struct prefix pref; + + pref.prefix = prefix; + end = template + (length ? length : strlen(template)); + opstr[0] = opstr[1] = '{'; + clstr[0] = clstr[1] = '}'; + oplen = cllen = 2; + stdalone = enabled = 1; + depth = pref.len = 0; + for (;;) { + /* search next openning delimiter */ + for (beg = template ; ; beg++) { + c = beg == end ? '\n' : *beg; + if (c == '\n') { + l = (beg != end) + (size_t)(beg - template); + if (stdalone != 2 && enabled) { + if (beg != template /* don't prefix empty lines */) { + rc = emitprefix(iwrap, &pref); + if (rc < 0) + return rc; + } + rc = iwrap->emit(iwrap->closure, template, l, 0, iwrap->file); + if (rc < 0) + return rc; + } + if (beg == end) /* no more mustach */ + return depth ? MUSTACH_ERROR_UNEXPECTED_END : MUSTACH_OK; + template += l; + stdalone = 1; + pref.len = 0; + pref.prefix = prefix; + } + else if (!isspace(c)) { + if (stdalone == 2 && enabled) { + rc = emitprefix(iwrap, &pref); + if (rc < 0) + return rc; + pref.len = 0; + stdalone = 0; + pref.prefix = NULL; + } + if (c == *opstr && end - beg >= (ssize_t)oplen) { + for (l = 1 ; l < oplen && beg[l] == opstr[l] ; l++); + if (l == oplen) + break; + } + stdalone = 0; + } + } + + pref.start = template; + pref.len = enabled ? (size_t)(beg - template) : 0; + beg += oplen; + + /* search next closing delimiter */ + for (term = beg ; ; term++) { + if (term == end) + return MUSTACH_ERROR_UNEXPECTED_END; + if (*term == *clstr && end - term >= (ssize_t)cllen) { + for (l = 1 ; l < cllen && term[l] == clstr[l] ; l++); + if (l == cllen) + break; + } + } + template = term + cllen; + len = (size_t)(term - beg); + c = *beg; + switch(c) { + case ':': + stdalone = 0; + if (iwrap->flags & Mustach_With_Colon) + goto exclude_first; + goto get_name; + case '!': + case '=': + break; + case '{': + for (l = 0 ; l < cllen && clstr[l] == '}' ; l++); + if (l < cllen) { + if (!len || beg[len-1] != '}') + return MUSTACH_ERROR_BAD_UNESCAPE_TAG; + len--; + } else { + if (term[l] != '}') + return MUSTACH_ERROR_BAD_UNESCAPE_TAG; + template++; + } + c = '&'; + /*@fallthrough@*/ + case '&': + stdalone = 0; + /*@fallthrough@*/ + case '^': + case '#': + case '/': + case '>': +exclude_first: + beg++; + len--; + goto get_name; + default: + stdalone = 0; +get_name: + while (len && isspace(beg[0])) { beg++; len--; } + while (len && isspace(beg[len-1])) len--; + if (len == 0 && !(iwrap->flags & Mustach_With_EmptyTag)) + return MUSTACH_ERROR_EMPTY_TAG; + if (len > MUSTACH_MAX_LENGTH) + return MUSTACH_ERROR_TAG_TOO_LONG; + memcpy(name, beg, len); + name[len] = 0; + break; + } + if (stdalone) + stdalone = 2; + else if (enabled) { + rc = emitprefix(iwrap, &pref); + if (rc < 0) + return rc; + pref.len = 0; + pref.prefix = NULL; + } + switch(c) { + case '!': + /* comment */ + /* nothing to do */ + break; + case '=': + /* defines delimiters */ + if (len < 5 || beg[len - 1] != '=') + return MUSTACH_ERROR_BAD_SEPARATORS; + beg++; + len -= 2; + while (len && isspace(*beg)) + beg++, len--; + while (len && isspace(beg[len - 1])) + len--; + for (l = 0; l < len && !isspace(beg[l]) ; l++); + if (l == len || l > MUSTACH_MAX_DELIM_LENGTH) + return MUSTACH_ERROR_BAD_SEPARATORS; + oplen = l; + memcpy(opstr, beg, l); + while (l < len && isspace(beg[l])) l++; + if (l == len || len - l > MUSTACH_MAX_DELIM_LENGTH) + return MUSTACH_ERROR_BAD_SEPARATORS; + cllen = len - l; + memcpy(clstr, beg + l, cllen); + break; + case '^': + case '#': + /* begin section */ + if (depth == MUSTACH_MAX_DEPTH) + return MUSTACH_ERROR_TOO_DEEP; + rc = enabled; + if (rc) { + rc = iwrap->enter(iwrap->closure, name); + if (rc < 0) + return rc; + } + stack[depth].name = beg; + stack[depth].again = template; + stack[depth].length = len; + stack[depth].enabled = enabled != 0; + stack[depth].entered = rc != 0; + if ((c == '#') == (rc == 0)) + enabled = 0; + depth++; + break; + case '/': + /* end section */ + if (depth-- == 0 || len != stack[depth].length || memcmp(stack[depth].name, name, len)) + return MUSTACH_ERROR_CLOSING; + rc = enabled && stack[depth].entered ? iwrap->next(iwrap->closure) : 0; + if (rc < 0) + return rc; + if (rc) { + template = stack[depth++].again; + } else { + enabled = stack[depth].enabled; + if (enabled && stack[depth].entered) + iwrap->leave(iwrap->closure); + } + break; + case '>': + /* partials */ + if (enabled) { + if (iwrap->nesting >= MUSTACH_MAX_NESTING) + rc = MUSTACH_ERROR_TOO_MUCH_NESTING; + else { + sbuf_reset(&sbuf); + rc = iwrap->partial(iwrap->closure_partial, name, &sbuf); + if (rc >= 0) { + iwrap->nesting++; + rc = process(sbuf.value, sbuf_length(&sbuf), iwrap, &pref); + sbuf_release(&sbuf); + iwrap->nesting--; + } + } + if (rc < 0) + return rc; + } + break; + default: + /* replacement */ + if (enabled) { + rc = iwrap->put(iwrap->closure_put, name, c != '&', iwrap->file); + if (rc < 0) + return rc; + } + break; + } + } +} + +int mustach_file(const char *template, size_t length, const struct mustach_itf *itf, void *closure, int flags, FILE *file) +{ + int rc; + struct iwrap iwrap; + + /* check validity */ + if (!itf->enter || !itf->next || !itf->leave || (!itf->put && !itf->get)) + return MUSTACH_ERROR_INVALID_ITF; + + /* init wrap structure */ + iwrap.closure = closure; + if (itf->put) { + iwrap.put = itf->put; + iwrap.closure_put = closure; + } else { + iwrap.put = iwrap_put; + iwrap.closure_put = &iwrap; + } + if (itf->partial) { + iwrap.partial = itf->partial; + iwrap.closure_partial = closure; + } else if (itf->get) { + iwrap.partial = itf->get; + iwrap.closure_partial = closure; + } else { + iwrap.partial = iwrap_partial; + iwrap.closure_partial = &iwrap; + } + iwrap.emit = itf->emit ? itf->emit : iwrap_emit; + iwrap.enter = itf->enter; + iwrap.next = itf->next; + iwrap.leave = itf->leave; + iwrap.get = itf->get; + iwrap.file = file; + iwrap.flags = flags; + iwrap.nesting = 0; + + /* process */ + rc = itf->start ? itf->start(closure) : 0; + if (rc == 0) + rc = process(template, length, &iwrap, NULL); + if (itf->stop) + itf->stop(closure, rc); + return rc; +} + +int mustach_fd(const char *template, size_t length, const struct mustach_itf *itf, void *closure, int flags, int fd) +{ + int rc; + FILE *file; + + file = fdopen(fd, "w"); + if (file == NULL) { + rc = MUSTACH_ERROR_SYSTEM; + errno = ENOMEM; + } else { + rc = mustach_file(template, length, itf, closure, flags, file); + fclose(file); + } + return rc; +} + +int mustach_mem(const char *template, size_t length, const struct mustach_itf *itf, void *closure, int flags, char **result, size_t *size) +{ + int rc; + FILE *file; + size_t s; + + *result = NULL; + if (size == NULL) + size = &s; + file = memfile_open(result, size); + if (file == NULL) + rc = MUSTACH_ERROR_SYSTEM; + else { + rc = mustach_file(template, length, itf, closure, flags, file); + if (rc < 0) + memfile_abort(file, result, size); + else + rc = memfile_close(file, result, size); + } + return rc; +} + +int fmustach(const char *template, const struct mustach_itf *itf, void *closure, FILE *file) +{ + return mustach_file(template, 0, itf, closure, Mustach_With_AllExtensions, file); +} + +int fdmustach(const char *template, const struct mustach_itf *itf, void *closure, int fd) +{ + return mustach_fd(template, 0, itf, closure, Mustach_With_AllExtensions, fd); +} + +int mustach(const char *template, const struct mustach_itf *itf, void *closure, char **result, size_t *size) +{ + return mustach_mem(template, 0, itf, closure, Mustach_With_AllExtensions, result, size); +} + diff --git a/src/templating/mustach.h b/src/templating/mustach.h new file mode 100644 index 000000000..1b44582d5 --- /dev/null +++ b/src/templating/mustach.h @@ -0,0 +1,319 @@ +/* + Author: José Bollo <jobol@nonadev.net> + + https://gitlab.com/jobol/mustach + + SPDX-License-Identifier: ISC +*/ + +#ifndef _mustach_h_included_ +#define _mustach_h_included_ + +struct mustach_sbuf; /* see below */ + +/** + * Current version of mustach and its derivates + */ +#define MUSTACH_VERSION 102 +#define MUSTACH_VERSION_MAJOR (MUSTACH_VERSION / 100) +#define MUSTACH_VERSION_MINOR (MUSTACH_VERSION % 100) + +/** + * Maximum nested section supported + */ +#define MUSTACH_MAX_DEPTH 256 + +/** + * Maximum nested template supported + */ +#define MUSTACH_MAX_NESTING 64 + +/** + * Maximum length of tags in mustaches {{...}} + */ +#define MUSTACH_MAX_LENGTH 4096 + +/** + * Maximum length of delimitors (2 normally but extended here) + */ +#define MUSTACH_MAX_DELIM_LENGTH 8 + +/** + * Flags specific to mustach core + */ +#define Mustach_With_NoExtensions 0 +#define Mustach_With_Colon 1 +#define Mustach_With_EmptyTag 2 +#define Mustach_With_AllExtensions 3 + +/* + * Definition of error codes returned by mustach + */ +#define MUSTACH_OK 0 +#define MUSTACH_ERROR_SYSTEM -1 +#define MUSTACH_ERROR_UNEXPECTED_END -2 +#define MUSTACH_ERROR_EMPTY_TAG -3 +#define MUSTACH_ERROR_TAG_TOO_LONG -4 +#define MUSTACH_ERROR_BAD_SEPARATORS -5 +#define MUSTACH_ERROR_TOO_DEEP -6 +#define MUSTACH_ERROR_CLOSING -7 +#define MUSTACH_ERROR_BAD_UNESCAPE_TAG -8 +#define MUSTACH_ERROR_INVALID_ITF -9 +#define MUSTACH_ERROR_ITEM_NOT_FOUND -10 +#define MUSTACH_ERROR_PARTIAL_NOT_FOUND -11 +#define MUSTACH_ERROR_UNDEFINED_TAG -12 +#define MUSTACH_ERROR_TOO_MUCH_NESTING -13 + +/* + * You can use definition below for user specific error + * + * The macro MUSTACH_ERROR_USER is involutive so for any value + * value = MUSTACH_ERROR_USER(MUSTACH_ERROR_USER(value)) + */ +#define MUSTACH_ERROR_USER_BASE -100 +#define MUSTACH_ERROR_USER(x) (MUSTACH_ERROR_USER_BASE-(x)) +#define MUSTACH_IS_ERROR_USER(x) (MUSTACH_ERROR_USER(x) >= 0) + +/** + * mustach_itf - pure abstract mustach - interface for callbacks + * + * The functions enter and next should return 0 or 1. + * + * All other functions should normally return MUSTACH_OK (zero). + * + * If any function returns a negative value, it means an error that + * stop the processing and that is reported to the caller. Mustach + * also has its own error codes. Using the macros MUSTACH_ERROR_USER + * and MUSTACH_IS_ERROR_USER could help to avoid clashes. + * + * @start: If defined (can be NULL), starts the mustach processing + * of the closure, called at the very beginning before any + * mustach processing occurs. + * + * @put: If defined (can be NULL), writes the value of 'name' + * to 'file' with 'escape' or not. + * As an extension (see NO_ALLOW_EMPTY_TAG), the 'name' can be + * the empty string. In that later case an implementation can + * return MUSTACH_ERROR_EMPTY_TAG to refuse empty names. + * If NULL and 'get' NULL the error MUSTACH_ERROR_INVALID_ITF + * is returned. + * + * @enter: Enters the section of 'name' if possible. + * Musts return 1 if entered or 0 if not entered. + * When 1 is returned, the function 'leave' will always be called. + * Conversely 'leave' is never called when enter returns 0 or + * a negative value. + * When 1 is returned, the function must activate the first + * item of the section. + * + * @next: Activates the next item of the section if it exists. + * Musts return 1 when the next item is activated. + * Musts return 0 when there is no item to activate. + * + * @leave: Leaves the last entered section + * + * @partial: If defined (can be NULL), returns in 'sbuf' the content of the + * partial of 'name'. @see mustach_sbuf + * If NULL but 'get' not NULL, 'get' is used instead of partial. + * If NULL and 'get' NULL and 'put' not NULL, 'put' is called with + * a true FILE. + * + * @emit: If defined (can be NULL), writes the 'buffer' of 'size' with 'escape'. + * If NULL the standard function 'fwrite' is used with a true FILE. + * If not NULL that function is called instead of 'fwrite' to output + * text. + * It implies that if you define either 'partial' or 'get' callback, + * the meaning of 'FILE *file' is abstract for mustach's process and + * then you can use 'FILE*file' pass any kind of pointer (including NULL) + * to the function 'fmustach'. An example of a such behaviour is given by + * the implementation of 'mustach_json_c_write'. + * + * @get: If defined (can be NULL), returns in 'sbuf' the value of 'name'. + * As an extension (see NO_ALLOW_EMPTY_TAG), the 'name' can be + * the empty string. In that later case an implementation can + * return MUSTACH_ERROR_EMPTY_TAG to refuse empty names. + * If 'get' is NULL and 'put' NULL the error MUSTACH_ERROR_INVALID_ITF + * is returned. + * + * @stop: If defined (can be NULL), stops the mustach processing + * of the closure, called at the very end after all mustach + * processing occurered. The status returned by the processing + * is passed to the stop. + * + * The array below summarize status of callbacks: + * + * FULLY OPTIONAL: start partial + * MANDATORY: enter next leave + * COMBINATORIAL: put emit get + * + * Not definig a MANDATORY callback returns error MUSTACH_ERROR_INVALID_ITF. + * + * For COMBINATORIAL callbacks the array below summarize possible combinations: + * + * combination : put : emit : get : abstract FILE + * -------------+---------+---------+---------+----------------------- + * HISTORIC : defined : NULL : NULL : NO: standard FILE + * MINIMAL : NULL : NULL : defined : NO: standard FILE + * CUSTOM : NULL : defined : defined : YES: abstract FILE + * DUCK : defined : NULL : defined : NO: standard FILE + * DANGEROUS : defined : defined : any : YES or NO, depends on 'partial' + * INVALID : NULL : any : NULL : - + * + * The DUCK case runs on one leg. 'get' is not used if 'partial' is defined + * but is used for 'partial' if 'partial' is NULL. Thus for clarity, do not use + * it that way but define 'partial' and let 'get' be NULL. + * + * The DANGEROUS case is special: it allows abstract FILE if 'partial' is defined + * but forbids abstract FILE when 'partial' is NULL. + * + * The INVALID case returns error MUSTACH_ERROR_INVALID_ITF. + */ +struct mustach_itf { + int (*start)(void *closure); + int (*put)(void *closure, const char *name, int escape, FILE *file); + int (*enter)(void *closure, const char *name); + int (*next)(void *closure); + int (*leave)(void *closure); + int (*partial)(void *closure, const char *name, struct mustach_sbuf *sbuf); + int (*emit)(void *closure, const char *buffer, size_t size, int escape, FILE *file); + int (*get)(void *closure, const char *name, struct mustach_sbuf *sbuf); + void (*stop)(void *closure, int status); +}; + +/** + * mustach_sbuf - Interface for handling zero terminated strings + * + * That structure is used for returning zero terminated strings -in 'value'- + * to mustach. The callee can provide a function for releasing the returned + * 'value'. Three methods for releasing the string are possible. + * + * 1. no release: set either 'freecb' or 'releasecb' with NULL (done by default) + * 2. release without closure: set 'freecb' to its expected value + * 3. release with closure: set 'releasecb' and 'closure' to their expected values + * + * @value: The value of the string. That value is not changed by mustach -const-. + * + * @freecb: The function to call for freeing the value without closure. + * For convenience, signature of that callback is compatible with 'free'. + * Can be NULL. + * + * @releasecb: The function to release with closure. + * Can be NULL. + * + * @closure: The closure to use for 'releasecb'. + * + * @length: Length of the value or zero if unknown and value null terminated. + * To return the empty string, let it to zero and let value to NULL. + */ +struct mustach_sbuf { + const char *value; + union { + void (*freecb)(void*); + void (*releasecb)(const char *value, void *closure); + }; + void *closure; + size_t length; +}; + +/** + * mustach_file - Renders the mustache 'template' in 'file' for 'itf' and 'closure'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @itf: the interface to the functions that mustach calls + * @closure: the closure to pass to functions called + * @file: the file where to write the result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_file(const char *template, size_t length, const struct mustach_itf *itf, void *closure, int flags, FILE *file); + +/** + * mustach_fd - Renders the mustache 'template' in 'fd' for 'itf' and 'closure'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @itf: the interface to the functions that mustach calls + * @closure: the closure to pass to functions called + * @fd: the file descriptor number where to write the result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_fd(const char *template, size_t length, const struct mustach_itf *itf, void *closure, int flags, int fd); + +/** + * mustach_mem - Renders the mustache 'template' in 'result' for 'itf' and 'closure'. + * + * @template: the template string to instantiate + * @length: length of the template or zero if unknown and template null terminated + * @itf: the interface to the functions that mustach calls + * @closure: the closure to pass to functions called + * @result: the pointer receiving the result when 0 is returned + * @size: the size of the returned result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +extern int mustach_mem(const char *template, size_t length, const struct mustach_itf *itf, void *closure, int flags, char **result, size_t *size); + +/*************************************************************************** +* compatibility with version before 1.0 +*/ +#ifdef __GNUC__ +#define DEPRECATED_MUSTACH(func) func __attribute__ ((deprecated)) +#elif defined(_MSC_VER) +#define DEPRECATED_MUSTACH(func) __declspec(deprecated) func +#elif !defined(DEPRECATED_MUSTACH) +#pragma message("WARNING: You need to implement DEPRECATED_MUSTACH for this compiler") +#define DEPRECATED_MUSTACH(func) func +#endif +/** + * OBSOLETE use mustach_file + * + * fmustach - Renders the mustache 'template' in 'file' for 'itf' and 'closure'. + * + * @template: the template string to instantiate, null terminated + * @itf: the interface to the functions that mustach calls + * @closure: the closure to pass to functions called + * @file: the file where to write the result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +DEPRECATED_MUSTACH(extern int fmustach(const char *template, const struct mustach_itf *itf, void *closure, FILE *file)); + +/** + * OBSOLETE use mustach_fd + * + * fdmustach - Renders the mustache 'template' in 'fd' for 'itf' and 'closure'. + * + * @template: the template string to instantiate, null terminated + * @itf: the interface to the functions that mustach calls + * @closure: the closure to pass to functions called + * @fd: the file descriptor number where to write the result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +DEPRECATED_MUSTACH(extern int fdmustach(const char *template, const struct mustach_itf *itf, void *closure, int fd)); + +/** + * OBSOLETE use mustach_mem + * + * mustach - Renders the mustache 'template' in 'result' for 'itf' and 'closure'. + * + * @template: the template string to instantiate, null terminated + * @itf: the interface to the functions that mustach calls + * @closure: the closure to pass to functions called + * @result: the pointer receiving the result when 0 is returned + * @size: the size of the returned result + * + * Returns 0 in case of success, -1 with errno set in case of system error + * a other negative value in case of error. + */ +DEPRECATED_MUSTACH(extern int mustach(const char *template, const struct mustach_itf *itf, void *closure, char **result, size_t *size)); + +#endif + diff --git a/src/templating/pkgcfgs b/src/templating/pkgcfgs new file mode 100644 index 000000000..c3e84dc0e --- /dev/null +++ b/src/templating/pkgcfgs @@ -0,0 +1,35 @@ +==libmustach.pc== +Name: libmustach +Version: VERSION +Description: C Mustach single library +Cflags: -Imustach +Libs: -lmustach + +==libmustach-core.pc== +Name: libmustach-core +Version: VERSION +Description: C Mustach core library +Cflags: -Imustach +Libs: -lmustach-core + +==libmustach-cjson.pc== +Name: libmustach-cjson +Version: VERSION +Description: C Mustach library for cJSON +Cflags: -Imustach +Libs: -lmustach-cjson + +==libmustach-json-c.pc== +Name: libmustach-json-c +Version: VERSION +Description: C Mustach library for json-c +Cflags: -Imustach +Libs: -lmustach-json-c + +==libmustach-jansson.pc== +Name: libmustach-jansson +Version: VERSION +Description: C Mustach library for jansson +Cflags: -Imustach +Libs: -lmustach-jansson + diff --git a/src/templating/run-original-tests.sh b/src/templating/run-original-tests.sh new file mode 100755 index 000000000..21481a286 --- /dev/null +++ b/src/templating/run-original-tests.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# This file is in the public domain. +set -eux + +export CFLAGS="-g" + +echo "Ensuring clean state on entry to upstream tests ..." +make clean + +# The build fails if libjson-c-dev is not installed. +# That's OK, we don't otherwise need it and don't +# even bother testing for it in configure.ac. +# However, in that case, skip the test suite. +make -f mustach-original-Makefile mustach mustach-json-c.o || exit 77 +make -f mustach-original-Makefile clean || true +make -f mustach-original-Makefile basic-tests +make -f mustach-original-Makefile clean || true + +exit 0 diff --git a/src/templating/templating_api.c b/src/templating/templating_api.c new file mode 100644 index 000000000..88a17c682 --- /dev/null +++ b/src/templating/templating_api.c @@ -0,0 +1,524 @@ +/* + This file is part of TALER + Copyright (C) 2020, 2022 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file templating_api.c + * @brief logic to load and complete HTML templates + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include "taler_util.h" +#include "taler_mhd_lib.h" +#include "taler_templating_lib.h" +#include "mustach.h" +#include "mustach-jansson.h" +#include <gnunet/gnunet_mhd_compat.h> + + +/** + * Entry in a key-value array we use to cache templates. + */ +struct TVE +{ + /** + * A name, used as the key. NULL for the last entry. + */ + char *name; + + /** + * Language the template is in. + */ + char *lang; + + /** + * 0-terminated (!) file data to return for @e name and @e lang. + */ + char *value; + +}; + + +/** + * Array of templates loaded into RAM. + */ +static struct TVE *loaded; + +/** + * Length of the #loaded array. + */ +static unsigned int loaded_length; + + +/** + * Load Mustach template into memory. Note that we intentionally cache + * failures, that is if we ever failed to load a template, we will never try + * again. + * + * @param connection the connection we act upon + * @param name name of the template file to load + * (MUST be a 'static' string in memory!) + * @return NULL on error, otherwise the template + */ +static const char * +lookup_template (struct MHD_Connection *connection, + const char *name) +{ + struct TVE *best = NULL; + const char *lang; + + lang = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == lang) + lang = "en"; + /* find best match by language */ + for (unsigned int i = 0; i<loaded_length; i++) + { + if (0 != strcmp (loaded[i].name, + name)) + continue; /* does not match by name */ + if ( (NULL == best) || + (TALER_language_matches (lang, + loaded[i].lang) > + TALER_language_matches (lang, + best->lang) ) ) + best = &loaded[i]; + } + if (NULL == best) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "No templates found for `%s'\n", + name); + return NULL; + } + return best->value; +} + + +/** + * Get the base URL for static resources. + * + * @param con the MHD connection + * @param instance_id the instance ID + * @returns the static files base URL, guaranteed + * to have a trailing slash. + */ +static char * +make_static_url (struct MHD_Connection *con, + const char *instance_id) +{ + const char *host; + const char *forwarded_host; + const char *uri_path; + struct GNUNET_Buffer buf = { 0 }; + + host = MHD_lookup_connection_value (con, + MHD_HEADER_KIND, + "Host"); + forwarded_host = MHD_lookup_connection_value (con, + MHD_HEADER_KIND, + "X-Forwarded-Host"); + + uri_path = MHD_lookup_connection_value (con, + MHD_HEADER_KIND, + "X-Forwarded-Prefix"); + if (NULL != forwarded_host) + host = forwarded_host; + + if (NULL == host) + { + GNUNET_break (0); + return NULL; + } + + GNUNET_assert (NULL != instance_id); + + if (GNUNET_NO == TALER_mhd_is_https (con)) + GNUNET_buffer_write_str (&buf, + "http://"); + else + GNUNET_buffer_write_str (&buf, + "https://"); + GNUNET_buffer_write_str (&buf, + host); + if (NULL != uri_path) + GNUNET_buffer_write_path (&buf, + uri_path); + if (0 != strcmp ("default", + instance_id)) + { + GNUNET_buffer_write_path (&buf, + "instances"); + GNUNET_buffer_write_path (&buf, + instance_id); + } + GNUNET_buffer_write_path (&buf, + "static/"); + return GNUNET_buffer_reap_str (&buf); +} + + +int +TALER_TEMPLATING_fill (const char *tmpl, + const json_t *root, + void **result, + size_t *result_size) +{ + int eno; + + if (0 != + (eno = mustach_jansson_mem (tmpl, + 0, /* length of tmpl */ + (json_t *) root, + Mustach_With_AllExtensions, + (char **) result, + result_size))) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "mustach failed on template with error %d\n", + eno); + *result = NULL; + *result_size = 0; + return eno; + } + return eno; +} + + +enum GNUNET_GenericReturnValue +TALER_TEMPLATING_build (struct MHD_Connection *connection, + unsigned int *http_status, + const char *template, + const char *instance_id, + const char *taler_uri, + const json_t *root, + struct MHD_Response **reply) +{ + char *body; + size_t body_size; + + { + const char *tmpl; + int eno; + + tmpl = lookup_template (connection, + template); + if (NULL == tmpl) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to load template `%s'\n", + template); + *http_status = MHD_HTTP_NOT_ACCEPTABLE; + *reply = TALER_MHD_make_error (TALER_EC_GENERIC_FAILED_TO_LOAD_TEMPLATE, + template); + return GNUNET_NO; + } + /* Add default values to the context */ + if (NULL != instance_id) + { + char *static_url = make_static_url (connection, + instance_id); + + GNUNET_break (0 == + json_object_set_new ((json_t *) root, + "static_url", + json_string (static_url))); + GNUNET_free (static_url); + } + if (0 != + (eno = mustach_jansson_mem (tmpl, + 0, + (json_t *) root, + Mustach_With_NoExtensions, + &body, + &body_size))) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "mustach failed on template `%s' with error %d\n", + template, + eno); + *http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + *reply = TALER_MHD_make_error (TALER_EC_GENERIC_FAILED_TO_EXPAND_TEMPLATE, + template); + return GNUNET_NO; + } + } + + /* try to compress reply if client allows it */ + { + bool compressed = false; + + if (MHD_YES == + TALER_MHD_can_compress (connection)) + { + compressed = TALER_MHD_body_compress ((void **) &body, + &body_size); + } + *reply = MHD_create_response_from_buffer (body_size, + body, + MHD_RESPMEM_MUST_FREE); + if (NULL == *reply) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (compressed) + { + if (MHD_NO == + MHD_add_response_header (*reply, + MHD_HTTP_HEADER_CONTENT_ENCODING, + "deflate")) + { + GNUNET_break (0); + MHD_destroy_response (*reply); + *reply = NULL; + return GNUNET_SYSERR; + } + } + } + + /* Add standard headers */ + if (NULL != taler_uri) + GNUNET_break (MHD_NO != + MHD_add_response_header (*reply, + "Taler", + taler_uri)); + GNUNET_break (MHD_NO != + MHD_add_response_header (*reply, + MHD_HTTP_HEADER_CONTENT_TYPE, + "text/html")); + return GNUNET_OK; +} + + +enum GNUNET_GenericReturnValue +TALER_TEMPLATING_reply (struct MHD_Connection *connection, + unsigned int http_status, + const char *template, + const char *instance_id, + const char *taler_uri, + const json_t *root) +{ + enum GNUNET_GenericReturnValue res; + struct MHD_Response *reply; + MHD_RESULT ret; + + res = TALER_TEMPLATING_build (connection, + &http_status, + template, + instance_id, + taler_uri, + root, + &reply); + if (GNUNET_SYSERR == res) + return res; + ret = MHD_queue_response (connection, + http_status, + reply); + MHD_destroy_response (reply); + if (MHD_NO == ret) + return GNUNET_SYSERR; + return (res == GNUNET_OK) + ? GNUNET_OK + : GNUNET_NO; +} + + +/** + * Function called with a template's filename. + * + * @param cls closure, NULL + * @param filename complete filename (absolute path) + * @return #GNUNET_OK to continue to iterate, + * #GNUNET_NO to stop iteration with no error, + * #GNUNET_SYSERR to abort iteration with error! + */ +static enum GNUNET_GenericReturnValue +load_template (void *cls, + const char *filename) +{ + char *lang; + char *end; + int fd; + struct stat sb; + char *map; + const char *name; + + (void) cls; + if ('.' == filename[0]) + return GNUNET_OK; + name = strrchr (filename, + '/'); + if (NULL == name) + name = filename; + else + name++; + lang = strchr (name, + '.'); + if (NULL == lang) + return GNUNET_OK; /* name must include .$LANG */ + lang++; + end = strchr (lang, + '.'); + if ( (NULL == end) || + (0 != strcmp (end, + ".must")) ) + return GNUNET_OK; /* name must end with '.must' */ + + /* finally open template */ + fd = open (filename, + O_RDONLY); + if (-1 == fd) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "open", + filename); + + return GNUNET_SYSERR; + } + if (0 != + fstat (fd, + &sb)) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "fstat", + filename); + GNUNET_break (0 == close (fd)); + return GNUNET_OK; + } + map = GNUNET_malloc_large (sb.st_size + 1); + if (NULL == map) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "malloc"); + GNUNET_break (0 == close (fd)); + return GNUNET_SYSERR; + } + if (sb.st_size != + read (fd, + map, + sb.st_size)) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "read", + filename); + GNUNET_break (0 == close (fd)); + return GNUNET_OK; + } + GNUNET_break (0 == close (fd)); + GNUNET_array_grow (loaded, + loaded_length, + loaded_length + 1); + loaded[loaded_length - 1].name = GNUNET_strndup (name, + (lang - 1) - name); + loaded[loaded_length - 1].lang = GNUNET_strndup (lang, + end - lang); + loaded[loaded_length - 1].value = map; + return GNUNET_OK; +} + + +MHD_RESULT +TALER_TEMPLATING_reply_error ( + struct MHD_Connection *connection, + const char *template_basename, + unsigned int http_status, + enum TALER_ErrorCode ec, + const char *detail) +{ + json_t *data; + enum GNUNET_GenericReturnValue ret; + + data = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("ec", + ec), + GNUNET_JSON_pack_string ("hint", + TALER_ErrorCode_get_hint (ec)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("detail", + detail)) + ); + ret = TALER_TEMPLATING_reply (connection, + http_status, + template_basename, + NULL, + NULL, + data); + json_decref (data); + switch (ret) + { + case GNUNET_OK: + return MHD_YES; + case GNUNET_NO: + return MHD_YES; + case GNUNET_SYSERR: + return MHD_NO; + } + GNUNET_assert (0); + return MHD_NO; +} + + +enum GNUNET_GenericReturnValue +TALER_TEMPLATING_init (const char *subsystem) +{ + char *dn; + int ret; + + { + char *path; + + path = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR); + if (NULL == path) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + GNUNET_asprintf (&dn, + "%s/%s/templates/", + path, + subsystem); + GNUNET_free (path); + } + ret = GNUNET_DISK_directory_scan (dn, + &load_template, + NULL); + GNUNET_free (dn); + if (-1 == ret) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +void +TALER_TEMPLATING_done (void) +{ + for (unsigned int i = 0; i<loaded_length; i++) + { + GNUNET_free (loaded[i].name); + GNUNET_free (loaded[i].lang); + GNUNET_free (loaded[i].value); + } + GNUNET_array_grow (loaded, + loaded_length, + 0); +} + + +/* end of templating_api.c */ diff --git a/src/templating/test-specs/test-specs-cjson.ref b/src/templating/test-specs/test-specs-cjson.ref new file mode 100644 index 000000000..8897c66cc --- /dev/null +++ b/src/templating/test-specs/test-specs-cjson.ref @@ -0,0 +1,425 @@ + +loading test-specs/spec/specs/comments.json +processing file test-specs/spec/specs/comments.json +[0] Inline + Comment blocks should be removed from the template. + => SUCCESS +[1] Multiline + Multiline comments should be permitted. + => SUCCESS +[2] Standalone + All standalone comment lines should be removed. + => SUCCESS +[3] Indented Standalone + All standalone comment lines should be removed. + => SUCCESS +[4] Standalone Line Endings + "\r\n" should be considered a newline for standalone tags. + => SUCCESS +[5] Standalone Without Previous Line + Standalone tags should not require a newline to precede them. + => SUCCESS +[6] Standalone Without Newline + Standalone tags should not require a newline to follow them. + => SUCCESS +[7] Multiline Standalone + All standalone comment lines should be removed. + => SUCCESS +[8] Indented Multiline Standalone + All standalone comment lines should be removed. + => SUCCESS +[9] Indented Inline + Inline comments should not strip whitespace + => SUCCESS +[10] Surrounding Whitespace + Comment removal should preserve surrounding whitespace. + => SUCCESS +[11] Variable Name Collision + Comments must never render, even if variable with same name exists. + => SUCCESS + +loading test-specs/spec/specs/delimiters.json +processing file test-specs/spec/specs/delimiters.json +[0] Pair Behavior + The equals sign (used on both sides) should permit delimiter changes. + => SUCCESS +[1] Special Characters + Characters with special meaning regexen should be valid delimiters. + => SUCCESS +[2] Sections + Delimiters set outside sections should persist. + => SUCCESS +[3] Inverted Sections + Delimiters set outside inverted sections should persist. + => SUCCESS +[4] Partial Inheritence + Delimiters set in a parent template should not affect a partial. + => SUCCESS +[5] Post-Partial Behavior + Delimiters set in a partial should not affect the parent template. + => SUCCESS +[6] Surrounding Whitespace + Surrounding whitespace should be left untouched. + => SUCCESS +[7] Outlying Whitespace (Inline) + Whitespace should be left untouched. + => SUCCESS +[8] Standalone Tag + Standalone lines should be removed from the template. + => SUCCESS +[9] Indented Standalone Tag + Indented standalone lines should be removed from the template. + => SUCCESS +[10] Standalone Line Endings + "\r\n" should be considered a newline for standalone tags. + => SUCCESS +[11] Standalone Without Previous Line + Standalone tags should not require a newline to precede them. + => SUCCESS +[12] Standalone Without Newline + Standalone tags should not require a newline to follow them. + => SUCCESS +[13] Pair with Padding + Superfluous in-tag whitespace should be ignored. + => SUCCESS + +loading test-specs/spec/specs/interpolation.json +processing file test-specs/spec/specs/interpolation.json +[0] No Interpolation + Mustache-free templates should render as-is. + => SUCCESS +[1] Basic Interpolation + Unadorned tags should interpolate content into the template. + => SUCCESS +[2] HTML Escaping + Basic interpolation should be HTML escaped. + => SUCCESS +[3] Triple Mustache + Triple mustaches should interpolate without HTML escaping. + => SUCCESS +[4] Ampersand + Ampersand should interpolate without HTML escaping. + => SUCCESS +[5] Basic Integer Interpolation + Integers should interpolate seamlessly. + => SUCCESS +[6] Triple Mustache Integer Interpolation + Integers should interpolate seamlessly. + => SUCCESS +[7] Ampersand Integer Interpolation + Integers should interpolate seamlessly. + => SUCCESS +[8] Basic Decimal Interpolation + Decimals should interpolate seamlessly with proper significance. + => SUCCESS +[9] Triple Mustache Decimal Interpolation + Decimals should interpolate seamlessly with proper significance. + => SUCCESS +[10] Ampersand Decimal Interpolation + Decimals should interpolate seamlessly with proper significance. + => SUCCESS +[11] Basic Null Interpolation + Nulls should interpolate as the empty string. + => SUCCESS +[12] Triple Mustache Null Interpolation + Nulls should interpolate as the empty string. + => SUCCESS +[13] Ampersand Null Interpolation + Nulls should interpolate as the empty string. + => SUCCESS +[14] Basic Context Miss Interpolation + Failed context lookups should default to empty strings. + => SUCCESS +[15] Triple Mustache Context Miss Interpolation + Failed context lookups should default to empty strings. + => SUCCESS +[16] Ampersand Context Miss Interpolation + Failed context lookups should default to empty strings. + => SUCCESS +[17] Dotted Names - Basic Interpolation + Dotted names should be considered a form of shorthand for sections. + => SUCCESS +[18] Dotted Names - Triple Mustache Interpolation + Dotted names should be considered a form of shorthand for sections. + => SUCCESS +[19] Dotted Names - Ampersand Interpolation + Dotted names should be considered a form of shorthand for sections. + => SUCCESS +[20] Dotted Names - Arbitrary Depth + Dotted names should be functional to any level of nesting. + => SUCCESS +[21] Dotted Names - Broken Chains + Any falsey value prior to the last part of the name should yield ''. + => SUCCESS +[22] Dotted Names - Broken Chain Resolution + Each part of a dotted name should resolve only against its parent. + => SUCCESS +[23] Dotted Names - Initial Resolution + The first part of a dotted name should resolve as any other name. + => SUCCESS +[24] Dotted Names - Context Precedence + Dotted names should be resolved against former resolutions. + => SUCCESS +[25] Implicit Iterators - Basic Interpolation + Unadorned tags should interpolate content into the template. + => SUCCESS +[26] Implicit Iterators - HTML Escaping + Basic interpolation should be HTML escaped. + => SUCCESS +[27] Implicit Iterators - Triple Mustache + Triple mustaches should interpolate without HTML escaping. + => SUCCESS +[28] Implicit Iterators - Ampersand + Ampersand should interpolate without HTML escaping. + => SUCCESS +[29] Implicit Iterators - Basic Integer Interpolation + Integers should interpolate seamlessly. + => SUCCESS +[30] Interpolation - Surrounding Whitespace + Interpolation should not alter surrounding whitespace. + => SUCCESS +[31] Triple Mustache - Surrounding Whitespace + Interpolation should not alter surrounding whitespace. + => SUCCESS +[32] Ampersand - Surrounding Whitespace + Interpolation should not alter surrounding whitespace. + => SUCCESS +[33] Interpolation - Standalone + Standalone interpolation should not alter surrounding whitespace. + => SUCCESS +[34] Triple Mustache - Standalone + Standalone interpolation should not alter surrounding whitespace. + => SUCCESS +[35] Ampersand - Standalone + Standalone interpolation should not alter surrounding whitespace. + => SUCCESS +[36] Interpolation With Padding + Superfluous in-tag whitespace should be ignored. + => SUCCESS +[37] Triple Mustache With Padding + Superfluous in-tag whitespace should be ignored. + => SUCCESS +[38] Ampersand With Padding + Superfluous in-tag whitespace should be ignored. + => SUCCESS + +loading test-specs/spec/specs/inverted.json +processing file test-specs/spec/specs/inverted.json +[0] Falsey + Falsey sections should have their contents rendered. + => SUCCESS +[1] Truthy + Truthy sections should have their contents omitted. + => SUCCESS +[2] Null is falsey + Null is falsey. + => SUCCESS +[3] Context + Objects and hashes should behave like truthy values. + => SUCCESS +[4] List + Lists should behave like truthy values. + => SUCCESS +[5] Empty List + Empty lists should behave like falsey values. + => SUCCESS +[6] Doubled + Multiple inverted sections per template should be permitted. + => SUCCESS +[7] Nested (Falsey) + Nested falsey sections should have their contents rendered. + => SUCCESS +[8] Nested (Truthy) + Nested truthy sections should be omitted. + => SUCCESS +[9] Context Misses + Failed context lookups should be considered falsey. + => SUCCESS +[10] Dotted Names - Truthy + Dotted names should be valid for Inverted Section tags. + => SUCCESS +[11] Dotted Names - Falsey + Dotted names should be valid for Inverted Section tags. + => SUCCESS +[12] Dotted Names - Broken Chains + Dotted names that cannot be resolved should be considered falsey. + => SUCCESS +[13] Surrounding Whitespace + Inverted sections should not alter surrounding whitespace. + => SUCCESS +[14] Internal Whitespace + Inverted should not alter internal whitespace. + => SUCCESS +[15] Indented Inline Sections + Single-line sections should not alter surrounding whitespace. + => SUCCESS +[16] Standalone Lines + Standalone lines should be removed from the template. + => SUCCESS +[17] Standalone Indented Lines + Standalone indented lines should be removed from the template. + => SUCCESS +[18] Standalone Line Endings + "\r\n" should be considered a newline for standalone tags. + => SUCCESS +[19] Standalone Without Previous Line + Standalone tags should not require a newline to precede them. + => SUCCESS +[20] Standalone Without Newline + Standalone tags should not require a newline to follow them. + => SUCCESS +[21] Padding + Superfluous in-tag whitespace should be ignored. + => SUCCESS + +loading test-specs/spec/specs/partials.json +processing file test-specs/spec/specs/partials.json +[0] Basic Behavior + The greater-than operator should expand to the named partial. + => SUCCESS +[1] Failed Lookup + The empty string should be used when the named partial is not found. + => SUCCESS +[2] Context + The greater-than operator should operate within the current context. + => SUCCESS +[3] Recursion + The greater-than operator should properly recurse. + => SUCCESS +[4] Nested + The greater-than operator should work from within partials. + => SUCCESS +[5] Surrounding Whitespace + The greater-than operator should not alter surrounding whitespace. + => SUCCESS +[6] Inline Indentation + Whitespace should be left untouched. + => SUCCESS +[7] Standalone Line Endings + "\r\n" should be considered a newline for standalone tags. + => SUCCESS +[8] Standalone Without Previous Line + Standalone tags should not require a newline to precede them. + => SUCCESS +[9] Standalone Without Newline + Standalone tags should not require a newline to follow them. + => SUCCESS +[10] Standalone Indentation + Each line of the partial should be indented before rendering. + => SUCCESS +[11] Padding Whitespace + Superfluous in-tag whitespace should be ignored. + => SUCCESS + +loading test-specs/spec/specs/sections.json +processing file test-specs/spec/specs/sections.json +[0] Truthy + Truthy sections should have their contents rendered. + => SUCCESS +[1] Falsey + Falsey sections should have their contents omitted. + => SUCCESS +[2] Null is falsey + Null is falsey. + => SUCCESS +[3] Context + Objects and hashes should be pushed onto the context stack. + => SUCCESS +[4] Parent contexts + Names missing in the current context are looked up in the stack. + => SUCCESS +[5] Variable test + Non-false sections have their value at the top of context, +accessible as {{.}} or through the parent context. This gives +a simple way to display content conditionally if a variable exists. + + => SUCCESS +[6] List Contexts + All elements on the context stack should be accessible within lists. + => SUCCESS +[7] Deeply Nested Contexts + All elements on the context stack should be accessible. + => SUCCESS +[8] List + Lists should be iterated; list items should visit the context stack. + => SUCCESS +[9] Empty List + Empty lists should behave like falsey values. + => SUCCESS +[10] Doubled + Multiple sections per template should be permitted. + => SUCCESS +[11] Nested (Truthy) + Nested truthy sections should have their contents rendered. + => SUCCESS +[12] Nested (Falsey) + Nested falsey sections should be omitted. + => SUCCESS +[13] Context Misses + Failed context lookups should be considered falsey. + => SUCCESS +[14] Implicit Iterator - String + Implicit iterators should directly interpolate strings. + => SUCCESS +[15] Implicit Iterator - Integer + Implicit iterators should cast integers to strings and interpolate. + => SUCCESS +[16] Implicit Iterator - Decimal + Implicit iterators should cast decimals to strings and interpolate. + => SUCCESS +[17] Implicit Iterator - Array + Implicit iterators should allow iterating over nested arrays. + => SUCCESS +[18] Implicit Iterator - HTML Escaping + Implicit iterators with basic interpolation should be HTML escaped. + => SUCCESS +[19] Implicit Iterator - Triple mustache + Implicit iterators in triple mustache should interpolate without HTML escaping. + => SUCCESS +[20] Implicit Iterator - Ampersand + Implicit iterators in an Ampersand tag should interpolate without HTML escaping. + => SUCCESS +[21] Implicit Iterator - Root-level + Implicit iterators should work on root-level lists. + => SUCCESS +[22] Dotted Names - Truthy + Dotted names should be valid for Section tags. + => SUCCESS +[23] Dotted Names - Falsey + Dotted names should be valid for Section tags. + => SUCCESS +[24] Dotted Names - Broken Chains + Dotted names that cannot be resolved should be considered falsey. + => SUCCESS +[25] Surrounding Whitespace + Sections should not alter surrounding whitespace. + => SUCCESS +[26] Internal Whitespace + Sections should not alter internal whitespace. + => SUCCESS +[27] Indented Inline Sections + Single-line sections should not alter surrounding whitespace. + => SUCCESS +[28] Standalone Lines + Standalone lines should be removed from the template. + => SUCCESS +[29] Indented Standalone Lines + Indented standalone lines should be removed from the template. + => SUCCESS +[30] Standalone Line Endings + "\r\n" should be considered a newline for standalone tags. + => SUCCESS +[31] Standalone Without Previous Line + Standalone tags should not require a newline to precede them. + => SUCCESS +[32] Standalone Without Newline + Standalone tags should not require a newline to follow them. + => SUCCESS +[33] Padding + Superfluous in-tag whitespace should be ignored. + => SUCCESS + +summary: + error 0 + differ 0 + success 133 diff --git a/src/templating/test-specs/test-specs-jansson.ref b/src/templating/test-specs/test-specs-jansson.ref new file mode 100644 index 000000000..a1cef19c1 --- /dev/null +++ b/src/templating/test-specs/test-specs-jansson.ref @@ -0,0 +1,429 @@ + +loading test-specs/spec/specs/comments.json +processing file test-specs/spec/specs/comments.json +[0] Inline + Comment blocks should be removed from the template. + => SUCCESS +[1] Multiline + Multiline comments should be permitted. + => SUCCESS +[2] Standalone + All standalone comment lines should be removed. + => SUCCESS +[3] Indented Standalone + All standalone comment lines should be removed. + => SUCCESS +[4] Standalone Line Endings + "\r\n" should be considered a newline for standalone tags. + => SUCCESS +[5] Standalone Without Previous Line + Standalone tags should not require a newline to precede them. + => SUCCESS +[6] Standalone Without Newline + Standalone tags should not require a newline to follow them. + => SUCCESS +[7] Multiline Standalone + All standalone comment lines should be removed. + => SUCCESS +[8] Indented Multiline Standalone + All standalone comment lines should be removed. + => SUCCESS +[9] Indented Inline + Inline comments should not strip whitespace + => SUCCESS +[10] Surrounding Whitespace + Comment removal should preserve surrounding whitespace. + => SUCCESS +[11] Variable Name Collision + Comments must never render, even if variable with same name exists. + => SUCCESS + +loading test-specs/spec/specs/delimiters.json +processing file test-specs/spec/specs/delimiters.json +[0] Pair Behavior + The equals sign (used on both sides) should permit delimiter changes. + => SUCCESS +[1] Special Characters + Characters with special meaning regexen should be valid delimiters. + => SUCCESS +[2] Sections + Delimiters set outside sections should persist. + => SUCCESS +[3] Inverted Sections + Delimiters set outside inverted sections should persist. + => SUCCESS +[4] Partial Inheritence + Delimiters set in a parent template should not affect a partial. + => SUCCESS +[5] Post-Partial Behavior + Delimiters set in a partial should not affect the parent template. + => SUCCESS +[6] Surrounding Whitespace + Surrounding whitespace should be left untouched. + => SUCCESS +[7] Outlying Whitespace (Inline) + Whitespace should be left untouched. + => SUCCESS +[8] Standalone Tag + Standalone lines should be removed from the template. + => SUCCESS +[9] Indented Standalone Tag + Indented standalone lines should be removed from the template. + => SUCCESS +[10] Standalone Line Endings + "\r\n" should be considered a newline for standalone tags. + => SUCCESS +[11] Standalone Without Previous Line + Standalone tags should not require a newline to precede them. + => SUCCESS +[12] Standalone Without Newline + Standalone tags should not require a newline to follow them. + => SUCCESS +[13] Pair with Padding + Superfluous in-tag whitespace should be ignored. + => SUCCESS + +loading test-specs/spec/specs/interpolation.json +processing file test-specs/spec/specs/interpolation.json +[0] No Interpolation + Mustache-free templates should render as-is. + => SUCCESS +[1] Basic Interpolation + Unadorned tags should interpolate content into the template. + => SUCCESS +[2] HTML Escaping + Basic interpolation should be HTML escaped. + => SUCCESS +[3] Triple Mustache + Triple mustaches should interpolate without HTML escaping. + => SUCCESS +[4] Ampersand + Ampersand should interpolate without HTML escaping. + => SUCCESS +[5] Basic Integer Interpolation + Integers should interpolate seamlessly. + => SUCCESS +[6] Triple Mustache Integer Interpolation + Integers should interpolate seamlessly. + => SUCCESS +[7] Ampersand Integer Interpolation + Integers should interpolate seamlessly. + => SUCCESS +[8] Basic Decimal Interpolation + Decimals should interpolate seamlessly with proper significance. + => SUCCESS +[9] Triple Mustache Decimal Interpolation + Decimals should interpolate seamlessly with proper significance. + => SUCCESS +[10] Ampersand Decimal Interpolation + Decimals should interpolate seamlessly with proper significance. + => SUCCESS +[11] Basic Null Interpolation + Nulls should interpolate as the empty string. + => SUCCESS +[12] Triple Mustache Null Interpolation + Nulls should interpolate as the empty string. + => SUCCESS +[13] Ampersand Null Interpolation + Nulls should interpolate as the empty string. + => SUCCESS +[14] Basic Context Miss Interpolation + Failed context lookups should default to empty strings. + => SUCCESS +[15] Triple Mustache Context Miss Interpolation + Failed context lookups should default to empty strings. + => SUCCESS +[16] Ampersand Context Miss Interpolation + Failed context lookups should default to empty strings. + => SUCCESS +[17] Dotted Names - Basic Interpolation + Dotted names should be considered a form of shorthand for sections. + => SUCCESS +[18] Dotted Names - Triple Mustache Interpolation + Dotted names should be considered a form of shorthand for sections. + => SUCCESS +[19] Dotted Names - Ampersand Interpolation + Dotted names should be considered a form of shorthand for sections. + => SUCCESS +[20] Dotted Names - Arbitrary Depth + Dotted names should be functional to any level of nesting. + => SUCCESS +[21] Dotted Names - Broken Chains + Any falsey value prior to the last part of the name should yield ''. + => SUCCESS +[22] Dotted Names - Broken Chain Resolution + Each part of a dotted name should resolve only against its parent. + => SUCCESS +[23] Dotted Names - Initial Resolution + The first part of a dotted name should resolve as any other name. + => SUCCESS +[24] Dotted Names - Context Precedence + Dotted names should be resolved against former resolutions. + => SUCCESS +[25] Implicit Iterators - Basic Interpolation + Unadorned tags should interpolate content into the template. + => SUCCESS +[26] Implicit Iterators - HTML Escaping + Basic interpolation should be HTML escaped. + => SUCCESS +[27] Implicit Iterators - Triple Mustache + Triple mustaches should interpolate without HTML escaping. + => SUCCESS +[28] Implicit Iterators - Ampersand + Ampersand should interpolate without HTML escaping. + => SUCCESS +[29] Implicit Iterators - Basic Integer Interpolation + Integers should interpolate seamlessly. + => SUCCESS +[30] Interpolation - Surrounding Whitespace + Interpolation should not alter surrounding whitespace. + => SUCCESS +[31] Triple Mustache - Surrounding Whitespace + Interpolation should not alter surrounding whitespace. + => SUCCESS +[32] Ampersand - Surrounding Whitespace + Interpolation should not alter surrounding whitespace. + => SUCCESS +[33] Interpolation - Standalone + Standalone interpolation should not alter surrounding whitespace. + => SUCCESS +[34] Triple Mustache - Standalone + Standalone interpolation should not alter surrounding whitespace. + => SUCCESS +[35] Ampersand - Standalone + Standalone interpolation should not alter surrounding whitespace. + => SUCCESS +[36] Interpolation With Padding + Superfluous in-tag whitespace should be ignored. + => SUCCESS +[37] Triple Mustache With Padding + Superfluous in-tag whitespace should be ignored. + => SUCCESS +[38] Ampersand With Padding + Superfluous in-tag whitespace should be ignored. + => SUCCESS + +loading test-specs/spec/specs/inverted.json +processing file test-specs/spec/specs/inverted.json +[0] Falsey + Falsey sections should have their contents rendered. + => SUCCESS +[1] Truthy + Truthy sections should have their contents omitted. + => SUCCESS +[2] Null is falsey + Null is falsey. + => SUCCESS +[3] Context + Objects and hashes should behave like truthy values. + => SUCCESS +[4] List + Lists should behave like truthy values. + => SUCCESS +[5] Empty List + Empty lists should behave like falsey values. + => SUCCESS +[6] Doubled + Multiple inverted sections per template should be permitted. + => SUCCESS +[7] Nested (Falsey) + Nested falsey sections should have their contents rendered. + => SUCCESS +[8] Nested (Truthy) + Nested truthy sections should be omitted. + => SUCCESS +[9] Context Misses + Failed context lookups should be considered falsey. + => SUCCESS +[10] Dotted Names - Truthy + Dotted names should be valid for Inverted Section tags. + => SUCCESS +[11] Dotted Names - Falsey + Dotted names should be valid for Inverted Section tags. + => SUCCESS +[12] Dotted Names - Broken Chains + Dotted names that cannot be resolved should be considered falsey. + => SUCCESS +[13] Surrounding Whitespace + Inverted sections should not alter surrounding whitespace. + => SUCCESS +[14] Internal Whitespace + Inverted should not alter internal whitespace. + => SUCCESS +[15] Indented Inline Sections + Single-line sections should not alter surrounding whitespace. + => SUCCESS +[16] Standalone Lines + Standalone lines should be removed from the template. + => SUCCESS +[17] Standalone Indented Lines + Standalone indented lines should be removed from the template. + => SUCCESS +[18] Standalone Line Endings + "\r\n" should be considered a newline for standalone tags. + => SUCCESS +[19] Standalone Without Previous Line + Standalone tags should not require a newline to precede them. + => SUCCESS +[20] Standalone Without Newline + Standalone tags should not require a newline to follow them. + => SUCCESS +[21] Padding + Superfluous in-tag whitespace should be ignored. + => SUCCESS + +loading test-specs/spec/specs/partials.json +processing file test-specs/spec/specs/partials.json +[0] Basic Behavior + The greater-than operator should expand to the named partial. + => SUCCESS +[1] Failed Lookup + The empty string should be used when the named partial is not found. + => SUCCESS +[2] Context + The greater-than operator should operate within the current context. + => SUCCESS +[3] Recursion + The greater-than operator should properly recurse. + => SUCCESS +[4] Nested + The greater-than operator should work from within partials. + => SUCCESS +[5] Surrounding Whitespace + The greater-than operator should not alter surrounding whitespace. + => SUCCESS +[6] Inline Indentation + Whitespace should be left untouched. + => SUCCESS +[7] Standalone Line Endings + "\r\n" should be considered a newline for standalone tags. + => SUCCESS +[8] Standalone Without Previous Line + Standalone tags should not require a newline to precede them. + => SUCCESS +[9] Standalone Without Newline + Standalone tags should not require a newline to follow them. + => SUCCESS +[10] Standalone Indentation + Each line of the partial should be indented before rendering. + => SUCCESS +[11] Padding Whitespace + Superfluous in-tag whitespace should be ignored. + => SUCCESS + +loading test-specs/spec/specs/sections.json +processing file test-specs/spec/specs/sections.json +[0] Truthy + Truthy sections should have their contents rendered. + => SUCCESS +[1] Falsey + Falsey sections should have their contents omitted. + => SUCCESS +[2] Null is falsey + Null is falsey. + => SUCCESS +[3] Context + Objects and hashes should be pushed onto the context stack. + => SUCCESS +[4] Parent contexts + Names missing in the current context are looked up in the stack. + => SUCCESS +[5] Variable test + Non-false sections have their value at the top of context, +accessible as {{.}} or through the parent context. This gives +a simple way to display content conditionally if a variable exists. + + => SUCCESS +[6] List Contexts + All elements on the context stack should be accessible within lists. + => SUCCESS +[7] Deeply Nested Contexts + All elements on the context stack should be accessible. + => SUCCESS +[8] List + Lists should be iterated; list items should visit the context stack. + => SUCCESS +[9] Empty List + Empty lists should behave like falsey values. + => SUCCESS +[10] Doubled + Multiple sections per template should be permitted. + => SUCCESS +[11] Nested (Truthy) + Nested truthy sections should have their contents rendered. + => SUCCESS +[12] Nested (Falsey) + Nested falsey sections should be omitted. + => SUCCESS +[13] Context Misses + Failed context lookups should be considered falsey. + => SUCCESS +[14] Implicit Iterator - String + Implicit iterators should directly interpolate strings. + => SUCCESS +[15] Implicit Iterator - Integer + Implicit iterators should cast integers to strings and interpolate. + => SUCCESS +[16] Implicit Iterator - Decimal + Implicit iterators should cast decimals to strings and interpolate. + => DIFFERS + .. DATA[{"list":[1.1000000000000001,2.2000000000000002,3.2999999999999998,4.4000000000000004,5.5]}] + .. TEMPLATE["{{#list}}({{.}}){{/list}}"] + .. EXPECTED["(1.1)(2.2)(3.3)(4.4)(5.5)"] + .. GOT["(1.1000000000000001)(2.2000000000000002)(3.2999999999999998)(4.4000000000000004)(5.5)"] +[17] Implicit Iterator - Array + Implicit iterators should allow iterating over nested arrays. + => SUCCESS +[18] Implicit Iterator - HTML Escaping + Implicit iterators with basic interpolation should be HTML escaped. + => SUCCESS +[19] Implicit Iterator - Triple mustache + Implicit iterators in triple mustache should interpolate without HTML escaping. + => SUCCESS +[20] Implicit Iterator - Ampersand + Implicit iterators in an Ampersand tag should interpolate without HTML escaping. + => SUCCESS +[21] Implicit Iterator - Root-level + Implicit iterators should work on root-level lists. + => SUCCESS +[22] Dotted Names - Truthy + Dotted names should be valid for Section tags. + => SUCCESS +[23] Dotted Names - Falsey + Dotted names should be valid for Section tags. + => SUCCESS +[24] Dotted Names - Broken Chains + Dotted names that cannot be resolved should be considered falsey. + => SUCCESS +[25] Surrounding Whitespace + Sections should not alter surrounding whitespace. + => SUCCESS +[26] Internal Whitespace + Sections should not alter internal whitespace. + => SUCCESS +[27] Indented Inline Sections + Single-line sections should not alter surrounding whitespace. + => SUCCESS +[28] Standalone Lines + Standalone lines should be removed from the template. + => SUCCESS +[29] Indented Standalone Lines + Indented standalone lines should be removed from the template. + => SUCCESS +[30] Standalone Line Endings + "\r\n" should be considered a newline for standalone tags. + => SUCCESS +[31] Standalone Without Previous Line + Standalone tags should not require a newline to precede them. + => SUCCESS +[32] Standalone Without Newline + Standalone tags should not require a newline to follow them. + => SUCCESS +[33] Padding + Superfluous in-tag whitespace should be ignored. + => SUCCESS + +summary: + error 0 + differ 1 + success 132 diff --git a/src/templating/test-specs/test-specs-json-c.ref b/src/templating/test-specs/test-specs-json-c.ref new file mode 100644 index 000000000..8897c66cc --- /dev/null +++ b/src/templating/test-specs/test-specs-json-c.ref @@ -0,0 +1,425 @@ + +loading test-specs/spec/specs/comments.json +processing file test-specs/spec/specs/comments.json +[0] Inline + Comment blocks should be removed from the template. + => SUCCESS +[1] Multiline + Multiline comments should be permitted. + => SUCCESS +[2] Standalone + All standalone comment lines should be removed. + => SUCCESS +[3] Indented Standalone + All standalone comment lines should be removed. + => SUCCESS +[4] Standalone Line Endings + "\r\n" should be considered a newline for standalone tags. + => SUCCESS +[5] Standalone Without Previous Line + Standalone tags should not require a newline to precede them. + => SUCCESS +[6] Standalone Without Newline + Standalone tags should not require a newline to follow them. + => SUCCESS +[7] Multiline Standalone + All standalone comment lines should be removed. + => SUCCESS +[8] Indented Multiline Standalone + All standalone comment lines should be removed. + => SUCCESS +[9] Indented Inline + Inline comments should not strip whitespace + => SUCCESS +[10] Surrounding Whitespace + Comment removal should preserve surrounding whitespace. + => SUCCESS +[11] Variable Name Collision + Comments must never render, even if variable with same name exists. + => SUCCESS + +loading test-specs/spec/specs/delimiters.json +processing file test-specs/spec/specs/delimiters.json +[0] Pair Behavior + The equals sign (used on both sides) should permit delimiter changes. + => SUCCESS +[1] Special Characters + Characters with special meaning regexen should be valid delimiters. + => SUCCESS +[2] Sections + Delimiters set outside sections should persist. + => SUCCESS +[3] Inverted Sections + Delimiters set outside inverted sections should persist. + => SUCCESS +[4] Partial Inheritence + Delimiters set in a parent template should not affect a partial. + => SUCCESS +[5] Post-Partial Behavior + Delimiters set in a partial should not affect the parent template. + => SUCCESS +[6] Surrounding Whitespace + Surrounding whitespace should be left untouched. + => SUCCESS +[7] Outlying Whitespace (Inline) + Whitespace should be left untouched. + => SUCCESS +[8] Standalone Tag + Standalone lines should be removed from the template. + => SUCCESS +[9] Indented Standalone Tag + Indented standalone lines should be removed from the template. + => SUCCESS +[10] Standalone Line Endings + "\r\n" should be considered a newline for standalone tags. + => SUCCESS +[11] Standalone Without Previous Line + Standalone tags should not require a newline to precede them. + => SUCCESS +[12] Standalone Without Newline + Standalone tags should not require a newline to follow them. + => SUCCESS +[13] Pair with Padding + Superfluous in-tag whitespace should be ignored. + => SUCCESS + +loading test-specs/spec/specs/interpolation.json +processing file test-specs/spec/specs/interpolation.json +[0] No Interpolation + Mustache-free templates should render as-is. + => SUCCESS +[1] Basic Interpolation + Unadorned tags should interpolate content into the template. + => SUCCESS +[2] HTML Escaping + Basic interpolation should be HTML escaped. + => SUCCESS +[3] Triple Mustache + Triple mustaches should interpolate without HTML escaping. + => SUCCESS +[4] Ampersand + Ampersand should interpolate without HTML escaping. + => SUCCESS +[5] Basic Integer Interpolation + Integers should interpolate seamlessly. + => SUCCESS +[6] Triple Mustache Integer Interpolation + Integers should interpolate seamlessly. + => SUCCESS +[7] Ampersand Integer Interpolation + Integers should interpolate seamlessly. + => SUCCESS +[8] Basic Decimal Interpolation + Decimals should interpolate seamlessly with proper significance. + => SUCCESS +[9] Triple Mustache Decimal Interpolation + Decimals should interpolate seamlessly with proper significance. + => SUCCESS +[10] Ampersand Decimal Interpolation + Decimals should interpolate seamlessly with proper significance. + => SUCCESS +[11] Basic Null Interpolation + Nulls should interpolate as the empty string. + => SUCCESS +[12] Triple Mustache Null Interpolation + Nulls should interpolate as the empty string. + => SUCCESS +[13] Ampersand Null Interpolation + Nulls should interpolate as the empty string. + => SUCCESS +[14] Basic Context Miss Interpolation + Failed context lookups should default to empty strings. + => SUCCESS +[15] Triple Mustache Context Miss Interpolation + Failed context lookups should default to empty strings. + => SUCCESS +[16] Ampersand Context Miss Interpolation + Failed context lookups should default to empty strings. + => SUCCESS +[17] Dotted Names - Basic Interpolation + Dotted names should be considered a form of shorthand for sections. + => SUCCESS +[18] Dotted Names - Triple Mustache Interpolation + Dotted names should be considered a form of shorthand for sections. + => SUCCESS +[19] Dotted Names - Ampersand Interpolation + Dotted names should be considered a form of shorthand for sections. + => SUCCESS +[20] Dotted Names - Arbitrary Depth + Dotted names should be functional to any level of nesting. + => SUCCESS +[21] Dotted Names - Broken Chains + Any falsey value prior to the last part of the name should yield ''. + => SUCCESS +[22] Dotted Names - Broken Chain Resolution + Each part of a dotted name should resolve only against its parent. + => SUCCESS +[23] Dotted Names - Initial Resolution + The first part of a dotted name should resolve as any other name. + => SUCCESS +[24] Dotted Names - Context Precedence + Dotted names should be resolved against former resolutions. + => SUCCESS +[25] Implicit Iterators - Basic Interpolation + Unadorned tags should interpolate content into the template. + => SUCCESS +[26] Implicit Iterators - HTML Escaping + Basic interpolation should be HTML escaped. + => SUCCESS +[27] Implicit Iterators - Triple Mustache + Triple mustaches should interpolate without HTML escaping. + => SUCCESS +[28] Implicit Iterators - Ampersand + Ampersand should interpolate without HTML escaping. + => SUCCESS +[29] Implicit Iterators - Basic Integer Interpolation + Integers should interpolate seamlessly. + => SUCCESS +[30] Interpolation - Surrounding Whitespace + Interpolation should not alter surrounding whitespace. + => SUCCESS +[31] Triple Mustache - Surrounding Whitespace + Interpolation should not alter surrounding whitespace. + => SUCCESS +[32] Ampersand - Surrounding Whitespace + Interpolation should not alter surrounding whitespace. + => SUCCESS +[33] Interpolation - Standalone + Standalone interpolation should not alter surrounding whitespace. + => SUCCESS +[34] Triple Mustache - Standalone + Standalone interpolation should not alter surrounding whitespace. + => SUCCESS +[35] Ampersand - Standalone + Standalone interpolation should not alter surrounding whitespace. + => SUCCESS +[36] Interpolation With Padding + Superfluous in-tag whitespace should be ignored. + => SUCCESS +[37] Triple Mustache With Padding + Superfluous in-tag whitespace should be ignored. + => SUCCESS +[38] Ampersand With Padding + Superfluous in-tag whitespace should be ignored. + => SUCCESS + +loading test-specs/spec/specs/inverted.json +processing file test-specs/spec/specs/inverted.json +[0] Falsey + Falsey sections should have their contents rendered. + => SUCCESS +[1] Truthy + Truthy sections should have their contents omitted. + => SUCCESS +[2] Null is falsey + Null is falsey. + => SUCCESS +[3] Context + Objects and hashes should behave like truthy values. + => SUCCESS +[4] List + Lists should behave like truthy values. + => SUCCESS +[5] Empty List + Empty lists should behave like falsey values. + => SUCCESS +[6] Doubled + Multiple inverted sections per template should be permitted. + => SUCCESS +[7] Nested (Falsey) + Nested falsey sections should have their contents rendered. + => SUCCESS +[8] Nested (Truthy) + Nested truthy sections should be omitted. + => SUCCESS +[9] Context Misses + Failed context lookups should be considered falsey. + => SUCCESS +[10] Dotted Names - Truthy + Dotted names should be valid for Inverted Section tags. + => SUCCESS +[11] Dotted Names - Falsey + Dotted names should be valid for Inverted Section tags. + => SUCCESS +[12] Dotted Names - Broken Chains + Dotted names that cannot be resolved should be considered falsey. + => SUCCESS +[13] Surrounding Whitespace + Inverted sections should not alter surrounding whitespace. + => SUCCESS +[14] Internal Whitespace + Inverted should not alter internal whitespace. + => SUCCESS +[15] Indented Inline Sections + Single-line sections should not alter surrounding whitespace. + => SUCCESS +[16] Standalone Lines + Standalone lines should be removed from the template. + => SUCCESS +[17] Standalone Indented Lines + Standalone indented lines should be removed from the template. + => SUCCESS +[18] Standalone Line Endings + "\r\n" should be considered a newline for standalone tags. + => SUCCESS +[19] Standalone Without Previous Line + Standalone tags should not require a newline to precede them. + => SUCCESS +[20] Standalone Without Newline + Standalone tags should not require a newline to follow them. + => SUCCESS +[21] Padding + Superfluous in-tag whitespace should be ignored. + => SUCCESS + +loading test-specs/spec/specs/partials.json +processing file test-specs/spec/specs/partials.json +[0] Basic Behavior + The greater-than operator should expand to the named partial. + => SUCCESS +[1] Failed Lookup + The empty string should be used when the named partial is not found. + => SUCCESS +[2] Context + The greater-than operator should operate within the current context. + => SUCCESS +[3] Recursion + The greater-than operator should properly recurse. + => SUCCESS +[4] Nested + The greater-than operator should work from within partials. + => SUCCESS +[5] Surrounding Whitespace + The greater-than operator should not alter surrounding whitespace. + => SUCCESS +[6] Inline Indentation + Whitespace should be left untouched. + => SUCCESS +[7] Standalone Line Endings + "\r\n" should be considered a newline for standalone tags. + => SUCCESS +[8] Standalone Without Previous Line + Standalone tags should not require a newline to precede them. + => SUCCESS +[9] Standalone Without Newline + Standalone tags should not require a newline to follow them. + => SUCCESS +[10] Standalone Indentation + Each line of the partial should be indented before rendering. + => SUCCESS +[11] Padding Whitespace + Superfluous in-tag whitespace should be ignored. + => SUCCESS + +loading test-specs/spec/specs/sections.json +processing file test-specs/spec/specs/sections.json +[0] Truthy + Truthy sections should have their contents rendered. + => SUCCESS +[1] Falsey + Falsey sections should have their contents omitted. + => SUCCESS +[2] Null is falsey + Null is falsey. + => SUCCESS +[3] Context + Objects and hashes should be pushed onto the context stack. + => SUCCESS +[4] Parent contexts + Names missing in the current context are looked up in the stack. + => SUCCESS +[5] Variable test + Non-false sections have their value at the top of context, +accessible as {{.}} or through the parent context. This gives +a simple way to display content conditionally if a variable exists. + + => SUCCESS +[6] List Contexts + All elements on the context stack should be accessible within lists. + => SUCCESS +[7] Deeply Nested Contexts + All elements on the context stack should be accessible. + => SUCCESS +[8] List + Lists should be iterated; list items should visit the context stack. + => SUCCESS +[9] Empty List + Empty lists should behave like falsey values. + => SUCCESS +[10] Doubled + Multiple sections per template should be permitted. + => SUCCESS +[11] Nested (Truthy) + Nested truthy sections should have their contents rendered. + => SUCCESS +[12] Nested (Falsey) + Nested falsey sections should be omitted. + => SUCCESS +[13] Context Misses + Failed context lookups should be considered falsey. + => SUCCESS +[14] Implicit Iterator - String + Implicit iterators should directly interpolate strings. + => SUCCESS +[15] Implicit Iterator - Integer + Implicit iterators should cast integers to strings and interpolate. + => SUCCESS +[16] Implicit Iterator - Decimal + Implicit iterators should cast decimals to strings and interpolate. + => SUCCESS +[17] Implicit Iterator - Array + Implicit iterators should allow iterating over nested arrays. + => SUCCESS +[18] Implicit Iterator - HTML Escaping + Implicit iterators with basic interpolation should be HTML escaped. + => SUCCESS +[19] Implicit Iterator - Triple mustache + Implicit iterators in triple mustache should interpolate without HTML escaping. + => SUCCESS +[20] Implicit Iterator - Ampersand + Implicit iterators in an Ampersand tag should interpolate without HTML escaping. + => SUCCESS +[21] Implicit Iterator - Root-level + Implicit iterators should work on root-level lists. + => SUCCESS +[22] Dotted Names - Truthy + Dotted names should be valid for Section tags. + => SUCCESS +[23] Dotted Names - Falsey + Dotted names should be valid for Section tags. + => SUCCESS +[24] Dotted Names - Broken Chains + Dotted names that cannot be resolved should be considered falsey. + => SUCCESS +[25] Surrounding Whitespace + Sections should not alter surrounding whitespace. + => SUCCESS +[26] Internal Whitespace + Sections should not alter internal whitespace. + => SUCCESS +[27] Indented Inline Sections + Single-line sections should not alter surrounding whitespace. + => SUCCESS +[28] Standalone Lines + Standalone lines should be removed from the template. + => SUCCESS +[29] Indented Standalone Lines + Indented standalone lines should be removed from the template. + => SUCCESS +[30] Standalone Line Endings + "\r\n" should be considered a newline for standalone tags. + => SUCCESS +[31] Standalone Without Previous Line + Standalone tags should not require a newline to precede them. + => SUCCESS +[32] Standalone Without Newline + Standalone tags should not require a newline to follow them. + => SUCCESS +[33] Padding + Superfluous in-tag whitespace should be ignored. + => SUCCESS + +summary: + error 0 + differ 0 + success 133 diff --git a/src/templating/test-specs/test-specs.c b/src/templating/test-specs/test-specs.c new file mode 100644 index 000000000..15c94a80e --- /dev/null +++ b/src/templating/test-specs/test-specs.c @@ -0,0 +1,520 @@ +/* + Author: José Bollo <jobol@nonadev.net> + + https://gitlab.com/jobol/mustach + + SPDX-License-Identifier: ISC +*/ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include <stdlib.h> +#include <stdio.h> +#include <sys/stat.h> +#include <unistd.h> +#include <fcntl.h> +#include <string.h> +#include <errno.h> + +#include "mustach-wrap.h" + +#define TEST_JSON_C 1 +#define TEST_JANSSON 2 +#define TEST_CJSON 3 + +static const char *errors[] = { + "??? unreferenced ???", + "system", + "unexpected end", + "empty tag", + "tag too long", + "bad separators", + "too depth", + "closing", + "bad unescape tag", + "invalid interface", + "item not found", + "partial not found" +}; + +const char *mustach_error_string(int status) +{ + return status >= 0 ? "no error" + : errors[status <= -(int)(sizeof errors / sizeof * errors) ? 0 : -status]; +} + +static const char *errmsg = 0; +static int flags = 0; +static FILE *output = 0; + +static void help(char *prog) +{ + char *name = basename(prog); +#define STR(x) #x + printf("%s version %s\n", name, STR(VERSION)); +#undef STR + printf("usage: %s test-files...\n", name); + exit(0); +} + +#if TEST == TEST_CJSON + +static const size_t BLOCKSIZE = 8192; + +static char *readfile(const char *filename, size_t *length) +{ + int f; + struct stat s; + char *result; + size_t size, pos; + ssize_t rc; + + result = NULL; + if (filename[0] == '-' && filename[1] == 0) + f = dup(0); + else + f = open(filename, O_RDONLY); + if (f < 0) { + fprintf(stderr, "Can't open file: %s\n", filename); + exit(1); + } + + fstat(f, &s); + switch (s.st_mode & S_IFMT) { + case S_IFREG: + size = s.st_size; + break; + case S_IFSOCK: + case S_IFIFO: + size = BLOCKSIZE; + break; + default: + fprintf(stderr, "Bad file: %s\n", filename); + exit(1); + } + + pos = 0; + result = malloc(size + 1); + do { + if (result == NULL) { + fprintf(stderr, "Out of memory\n"); + exit(1); + } + rc = read(f, &result[pos], (size - pos) + 1); + if (rc < 0) { + fprintf(stderr, "Error while reading %s\n", filename); + exit(1); + } + if (rc > 0) { + pos += (size_t)rc; + if (pos > size) { + size = pos + BLOCKSIZE; + result = realloc(result, size + 1); + } + } + } while(rc > 0); + + close(f); + if (length != NULL) + *length = pos; + result[pos] = 0; + return result; +} +#endif + +typedef struct { + unsigned nerror; + unsigned ndiffers; + unsigned nsuccess; + unsigned ninvalid; +} counters; + +static int load_json(const char *filename); +static int process(counters *c); +static void close_json(); +static int get_partial(const char *name, struct mustach_sbuf *sbuf); + +int main(int ac, char **av) +{ + char *f; + char *prog = *av; + int s; + counters c; + + (void)ac; /* unused */ + flags = Mustach_With_SingleDot | Mustach_With_IncPartial; + output = stdout; + mustach_wrap_get_partial = get_partial; + + memset(&c, 0, sizeof c); + while (*++av) { + if (!strcmp(*av, "-h") || !strcmp(*av, "--help")) + help(prog); + f = (av[0][0] == '-' && !av[0][1]) ? "/dev/stdin" : av[0]; + fprintf(output, "\nloading %s\n", f); + s = load_json(f); + if (s < 0) { + fprintf(stderr, "error when loading %s!\n", f); + if(errmsg) + fprintf(stderr, " reason: %s\n", errmsg); + exit(1); + } + fprintf(output, "processing file %s\n", f); + s = process(&c); + if (s < 0) { + fprintf(stderr, "error bad test file %s!\n", f); + exit(1); + } + close_json(); + } + fprintf(output, "\nsummary:\n"); + if (c.ninvalid) + fprintf(output, " invalid %u\n", c.ninvalid); + fprintf(output, " error %u\n", c.nerror); + fprintf(output, " differ %u\n", c.ndiffers); + fprintf(output, " success %u\n", c.nsuccess); + if (c.nerror) + return 2; + if (c.ndiffers) + return 1; + return 0; +} + +void emit(FILE *f, const char *s) +{ + for(;;s++) { + switch(*s) { + case 0: return; + case '\\': fprintf(f, "\\\\"); break; + case '\t': fprintf(f, "\\t"); break; + case '\n': fprintf(f, "\\n"); break; + case '\r': fprintf(f, "\\r"); break; + default: fprintf(f, "%c", *s); break; + } + } +} + +#if TEST == TEST_JSON_C + +#include "mustach-json-c.h" + +static struct json_object *o; + +static struct json_object *partials; +static int get_partial(const char *name, struct mustach_sbuf *sbuf) +{ + struct json_object *x; + if (partials == NULL || !json_object_object_get_ex(partials, name, &x)) + return MUSTACH_ERROR_PARTIAL_NOT_FOUND; + sbuf->value = json_object_get_string(x); + return MUSTACH_OK; +} + +static int load_json(const char *filename) +{ + o = json_object_from_file(filename); +#if JSON_C_VERSION_NUM >= 0x000D00 + errmsg = json_util_get_last_err(); + if (errmsg != NULL) + return -1; +#endif + if (o == NULL) { + errmsg = "null json"; + return -1; + } + return 0; +} +static int process(counters *c) +{ + const char *t, *e; + char *got; + unsigned i, n; + size_t length; + int s; + json_object *tests, *unit, *name, *desc, *data, *template, *expected; + + if (!json_object_object_get_ex(o, "tests", &tests) || json_object_get_type(tests) != json_type_array) + return -1; + + i = 0; + n = (unsigned)json_object_array_length(tests); + while (i < n) { + unit = json_object_array_get_idx(tests, i); + if (json_object_get_type(unit) != json_type_object + || !json_object_object_get_ex(unit, "name", &name) + || !json_object_object_get_ex(unit, "desc", &desc) + || !json_object_object_get_ex(unit, "data", &data) + || !json_object_object_get_ex(unit, "template", &template) + || !json_object_object_get_ex(unit, "expected", &expected) + || json_object_get_type(name) != json_type_string + || json_object_get_type(desc) != json_type_string + || json_object_get_type(template) != json_type_string + || json_object_get_type(expected) != json_type_string) { + fprintf(stderr, "invalid test %u\n", i); + c->ninvalid++; + } + else { + fprintf(output, "[%u] %s\n", i, json_object_get_string(name)); + fprintf(output, "\t%s\n", json_object_get_string(desc)); + if (!json_object_object_get_ex(unit, "partials", &partials)) + partials = NULL; + t = json_object_get_string(template); + e = json_object_get_string(expected); + s = mustach_json_c_mem(t, 0, data, flags, &got, &length); + if (s == 0 && strcmp(got, e) == 0) { + fprintf(output, "\t=> SUCCESS\n"); + c->nsuccess++; + } + else { + if (s < 0) { + fprintf(output, "\t=> ERROR %s\n", mustach_error_string(s)); + c->nerror++; + } + else { + fprintf(output, "\t=> DIFFERS\n"); + c->ndiffers++; + } + if (partials) + fprintf(output, "\t.. PARTIALS[%s]\n", json_object_to_json_string_ext(partials, 0)); + fprintf(output, "\t.. DATA[%s]\n", json_object_to_json_string_ext(data, 0)); + fprintf(output, "\t.. TEMPLATE["); + emit(output, t); + fprintf(output, "]\n"); + fprintf(output, "\t.. EXPECTED["); + emit(output, e); + fprintf(output, "]\n"); + if (s == 0) { + fprintf(output, "\t.. GOT["); + emit(output, got); + fprintf(output, "]\n"); + } + } + free(got); + } + i++; + } + return 0; +} +static void close_json() +{ + json_object_put(o); +} + +#elif TEST == TEST_JANSSON + +#include "mustach-jansson.h" + +static json_t *o; +static json_error_t e; + +static json_t *partials; +static int get_partial(const char *name, struct mustach_sbuf *sbuf) +{ + json_t *x; + if (partials == NULL || !(x = json_object_get(partials, name))) + return MUSTACH_ERROR_PARTIAL_NOT_FOUND; + sbuf->value = json_string_value(x); + return MUSTACH_OK; +} + +static int load_json(const char *filename) +{ + o = json_load_file(filename, JSON_DECODE_ANY, &e); + if (o == NULL) { + errmsg = e.text; + return -1; + } + return 0; +} +static int process(counters *c) +{ + const char *t, *e; + char *got, *tmp; + int i, n; + size_t length; + int s; + json_t *tests, *unit, *name, *desc, *data, *template, *expected; + + tests = json_object_get(o, "tests"); + if (!tests || json_typeof(tests) != JSON_ARRAY) + return -1; + + i = 0; + n = json_array_size(tests); + while (i < n) { + unit = json_array_get(tests, i); + if (!unit || json_typeof(unit) != JSON_OBJECT + || !(name = json_object_get(unit, "name")) + || !(desc = json_object_get(unit, "desc")) + || !(data = json_object_get(unit, "data")) + || !(template = json_object_get(unit, "template")) + || !(expected = json_object_get(unit, "expected")) + || json_typeof(name) != JSON_STRING + || json_typeof(desc) != JSON_STRING + || json_typeof(template) != JSON_STRING + || json_typeof(expected) != JSON_STRING) { + fprintf(stderr, "invalid test %u\n", i); + c->ninvalid++; + } + else { + fprintf(output, "[%u] %s\n", i, json_string_value(name)); + fprintf(output, "\t%s\n", json_string_value(desc)); + partials = json_object_get(unit, "partials"); + t = json_string_value(template); + e = json_string_value(expected); + s = mustach_jansson_mem(t, 0, data, flags, &got, &length); + if (s == 0 && strcmp(got, e) == 0) { + fprintf(output, "\t=> SUCCESS\n"); + c->nsuccess++; + } + else { + if (s < 0) { + fprintf(output, "\t=> ERROR %s\n", mustach_error_string(s)); + c->nerror++; + } + else { + fprintf(output, "\t=> DIFFERS\n"); + c->ndiffers++; + } + if (partials) { + tmp = json_dumps(partials, JSON_ENCODE_ANY | JSON_COMPACT); + fprintf(output, "\t.. PARTIALS[%s]\n", tmp); + free(tmp); + } + tmp = json_dumps(data, JSON_ENCODE_ANY | JSON_COMPACT); + fprintf(output, "\t.. DATA[%s]\n", tmp); + free(tmp); + fprintf(output, "\t.. TEMPLATE["); + emit(output, t); + fprintf(output, "]\n"); + fprintf(output, "\t.. EXPECTED["); + emit(output, e); + fprintf(output, "]\n"); + if (s == 0) { + fprintf(output, "\t.. GOT["); + emit(output, got); + fprintf(output, "]\n"); + } + } + free(got); + } + i++; + } + return 0; +} +static void close_json() +{ + json_decref(o); +} + +#elif TEST == TEST_CJSON + +#include "mustach-cjson.h" + +static cJSON *o; +static cJSON *partials; +static int get_partial(const char *name, struct mustach_sbuf *sbuf) +{ + cJSON *x; + if (partials == NULL || !(x = cJSON_GetObjectItemCaseSensitive(partials, name))) + return MUSTACH_ERROR_PARTIAL_NOT_FOUND; + sbuf->value = x->valuestring; + return MUSTACH_OK; +} + +static int load_json(const char *filename) +{ + char *t; + size_t length; + + t = readfile(filename, &length); + o = t ? cJSON_ParseWithLength(t, length) : NULL; + free(t); + return -!o; +} +static int process(counters *c) +{ + const char *t, *e; + char *got, *tmp; + int i, n; + size_t length; + int s; + cJSON *tests, *unit, *name, *desc, *data, *template, *expected; + + tests = cJSON_GetObjectItemCaseSensitive(o, "tests"); + if (!tests || tests->type != cJSON_Array) + return -1; + + i = 0; + n = cJSON_GetArraySize(tests); + while (i < n) { + unit = cJSON_GetArrayItem(tests, i); + if (!unit || unit->type != cJSON_Object + || !(name = cJSON_GetObjectItemCaseSensitive(unit, "name")) + || !(desc = cJSON_GetObjectItemCaseSensitive(unit, "desc")) + || !(data = cJSON_GetObjectItemCaseSensitive(unit, "data")) + || !(template = cJSON_GetObjectItemCaseSensitive(unit, "template")) + || !(expected = cJSON_GetObjectItemCaseSensitive(unit, "expected")) + || name->type != cJSON_String + || desc->type != cJSON_String + || template->type != cJSON_String + || expected->type != cJSON_String) { + fprintf(stderr, "invalid test %u\n", i); + c->ninvalid++; + } + else { + fprintf(output, "[%u] %s\n", i, name->valuestring); + fprintf(output, "\t%s\n", desc->valuestring); + partials = cJSON_GetObjectItemCaseSensitive(unit, "partials"); + t = template->valuestring; + e = expected->valuestring; + s = mustach_cJSON_mem(t, 0, data, flags, &got, &length); + if (s == 0 && strcmp(got, e) == 0) { + fprintf(output, "\t=> SUCCESS\n"); + c->nsuccess++; + } + else { + if (s < 0) { + fprintf(output, "\t=> ERROR %s\n", mustach_error_string(s)); + c->nerror++; + } + else { + fprintf(output, "\t=> DIFFERS\n"); + c->ndiffers++; + } + if (partials) { + tmp = cJSON_PrintUnformatted(partials); + fprintf(output, "\t.. PARTIALS[%s]\n", tmp); + free(tmp); + } + tmp = cJSON_PrintUnformatted(data); + fprintf(output, "\t.. DATA[%s]\n", tmp); + free(tmp); + fprintf(output, "\t.. TEMPLATE["); + emit(output, t); + fprintf(output, "]\n"); + fprintf(output, "\t.. EXPECTED["); + emit(output, e); + fprintf(output, "]\n"); + if (s == 0) { + fprintf(output, "\t.. GOT["); + emit(output, got); + fprintf(output, "]\n"); + } + } + free(got); + } + i++; + } + return 0; +} +static void close_json() +{ + cJSON_Delete(o); +} + +#else +#error "no defined json library" +#endif diff --git a/src/templating/test1/.gitignore b/src/templating/test1/.gitignore new file mode 100644 index 000000000..4d897daa0 --- /dev/null +++ b/src/templating/test1/.gitignore @@ -0,0 +1,2 @@ +resu.last +vg.last diff --git a/src/templating/test1/Makefile b/src/templating/test1/Makefile new file mode 100644 index 000000000..1a3e57914 --- /dev/null +++ b/src/templating/test1/Makefile @@ -0,0 +1,8 @@ +.PHONY: test clean + +test: + @../dotest.sh json must + +clean: + rm -f resu.last vg.last + diff --git a/src/templating/test1/json b/src/templating/test1/json new file mode 100644 index 000000000..6562fb064 --- /dev/null +++ b/src/templating/test1/json @@ -0,0 +1,23 @@ +{ + "name": "Chris", + "value": 10000, + "taxed_value": 6000, + "in_ca": true, + "person": false, + "repo": [ + { "name": "resque", "who": [ { "commiter": "joe" }, { "reviewer": "avrel" }, { "commiter": "william" } ] }, + { "name": "hub", "who": [ { "commiter": "jack" }, { "reviewer": "avrel" }, { "commiter": "greg" } ] }, + { "name": "rip", "who": [ { "reviewer": "joe" }, { "reviewer": "jack" }, { "commiter": "greg" } ] } + ], + "person?": { "name": "Jon" }, + "special": "----{{extra}}----\n", + "extra": 3.14159, + "#sharp": "#", + "!bang": "!", + "/slash": "/", + "^circ": "^", + "=equal": "=", + ":colon": ":", + ">greater": ">", + "~tilde": "~" +} diff --git a/src/templating/test1/must b/src/templating/test1/must new file mode 100644 index 000000000..92d30b0b2 --- /dev/null +++ b/src/templating/test1/must @@ -0,0 +1,49 @@ +Hello {{name}} +You have just won {{value}} dollars! +{{#in_ca}} +Well, {{taxed_value}} dollars, after taxes. +{{/in_ca}} +Shown. +{{#person}} + Never shown! +{{/person}} +{{^person}} + No person +{{/person}} + +{{#repo}} + <b>{{name}}</b> reviewers:{{#who}} {{reviewer}}{{/who}} commiters:{{#who}} {{commiter}}{{/who}} +{{/repo}} + +{{#person?}} + Hi {{name}}! +{{/person?}} + +{{=%(% %)%=}} +===================================== +%(%! gros commentaire %)% +%(%#repo%)% + <b>%(%name%)%</b> reviewers:%(%#who%)% %(%reviewer%)%%(%/who%)% commiters:%(%#who%)% %(%commiter%)%%(%/who%)% +%(%/repo%)% +===================================== +%(%={{ }}=%)% +ggggggggg +{{> special}} +jjjjjjjjj +end + +{{:#sharp}} +{{:!bang}} +{{:~tilde}} +{{:/~0tilde}} +{{:/~1slash}} see json pointers IETF RFC 6901 +{{:^circ}} +{{:\=equal}} +{{::colon}} +{{:>greater}} + +{{#repo}} +who 0 {{who.0}} +who 1 {{who.1}} +who 2 {{who.2}} +{{/repo}} diff --git a/src/templating/test1/resu.ref b/src/templating/test1/resu.ref new file mode 100644 index 000000000..6cd11bb27 --- /dev/null +++ b/src/templating/test1/resu.ref @@ -0,0 +1,41 @@ +Hello Chris +You have just won 10000 dollars! +Well, 6000 dollars, after taxes. +Shown. + No person + + <b>resque</b> reviewers: avrel commiters: joe william + <b>hub</b> reviewers: avrel commiters: jack greg + <b>rip</b> reviewers: joe jack commiters: greg + + Hi Jon! + +===================================== + <b>resque</b> reviewers: avrel commiters: joe william + <b>hub</b> reviewers: avrel commiters: jack greg + <b>rip</b> reviewers: joe jack commiters: greg +===================================== +ggggggggg +----3.14159---- +jjjjjjjjj +end + +# +! +~ +~ +/ see json pointers IETF RFC 6901 +^ += +: +> + +who 0 {"commiter":"joe"} +who 1 {"reviewer":"avrel"} +who 2 {"commiter":"william"} +who 0 {"commiter":"jack"} +who 1 {"reviewer":"avrel"} +who 2 {"commiter":"greg"} +who 0 {"reviewer":"joe"} +who 1 {"reviewer":"jack"} +who 2 {"commiter":"greg"} diff --git a/src/templating/test1/vg.ref b/src/templating/test1/vg.ref new file mode 100644 index 000000000..d086e59c5 --- /dev/null +++ b/src/templating/test1/vg.ref @@ -0,0 +1,14 @@ +Memcheck, a memory error detector +Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al. +Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info +Command: ../mustach json must + + +HEAP SUMMARY: + in use at exit: 0 bytes in 0 blocks + total heap usage: 111 allocs, 111 frees, 9,702 bytes allocated + +All heap blocks were freed -- no leaks are possible + +For lists of detected and suppressed errors, rerun with: -s +ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) diff --git a/src/templating/test2/.gitignore b/src/templating/test2/.gitignore new file mode 100644 index 000000000..4d897daa0 --- /dev/null +++ b/src/templating/test2/.gitignore @@ -0,0 +1,2 @@ +resu.last +vg.last diff --git a/src/templating/test2/Makefile b/src/templating/test2/Makefile new file mode 100644 index 000000000..1a3e57914 --- /dev/null +++ b/src/templating/test2/Makefile @@ -0,0 +1,8 @@ +.PHONY: test clean + +test: + @../dotest.sh json must + +clean: + rm -f resu.last vg.last + diff --git a/src/templating/test2/json b/src/templating/test2/json new file mode 100644 index 000000000..8c668b3b1 --- /dev/null +++ b/src/templating/test2/json @@ -0,0 +1,9 @@ +{ + "header": "Colors", + "items": [ + {"name": "red", "first": true, "url": "#Red"}, + {"name": "green", "link": true, "url": "#Green"}, + {"name": "blue", "link": true, "url": "#Blue"} + ], + "empty": false +} diff --git a/src/templating/test2/must b/src/templating/test2/must new file mode 100644 index 000000000..aa6da7077 --- /dev/null +++ b/src/templating/test2/must @@ -0,0 +1,17 @@ +<h1>{{header}}</h1> +{{#bug}} +{{/bug}} + +{{#items}} + {{#first}} + <li><strong>{{name}}</strong></li> + {{/first}} + {{#link}} + <li><a href="{{url}}">{{name}}</a></li> + {{/link}} +{{/items}} + +{{#empty}} + <p>The list is empty.</p> +{{/empty}} + diff --git a/src/templating/test2/resu.ref b/src/templating/test2/resu.ref new file mode 100644 index 000000000..5a200a9bf --- /dev/null +++ b/src/templating/test2/resu.ref @@ -0,0 +1,7 @@ +<h1>Colors</h1> + + <li><strong>red</strong></li> + <li><a href="#Green">green</a></li> + <li><a href="#Blue">blue</a></li> + + diff --git a/src/templating/test2/vg.ref b/src/templating/test2/vg.ref new file mode 100644 index 000000000..e4b4f6d37 --- /dev/null +++ b/src/templating/test2/vg.ref @@ -0,0 +1,14 @@ +Memcheck, a memory error detector +Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al. +Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info +Command: ../mustach json must + + +HEAP SUMMARY: + in use at exit: 0 bytes in 0 blocks + total heap usage: 38 allocs, 38 frees, 5,712 bytes allocated + +All heap blocks were freed -- no leaks are possible + +For lists of detected and suppressed errors, rerun with: -s +ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) diff --git a/src/templating/test3/.gitignore b/src/templating/test3/.gitignore new file mode 100644 index 000000000..4d897daa0 --- /dev/null +++ b/src/templating/test3/.gitignore @@ -0,0 +1,2 @@ +resu.last +vg.last diff --git a/src/templating/test3/Makefile b/src/templating/test3/Makefile new file mode 100644 index 000000000..1a3e57914 --- /dev/null +++ b/src/templating/test3/Makefile @@ -0,0 +1,8 @@ +.PHONY: test clean + +test: + @../dotest.sh json must + +clean: + rm -f resu.last vg.last + diff --git a/src/templating/test3/json b/src/templating/test3/json new file mode 100644 index 000000000..792788171 --- /dev/null +++ b/src/templating/test3/json @@ -0,0 +1,7 @@ +{ + "name": "Chris", + "company": "<b>GitHub & Co</b>", + "names": ["Chris", "Kross"], + "skills": ["JavaScript", "PHP", "Java"], + "age": 18 +} diff --git a/src/templating/test3/must b/src/templating/test3/must new file mode 100644 index 000000000..5c490469b --- /dev/null +++ b/src/templating/test3/must @@ -0,0 +1,15 @@ +* {{name}} +* {{age}} +* {{company}} +* {{&company}} +* {{{company}}} +{{=<% %>=}} +* <%company%> +* <%&company%> +* <%{company}%> + +<%={{ }}=%> +* <ul>{{#names}}<li>{{.}}</li>{{/names}}</ul> +* skills: <ul>{{#skills}}<li>{{.}}</li>{{/skills}}</ul> +{{#age}}* age: {{.}}{{/age}} + diff --git a/src/templating/test3/resu.ref b/src/templating/test3/resu.ref new file mode 100644 index 000000000..ee6dad3fb --- /dev/null +++ b/src/templating/test3/resu.ref @@ -0,0 +1,13 @@ +* Chris +* 18 +* <b>GitHub & Co</b> +* <b>GitHub & Co</b> +* <b>GitHub & Co</b> +* <b>GitHub & Co</b> +* <b>GitHub & Co</b> +* <b>GitHub & Co</b> + +* <ul><li>Chris</li><li>Kross</li></ul> +* skills: <ul><li>JavaScript</li><li>PHP</li><li>Java</li></ul> +* age: 18 + diff --git a/src/templating/test3/vg.ref b/src/templating/test3/vg.ref new file mode 100644 index 000000000..21f7931eb --- /dev/null +++ b/src/templating/test3/vg.ref @@ -0,0 +1,14 @@ +Memcheck, a memory error detector +Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al. +Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info +Command: ../mustach json must + + +HEAP SUMMARY: + in use at exit: 0 bytes in 0 blocks + total heap usage: 30 allocs, 30 frees, 5,831 bytes allocated + +All heap blocks were freed -- no leaks are possible + +For lists of detected and suppressed errors, rerun with: -s +ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) diff --git a/src/templating/test4/.gitignore b/src/templating/test4/.gitignore new file mode 100644 index 000000000..4d897daa0 --- /dev/null +++ b/src/templating/test4/.gitignore @@ -0,0 +1,2 @@ +resu.last +vg.last diff --git a/src/templating/test4/Makefile b/src/templating/test4/Makefile new file mode 100644 index 000000000..1a3e57914 --- /dev/null +++ b/src/templating/test4/Makefile @@ -0,0 +1,8 @@ +.PHONY: test clean + +test: + @../dotest.sh json must + +clean: + rm -f resu.last vg.last + diff --git a/src/templating/test4/json b/src/templating/test4/json new file mode 100644 index 000000000..a10836072 --- /dev/null +++ b/src/templating/test4/json @@ -0,0 +1,13 @@ +{ + "person": { "name": "Jon", "age": 25 }, + "person.name": "Fred", + "person.name=Fred": "The other Fred.", + "persons": [ + { "name": "Jon", "age": 25, "lang": "en" }, + { "name": "Henry", "age": 27, "lang": "en" }, + { "name": "Amed", "age": 24, "lang": "fr" } ], + "fellows": { + "Jon": { "age": 25, "lang": "en" }, + "Henry": { "age": 27, "lang": "en" }, + "Amed": { "age": 24, "lang": "fr" } } +} diff --git a/src/templating/test4/must b/src/templating/test4/must new file mode 100644 index 000000000..003b93666 --- /dev/null +++ b/src/templating/test4/must @@ -0,0 +1,58 @@ +This are extensions!! + +{{person.name}} +{{person.age}} + +{{person\.name}} +{{person\.name\=Fred}} + +{{#person.name=Jon}} +Hello Jon +{{/person.name=Jon}} + +{{^person.name=Jon}} +No Jon? Hey Jon... +{{/person.name=Jon}} + +{{^person.name=Harry}} +No Harry? Hey Calahan... +{{/person.name=Harry}} + +{{#person\.name=Fred}} +Hello Fred +{{/person\.name=Fred}} + +{{^person\.name=Fred}} +No Fred? Hey Fred... +{{/person\.name=Fred}} + +{{#person\.name\=Fred=The other Fred.}} +Hello Fred#2 +{{/person\.name\=Fred=The other Fred.}} + +{{^person\.name\=Fred=The other Fred.}} +No Fred#2? Hey Fred#2... +{{/person\.name\=Fred=The other Fred.}} + +{{#persons}} +{{#lang=!fr}}Hello {{name}}, {{age}} years{{/lang=!fr}} +{{#lang=fr}}Salut {{name}}, {{age}} ans{{/lang=fr}} +{{/persons}} + +{{#persons}} +{{name}}: {{age=24}}/{{age}}/{{age=!27}} +{{/persons}} + +{{#fellows.*}} +{{*}}: {{age=24}}/{{age}}/{{age=!27}} +{{/fellows.*}} + +{{#*}} + (1) {{*}}: {{.}} + {{#*}} + (2) {{*}}: {{.}} + {{#*}} + (3) {{*}}: {{.}} + {{/*}} + {{/*}} +{{/*}} diff --git a/src/templating/test4/resu.ref b/src/templating/test4/resu.ref new file mode 100644 index 000000000..8a71c4e82 --- /dev/null +++ b/src/templating/test4/resu.ref @@ -0,0 +1,50 @@ +This are extensions!! + +Jon +25 + +Fred +The other Fred. + +Hello Jon + + +No Harry? Hey Calahan... + +Hello Fred + + +Hello Fred#2 + + +Hello Jon, 25 years + +Hello Henry, 27 years + + +Salut Amed, 24 ans + +Jon: /25/25 +Henry: /27/ +Amed: 24/24/24 + +Jon: /25/25 +Henry: /27/ +Amed: 24/24/24 + + (1) person: {"name":"Jon","age":25} + (2) name: Jon + (2) age: 25 + (1) person.name: Fred + (1) person.name=Fred: The other Fred. + (1) persons: [{"name":"Jon","age":25,"lang":"en"},{"name":"Henry","age":27,"lang":"en"},{"name":"Amed","age":24,"lang":"fr"}] + (1) fellows: {"Jon":{"age":25,"lang":"en"},"Henry":{"age":27,"lang":"en"},"Amed":{"age":24,"lang":"fr"}} + (2) Jon: {"age":25,"lang":"en"} + (3) age: 25 + (3) lang: en + (2) Henry: {"age":27,"lang":"en"} + (3) age: 27 + (3) lang: en + (2) Amed: {"age":24,"lang":"fr"} + (3) age: 24 + (3) lang: fr diff --git a/src/templating/test4/vg.ref b/src/templating/test4/vg.ref new file mode 100644 index 000000000..922b0676d --- /dev/null +++ b/src/templating/test4/vg.ref @@ -0,0 +1,14 @@ +Memcheck, a memory error detector +Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al. +Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info +Command: ../mustach json must + + +HEAP SUMMARY: + in use at exit: 0 bytes in 0 blocks + total heap usage: 121 allocs, 121 frees, 14,608 bytes allocated + +All heap blocks were freed -- no leaks are possible + +For lists of detected and suppressed errors, rerun with: -s +ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) diff --git a/src/templating/test5/.gitignore b/src/templating/test5/.gitignore new file mode 100644 index 000000000..4d897daa0 --- /dev/null +++ b/src/templating/test5/.gitignore @@ -0,0 +1,2 @@ +resu.last +vg.last diff --git a/src/templating/test5/Makefile b/src/templating/test5/Makefile new file mode 100644 index 000000000..1a3e57914 --- /dev/null +++ b/src/templating/test5/Makefile @@ -0,0 +1,8 @@ +.PHONY: test clean + +test: + @../dotest.sh json must + +clean: + rm -f resu.last vg.last + diff --git a/src/templating/test5/json b/src/templating/test5/json new file mode 100644 index 000000000..6562fb064 --- /dev/null +++ b/src/templating/test5/json @@ -0,0 +1,23 @@ +{ + "name": "Chris", + "value": 10000, + "taxed_value": 6000, + "in_ca": true, + "person": false, + "repo": [ + { "name": "resque", "who": [ { "commiter": "joe" }, { "reviewer": "avrel" }, { "commiter": "william" } ] }, + { "name": "hub", "who": [ { "commiter": "jack" }, { "reviewer": "avrel" }, { "commiter": "greg" } ] }, + { "name": "rip", "who": [ { "reviewer": "joe" }, { "reviewer": "jack" }, { "commiter": "greg" } ] } + ], + "person?": { "name": "Jon" }, + "special": "----{{extra}}----\n", + "extra": 3.14159, + "#sharp": "#", + "!bang": "!", + "/slash": "/", + "^circ": "^", + "=equal": "=", + ":colon": ":", + ">greater": ">", + "~tilde": "~" +} diff --git a/src/templating/test5/must b/src/templating/test5/must new file mode 100644 index 000000000..44305df24 --- /dev/null +++ b/src/templating/test5/must @@ -0,0 +1,23 @@ +===================================== +from json +{{> special}} +===================================== +not found +{{> notfound}} +===================================== +without extension first +{{> must2 }} +===================================== +last with extension +{{> must3 }} +===================================== +Ensure must3 didn't change specials + +{{#person?}} + Hi {{name}}! +{{/person?}} + +%(%#person?%)% + Hi %(%name%)%! +%(%/person?%)% + diff --git a/src/templating/test5/must2 b/src/templating/test5/must2 new file mode 100644 index 000000000..d4a1d3783 --- /dev/null +++ b/src/templating/test5/must2 @@ -0,0 +1,14 @@ +must2 == BEGIN +Hello {{name}} +You have just won {{value}} dollars! +{{#in_ca}} +Well, {{taxed_value}} dollars, after taxes. +{{/in_ca}} +Shown. +{{#person}} + Never shown! +{{/person}} +{{^person}} + No person +{{/person}} +must2 == END diff --git a/src/templating/test5/must2.mustache b/src/templating/test5/must2.mustache new file mode 100644 index 000000000..33f1ead38 --- /dev/null +++ b/src/templating/test5/must2.mustache @@ -0,0 +1 @@ +must2.mustache ==SHOULD NOT BE SEEN== diff --git a/src/templating/test5/must3.mustache b/src/templating/test5/must3.mustache new file mode 100644 index 000000000..67eddb1ef --- /dev/null +++ b/src/templating/test5/must3.mustache @@ -0,0 +1,17 @@ +must3.mustache == BEGIN +{{#repo}} + <b>{{name}}</b> reviewers:{{#who}} {{reviewer}}{{/who}} commiters:{{#who}} {{commiter}}{{/who}} +{{/repo}} + +{{#person?}} + Hi {{name}}! +{{/person?}} + +{{=%(% %)%=}} +===================================== +%(%! big comment %)% +%(%#repo%)% + <b>%(%name%)%</b> reviewers:%(%#who%)% %(%reviewer%)%%(%/who%)% commiters:%(%#who%)% %(%commiter%)%%(%/who%)% +%(%/repo%)% +===================================== +must3.mustache == END diff --git a/src/templating/test5/resu.ref b/src/templating/test5/resu.ref new file mode 100644 index 000000000..f2a608568 --- /dev/null +++ b/src/templating/test5/resu.ref @@ -0,0 +1,38 @@ +===================================== +from json +----3.14159---- +===================================== +not found +===================================== +without extension first +must2 == BEGIN +Hello Chris +You have just won 10000 dollars! +Well, 6000 dollars, after taxes. +Shown. + No person +must2 == END +===================================== +last with extension +must3.mustache == BEGIN + <b>resque</b> reviewers: avrel commiters: joe william + <b>hub</b> reviewers: avrel commiters: jack greg + <b>rip</b> reviewers: joe jack commiters: greg + + Hi Jon! + +===================================== + <b>resque</b> reviewers: avrel commiters: joe william + <b>hub</b> reviewers: avrel commiters: jack greg + <b>rip</b> reviewers: joe jack commiters: greg +===================================== +must3.mustache == END +===================================== +Ensure must3 didn't change specials + + Hi Jon! + +%(%#person?%)% + Hi %(%name%)%! +%(%/person?%)% + diff --git a/src/templating/test5/vg.ref b/src/templating/test5/vg.ref new file mode 100644 index 000000000..89dc21bcb --- /dev/null +++ b/src/templating/test5/vg.ref @@ -0,0 +1,14 @@ +Memcheck, a memory error detector +Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al. +Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info +Command: ../mustach json must + + +HEAP SUMMARY: + in use at exit: 0 bytes in 0 blocks + total heap usage: 123 allocs, 123 frees, 20,610 bytes allocated + +All heap blocks were freed -- no leaks are possible + +For lists of detected and suppressed errors, rerun with: -s +ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) diff --git a/src/templating/test6/.gitignore b/src/templating/test6/.gitignore new file mode 100644 index 000000000..15e6dd5a5 --- /dev/null +++ b/src/templating/test6/.gitignore @@ -0,0 +1,3 @@ +resu.last +vg.last +test-custom-write diff --git a/src/templating/test6/Makefile b/src/templating/test6/Makefile new file mode 100644 index 000000000..ea4f86e79 --- /dev/null +++ b/src/templating/test6/Makefile @@ -0,0 +1,12 @@ +.PHONY: test clean + +test-custom-write: test-custom-write.c ../mustach-json-c.h ../mustach-json-c.c ../mustach-wrap.c ../mustach.h ../mustach.c + @echo building test-custom-write + $(CC) $(CFLAGS) $(LDFLAGS) -g -o test-custom-write test-custom-write.c ../mustach.c ../mustach-json-c.c ../mustach-wrap.c -ljson-c + +test: test-custom-write + @mustach=./test-custom-write ../dotest.sh json -U must -l must -x must + +clean: + rm -f resu.last vg.last test-custom-write + diff --git a/src/templating/test6/json b/src/templating/test6/json new file mode 100644 index 000000000..6562fb064 --- /dev/null +++ b/src/templating/test6/json @@ -0,0 +1,23 @@ +{ + "name": "Chris", + "value": 10000, + "taxed_value": 6000, + "in_ca": true, + "person": false, + "repo": [ + { "name": "resque", "who": [ { "commiter": "joe" }, { "reviewer": "avrel" }, { "commiter": "william" } ] }, + { "name": "hub", "who": [ { "commiter": "jack" }, { "reviewer": "avrel" }, { "commiter": "greg" } ] }, + { "name": "rip", "who": [ { "reviewer": "joe" }, { "reviewer": "jack" }, { "commiter": "greg" } ] } + ], + "person?": { "name": "Jon" }, + "special": "----{{extra}}----\n", + "extra": 3.14159, + "#sharp": "#", + "!bang": "!", + "/slash": "/", + "^circ": "^", + "=equal": "=", + ":colon": ":", + ">greater": ">", + "~tilde": "~" +} diff --git a/src/templating/test6/must b/src/templating/test6/must new file mode 100644 index 000000000..6df523669 --- /dev/null +++ b/src/templating/test6/must @@ -0,0 +1,43 @@ +Hello {{name}} +You have just won {{value}} dollars! +{{#in_ca}} +Well, {{taxed_value}} dollars, after taxes. +{{/in_ca}} +Shown. +{{#person}} + Never shown! +{{/person}} +{{^person}} + No person +{{/person}} + +{{#repo}} + <b>{{name}}</b> reviewers:{{#who}} {{reviewer}}{{/who}} commiters:{{#who}} {{commiter}}{{/who}} +{{/repo}} + +{{#person?}} + Hi {{name}}! +{{/person?}} + +{{=%(% %)%=}} +===================================== +%(%! gros commentaire %)% +%(%#repo%)% + <b>%(%name%)%</b> reviewers:%(%#who%)% %(%reviewer%)%%(%/who%)% commiters:%(%#who%)% %(%commiter%)%%(%/who%)% +%(%/repo%)% +===================================== +%(%={{ }}=%)% +ggggggggg +{{> special}} +jjjjjjjjj +end + +{{:#sharp}} +{{:!bang}} +{{:~tilde}} +{{:/~0tilde}} +{{:/~1slash}} see json pointers IETF RFC 6901 +{{:^circ}} +{{:\=equal}} +{{::colon}} +{{:>greater}} diff --git a/src/templating/test6/resu.ref b/src/templating/test6/resu.ref new file mode 100644 index 000000000..377eb11af --- /dev/null +++ b/src/templating/test6/resu.ref @@ -0,0 +1,93 @@ +HELLO CHRIS +YOU HAVE JUST WON 10000 DOLLARS! +WELL, 6000 DOLLARS, AFTER TAXES. +SHOWN. + NO PERSON + + <B>RESQUE</B> REVIEWERS: AVREL COMMITERS: JOE WILLIAM + <B>HUB</B> REVIEWERS: AVREL COMMITERS: JACK GREG + <B>RIP</B> REVIEWERS: JOE JACK COMMITERS: GREG + + HI JON! + +===================================== + <B>RESQUE</B> REVIEWERS: AVREL COMMITERS: JOE WILLIAM + <B>HUB</B> REVIEWERS: AVREL COMMITERS: JACK GREG + <B>RIP</B> REVIEWERS: JOE JACK COMMITERS: GREG +===================================== +GGGGGGGGG +----3.14159---- +JJJJJJJJJ +END + +# +! +~ +~ +/ SEE JSON POINTERS IETF RFC 6901 +^ += +: +> +hello chris +you have just won 10000 dollars! +well, 6000 dollars, after taxes. +shown. + no person + + <b>resque</b> reviewers: avrel commiters: joe william + <b>hub</b> reviewers: avrel commiters: jack greg + <b>rip</b> reviewers: joe jack commiters: greg + + hi jon! + +===================================== + <b>resque</b> reviewers: avrel commiters: joe william + <b>hub</b> reviewers: avrel commiters: jack greg + <b>rip</b> reviewers: joe jack commiters: greg +===================================== +ggggggggg +----3.14159---- +jjjjjjjjj +end + +# +! +~ +~ +/ see json pointers ietf rfc 6901 +^ += +: +> +Hello Chris +You have just won 10000 dollars! +Well, 6000 dollars, after taxes. +Shown. + No person + + <b>resque</b> reviewers: avrel commiters: joe william + <b>hub</b> reviewers: avrel commiters: jack greg + <b>rip</b> reviewers: joe jack commiters: greg + + Hi Jon! + +===================================== + <b>resque</b> reviewers: avrel commiters: joe william + <b>hub</b> reviewers: avrel commiters: jack greg + <b>rip</b> reviewers: joe jack commiters: greg +===================================== +ggggggggg +----3.14159---- +jjjjjjjjj +end + +# +! +~ +~ +/ see json pointers IETF RFC 6901 +^ += +: +> diff --git a/src/templating/test6/test-custom-write.c b/src/templating/test6/test-custom-write.c new file mode 100644 index 000000000..20042c1ed --- /dev/null +++ b/src/templating/test6/test-custom-write.c @@ -0,0 +1,149 @@ +/* + Author: José Bollo <jobol@nonadev.net> + + https://gitlab.com/jobol/mustach + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include <stdlib.h> +#include <stdio.h> +#include <ctype.h> +#include <sys/stat.h> +#include <unistd.h> +#include <fcntl.h> +#include <string.h> +#include <libgen.h> + +#include "../mustach-json-c.h" + +static const size_t BLOCKSIZE = 8192; + +static char *readfile(const char *filename) +{ + int f; + struct stat s; + char *result, *ptr; + size_t size, pos; + ssize_t rc; + + result = NULL; + if (filename[0] == '-' && filename[1] == 0) + f = dup(0); + else + f = open(filename, O_RDONLY); + if (f < 0) { + fprintf(stderr, "Can't open file: %s\n", filename); + exit(1); + } + + fstat(f, &s); + switch (s.st_mode & S_IFMT) { + case S_IFREG: + size = s.st_size; + break; + case S_IFSOCK: + case S_IFIFO: + size = BLOCKSIZE; + break; + default: + fprintf(stderr, "Bad file: %s\n", filename); + exit(1); + } + + pos = 0; + result = malloc(size + 1); + do { + if (result == NULL) { + fprintf(stderr, "Out of memory\n"); + exit(1); + } + rc = read(f, &result[pos], (size - pos) + 1); + if (rc < 0) { + fprintf(stderr, "Error while reading %s\n", filename); + exit(1); + } + if (rc > 0) { + pos += (size_t)rc; + if (pos > size) { + size = pos + BLOCKSIZE; + ptr = realloc(result, size + 1); + if (!ptr) + free(result); + result = ptr; + } + } + } while(rc > 0); + + close(f); + result[pos] = 0; + return result; +} + +enum { None, Upper, Lower } mode = None; + +int uwrite(void *closure, const char *buffer, size_t size) +{ + switch(mode) { + case None: + fwrite(buffer, size, 1, stdout); + break; + case Upper: + while(size--) + fputc(toupper(*buffer++), stdout); + break; + case Lower: + while(size--) + fputc(tolower(*buffer++), stdout); + break; + } + return 0; +} + +int main(int ac, char **av) +{ + struct json_object *o; + char *t; + char *prog = *av; + int s; + + if (*++av) { + o = json_object_from_file(av[0]); + if (o == NULL) { + fprintf(stderr, "Aborted: null json (file %s)\n", av[0]); + exit(1); + } + while(*++av) { + if (!strcmp(*av, "-U")) + mode = Upper; + else if (!strcmp(*av, "-l")) + mode = Lower; + else if (!strcmp(*av, "-x")) + mode = None; + else { + t = readfile(*av); + s = mustach_json_c_write(t, 0, o, Mustach_With_AllExtensions, uwrite, NULL); + if (s != 0) + fprintf(stderr, "Template error %d\n", s); + free(t); + } + } + json_object_put(o); + } + return 0; +} + diff --git a/src/templating/test6/vg.ref b/src/templating/test6/vg.ref new file mode 100644 index 000000000..fb0e31bd8 --- /dev/null +++ b/src/templating/test6/vg.ref @@ -0,0 +1,14 @@ +Memcheck, a memory error detector +Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al. +Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info +Command: ./test-custom-write json -U must -l must -x must + + +HEAP SUMMARY: + in use at exit: 0 bytes in 0 blocks + total heap usage: 174 allocs, 174 frees, 24,250 bytes allocated + +All heap blocks were freed -- no leaks are possible + +For lists of detected and suppressed errors, rerun with: -s +ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) diff --git a/src/templating/test7/Makefile b/src/templating/test7/Makefile new file mode 100644 index 000000000..8e3a3b990 --- /dev/null +++ b/src/templating/test7/Makefile @@ -0,0 +1,8 @@ +.PHONY: test clean + +test: + @../dotest.sh json base.mustache + +clean: + rm -f resu.last vg.last + diff --git a/src/templating/test7/base.mustache b/src/templating/test7/base.mustache new file mode 100644 index 000000000..f701e0c65 --- /dev/null +++ b/src/templating/test7/base.mustache @@ -0,0 +1,2 @@ +family: +{{> node}} diff --git a/src/templating/test7/json b/src/templating/test7/json new file mode 100644 index 000000000..c9ee47150 --- /dev/null +++ b/src/templating/test7/json @@ -0,0 +1,8 @@ +{ "data": "grandparent", "children": [ + { "data": "parent", "children": [ + { "data": "child", "children": [] } + ]}, + { "data": "parent2", "children": [ + { "data": "child2", "children": [ { "data": "pet", "children": false } ]} + ]} +]} diff --git a/src/templating/test7/node.mustache b/src/templating/test7/node.mustache new file mode 100644 index 000000000..4154b12ba --- /dev/null +++ b/src/templating/test7/node.mustache @@ -0,0 +1,4 @@ +<{{data}}> +{{#children}} + {{> node}} +{{/children}} diff --git a/src/templating/test7/resu.ref b/src/templating/test7/resu.ref new file mode 100644 index 000000000..d02b24e11 --- /dev/null +++ b/src/templating/test7/resu.ref @@ -0,0 +1,7 @@ +family: +<grandparent> + <parent> + <child> + <parent2> + <child2> + <pet> diff --git a/src/templating/test7/vg.ref b/src/templating/test7/vg.ref new file mode 100644 index 000000000..032e6c447 --- /dev/null +++ b/src/templating/test7/vg.ref @@ -0,0 +1,14 @@ +Memcheck, a memory error detector +Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al. +Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info +Command: ../mustach json base.mustache + + +HEAP SUMMARY: + in use at exit: 0 bytes in 0 blocks + total heap usage: 69 allocs, 69 frees, 36,313 bytes allocated + +All heap blocks were freed -- no leaks are possible + +For lists of detected and suppressed errors, rerun with: -s +ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) diff --git a/src/templating/test8/.gitignore b/src/templating/test8/.gitignore new file mode 100644 index 000000000..4d897daa0 --- /dev/null +++ b/src/templating/test8/.gitignore @@ -0,0 +1,2 @@ +resu.last +vg.last diff --git a/src/templating/test8/Makefile b/src/templating/test8/Makefile new file mode 100644 index 000000000..1a3e57914 --- /dev/null +++ b/src/templating/test8/Makefile @@ -0,0 +1,8 @@ +.PHONY: test clean + +test: + @../dotest.sh json must + +clean: + rm -f resu.last vg.last + diff --git a/src/templating/test8/json b/src/templating/test8/json new file mode 100644 index 000000000..04a1e4aaa --- /dev/null +++ b/src/templating/test8/json @@ -0,0 +1,8 @@ +{ +"val1": "", +"val2": 0, +"val3": false, +"val4": null, +"val5": [], +"val6": 0.0 +} diff --git a/src/templating/test8/must b/src/templating/test8/must new file mode 100644 index 000000000..a22374438 --- /dev/null +++ b/src/templating/test8/must @@ -0,0 +1,6 @@ +x{{#val1}} {{.}} {{/val1}}x +x{{#val2}} {{.}} {{/val2}}x +x{{#val3}} {{.}} {{/val3}}x +x{{#val4}} {{.}} {{/val4}}x +x{{#val5}} {{.}} {{/val5}}x +x{{#val6}} {{.}} {{/val6}}x diff --git a/src/templating/test8/resu.ref b/src/templating/test8/resu.ref new file mode 100644 index 000000000..e0c16e49a --- /dev/null +++ b/src/templating/test8/resu.ref @@ -0,0 +1,6 @@ +xx +xx +xx +xx +xx +xx diff --git a/src/templating/test8/vg.ref b/src/templating/test8/vg.ref new file mode 100644 index 000000000..b5b0235f9 --- /dev/null +++ b/src/templating/test8/vg.ref @@ -0,0 +1,14 @@ +Memcheck, a memory error detector +Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al. +Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info +Command: ../mustach json must + + +HEAP SUMMARY: + in use at exit: 0 bytes in 0 blocks + total heap usage: 17 allocs, 17 frees, 4,832 bytes allocated + +All heap blocks were freed -- no leaks are possible + +For lists of detected and suppressed errors, rerun with: -s +ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) diff --git a/src/templating/test_mustach_jansson.c b/src/templating/test_mustach_jansson.c new file mode 100644 index 000000000..beb155f6d --- /dev/null +++ b/src/templating/test_mustach_jansson.c @@ -0,0 +1,125 @@ +/* + This file is part of TALER + Copyright (C) 2014-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 3, or + (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ + +/** + * @file test_mustach_jansson.c + * @brief testcase to test the mustach/jansson integration + * @author Florian Dold + */ +#include "platform.h" +#include "mustach-jansson.h" +#include <gnunet/gnunet_util_lib.h> + +static void +assert_template (const char *template, + json_t *root, + const char *expected) +{ + char *r; + size_t sz; + + GNUNET_assert (0 == mustach_jansson_mem (template, + 0, + root, + Mustach_With_AllExtensions, + &r, + &sz)); + GNUNET_assert (0 == strcmp (r, + expected)); + GNUNET_free (r); +} + + +int +main (int argc, + char *const *argv) +{ + json_t *root = json_object (); + json_t *arr = json_array (); + json_t *obj = json_object (); + /* test 1 */ + const char *t1 = "hello world"; + const char *x1 = "hello world"; + /* test 2 */ + const char *t2 = "hello {{ v1 }}"; + const char *x2 = "hello world"; + /* test 3 */ + const char *t3 = "hello {{ v3.x }}"; + const char *x3 = "hello baz"; + /* test 4 */ + const char *t4 = "hello {{# v2 }}{{ . }}{{/ v2 }}"; + const char *x4 = "hello foobar"; + /* test 5 */ + const char *t5 = "hello {{# v3 }}{{ y }}/{{ x }}{{ z }}{{/ v3 }}"; + const char *x5 = "hello quux/baz"; + /* test 8 */ + const char *t8 = "{{^ v4 }}fallback{{/ v4 }}"; + const char *x8 = "fallback"; + + (void) argc; + (void) argv; + GNUNET_log_setup ("test-mustach-jansson", + "INFO", + NULL); + GNUNET_assert (NULL != root); + GNUNET_assert (NULL != arr); + GNUNET_assert (NULL != obj); + GNUNET_assert (0 == + json_object_set_new (root, + "v1", + json_string ("world"))); + GNUNET_assert (0 == + json_object_set_new (root, + "v4", + json_array ())); + GNUNET_assert (0 == + json_array_append_new (arr, + json_string ("foo"))); + GNUNET_assert (0 == + json_array_append_new (arr, + json_string ("bar"))); + GNUNET_assert (0 == + json_object_set_new (root, + "v2", + arr)); + GNUNET_assert (0 == + json_object_set_new (root, + "v3", + obj)); + GNUNET_assert (0 == + json_object_set_new (root, + "amt", + json_string ("EUR:123.00"))); + GNUNET_assert (0 == + json_object_set_new (obj, + "x", + json_string ("baz"))); + GNUNET_assert (0 == + json_object_set_new (obj, + "y", + json_string ("quux"))); + assert_template (t1, root, x1); + assert_template (t2, root, x2); + assert_template (t3, root, x3); + assert_template (t4, root, x4); + assert_template (t5, root, x5); + assert_template (t8, root, x8); + json_decref (root); + return 0; +} |