conkitty
v0.5.21
Published
Template Engine
Downloads
67
Readme
Conkitty Template Engine
Conkitty templates are being compiled to concat.js chains. It is not a regular template engine, it doesn't produce strings of HTML, but generates DOM instead.
Documentation is in painful progress, but it is better than nothing.
- Quick start
- Syntax
- Template declaration
- Strings
- JavaScript expressions
- Tags
- Variables
- Commands
- Unescaped strings
- Unescaped JavaScript expressions
- Line wrap
- Namespaced templates
- Remembering created nodes
- Returning more from templates
- Node appender
- External files dependency declaration
- Precompile expressions
- Generated code notes
- Source maps
- Performance notes
Quick start
Three quick start options are available:
npm install -g conkitty
andconkitty -c common.js -t tpl.js file.ctpl
will compilefile.ctpl
tocommon.js
andtpl.js
files.- Use grunt-conkitty to build Conkitty templates with Grunt, check out this example.
- Use gulp-conkitty to build Conkitty templates with Gulp, check out this example (it has example dependencies).
Compiled templates consist of two and a half parts:
- Common core code needed for runtime,
- compiled templates code,
- external dependencies (in case they are declared in templates).
To start using compiled templates, link common core and compiled templates
to your page with <script>
tags.
There is a Conkitty syntax highlighting plugin for JetBrains products (IntelliJ IDEA, WebStorm, PhpStorm and so on). It is available from official plugin repository.
<html>
<head>
<title>Hello</title>
</head>
<body>
<script src="/path/to/common.js"></script>
<script src="/path/to/compiled/templates.js"></script>
<script>
// Insert `template-name` result into document body right away.
$C.tpl['template-name'].call(document.body, 'Hello', [1, 2, 3], {k1: 'v1', k2: 'v2'});
// The same, but assign generated DOM to a variable.
var rendered = $C.tpl['template-name']('Hello', [1, 2, 3], {k1: 'v1', k2: 'v2'});
document.body.appendChild(rendered);
// `rendered` is a document fragment, so, it's not a valid reference
// after you call appendChild for it.
</script>
</body>
</html>
Syntax
Block nesting is done via indentation. Top level (zero-indented lines) is for templates declarations. Below each template declaration are commands, tags, strings, variables and JavaScript expressions.
template-name $arg1 $arg2 $arg3
h1
$arg1
ul
EACH $item $arg2
li
"Item: "
$item
dl
EACH $key $val $arg3
dt
$key
dd
$val
// $C.tpl['template-name']('Hello', [1, 2, 3], {k1: 'v1', k2: 'v2'}) will produce:
//
// <h1>Hello</h1>
// <ul>
// <li>Item: 1</li>
// <li>Item: 2</li>
// <li>Item: 3</li>
// </ul>
// <dl>
// <dt>k1</dt>
// <dd>v1</dd>
// <dt>k2</dt>
// <dd>v2</dd>
// </dl>
Template declaration
template-name [$arg1 [$arg2 […]]]
Where template-name
is a name of the template, this name is used to call the
template. When you call the template, you can pass any number of arguments to
it. These arguments will be accessible from JavaScript expressions of the
template via appropriate names. Argument names should be a $
sign plus a
valid JavaScript variable names.
template1 $arg1 $arg2
h1
$arg1
p
($arg2 + ' ' + $arg2) // JavaScript expression, arguments are
// accessible here too.
// $C.tpl.template1('Hello', 'World') will produce:
//
// <h1>Hello</h1>
// <p>World World</p>
You can specify default values for arguments.
template2 $arg1="Hello" $arg2=({a: 1, b: 2})
h1
$arg1
p
(JSON.stringify($arg2))
// $C.tpl.template2('Pillow', 'World') will produce:
//
// <h1>Pillow</h1>
// <p>"World"</p>
// $C.tpl.template2() will produce:
//
// <h1>Hello</h1>
// <p>{"a":1,"b":2}</p>
Strings
Strings are enclosed in single or double quotes. String should begin and end in the same line. String will be properly escaped in the result.
Good
"Hello world"
'Hello \'world\''
"Hello <world>"
Bad
"Hello
world"
JavaScript expressions
JavaScript expressions are enclosed in parenthesis. A JavaScript expression value will be coerced to string and properly escaped in the result. You can pass a function expression, this function will be called.
Good
(1 + 2 + 3)
('Hello ' + Math.random() + ' world')
(function() {
// Expressions could be multiline.
// `this` is pointing to current DOM node.
return this.innerHTML + this.innerHTML;
})
Bad
()
(())
(return 123)
Tags
Use CSS selector-like constructions to create tags.
// Create <div class="test"></div>
div.test
// Create <a id="ppp" class="hello world" href="http://conkitty.io"></a>
a#ppp.hello.world[href="http://conkitty.io/"]
// Create <a href="http://conkitty.io" data-rnd="0.8223862457089126">Yo</a>
a
@href "http://conkitty.io/"
@data-rnd (Math.random())
"Yo"
// or
a[href="http://conkitty.io/"][data-rnd=(Math.random())]
"Yo"
There are :if(expr, trueSelector, falseSelector)
and :elem(expr)
pseudo
selectors.
template $arg1 $arg2
strong:if($arg1, .arg1-is-true#piu, [data-something="yes"])
"Hello"
#identifier:elem($arg2)
"World"
// $C.tpl.template(true, 'em') will produce:
//
// <strong class="arg1-is-true" id="piu">Hello</strong>
// <em id="identifier">World</em>
// $C.tpl.template(false, 'span') will produce:
//
// <strong data-something="yes">Hello</strong>
// <span id="identifier">World</span>
There are several ways to specify attributes:
[attr=val]
parts of selectors,@attr val
below selectors,ATTR
command (see this command description below).
template $val2 $val5
div.class1[attr1="val1"][attr2=$val2][attr3=('val' + 3)]
@attr4 "val4"
@attr5 $val5
@attr6 ('val' + 6)
// Special case for `class` attribute, you can add, subtract and
// use selector syntax:
@class +.class2.class3 // Add using selector syntax.
@class -.class2 // Subtract using selector syntax.
@class +"class4 class5" // Add.
// $C.tpl.template('val222', 'val555') will produce:
//
// <div class="class1 class3 class4 class5"
// attr1="val1"
// attr2="val222"
// attr3="val3"
// attr4="val4"
// attr5="val555"
// attr6="val6"></div>
Variables
A number of variables could be accessible inside a template. Template
arguments, SET
, EACH
and WITH
commands, AS $var
constructions create
variables inside a template. Variable names should be a $
sign followed by
a valid JavaScript variable name, all template variables are accessible in
template JavaScript expressions via their names.
template $arg1 $arg2
SET $var1 $arg1
SET $var2 ('Concatenated ' + $arg2 + $arg2)
$arg1
($var1 + ' piu ' + $arg2)
$var2
Commands
ATTR name value
This command should be used to add an attribute with a dynamic name.
name and value are strings, variables or JavaScript expressions.
// Create
// <div id="yep"></div>
// or
// <div class="yep"></div>
div
ATTR (Math.random() < 0.5 ? 'id' : 'class') "yep"
CALL template-name [arg1 [arg2 […]]]
You can call one template from another. argN are arguments for a template. Arguments should be strings, variables or JavaScript expressions.
template1
div
CALL template2 "Hello" (' wo' + 'rld')
template2 $arg1 $arg2
h1
$arg1
$arg2
"!"
// $C.tpl.template1() will produce:
//
// <div>
// <h1>Hello world!</h1>
// </div>
You can pass arguments by names.
template1
div
// Notice that `$` signs in argument names should not present here.
CALL template2 "111" "22" a6=('si' + 'x') a4="ffff"
// Say, we have a template with a bunch of arguments with default values.
template2 $a1="1" $a2="2" $a3="3" $a4="4" $a5="5" $a6="6"
h1
$a1
h2
$a2
h3
$a3
h4
$a4
h5
$a5
h6
$a6
// $C.tpl.template1() will produce:
//
// <div>
// <h1>111</h1>
// <h2>22</h2>
// <h3>3</h3>
// <h4>ffff</h4>
// <h5>5</h5>
// <h6>six</h6>
// </div>
Additionally, you can pass a subtree when you call a template.
template1
div
CALL template2 "Hello world"
// This is PAYLOAD for template2, it will be calculated lazily
// when (and if) you use PAYLOAD command inside template2.
span
"!"
template2 $arg1
h1
$arg1
PAYLOAD
// $C.tpl.template1() will produce:
//
// <div>
// <h1>Hello world<span>!</span></h1>
// </div>
It is possible to get template name dynamically, as JavaScript expression.
template1
div
CALL ('template' + 2) "Hello" "world"
template2 $arg1 $arg2
$arg1
" "
$arg2
You can compliment CALL
command with EXCEPT
section, which will be executed
in case of any exception during CALL
command processing.
template1
div
CALL template2
// Payload could be here too.
EXCEPT
"Oops"
div
CALL (function() { throw new Error('Template name getter exception'); })
EXCEPT
"It is safe"
template2
span
JS
throw new Error('Exception, baby')
CHOOSE
CHOOSE
is a command to choose one of multiple choices.
template $arg1
CHOOSE
WHEN ($arg1 === 1)
div
"111"
WHEN $arg1
span
"222"
OTHERWISE
p
($arg1 + ' aaa ' + $arg1)
// $C.tpl.template(1) will produce: <div>111</div>.
// $C.tpl.template(2) will produce: <span>222</span>.
// $C.tpl.template(false) will produce: <p>false aaa false</p>.
Any number of WHEN
sections is possible. OTHERWISE
section is optional.
EACH [$key] $value expr
Iterate over an array or an object.
$key and $value are references to items you're being iterating through.
They should be a $
sign plus a valid JavaScript variable name. $key is
optional.
expr is a variable or a JavaScript expression returns an array or an object.
template
EACH $val ([11, 22])
p
($val + ' aa ' + $val)
EACH $index $val ([33, 44])
span
$index
": "
$val
EACH $vvv ({k1: 'v1', k2: 'v2'})
div
$vvv
EACH $k $v ({k3: 'v3', k4: 'v4'})
em
$k
": "
$v
// $C.tpl.template() will produce:
//
// <p>11 aa 11</p>
// <p>22 aa 22</p>
// <span>0: 33</span>
// <span>1: 44</span>
// <div>v1</div>
// <div>v2</div>
// <em>k3: v3</em>
// <em>k4: v4</em>
It is possible to add fallback for an empty array (or object):
template
ul
EACH $val ([])
li
$val
ELSE
li.empty
"No items."
EACH $key $val ({})
p
$key
": "
$val
ELSE
em
"Empty."
JS [$item $index $obj]
Run arbitrary JavaScript code.
$item, $index and $obj are references to current context items.
Everything that's below JS
command with higher indentation will be executed
as JavaScript code.
template
div
JS
window.everythingIsOk = true
p
"Hello world"
JS
var y = 'y',
e = 'e',
s = 's';
window.thisIsOkToo = y + e + s;
ul
EACH $i $v ([11, 22])
li
JS $item $index $arr
console.log($item === $v, $index === $i, $item, $arr);
$v
// $C.tpl.template() will produce:
//
// <div>
// <p>Hello world</p>
// <ul>
// <li>11</li>
// <li>22</li>
// </ul>
// </div>
//
// `window.everythingIsOk` and `window.thisIsOkToo` will be set to
// `true` and 'yes' respectively, and there will be two lines in console
// log: `true true 11 [11, 22]` and `true true 22 [11, 22]`.
MEM key [expr]
You have access to exact DOM nodes during their creation process. You can memorize some of these nodes for future use.
template
div
MEM "my-div"
p
MEM ('my' + '-' + 'p') ({ppp: this})
// $C.tpl.template();
// `$C.mem` will be {'my-div': <div>, 'my-p': {'ppp': <p>}}
SET $name expr
$name is a $
sign plus a valid JavaScript variable name.
expr is a JavaScript expression for a variable value.
Sometimes you need to define a variable.
template1
div
SET $myvar ({some: 'data', inside: '.'})
EACH $val $myvar
$val
// You can reuse variable names.
SET $myvar ({another: 'value'})
($myvar.another)
// $C.tpl.template1() will produce:
//
// <div>data.value</div>
You can also assign a subtree to a variable.
template2
SET $myvar2
em
"hello"
strong
"world"
div
// Use unescaped JavaScript expression (see below) to insert the result.
((($myvar2)))
// $C.tpl.template2() will produce:
//
// <div>
// <em>hello</em>
// <strong>world</strong>
// </div>
TEST expr
TEST
is a simplified CHOOSE
for the cases you have only one option to check.
template $title
TEST $title
h1
$title
p
"Some content"
// $C.tpl.template('Tiiiiiii') will produce:
//
// <h1>Tiiiiiii</h1>
// <p>Some content</p>
// $C.tpl.template() will produce:
//
// <p>Some content</p>
TRIGGER [arg1 [arg2 […]]]
You can subscribe to TRIGGER
command calls from your application:
$C.on(function(arg1, arg2) {
// `this` will be currently created DOM element.
console.log(arg1, arg2, this);
});
After that:
template
div
TRIGGER "some" ({arg: true})
"Hello"
// Calling $C.tpl.template.call(document.body) will add `<div>Hello</div>`
// to `document.body` and print `some {arg: true} <div>Hello</div>` in your
// console log.
WITH $name expr
$name is a $
sign plus a valid JavaScript variable name.
expr is a JavaScript expression.
template
div
SET $v ({a: {b: {c: 'd', e: false}}})
div
WITH $ok ($v.a.b.c)
$ok
ELSE
"HECK"
div
// Go to ELSE section in case of exception.
WITH $ok ($v.e.f.g)
$ok
ELSE
"HECK"
div
WITH $ok ($v.a.b.e)
$ok
ELSE
"HECK"
div
// Go to ELSE section in case of undefined value.
WITH $ok ($v.a.b.no)
$ok
ELSE
"HECK"
// $C.tpl.template() will produce:
//
// <div>
// <div>d</div>
// <div>HECK</div>
// <div>false</div>
// <div>HECK</div>
// </div>
Unescaped strings
A string like " "
will produce &nbsp;
in the result. To put an
unescaped text to the result, enclose a string in triple quotes. Note that the
markup inside triple quotes should be valid.
Good
"""Hello world"""
'''<p>Hello <strong>world</strong></p>'''
Bad
"""</div>"""
Unescaped JavaScript expressions
When you use a JavaScript expression to insert something to the result, its value is coerced to a properly escaped string. Sometimes you have a DOM node as a template argument or you want to insert an unescaped JavaScript expression value. To do that, enclose your JavaScript expression in triple parenthesis. When you do that, typecheck is performed, if your JavaScript expression value is an instance of Node, it will be inserted as node, otherwise the value will be coerced to a string and this string will be inserted unescaped (note that the markup should be valid).
Please, keep in mind, that if you need unescaped JavaScript expressions often, you are probably doing something wrong.
template1
div
CALL template2
span
"some DOM inside"
('<em>' + 'no, it is not' + '</em>')
((('<strong>' + 'yes, it is' + '</strong>')))
template2
p
// Pass this template's PAYLOAD as an argument to next template.
CALL template3 PAYLOAD
template3 $arg
"This is not "
$arg
", this is "
((($arg)))
" indeed."
// $C.tpl.template1() will produce:
//
// <div>
// <p>
// This is not [object DocumentFragment], this is <span>some DOM inside</span> indeed.
// </p>
// <em>no, it is not</em>
// <strong>yes, it is</strong>
// </div>
Line wrap
Templates can accept many arguments, selectors could be pretty long. It is
possible to split command into several lines. When parser meets \
, it skips
all the whitespaces after and continues with first non-whitespace character.
template1
CALL template2 \
"Argument1" \
"Argument2" \
"Argument3"
template2 $arg1 $arg2 $arg3
div.class1\
[attr1=$arg1]\
[attr2=$arg2]\
[attr3=$arg3]
"Hello"
// $C.tpl.template1() will produce:
//
// <div class="class1" attr1="Argument1" attr2="Argument2" attr3="Argument3">Hello</div>
Namespaced templates
When you build a set of files, all the templates go to compiled result. But you can declare namespaced templates, they will go to the result only in case they are used from regular templates.
Namespaced templates can be called from regular templates and can't be called
directly using $C.tpl[name]
functions.
// Regular template, will go to the result anyway.
template
div
CALL ns1::template1 "World"
// You can skip `CALL` keyword when you call namespaced template:
ns1::template1 "Pillow"
// Namespaced template, will go to the result because it's used from regular template above.
ns1::template1 $arg
span
"Hello "
$arg
// Another namespaced template, will not appear in the result since it's not used.
another-ns::template1
strong
"No way"
// $C.tpl.template() will produce:
//
// <div>
// <span>Hello World</span>
// <span>Hello Pillow</span>
// </div>
It is possible to skip CALL
keyword, when you call namespaced templates.
Remembering created nodes
Conkitty is a DOM generator, not a string generator. As a benefit, you have an
access to exact DOM nodes during their creation process. You can push these
nodes back to your application using TRIGGER
and MEM
commands. But you also
can assign nodes to variables with AS $varName
constructions.
template
div
span.hello AS $mySpan
em.world AS $world
// Now you have `<span>` in `$mySpan` and `<em>` in `$world`.
// You can pass them to other templates or do something sophisticated with them.
JS
$mySpan.innerHTML = 'Hello';
$world.innerHTML = ' beautiful world';
// $C.tpl.template() will produce:
//
// <div>
// <span class="hello">Hello</span>
// <em class="world"> beautiful world</em>
// </div>
Returning more from templates
You can return complex structures from templates, this is useful when you create reusable components (for example, you can return component's API).
Use EXPOSE
command in combination with string, variable, expression or
JS
command to return value and AS $varName
construction to get returned
value.
template
div
// $btn will have `{btn: <button>, title: <span>}`
ctrl::button "My sweet button" AS $btn
JS
$btn.title.innerHTML += '!!!';
ctrl::button $title $type="button"
button[type=$type] AS $btnNode
span AS $titleNode
$title
EXPOSE ({btn: $btnNode, title: $titleNode})
// Or the same with `JS` command:
//
// EXPOSE JS
// return {btn: $btnNode, title: $titleNode}
//
// Or the same with variable:
//
// SET $ret ({btn: $btnNode, title: $titleNode})
// EXPOSE $ret
// $C.tpl.template() will produce:
//
// <div>
// <button type="button">
// <span>My sweet button!!!</span>
// </button>
// </div>
Only one EXPOSE
command per template is allowed.
You can return values from regular templates too and have them as the result of template function call.
template1
div
"Hello World"
template2
div AS $d
"Hello World"
EXPOSE ({elem: $d, ololo: 'something else'})
Feel the difference in calls below.
var ret;
ret = $C.tpl.template1();
// `ret` is a document fragment with `<div>Hello World</div>` inside.
ret = $C.tpl.template1.call(document.body);
// `ret` is undefined and `<div>Hello World</div>` is added to `<body>`.
ret = $C.tpl.template2();
// `ret` is `{elem: <div>, ololo: 'something else'}`.
ret = $C.tpl.template2.call(document.body);
// `ret` is `{elem: <div>, ololo: 'something else'}` and
// `<div>Hello World</div>` is added to `<body>`.
Node appender
If you have DOM node in a variable (or as an expression result), you can use
Conkitty syntax to modify attributes and add children to this node. Use ^
operator for this.
template1
div.hello AS $myDiv
CALL template2 $myDiv
div.world AS $myDiv2
CALL template2 $myDiv2
template2 $div
strong
"Yo"
// Manipulations with $div node.
TEST $div
^$div
@class +.class1.class2
span
"Hello"
em
"Yep"
// $C.tpl.template1() will produce (notice node order):
//
// <div class="hello class1 class2">
// <span>Hello</span>
// <strong>Yo</strong>
// <em>Yep</em>
// </div>
// <div class="hello class1 class2">
// <span>Hello</span>
// </div>
// <strong>Yo</strong>
// <em>Yep</em>
External files dependency declaration
It often happens that you need some additional dependencies for your templates (such as stylesheets, JavaScript files or images).
It is possible to declare these dependencies and have their properly ordered
list along with compiled templates. Use &
operator for this.
Say, you have tpl.ctpl
file like this:
template
&"file1.css"
&"file1.js"
div
ctrl::button "My button"
// You can declare namespace-wide dependencies (they will go to the result
// in case any template of this namespace is used in regular templates).
ctrl::
&"ctrl.css"
&"ctrl.js"
ctrl::button $title $type="button"
&"button.css"
button[type=$type]
$title
// Will not appear in the result since not used.
ctrl::input $name
&"input.css"
input[type="text"][name=$name]
// Will not appear in the result since not used.
aaa::
&"aaa.css"
// Will not appear in the result since not used.
aaa::template
&"aaa.js"
conkitty
command line tool has --deps
option, it means filename to put
dependencies list to. grunt-conkitty
has deps
option too, for Grunt plugin it means directory to copy all the
dependencies to.
Let's use command line tool like
conkitty --common=common.js --templates=tpl.js --deps=deps.txt tpl.ctpl
deps.txt
will be:
/absolute/path/to/ctrl.css
/absolute/path/to/ctrl.js
/absolute/path/to/button.css
/absolute/path/to/file1.css
/absolute/path/to/file1.js
Precompile expressions
Precompile expressions are a kind of black voodoo and should be used in exceptional cases only.
Precompile expressions are JavaScript expressions that are evaluated by
template parser. Precompile expression should start with |(
and end with )|
.
For example, we have a template like this:
template|('-suffix' + prop1)|
&|('"style.' + prop2 + '.css"')|
p|(prop3 ? '.class1' : '')|
"Hello"
In this example prop1
, prop2
and prop3
are keys of environment object
passed to Conkitty
constructor. Here is the actual template that will be
compiled if we pass environment object like
{prop1: 111, prop2: 'dark', prop3: true}
:
template-suffix111
&"style.dark.css"
p.class1
"Hello"
Generated code notes
Generated code for a template like:
b-checkbox $props
div.checkbox
label
input[type="checkbox"][name=($props.name)][id=($props.id)][value=($props.value)]
TEST ($props.label)
em
($props.label)
will look like:
$C.tpl["b-checkbox"] = function($props) {
var $ConkittyEnv = $ConkittyGetEnv(this);
return $C($ConkittyEnv.p)
.div({"class": "checkbox"})
.elem("label")
.elem("input", function $C_b_checkbox_4_13(){return{type:"checkbox",name:$props.name,id:$props.id,value:$props.value}})
.end()
.test(function $C_b_checkbox_5_18() { return ($props.label); })
.elem("em")
.text(function $C_b_checkbox_7_21() { return ($props.label); })
.end(5);
};
You might notice anonymous functions named like $C_b_checkbox_7_21
. These
names are for easier debugging — you'll see template name, line number and
character position in your call stack. JavaScript minifiers like UglifyJS
remove these names during minification, so, compiled templates minify really
well.
Source maps
Since version 0.5.7
, Conkitty is capable of creating source maps for compiled
templates.
Check out gulp-conkitty and grunt-conkitty to see how to create source maps.
Or use --sourcemap
argument of command line utility:
conkitty --common=common.js --templates=tpl.js --sourcemap=tpl.map tpl.ctpl
When you compile templates with source map, //@ sourceMappingURL=your.source.map
comment is being added on top of your compiled templates code.
Performance notes
It is hard to do adequate performance tests. I did simple ones like (simplified example):
function render(parent, arr) {
var ret,
i;
ret = '<ul>';
for (i = 0; i < arr.length; i++) {
ret += '<li>' + arr[i] + '</li>';
}
ret += '</ul>';
parent.innerHTML = ret;
}
render(document.body, [1, 2, 3, 4, 5])
versus template:
render $arr
ul
EACH $item $arr
li
$item
and template call:
$C.tpl.render.call(document.body, [1, 2, 3, 4, 5])
And first ones were about 50% faster. But there are at least three things to keep in mind:
- After you've built DOM from HTML string, you need to spend some time traversing this DOM to find all key nodes your application needs. With Conkitty you don't need DOM traversals — you have access to exact DOM nodes during template execution.
- Pages are mostly reasonably-sized. This means that even bigger performance difference will not have noticeable impact to application speed.
- Many optimizations could be done in future (like merging adjacent text node generators into single expression). But because of first two things I clarify these optimizations as premature.