diff --git a/djblets/util/decorators.py b/djblets/util/decorators.py
index fbcae2c4ed2db038dfb004307a80618b0b775631..db0c4aff49d25c0921b2b3964ecb57b5c845e48e 100644
--- a/djblets/util/decorators.py
+++ b/djblets/util/decorators.py
@@ -229,12 +229,12 @@ def blocktag(*args, **kwargs):
             tag_name = bits[0]
             del(bits[0])
 
-            params, xx, xxx, defaults = getargspec(tag_func)
+            params, varargs, xxx, defaults = getargspec(tag_func)
             max_args = len(params) - 2  # Ignore context and nodelist
             min_args = max_args - len(defaults or [])
 
-            if not min_args <= len(bits) <= max_args:
-                if min_args == max_args:
+            if len(bits) < min_args or (not varargs and len(bits) > max_args):
+                if not varargs and min_args == max_args:
                     raise TemplateSyntaxError(
                         "%r tag takes %d arguments." % (tag_name, min_args))
                 else:
diff --git a/djblets/util/templatetags/djblets_utils.py b/djblets/util/templatetags/djblets_utils.py
index 8a8a0477643b7821e922574a28651a8ed4c3c208..7c06192269fc556a3bb3c2f41b0f264257b54326 100644
--- a/djblets/util/templatetags/djblets_utils.py
+++ b/djblets/util/templatetags/djblets_utils.py
@@ -27,12 +27,14 @@ from __future__ import unicode_literals
 
 import datetime
 import os
+import re
 
 from django import template
 from django.template import TemplateSyntaxError, Variable
 from django.template.defaultfilters import stringfilter
 from django.template.loader import render_to_string
-from django.utils.html import escape
+from django.utils import six
+from django.utils.html import escape, format_html, strip_spaces_between_tags
 from django.utils.safestring import mark_safe
 from django.utils.six.moves.urllib.parse import urlencode
 from django.utils.timezone import is_aware
@@ -46,28 +48,39 @@ from djblets.util.humanize import humanize_list
 register = template.Library()
 
 
+WS_RE = re.compile('\s+')
+
+
 @register.tag
 @blocktag(resolve_vars=False)
-def definevar(context, nodelist, varname, stripped=None):
+def definevar(context, nodelist, varname, *options):
     """Define a variable for later use in the template.
 
     The variable defined can be used within the same context (such as the
     same block or for loop). This is useful for caching a portion of a
     template that would otherwise be expensive to repeatedly compute.
 
-    Callers can also pass an additional ``stripped`` argument after the
-    variable name to cause the contents to be stripped before being stored.
-
-    .. versionadded: 0.10
+    .. versionadded:: 0.10
 
-       Added the ``stripped`` argument.
+       Added new ``strip``, ``spaceless``, and ``unsafe`` options.
 
     Args:
         varname (unicode):
             The variable name.
 
-        stripped (unicode, optional):
-            If ``stripped`` is passed, the contents will be stripped.
+        *options (list of unicode, optional):
+            A list of options passed. This supports the following:
+
+            ``strip``:
+                Strip whitespace at the beginning/end of the value.
+
+            ``spaceless``:
+                Strip whitespace at the beginning/end and remove all spaces
+                between tags. This implies ``strip``.
+
+            ``unsafe``:
+                Mark the text as unsafe. The contents will be HTML-escaped when
+                inserted into the page.
 
         block_content (unicode):
             The block content to set in the variable.
@@ -75,20 +88,33 @@ def definevar(context, nodelist, varname, stripped=None):
     Example:
         .. code-block:: html+django
 
-           {% definevar "myvar1" %}{% expensive_tag %}{% enddefinevar %}
-           {% definevar "myvar2" stripped %}
+           {% definevar "myvar1" %}
            {%  expensive_tag %}
            {% enddefinevar %}
 
+           {% definevar "myvar2" spaceless %}
+           <div>
+            <a href="#">Click me!</a>
+           </div>
+           {% enddefinevar %}
+
            {{myvar1}}
            {{myvar2}}
     """
     varname = Variable(varname).resolve(context)
     result = nodelist.render(context)
 
-    if stripped == 'stripped':
+    if 'spaceless' in options:
+        result = strip_spaces_between_tags(result.strip())
+    elif 'strip' in options:
         result = result.strip()
 
+    if 'unsafe' in options:
+        # Unicode strings are inherently "unsafe".
+        result = six.text_type(result)
+    else:
+        result = mark_safe(result)
+
     context[varname] = result
 
     return ''
@@ -224,25 +250,36 @@ def include_as_string(context, template_name):
 
 
 @register.tag
-@blocktag
-def attr(context, nodelist, attrname):
+@blocktag(resolve_vars=False)
+def attr(context, nodelist, attrname, *options):
     """Set an HTML attribute to a value if the value is not an empty string.
 
     This is a handy way of adding attributes with non-empty values to an
     HTML element without requiring several `{% if %}` tags.
 
-    The contents will be stripped before being considered or rendered.
-    Whitespace should not be expected to be preserved.
+    The contents will be stripped and all whitespace within condensed before
+    being considered or rendered. This can be turned off (restoring pre-0.10
+    behavior) by passing ``nocondense`` as an option.
 
     .. versionchanged:: 0.10
 
-       Prior to this release, whitespace before/after the attribute value
-       was preserved.
+       Prior to this release, all whitespace before/after/within the
+       attribute value was preserved. Now ``nocondense`` is required for this
+       behavior.
+
+       The value is now escaped as well. Previously the value was assumed to
+       be safe, requiring the consumer to escape the contents.
 
     Args:
         attrname (unicode):
             The name for the HTML attribute.
 
+        *options (list unicode, optional):
+            A list of options passed. This supports the following:
+
+            ``nocondense``:
+                Preserves all whitespace in the value.
+
         block_content (unicode):
             The block content to render for the attribute value.
 
@@ -253,14 +290,21 @@ def attr(context, nodelist, attrname):
     Example:
         .. code-block:: html+django
 
-           <div{% attr "data-description" %}{{obj.description}}{% endattr %}>
+           <div{% attr "class" %}{{obj.name}}{% endattr %}>
+           <div{% attr "data-description" nocondense %}
+           Space-sensitive     whitspace.
+           {% endattr %}>
     """
-    content = nodelist.render(context).strip()
+    attrname = Variable(attrname).resolve(context)
+    content = nodelist.render(context)
+
+    if 'nocondense' not in options:
+        content = WS_RE.sub(' ', content.strip())
 
     if not content:
         return ''
 
-    return ' %s="%s"' % (attrname, content)
+    return format_html(' {0}="{1}"', attrname, six.text_type(content))
 
 
 @register.filter
diff --git a/djblets/util/templatetags/tests.py b/djblets/util/templatetags/tests.py
index 260409c53c844583ff080c32b01aa7760b35bc46..b1d19469a917438f6665c9b55dcd7f71052ff5bc 100644
--- a/djblets/util/templatetags/tests.py
+++ b/djblets/util/templatetags/tests.py
@@ -54,23 +54,66 @@ class UtilsTagTests(TestCase):
             })),
             '<span>')
 
+    def test_attr_escapes_value(self):
+        """Testing attr template tag escapes value"""
+        t = Template('{% load djblets_utils %}'
+                     '<span{% attr "data-foo" %}<hello>{% endattr %}>')
+
+        self.assertEqual(
+            t.render(Context()),
+            '<span data-foo="&lt;hello&gt;">')
+
+    def test_attr_condenses_whitespace(self):
+        """Testing attr template tag condenses/strips extra whitespace by
+        default
+        """
+        t = Template('{% load djblets_utils %}'
+                     '<span{% attr "data-foo" %}\n'
+                     'some    \n\n'
+                     'value\n'
+                     '{% endattr %}>')
+
+        self.assertEqual(
+            t.render(Context()),
+            '<span data-foo="some value">')
+
+    def test_attr_with_nocondense_preserves_whitespace(self):
+        """Testing attr template tag with "nocondense" option preserves
+        whitespace
+        """
+        t = Template('{% load djblets_utils %}'
+                     '<span{% attr "data-foo" nocondense %}\n'
+                     'some    \n\n'
+                     'value\n'
+                     '{% endattr %}>')
+
+        self.assertEqual(
+            t.render(Context()),
+            '<span data-foo="\nsome    \n\nvalue\n">')
+
     def test_definevar(self):
         """Testing definevar template tag"""
         t = Template('{% load djblets_utils %}'
-                     '{% definevar "myvar" %}test{{num}}{% enddefinevar %}'
+                     '{% definevar "myvar" %}\n'
+                     'test{{num}}\n'
+                     '{% enddefinevar %}'
                      '{{myvar}}')
 
         self.assertEqual(
             t.render(Context({
                 'num': 123,
             })),
-            'test123')
+            '\ntest123\n')
 
-    def test_definevar_with_stripped(self):
-        """Testing definevar template tag with stripped argument"""
+    def test_definevar_with_strip(self):
+        """Testing definevar template tag with strip option"""
         t = Template('{% load djblets_utils %}'
-                     '{% definevar "myvar" stripped %}\n'
-                     '    test{{num}}\n'
+                     '{% definevar "myvar" strip %}\n'
+                     '<span>\n'
+                     ' <strong>\n'
+                     '  test{{num}}\n'
+                     ' </strong>\n'
+                     '</span>\n'
                      '{% enddefinevar %}'
                      '[{{myvar}}]')
 
@@ -78,7 +121,33 @@ class UtilsTagTests(TestCase):
             t.render(Context({
                 'num': 123,
             })),
-            '[test123]')
+            '[<span>\n <strong>\n  test123\n </strong>\n</span>]')
+
+    def test_definevar_with_spaceless(self):
+        """Testing definevar template tag with spaceless option"""
+        t = Template('{% load djblets_utils %}'
+                     '{% definevar "myvar" spaceless %}\n'
+                     '<span>\n'
+                     ' <strong>\n'
+                     '  test{{num}}\n'
+                     ' </strong>\n'
+                     '</span>\n'
+                     '{% enddefinevar %}'
+                     '[{{myvar}}]')
+
+        self.assertEqual(
+            t.render(Context({
+                'num': 123,
+            })),
+            '[<span><strong>\n  test123\n </strong></span>]')
+
+    def test_definevar_with_unsafe(self):
+        """Testing definevar template tag with unsafe option"""
+        t = Template('{% load djblets_utils %}'
+                     '{% definevar "myvar" unsafe %}<hello>{% enddefinevar %}'
+                     '{{myvar}}')
+
+        self.assertEqual(t.render(Context()), '&lt;hello&gt;')
 
     def test_include_as_string_tag(self):
         """Testing include_as_string template tag"""
