jump
is a templating language for Python.
import jump
template = '''
@for user, lang in greetings
@if lang == 'en'
Hello, {user}!
@elif lang == 'tlh'
Qapla, {user}!
@end
@end
'''
args = {
'greetings': {
'Jadzia': 'en',
'Quark': 'en',
'Worf': 'tlh',
}
}
print(jump.render(template, args))
Hello, Jadzia!
Hello, Quark!
Qapla, Worf!
A jump
template consists of plain text, expressions, or "echoes", enclosed in {}
and "commands", or statements, which start with a @
. Lines starting with @#
are considered comments and ignored.
@# test -- this is a comment
@let foo = 0 -- this is a command
<h1>{@include header}</h1> -- this is an inline command
Hello, {person.name} -- this is an echo
A command always starts with a word, and there should be no whitespace after a @
. If @
occurs elsewhere, it's not special. {
is only special if followed by a non-whitespace, other occurrences are not special. In "parsed" positions, @
, {
and }
can be escaped by doubling them:
@@escaped command
no need to escape some@email
this is {{escaped}}
no need to escape { this }
@escaped command
no need to escape some@email
this is {escaped}
no need to escape { this }
You can easily change the delimiters @
and {}
to something else (see "options" below).
Commands can start on a new line, like in Python, or inline, mixed with plain text:
@# line command
@print 'Quark'
@# inline command
<h1>{@print 'Rom'}</h1>
Quark
<h1>Rom</h1>
"Block" commands, like if
or for
, can span across multiple lines and are closed with @end
, optionally followed by the command name:
@if enemy
red alert
lock phasers
@end if
all hands {@if enemy} to battlestations {@else} dismissed {@end}
jump
expressions are similar to Python expressions. This is what is supported:
- names like
foo
and attributes likefoo.bar
- strings, numbers, list and dict literals
- subscripts and slices
- arithmetic, boolean and comparison operators
- conditional operator
...if...else...
- function calls with keyword and star arguments
- the pipe or "filter" operator
expression | function
Commas in list/dict literals and argument lists are optional.
jump
is picky about whitespace in expressions. Binary operators must have equal amount of whitespace on both sides, there must be no whitespace after unary +
and -
, no whitespace in star and keyword arguments and no whitespace before a (
or [
in function calls and indexes. To put it simply, just stick to PEP8 all the time.
a + b, a+b - ok
a+ b - not ok
-a + 1 - ok
- a + 1 - not ok
foo(*a, **kw, bar=1) - ok
foo(* a, ** kw, bar = 1) - not ok
fun(11), lst[12] - ok
fun (11), lst [12] - not ok
Names (identifiers) in expressions can reference python builtins, arguments (passed to the template) or local variables (defined in the template). Dot syntax object.property
picks an object attribute or a dict key:
@# built-in
@print len('hi')
@# arguments
{alert} alert, all hands {personnel.status}
@# local variable
@let names = ['Jadzia' 'Julian' 'Kira']
@print names|spaces
{
'alert': 'yellow',
'personnel': {
'status': 'standby',
}
}
2
yellow alert, all hands standby
Jadzia Julian Kira
Echoes are expressions enclosed in {}
. When rendering a template, jump
replaces an echo with its value.
{person.name.first}'s debt = {person.income - sum(person.expenses)}
{
'person': {
'name': {'first': 'Quark'},
'income': 400,
'expenses': [100, 200, 300],
}
}
Quark's debt = -200
An echo expression can be followed by a Python format specifier, starting with a :
or a !
:
{amount:,} strips = {amount/20 :.5f} bars
{'amount': 123456.7}
123,456.7 strips = 6172.83500 bars
The pipe operator |
runs an expression through a "filter". In the simplest case, a filter is just a function name (predefined or defined in the template).
<h1> {title | html} </h1>
{'title': 'Profit & Lace'}
<h1> Profit & Lace </h1>
A filter can be also a function call, in which case the expression is injected as its first argument.
@print couple | str.replace('Jadzia', 'Ezri')
{'couple': 'Jadzia & Worf'}
Ezri & Worf
Filters can be chained:
@print people | sort | commas | upper
{'people': ['Quark', 'Miles', 'Julian', 'Kira']}
JULIAN,KIRA,MILES,QUARK
Any built-in or locally defined function can act as a filter:
@def mirror x = ''.join(reversed(x))
{'quark' | len}
{'quark' | mirror}
5
krauq
With the option filter
you can define the default filter, which is applied to all echoes, unless they are marked as safe
:
@option filter = 'html'
<h1>{title}</h1>
<h2>{sub}</h2>
{body | safe}
{
'title': 'Rocks & Shoals',
'sub': 'Episode <2>',
'body': '<b>Garak</b> and <b>Keevan</b>',
}
<h1>Rocks & Shoals</h1>
<h2>Episode <2></h2>
<b>Garak</b> and <b>Keevan</b>
jump
comes with a set of built-in filters:
filter | input | output |
---|---|---|
as_int |
{40 + ("2" | as_int)} |
42 |
as_float |
{40 + ("2e5" | as_float)} |
200040.0 |
as_str |
{bytes.fromhex('66c3bcc39f6368656e') | as_str} |
fĂĽĂźchen |
commas |
{['no' 'funny' 'stuff'] | commas} |
no,funny,stuff |
cut |
{'yoknapatawpha' | cut(3, ' etc.')} |
yok etc. |
h |
{'<b>hi</b>' | h} |
<b>hi</b> |
html |
{'<b>hi</b>' | html} |
<b>hi</b> |
join |
{[11 22 33] | join(':')} |
11:22:33 |
json |
{'fĂĽĂźchen' | json} |
"f\u00fc\u00dfchen" |
lines |
{'one \n two \n three' | lines(strip=True) | join('=')} |
one=two=three |
linkify |
{'see http://google.com' | linkify(target='_blank')} |
see <a href="http://google.com" target="_blank">http://google.com</a> |
lower |
{'HELLO' | lower} |
hello |
nl2br |
{'one\ntwo\nthree' | nl2br} |
one<br/>two<br/>three |
shorten |
{'yoknapatawpha' | shorten(6, '...')} |
yok...pha |
sort |
{'QUARK' | sort | join} |
AKQRU |
spaces |
{['no' 'funny' 'stuff'] | spaces} |
no funny stuff |
split |
{'1/2/3' | split('/') | join('.')} |
1.2.3 |
strip |
<{' xyz ' | strip}> |
<xyz> |
titlecase |
{'hi there' | titlecase} |
Hi There |
unhtml |
{'<b>' | unhtml} |
<b> |
upper |
{'hello' | upper} |
HELLO |
Conditional output. The syntax is
@if expression
text
@elif expression
text
@elif expression
text
@else
text
@end
elif
and else
are optional.
@if race == 'human'
Hello
@elif race == 'klingon'
Qapla
@end
{'race': 'klingon'}
Qapla
Loop construct.
@for variables in expression
text
@end
An extra clause index someVariable
stores the loop index (1-based) in a variable. An extra clause length someVariable
stores the overall length of the iterable. If two loop variables are given and the loop expression happens to be a dict, its items
are iterated automatically. An optional else
block is rendered when a loop expression is empty:
@for name, friends in people index num length total
Member {num} of {total}: {name}
@for f in friends
- friend {f}
@else
- no friends!
@end
@end
{
'people': {
'Kira': ['Odo'],
'Jadzia': ['Julian', 'Worf'],
'Quark': None,
}
}
Member 1 of 3: Kira
- friend Odo
Member 2 of 3: Jadzia
- friend Julian
- friend Worf
Member 3 of 3: Quark
- no friends!
break
and continue
are also supported:
@for number in '12345678'
@if number == '3'
@continue
@elif number == '6'
@break
@else
{number}
@end
@end
1
2
4
5
Conditionally renders a block if the expression is not "empty" (undefined, whitespace-only string, an empty list or dict). A complex expression can be aliased with as variable
. An optional else
block is rendered for empty expressions. Undefined variables and properties in the with
expression are considered "empty" and no error is raised:
@for ship in ships
{ship.name}
@with ship.properties.physical.weight as w
weight {w}
@else
weight unknown
@end
@end
{
'ships': [
{'name': 'Defiant', 'properties': {
'physical': {'weight': 1234}
}},
{'name': 'Valiant'},
]
}
Defiant
weight 1234
Valiant
weight unknown
@without
is the same as @with
, but the condition is inverted:
@without messages
No messages!
@end
{'messages': []}
No messages!
Defines a function. A function definition can be written in a block form:
@def name arguments
content
@end
or as an expression:
@def name arguments = expression
Parentheses around arguments are optional. Arguments can be separated by whitespace or commas. The result of a block function is its content, unless there is an explicit @return
command. The result of an expression function is the evaluated expression.
Once defined, a function can be called as an ordinary python function, or as a single-line @
command, or used as a filter:
@def square n = n * n
{42 + square(10)}
@def banner(text open close)
{open * 3} {text} {close * 3}
@end
@banner 'red alert' open='!' close='*'
@def translate(text)
@if text == 'Hello'
@return 'Qapla'
@end
@end
Worf says: {'Hello' | translate}
142
!!! red alert ***
Worf says: Qapla
Defines a "box" function. Box functions are similar to def
functions, but can be used as block commands. When such a command is used, the content until the respective @end
is captured, evaluated and passed as a first argument to the function:
@box header(text)
<h1> !!! {text | strip} !!! </h1>
@end
@header
Attention citizens
@end
<h1> !!! Attention citizens !!! </h1>
Box functions can accept other arguments as well:
@box header2(text className symbol)
<h2 class="{className}"> {symbol*3} {text|strip} {symbol*3} </h2>
@end
@header2 'red' symbol='*'
Stand by for an update
@end
<h2 class="red"> *** Stand by for an update *** </h2>
"Macro" commands mdef
and mbox
are similar to def
and box
, but their arguments are passed as is, without parsing. These commands can be used to implement custom mini-languages within jump
templates.
@# definitions
@import subprocess
@mdef bash(command)
@print subprocess.check_output(command, shell=True) | as_str
@end
@mbox javascript(source)
@do open('/tmp/js', 'wt').write(source)
@print subprocess.check_output(['node', '/tmp/js']) | as_str
@end
@# usage
@bash cat /usr/share/dict/words | grep jump$ | tr -s '\n' ','
@javascript
const lang = 'javascript'
console.log(`hello from ${lang}`.toUpperCase())
@end javascript
buckjump,gelandejump,jump,outjump,overjump,
HELLO FROM JAVASCRIPT
@return expression
returns an expression as a result of a def
or box
function. If you return None
, nothing will be rendered:
@def div a, b
@if b == 0
@return
@end
@return a / b
@end
@div 200 100
@div 200 0
@div 500 100
2.0
5.0
When used at the top level, @return
terminates the template, discards all evaluated content so far and returns its argument to the caller:
some text...
@if error
@return 'no way!'
@end
more text...
{'error': True}
no way!
Adds a new local variable. Like @def
, can have a block form:
@let variable
text
@end
or an expression form:
@let variable = expression
@let list of variables = list of expressions
@let number = 5
@let race = 'klingon'
@let c1 c2 = 100 200
@let message
{number} {race} ships detected, heading {c1}-mark-{c2}
@end
The message was: {message | strip}
The message was: 5 klingon ships detected, heading 100-mark-200
Inserts raw python code. The indentation doesn't have to match the outer level, but has to be consistent within a block. print
emits the content to the template output. Template arguments can be accessed via the ARGS
dict.
<div>
@code
a = ARGS['first']
b = ARGS['second']
if a > b:
print(a, 'is smarter than', b)
else:
print(a, 'is no smarter than', b)
@end
</div>
{'first': 'Quark', 'second': 'Rom' }
<div>
Quark is no smarter than Rom
</div>
@import module
imports a python module into the template
@import sys
This is python {sys.version}
This is python 3.9.13 (main, May 24 2022, 21:13:51)
[Clang 13.1.6 (clang-1316.0.21.2)]
@do expression
evaluates an expression and discards the result. Useful for side effects:
@let lst = [1 2 3]
@do lst.append(9)
{lst | commas}
1,2,3,9
@print expression, expression...
evaluates and prints expressions:
@print 'test:', 2+2, 'should be', 10 // 2
test: 4 should be 5
@include path
includes another template. The path argument, unless starts with a /
, is relative to the current template path:
@include sub/directory/other-template
Template loading can be customized by passing the loader
option. A loader
is a function which accepts the current template path and the include path and is expected to return a tuple (source_text, resolved_path)
:
loader(template_path: str, include_path: str) -> Tuple(str, str)
@quote name
returns the unparsed text until @end name
is encountered. name
can be omitted if there are no other @end
s in the text.
Try this:
@quote test
@if expression
{variable}
@end
@end test
{@quote}{no}{escaping}{needed}{@end}
Try this:
@if expression
{variable}
@end
{no}{escaping}{needed}
@skip name
ignores the text until @end name
is encountered. name
can be omitted if there are no commands in the text.
Quark
Julian
@skip
Jadzia
@end
Ezri
Quark
Julian
Ezri
@option name = value
sets a compile-time option for this template (see "options" below):
@option filter = 'html'
{text}
@option filter = None
{text}
{'text': 'this & that' }
this & that
this & that
jump.render(text: str, args: dict = None, error: Callable = None, **options) -> Any
jump.render_path(path: PathLike, args: dict = None, error: Callable = None, **options) -> Any
render/render_path
accept a template source or a path and return the template output. The output is normally a str
, but can also be something else if the template has a @return
at the top level.
An optional error
callback is invoked when a runtime error occurs in the template. The signature of the callback is
error(exc: Exception, source_path: str, source_lineno: int, env)
where env
is the jump runtime environment object. You can use env.print()
to emit error messages in the template output and env.ARGS
to access the arguments. If the callback did not raise an exception, the template is evaluated further.
import jump
def log_error(exc, path, line, env):
env.print('<ERROR', exc, ', count is', env.ARGS.get('count'), end='>')
template = '''\
first line
undefined {foo} here
next line
runtime error {100 / count} here
next line
'''
print(jump.render(template, {'count': 0}, error=log_error))
first line
undefined <ERROR name 'foo' is not defined , count is 0> here
next line
runtime error <ERROR division by zero , count is 0> here
next line
The core of jump
is its Engine
. It provides runtime support for templates and contains the built-in filters.
You can obtain the default Engine
object by calling jump.engine()
. All jump
public functions are actually methods of the default Engine
:
jump.render(tpl, args)
# is the same as
jump.engine().render(tpl, args)
You can extend the default Engine
to add new filters or commands. In this case, render
and friends must be called as methods of your engine.
import jump
class MyEngine(jump.Engine):
def box_my_custom_command(self, text):
return '((( ' + text.strip() + ' )))'
template = '''
@my_custom_command
hey
@end
'''
eng = MyEngine()
print(eng.render(template))
((( hey )))
Internally, jump
templates are compiled to python functions. The following compiler API is available:
jump.parse(text: str, **options) -> object
jump.parse_path(path: PathLike, **options) -> object
jump.translate(text: str, **options) -> str
jump.translate_path(path: PathLike, **options) -> str
jump.compile(text: str, **options) -> Callable
jump.compile_path(path: PathLike, **options) -> Callable
parse
, translate
and compile
compile the template into an AST, python source code and a function object respectively.
The signature of the compiled function is
template_fn(engine: jump.Engine, args: dict=None, error: Callable=None) -> Any
It can be invoked directly, or using the call
shortcut:
# compile once
cached_template_fn = jump.compile_path('some.template')
# invoke with the default Engine
output = cached_template_fn(jump.engine(), args, error_handler)
# or
output = jump.call(cached_template_fn, args, error_handler)
# invoke with the custom Engine
eng = MyEngine()
output = cached_template_fn(eng, args, error_handler)
# or
output = eng.call(my_template_fn, args, error_handler)
The APIs accept a variety of options, which affect how templates are compiled:
option | default value | |
---|---|---|
path |
override the default path for the template | '<string>' |
name |
name for the compiled function | '_RENDER_' |
filter |
default filter function | None |
loader |
template loader | None |
strip |
remove leading and trailing whitespace from each text line | False |
escapes |
a space-separated sting "escape replacement escape replacement..." | '@@ @ {{ { }} }' |
comment_symbol |
a string that starts a comment | '@#' |
command_symbol |
a string that starts a line command | '@' |
inline_open_symbol |
a string that starts an inline command | '{@' |
inline_close_symbol |
a string that ends an inline command | '}' |
inline_start_whitespace |
whether a space is allowed after the inline open symbol | False |
echo_open_symbol |
a string that starts an echo | '{' |
echo_close_symbol |
a string that ends an echo | '}' |
echo_start_whitespace |
whether a space is allowed after the echo open symbol | False |
...symbol
options can be used to change jump
syntax:
alt_syntax = {
'command_symbol': '# ',
'echo_open_symbol': '<%=',
'echo_close_symbol': '%>',
'echo_start_whitespace': True,
}
template = '''
# for name in ['Quark', 'Jadzia', 'Miles']
Hello, <%= name %>
# end
'''
print(jump.render(template, **alt_syntax))
Hello, Quark
Hello, Jadzia
Hello, Miles
(c) 2022 Georg Barikin (https://github.com/gebrkn). MIT license.