Cottle: Compact Object to Text Transform Language¶
Table of contents¶
Overview¶
What does it looks like?¶
Cottle (short for Compact Object to Text Transform Language) is a lightweight template engine for .NET (.NET Framework >= 4.7.2 & .NET Standard >= 2.0) allowing you to transform structured data into any text-based format output such as plain text, HTML or XML. It uses a simple yet easily extensible template language, thus enabling clean separation of document contents and layout.
A simple Cottle template printing an HTML document showing how many messages are in your mailbox could look like this:
{wrap html:
<h1>Hello, {name}!</h1>
<p>
{if len(messages) > 0:
You have {len(messages)} new message{if len(messages) > 1:s} in your mailbox!
|else:
You have no new message.
}
</p>
}
As you can guess by looking at this code, a Cottle template contains both plain text printed as-is as well as commands used to output dynamic contents. Cottle supports most common template engine features, such as:
- Text substitution with variables,
- Mathematical and boolean expressions,
- Built-in and used-defined functions,
- Variables & functions declaration and assignments,
- Text escaping control (wrap, unwrap),
- Conditional statements (if),
- Loops (for, while).
Source code is open for reviews and contributions!
Download the library¶
Cottle is available as an installable package on NuGet official website. Just open your extension manager in Visual Studio, search for “Cottle” and install from there.
You can also read, download or contribute to the source code on GitHub.
Getting started¶
To start using Cottle, first reference the package in your solution (using NuGet or manual install as detailed above). You’ll then need two things:
- An input template written with Cottle’s template language, used to define how your data will be rendered. This template can be contained in a String or streamed from any source compatible with IO.TextReader class (text file, memory buffer, network socket…) as shown in the example below.
- An executable code that reads your input template, create a
IDocument
object from it then render it to an output string or IO.TextWriter instance.
Here is a basic sample rendering a template with a single injected variable. Copy the C# source snippet somewhere in your program and get it executed. You should see the content of Rendering output snippet printed to standard output:
void RenderAndPrintTemplate()
{
var template = "Hello {who}, stay awhile and listen!";
var documentResult = Document.CreateDefault(template); // Create from template string
var document = documentResult.DocumentOrThrow; // Throws ParseException on error
var context = Context.CreateBuiltin(new Dictionary<Value, Value>
{
["who"] = "my friend" // Declare new variable "who" with value "my friend"
});
// TODO: customize rendering if needed
Console.Write(document.Render(context));
}
Hello my friend, stay awhile and listen!
For following code samples we’ll introduce Cottle template, C# source and Rendering output snippets to hold corresponding fragments. You’ll always need a C# wrapper similar to the one above in your code, so only new features will be specified in following examples ; they should replace the TODO comment highligted in above Rendering outout snippet.
Template language¶
Language syntax¶
Plain text and commands¶
A Cottle template can contain plain text printed as-is as well as code blocks containing commands that will be executed when document is rendered. These commands can either print dynamic content or have side-effects such as defining variables or controlling the rendering flow.
The most important command you’ll need is the echo
command that takes an argument and outputs its contents. Here is how it works:
Value of x is {echo x}.
var context = Context.CreateBuiltin(new Dictionary<Value, Value>
{
["x"] = 53
});
Value of x is 53.
In this example we’re creating a variable named x
with value 53 and pass it when rendering our template, then we’re using the echo
command to print the value of this variable. As you can tell the part between {
and }
is a code block containing executable commands, while everything else is plain text that is copied to document output.
Block delimiters¶
All commands must be wrapped in a code block between {
(block begin) and }
(block end) delimiters, which can be redefined in configuration if needed (read section Delimiters customization to learn how). Delimiters must be escaped if you want to use them in plain text, otherwise they would be misinterpreted as delimiters. This can be achieved by using \\
(escape) delimiter as shown below:
Characters \{, \}, \| and \\ must be escaped when used in plain text.
Characters {, }, | and \ must be escaped when used in plain text.
As visible in this example, backslash character \\
must also be used to escape itself when you want to output a backslash. Similar to other delimiters, the escape delimiter can be redefined through configuration.
Implicit echo¶
Since echo
is the most frequent command it supports a shorter implicit form where the “echo” keyword can be omitted:
Value of x is {x}.
Implicit form of echo
command can be used everywhere as long as you’re not printing a variable having the same name than a Cottle command such as for
. While technically possible, using Cottle command names as variables should be avoided for readability reasons anyway.
Expressions¶
Passing variables¶
To send variables so they can be used when a document is rendered you must provide them through a IContext
instance which is used as a render-time and read-only storage. This interface behaves quite like a IReadOnlyDictionary<Cottle.Value, Cottle.Value>
where Value
is a data structure able to store any value Cottle can handle. Key and value pairs within this dictionary are used as variable names and their associated values.
Implicit constructors from some native .NET types to Value
type are provided so you usually don’t have to explicitly do the conversion yourself but you can also create values using Value.FromSomething()
static construction methods (where “Something” is a known .NET type). See API documentation about Value
type for details.
Once you assigned variables to a context, pass it to your document’s rendering method so you can read them from your template (see section Getting started for a full example):
Hello {name}, you have no new message.
var context = Context.CreateBuiltin(new Dictionary<Value, Value>
{
["name"] = "John" // Implicit conversion from string on both key and value
});
Hello John, you have no new message.
Instances of IContext
are passed at document render time so they can be changed from one render to another, while instances of IDocument
can then be rendered as many time as you want. Compiling a template string into an IDocument
is a costly process implying parsing the string, validating its contents, applying code optimizations and storing it as an internal data structure. You should organize your code to avoid re-creating documents from the same template multiple time, as compiling a document is significantly more costly than rendering it.
Value types¶
Cottle supports immutable values which can either be declared as constants in templates or set in contexts you pass when rendering a document. Values have a type which can be one of the following:
- Boolean (value is either true or false),
- Number (equivalent to .NET’s double),
- String (sequence of character),
- Map (associative key/value container),
- Void (value is undefined ; any undeclared variable has void type).
Map values are associative tables that contain multiple children values stored as key/value pairs. Values within a map can be accessed directly by their key, using either dotted or subscript notation:
You can use either {mymap.f1} or {mymap["f2"]} notations for map values.
var context = Context.CreateBuiltin(new Dictionary<Value, Value>
{
["mymap"] = new Dictionary<Value, Value> // Implicit conversion to Value
{
["f1"] = "dotted",
["f2"] = "subscript"
}
});
You can use either dotted or subscript notations for map values.
Please note the quotes used in subscript notation. Trying to access value of {mymap[f2]}
will result in a very different behavior, since it will search for the value whose key is the value of f2
(which hasn’t be defined), leading to an undefined result. It is valid to have a map in which two or more keys are equal, but you will only be able to access the last one when using direct access. Iterating over the map’s elements will however show you its entire contents.
Implicit constructors on Value
class allow you to convert most .NET standard types into a Cottle value instance. To get an undefined value your from C# code use the Cottle.Value.Undefined
static field.
You can also declare constant values in your templates with following constructs:
{17.42}
{"Constant string"}
{'String with single quotes'}
{["key1": "value1", "key2": "value2"]}
{["map", "with", "numeric", "keys"]}
When declaring a constant map without keys, numeric increasing keys (starting at index 0) are implied. Also remember that both keys and values can be of any value type (numbers, strings, other nested maps, etc.).
Note
There are no false nor true constants in Cottle. You can inject them as variables if needed, but numeric values 0 and 1 can be considered as equivalent in most scenarios.
Expression operators¶
Cottle supports common mathematical and logical operators. Here is the list of all operators sorted by decreasing precedence order:
+
,-
and!
: unary plus, minus and logical “not” operator ;*
,/
and%
: binary multiplication, division and modulo operators ;+
and-
: binary addition and subtraction operators ;<
,<=
,=
,!=
,>=
and>
: binary logical comparison operators ;&&
and||
: binary “and” and “or” logical operators.
You can also use (
and )
to group sub-expressions and change natural precedence order. Here are some example of valid expressions:
{1 + 2 * 3}
{(1 + 2) * 3}
{!(x < 1 || x > 9)}
{value / 2 >= -10}
{"aaa" < "aab"}
Note
Mathematical operators (+
, -
, *
, /
and %
) only accept numeric operands and will try to cast other types to numbers (see Value
type for details about conversion to number).
Note
Logical operators can compare any type of operand and uses the same comparison algorithm than built-in function cmp(x, y).
Calling functions¶
Functions in Cottle are special values that can be invoked with arguments specified between a pair of parenthesis and separated by commas. Functions must be registered in a context as any other value type, and a helper method is available so you can start with a predefined set of built-in functions when rendering your documents. Create a context using Context.CreateBuiltin
method to have all built-in functions available in your document:
You have {len(messages)} new message{when(len(messages) > 1, 's')} in your inbox.
var context = Context.CreateBuiltin(new Dictionary<Value, Value>
{
["messages"] = new Value[]
{
"message #0",
"message #1",
"message #2"
}
});
You have 3 new messages in your inbox.
The list of all built-in functions as well as their behavior is available in section Built-in functions. For all following samples in this document we’ll assume that built-in functions are available when rendering a template.
Note
If you don’t want any built-in function to be available in your template, you can start off with a blank context by calling Context.CreateCustom
method.
Commands¶
Text escaping: wrap & unwrap¶
Added in version 2.0.0
You’ll most probably want to escape unsafe values (e.g. user input) before printing their contents from your templates, like making sure characters “<” and “>” are replaced by “<” and “>” when printing variables to an HTML document.
While this can be achieved by injecting an escaping function (e.g. Web.HttpUtility.HtmlEncode) and call it on every expression you pass to echo
command, a nice alternative is using wrap
command to ensure nothing is left unescaped before printing:
{wrap html:
<p data-description="{op_description}">
{op_name}
</p>
}
var htmlEncode = Function.CreatePure1((s, v) => HttpUtility.HtmlEncode(v.AsString));
var context = Context.CreateBuiltin(new Dictionary<Value, Value>
{
["html"] = Value.FromFunction(htmlEncode),
["op_description"] = "Three-way comparison or \"spaceship operator\"",
["op_name"] = "<=>"
});
<p data-description="Three-way comparison or "spaceship operator"">
<=>
</p>
The wrap
command syntax is {wrap function:some {body} here}
where function
is a function expression and the part between :
(body declaration) and }
(block end) delimiters is template code. The template code enclosed by wrap
command will have function
invoked on the expression of every echo
command it contains to modify its value before it gets printed. This means our previous example will produce an output equivalent to this template:
<p data-description="{html(op_description)}">
{html(op_name)}
</p>
You may occasionally want to cancel wrapping for printing a safe HTML snippet without wrapping it. This can be achieved with the unwrap
command that cancels its parent wrap
command:
{wrap html:
<p>This {variable} will be HTML-escaped.</p>
{unwrap:
<p>This {raw} one won't so make sure it doesn't contain unvalidated user input!</p>
}
<p>We're back in {safe} context here with HTML escaping enabled.</p>
}
Multiple wrap
commands can be nested, resulting in their functions being called from the innermost to outermost wrap
command.
Conditionals: if¶
You can write conditional statements by using the if
command which uses an expression as a predicate to check whether its body should be printed or not. Predicate is verified if value, once converted to a boolean type, is true (see Value
type for details about conversion to boolean).
{if 1:
A condition on a numeric value is true if the value is non-zero.
}
{if "aaa":
{if 1 + 1 = 2:
Commands can be nested.
}
}
A condition on a numeric value is true if the value is non-zero.
Commands can be nested.
The if
command syntax, similarly to wrap
command, is {if condition:when {condition} is true}
where condition
is a predicate expression and the part between :
(body declaration) and }
(block end) delimiters is template code. It also supports optional elif
(else if) and else
blocks that behave like in most programming languages, using syntax {if first:X|elif second:Y|else:Z}
. Both elif
and else
commands must be preceeded by a |
(block continue) delimiter.
{if len(items) > 2:
There are more than two items in map ({len(items)}, actually).
}
{if test:
Variable "test" is true!
|else:
Variable "test" is false!
}
{if x < 0:
X is negative.
|elif x > 0:
X is positive.
|else:
X is zero.
}
var context = Context.CreateBuiltin(new Dictionary<Value, Value>
{
["items"] = new Value[]
{
"item #0",
"item #1",
"item #2"
},
["test"] = 42,
["x"] = -3
});
There are more than two items in map (3, actually).
Variable "test" is true!
X is negative.
Enumerations: for¶
Keys and values within a map can be enumerated using the for
command, which repeatedly evaluates its body for each key/value pair contained within the map. The for
command also supports an optional empty
block evaluated when the map you enumerated doesn’t contain any key/value pair.
The for
command syntax and the one of its optional empty
block are similar to if
and else
commands (see section Conditionals: if):
Tags for this album:
{for tag in tags:
{tag}
}
{for index, text in messages:
Message #{index + 1}: {text}
|empty:
No messages to display.
}
var context = Context.CreateBuiltin(new Dictionary<Value, Value>
{
["messages"] = new Value[]
{
"Hi, this is a sample message!",
"Hi, me again!",
"Hi, guess what?"
},
["tags"] = new Value[]
{
"action",
"horror",
"fantastic"
}
});
Tags for this album: action horror fantastic
Message #1: Hi, this is a sample message!
Message #2: Hi, me again!
Message #3: Hi, guess what?
Note
Use syntax for value in map
instead of for key, value in map
if you don’t need to use map keys.
Assignments: set¶
You can assign variables during rendering with the set
command. Variable assignment helps you improving performance by storing intermediate results (such as function calls) when using them multiple times.
{set nb_msgs to len(messages)}
{if nb_msgs > 0:
You have {nb_msgs} new message{if nb_msgs > 1:s} in your mailbox!
|else:
You have no new message.
}
{set nb_long to 0}
{for message in messages:
{if len(message) > 20:
{set nb_long to nb_long + 1}
}
}
{nb_long} message{if nb_long > 1:s are|else: is} more than 20 characters long.
var context = Context.CreateBuiltin(new Dictionary<Value, Value>
{
["messages"] = new Value[]
{
"Hi, this is a sample message!"
"Hi, me again!",
"Hi, guess what?"
}
});
You have 3 new messages in your mailbox!
1 message is more than 20 characters long.
Note
Cottle variables have visibility scopes, which are described in section Variable scope.
Loops: while¶
The while
command evaluates a predicate expression and continues executing its body until predicate becomes false. Be sure to check for a condition that will become false after a finite number of iterations, otherwise rendering of your template may never complete.
{set min_length to 64}
{set result to ""}
{set words to ["foo", "bar", "baz"]}
{while len(result) < min_length:
{set result to cat(result, words[rand(len(words))])}
}
{result}
barbazfoobarbazbazbazbarbarbarbarfoofoofoobarfoobazfoofoofoofoobaz
Warning
Prefer the use of the for
command over while
command whenever possible, as the former provides better protection against infinite loops.
Debug: dump¶
When your template doesn’t render as you would expect, the dump
command can help you identify issues by showing value as an explicit human readable string. For example undefined values won’t print anything when passed through the echo
command, but the dump
command will show them as <void>
.
{dump "string"}
{dump 42}
{dump unknown(3)}
{dump [856, "hello", "x": 17]}
"string"
42
<void>
[856, "hello", "x": 17]
Note
Command dump
is a debugging command. If you want to get type of a value in production code, see type(value) method.
Comments: _¶
You can use the _
(underscore) command to add comments to your template. This command can be followed by an arbitrary plain text and will be stripped away when template is rendered.
{_ This is a comment that will be ignored when rendering the template}
Hello, World!
Hello, World!
Chaining¶
Added in version 2.0.7
Multiple commands can be chained using to the |
(block continue) delimiter. This delimiter can replace the }
(end of command) delimiter of any command to issue multiple commands without having to close and open new code blocks. In other words this allow writing {set x to 5 | echo x}
instead of {set x to 5}{echo x}
, which helps keeping your code easier to read by letting you indent it as you like without producing unwanted whitespace characters in output result (since whitespaces inside a code block are ignored).
{
_ Compute x to the power n using exponentiation by squaring |
declare power(x, n) as:{
declare m as 1 |
while n > 1:{
if n % 2 = 0:{
set x to x * x |
set n to n / 2
}|
else:{
set m to m * x |
set n to n - 1
}
}|
return m * x
}|
power(2, 5)
}
32
Built-in functions¶
Logical¶
and(x, y, …)¶
Perform logical “and” between given boolean values, i.e. return true
if all arguments are equivalent to true
(see Value
type for details about conversion to boolean).
{and(2 < 3, 5 > 1)}
true
Note
This function is equivalent to operator &&
.
cmp(x, y)¶
Compare x
against y
, and return -1 if x
is lower than y
, 0 if they’re equal, or 1 otherwise. When used on numeric values, the cmp
function uses numerical order. When used on strings, it uses alphabetical order. When used on maps, it first performs numerical comparison on their length then compares keys and values two by two. Two values of different types are always different, but the order between them is undefined.
{cmp("abc", "bcd")}
{cmp(9, 6)}
{cmp([2, 4], [2, 4])}
-1
1
0
default(primary, fallback)¶
Return primary
if primary
is equivalent to true
(see Value
type for details about conversion to boolean) or fallback
otherwise.
{set x to 3}
{default(x, "invisible")}
{default(y, "visible")}
3
visible
defined(x)¶
Check whether value x
is defined by checking it has a non-void type.
This is different than checking whether a value is equivalent to true
(see Value
type for details about conversion to boolean), for example integer 0
is equivalent to false
when used as a boolean expression but defined(0)
is true
. This function is mostly useful for testing whether a variable has been assigned a value or not.
{dump defined(undefined)}
{set a to 0}
{dump defined(a)}
<false>
<true>
eq(x, y, …)¶
Return true
if all arguments are equal or false
otherwise. It uses the same comparison algorithm than function cmp(x, y).
{eq(7, 7)}
{eq(1, 4)}
{eq("test", "test")}
{eq(1 = 1, 2 = 2, 3 = 3)}
true
false
true
true
Note
This function is equivalent to operator =
when used with 2 arguments.
ge(x, y)¶
Return true
if x
has a value greater than or equal to y
or false
otherwise. It uses the same comparison algorithm than function cmp(x, y).
{ge(7, 3)}
{ge(2, 2)}
{ge("abc", "abx")}
true
true
false
Note
This function is equivalent to operator >=
.
gt(x, y)¶
Return true
if x
has a value greater than y
or false
otherwise. It uses the same comparison algorithm than function cmp(x, y).
{gt(7, 3)}
{gt(2, 2)}
{gt("abc", "abx")}
true
false
false
Note
This function is equivalent to operator >
.
has(map, key)¶
Return true
if given map has a value associated to given key or false
otherwise.
{has(["name": "Paul", "age": 37, "sex": "M"], "age")}
true
Note
Result of this function is close to but not strictly equivalent to defined(map[key])
as the former will return true
if map
contains a key key
associated to an undefined value while the later will return false
.
le(x, y)¶
Return true
if x
has a value lower than or equal to y
or false
otherwise. It uses the same comparison algorithm than function cmp(x, y).
{le(3, 7)}
{le(2, 2)}
{le("abc", "abx")}
true
true
true
Note
This function is equivalent to operator <=
.
lt(x, y)¶
Return true
if x
has a value lower than y
or false
otherwise. It uses the same comparison algorithm than function cmp(x, y).
{lt(3, 7)}
{lt(2, 2)}
{lt("abc", "abx")}
true
false
true
Note
This function is equivalent to operator <
.
ne(x, y)¶
Return true
if x
equals y
or false
otherwise. It uses the same comparison algorithm than function cmp(x, y).
{ne(7, 7)}
{ne(1, 4)}
{ne("test", "test")}
false true false
Note
This function is equivalent to operator !=
when used with 2 arguments.
not(x)¶
Perform logical “not” on given boolean value, i.e return false
if value was equivalent to true
(see Value
type for details about conversion to boolean) or false
otherwise.
{not(1 = 2)}
true
Note
This function is equivalent to operator !
.
or(x, y, …)¶
Perform logical “or” between given boolean values, i.e. return true
if at least one argument is equivalent to true
(see Value
type for details about conversion to boolean).
{or(2 = 3, 5 > 1)}
true
Note
This function is equivalent to operator ||
.
xor(x, y, …)¶
Perform logical “xor” between given boolean values, i.e. return true
if exactly one argument is true
and all the others are false
.
{xor(2 < 3, 1 = 2)}
true
when(condition[, truthy[, falsy]])¶
Return truthy
if condition
is equivalent to true
(see Value
type for details about conversion to boolean) or falsy
otherwise (or an undefined value if falsy
is missing). This function is intended to act as the ternary operator you can find in some programming languages.
{set x to 3}
{set y to 0}
{when(x, "x is true", "x is false")}
{when(y, "y is true", "y is false")}
x is true
y is false
Mathematical¶
abs(x)¶
Return the absolute value of given numeric value x
.
{abs(-3)}
{abs(5)}
3
5
add(x, y)¶
Return the sum of two numeric values.
{add(3, 7)}
10
Note
This function is equivalent to operator +
.
ceil(x)¶
Returns the smallest integer greater than or equal to number value x
.
{ceil(2.7)}
3
cos(x)¶
Get the cosine of angle x
in radians.
{cos(-1.57)}
0.000796326710733263
div(x, y)¶
Return the numeric value of x
divided by the numeric value of y
, or an undefined value if y
was equal to zero.
{div(5, 2)}
2.5
Note
This function is equivalent to operator /
.
floor(x)¶
Returns the largest integer less than or equal to number value x
.
{floor(2.7)}
2
max(x[, y[, z, …]])¶
Return the highest numeric value among given ones.
{max(7, 5)}
{max(6, 8, 5, 7, 1, 2)}
7
8
Note
Combine with function call(func, map) if you want to get the highest numeric value from an array.
min(x[, y[, z, …]])¶
Return the lowest numeric value among given ones.
{min(9, 3)}
{min(6, 8, 5, 7, 1, 2)}
3
1
Note
Combine with function call(func, map) if you want to get the lowest numeric value from an array.
mod(x, y)¶
Return the value of x
modulo y
, or an undefined value if y
was equal to zero.
{mod(7, 3)}
1
Note
This function is equivalent to operator %
.
mul(x, y)¶
Return the numeric value of x
times y
.
{mul(3, 4)}
12
Note
This function is equivalent to operator *
.
pow(x, y)¶
Get specified number x
raised to the power y
.
{pow(2, 10)}
1024
rand([a[, b]])¶
Get a pseudo-random numeric value between 0 and 2.147.483.647 inclusive. If numeric a
value is specified, return a pseudo-random numeric value between 0 and a
exclusive. If both numeric values a
and b
are specified, return a pseudo-random numeric value between a
inclusive and b
exclusive.
{rand()}
{rand(1, 7)}
542180393
5
round(x[, digits])¶
Rounds number value x
to a specified number of fractional digits digits
, or to the nearest integral value if digits
is not specified.
{round(1.57)}
{round(1.57, 1)}
2
1.6
Collection¶
cat(a, b, …)¶
Concatenate all input maps or strings into a single one. Keys are not preserved when this function used on map values.
{dump cat("Hello, ", "World!")}
{dump cat([1, 2], [3])}
"Hello, World!"
[1, 2, 3]
Warning
All arguments must share the same type than first one, either map or string.
cross(map1, map2, …)¶
Return a map containing all pairs from map1
having a key that also exists in map2
and all following maps. Output pair values will always be taken from map1
.
{dump cross([1: "a", 2: "b", 3: "c"], [1: "x", 3: "y"])}
[1: "a", 3: "c"]
except(map1, map2, …)¶
Return a map containing all pairs from map1
having a key that does not exist in map2
and any of following maps. This function can also be used to remove a single pair from a map (if you are sure that it’s key is not used by any other pair, otherwise all pairs using that key would be removed also).
{dump except([1: "a", 2: "b", 3: "c"], [2: "x", 4: "y"])}
[1: "a", 3: "c"]
find(subject, search[, start])¶
Find index of given search
value in a map or sub-string in a string. Returns 0-based index of match if found or -1 otherwise. Search starts at index 0 unless start
argument is specified.
{find([89, 3, 572, 35, 7], 35)}
{find("hello, world!", "o", 5)}
{find("abc", "d")}
3
8
-1
filter(map, predicate[, a, b, …])¶
Return a map containing all pairs having a value that satisfies given predicate. Function predicate
is invoked for each value from map
with this value as its first argument, and pair is added to output map if predicate result is equivalent to true
(see Value
type for details about conversion to boolean).
Optional arguments can be specified when calling filter
and will be passed to each invocation of predicate
as second, third, forth argument and so on.
{dump filter(["a", "", "b", "", "c"], len)}
{declare multiple_of(x, y) as:
{return x % y = 0}
}
{dump filter([1, 6, 7, 4, 9, 5, 0], multiple_of, 3)}
["a", "b", "c"]
[6, 9, 0]
flip(map)¶
Return a map were pairs are created by swapping each key and value pair from input map. Using resulting map with the for
command will still iterate through each pair even if there was duplicates, but only the last occurrence of each duplicate can be accessed by key.
{dump flip([1: "hello,", 2: "world!"])}
{dump flip(["a": 0, "b": 0])}
["hello,": 1, "world!": 2]
["a", 0: "b"]
join(map[, string])¶
Concatenate all values from given map pairs, using given string as a separator (or empty string if no separator is provided).
{join(["2011", "01", "01"], "/")}
2011/01/01
len(x)¶
Return number of elements in given value, which means the number of pairs for a map or the number of character for a string.
{len("Hello!")}
{len([17, 22, 391, 44])}
6
4
map(source, modifier[, a, b, …])¶
Return a map where values are built by applying given modifier to map values, while preserving keys. Function modifier
is invoked for each value in source
with this value as its first argument.
Optional arguments can be specified when calling map
and will be passed to each invocation of modifier
as second, third, forth argument and so on.
{declare square(x) as:
{return x * x}
}
{dump map([1, 2, 3, 4], square)}
{dump map(["a": 1, "b": 7, "c": 4, "d": 5, "e": 3, "f": 2, "g": 6], lt, 4)}
[1, 4, 9, 16]
["a": 1, "b": 0, "c": 0, "d": 0, "e": 1, "f": 1, "g": 0]
range([start, ]stop[, step])¶
Generate a map where value of the i-th pair is start + step * i and last value is lower (or higher if step
is a negative integer) than stop
. Default base index is 0 if the start
argument is omitted, and default value for step
is 1 if start
< stop
or -1 otherwise.
{for v in range(5): {v}}
{for v in range(2, 20, 3): {v}}
0 1 2 3 4
2 5 8 11 14 17
slice(subject, index[, count])¶
Extact sub-string from a string or elements from a map (keys are not preserved when used with maps). count
items or characters are extracted from given 0-based numeric index
. If no count
argument is specified, all elements starting from given index
are extracted.
{for v in slice([68, 657, 54, 3, 12, 9], 3, 2): {v}}
{slice("abchello", 4)}
3 12
hello
sort(map[, callback])¶
Return a sorted copy of given map. First argument is the input map, and will be sorted using natural order (numerical or alphabetical, depending on value types) by default. You can specify a second argument as comparison delegate, that should accept two arguments and return -1 if the first should be placed “before” the second, 0 if they are equal, or 1 otherwise.
{set shuffled to ["in", "order", "elements" "natural"]}
{for item in sort(shuffled):
{item}
}
{declare by_length(a, b) as:
{return cmp(len(b), len(a))}
}
{set shuffled to ["by their", "are sorted", "length", "these strings"]}
{for item in sort(shuffled, by_length):
{item}
}
elements in natural order
these strings are sorted by their length
union(map1, map2, …)¶
Return a map containing all pairs from input maps, but without duplicating any key. If a key exists more than once in all input maps, the last one will overwrite any previous pair using it.
{dump union([1: "a", 2: "b"], [2: "x", 3: "c"], [4: "d"])}
[1: "a", 2: "x", 3: "c", 4: "d"]
zip(k, v)¶
Combine given maps of same length to create a new one. The n-th pair in result map will use the n-th value from k
as its key and the n-th value from v
as its value.
{set k to ["key1", "key2", "key3"]}
{set v to ["value1", "value2", "value3"]}
{dump zip(k, v)}
["key1": "value1", "key2": "value2", "key3": "value3"]
Text¶
char(codepoint)¶
Get a 1-character string from its Unicode code point integer value. See more about Unicode and code points on Wikipedia.
{char(97)}
{char(916)}
a
Δ
format(value, format[, culture])¶
Convert any value
to a string using given formatting from format
string expression. Format should use syntax str
or t:str
where t
indicates the type of the formatter to use and str
is the associated .NET format string. Available formatter types are:
a
: automatic (default, used ift
is omitted)b
: System.Booleand
ordu
: System.DateTime (UTC)dl
: System.DateTime (local)i
: System.Int64n
: System.Doubles
: System.String
Format string depends on the type of formatter selected, see help about Format String Component for more information about formats.
{format(1339936496, "d:yyyy-MM-dd HH:mm:ss")}
{format(0.165, "n:p2", "fr-FR")}
{format(1, "b:n2")}
2012-06-17 12:34:56
16,50 %
True
Formatters use current culture, unless a culture name is specified in the culture
argument. See documentation of CultureInfo.GetCultureInfo method to read more about culture names.
lcase(string)¶
Return a lowercase conversion of given string value.
{lcase("Mixed Case String"}
mixed case string
match(subject, pattern)¶
Match subject
against given regular expression pattern. If match is successful, a map containing full match followed by captured groups is returned, otherwise result is an undefined value. See .NET Framework Regular Expressions for more information.
{dump match("abc123", "^[a-z]+([0-9]+)$")}
{dump match("xyz", "^[a-z]+([0-9]+)$")}
["abc123", "123"]
<void>
ord(character)¶
Get the Unicode code point value of the first character of given string. See more about Unicode and code points on Wikipedia.
{ord("a")}
{ord("Δ")}
97
916
split(subject, separator)¶
Split subject
string according to given string separator separator
. Result is an map where pair values contain split sub-strings.
{dump split("2011/01/01", "/")}
["2011", "01", "01"]
token(subject, search, index[, replace])¶
Either return the n-th section of a string delimited by separator substring search
if no replace
argument is provided, or replace this section by replace
else. This function can be used as a faster alternative to combined split/slice/join calls in some cases.
{token("First.Second.Third", ".", 1)}
{token("A//B//C//D", "//", 2)}
{token("XX-??-ZZ", "-", 1, "YY")}
{token("1;2;3", ";", 3, "4")}
Second
C
XX-YY-ZZ
1;2;3;4
Type¶
cast(value, type)¶
Get value converted to requested scalar type. Type must be a string value specifying desired type:
"b"
or"boolean"
: convert to boolean value"n"
or"number"
: convert to numeric value"s"
or"string"
: convert to string value
{dump cast("2", "n") = 2}
{dump ["value for key 0"][cast("0", "n")]}
{dump cast("some string", "b")}
<true>
"value for key 0"
<true>
Compiler configuration¶
Specifying configuration¶
You can specify configuration parameters by passing a DocumentConfiguration
instance when creating a new document. Here is how to specify configuration parameters:
void RenderAndPrintTemplate()
{
var configuration = new DocumentConfiguration
{
NoOptimize = true
};
var template = "This is my input template file";
var documentResult = Document.CreateDefault(template, configuration);
// TODO: render document
}
Options can be set by assigning a value to optional fields of structure DocumentConfiguration
, as described below. Any undefined field will keep its default value.
Plain text trimming¶
Cottle’s default behavior when rendering plain text is to output it without any modification. While this gives you a perfect character-level control of how a template is rendered, it may prevent you from writing clean indented code for target formats where whitespaces are not meaningful, such as HTML or JSON.
For this reason you can change the way plain text is transformed through the use of text trimmers. A text trimmer is a simple Func<string, string>
function that takes a plain text value and returns it as it should be written to output. Some default trimmer functions are provided by Cottle, but you can inject any custom function you need as well.
TrimEnclosingWhitespaces¶
DocumentConfiguration.TrimEnclosingWhitespaces
removes all leading and trailing blank characters from plain text blocks. You may need to use expression {' '}
to force insertion of whitespaces between blocks:
{'white'} {'spaces '} around plain text blocks {'will'}{' '}{'be'} coll {'apsed'} .
var configuration = new DocumentConfiguration
{
Trimmer = DocumentConfiguration.TrimEnclosingWhitespaces
};
whitespaces around plain text blocks will be collapsed.
TrimFirstAndLastBlankLines¶
Added in version 2.0.2
DocumentConfiguration.TrimFirstAndLastBlankLines
removes end of line followed by blank characters at beginning and end of plain text blocks. You may have to introduce two line breaks instead of one when interleaving plain text and code blocks so one of them is preserved, or use {" "}
to force some whitespaces at the beginning or end of plain text blocks.
You have {len(messages)} message
{if len(messages) > 1:
s
}
{" "}in your inbox.
I can force
{"line breaks"}
to appear.
var configuration = new DocumentConfiguration
{
Trimmer = DocumentConfiguration.TrimFirstAndLastBlankLines
};
You have 4 messages in your inbox.
I can force
line breaks
to appear.
Note
This trimmer is used by default when no configuration is specified.
TrimNothing¶
DocumentConfiguration.TrimNothing
doesn’t changing anything on plain text blocks:
{'no'} change {'will'}
be applied
{'on'} plain {'text'} blocks.
var configuration = new DocumentConfiguration
{
Trimmer = DocumentConfiguration.TrimNothing
};
no change will
be applied
on plain text blocks.
TrimRepeatedWhitespaces¶
DocumentConfiguration.TrimRepeatedWhitespaces
replaces all sequences of white characters (spaces, line breaks, etc.) by a single space, similar to what HTML or XML languages do:
<ul> {for s in ["First", "Second", "Third"]: <li> {s} </li> } </ul>
var configuration = new DocumentConfiguration
{
Trimmer = DocumentConfiguration.TrimRepeatedWhitespaces
};
<ul> <li> First </li> <li> Second </li> <li> Third </li> </ul>
Delimiters customization¶
Default Cottle configuration uses "{"
character as block begin delimiter, "|"
as block continue delimiter and "}"
as block end delimiter. These characters may not be a good choice if you want to write a template that would often use them in plain text context, for example if you’re writing a JavaScript template, because you would have to escape every {, } and | to avoid Cottle seeing them as delimiters.
A good solution to this problem is changing default delimiters to replace them by more convenient sequences for your needs. Any string can be used as a delimiter as long as it doesn’t conflict with a valid Cottle expression (e.g. "["
, "+"
or "<"
). Make sure at least the first character of your custom delimiters won’t cause any ambiguity when choosing them, as the compilation error messages you may have would be confusing.
Default escape delimiter \ can be replaced in a similar way, however it must be a single-character value.
Delimiters are {{block_begin}}, {{block_continue}} and {{block_end}}.
Backslash \ is not an escape character.
var configuration = new DocumentConfiguration
{
BlockBegin = "{{",
BlockContinue = "{|}",
BlockEnd = "}}",
Escape = '\0'
};
var context = Context.CreateBuiltin(new Dictionary<Value, Value>
{
["block_begin"] = "double left brace (" + configuration.BlockBegin + ")"
["block_continue"] = "brace pipe brace (" + configuration.BlockContinue + ")",
["block_end"] = "double right brace (" + configuration.BlockEnd + ")"
});
Delimiters are double left brace ({{), brace pipe brace ({|}) and double right brace (}}).
Backslash \ is not an escape character.
Optimizer deactivation¶
Cottle performs various code optimizations on documents after parsing them from a template to achieve better rendering performance. These optimizations have an additional cost at compilation, which you may not want to pay if you’re frequently re-building document instances (which is something you should avoid if possible):
var configuration = new DocumentConfiguration
{
NoOptimize = true
};
Warning
Disabling optimizations is not recommended for production usage.
Compilation reports¶
The DocumentResult
structure returned after compiling a document contains information about any issue detected from input template along with their criticity level (see DocumentSeverity
), even though only Error ones prevent the document from being built. These issues can be accessed like this:
var documentResult = Document.CreateDefault(template, configuration);
for (var report in documentResult.Reports)
{
Console.WriteLine($"[{report.Severity}] {report.Message}");
}
Reports can be logged somewhere so you receive notifications whenever an issue is detected in your templates or a migration is suggested.
Note
The DocumentOrThrow helper from DocumentResult
will throw if reports contains one or more item with Error criticity level, and use the message from this item as the exception message.
Native documents¶
You can use “native” documents instead of default ones to achieve better rendering performance at a higher compilation cost. Native documents rely on IL code generation instead of runtime evaluation, and can provide a rendering performance boost from 10% to 20% depending on templates and environment (see benchmark). They’re however two to three times most costly to build, so this feature should be used only when you need high rendering performances on long-lived documents.
To create native documents, simply invoke Document.CreateNative
instead of default method:
var document = Document.CreateNative(template).DocumentOrThrow;
Advanced features¶
Understanding value types¶
Every value has a type in Cottle, even if you usually don’t have to worry about it (see Value
type for details). Functions that expect arguments of specific types will try to cast them silently and fallback to undefined values when they can’t. However in some rare cases you may have to force a cast yourself to get desired result, for example when accessing values from a map:
{set map to ["first", "second", "third"]}
{set key to "1"}
{dump map[key]}
<void>
You could have expected this template to display “second”, but Cottle actually searches for the map value associated to key "1"
(as a string), not key 1
(as a number). These are two different values and storing two different values for keys "1"
and 1
in a map is valid, hence no automatic cast can be performed for you.
In this example, you can explicitly change the type of key
to a number by using built-in function cast(value, type). Also remember you can use the Debug: dump command to troubleshoot variable types in your templates.
Variables immutability¶
All variables in Cottle are immutable, meaning it’s not possible to replace a section within a string or change the value associated to some key in a map. If you want to append, replace or erase a value in a map you’ll have to rebuild a new one where you inject, filter out or replace desired value. There are a few built-in functions you may find handy to achieve such tasks:
- cat(a, b, …) and union(map1, map2, …) can merge strings (
cat
only) or maps ; - slice(subject, index[, count]) can extract part of a string or a map ;
- except(map1, map2, …) can extract the intersection between two maps.
Here are a few examples about how to use them:
{set my_string to "Modify me if you can"}
{set my_string to cat("I", slice(my_string, 16), ".")}
{dump my_string}
{set my_array to [4, 8, 50, 90, 23, 42]}
{set my_array to cat(slice(my_array, 0, 2), slice(my_array, 4))}
{set my_array to cat(slice(my_array, 0, 2), [15, 16], slice(my_array, 2))}
{dump my_array}
{set my_hash to ["delete_me": "TODO: delete this value", "let_me": "I shouldn't be touched"]}
{set my_hash to union(my_hash, ["append_me": "I'm here!"])}
{set my_hash to except(my_hash, ["delete_me": 0])}
{dump my_hash}
"I can."
[4, 8, 15, 16, 23, 42]
["let_me": "I shouldn't be touched", "append_me": "I'm here!"]
Function declaration¶
Cottle allows you to declare functions directly in your template code so you can reuse code as you would do with any other programming language. To declare a function and assign it to a variable, use the same set
command you used for regular values assignments (see section Assignments: set) with a slightly different syntax. Function arguments must be specified between parenthesis right after the variable name that should receive the function, and the to
keyword must be followed by a “:” (semicolon) character, then function body declaration as a Cottle template.
Functions can return a value that can be used in any expression or stored in a variable. To make a function halt and return a value, use the return
command within its body:
{set factorial(n) to:{
if n > 1:{
return n * factorial(n - 1)
}|
else:{
return 1
}}
}
Factorial 1 = {factorial(1)}
Factorial 3 = {factorial(3)}
Factorial 8 = {factorial(8)}
{set hanoi_recursive(n, from, by, to) to:{
if n > 0:
{hanoi_recursive(n - 1, from, to, by)}
Move one disk from {from} to {to}
{hanoi_recursive(n - 1, by, from, to)}
}}
{set hanoi(n) to:{
hanoi_recursive(n, "A", "B", "C")
}}
{hanoi(3)}
Factorial 1 = 1
Factorial 3 = 6
Factorial 8 = 40320
Move one disk from A to C
Move one disk from A to B
Move one disk from C to B
Move one disk from A to C
Move one disk from B to A
Move one disk from B to C
Move one disk from A to C
You can see in this example that returning a value and printing text are two very different things. Plain text within function body is printed each time the function is called, or more precisely each time its enclosing block is executed (that means it won’t print if contained in an if
command that fails to pass, for example).
The value returned by the function won’t be printed unless you explicitly require it by using the echo
command (e.g. something like {factorial(8)}
). If a function doesn’t use any return
command it returns an undefined value, that’s why the call to {hanoi(3)}
in the sample above does not print anything more than the plain text blocks it contains.
Variable scope¶
When writing complex templates using nested or recursive functions, you may have to take care of variable scopes to avoid potential issues. A scope is the local evaluation context of any function or command having a body. When assigning a value to a variable (see section Assignments: set for details) all variables belong to the same global scope. Consider this template:
{set depth(item) to:{
set res to 0 |
for child in item:{
set res_child to depth(child) + 1 |
set res to max(res, res_child)
}|
return res
}}
{depth([["1.1", "1.2", ["1.3.1", "1.3.2"]], "2", "3", ["4.1", "4.2"]])}
The depth
function is expected to return the level of the deepest element in a value that contains nested maps. Of course it could be written in a more efficient way without using non-necessary temporary variables, but it would hide the problem we want to illustrate. If you try to execute this code you’ll notice it returns 2
where 3
would have been expected.
Here is the explanation: when using the set
method to assign a value to variable res
it always uses the same res
instance. The depth
function recursively calls itself but overwrite the unique res
variable each time it tries to store a value in it, and therefore fails to store the actual deepest level as it should.
To solve this issue, the res
variable needs to be local to function depth
so that each invocation uses its own res
instance. This can be achieved by using the declare
command that creates a variable in current scope. Our previous example can then be fixed by declaring a new res
variable inside body of function depth
, so that every subsequent reference to res
resolves to our local instance:
{set depth(item) to:{
declare res |
set res to 0 |
for child in item:{
set res_child to depth(child) + 1 |
set res to max(res, res_child)
}|
return res
}}
{depth([["1.1", "1.2", ["1.3.1", "1.3.2"]], "2", "3", ["4.1", "4.2"]])}
You could even optimize the first set
command away by assigning a value to res
during declaration ; the declare
command actually supports the exact same syntax than set
, the only difference being than “to” should be replaced by “as”:
{declare res as 0}
The same command can also be used to declare functions:
{declare square(n) as:{
return n * n
}}
Note that the set
command can also be used without argument, and assigns variable an undefined value (which is equivalent to reset it to an undefined state).
Native .NET functions¶
If you need new features or improved performance, you can assign your own .NET methods to template variables so they’re available as Cottle functions. That’s actually what Cottle does when you use Context.CreateBuiltin
method: a set of Cottle methods is added to your context, and you can have a look at the source code to see how these methods work.
To pass a function in a context, use one of the methods from Function
class, then pass it to Value.FromFunction
method to wrap it into a value you can add to a context:
Testing custom "repeat" function:
{repeat("a", 15)}
{repeat("oh", 7)}
{repeat("!", 10)}
var context = Context.CreateBuiltin(new Dictionary<Value, Value>
{
["repeat"] = Value.CreateFunction(Function.CreatePure2((state, subject, count) =>
{
var builder = new StringBuilder();
for (var i = 0; i < count; ++i)
builder.Append(subject);
return builder.ToString();
}))
});
Testing custom "repeat" function:
aaaaaaaaaaaaaaa
ohohohohohohoh
!!!!!!!!!!
Static class Function
supports multiple methods to create Cottle functions. Each method expects a .NET callback that contains the code to be executed when the method is invoked, and some of them also ask for the accepted number of parameters for the function being defined. Methods from Function
are defined across a combination of 2 criteria:
- Whether they’re having side effects or not:
- Methods
Function.CreatePure
,Function.CreatePure1
andFunction.CreatePure2
must be pure functions having no side effect and not relying on anything but their arguments. This assumption is used by Cottle to perform optimizations in your templates. For this reason their callbacks don’t receive aTextWriter
argument as pure methods are not allowed to write anything to output. - Methods
Function.Create
,Function.Create1
andFunction.Create2
are allowed to perform side effects but will be excluded from most optimizations. Their callbacks receive aTextWriter
argument so they can write any text contents to it.
- Methods
- How many arguments they accept:
- Methods
Function.Create
andFunction.CreatePure
with no integer argument accept any number of arguments, it is the responsibility of provided callback to validate this number. - Methods
Function.Create
andFunction.CreatePure
with acount
integer accept exactly this number of arguments or return an undefined value otherwise. - Methods
Function.Create
andFunction.CreatePure
with twomin
andmax
integers accept a number of arguments contained between these two values or return an undefined value otherwise. - Methods
Function.CreateN
andFunction.CreatePureN
only accept exactlyN
arguments or return an undefined value otherwise.
- Methods
The callback you’ll pass to Function
takes multiple arguments:
- First argument is always an internal state that must be forwarded to any nested function call ;
- Next arguments are either a list of values (for functions accepting variable number of arguments) or separate scalar values (for functions accepting a fixed number of arguments) received as arguments when invoking the function ;
- Last argument, for non-pure functions only, is a
TextWriter
instance open to current document output.
Lazy value evaluation¶
In some cases, you may want to inject to your template big and/or complex values that may or may not be needed at rendering, depending on other parameters. In such configurations, it may be better to avoid injecting the entire value in your context if there is chances it won’t be used, and use lazy evaluation instead.
Lazy evaluation allows you to inject a value with a resolver callback which will be called only the first time value is accessed, or not called at all if value is not used for rendering. Lazy values can be created through implicit conversion from any Func<Value>
instance or by using Value.FromLazy
construction method:
{if is_admin:
Administration log: {log}
}
var context = Context.CreateBuiltin(new Dictionary<Value, Value>
{
["is_admin"] = user.IsAdmin,
["log"] = () => log.BuildComplexLogValue() // Implicit conversion to lazy value
});
document.Render(context, Console.Out);
In this example, method log.BuildComplexLogValue
won’t be called unless is_admin
value is true.
Reflection values¶
Instead of converting complex object hierarchies to Cottle values, you can have the library do it for you by using .NET reflection. This approach is somehow slower than creating Cottle values manually but as it’s a lazy mechanism it may be a good choice if you have complex objects and don’t know in advance which fields might be used in your templates.
To use reflection, invoke Value.FromReflection
method on any .NET object instance and specify binding flags to indicate which members should be made visible to Cottle. Fields and properties resolved on the object will be accessible like if it were a Cottle map. Instances of types that implement IDictionary<TKey, TValue>
, IReadOnyDictionary<TKey, TValue>
or IEnumerable<TElement>
will have their key/value or index/element pairs transformed into Cottle maps.
Current culture is {culture.DisplayName} with keyboard layout ID {culture.KeyboardLayoutId}.
{for key, value in culture:
{if cast(value, 's'):
{key} = {value}
}
}
var context = Context.CreateBuiltin(new Dictionary<Value, Value>
{
["culture"] = Value.FromReflection(CultureInfo.InvariantCulture, BindingFlags.Instance | BindingFlags.Public)
});
Current culture is Invariant Language (Invariant Country) with keyboard layout ID 127.
LCID = 127
KeyboardLayoutId = 127
DisplayName = Invariant Language (Invariant Country)
NativeName = Invariant Language (Invariant Country)
EnglishName = Invariant Language (Invariant Country)
TwoLetterISOLanguageName = iv
ThreeLetterISOLanguageName = ivl
ThreeLetterWindowsLanguageName = IVL
IsReadOnly = true
Warning
Using reflection values has a negative impact on execution performance compared to regular values. Prefer explicit conversions to Value
instances unless performance is not relevant for your application.
Spying values¶
Added in version 2.0.5
If you’re working with many templates you may lose track of what variables are used and how. This is where the spying feature can come handy: it allows gathering information on each variable referenced in a template and their associated values. To use this feature, start by wrapping a context within a spying context using the Context.CreateSpy
method and use it for rendering your documents:
var spyContext = Context.CreateSpy(regularContext);
var output = document.Render(spyContext);
var someVariable = spyContext.SpyVariable("one"); // Access information about variable "one"
var someVariableField = someVariable.SpyField("field"); // Access map value field information
var allVariables = spyContext.SpyVariables(); // Access information about all template variables
Read about interface ISpyContext
for more information about how to use spying context methods.
Warning
Spying context has a negative impact on both performance and memory usage. You may want to apply some sampling strategy if you need to enable this feature in production.
API reference¶
Public API definition¶
This page contains information about types that are part of Cottle’s public API.
Warning
Types not listed in this page should not be considered as part of the public API, and are not taken into consideration when changing version number (see section Versioning convention).
Warning
You should avoid relying on method behaviors not documented in this page as they could change from one version to another.
Compiled documents¶
-
class IDocument
¶ A document in Cottle is a compiled template, which means a template converted to an optimized in-memory representation.
-
Value
Render
(IContext context, IO.TextWriter writer)¶ Render document and write output to given IO.TextWriter instance. Return value is the value passed to top-level
return
command if any, or an undefined value otherwise.
-
Value
-
class Document
¶ Methods from this static class must be used to create instances of
DocumentResult
.-
DocumentResult
CreateDefault
(IO.TextReader template, DocumentConfiguration configuration = default)¶ Create a new default
IDocument
instance suitable for most use cases. Template is read from any non-seekable IO.TextWriter instance.
-
DocumentResult
CreateDefault
(String template, DocumentConfiguration configuration = default) Create a new default
IDocument
instance similar to previous method. Template is read from given String instance.
-
DocumentResult
CreateNative
(IO.TextReader template, DocumentConfiguration configuration = default)¶ Create a new native
IDocument
instance for better rendering performance but higher compilation cost. Template is read from any non-seekable IO.TextWriter instance. See section Native documents for details about native documents.
-
DocumentResult
CreateNative
(String template, DocumentConfiguration configuration = default) Create a new native
IDocument
instance similar to previous method. Template is read from given String instance.
-
DocumentResult
-
class DynamicDocument
¶ -
Deprecated class, use
Cottle.Document.CreateNative
to create native documents.
-
class SimpleDocument
¶ -
Deprecated class, use
Cottle.Document.CreateDefault
to create documents.
-
class DocumentConfiguration
¶ Document configuration options, can be passed as an optional argument when creating a new document.
-
String
BlockBegin
{ get; set; }¶ Delimiter for block begin, see section Delimiters customization for details.
-
String
BlockContinue
{ get; set; }¶ Delimiter for block continue, see section Delimiters customization for details.
-
String
BlockEnd
{ get; set; }¶ Delimiter for block end, see section Delimiters customization for details.
-
Nullable<char>
Escape
{ get; set; }¶ Delimiter for escape, see section Delimiters customization for details. Default escape character is \ when this property is null.
-
Boolean
NoOptimize
{ get; set; }¶ Disable code optimizations after compiling a document, see Optimizer deactivation for details.
-
Func<String, String>
Trimmer
{ get; set; }¶ Function used to trim unwanted character out of plain text when parsing a document, see section Plain text trimming for details.
-
String
-
class DocumentResult
¶ This structure holds result of a template compilation, which can either be successful and provide compiled
IDocument
instance or failed and provide compilation error details as a list ofDocumentReport
elements:-
IDocument
Document
{ get; }¶ Instance of compiled document, only if compilation was successful (see
DocumentResult.Success
).
-
IReadOnlyList<DocumentReport>
Reports
{ get; }¶ List of anomalies detected during compilation, as a read-only list of
DocumentReport
items.
-
IDocument
DocumentOrThrow
{ get; }¶ Helper to return compiled document when compilation was successful or throw a
Exceptions.ParseException
exception with details about first compilation error otherwise.
-
IDocument
-
class DocumentReport
¶ Anomaly report on compiled template, with references to related code location.
-
String
Message
{ get; }¶ Human-readable description of the anomaly. This value is meant for being displayed in a user interface but not processed, as its contents is not predictable.
-
DocumentSeverity
Severity
{ get; }¶ Report severity level.
-
String
-
enum DocumentSeverity
¶ Report severity level.
-
Error
¶ Template issue that prevents document from being constructed.
-
Warning
¶ Template issue that doesn’t prevent document from being constructed nor rendered, but may impact rendered result or performance and require your attention.
-
Notice
¶ Template issue with no visible impact, mostly used for code suggestions or deprecation messages.
-
Rendering contexts¶
-
class IContext
¶ This interface is used to pass variables to a document when rendering it.
-
class Context
¶ Methods from this static class must be used to create instances of
IContext
.-
IContext
CreateBuiltin
(IContext custom)¶ Create a rendering context by combining a given existing context with all Cottle built-in functions (see section Built-in functions). Variables from the input context always have priority over built-in functions in case of collision.
-
IContext
CreateBuiltin
(IReadOnlyDictionary<Value, Value> symbols) Create a rendering context by combining variables from given dictionary with all Cottle built-in functions. This method is similar to previous one and only exists as a convenience helper.
-
IContext
CreateCascade
(IContext primary, IContext fallback)¶ Create a rendering context by combining two existing contexts that will be searched in order when querying a variable. Primary context is searched first, then fallback context is searched second if the result from first one was an undefined value.
-
IContext
CreateCustom
(Func<Value, Value> callback)¶ Create a rendering context using given callback for resolving variables. Callback must always expected to return a non-null result, possibly an undefined value.
-
IContext
CreateCustom
(IReadOnlyDictionary<Value, Value> symbols) Create a rendering context from given variables dictionary.
-
ISpyContext
CreateSpy
(IContext source)¶ Wrap given context inside a spying context to get information about variables referenced in a template, along with their last known value and accessed fields. See section Spying values for details about lazy value resolution.
-
(IContext,ISymbolUsage)
CreateMonitor
(IContext context)¶ Obsolete alternative to
Context.CreateSpy
.
-
IContext
Function declaration¶
-
class IFunction
¶ Cottle function interface.
-
Boolean
IsPure
{ get; }¶ Indicates whether function is pure or not. Pure functions have no side effects nor rely on them, and may offer better rendering performance as they’re eligible to more compilation optimizations.
-
Value
Invoke
(Object state, IReadOnlyList<Value> arguments, IO.TextWriter output)¶ Invoke function with given arguments. Variable
state
is an opaque payload that needs to be passed to nested function calls if any,arguments
contains the ordered list of values passed to function, andoutput
is a text writer to document output result.
-
Boolean
-
class Function
¶ Methods from this static class must be used to create instances of
IFunction
.-
IFunction
Create
(Func<Object, IReadOnlyList<Value>, IO.TextWriter, Value> callback, Int32 min, Int32 max)¶ Create a non-pure function accepting between
min
andmax
arguments (included).
-
IFunction
Create
(Func<Object, IReadOnlyList<Value>, IO.TextWriter, Value> callback, Int32 count) Create a non-pure function accepting exactly
count
arguments.
-
IFunction
Create
(Func<Object, IReadOnlyList<Value>, IO.TextWriter, Value> callback) Create a non-pure function accepting any number of arguments.
-
IFunction
Create0
(Func<Object, IO.TextWriter, Value> callback)¶ Create a non-pure function accepting zero argument.
-
IFunction
Create1
(Func<Object, Value, IO.TextWriter, Value> callback)¶ Create a non-pure function accepting one argument.
-
IFunction
Create2
(Func<Object, Value, Value, IO.TextWriter, Value> callback)¶ Create a non-pure function accepting two arguments.
-
IFunction
Create3
(Func<Object, Value, Value, Value, IO.TextWriter, Value> callback)¶ Create a non-pure function accepting three arguments.
-
IFunction
CreatePure
(Func<Object, IReadOnlyList<Value>, Value> callback, Int32 min, Int32 max)¶ Create a pure function accepting between
min
andmax
arguments (included).
-
IFunction
CreatePure
(Func<Object, IReadOnlyList<Value>, Value> callback, Int32 count) Create a pure function accepting exactly
count
arguments.
-
IFunction
CreatePure
(Func<Object, IReadOnlyList<Value>, Value> callback) Create a pure function accepting any number of arguments.
-
IFunction
CreatePure0
(Func<Object, Value> callback)¶ Create a pure function accepting zero argument.
-
IFunction
CreatePure1
(Func<Object, Value, Value> callback)¶ Create a pure function accepting one argument.
-
IFunction
Value declaration¶
-
class Value
¶ Cottle values can hold instances of any of the supported types (see section Value types).
-
Value
EmptyMap
{ get; }¶ Static and read-only empty map value, equal to
Value.FromEnumerable(Array.Empty<Value>()))
.
-
Value
EmptyString
{ get; }¶ Static and read-only empty string value, equal to
Value.FromString(string.Empty)
.
-
Value
False
{ get; }¶ Static and read-only boolean “false” value, equal to
Value.FromBoolean(false)
.
-
Value
Undefined
{ get; }¶ Static and read-only undefined value, equal to
new Value()
ordefault(Value)
.
-
Boolean
AsBoolean
{ get; }¶ Read value as a boolean after converting it if needed. Following conversion is applied depending on base type:
- From numbers, return
true
for non-zero values andfalse
otherwise. - From strings, return
true
for non-zero length values andfalse
for empty strings. - From undefined values, always return
false
.
- From numbers, return
-
IFunction
AsFunction
{ get; }¶ Read value as a function, only if base type was already a function. No conversion is applied on this property, and return value is undefined if value was not a function.
-
Double
AsNumber
{ get; }¶ Read value as a double precision floating point number after converting it if needed. Following conversion is applied depending on base type:
- From booleans, return
0
forfalse
or1
fortrue
. - From strings, convert to double number if value can be parsed as one using
double.TryParse()
on invariant culture, or return0
otherwise. - From undefined values, always return
0
.
- From booleans, return
-
String
AsString
{ get; }¶ Read value as a string after converting it if needed. Following conversion is applied depending on base type:
- From booleans, return string
"true"
fortrue
and empty string otherwise. - From numbers, return result of call to
double.ToString()
method with invariant culture. - From undefined values, always return an empty string.
- From booleans, return string
-
ValueContent
Type
{ get; }¶ Get base type of current value instance.
-
FromDictionary
(IReadOnlyDictionary<Value, Value> dictionary)¶ Create a map value from given keys and associated value in given
dictionary
, without preserving any ordering. This override assumes input dictionary is immutable and simply keeps a reference on it without duplicating the data structure.
-
FromEnumerable
(IEnumerable<KeyValuePair<Value, Value>> pairs)¶ Create a map value from given
elements
, preserving element ordering but also allowing O(1) access to values by key.
-
FromEnumerable
(IEnumerable<Value> elements) Create a map value from given
elements
. Numeric keys are generated for each element starting at index0
.
-
FromFunction
(IFunction function)¶ Create a function value by wrapping an executable
IFunction
instance. See sections Function declaration and Native .NET functions for details about functions in Cottle.
-
FromGenerator
(Func<Int32, Value> generator, Int32 count)¶ Create map value from given generator. Generator function
generator
is used to create elements based on their index, and the map will containcount
values associated to keys0
tocount - 1
. Values are created only when retrieved, so creating a generator value with 10000000 elements won’t have any cost until you actually access these elements from your template.
-
FromLazy
(Func<Value> resolver)¶ Create a lazy value from given value resolver. See section Lazy value evaluation for details about lazy value resolution.
-
FromReflection
<TSource> (TSource source, Reflection.BindingFlags bindingFlags)¶ Create a reflection-based value to read members from object
source
. Source object fields and properties are resolved using Type.GetFields and Type.GetProperties methods and provided binding flags for resolution. See section Reflection values for details about reflection-based inspection.
-
Value
-
class FunctionValue
¶ -
Deprecated class, use
Value.FromFunction
to create function values.Class constructor.
-
class LazyValue
¶ -
Deprecated class, use
Value.FromLazy
to create lazy values.Class constructor.
-
class ReflectionValue
¶ -
Deprecated class, use
Value.FromReflection
to create reflection values.-
ReflectionValue
(Object source, Reflection.BindingFlags binding)¶
Class constructor with explicit binding flags.
-
ReflectionValue
(Object source)
Class constructor with default binding flags for resolution (public + private + instance).
-
-
class IMap
¶ Value fields container.
-
Value
this
[, Value key]
{ get; }¶ Get field by its key (usually its name), or an undefined value if no field was defined with this name.
-
Value
Spying context¶
-
class ISpyContext
¶ -
Rendering context able to spy on variables and fields used during document rendering.
-
ISpyRecord
SpyVariable
(Value key)¶ Spy variable matching given key from underlying context. This method can be called either before or after rendering a document, as returned record is updated on each rendering.
-
IReadOnlyDictionary<Value, ISpyRecord>
SpyVariables
()¶ Spy all variables used in rendered document from underlying context and return them as a dictionary indexed by variable key. Note that every variable referenced in a document will have an entry in returned dictionary, even if they were not accessed at rendering.
-
ISpyRecord
-
class ISpyRecord
¶ Spying information about variable or field value.
-
ISpyRecord
SpyField
(Value key)¶ Spy field matching given key from current variable or field. This method is similar to
ISpyContext.SpyVariable
but works on variable fields instead of context variables.
-
IReadOnlyDictionary<Value, ISpyRecord>
SpyFields
()¶ Spy all fields from current variable or field and return then as a dictionary indexed by field key.
-
ISpyRecord
Versioning¶
Versioning convention¶
Cottle versioning does NOT (exactly) follow SemVer convention but uses closely-related version numbers with form MAJOR.MINOR.PATCH
where:
MAJOR
increases when breaking changes are applied and break source compatibility, meaning client code must be changed before it can compile.MINOR
increases when binary compatibility is broken but source compatibility is maintained, meaning client code can be rebuilt with no source change.PATCH
increases when binary compatibility is maintained from previous version, meaning new library version can be used as a drop-in replacement and doesn’t require recompiling code.
The main difference between this approach and SemVer is the distinction made between binary compatibility and source compatibility. For example replacing a public field by a property, or doing the opposite, would break strict binary compatibility but wouldn’t require any change when recompiling client code unless it’s using reflection.
Migration guide¶
From 1.6.* to 2.0.*¶
- Cottle now uses Double type for number values instead of Decimal ; use builtin function format(value, format[, culture]) if you need to control decimal precision when printing decimal numbers.
- Type
Value
is now a value type to reduce runtime allocations ; API was upgraded to be source-compatible with previous Cottle versions. - Specialized value classes (e.g.
Values.FunctionValue
) are deprecated, useValue.From*
static construction methods instead (e.g.Value.FromFunction
).
// Version 1.6.*
var context = Context.CreateBuiltin(new Dictionary<Value, Value>
{
["f"] = new FunctionValue(myFunction),
["n"] = new NumberValue(myNumber)
};
// Version 2.0.*
var context = Context.CreateBuiltin(new Dictionary<Value, Value>
{
["f"] = Value.FromFunction(myFunction),
["n"] = Value.FromNumber(myNumber) // Or just `myNumber` to use implicit conversion
};
From 1.5.* to 1.6.*¶
- All documents should be constructed using methods from
Document
static class. - All contexts should be constructed using methods from
Context
static class. - All functions should be constructed using methods from
Function
static class.
// Version 1.5.*
IDocument document;
try
{
document = new SimpleDocument(template, new CustomSetting
{
Trimmer = BuiltinTrimmers.FirstAndLastBlankLines
});
}
catch (ParseException exception)
{
MyErrorHandler(exception.Message);
return string.Empty;
}
return document.Render(new BuiltinStore
{
["f"] = new NativeFunction((args, store, output) => MyFunction(args[0].AsNumber, output), 1)
});
// Version 1.6.*
var result = Document.CreateDefault(template, new DocumentConfiguration
{
Trimmer = DocumentConfiguration.TrimIndentCharacters
});
if (!result.Success)
{
MyErrorHandler(result.Reports);
return string.Empty;
}
// Can be replaced by result.DocumentOrThrow to factorize test on "Success" field and use
// the exception-based API which is closer to what was available in version 1.5.*
var document = result.Document;
return document.Render(Context.CreateBuiltin(new Dictionary<Value, Value>
{
["f"] = new FunctionValue(Function.Create1((state, arg, output) => MyFunction(arg.AsNumber, output)))
});
From 1.4.* to 1.5.*¶
IStore
replaced by immutableIContext
interface for rendering documents. Since the former extends the later, migration should only imply recompiling without any code change.- Cottle function delegates now receive a
IReadOnlyList<Value>
instead of their mutable equivalent. - Method
Save
fromDynamicDocument
can only be used in the .NET Framework version, not the .NET Standard one.
From 1.3.* to 1.4.*¶
- Change of version number convention, breaking source compatibility must now increase major version number.
- Cottle now requires .NET 4.0 or above.
From 1.2.* to 1.3.*¶
- Removed deprecated code (flagged as “obsolete” in previous versions).
From 1.1.* to 1.2.*¶
IScope
replaced by similarIStore
interface (they mostly differ by the return type of their “Set” method which made this impossible to change without breaking the API).- Callback argument of constructors for
NativeFunction
are not compatible withIScope
to avoid ambiguous statements.
From 1.0.* to 1.1.*¶
LexerConfig
must be replaced byCustomSetting
object to change configuration.FieldMap
has been replaced by multiple implementations of the newIMap
interface.- Two values with different types are always different, even if casts could have made them equal (i.e. removed automatic casts when comparing values).
- Common functions
cross
andexcept
now preserve duplicated keys.
Credits¶
Greetings¶
- Huge thanks to Zerosquare for the lovely project icon!