<NME> bundler_tags.py
<BEF> ADDFILE
<MSG> Initial working prototype, sure to be full of bugs.

<DFF> @@ -0,0 +1,174 @@
+# media_bundler/templatetags/bundler_tags.py
+
+"""
+Template tags for the django media bundler.
+"""
+
+from django import template
+
+from media_bundler import bundler
+from media_bundler import bundler_settings
+
+register = template.Library()
+
+
+def context_set_default(context, key, default):
+    """Like setdefault for Contexts, only we use the root Context dict."""
+    if context.has_key(key):
+        return context[key]
+    else:
+        # Set the value on the root context so our value isn't popped off.
+        context.dicts[-1][key] = default
+        return default
+
+
+def _in_quotes(string):
+    """Return True if string is quoted with ' or "."""
+    return (string[0] == string[-1] and
+            string[0] in ('"', "'"))
+
+
+def bundle_tag(tag_func):
+    def new_tag_func(parser, token):
+        try:
+            tag_name, bundle_name, file_name = token.split_contents()
+        except ValueError:
+            tag_name = token.contents.split()[0]
+            msg = "%r tag takes two arguments: bundle_name and file_name."
+            raise template.TemplateSyntaxError(msg % tag_name)
+        if not _in_quotes(bundle_name) or not _in_quotes(file_name):
+            msg = "%r tag's arguments should be in quotes" % tag_name
+            raise template.TemplateSyntaxError(msg)
+        return tag_func(bundle_name[1:-1], file_name[1:-1])
+    # Hack so that register.tag behaves correctly.
+    new_tag_func.__name__ = tag_func.__name__
+    return new_tag_func
+
+
+class BundleNode(template.Node):
+
+    """
+    Base class for any nodes that are linking bundles.
+
+    Subclasses must define class variables TAG and CONTEXT_VAR.  They can
+    optionally override the method 'really_render(url)' to control tag-specific
+    rendering behavior.
+    """
+
+    TAG = None
+
+    CONTEXT_VAR = None
+
+    def __init__(self, bundle_name, file_name):
+        super(BundleNode, self).__init__()
+        self.bundle_name = bundle_name
+        self.file_name = file_name
+
+    def render(self, context):
+        bundle = bundler.get_bundles()[self.bundle_name]
+        if self.file_name not in bundle.files:
+            msg = "File %r is not in bundle %r." % (self.file_name,
+                                                    self.bundle_name)
+            raise template.TemplateSyntaxError(msg)
+        url_set = context_set_default(context, self.CONTEXT_VAR, set())
+        if bundler_settings.USE_BUNDLES:
+            url = bundle.get_bundle_url()
+        else:
+            url = bundle.url + self.file_name
+        if url in url_set:
+            return ""  # Don't add a bundle or css url twice.
+        else:
+            url_set.add(url)
+            return self.really_render(context, url)
+
+    def really_render(self, context, url):
+        """Implement bundle type specific rendering behavior."""
+        return self.TAG % url
+
+
+@register.tag
+@bundle_tag
+def javascript(bundle_name, script_name):
+    """Tag to include JavaScript in the template."""
+    return JavascriptNode(bundle_name, script_name)
+
+
+class JavascriptNode(BundleNode):
+
+    """Add a script tag for a script or its bundle inline or at the bottom."""
+
+    TAG = '<script type="text/javascript" src="%s"></script>'
+
+    CONTEXT_VAR = "_script_urls"
+
+    def __init__(self, bundle_name, script_name):
+        super(JavascriptNode, self).__init__(bundle_name, script_name)
+
+    def really_render(self, context, url):
+        content = super(JavascriptNode, self).really_render(context, url)
+        if bundler_settings.DEFER_JAVASCRIPT:
+            deferred = context_set_default(context, "_deferred_content", [])
+            deferred.append(content)
+            return ""
+        else:
+            return content
+
+
+@register.tag
+@bundle_tag
+def css(bundle_name, css_name):
+    """Tag to include CSS in the template."""
+    return CssNode(bundle_name, css_name)
+
+
+class CssNode(BundleNode):
+
+    """Add link tags for a CSS file or its bundle."""
+
+    TAG = '<link rel="stylesheet" type="text/css" href="%s"/>'
+
+    CONTEXT_VAR = "_css_urls"
+
+    def __init__(self, bundle_name, css_name):
+        super(CssNode, self).__init__(bundle_name, css_name)
+
+
+@register.tag
+def defer(parser, token):
+    nodelist = parser.parse(('enddefer',))
+    parser.delete_first_token()
+    return DeferNode(nodelist)
+
+
+class DeferNode(template.Node):
+
+    """Defer some content until later."""
+
+    def __init__(self, nodelist):
+        super(DeferNode, self).__init__()
+        self.nodelist = nodelist
+
+    def render(self, context):
+        # We render the content in this context so that the scoping rules make
+        # sense, ie all variables that seem to be in scope really are.
+        content = self.nodelist.render(context)
+        if bundler_settings.DEFER_JAVASCRIPT:
+            deferred = context_set_default(context, "_deferred_content", [])
+            deferred.append(content)
+            return ""
+        else:
+            return content
+
+
+@register.tag
+def deferred_content(parser, token):
+    """Tag to load deferred content."""
+    return DeferredContentNode()
+
+
+class DeferredContentNode(template.Node):
+
+    """Add script tags for deferred scripts."""
+
+    def render(self, context):
+        return "\n".join(context.get("_deferred_content", ()))

{'commit': 'diff --git a/media_bundler/bundler.py b/media_bundler/bundler.py\nnew file mode 100644\nindex 0000000..9edb817\n--- /dev/null\n+++ b/media_bundler/bundler.py\n@@ -0,0 +1,61 @@\n+# media_bundler/bundle.py\n+\n+import os\n+\n+from media_bundler import bundler_settings\n+\n+_BUNDLES = None\n+\n+\n+class InvalidBundleType(Exception):\n+\n+    def __init__(self, type):\n+        msg = "Invalid bundle type: \'%s\'" % self.type\n+        super(InvalidBundleType, self).__init__(msg)\n+\n+\n+class Bundle(object):\n+\n+    def __init__(self, type, name, path, url, files, minify):\n+        self.type = type\n+        self.name = name\n+        self.path = path\n+        self.url = url\n+        if not url.endswith("/"):\n+            raise ValueError("Bundle URLs must end with a \'/\'.")\n+        self.files = files\n+        self.minify = minify\n+\n+    def get_paths(self):\n+        return [os.path.join(self.path, f) for f in self.files]\n+\n+    def get_extension(self):\n+        if self.type == "javascript":\n+            return ".js"\n+        elif self.type == "css":\n+            return ".css"\n+        elif self.type == "sprite":\n+            return ".png"\n+        else:\n+            raise InvalidBundleType(self.type)\n+\n+    def get_bundle_path(self):\n+        path = self.name + self.get_extension()\n+        return os.path.join(self.path, path)\n+\n+    def get_bundle_url(self):\n+        url = self.name + self.get_extension()\n+        return self.url + url\n+\n+    @classmethod\n+    def from_dict(cls, attrs):\n+        return cls(attrs["type"], attrs["name"], attrs["path"], attrs["url"],\n+                   attrs["files"], attrs["minify"])\n+\n+\n+def get_bundles():\n+    global _BUNDLES\n+    if not _BUNDLES:\n+        _BUNDLES = dict((bundle[\'name\'], Bundle.from_dict(bundle))\n+                        for bundle in bundler_settings.MEDIA_BUNDLES)\n+    return _BUNDLES\ndiff --git a/media_bundler/bundler_settings.py b/media_bundler/bundler_settings.py\nnew file mode 100644\nindex 0000000..76e76fb\n--- /dev/null\n+++ b/media_bundler/bundler_settings.py\n@@ -0,0 +1,23 @@\n+# media_bundler/bundler_settings.py\n+\n+"""\n+media_bundler specific settings with the defaults filled in.\n+\n+If the user has overridden a setting in their settings module, we\'ll use that\n+value, but otherwise we\'ll fall back on the value from\n+media_bundler.default_settings.  All bundler- specific settings checks should\n+go through this module, but to check global Django settings, use the normal\n+django.conf.settings module.\n+"""\n+\n+from django.conf import settings\n+\n+from media_bundler import default_settings\n+\n+\n+USE_BUNDLES = getattr(settings, "USE_BUNDLES",\n+                      default_settings.USE_BUNDLES)\n+DEFER_JAVASCRIPT = getattr(settings, "DEFER_JAVASCRIPT",\n+                           default_settings.DEFER_JAVASCRIPT)\n+MEDIA_BUNDLES = getattr(settings, "MEDIA_BUNDLES",\n+                        default_settings.MEDIA_BUNDLES)\ndiff --git a/media_bundler/cssmin.py b/media_bundler/cssmin.py\nnew file mode 100644\nindex 0000000..0ce862c\n--- /dev/null\n+++ b/media_bundler/cssmin.py\n@@ -0,0 +1,38 @@\n+# media_bundler/cssmin.py\n+\n+# Original source code:\n+# http://stackoverflow.com/questions/222581/python-script-for-minifying-css\n+\n+# This is obviously a hacky script, and it probably has bugs.\n+\n+import re\n+\n+def minify_css(css):\n+    # remove comments - this will break a lot of hacks :-P\n+    css = re.sub(r\'\\s*/\\*\\s*\\*/\', "$$HACK1$$", css)\n+    css = re.sub(r\'/\\*[\\s\\S]*?\\*/\', "", css)\n+    css = css.replace("$$HACK1$$", \'/**/\') # preserve IE<6 comment hack\n+    # url() don\'t need quotes\n+    css = re.sub(r\'url\\((["\\\'])([^)]*)\\1\\)\', "url(\\\\2)", css)\n+    # spaces may be safely collapsed as generated content will collapse them anyway\n+    css = re.sub(r\'\\s+\', " ", css)\n+    return "".join(generate_rules(css))\n+\n+def generate_rules(css):\n+    for rule in re.findall(r\'([^{]+){([^}]*)}\', css):\n+        selectors = []\n+        for selector in rule[0].split(\',\'):\n+            selectors.append(selector.strip())\n+        # order is important, but we still want to discard repetitions\n+        properties = {}\n+        porder  = []\n+        for prop in re.findall(\'(.*?):(.*?)(;|$)\', rule[1]):\n+            key = prop[0].strip().lower()\n+            if key not in porder:\n+                porder.insert(0, key)\n+            properties[ key ] = prop[1].strip()\n+        porder.reverse()\n+        # output rule if it contains any declarations\n+        if len(properties) > 0:\n+            s = ";".join(key + ":" + properties[key] for key in porder)\n+            yield ",".join(selectors) + "{" + s + "}"\ndiff --git a/media_bundler/default_settings.py b/media_bundler/default_settings.py\nnew file mode 100644\nindex 0000000..d192c7c\n--- /dev/null\n+++ b/media_bundler/default_settings.py\n@@ -0,0 +1,46 @@\n+# media_bundler/default_settings.py\n+\n+"""\n+These are the default settings for media_bunder.\n+\n+You can copy, paste, and modify these values into your own settings.py file.\n+"""\n+\n+from django.conf import settings\n+\n+# This flag determines whether to enable bundling or not.  To assist in\n+# debugging, we recommend keeping files separate during development and bundle\n+# them during production, so by default we just use settings.DEBUG, but you can\n+# override that value if you wish.\n+USE_BUNDLES = settings.DEBUG\n+\n+# This puts your JavaScript at the bottom of your templates instead of the top\n+# in order to allow the page to load before script execution, as described in\n+# YUI rule #5:\n+# http://developer.yahoo.net/blog/archives/2007/07/high_performanc_5.html\n+DEFER_JAVASCRIPT = True\n+\n+MEDIA_BUNDLES = (\n+    # This should contain something like:\n+\n+    #{"type": "javascript",\n+    # "name": "myapp_scripts",\n+    # "path": MEDIA_ROOT + "/scripts/",\n+    # "url": MEDIA_URL + "/scripts/",\n+    # "compression": None,  # other values are "minify" and "pack".\n+    # "files": ("foo.js", "bar.js", "baz.js")},\n+\n+    #{"type": "css",\n+    # "name": "myapp_styles",\n+    # "path": MEDIA_ROOT + "/styles/",\n+    # "url": MEDIA_URL + "/styles/",\n+    # "compression": None,  # other values are "minify".\n+    # "files": ("foo.css", "bar.css", "baz.css")},\n+\n+    ## Image bundling is not implemented yet, but this is how it might look.\n+    #{"type": "sprite",\n+    # "name": "myapp_sprites",\n+    # "path": MEDIA_ROOT + "/images/",\n+    # "url": MEDIA_URL + "/images/",\n+    # "files": ("foo.png", "bar.png", "baz.png")},\n+)\ndiff --git a/media_bundler/jsmin.py b/media_bundler/jsmin.py\nnew file mode 100644\nindex 0000000..8d9f32b\n--- /dev/null\n+++ b/media_bundler/jsmin.py\n@@ -0,0 +1,218 @@\n+#!/usr/bin/python\n+\n+# This code is original from jsmin by Douglas Crockford, it was translated to\n+# Python by Baruch Even. The original code had the following copyright and\n+# license.\n+#\n+# /* jsmin.c\n+#    2007-05-22\n+#\n+# Copyright (c) 2002 Douglas Crockford  (www.crockford.com)\n+#\n+# Permission is hereby granted, free of charge, to any person obtaining a copy of\n+# this software and associated documentation files (the "Software"), to deal in\n+# the Software without restriction, including without limitation the rights to\n+# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\n+# of the Software, and to permit persons to whom the Software is furnished to do\n+# so, subject to the following conditions:\n+#\n+# The above copyright notice and this permission notice shall be included in all\n+# copies or substantial portions of the Software.\n+#\n+# The Software shall be used for Good, not Evil.\n+#\n+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n+# SOFTWARE.\n+# */\n+\n+def jsmin(js):\n+    from StringIO import StringIO\n+    ins = StringIO(js)\n+    outs = StringIO()\n+    JavascriptMinify().minify(ins, outs)\n+    str = outs.getvalue()\n+    if len(str) > 0 and str[0] == \'\\n\':\n+        str = str[1:]\n+    return str\n+\n+def isAlphanum(c):\n+    """return true if the character is a letter, digit, underscore,\n+           dollar sign, or non-ASCII character.\n+    """\n+    return ((c >= \'a\' and c <= \'z\') or (c >= \'0\' and c <= \'9\') or\n+            (c >= \'A\' and c <= \'Z\') or c == \'_\' or c == \'$\' or c == \'\\\\\' or\n+            (c is not None and ord(c) > 126))\n+\n+class UnterminatedComment(Exception):\n+    pass\n+\n+class UnterminatedStringLiteral(Exception):\n+    pass\n+\n+class UnterminatedRegularExpression(Exception):\n+    pass\n+\n+class JavascriptMinify(object):\n+\n+    def _outA(self):\n+        self.outstream.write(self.theA)\n+    def _outB(self):\n+        self.outstream.write(self.theB)\n+\n+    def _get(self):\n+        """return the next character from stdin. Watch out for lookahead. If\n+           the character is a control character, translate it to a space or\n+           linefeed.\n+        """\n+        c = self.theLookahead\n+        self.theLookahead = None\n+        if c == None:\n+            c = self.instream.read(1)\n+        if c >= \' \' or c == \'\\n\':\n+            return c\n+        if c == \'\': # EOF\n+            return \'\\000\'\n+        if c == \'\\r\':\n+            return \'\\n\'\n+        return \' \'\n+\n+    def _peek(self):\n+        self.theLookahead = self._get()\n+        return self.theLookahead\n+\n+    def _next(self):\n+        """get the next character, excluding comments. peek() is used to see\n+           if an unescaped \'/\' is followed by a \'/\' or \'*\'.\n+        """\n+        c = self._get()\n+        if c == \'/\' and self.theA != \'\\\\\':\n+            p = self._peek()\n+            if p == \'/\':\n+                c = self._get()\n+                while c > \'\\n\':\n+                    c = self._get()\n+                return c\n+            if p == \'*\':\n+                c = self._get()\n+                while 1:\n+                    c = self._get()\n+                    if c == \'*\':\n+                        if self._peek() == \'/\':\n+                            self._get()\n+                            return \' \'\n+                    if c == \'\\000\':\n+                        raise UnterminatedComment()\n+\n+        return c\n+\n+    def _action(self, action):\n+        """do something! What you do is determined by the argument:\n+           1   Output A. Copy B to A. Get the next B.\n+           2   Copy B to A. Get the next B. (Delete A).\n+           3   Get the next B. (Delete B).\n+           action treats a string as a single character. Wow!\n+           action recognizes a regular expression if it is preceded by ( or , or =.\n+        """\n+        if action <= 1:\n+            self._outA()\n+\n+        if action <= 2:\n+            self.theA = self.theB\n+            if self.theA == "\'" or self.theA == \'"\':\n+                while 1:\n+                    self._outA()\n+                    self.theA = self._get()\n+                    if self.theA == self.theB:\n+                        break\n+                    if self.theA <= \'\\n\':\n+                        raise UnterminatedStringLiteral()\n+                    if self.theA == \'\\\\\':\n+                        self._outA()\n+                        self.theA = self._get()\n+\n+\n+        if action <= 3:\n+            self.theB = self._next()\n+            if self.theB == \'/\' and (self.theA == \'(\' or self.theA == \',\' or\n+                                     self.theA == \'=\' or self.theA == \':\' or\n+                                     self.theA == \'[\' or self.theA == \'?\' or\n+                                     self.theA == \'!\' or self.theA == \'&\' or\n+                                     self.theA == \'|\' or self.theA == \';\' or\n+                                     self.theA == \'{\' or self.theA == \'}\' or\n+                                     self.theA == \'\\n\'):\n+                self._outA()\n+                self._outB()\n+                while 1:\n+                    self.theA = self._get()\n+                    if self.theA == \'/\':\n+                        break\n+                    elif self.theA == \'\\\\\':\n+                        self._outA()\n+                        self.theA = self._get()\n+                    elif self.theA <= \'\\n\':\n+                        raise UnterminatedRegularExpression()\n+                    self._outA()\n+                self.theB = self._next()\n+\n+\n+    def _jsmin(self):\n+        """Copy the input to the output, deleting the characters which are\n+           insignificant to JavaScript. Comments will be removed. Tabs will be\n+           replaced with spaces. Carriage returns will be replaced with linefeeds.\n+           Most spaces and linefeeds will be removed.\n+        """\n+        self.theA = \'\\n\'\n+        self._action(3)\n+\n+        while self.theA != \'\\000\':\n+            if self.theA == \' \':\n+                if isAlphanum(self.theB):\n+                    self._action(1)\n+                else:\n+                    self._action(2)\n+            elif self.theA == \'\\n\':\n+                if self.theB in [\'{\', \'[\', \'(\', \'+\', \'-\']:\n+                    self._action(1)\n+                elif self.theB == \' \':\n+                    self._action(3)\n+                else:\n+                    if isAlphanum(self.theB):\n+                        self._action(1)\n+                    else:\n+                        self._action(2)\n+            else:\n+                if self.theB == \' \':\n+                    if isAlphanum(self.theA):\n+                        self._action(1)\n+                    else:\n+                        self._action(3)\n+                elif self.theB == \'\\n\':\n+                    if self.theA in [\'}\', \']\', \')\', \'+\', \'-\', \'"\', \'\\\'\']:\n+                        self._action(1)\n+                    else:\n+                        if isAlphanum(self.theA):\n+                            self._action(1)\n+                        else:\n+                            self._action(3)\n+                else:\n+                    self._action(1)\n+\n+    def minify(self, instream, outstream):\n+        self.instream = instream\n+        self.outstream = outstream\n+        self.theA = \'\\n\'\n+        self.theB = None\n+        self.theLookahead = None\n+\n+        self._jsmin()\n+        self.instream.close()\n+\n+if __name__ == \'__main__\':\n+    import sys\n+    jsm = JavascriptMinify()\n+    jsm.minify(sys.stdin, sys.stdout)\ndiff --git a/media_bundler/management/commands/bundle_media.py b/media_bundler/management/commands/bundle_media.py\nindex e69de29..385355c 100644\n--- a/media_bundler/management/commands/bundle_media.py\n+++ b/media_bundler/management/commands/bundle_media.py\n@@ -0,0 +1,57 @@\n+# media_bundler/management/commands/bundle_media.py\n+\n+from __future__ import with_statement\n+\n+"""\n+A Django management command to bundle our media.\n+\n+This command should be integrated into any build or deploy process used with\n+the project.\n+"""\n+\n+from django.core.management.base import NoArgsCommand\n+\n+from media_bundler import bundler\n+from media_bundler.jsmin import jsmin\n+from media_bundler.cssmin import minify_css\n+\n+\n+def concatenate_files(paths):\n+    """Generate the contents of several files in 8K blocks."""\n+    for path in paths:\n+        with open(path) as input:\n+            buffer = input.read(8192)\n+            while buffer:\n+                yield buffer\n+                buffer = input.read(8192)\n+        #yield "\\n"\n+\n+\n+class Command(NoArgsCommand):\n+\n+    """Bundles your media as specified in settings.py."""\n+\n+    def handle_noargs(self, **options):\n+        for bundle in bundler.get_bundles().itervalues():\n+            self.do_bundle(bundle)\n+\n+    def do_bundle(self, bundle):\n+        if bundle.type == "javascript":\n+            self.do_text_bundle(bundle, jsmin)\n+        elif bundle.type == "css":\n+            self.do_text_bundle(bundle, minify_css)\n+        elif bundle.type == "sprite":\n+            msg = "Image spriting is not yet implemented."\n+            raise NotImplementedError(msg)\n+        else:\n+            raise bundler.InvalidBundleType(self.type)\n+\n+    def do_text_bundle(self, bundle, minifier):\n+        with open(bundle.get_bundle_path(), \'w\') as output:\n+            generator = concatenate_files(bundle.get_paths())\n+            if bundle.minify:\n+                # Eventually we should use generators to concatenate and minify\n+                # things one bit at a time, but for now we use strings.\n+                output.write(minifier("".join(generator)))\n+            else:\n+                output.write("".join(generator))\ndiff --git a/media_bundler/templatetags/bundler_tags.py b/media_bundler/templatetags/bundler_tags.py\nnew file mode 100644\nindex 0000000..962dab5\n--- /dev/null\n+++ b/media_bundler/templatetags/bundler_tags.py\n@@ -0,0 +1,174 @@\n+# media_bundler/templatetags/bundler_tags.py\n+\n+"""\n+Template tags for the django media bundler.\n+"""\n+\n+from django import template\n+\n+from media_bundler import bundler\n+from media_bundler import bundler_settings\n+\n+register = template.Library()\n+\n+\n+def context_set_default(context, key, default):\n+    """Like setdefault for Contexts, only we use the root Context dict."""\n+    if context.has_key(key):\n+        return context[key]\n+    else:\n+        # Set the value on the root context so our value isn\'t popped off.\n+        context.dicts[-1][key] = default\n+        return default\n+\n+\n+def _in_quotes(string):\n+    """Return True if string is quoted with \' or "."""\n+    return (string[0] == string[-1] and\n+            string[0] in (\'"\', "\'"))\n+\n+\n+def bundle_tag(tag_func):\n+    def new_tag_func(parser, token):\n+        try:\n+            tag_name, bundle_name, file_name = token.split_contents()\n+        except ValueError:\n+            tag_name = token.contents.split()[0]\n+            msg = "%r tag takes two arguments: bundle_name and file_name."\n+            raise template.TemplateSyntaxError(msg % tag_name)\n+        if not _in_quotes(bundle_name) or not _in_quotes(file_name):\n+            msg = "%r tag\'s arguments should be in quotes" % tag_name\n+            raise template.TemplateSyntaxError(msg)\n+        return tag_func(bundle_name[1:-1], file_name[1:-1])\n+    # Hack so that register.tag behaves correctly.\n+    new_tag_func.__name__ = tag_func.__name__\n+    return new_tag_func\n+\n+\n+class BundleNode(template.Node):\n+\n+    """\n+    Base class for any nodes that are linking bundles.\n+\n+    Subclasses must define class variables TAG and CONTEXT_VAR.  They can\n+    optionally override the method \'really_render(url)\' to control tag-specific\n+    rendering behavior.\n+    """\n+\n+    TAG = None\n+\n+    CONTEXT_VAR = None\n+\n+    def __init__(self, bundle_name, file_name):\n+        super(BundleNode, self).__init__()\n+        self.bundle_name = bundle_name\n+        self.file_name = file_name\n+\n+    def render(self, context):\n+        bundle = bundler.get_bundles()[self.bundle_name]\n+        if self.file_name not in bundle.files:\n+            msg = "File %r is not in bundle %r." % (self.file_name,\n+                                                    self.bundle_name)\n+            raise template.TemplateSyntaxError(msg)\n+        url_set = context_set_default(context, self.CONTEXT_VAR, set())\n+        if bundler_settings.USE_BUNDLES:\n+            url = bundle.get_bundle_url()\n+        else:\n+            url = bundle.url + self.file_name\n+        if url in url_set:\n+            return ""  # Don\'t add a bundle or css url twice.\n+        else:\n+            url_set.add(url)\n+            return self.really_render(context, url)\n+\n+    def really_render(self, context, url):\n+        """Implement bundle type specific rendering behavior."""\n+        return self.TAG % url\n+\n+\n+@register.tag\n+@bundle_tag\n+def javascript(bundle_name, script_name):\n+    """Tag to include JavaScript in the template."""\n+    return JavascriptNode(bundle_name, script_name)\n+\n+\n+class JavascriptNode(BundleNode):\n+\n+    """Add a script tag for a script or its bundle inline or at the bottom."""\n+\n+    TAG = \'<script type="text/javascript" src="%s"></script>\'\n+\n+    CONTEXT_VAR = "_script_urls"\n+\n+    def __init__(self, bundle_name, script_name):\n+        super(JavascriptNode, self).__init__(bundle_name, script_name)\n+\n+    def really_render(self, context, url):\n+        content = super(JavascriptNode, self).really_render(context, url)\n+        if bundler_settings.DEFER_JAVASCRIPT:\n+            deferred = context_set_default(context, "_deferred_content", [])\n+            deferred.append(content)\n+            return ""\n+        else:\n+            return content\n+\n+\n+@register.tag\n+@bundle_tag\n+def css(bundle_name, css_name):\n+    """Tag to include CSS in the template."""\n+    return CssNode(bundle_name, css_name)\n+\n+\n+class CssNode(BundleNode):\n+\n+    """Add link tags for a CSS file or its bundle."""\n+\n+    TAG = \'<link rel="stylesheet" type="text/css" href="%s"/>\'\n+\n+    CONTEXT_VAR = "_css_urls"\n+\n+    def __init__(self, bundle_name, css_name):\n+        super(CssNode, self).__init__(bundle_name, css_name)\n+\n+\n+@register.tag\n+def defer(parser, token):\n+    nodelist = parser.parse((\'enddefer\',))\n+    parser.delete_first_token()\n+    return DeferNode(nodelist)\n+\n+\n+class DeferNode(template.Node):\n+\n+    """Defer some content until later."""\n+\n+    def __init__(self, nodelist):\n+        super(DeferNode, self).__init__()\n+        self.nodelist = nodelist\n+\n+    def render(self, context):\n+        # We render the content in this context so that the scoping rules make\n+        # sense, ie all variables that seem to be in scope really are.\n+        content = self.nodelist.render(context)\n+        if bundler_settings.DEFER_JAVASCRIPT:\n+            deferred = context_set_default(context, "_deferred_content", [])\n+            deferred.append(content)\n+            return ""\n+        else:\n+            return content\n+\n+\n+@register.tag\n+def deferred_content(parser, token):\n+    """Tag to load deferred content."""\n+    return DeferredContentNode()\n+\n+\n+class DeferredContentNode(template.Node):\n+\n+    """Add script tags for deferred scripts."""\n+\n+    def render(self, context):\n+        return "\\n".join(context.get("_deferred_content", ()))\ndiff --git a/media_bundler/templatetags/media_bundler.py b/media_bundler/templatetags/media_bundler.py\ndeleted file mode 100644\nindex e69de29..0000000\n', 'commit_subject': 'Initial working prototype, sure to be full of bugs.', 'repo_name': 'rnk/django-media-bundler', 'committer': {'name': 'Reid Kleckner', 'email': 'd7a6b5668afca32909ec82de7d490acab5faf1b8@muikyl.mit.edu', 'time_sec': '1233269704', 'tz_offset': '-300', 'date': {'seconds': '1233269704'}}, 'commit_tree': 'cfe611750991599b49b6ed8b66abf4314c59b157', 'commit_message': 'Initial working prototype, sure to be full of bugs.\n', 'language_name': 'Python', 'license': 'bsd-3-clause', 'hunks': '@@ -0,0 +1,174 @@\n+# media_bundler/templatetags/bundler_tags.py\n+\n+"""\n+Template tags for the django media bundler.\n+"""\n+\n+from django import template\n+\n+from media_bundler import bundler\n+from media_bundler import bundler_settings\n+\n+register = template.Library()\n+\n+\n+def context_set_default(context, key, default):\n+    """Like setdefault for Contexts, only we use the root Context dict."""\n+    if context.has_key(key):\n+        return context[key]\n+    else:\n+        # Set the value on the root context so our value isn\'t popped off.\n+        context.dicts[-1][key] = default\n+        return default\n+\n+\n+def _in_quotes(string):\n+    """Return True if string is quoted with \' or "."""\n+    return (string[0] == string[-1] and\n+            string[0] in (\'"\', "\'"))\n+\n+\n+def bundle_tag(tag_func):\n+    def new_tag_func(parser, token):\n+        try:\n+            tag_name, bundle_name, file_name = token.split_contents()\n+        except ValueError:\n+            tag_name = token.contents.split()[0]\n+            msg = "%r tag takes two arguments: bundle_name and file_name."\n+            raise template.TemplateSyntaxError(msg % tag_name)\n+        if not _in_quotes(bundle_name) or not _in_quotes(file_name):\n+            msg = "%r tag\'s arguments should be in quotes" % tag_name\n+            raise template.TemplateSyntaxError(msg)\n+        return tag_func(bundle_name[1:-1], file_name[1:-1])\n+    # Hack so that register.tag behaves correctly.\n+    new_tag_func.__name__ = tag_func.__name__\n+    return new_tag_func\n+\n+\n+class BundleNode(template.Node):\n+\n+    """\n+    Base class for any nodes that are linking bundles.\n+\n+    Subclasses must define class variables TAG and CONTEXT_VAR.  They can\n+    optionally override the method \'really_render(url)\' to control tag-specific\n+    rendering behavior.\n+    """\n+\n+    TAG = None\n+\n+    CONTEXT_VAR = None\n+\n+    def __init__(self, bundle_name, file_name):\n+        super(BundleNode, self).__init__()\n+        self.bundle_name = bundle_name\n+        self.file_name = file_name\n+\n+    def render(self, context):\n+        bundle = bundler.get_bundles()[self.bundle_name]\n+        if self.file_name not in bundle.files:\n+            msg = "File %r is not in bundle %r." % (self.file_name,\n+                                                    self.bundle_name)\n+            raise template.TemplateSyntaxError(msg)\n+        url_set = context_set_default(context, self.CONTEXT_VAR, set())\n+        if bundler_settings.USE_BUNDLES:\n+            url = bundle.get_bundle_url()\n+        else:\n+            url = bundle.url + self.file_name\n+        if url in url_set:\n+            return ""  # Don\'t add a bundle or css url twice.\n+        else:\n+            url_set.add(url)\n+            return self.really_render(context, url)\n+\n+    def really_render(self, context, url):\n+        """Implement bundle type specific rendering behavior."""\n+        return self.TAG % url\n+\n+\n+@register.tag\n+@bundle_tag\n+def javascript(bundle_name, script_name):\n+    """Tag to include JavaScript in the template."""\n+    return JavascriptNode(bundle_name, script_name)\n+\n+\n+class JavascriptNode(BundleNode):\n+\n+    """Add a script tag for a script or its bundle inline or at the bottom."""\n+\n+    TAG = \'<script type="text/javascript" src="%s"></script>\'\n+\n+    CONTEXT_VAR = "_script_urls"\n+\n+    def __init__(self, bundle_name, script_name):\n+        super(JavascriptNode, self).__init__(bundle_name, script_name)\n+\n+    def really_render(self, context, url):\n+        content = super(JavascriptNode, self).really_render(context, url)\n+        if bundler_settings.DEFER_JAVASCRIPT:\n+            deferred = context_set_default(context, "_deferred_content", [])\n+            deferred.append(content)\n+            return ""\n+        else:\n+            return content\n+\n+\n+@register.tag\n+@bundle_tag\n+def css(bundle_name, css_name):\n+    """Tag to include CSS in the template."""\n+    return CssNode(bundle_name, css_name)\n+\n+\n+class CssNode(BundleNode):\n+\n+    """Add link tags for a CSS file or its bundle."""\n+\n+    TAG = \'<link rel="stylesheet" type="text/css" href="%s"/>\'\n+\n+    CONTEXT_VAR = "_css_urls"\n+\n+    def __init__(self, bundle_name, css_name):\n+        super(CssNode, self).__init__(bundle_name, css_name)\n+\n+\n+@register.tag\n+def defer(parser, token):\n+    nodelist = parser.parse((\'enddefer\',))\n+    parser.delete_first_token()\n+    return DeferNode(nodelist)\n+\n+\n+class DeferNode(template.Node):\n+\n+    """Defer some content until later."""\n+\n+    def __init__(self, nodelist):\n+        super(DeferNode, self).__init__()\n+        self.nodelist = nodelist\n+\n+    def render(self, context):\n+        # We render the content in this context so that the scoping rules make\n+        # sense, ie all variables that seem to be in scope really are.\n+        content = self.nodelist.render(context)\n+        if bundler_settings.DEFER_JAVASCRIPT:\n+            deferred = context_set_default(context, "_deferred_content", [])\n+            deferred.append(content)\n+            return ""\n+        else:\n+            return content\n+\n+\n+@register.tag\n+def deferred_content(parser, token):\n+    """Tag to load deferred content."""\n+    return DeferredContentNode()\n+\n+\n+class DeferredContentNode(template.Node):\n+\n+    """Add script tags for deferred scripts."""\n+\n+    def render(self, context):\n+        return "\\n".join(context.get("_deferred_content", ()))\n', 'addition_count': 174, 'deletion_count': 0, 'src_file': '/dev/null', 'tgt_file': 'b/media_bundler/templatetags/bundler_tags.py', 'file_extension': '.py', 'before_file': 'ADDFILE', 'message': 'Initial working prototype, sure to be full of bugs.\n', 'author_name': 'Reid Kleckner', 'lang': 'py'}