Index: django/utils/html.py
===================================================================
--- django/utils/html.py	(revision 14504)
+++ django/utils/html.py	(working copy)
@@ -47,8 +47,9 @@
     """Converts newlines into <p> and <br />s."""
     value = re.sub(r'\r\n|\r|\n', '\n', force_unicode(value)) # normalize newlines
     paras = re.split('\n{2,}', value)
+    _escape = callable(autoescape) and autoescape or escape
     if autoescape:
-        paras = [u'<p>%s</p>' % escape(p).replace('\n', '<br />') for p in paras]
+        paras = [u'<p>%s</p>' % _escape(p).replace('\n', '<br />') for p in paras]
     else:
         paras = [u'<p>%s</p>' % p.replace('\n', '<br />') for p in paras]
     return u'\n\n'.join(paras)
@@ -91,6 +92,7 @@
 
     If autoescape is True, the link text and URLs will get autoescaped.
     """
+    _escape = callable(autoescape) and autoescape or escape
     trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
     safe_input = isinstance(text, SafeData)
     words = word_split_re.split(force_unicode(text))
@@ -116,19 +118,19 @@
             if url:
                 trimmed = trim_url(middle)
                 if autoescape and not safe_input:
-                    lead, trail = escape(lead), escape(trail)
-                    url, trimmed = escape(url), escape(trimmed)
+                    lead, trail = _escape(lead), _escape(trail)
+                    url, trimmed = _escape(url), _escape(trimmed)
                 middle = '<a href="%s"%s>%s</a>' % (url, nofollow_attr, trimmed)
                 words[i] = mark_safe('%s%s%s' % (lead, middle, trail))
             else:
                 if safe_input:
                     words[i] = mark_safe(word)
                 elif autoescape:
-                    words[i] = escape(word)
+                    words[i] = _escape(word)
         elif safe_input:
             words[i] = mark_safe(word)
         elif autoescape:
-            words[i] = escape(word)
+            words[i] = _escape(word)
     return u''.join(words)
 urlize = allow_lazy(urlize, unicode)
 
Index: django/utils/safestring.py
===================================================================
--- django/utils/safestring.py	(revision 14504)
+++ django/utils/safestring.py	(working copy)
@@ -117,3 +117,17 @@
         return EscapeUnicode(s)
     return EscapeString(str(s))
 
+def conditional_custom_escape(escape_function):
+    """ Return a new function that conditionally escapes its input using the
+        given escape function.
+    """
+    def _escape(value):
+        if isinstance(value, SafeData):
+            return value
+        else:
+            return escape_function(value)
+    for attr in ('autoescape_context', 'safe_filters'):
+        if hasattr(escape_function, attr):
+            setattr(_escape, attr, getattr(escape_function, attr))
+    return _escape
+
Index: django/template/defaultfilters.py
===================================================================
--- django/template/defaultfilters.py	(revision 14504)
+++ django/template/defaultfilters.py	(working copy)
@@ -32,8 +32,6 @@
         if args:
             args = list(args)
             args[0] = force_unicode(args[0])
-            if isinstance(args[0], SafeData) and getattr(func, 'is_safe', False):
-                return mark_safe(func(*args, **kwargs))
         return func(*args, **kwargs)
 
     # Include a reference to the real function (used to check original
@@ -163,14 +161,14 @@
         return input_val
 
     if not m and p < 0:
-        return mark_safe(formats.number_format(u'%d' % (int(d)), 0))
+        return formats.number_format(u'%d' % (int(d)), 0)
 
     if p == 0:
         exp = Decimal(1)
     else:
         exp = Decimal('1.0') / (Decimal(10) ** abs(p))
     try:
-        return mark_safe(formats.number_format(u'%s' % str(d.quantize(exp, ROUND_HALF_UP)), abs(p)))
+        return formats.number_format(u'%s' % str(d.quantize(exp, ROUND_HALF_UP)), abs(p))
     except InvalidOperation:
         return input_val
 floatformat.is_safe = True
@@ -183,7 +181,10 @@
 
 def linenumbers(value, autoescape=None):
     """Displays text with line numbers."""
-    from django.utils.html import escape
+    if callable(autoescape):
+        _escape = autoescape
+    else:
+        from django.utils.html import escape as _escape
     lines = value.split(u'\n')
     # Find the maximum width of the line count, for use with zero padding
     # string format command
@@ -193,7 +194,7 @@
             lines[i] = (u"%0" + width  + u"d. %s") % (i + 1, line)
     else:
         for i, line in enumerate(lines):
-            lines[i] = (u"%0" + width  + u"d. %s") % (i + 1, escape(line))
+            lines[i] = (u"%0" + width  + u"d. %s") % (i + 1, _escape(line))
     return mark_safe(u'\n'.join(lines))
 linenumbers.is_safe = True
 linenumbers.needs_autoescape = True
@@ -224,7 +225,7 @@
     import unicodedata
     value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
     value = unicode(re.sub('[^\w\s-]', '', value).strip().lower())
-    return mark_safe(re.sub('[-\s]+', '-', value))
+    return re.sub('[-\s]+', '-', value)
 slugify.is_safe = True
 slugify = stringfilter(slugify)
 
@@ -373,16 +374,17 @@
 center.is_safe = True
 center = stringfilter(center)
 
-def cut(value, arg):
+def cut(value, arg, autoescape=None):
     """
     Removes all values of arg from the given string.
     """
     safe = isinstance(value, SafeData)
     value = value.replace(arg, u'')
-    if safe and arg != ';':
+    if safe and arg != ';' and not callable(autoescape):
         return mark_safe(value)
     return value
 cut = stringfilter(cut)
+cut.needs_autoescape = True
 
 ###################
 # HTML STRINGS    #
@@ -397,16 +399,20 @@
 escape.is_safe = True
 escape = stringfilter(escape)
 
-def force_escape(value):
+def force_escape(value, autoescape=None):
     """
     Escapes a string's HTML. This returns a new string containing the escaped
     characters (as opposed to "escape", which marks the content for later
     possible escaping).
     """
-    from django.utils.html import escape
-    return mark_safe(escape(value))
+    if callable(autoescape):
+        return mark_safe(autoescape(value))
+    else:
+        from django.utils.html import escape
+        return mark_safe(_escape(value))
 force_escape = stringfilter(force_escape)
 force_escape.is_safe = True
+force_escape.needs_autoescape = True
 
 def linebreaks(value, autoescape=None):
     """
@@ -415,7 +421,7 @@
     followed by a blank line becomes a paragraph break (``</p>``).
     """
     from django.utils.html import linebreaks
-    autoescape = autoescape and not isinstance(value, SafeData)
+    autoescape = not isinstance(value, SafeData) and autoescape
     return mark_safe(linebreaks(value, autoescape))
 linebreaks.is_safe = True
 linebreaks.needs_autoescape = True
@@ -427,8 +433,11 @@
     (``<br />``).
     """
     if autoescape and not isinstance(value, SafeData):
-        from django.utils.html import escape
-        value = escape(value)
+        if callable(autoescape):
+            value = autoescape(value)
+        else:
+            from django.utils.html import escape
+            value = escape(value)
     return mark_safe(value.replace('\n', '<br />'))
 linebreaksbr.is_safe = True
 linebreaksbr.needs_autoescape = True
@@ -510,10 +519,11 @@
     Joins a list with a string, like Python's ``str.join(list)``.
     """
     value = map(force_unicode, value)
+    _escape = callable(autoescape) and autoescape.conditional or conditional_escape
     if autoescape:
-        value = [conditional_escape(v) for v in value]
+        value = [_escape(v) for v in value]
     try:
-        data = conditional_escape(arg).join(value)
+        data = _escape(arg).join(value)
     except AttributeError: # fail silently but nicely
         return value
     return mark_safe(data)
@@ -591,7 +601,9 @@
         </ul>
         </li>
     """
-    if autoescape:
+    if callable(autoescape):
+        escaper = autoescape.conditional
+    elif autoescape:
         from django.utils.html import conditional_escape
         escaper = conditional_escape
     else:
Index: django/template/__init__.py
===================================================================
--- django/template/__init__.py	(revision 14504)
+++ django/template/__init__.py	(working copy)
@@ -566,18 +566,29 @@
                         obj = settings.TEMPLATE_STRING_IF_INVALID
         else:
             obj = self.var
+
+        # Custom autoescape functions can define a context other than html 
+        # to prevent html functions from marking output as safe
+        ae_context = getattr(context.autoescape, 'autoescape_context', 'html')
+        # Custom autoescape functions can define a list of safe filter names
+        ae_safe_filters = getattr(context.autoescape, 'safe_filters', set())
+
         for func, args in self.filters:
+            func_ae_context = getattr(func, 'autoescape_context', 'html')
             arg_vals = []
             for lookup, arg in args:
                 if not lookup:
                     arg_vals.append(mark_safe(arg))
                 else:
                     arg_vals.append(arg.resolve(context))
+
+
             if getattr(func, 'needs_autoescape', False):
                 new_obj = func(obj, autoescape=context.autoescape, *arg_vals)
             else:
                 new_obj = func(obj, *arg_vals)
-            if getattr(func, 'is_safe', False) and isinstance(obj, SafeData):
+            if (getattr(func, 'is_safe', False) and isinstance(obj, SafeData) 
+                    and (ae_context == func_ae_context or func.__name__ in ae_safe_filters)):
                 obj = mark_safe(new_obj)
             elif isinstance(obj, EscapeData):
                 obj = mark_for_escaping(new_obj)
@@ -828,7 +839,10 @@
     value = localize(value, use_l10n=context.use_l10n)
     value = force_unicode(value)
     if (context.autoescape and not isinstance(value, SafeData)) or isinstance(value, EscapeData):
-        return escape(value)
+        if callable(context.autoescape): 
+            return context.autoescape(value) 
+        else: 
+            return escape(value) 
     else:
         return value
 
Index: django/template/context.py
===================================================================
--- django/template/context.py	(revision 14504)
+++ django/template/context.py	(working copy)
@@ -1,5 +1,6 @@
 from django.core.exceptions import ImproperlyConfigured
 from django.utils.importlib import import_module
+from django.utils.safestring import conditional_custom_escape
 
 # Cache of actual callables.
 _standard_context_processors = None
@@ -67,6 +68,8 @@
 class Context(BaseContext):
     "A stack container for variable context"
     def __init__(self, dict_=None, autoescape=True, current_app=None, use_l10n=None):
+        if callable(autoescape):
+            autoescape.conditional = conditional_custom_escape(autoescape)
         self.autoescape = autoescape
         self.use_l10n = use_l10n
         self.current_app = current_app
Index: django/template/debug.py
===================================================================
--- django/template/debug.py	(revision 14504)
+++ django/template/debug.py	(working copy)
@@ -97,6 +97,9 @@
         except UnicodeDecodeError:
             return ''
         if (context.autoescape and not isinstance(output, SafeData)) or isinstance(output, EscapeData):
-            return escape(output)
+            if callable(context.autoescape): 
+                return context.autoescape(output) 
+            else: 
+                return escape(output)
         else:
             return output
