├── .gitignore ├── LICENSE ├── README.rst ├── setup.cfg ├── setup.py └── templatetag_sugar ├── __init__.py ├── models.py ├── node.py ├── parser.py ├── register.py └── tests ├── __init__.py ├── models.py ├── settings.py ├── templatetags ├── __init__.py └── test_tags.py └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Alex Gaynor and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of django-templatetag-sugar nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-templatetag-sugar 2 | =========================== 3 | 4 | A library to make writing templatetags in Django sweet. 5 | 6 | Here's an example of using: 7 | 8 | .. code-block:: python 9 | 10 | from django import template 11 | 12 | from templatetag_sugar.register import tag 13 | from templatetag_sugar.parser import Name, Variable, Constant, Optional, Model 14 | 15 | register = template.Library() 16 | 17 | @tag(register, [Constant("for"), Variable(), Optional([Constant("as"), Name()])]): 18 | def example_tag(context, val, asvar=None): 19 | if asvar: 20 | context[asvar] = val 21 | return "" 22 | else: 23 | return val 24 | 25 | 26 | As you can see it makes it super simple to define the syntax for a tag. 27 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | with open("README.rst") as f: 5 | long_description = f.read() 6 | 7 | setup( 8 | name="django-templatetag-sugar", 9 | version=__import__("templatetag_sugar").__version__, 10 | author="Alex Gaynor", 11 | author_email="alex.gaynor@gmail.com", 12 | description="A library to make Django's template tags sweet.", 13 | long_description=long_description, 14 | license="BSD", 15 | url="http://github.com/alex/django-templatetag-sugar/", 16 | packages=[ 17 | "templatetag_sugar", 18 | ], 19 | classifiers=[ 20 | "Environment :: Web Environment", 21 | "Intended Audience :: Developers", 22 | "License :: OSI Approved :: BSD License", 23 | "Operating System :: OS Independent", 24 | "Framework :: Django", 25 | "Programming Language :: Python", 26 | "Programming Language :: Python :: 2", 27 | "Programming Language :: Python :: 2.6", 28 | "Programming Language :: Python :: 2.7", 29 | "Programming Language :: Python :: 3", 30 | "Programming Language :: Python :: 3.2", 31 | "Programming Language :: Python :: 3.3", 32 | "Programming Language :: Python :: 3.4", 33 | "Programming Language :: Python :: Implementation :: CPython", 34 | "Programming Language :: Python :: Implementation :: PyPy", 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /templatetag_sugar/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.0" 2 | -------------------------------------------------------------------------------- /templatetag_sugar/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex/django-templatetag-sugar/de173c82089ab2d050f48a0fedad65bca2a8e26f/templatetag_sugar/models.py -------------------------------------------------------------------------------- /templatetag_sugar/node.py: -------------------------------------------------------------------------------- 1 | from django.template import Node 2 | 3 | 4 | class SugarNode(Node): 5 | def __init__(self, pieces, function): 6 | self.pieces = pieces 7 | self.function = function 8 | 9 | def render(self, context): 10 | args = [] 11 | kwargs = {} 12 | for part, name, value in self.pieces: 13 | value = part.resolve(context, value) 14 | if name is None: 15 | args.append(value) 16 | else: 17 | kwargs[name] = value 18 | 19 | return self.function(context, *args, **kwargs) 20 | -------------------------------------------------------------------------------- /templatetag_sugar/parser.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from copy import copy 3 | 4 | from django.db.models.loading import cache 5 | from django.template import TemplateSyntaxError 6 | 7 | from templatetag_sugar.node import SugarNode 8 | 9 | 10 | class Parser(object): 11 | def __init__(self, syntax, function): 12 | self.syntax = syntax 13 | self.function = function 14 | 15 | def __call__(self, parser, token): 16 | # we're going to be doing pop(0) a bit, so a deque is way more 17 | # efficient 18 | bits = deque(token.split_contents()) 19 | # pop the name of the tag off 20 | tag_name = bits.popleft() 21 | pieces = [] 22 | error = False 23 | for part in self.syntax: 24 | try: 25 | result = part.parse(parser, bits) 26 | except TemplateSyntaxError: 27 | error = True 28 | break 29 | if result is None: 30 | continue 31 | pieces.extend(result) 32 | if bits or error: 33 | raise TemplateSyntaxError( 34 | "%s has the following syntax: {%% %s %s %%}" % ( 35 | tag_name, 36 | tag_name, 37 | " ".join(part.syntax() for part in self.syntax), 38 | ) 39 | ) 40 | return SugarNode(pieces, self.function) 41 | 42 | 43 | class Parsable(object): 44 | def resolve(self, context, value): 45 | return value 46 | 47 | 48 | class NamedParsable(Parsable): 49 | def __init__(self, name=None): 50 | self.name = name 51 | 52 | def syntax(self): 53 | if self.name: 54 | return "<%s>" % self.name 55 | return "" 56 | 57 | 58 | class Constant(Parsable): 59 | def __init__(self, text): 60 | self.text = text 61 | 62 | def syntax(self): 63 | return self.text 64 | 65 | def parse(self, parser, bits): 66 | if not bits: 67 | raise TemplateSyntaxError 68 | if bits[0] == self.text: 69 | bits.popleft() 70 | return None 71 | raise TemplateSyntaxError 72 | 73 | 74 | class Variable(NamedParsable): 75 | def parse(self, parser, bits): 76 | bit = bits.popleft() 77 | val = parser.compile_filter(bit) 78 | return [(self, self.name, val)] 79 | 80 | def resolve(self, context, value): 81 | return value.resolve(context) 82 | 83 | 84 | class Name(NamedParsable): 85 | def parse(self, parser, bits): 86 | bit = bits.popleft() 87 | return [(self, self.name, bit)] 88 | 89 | 90 | class Optional(Parsable): 91 | def __init__(self, parts): 92 | self.parts = parts 93 | 94 | def syntax(self): 95 | return "[%s]" % (" ".join(part.syntax() for part in self.parts)) 96 | 97 | def parse(self, parser, bits): 98 | result = [] 99 | # we make a copy so that if part way through the optional part it 100 | # doesn't match no changes are made 101 | bits_copy = copy(bits) 102 | for part in self.parts: 103 | try: 104 | val = part.parse(parser, bits_copy) 105 | if val is None: 106 | continue 107 | result.extend(val) 108 | except (TemplateSyntaxError, IndexError): 109 | return None 110 | # however many bits we popped off our copy pop off the real one 111 | diff = len(bits) - len(bits_copy) 112 | for _ in range(diff): 113 | bits.popleft() 114 | return result 115 | 116 | 117 | class Model(NamedParsable): 118 | def parse(self, parser, bits): 119 | bit = bits.popleft() 120 | app, model = bit.split(".") 121 | return [(self, self.name, cache.get_model(app, model))] 122 | -------------------------------------------------------------------------------- /templatetag_sugar/register.py: -------------------------------------------------------------------------------- 1 | from templatetag_sugar.parser import Parser 2 | 3 | 4 | def tag(register, syntax, name=None): 5 | def inner(func): 6 | register.tag(name or func.__name__, Parser(syntax, func)) 7 | return func 8 | return inner 9 | -------------------------------------------------------------------------------- /templatetag_sugar/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex/django-templatetag-sugar/de173c82089ab2d050f48a0fedad65bca2a8e26f/templatetag_sugar/tests/__init__.py -------------------------------------------------------------------------------- /templatetag_sugar/tests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Book(models.Model): 5 | title = models.CharField(max_length=50) 6 | 7 | def __str__(self): 8 | return self.title 9 | -------------------------------------------------------------------------------- /templatetag_sugar/tests/settings.py: -------------------------------------------------------------------------------- 1 | SECRET_KEY = b"a" 2 | 3 | DATABASES = { 4 | 'default': { 5 | 'ENGINE': 'django.db.backends.sqlite3' 6 | } 7 | } 8 | 9 | INSTALLED_APPS = [ 10 | "templatetag_sugar", 11 | "templatetag_sugar.tests", 12 | ] 13 | -------------------------------------------------------------------------------- /templatetag_sugar/tests/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex/django-templatetag-sugar/de173c82089ab2d050f48a0fedad65bca2a8e26f/templatetag_sugar/tests/templatetags/__init__.py -------------------------------------------------------------------------------- /templatetag_sugar/tests/templatetags/test_tags.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django import template 4 | 5 | from templatetag_sugar.register import tag 6 | from templatetag_sugar.parser import Name, Variable, Constant, Optional, Model 7 | 8 | register = template.Library() 9 | 10 | 11 | @tag( 12 | register, 13 | [Constant("for"), Variable(), Optional([Constant("as"), Name()])] 14 | ) 15 | def test_tag_1(context, val, asvar=None): 16 | if asvar: 17 | context[asvar] = val 18 | return "" 19 | else: 20 | return val 21 | 22 | 23 | @tag(register, [Model(), Variable(), Optional([Constant("as"), Name()])]) 24 | def test_tag_2(context, model, limit, asvar=None): 25 | objs = model._default_manager.all()[:limit] 26 | if asvar: 27 | context[asvar] = objs 28 | return "" 29 | if sys.version_info[0] == 2: 30 | return unicode(objs) 31 | else: 32 | return str(objs) 33 | 34 | 35 | @tag(register, [Variable()]) 36 | def test_tag_3(context, val): 37 | return val 38 | 39 | 40 | @tag( 41 | register, 42 | [ 43 | Optional([Constant("width"), Variable('width')]), 44 | Optional([Constant("height"), Variable('height')]) 45 | ] 46 | ) 47 | def test_tag_4(context, width=None, height=None): 48 | return "%s, %s" % (width, height) 49 | -------------------------------------------------------------------------------- /templatetag_sugar/tests/tests.py: -------------------------------------------------------------------------------- 1 | from django.template import Template, Context, TemplateSyntaxError 2 | from django.test import TestCase 3 | 4 | from templatetag_sugar.tests.models import Book 5 | 6 | 7 | class SugarTestCase(TestCase): 8 | def assert_renders(self, tmpl, context, value): 9 | tmpl = Template(tmpl) 10 | self.assertEqual(tmpl.render(context), value) 11 | 12 | def assert_syntax_error(self, tmpl, error): 13 | try: 14 | Template(tmpl) 15 | except TemplateSyntaxError as e: 16 | self.assertTrue( 17 | str(e).endswith(error), 18 | "%s didn't end with %s" % (str(e), error) 19 | ) 20 | else: 21 | self.fail("Didn't raise") 22 | 23 | def test_basic(self): 24 | self.assert_renders( 25 | """{% load test_tags %}{% test_tag_1 for "alex" %}""", 26 | Context(), 27 | "alex" 28 | ) 29 | 30 | c = Context() 31 | self.assert_renders( 32 | """{% load test_tags %}{% test_tag_1 for "brian" as name %}""", 33 | c, 34 | "" 35 | ) 36 | self.assertEqual(c["name"], "brian") 37 | 38 | self.assert_renders( 39 | """{% load test_tags %}{% test_tag_1 for variable %}""", 40 | Context({"variable": [1, 2, 3]}), 41 | "[1, 2, 3]", 42 | ) 43 | 44 | def test_model(self): 45 | Book.objects.create(title="Pro Django") 46 | self.assert_renders( 47 | """{% load test_tags %}{% test_tag_2 tests.Book 2 %}""", 48 | Context(), 49 | "[]" 50 | ) 51 | 52 | def test_errors(self): 53 | self.assert_syntax_error( 54 | """{% load test_tags %}{% test_tag_1 for "jesse" as %}""", 55 | "test_tag_1 has the following syntax: {% test_tag_1 for [as " 56 | "] %}" 57 | ) 58 | 59 | self.assert_syntax_error( 60 | """{% load test_tags %}{% test_tag_4 width %}""", 61 | "test_tag_4 has the following syntax: {% test_tag_4 [width " 62 | "] [height ] %}" 63 | ) 64 | 65 | def test_variable_as_string(self): 66 | self.assert_renders( 67 | """{% load test_tags %}{% test_tag_3 "xela alex" %}""", 68 | Context(), 69 | "xela alex", 70 | ) 71 | 72 | def test_optional(self): 73 | self.assert_renders( 74 | """{% load test_tags %}{% test_tag_4 width 100 height 200 %}""", 75 | Context(), 76 | "100, 200", 77 | ) 78 | 79 | self.assert_renders( 80 | """{% load test_tags %}{% test_tag_4 width 100 %}""", 81 | Context(), 82 | "100, None" 83 | ) 84 | 85 | self.assert_renders( 86 | """{% load test_tags %}{% test_tag_4 height 100 %}""", 87 | Context(), 88 | "None, 100", 89 | ) 90 | 91 | self.assert_syntax_error( 92 | """{% load test_tags %}{% test_tag_1 %}""", 93 | "test_tag_1 has the following syntax: {% test_tag_1 for [as " 94 | "] %}" 95 | ) 96 | --------------------------------------------------------------------------------