diff --git a/djblets/compress/filters/lesscss.py b/djblets/compress/filters/lesscss.py
new file mode 100644
index 0000000000000000000000000000000000000000..6038c3167a93c1c821621fb3d5af5d603316d9b2
--- /dev/null
+++ b/djblets/compress/filters/lesscss.py
@@ -0,0 +1,78 @@
+import mimetools
+import os
+import re
+import urllib2
+
+try:
+    from cStringIO import StringIO
+except ImportError:
+    from StringIO import StringIO
+
+from compress.filter_base import FilterBase, FilterError
+from django.conf import settings
+
+
+LESSCSS_URL = getattr(settings, 'LESSCSS_URL',
+                      'http://blesscss.cloudfoundry.com/min')
+LESSCSS_IMPORT_PATHS = getattr(settings, 'LESSCSS_IMPORT_PATHS', [])
+
+
+class LessCSSFilter(FilterBase):
+    IMPORT_RE = re.compile(r'^@import "([^"]+)";')
+
+    def filter_css(self, lesscss):
+        if self.verbose:
+            print 'Converting lesscss using %s' % LESSCSS_URL
+
+        boundary = mimetools.choose_boundary()
+
+        content  = '--%s\r\n' % boundary
+        content += 'Content-Disposition: form-data; name="style.less"\r\n'
+        content += '\r\n'
+
+        for line in lesscss.splitlines(True):
+            m = self.IMPORT_RE.match(line)
+
+            if m:
+                filename = m.group(1)
+
+                if (not filename.endswith(".css") and
+                    not filename.endswith(".less")):
+                    filename += '.less'
+
+                line = self._load_import(filename)
+
+            content += line
+
+        content += '\r\n'
+        content += '--%s--\r\n' % boundary
+        content += '\r\n'
+
+        headers = {
+            'Content-Type': 'multipart/form-data; boundary=%s' % boundary,
+            'Content-Length': str(len(content)),
+        }
+
+        r = urllib2.Request(LESSCSS_URL, content, headers)
+
+        try:
+            return urllib2.urlopen(r).read()
+        except urllib2.HTTPError, e:
+            if e.code == 400:
+                raise FilterError("Error processing lessCSS files: %s" %
+                                  e.read())
+
+            raise
+
+    def _load_import(self, filename):
+        for import_path in LESSCSS_IMPORT_PATHS:
+            path = os.path.join(settings.MEDIA_ROOT, import_path, filename)
+
+            if os.path.exists(path):
+                fp = open(path, 'r')
+                content = fp.read()
+                fp.close()
+
+                return content
+
+        raise FilterError('Unable to find import file "%s"' % filename)
diff --git a/djblets/compress/templates/compress/css.html b/djblets/compress/templates/compress/css.html
new file mode 100644
index 0000000000000000000000000000000000000000..6569f91a40038f657751cf452a1cf93be7c20b31
--- /dev/null
+++ b/djblets/compress/templates/compress/css.html
@@ -0,0 +1,2 @@
+{% load djblets_utils %}
+<link href="{{url}}{% if MEDIA_SERIAL %}?{{MEDIA_SERIAL}}{% endif %}" rel="stylesheet{% if url|endswith:"less" %}/less{% endif %}" type="text/css"{% if media %} media="{{media}}"{% endif %}{% if title %} title="{{title|default:"all"}}"{% endif %}{% if charset %} charset="{{charset}}"{% endif %} />
diff --git a/djblets/compress/templates/compress/css_ie.html b/djblets/compress/templates/compress/css_ie.html
new file mode 100644
index 0000000000000000000000000000000000000000..992986b083ee952dc3da7c3bb2b089bedc626801
--- /dev/null
+++ b/djblets/compress/templates/compress/css_ie.html
@@ -0,0 +1,2 @@
+{% load djblets_utils %}
+<!--[if {{condition|default:"IE"}}]><link href="{{url}}{% if MEDIA_SERIAL %}?{{MEDIA_SERIAL}}{% endif %}" rel="stylesheet{% if url|endswith:"less" %}/less{% endif %}" type="text/css" media="{{media|default:"all"}}" title="{{title|default:"all"}}" charset="utf-8" /><![endif]-->
diff --git a/djblets/compress/templates/compress/js.html b/djblets/compress/templates/compress/js.html
new file mode 100644
index 0000000000000000000000000000000000000000..3a9509c66c8b5ff154b1902f7ac4325dd7bba998
--- /dev/null
+++ b/djblets/compress/templates/compress/js.html
@@ -0,0 +1 @@
+<script type="text/javascript" src="{{url}}{% if MEDIA_SERIAL %}?{{MEDIA_SERIAL}}{% endif %}" charset="utf-8"></script>
diff --git a/djblets/compress/templates/compress/js_ie.html b/djblets/compress/templates/compress/js_ie.html
new file mode 100644
index 0000000000000000000000000000000000000000..f8cc2ba1861146a9add9271bcea31689c963a2ad
--- /dev/null
+++ b/djblets/compress/templates/compress/js_ie.html
@@ -0,0 +1 @@
+<!--[if {{condition|default:"IE"}}]><script type="text/javascript" src="{{url}}{% if MEDIA_SERIAL %}?{{MEDIA_SERIAL}}{% endif %}" charset="utf-8"></script><![endif]-->
diff --git a/djblets/util/templatetags/djblets_utils.py b/djblets/util/templatetags/djblets_utils.py
index e0c47cb0362075795dcccb5782b93a76a6e70078..6c7482ed44669f1c83d12c3af0594c29282464f6 100644
--- a/djblets/util/templatetags/djblets_utils.py
+++ b/djblets/util/templatetags/djblets_utils.py
@@ -304,6 +304,13 @@ def startswith(value1, value2):
 
 @register.filter
 @stringfilter
+def endswith(value1, value2):
+    """Returns true if value1 ends with value2."""
+    return value1.endswith(value2)
+
+
+@register.filter
+@stringfilter
 def paragraphs(text):
     """
     Adds <p>...</p> tags around blocks of text in a string. This expects
