'Cause every pirate should have Beard.
— a random pirate with a beard
Github: http://github.com/ITikhonov/beard
In the time of XMLHttpRequest need arise for templates to be used both on server-side and client-side.
It's not hard when you use same languageJavaScript on both sides.
But what to do if you are not fond of Node.js?
Well, you need a cross-language template engine! For now Beard can be used from Python and Javascript, but it's so simple that writing support for another language is a piece of cake.
Download beard-0.8.zip.
Execute easy_install beard-0.8.zip.
For JavaScript part you'll also need beard.js.
Current implementation uses compiled templates. This means that beard template file compiled into python .py and JavaScript .js file.
To compile quick.beard, run
python -m beard.beard quick.beard
This will produce quick.py and quick.js.
Now from python:
-import beard -import quick -print beard.render(quick.template,{'some':'data'})
From HTML:
-<html> -<head> -<script src="beard.js"></script> -<script src="quick.js"></script> -</head> -<body><pre></pre></body> -<script> - document.body.firstChild.innerHTML=Beard.render('quick',{some:'data'}); -</script> -</html>
>quick.beard.init -Substition: ##variable## -If block: ##@variable## - only show when true - #### -If..else: ##@variable## - only show when true - ##*## - only show when false - #### -If not block: ##!variable## - only show when false - #### -If not else: ##!variable## - only show when false - ##*## - only show when true - #### -Choice: ##one?variable## - only show when variable is "one" - ##two?## - note, no variable specified - only show when variable is "two" - ##*## - show when variable is something else - #### -Blocks: ##blockvar=blockname## - This block is called with ##blockvar## - #### -Block call: ##variable>blockname## -For..in: ##item:list## - ##item## - #### -Nested data: ##dictvar.subdict.item## -Literal #: ###hexcolor## is not what you want (it's beard, then #hexcolor, then beard). - ##\####variable## will produce something like #ffffff. -Host language blocks: -##{py## -def block_custom(data,value,output,getvalue,compare): - """ - data is a dictionary with template variables - value is a value of variable passed with block call - output(x) converts x into string and writes it into template - getvalue(x) will get value of x template variable - compare(x,y) is cross-language comparison function - """ - output(len(data)) -#### -Roughly the same in JavaScript: -##{js## -function block_custom(data,value,output,getvalue,compare) { - // the very same as above - var n=0; - for(var i in data) n++; - output(n); -} -#### -And now call it as ordinary block: ##variable>custom## -Yes, that's all in a single template file
quick.beard.init
-You can even do weird things with host blocks: -##{py## -def block_init(data,value,**kw): - data['variable']='VARIABLE' - data['list']=[1,2,3] - data['dictvar']={'subdict':{'item':'NESTED ITEM'}} -#### -##{js## -function block_init(data) { - data['variable']='VARIABLE'; - data['list']=[1,2,3]; - data['dictvar']={'subdict':{'item':'NESTED ITEM'}}; -} -#### -##>init##
Ahoy! Here comes Beard! Instead of abandoning logic on a religious crusade for purity of ideas like our Mustache-wearing comrades, Beard design is dictated by practice.
To support expressions (like in {% if x==1 or y in "abc" %}), you need a language. You can't resort to some simple half-assed hack by transforming it with clever regexp into host language expression (like in underscore), because you have at least TWO host languages.
Writing expressive expression languages is a tricky thing. And you need to do it for each host language you going to support (so, at least twice).
Beard avoids this by having no expressions. Beard is computation-less (well, Mustache is cleary too, not sure how and why they come up with their illogical motto).
But what Beard has? Beard has conditional rendering, including switch, iteration over lists and parametrized subtemplates.
Also it allows to define computations in several host languages all in one template file. See? Beard don't try to force you into facial-features related religion, the ultimate goal of Beard is to conserve your precious time.
Beard template accepts single JSON object as input. It means only strings, numbers, arrays and nested objects are guaranteed to work consitently cross- language.
example-data
-{ - 'booltrue' : 1==1, - 'boolfalse' : 1==2, - 'zero' : 0, - 'nonzero' : -1, - 'string' : 'this is a string', - 'emptystring' : '', - 'list' : [1,2,3], - 'emptylist' : [], - 'object' : {'field':'field-value'}, -}
Core syntax of Beard is very simple. It's a beard made of two hashes (number or pound signs, depending on where are you from).
Here is the beard:
##
tokenize
-def tokenize(t): - return t.split('##')
So when you want to instruct Beard to do something, you enclose command into two beards, two hashes each.
To insert variable, use beard then variable name, then beard again:
example
-Here, 'this is a string' without quotes is shown: ##string##
Instead of non-existent variables nothing is substuted.
example
-Just two quotes: '##nonexistent##'
Substition for anything except variable, containing a string is undefined behaviour.
Nested objects can be addressed with conventional dot-syntax:
example
-Object field: ##object.field## -Non-existig object field: ##notexistent.field##
generate.substition
-gen.ivar(token)
To only output things when variable exists and is true, non-empty list, non-zero or non-empty
string, put ##@variable##
before these things and end them with ####
.
Note, that operation on objects is undefined.
example
-If true: - ##@booltrue## - shown boolean true - #### - ##@nonzero## - shown non-zero - #### - ##@list## - shown non-empty list - #### - ##@string## - shown non-empty string - #### - ##@boolfalse## - NOT SHOWN (boolfalse) - #### - ##@zero## - NOT SHOWN (zero) - #### - ##@emptylist## - NOT SHOWN (emptylist) - #### - ##@emptystring## - NOT SHOWN (emptystring) - #### - ##@nonexistent## - NOT SHOWN (nonexistent) - ####
generate.truth
-elif token.startswith('@'): - gen.iiftrue(token[1:]) - stack.append(['if'])
To only output things when variable does not exists, is false, zero, empty list
or empty string, put ##!variable##
before these things and
end them with ####
.
Note, that operation on objects is undefined.
example
-If false: - ##!booltrue## - NOT SHOWN (booltrue) - #### - ##!nonzero## - NOT SHOW (nonzero) - #### - ##!list## - NOT SHOWN (list) - #### - ##!string## - NOT SHOWN (string) - #### - ##!boolfalse## - shown boolean false - #### - ##!zero## - shown zero - #### - ##!emptylist## - shown empty list - #### - ##!emptystring## - shown empty string - #### - ##!nonexistent## - shown non-existent - ####
generate.false
-elif token.startswith('!'): - gen.iiffalse(token[1:]) - stack.append(['if'])
If you want to choose what to render depending on value of variable, use question mark.
First choice define what variable will be used for
comparison: ##first?variable##
.
Subsequent choices DO NOT specify variable they just ask a
question: ##second?##
, ##third?##
Last question could be ##*##
to match anything.
Whole thing ends with ####
.
Behaviour of choice on anything except strings and numbers is undefined.
example
-Choice: - ##apple?string## - NOT SHOWN - ##nut?## - NOT SHOWN - ##?## - NOT SHOWN - ##*## - shown - #### - ##apple?emptystring## - NOT SHOWN - ##nut?## - NOT SHOWN - ##?## - shown - ##*## - NOT SHOWN - #### - ##apple?string## - NOT SHOWN - ##nut?## - NOT SHOWN - ##?## - NOT SHOWN - ##this is a string?## - shown - ##*## - NOT SHOWN - #### - ##apple?nonzero## - NOT SHOWN - ##nut?## - NOT SHOWN - ##?## - NOT SHOWN - ##-1?## - shown - ##*## - NOT SHOWN - ####
generate.choice
-elif token.endswith('?'): - gen.ielif(stack[-1][1],token[:-1]) -elif '?' in token: - value,var=token.split('?') - gen.iif(var,value) - stack.append(['if',var]) -elif token=='*': - gen.ielse()
To iterate over array use colon like ##item:list##
. Everything
between this and relevant ####
will be repeated for each element
of array. This element will be bound to variable whose name is specified before
colon. This binding do not exist outside of iteration.
example
-Iteration: - ##i:list## - ##i## - #### - ##i:emptylist## - NOT SHOWN - ####
generate.iteration
-elif ':' in token: - var,lst=token.split(':') - gen.iiter(var,lst) - stack.append(['for',var])
To define things, which will be repeated with small alteration, use
##binding=blockname##
. Everything between that and relevant
####
won't be shown right there, but when you write
##variable>blockname##
later.
Value of that variable
, specified when inserting block
can be accessed under name binding
.
You can omit binding
, variable
or both. Value
of binding in block when variable is omited is unspecified. If binding
is omited passed variable is not taken in account.
example
-Block: - ##value=block## - shown: ##value## - #### - ##i:list## - ##i>block## - #### - ##string>block## - ##nonexistent>block## - ##>block## - ##=repeat## - shown - #### - ##>repeat##
generate.block
-elif '=' in token: - var,name=token.split('=') - gen.iblock(name,var) - stack.append(['block',var]) -elif '>' in token: - var,block=token.split('>') - gen.icall(block,var)
Sometimes you need more then Beard can natively provide and you need a power of host language. You can do that by writing blocks in host languages, just note you'll probably need to write own implementation for each of them.
Host language blocks are executed just like ordinary template
blocks with ##variable>blockname##
syntax.
In both Python and JavaScript, block is a function with 'block_' prefix, rest is a block name to be referenced from template.
This function receives 5 arguments: data, value, output, getvalue, compare.
example
-Host language block: -##{py## -def block_example(data,value,output,getvalue,compare): >example-block-py -#### -##{js## -function block_example(data,value,output,getvalue,compare) { >example-block-js -} -#### -##string>example##
1st argument (data) is a current bindings. It's what you passed into template for rendering, except it may be slightly altered by interations and block calls. This bindings are mutable, so you can change them and this will affect template for the rest of it's life.
example-block-py
- data['changed']="changed in block"
example-block-js
- data.changed="changed in block";
2nd argument (value) is a value of variable, passed when block is
inserted with ##variable>block##
. What happens if you change
this value is undefined in general, but changing objects and arrays will
work in both JavaScript and Python with some care.
example-block-py
- data['example_value']=value
example-block-js
- data.example_value=value;
3rd argument (output) is a callable, which accepts single argument convert it into string and insert into template at relevant point.
example-block-py
- output(value)
example-block-js
- output(value);
4th argument (getvalue) is a callable, which accepts two arguments returns value of specified binding in data. First argument is binding object (data) to search in, and second is variable name to search for.
example-block-py
- output(getvalue(data,value))
example-block-js
- output(getvalue(data,value));
5th argument (compare) is a generic comparison function of two arguments, consistent across host languages. Returns true if equal, false otherwise.
example-block-py
- output('equal' if compare(value,1) else 'different')
example-block-js
- output(compare(value,1)?'equal':'different')
Here is an example block, which capitalizes string, passed to it:
example
-##{py## -def block_uppercase(data,value,output,getvalue,compare): - output(value.upper()); -#### -##{js## -function block_uppercase(data,value,output) { - output(value.toUpperCase()); -} -#### - And now show in all caps: ##string>uppercase##
generate.code.start
-elif token.startswith('{'): - code=[] if gen.tag==token[1:] else False
generate.code.loop
-if code is not None: - if i%2 and token=='': - if code: gen.icode(''.join(code)) - code=None - else: - if type(code)==list : code.append(token) - continue
And last, but not least. Sometimes you have a hash symbol interfering with beard. Like in this CSS:
example
-False beard: - ###string## -
It won't resolve into value of variable string
with
preceding hash, instead it will try to insert value of
variable #string
(###string##
).
To overcome this, insert offending hash with ugly ##\##
.
example
-True beard - ##\####string##
generate
-def generate(gen,tokens): - stack=[] - code=None - pos=0 - for i in range(len(tokens)): - token=tokens[i] - pos+=len(token)+2 > generate.code.loop - if i%2: - if token=='\\': - gen.ilit('#') > generate.truth > generate.false > generate.code.start > generate.choice > generate.block > generate.iteration - elif token=='': - gen.pop(stack.pop()) - else: > generate.substition - else: - if token!='': gen.ilit(token) - return gen.text()
jsgenerator
-class JavascriptGenerator(object): - def __init__(_,name): - _.tag='js' - _.code=[ - "Beard.template[%s]=function(data,value,output,getvalue,compare) {"%(repr(name)), - " function _istrue(x) { if(x && x.length!=undefined) return x.length>0; return x; }" - ] - _.indent=" " - def c(_,p,*args): - _.code.append(_.indent+(p%args)) - def c0(_,p,*args): - _.code.append(_.indent[:-4]+(p%args)) - def text(_): - return '\n'.join(_.code+['}']) - def getvalue(_,x): - return 'getvalue(data,%s)'%(repr(x),) - def icall(_,block,var): - _.c("(%s?%s:block_%s)(data,%s,output,getvalue,compare);",_.getvalue(block),_.getvalue(block),block,_.getvalue(var)) - def ielif(_,var,value): - _.c0("} else if(compare(%s,%s)) {",repr(value),_.getvalue(var)) - def iiter(_,var,lst): - _.c("for(var arr=%s,idx=0;idx<arr.length;idx++) {",_.getvalue(lst)) - _.indent+=' ' - _.c("var hold=data[%s]; ",repr(var)) - _.c("data[%s]=arr[idx]; ",repr(var)) - def iblock(_,name,var): - _.c("function block_%s(data,value) {",name) - _.indent+=' ' - _.c("var hold=data[%s];",repr(var)) - _.c("data[%s]=value;",repr(var)) - def iif(_,var,value): - _.c("if(compare(%s,%s)) {",repr(value),_.getvalue(var)) - _.indent+=' ' - def iiftrue(_,var): - _.c("if(_istrue(%s)) {",_.getvalue(var)) - _.indent+=' ' - def iiffalse(_,var): - _.c("if(!_istrue(%s)) {",_.getvalue(var)) - _.indent+=' ' - def ielse(_): - _.c0("} else {") - def ivar(_,var): - _.c("output(%s);",_.getvalue(var)) - def ilit(_,text): - _.c("output(%s);",repr(text)) - def icode(_,code): - _.code.insert(1,code) - def pop(_,what): - if what[0]=='for':_.c("if(hold!=undefined) data[%s]=hold;",repr(what[1])) - elif what[0]=='block': _.c("if(hold!=undefined) data[%s]=hold",repr(what[1])) - _.indent=_.indent[:-4] - _.c("}")
generator
-class PythonGenerator(object): - def __init__(_): - _.nest=0 - _.tag='py' - _.code=[ - "import beard", - "def template(data,value=None,output=beard.output,getvalue=beard.getvalue,compare=beard.compare):" - ] - _.indent=" " - def c(_,p,*args): - _.code.append(_.indent+(p%args)) - def c0(_,p,*args): - _.code.append(_.indent[:-4]+(p%args)) - def text(_): - return '\n'.join(_.code) - def getvalue(_,x): - return 'getvalue(data,%s)'%(repr(x),) - def icall(_,block,var): - _.c("(%s if %s else block_%s)(data,%s,output=output,getvalue=getvalue,compare=compare)", - _.getvalue(block),_.getvalue(block),block,_.getvalue(var) if var else 'None') - def ielif(_,var,value): - _.c0("elif compare(%s,%s):",repr(value),_.getvalue(var)) - def iiter(_,var,lst): - _.c("hold%u=data.get(%s)",_.nest,repr(var)); - _.c("for var%u in %s:",_.nest,_.getvalue(lst)) - _.indent+=' ' - _.c("data[%s]=var%u",repr(var),_.nest); - _.nest+=1 - def iblock(_,name,var): - _.c("def block_%s(data,value,**kw):",name) - _.indent+=' ' - if var: - _.c("hold=data.get(%s)",repr(var)) - _.c("data[%s]=value",repr(var)) - def iif(_,var,value): - _.c("if compare(%s,%s):",repr(value),_.getvalue(var)) - _.indent+=' ' - def iiftrue(_,var): - _.c("if %s:",_.getvalue(var)) - _.indent+=' ' - def iiffalse(_,var): - _.c("if not %s:",_.getvalue(var)) - _.indent+=' ' - def ielse(_): - _.c0("else:") - def ivar(_,var): - _.c("output(%s)",_.getvalue(var)) - def ilit(_,text): - _.c("output(%s)",repr(text)) - def icode(_,code): - _.code.insert(1,code) - def pop(_,what): - if what[0]=='for': - _.nest-=1 - _.c("if hold%u is not None: data[%s]=hold%u",_.nest,repr(what[1]),_.nest) - elif what[0]=='block': - if what[1]: - _.c("if hold is not None: data[%s]=hold",repr(what[1])) - _.indent=_.indent[:-4]
defaults
-import sys -def output(x): - if x is not None: - sys.stdout.write(str(x)) -def getvalue(data,x): - vs=x.split('.') - v=data - for y in vs: - v=v.get(y) - if v is None: return None - return v -def compare(x,y): - if repr(x)==repr(y): return True - return False
source
-def source(text,gen): - tokens=tokenize(text) - return generate(gen,tokens)
compile
-def compile(text): - template=source(text,PythonGenerator()) - g={} - l={} - exec(template,g,l) - compiled=l['template'] - compiled.func_globals.update(l) - return compiled
render
-def render(template,data): - s=[] - def output(x): - if x is not None: s.append(str(x)) - template(data,output=output) - return ''.join(s)
>tokenize >generator >jsgenerator >source >generate >defaults >render >compile -def main(argv): - if argv[1]=='--test': return test() - text=open(argv[1]).read() - tokens=tokenize(text) - name=argv[1][:-6] if argv[1].endswith('.beard') else argv[1] - f=open(name+'.py','w') - template=generate(PythonGenerator(),tokens) - f.write(template) - f.close() - f=open(name+'.js','w') - template=generate(JavascriptGenerator(name),tokens) - f.write(template) - f.close() -def test(): - from sys import argv - from importlib import import_module - m=import_module(argv[2]) - - m.template({ - 'simple_var': 'Simple Variable', - 'simple_cond_var': True, - 'list_var': ['left','center','right','unknown'], - 'ext_block': lambda data,x,output,**kw: output("External block received '%s' value"%(x,)), - 'nestlist_var': [{'cond':'left'},{'cond':'center'},{'cond':'right'},{'cond':'unknown'}], - }) -if __name__=='__main__': - from sys import argv - main(argv)
-Beard={ - template: {}, - - getvalue: function(data,x) { - var vs=x.split('.'); - var v=data; - var i; - for(i=0;i<vs.length;i++) { - y=v[vs[i]]; - if(y==undefined) return; - v=y; - } - return v; - }, - - compare: function(x,y) { return x==y; }, - - render: function(name,data) { - var html=[]; - function output(x) { html.push(x); } - Beard.template[name](data,undefined,output,this.getvalue,this.compare); - return html.join(''); - } -};
-name=arguments[0]; -load("beard.js"); -load(name+'.js'); -Beard.render(name,{ - 'simple_var': 'Simple Variable', - 'simple_cond_var': true, - 'list_var': ['left','center','right','unknown'], - 'ext_block': function(data,x,output) { output("External block received '"+x+"' value"); }, - 'nestlist_var': [{'cond':'left'},{'cond':'center'},{'cond':'right'},{'cond':'unknown'}], -});
-##{py## -def block_init(data,value,**kw): - data.update( > example-data - ) -#### -##{js## -function block_init(data) { - var test = ( > example-data - ); - for(var i in test) { - data[i]=test[i]; - } -} -#### -##>init## >example
-<html> -<head> - <title>Hello, world</title> -</head> -<body> - <div>Simple variable substition ##simple_var##</div> - - <div> - Simple boolean conditional - ##@simple_cond_var## - <div>Yes</div> - #### - ##!simple_cond_var## - <div>no</div> - #### - </div> - ##cond_var=block## - <div> - Choice conditional - ##left?cond_var## - <div style="text-align:left">Stay at Left</div> - ##center?## - <div> style="text-align:center">Hold Center</div> - ##right?## - <div> style="text-align:center">Lean to Right</div> - ##*## - <div> style="text-align:center">Dunno what to do with '##cond_var##'</div> - #### - </div> - #### - <div> - Iteration - ##i:list_var## - ##i>block## - #### - </div> - <div> - External blocks - ##i:list_var## - ##i>ext_block## - #### - </div> - ##v=nest_block## - <div> - Choice conditional on ##v.cond##. - ##left?v.cond## - <div style="text-align:left">Stay at Left</div> - ##center?## - <div> style="text-align:center">Hold Center</div> - ##right?## - <div> style="text-align:center">Lean to Right</div> - ##*## - <div> style="text-align:center">Dunno what to do with '##v.cond##'</div> - #### - </div> - #### - <div> - Dot-notation - ##i:nestlist_var## - ##i>nest_block## - #### - </div> -</body> -</html>
-from .beard import render,source,compile,main -from .beard import output,getvalue,compare -from .beard import JavascriptGenerator
-from django.template.loaders import app_directories -from django.template.base import TemplateDoesNotExist -import beard -class Template: - def __init__(self, source): - self.template = beard.compile(source) - - def render(self, context): - context_dict = {} - for d in context.dicts: - context_dict.update(d) - - return beard.render(self.template, context_dict) -class Loader(app_directories.Loader): - is_usable = True - def load_template_source(self, template_name, template_dirs=None): - if not template_name.endswith('.beard'): raise TemplateDoesNotExist(template_name) - return super(Loader, self).load_template_source(template_name, template_dirs) - def load_template(self, template_name, template_dirs=None): - source, origin = self.load_template_source(template_name, template_dirs) - template = Template(source) - return template, origin
-from django.core.files.base import ContentFile -from django.contrib.staticfiles.finders import AppDirectoriesFinder -from django.contrib.staticfiles.storage import AppStaticStorage,staticfiles_storage -from . import source,JavascriptGenerator -import os -import sys -from django.conf import settings -class BeardJSStorage(AppStaticStorage): - prefix = None - source_dir = 'templates' - - def __init__(self, app, *args, **kwargs): - super(BeardJSStorage, self).__init__(app, *args, **kwargs) - def _open(self, name, mode='rb'): - if not name.endswith('.js'): raise NotImplementedError() - if not name.startswith('beard/'): raise NotImplementedError() - if not mode.startswith('r'): raise NotImplementedError() - name=name[6:-3] - text = open(self.path(name+'.beard'), mode).read() - return ContentFile(source(text,JavascriptGenerator(name))) - def _save(self, name, content): - raise NotImplementedError() -class Finder(AppDirectoriesFinder): - storage_class = BeardJSStorage - def __init__(self, apps=None, *args, **kwargs): - super(Finder, self).__init__(apps,*args, **kwargs) - - def list(self, ignore_patterns): - beards=[] - for name,storage in super(Finder,self).list(ignore_patterns): - if name.endswith('.beard'): - beards.append(('beard/'+name[:-6]+'.js',storage)) - return beards - def find(self, opath, all=False): - if all: - raise NotImplementedError() - path=opath[6:-3]+'.beard' - abspath=super(Finder,self).find(path) - text = open(abspath).read() - template=source(text,JavascriptGenerator(path[:-6])) - dir=os.path.join(settings.STATIC_ROOT,os.path.dirname(path)) - try: - os.makedirs(dir) - except OSError: - pass - staticpath=os.path.join(settings.STATIC_ROOT,opath) - f=open(staticpath,'w') - f.write(template) - f.close() - - return staticpath