mirror of
https://github.com/ankitects/anki.git
synced 2025-11-14 08:37:11 -05:00
add pystache
This commit is contained in:
parent
f247133ed8
commit
9e790ce747
5 changed files with 375 additions and 0 deletions
20
pystache/LICENSE
Normal file
20
pystache/LICENSE
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
Copyright (c) 2009 Chris Wanstrath
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
78
pystache/README.rst
Normal file
78
pystache/README.rst
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
========
|
||||||
|
Pystache
|
||||||
|
========
|
||||||
|
|
||||||
|
Inspired by ctemplate_ and et_, Mustache_ is a
|
||||||
|
framework-agnostic way to render logic-free views.
|
||||||
|
|
||||||
|
As ctemplates says, "It emphasizes separating logic from presentation:
|
||||||
|
it is impossible to embed application logic in this template language."
|
||||||
|
|
||||||
|
Pystache is a Python implementation of Mustache. Pystache requires
|
||||||
|
Python 2.6.
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
=============
|
||||||
|
|
||||||
|
The different Mustache tags are documented at `mustache(5)`_.
|
||||||
|
|
||||||
|
Install It
|
||||||
|
==========
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
pip install pystache
|
||||||
|
|
||||||
|
|
||||||
|
Use It
|
||||||
|
======
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
>>> import pystache
|
||||||
|
>>> pystache.render('Hi {{person}}!', {'person': 'Mom'})
|
||||||
|
'Hi Mom!'
|
||||||
|
|
||||||
|
You can also create dedicated view classes to hold your view logic.
|
||||||
|
|
||||||
|
Here's your simple.py::
|
||||||
|
|
||||||
|
import pystache
|
||||||
|
class Simple(pystache.View):
|
||||||
|
def thing(self):
|
||||||
|
return "pizza"
|
||||||
|
|
||||||
|
Then your template, simple.mustache::
|
||||||
|
|
||||||
|
Hi {{thing}}!
|
||||||
|
|
||||||
|
Pull it together::
|
||||||
|
|
||||||
|
>>> Simple().render()
|
||||||
|
'Hi pizza!'
|
||||||
|
|
||||||
|
|
||||||
|
Test It
|
||||||
|
=======
|
||||||
|
|
||||||
|
nose_ works great! ::
|
||||||
|
|
||||||
|
pip install nose
|
||||||
|
cd pystache
|
||||||
|
nosetests
|
||||||
|
|
||||||
|
|
||||||
|
Author
|
||||||
|
======
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
context = { 'author': 'Chris Wanstrath', 'email': 'chris@ozmm.org' }
|
||||||
|
pystache.render("{{author}} :: {{email}}", context)
|
||||||
|
|
||||||
|
|
||||||
|
.. _ctemplate: http://code.google.com/p/google-ctemplate/
|
||||||
|
.. _et: http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html
|
||||||
|
.. _Mustache: http://defunkt.github.com/mustache/
|
||||||
|
.. _mustache(5): http://defunkt.github.com/mustache/mustache.5.html
|
||||||
|
.. _nose: http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html
|
||||||
7
pystache/__init__.py
Normal file
7
pystache/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
from pystache.template import Template
|
||||||
|
from pystache.view import View
|
||||||
|
|
||||||
|
def render(template, context=None, **kwargs):
|
||||||
|
context = context and context.copy() or {}
|
||||||
|
context.update(kwargs)
|
||||||
|
return Template(template, context).render()
|
||||||
154
pystache/template.py
Normal file
154
pystache/template.py
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
import re
|
||||||
|
import cgi
|
||||||
|
import collections
|
||||||
|
|
||||||
|
modifiers = {}
|
||||||
|
def modifier(symbol):
|
||||||
|
"""Decorator for associating a function with a Mustache tag modifier.
|
||||||
|
|
||||||
|
@modifier('P')
|
||||||
|
def render_tongue(self, tag_name=None, context=None):
|
||||||
|
return ":P %s" % tag_name
|
||||||
|
|
||||||
|
{{P yo }} => :P yo
|
||||||
|
"""
|
||||||
|
def set_modifier(func):
|
||||||
|
modifiers[symbol] = func
|
||||||
|
return func
|
||||||
|
return set_modifier
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_attr(obj, name, default=None):
|
||||||
|
try:
|
||||||
|
return obj[name]
|
||||||
|
except KeyError:
|
||||||
|
return default
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
return getattr(obj, name)
|
||||||
|
except AttributeError:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
class Template(object):
|
||||||
|
# The regular expression used to find a #section
|
||||||
|
section_re = None
|
||||||
|
|
||||||
|
# The regular expression used to find a tag.
|
||||||
|
tag_re = None
|
||||||
|
|
||||||
|
# Opening tag delimiter
|
||||||
|
otag = '{{'
|
||||||
|
|
||||||
|
# Closing tag delimiter
|
||||||
|
ctag = '}}'
|
||||||
|
|
||||||
|
def __init__(self, template, context=None):
|
||||||
|
self.template = template
|
||||||
|
self.context = context or {}
|
||||||
|
self.compile_regexps()
|
||||||
|
|
||||||
|
def render(self, template=None, context=None, encoding=None):
|
||||||
|
"""Turns a Mustache template into something wonderful."""
|
||||||
|
template = template or self.template
|
||||||
|
context = context or self.context
|
||||||
|
|
||||||
|
template = self.render_sections(template, context)
|
||||||
|
result = self.render_tags(template, context)
|
||||||
|
if encoding is not None:
|
||||||
|
result = result.encode(encoding)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def compile_regexps(self):
|
||||||
|
"""Compiles our section and tag regular expressions."""
|
||||||
|
tags = { 'otag': re.escape(self.otag), 'ctag': re.escape(self.ctag) }
|
||||||
|
|
||||||
|
section = r"%(otag)s[\#|^]([^\}]*)%(ctag)s\s*(.+?)\s*%(otag)s/\1%(ctag)s"
|
||||||
|
self.section_re = re.compile(section % tags, re.M|re.S)
|
||||||
|
|
||||||
|
tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+"
|
||||||
|
self.tag_re = re.compile(tag % tags)
|
||||||
|
|
||||||
|
def render_sections(self, template, context):
|
||||||
|
"""Expands sections."""
|
||||||
|
while 1:
|
||||||
|
match = self.section_re.search(template)
|
||||||
|
if match is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
section, section_name, inner = match.group(0, 1, 2)
|
||||||
|
section_name = section_name.strip()
|
||||||
|
|
||||||
|
it = get_or_attr(context, section_name, None)
|
||||||
|
replacer = ''
|
||||||
|
if it and isinstance(it, collections.Callable):
|
||||||
|
replacer = it(inner)
|
||||||
|
elif it and not hasattr(it, '__iter__'):
|
||||||
|
if section[2] != '^':
|
||||||
|
replacer = inner
|
||||||
|
elif it and hasattr(it, 'keys') and hasattr(it, '__getitem__'):
|
||||||
|
if section[2] != '^':
|
||||||
|
replacer = self.render(inner, it)
|
||||||
|
elif it:
|
||||||
|
insides = []
|
||||||
|
for item in it:
|
||||||
|
insides.append(self.render(inner, item))
|
||||||
|
replacer = ''.join(insides)
|
||||||
|
elif not it and section[2] == '^':
|
||||||
|
replacer = inner
|
||||||
|
|
||||||
|
template = template.replace(section, replacer)
|
||||||
|
|
||||||
|
return template
|
||||||
|
|
||||||
|
def render_tags(self, template, context):
|
||||||
|
"""Renders all the tags in a template for a context."""
|
||||||
|
while 1:
|
||||||
|
match = self.tag_re.search(template)
|
||||||
|
if match is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
tag, tag_type, tag_name = match.group(0, 1, 2)
|
||||||
|
tag_name = tag_name.strip()
|
||||||
|
func = modifiers[tag_type]
|
||||||
|
replacement = func(self, tag_name, context)
|
||||||
|
template = template.replace(tag, replacement)
|
||||||
|
|
||||||
|
return template
|
||||||
|
|
||||||
|
@modifier(None)
|
||||||
|
def render_tag(self, tag_name, context):
|
||||||
|
"""Given a tag name and context, finds, escapes, and renders the tag."""
|
||||||
|
raw = get_or_attr(context, tag_name, '')
|
||||||
|
if not raw and raw is not 0:
|
||||||
|
return ''
|
||||||
|
return cgi.escape(unicode(raw))
|
||||||
|
|
||||||
|
@modifier('!')
|
||||||
|
def render_comment(self, tag_name=None, context=None):
|
||||||
|
"""Rendering a comment always returns nothing."""
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@modifier('{')
|
||||||
|
@modifier('&')
|
||||||
|
def render_unescaped(self, tag_name=None, context=None):
|
||||||
|
"""Render a tag without escaping it."""
|
||||||
|
return unicode(get_or_attr(context, tag_name, ''))
|
||||||
|
|
||||||
|
@modifier('>')
|
||||||
|
def render_partial(self, tag_name=None, context=None):
|
||||||
|
"""Renders a partial within the current context."""
|
||||||
|
# Import view here to avoid import loop
|
||||||
|
from pystache.view import View
|
||||||
|
|
||||||
|
view = View(context=context)
|
||||||
|
view.template_name = tag_name
|
||||||
|
|
||||||
|
return view.render()
|
||||||
|
|
||||||
|
@modifier('=')
|
||||||
|
def render_delimiter(self, tag_name=None, context=None):
|
||||||
|
"""Changes the Mustache delimiter."""
|
||||||
|
self.otag, self.ctag = tag_name.split(' ')
|
||||||
|
self.compile_regexps()
|
||||||
|
return ''
|
||||||
116
pystache/view.py
Normal file
116
pystache/view.py
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
from pystache import Template
|
||||||
|
import os.path
|
||||||
|
import re
|
||||||
|
|
||||||
|
class View(object):
|
||||||
|
# Path where this view's template(s) live
|
||||||
|
template_path = '.'
|
||||||
|
|
||||||
|
# Extension for templates
|
||||||
|
template_extension = 'mustache'
|
||||||
|
|
||||||
|
# The name of this template. If none is given the View will try
|
||||||
|
# to infer it based on the class name.
|
||||||
|
template_name = None
|
||||||
|
|
||||||
|
# Absolute path to the template itself. Pystache will try to guess
|
||||||
|
# if it's not provided.
|
||||||
|
template_file = None
|
||||||
|
|
||||||
|
# Contents of the template.
|
||||||
|
template = None
|
||||||
|
|
||||||
|
# Character encoding of the template file. If None, Pystache will not
|
||||||
|
# do any decoding of the template.
|
||||||
|
template_encoding = None
|
||||||
|
|
||||||
|
def __init__(self, template=None, context=None, **kwargs):
|
||||||
|
self.template = template
|
||||||
|
self.context = context or {}
|
||||||
|
|
||||||
|
# If the context we're handed is a View, we want to inherit
|
||||||
|
# its settings.
|
||||||
|
if isinstance(context, View):
|
||||||
|
self.inherit_settings(context)
|
||||||
|
|
||||||
|
if kwargs:
|
||||||
|
self.context.update(kwargs)
|
||||||
|
|
||||||
|
def inherit_settings(self, view):
|
||||||
|
"""Given another View, copies its settings."""
|
||||||
|
if view.template_path:
|
||||||
|
self.template_path = view.template_path
|
||||||
|
|
||||||
|
if view.template_name:
|
||||||
|
self.template_name = view.template_name
|
||||||
|
|
||||||
|
def load_template(self):
|
||||||
|
if self.template:
|
||||||
|
return self.template
|
||||||
|
|
||||||
|
if self.template_file:
|
||||||
|
return self._load_template()
|
||||||
|
|
||||||
|
name = self.get_template_name() + '.' + self.template_extension
|
||||||
|
|
||||||
|
if isinstance(self.template_path, basestring):
|
||||||
|
self.template_file = os.path.join(self.template_path, name)
|
||||||
|
return self._load_template()
|
||||||
|
|
||||||
|
for path in self.template_path:
|
||||||
|
self.template_file = os.path.join(path, name)
|
||||||
|
if os.path.exists(self.template_file):
|
||||||
|
return self._load_template()
|
||||||
|
|
||||||
|
raise IOError('"%s" not found in "%s"' % (name, ':'.join(self.template_path),))
|
||||||
|
|
||||||
|
|
||||||
|
def _load_template(self):
|
||||||
|
f = open(self.template_file, 'r')
|
||||||
|
try:
|
||||||
|
template = f.read()
|
||||||
|
if self.template_encoding:
|
||||||
|
template = unicode(template, self.template_encoding)
|
||||||
|
finally:
|
||||||
|
f.close()
|
||||||
|
return template
|
||||||
|
|
||||||
|
def get_template_name(self, name=None):
|
||||||
|
"""TemplatePartial => template_partial
|
||||||
|
Takes a string but defaults to using the current class' name or
|
||||||
|
the `template_name` attribute
|
||||||
|
"""
|
||||||
|
if self.template_name:
|
||||||
|
return self.template_name
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
name = self.__class__.__name__
|
||||||
|
|
||||||
|
def repl(match):
|
||||||
|
return '_' + match.group(0).lower()
|
||||||
|
|
||||||
|
return re.sub('[A-Z]', repl, name)[1:]
|
||||||
|
|
||||||
|
def __contains__(self, needle):
|
||||||
|
return needle in self.context or hasattr(self, needle)
|
||||||
|
|
||||||
|
def __getitem__(self, attr):
|
||||||
|
val = self.get(attr, None)
|
||||||
|
if not val:
|
||||||
|
raise KeyError("No such key.")
|
||||||
|
return val
|
||||||
|
|
||||||
|
def get(self, attr, default):
|
||||||
|
attr = self.context.get(attr, getattr(self, attr, default))
|
||||||
|
|
||||||
|
if hasattr(attr, '__call__'):
|
||||||
|
return attr()
|
||||||
|
else:
|
||||||
|
return attr
|
||||||
|
|
||||||
|
def render(self, encoding=None):
|
||||||
|
template = self.load_template()
|
||||||
|
return Template(template, self).render(encoding=encoding)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.render()
|
||||||
Loading…
Reference in a new issue