.
kurly
is a tiny ~1018 bytes pluggable templating engine for
Node and browsers. It can parse templates with tags to abstract syntax trees,
which it can then compile into functions.
- kurly.js (fully commented source ~7kB)
- kurly.min.js (~1018 bytes minified and gzipped)
index.html
<script src="https://unpkg.com/kurly@2.0.0-beta.1/kurly.min.js"></script>
<script>(function(){ // IIFE
var ast = kurly.parse('{noun} {verb} {adjective}!')
var tags = { '*': ({name}) => (rec) => `${rec[name]}` }
var template = kurly.compile(ast, tags)
var record = { noun: 'Kurly', verb: 'is', adjective: 'easy' }
var output = template(record) // ['Kurly', ' ', 'is', ' ', 'easy', '!']
console.info(output.join('')) // > "Kurly is easy!"
})()</script>
npm install --save kurly
// main functions
var parse = require('kurly/parse')
var compile = require('kurly/compile')
// extra utils
var pipe = require('kurly/pipe')
var tag = require('kurly/tag')
// main functions
import parse from 'kurly/parse'
import compile from 'kurly/compile'
// extra utils
import pipe from 'kurly/pipe'
import tag from 'kurly/tag'
Call parse
to parse text with tags into an abstract syntax tree:
var ast = parse('Hello, {kurly}')
Create tags:
var tags = {
kurly: function(ctx){
return function(rec) {
return `${rec.planet}!`
}
}
}
Call compile
with the ast and your tags to create a template function:
var template = compile(ast, tags)
Call the resulting function, passing it a record with parameters:
var result = template({ planet: 'World' }) // ['Hello, ', 'World!']
The kurly parser is nice because it's small but powerful. It handles nesting and performs escaping and it returns an ast that is fully serializable and from which you can reconstruct the input string.
function parse(str: string, options?: Options): Ast
Parses the string str
to an Ast
.
If parse options
are giventhey are used to determine whether open/close markers are optional and which characters to use for them. By default
{and
}` are required.
The string to parse
options
: Options
Optional parse options.
Pass options to parse
to control it's behavior.
type Options = {
/**
* Whether open/close markers are optional
*/
optional?: boolean,
/**
* The character to use as open marker
*/
open?: string,
/**
* The character to use as close marker
*/
close?: string
}
An Abstract Syntax Tree. An array of strings or Node
s, where a Node
has a field ast
that contains the ast of it's children.
type Ast = Array<Node | string>
parse
returns an Ast
and compile
accepts an Ast
.
Compiles an ast into a template function.
function compile(ast: Ast, tags: Tags, rec?: object): TagFn
This function will create a pipe from the ast
and tags
by calling pipe()
, and then will create a single
function from it by calling tag()
, and return that.
ast
: Ast
The ast to compile into a function
tags
: Tags
The tags to use in the compile
The optional static record object.
kurly
is just a tiny parser / compiler. Any functionality should be
provided by tags.
You provide the tags to use to compile
or pipe
in the form of a dictionary object where each key's name is a string
to match tag names to and each key's value is a Tag
.
type Tags = {
[key : string]: Tag;
}
One special entry is the wildcard tag which has key name '*'
.
kurly
matches tags following a variation of this regex pattern:
/({)([_a-zA-Z][_a-zA-Z0-9]*)([^_a-zA-Z0-9\}].*)?(})/
This expression matches an open curly brace, a tag name, optionally some text starting with a non-identifier character and a closing curly brace.
Tag names can not contain any special characters such as punctuation, diacritics, whitespace, unicode symbols etc. They must start with an uppercase or lowercase letter or the underscore and may be followed by zero or more alphanumerical characters.
If a tag is enclosed in braces, any text following the tag name is parsed and
escaping is applied. A tag can contain a closing brace as text by escaping it.
The string "a {tag with a closing curly brace \} in it}"
will be parsed
correctly.
If a tag is not enclosed in braces, it's text ends at the first whitespace character following the tag name.
Kurly's parse
function accepts an options
object to control whether
open and close braces are optional and which characters are used for them.
To create a kurly tag, we create a higher order function; a function that returns a function:
function outer(cfg) {
return function inner(rec) {
return `My first ${rec.thing}`
}
}
The outer function is called during the compilation phase. It is passed a configuration object containing the tag name and function, the tag content text and an abstract syntax tree of it's children (see Nested tags). Any expensive work that needs to be done only once can be done here.
The inner function is called during the render phase.
It returns an (array of) output(s). The output entries can be any type. It's
argument is a record object that was initialized when the compiled function was
called. One key is always added to this object: children
. This contains the
rendered output of the children and can be used in the tag output.
Created a nice tag and want to share it with the world?
Publish it to NPM! Make sure to include the keyword "kurly"
in your
package,json
so it will show up in the list of
projects related to kurly.
Since v2, Kurly supports 'static' tags. These are tags that don't depend on the record object passed to the template function. Instead, they get access to a static version of that object at compile time. To create a static tag we write:
function outer(cfg, rec) {
return function inner() {
return `My first ${rec.thing}`
}
}
Notice how the rec
parameter has moved from the inner to the outer function.
If any of the tags in an ast are not static, all static tags will be converted to dynamic tags automatically. The reverse is not possible.
If all tags in an ast are static and a static record object is passed to
compile
, it will yield a function that can be called without arguments.
Static tags are more restricted in their abilities, but they can exist in both static and dynamic ast's, so they are the most flexible option if your tag does not need to access any field from the dynamic record.
Kurly supports nested tags:
var ast = parse('{greeting, {kurly}}')
var template = compile(ast, {
greeting: () => ({ children }) => ['Hello'].concat(children),
kurly: () => () => 'World!'
})
var result = template() // ['Hello', ', ', 'World!']
For a tag to support nesting, it should pick up it's children and add them
to the result it is returning. In the example above, greeting
is adding
it's children to the array it is returning using concat
.
Static tags can also allow nesting, but because they don't have access
to the dynamic record, they need to do a little bit more work for it.
Fortunately, children
does that work for us:
var children = require('kurly/children')
var ast = parse('{greeting, {kurly}}')
var template = compile(ast, {
greeting: (ctx, rec) => () => ['Hello'].concat(children(ctx, rec)),
kurly: () => () => 'World!'
})
var result = template() // ['Hello', ', ', 'World!']
children()
is written in such a way, that it can be used both
from static and dynamic tags alike:
var children = require('kurly/children')
var ast = parse('{greeting, {kurly}}')
var template = compile(ast, {
greeting: (ctx) => (rec) => ['Hello'].concat(children(ctx, rec)),
kurly: () => () => 'World!'
})
var result = template() // ['Hello', ', ', 'World!']
You can register a wildcard / catch-all tag under the name '*'
that will
be called for everything that matches the tag syntax, but for which no
registered tag was found:
var ast = parse('{a}, {b}, {c}.')
var catchAll = ({name}) => ({greet}) => `${greet} ${name}`
var template = compile(ast, { '*': catchAll })
var result = template({ greet: 'Hi' })
// result: ['Hi a', ', ', 'Hi b', ', ', 'Hi c', '.']
A tag may return just about anything. Eventually, all the return values of all the tags will end up in a flattened array, which is returned by the template function, together with all the unmatched text, in the right order.
If you need the end result to be a string and all your tags are returning (arrays of) strings, you can convert the template result to a string like this:
result = result.join('')
Tags come in two flavours: dynamic tags, which have access to the dynamic record object
that is passed to the template function returned by compile
, and static tags, which don't need / have access to the dynamic record, but instead only have access to a static version of that record.
type Tag = DynamicTag | StaticTag
With tag
, you can 'upgrade' a Node
to a PipeNode
:
function tag(pipe: Pipe, rec?: object, parent?: TagFn): TagFn
Compiles a Pipe
into a single TagFn
pipe
: Pipe
The pipe to compile
Optional static record object
Optional parent TagFn
A function that accepts an ast Node
and returns a dynamic tag function.
type DynamicTag = (ctx: Node) => DynamicTagFn
A function that accepts an ast Node
and an optional static
record object and returns a static tag function.
type StaticTag = (ctx: Node, rec?: object) => StaticTagFn
kurly
parses and finds tags during the parse
phase and builds
an ast. Then during compile
it replaces the tags it found in the
ast with the tag functions it was given.
These tag functions are either static or dynamic tag functions.
DynamicTag
s return a DynamicTagFn
and StaticTag
s return a StaticTagFn
.
export type TagFn = DynamicTagFn | StaticTagFn
A function that accepts a dynamic record object and returns some output.
type DynamicTagFn = (rec: object) => any
A function that accepts no arguments and optionally uses a static record object in it's output only.
type StaticTagFn = () => any
Represents a possible tag. It includes fields to store the tag
open
marker, it's name
, any sep
whitespace, the tag content
text
, the close
marker and a parsed ast
of it's content text.
interface Node extends Object {
open: string,
name: string,
sep: string,
text: string,
close: string,
ast: Ast,
}
See also: Ast
A node that is 'instantiated'. That is, a Tag
was found that matched
it's name (or a wildcard tag was found) and that tag was invoked to
create a TagFn
, stored in property tag
on the node.
interface PipeNode extends Node {
tag: TagFn,
}
An 'instantiated' Ast
, where all nodes have a populated tag
field.
type Pipe = Array<PipeNode | string>
Use pipe()
to create pipes:
function pipe(ast: Ast, tags: Tags, rec?: object): Pipe
Creates a Pipe
from an ast.
Instead of using compile
, to compile an ast directly
into a TagFn
, you can use pipe
to create
a Pipe
, which is an array of strings or PipeNode
s.
You can then call tag()
on that pipe to get basically the same result as
you would have gotten from compile()
, or you can choose to do something
else entirely with that pipe.
ast
: Ast
The ast to create a Pipe
from.
tags
: Tags
The tags to use in the Pipe
.
Optional static record object
Add an issue in the issue tracker to let me know of any problems you find, or questions you may have.
Copyright 2021 by Stijn de Witt.
Licensed under the MIT Open Source license.
The GZIP algorithm is available in different flavours and with different possible compression settings. The sizes quoted in this README have been measured using gzip-size by Sindre Sorhus, your mileage may vary.