Type safe "Keep it simple, stupid" text templates for C++. If you are familiar with the idea of text templates and with C++, you can learn how to use it in just a few minutes.
Branch / Compiler | clang-3.4, gcc-4.8 | VS14 2015 |
---|---|---|
Master | ||
Develop |
Use kiste2cpp to turn text templates into type- and name-safe C++ code. Use this code to generate text from your data and the serializer of choice.
Template are a mix of
- kiss template commands (there are VERY few)
- C++
- text
%namespace test
%{
$class Hello
%auto render() -> void
%{
Hello ${data.name}!
%}
$endclass
%}
I bet you can guess what this all means (and it is documented below), so let's compile this into a C++ header file:
kiste2cpp hello_world.kiste > hello_world.h
And now we use it in our C++ project like this
#include <iostream>
#include <hello_world.h>
#include <kiste/raw.h>
struct Data
{
std::string name;
};
int main()
{
const auto data = Data{"World"};
auto& os = std::cout;
auto serializer = kiste::raw{os};
auto hello = test::Hello(data, serializer);
hello.render();
}
Compile and run:
$ ./examples/0_hello_world/hello_world
Hello World!
Yeah!
%<whatever>
C++ code$class <name>
starts a template class$class <name> : <base>
starts a template class, which inherits from a base class$member <class> <name>
adds a template as a member$endclass
ends a template class${<expression>}
send expression to serializer (which takes care of encoding, quoting, escaping, etc)$raw{<expression>}
send expression to the ostream directly (no escaping)$call{<function>}
call a function (do not serialize result)$|
trim left/right$$
and$%
escape$
and%
respectively- Anything else inside a function of a template class is text
Any line that starts with zero or more spaces and a %
is interpreted as C++.
For example
%#include<string>
%namespace
%{
%auto foo() -> std::string
%{
return "bar";
%}
%}
There is really nothing to it, just a %
at the beginning of the line
All text of the template is located in functions of template classes. Template classes start with $class <name> [: <base>]
and end with $endclass
.
This is a stand-alone class:
$class base
% // Some stuff
$endclass
And this is a derived class
%#include <base.h>
$class derived : base
% // Some other stuff
$endclass
In the generated code, the parent will also be the base of the child. They are linked in such a way that
- you can access the direct child via a
child
member variable in the parent - you can access anything inherited from parent, grandparent, etc via a
parent
member
If you want to reuse some template elements or just want to organize your templates into smaller units and use composition.
A helper class
$class Helper
% // Some stuff
$endclass
And this is a composite class
%#include <Helper.h>
$class composite
$member Helper helper
% // Some other stuff
$endclass
In the generated code, the member template will also be a member of the composite. They are linked in such a way that
- you can access the member via its name in the composite
- you can access the composite as
child
from the member template
As you saw in the initial example, the generated template code is initialized with data and a serializer. You can serialize members of that data or in fact any C++ expression by enclosing it in ${}
. For instance
%for (const auto& entry : data.entries)
%{
First name: ${entry.first}
Last name: ${entry.last}
Size of names: ${entry.first.size() + entry.last.size()}
%}
The serializer takes care of the required escaping, quoting, encoding, etc.
Sometimes you need to actually output some text as is. Then use $raw{expression}
. It will just pipe whatever you give it to the ostream
directly.
If you want to call a function without serializing the result (e.g. because the function returns void
), you can enclose the call in $call{}
.
- left-trim of a line: Zero or more spaces/tabs followed by
$|
- right-trim of a line (including the trailing return):
$|
at the end of the line
For example:
%auto title() -> void
%{
$| Hello ${data.name}! $|
%}
This will get rid of the leading spaces and the trailing return, yielding something like
Hello Mr. Wolf!
$$
->$
$%
->%
Text is everything else, as long as it is inside a function of a template class.
The interface of a serializer has to have
auto text(const char*) -> void;
This function is called by the kiss templates to serialize their texts.auto escape(...) -> void;
This function is called with expressions from${whatever}
. Make it accept whatever you need and like.
Optionally, the serializer might offer
auto raw(...) -> void;
This function is called with expressions from$raw{whatever}
. Make it accept whatever you need and like.auto report_exception(long lineNo, const std::string& expression, std::exception_ptr e);
This function gets called if kiste2cpp is called with --report-exceptions. Handle reported exceptions here in any way you seem fit.
At some point you will probably want to serialize your types.
If extending of kiste::html
for one or two types works,
extending for more types is not flexible, especially if you want to customize your serializer.
Then this is a moment when serializer policies may help. It allows you to implement a serializer for your output format in one class and implement policies (how to serialize specific types) as separate classes. Policies should know nothing about serializers, only how to convert their types to string (or even other types).
Have a look at example ratio_policy
for our type ratio
:
struct ratio
{
int num;
int den;
};
struct ratio_policy
{
template <typename SerializerT>
void escape(SerializerT& serializer, const ratio& value)
{
serializer.escape(value.num);
if (value.den != 1)
{
serializer.escape('/');
serializer.escape(value.den);
}
}
};
Then we need to extend kiste::html
(or your serializer) with one template method escape(SerializerT&, const T& t)
:
struct html : kiste::html
{
html(std::ostream& os) : kiste::html(os)
{
}
template <typename SerializerT, typename T>
void escape(SerializerT&, const T& t)
{
kiste::html::escape(t);
}
};
Finally we can build a serializer as kiste::build_serializer(kiste::html{os}, ratio_policy{})
.
kiste::build_serializer
accepts an arbitary number of policies and builds one serializer that uses them all.
This approach allows to keep knowledge about types in policies, provide arguments to policies and even reuse them for different serializers. Check out examples for more complex usages.
This is pretty much it.
There are several examples in the examples
folder. If you have questions, please do not hesitate to open an issue.