Commits

Daniel LaMotte committed 5216ebb

haml: initial commit

Comments (0)

Files changed (17)

+haml and sass (for python)
+=================
+
+***Note: sass is currently not implemented, but it's coming***
+
+There are a few other implementations, but didn't seem to support as many
+features of haml as well as integrating well with typical python template
+languages.  This was written with Django in mind, but made room for other
+python web frameworks to integrate.  Eventually, modules similar to the
+`haml/dj.py` for Django will be included with this package for each framework
+because easy integration is important for adoption.
+
+Probably the main difference between this library and the actual haml ruby
+implementation is that this implementation strives to "compile" into a
+different template language and let the underlying template engine (like
+Jinja2 and Django's default) do what they do best.
+
+Django Installation/Setup
+=========================
+
+In `settings.py`, modify `TEMPLATE_LOADERS` like:
+
+    TEMPLATE_LOADERS = (
+        'haml.dj.FSLoader',
+        'haml.dj.AppLoader',
+    )
+
+These replace your usual Django loaders:
+
+    django.template.loaders.filesystem.Loader
+    django.template.loaders.app_directories.Loader
+
+Quick Overview
+=========================
+
+Simple document
+---------------
+
+    %element
+        %subelement
+            some text
+
+Compiles to:
+
+    <element>
+        <subelement>
+            some text
+        </subelement>
+    </element>
+
+Embedded HTML
+-------------
+
+    %p
+        <div id="blah">embedded html</div>
+
+Compiles to:
+
+    <p>
+        <div id="blah">embedded html</div>
+
+    </p>
+
+Attributes
+----------
+
+    %link{ref="stylesheet", type="text/css", href="media/css/style.css"}
+
+Compiles to:
+
+    <link href="media/css/style.css" ref="stylesheet" type="text/css" />
+
+Or with a template variable:
+
+    %link{ref="stylesheet", type="text/css", href=STATIC_URL + "css/style.css"}
+
+Compiles to:
+
+    <link href="{{ STATIC_URL }}css/style.css" ref="stylesheet" type="text/css" />
+
+DOCTYPE
+-------
+
+    !html
+
+Compiles to:
+
+    <!DOCTYPE html>
+
+***Note: Ruby implementation uses "!!!..." where "..." is a shortcut. This
+implementation does not provide these shortcuts, but allows anything after the
+exclamation to be passed through directly***
+
+Classes and IDs
+---------------
+
+    %div.myclass
+        %div#myid
+            %div.class1.class2
+                text
+
+Compiles to:
+
+    <div class="myclass">
+        <div id="myid">
+            <div class="class1 class2">
+                text
+            </div>
+        </div>
+    </div>
+
+As a shortcut, `div` is assumed.  The following also compiles to the above:
+
+    .myclass
+        #myid
+            .class1.class2
+                text
+
+Self-Closing Tags
+-----------------
+
+    %mytag/
+
+Compiles to:
+
+    <mytag />
+
+Many tags are automatically closed
+(`meta`, `img`, `link`, `br`, `hr`, `input`, `area`, `param`, `col`, `base`):
+
+    %br
+
+Compiles to:
+
+    <br />
+
+Whitespace Removal
+------------------
+
+Consume space inside tag:
+
+    %blockquote<
+        %div
+            foo
+
+Compiles to:
+
+    <blockquote><div>
+            foo
+    </div></blockquote>
+
+Consume space outside tag:
+
+    %img
+    %img>
+    %img
+
+Compiles to:
+
+    <img /><img /><img />
+
+
+Put it all together:
+
+    %img
+    %pre><
+        foo
+        bar
+    %img
+
+Compiles to:
+
+    <img /><pre>foo
+        bar</pre><img />
+
+Comments
+--------
+
+    / html comment
+
+Compiles to:
+
+    <!-- html comment -->
+
+And a block comment:
+
+    /
+        an
+        html
+        block
+        comment
+
+Compiles to:
+
+    <!-- 
+        an
+        html
+        block
+        comment
+     -->
+
+Conditional Comments
+--------------------
+
+    /[if IE]
+        %link{ref="stylesheet", type="text/css", href="media/css/ie.css"}
+
+Compiles to:
+
+    <!--[if IE]>
+        <link href="media/css/ie.css" ref="stylesheet" type="text/css" />
+    <![endif]-->
+
+
+haml comments
+-------------
+
+    -# haml comment
+
+The compiler ignores these.
+
+Filters
+-------
+
+Filters preserve everything indented below them and wrap their contents in some
+kind of tag (sometimes).
+
+Example filter:
+
+    :javascript
+        $(function() {
+            alert("document loaded");
+        });
+
+Compiles to:
+
+    <script type="text/javascript">
+        $(function() {
+            alert("document loaded");
+        });
+    </script>
+
+Supported Filters:
+
+    :css
+        ...
+
+Compiles to:
+
+    <style type="text/css">
+        ...
+    </style>
+
+The `:plain` filter simply ignores all formatting that would normally be
+interpreted by the haml compiler.
+
+    :plain
+        ...
+
+Compiles to:
+
+        ...
+
+    :javascript
+        ...
+
+Compiles to:
+
+    <script type="text/javascript">
+        ...
+    </script>
+
+Django Template Integration
+---------------------------
+
+Template Variables:
+
+    %p= variable
+
+Compiles to:
+
+    <p>{{ variable }}</p>
+
+    %p
+        = one
+        = two
+        = three
+
+Compiles to:
+
+    <p>
+        {{ one }}
+        {{ two }}
+        {{ three }}
+    </p>
+
+
+Template Tags:
+
+    - if variable
+        %p some text
+
+Compiles to:
+
+    {% if variable %}
+        <p>some text</p>
+    {% endif %}
+
+    - extends "base.html"
+
+Compiles to:
+
+    {% extends "base.html" %}
+
+There is a list of common tags that require an **end** tag, however, if this
+module doesn't currently auto-end a tag, file an issue for common tags and/or
+work around it like this:
+
+    - mytag variable
+        some text
+    - endmytag
+
+Compiles to:
+
+    {% mytag variable %}
+        some text
+    {% endmytag %}
+
+Example Django Project
+===============
+
+See the `example/` subdirectory for an example project that uses this.

example/__init__.py

Empty file added.

example/manage.py

+#!/usr/bin/env python
+from django.core.management import execute_manager
+try:
+    import settings # Assumed to be in the same directory.
+except ImportError:
+    import sys
+    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+    sys.exit(1)
+
+if __name__ == "__main__":
+    execute_manager(settings)

example/settings.py

+import os
+
+PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
+
+TEMPLATE_LOADERS = (
+    'haml.dj.FSLoader',
+    'haml.dj.AppLoader',
+    #'django.template.loaders.filesystem.Loader', XXX no longer necessary
+    #'django.template.loaders.app_directories.Loader', XXX no longer necessary
+)
+TEMPLATE_DIRS = (
+    os.path.join(PROJECT_DIR, 'templates'),
+)
+
+# everything below is unimportant
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+MANAGERS = ADMINS = ()
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': 'example.db',
+        'USER': '',
+        'PASSWORD': '',
+        'HOST': '',
+        'PORT': '',
+    }
+}
+
+TIME_ZONE = 'America/Chicago'
+LANGUAGE_CODE = 'en-us'
+SITE_ID = 1
+USE_I18N = True
+USE_L10N = True
+
+MEDIA_ROOT = ''
+MEDIA_URL = ''
+ADMIN_MEDIA_PREFIX = '/media/'
+SECRET_KEY = '3b7gb3rwf)hpw2&rs06!xdy2^68ak2yct=ny*ofsf4au2^q$64'
+
+MIDDLEWARE_CLASSES = (
+    'django.middleware.common.CommonMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+)
+
+ROOT_URLCONF = 'example.urls'
+
+INSTALLED_APPS = (
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.sites',
+    'django.contrib.messages',
+    'django.contrib.admin',
+)

example/templates/base.html

+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Example of mixing templates</title>
+  </head>
+  <body>
+{% block content %}{% endblock %}
+  </body>
+</html>

example/templates/index.haml

+!html
+%html
+    %head
+        %title example haml template
+    %body
+        %p example haml template
+        - if 1
+            %p rendered if true inside a django template tag
+
+        .implicitdiv
+            %a{href='/mixed/'} example of mixing .haml and other templates

example/templates/subbase.haml

+- extends "base.html"
+
+- block content
+    %p main content is haml compiled while base.html is not
+from django.conf.urls.defaults import *
+
+from django.contrib import admin
+admin.autodiscover()
+
+urlpatterns = patterns('',
+    (r'^admin/', include(admin.site.urls)),
+)
+
+urlpatterns += patterns('django.views.generic.simple',
+    ('^$', 'direct_to_template', {
+        'template': 'index.haml',
+    }),
+    ('^mixed/$', 'direct_to_template', {
+        'template': 'subbase.haml',
+    }),
+)

haml/__init__.py

Empty file added.
+from haml import util
+
+import regex
+
+class Node(object):
+    autoclose_tags = [
+        'meta',
+        'img',
+        'link',
+        'br',
+        'hr',
+        'input',
+        'area',
+        'param',
+        'col',
+        'base'
+    ]
+    kinds = (
+        'element',
+        'doctype',
+        'html_cond_comment',
+        'html_comment',
+        'comment',
+        'plain',
+        'variable',
+        'tag',
+    )
+    tags_to_close = (
+        'autoescape',
+        'block',
+        'comment',
+        'filter',
+        'for',
+        'if',
+        'ifchanged',
+        'ifequal',
+        'ifnotequal',
+        'spaceless',
+        'with',
+    )
+
+    def __init__(self, content, match):
+        self.children = []
+        self.content = content
+        self.match = match
+        self.plain = self.match['plain']
+
+    def __iadd__(self, other):
+        # XXX only makes sense at the moment for plain nodes
+        self.plain += '\n' + other.match['indent'] + other.plain
+        return self
+
+    def build_attrs(self, kvs):
+        attrs = {}
+        for key, value in kvs:
+            v = ''
+            for part in value.split('+'):
+                part = part.strip()
+                if part.startswith('"'):
+                    if not part.endswith('"'):
+                        raise self.ParseError(
+                            'part missing ending double quote: %s' % part
+                        )
+                    v += part[1:-1]
+                elif part.startswith("'"):
+                    if not part.endswith("'"):
+                        raise self.ParseError(
+                            'part missing ending single quote: %s' % part
+                        )
+                    v += part[1:-1]
+                else:
+                    v += self.template_variable(part)
+            attrs[key] = v
+        return attrs
+
+    def closed(self):
+        try:
+            return getattr(self, 'closed_%s' % self.kind)()
+        except AttributeError:
+            return True
+
+    def closed_element(self):
+        tag, ids, classes, attrs, close, has_content = self._render_element()
+        if has_content:
+            return True
+        return close
+
+    def closed_html_comment(self):
+        return bool(self.match['html_comment'][1:])
+
+    def closed_html_cond_comment(self):
+        return False
+
+    def closed_tag(self):
+        return not self.tag_name() in self.tags_to_close
+
+    def iter_children(self):
+        assert self.children # required
+
+        kwargs = util.DictAttr()
+        kwargs.prev = self
+        kwargs.prev_sibling = False
+        curr = None
+        for child in self.children:
+            if curr is not None:
+                if curr.kind == 'plain' and child.kind == 'plain':
+                    curr += child
+                    continue
+
+                else:
+                    kwargs.next = child
+                    kwargs.next_sibling = True
+                    yield curr, kwargs
+                    kwargs.prev = curr
+                    kwargs.prev_sibling = True
+            curr = child
+
+        kwargs.next = self
+        kwargs.next_sibling = False
+        yield curr, kwargs
+
+    @property
+    def kind(self):
+        for name in self.kinds:
+            if self.match[name]:
+                return name
+        return ''
+
+    def parse_elements(self, elements):
+        tag = None
+        ids = []
+        classes = []
+        for el in elements:
+            if el.startswith('%'):
+                if tag is not None:
+                    raise self.ParseError('multiple tags specified')
+                tag = el[1:]
+
+            elif el.startswith('.'):
+                classes.append(el[1:])
+
+            elif el.startswith('#'):
+                ids.append(el[1:])
+
+            else:
+                raise self.ParseError('unknown element type: %s' % el)
+        return tag, ids, classes
+
+    def render(self, stream,
+               prev=None, prev_sibling=False,
+               next=None, next_sibling=False):
+        content_or_f = getattr(self, 'render_%s' % self.kind, self.content)
+        if callable(content_or_f):
+            indent = self.match['indent']
+            starttag, content, endtag = content_or_f()
+            if not (prev and not prev_sibling and prev.match['consume_within']) \
+                and not self.match['consume_around'] \
+                and not (prev and prev.match['consume_around']):
+                stream.write(indent)
+            if starttag:
+                stream.write(starttag)
+                if content:
+                    stream.write(content)
+
+                if endtag:
+                    if self.children:
+                        if not self.match['consume_within']:
+                            stream.write('\n')
+
+                        for child, kwargs in self.iter_children():
+                            child.render(stream, **kwargs)
+
+                        if not self.match['consume_within']:
+                            if not (next and not next_sibling and next.match['consume_within']):
+                                stream.write(indent)
+                            else:
+                                stream.write(next.match['indent'])
+
+                    stream.write(endtag)
+                    if not (next and not next_sibling and next.match['consume_within']) \
+                        and not self.match['consume_around']:
+                        stream.write('\n')
+
+                else:
+                    if not (next and next.match['consume_around']) \
+                        and not self.match['consume_around']:
+                        stream.write('\n')
+
+            elif content:
+                stream.write(content)
+                if not (prev and not prev_sibling and prev.match['consume_within']):
+                    stream.write('\n')
+        else:
+            stream.write(content_or_f)
+            stream.write('\n')
+
+    def render_comment(self):
+        return '', '', ''
+
+    def render_doctype(self):
+        return '<!DOCTYPE %s>' % self.match['doctype'][1:], '', ''
+
+    def _render_element(self):
+        keys = []
+        values = []
+        keys.extend(self.match.captures('key'))
+        keys.extend(self.match.captures('key2'))
+        values.extend(self.match.captures('value'))
+        values.extend(self.match.captures('value2'))
+
+        tag, ids, classes = self.parse_elements(self.match.captures('element'))
+        attrs = self.build_attrs(zip(keys, values))
+
+        if tag is None:
+            tag = 'div'
+
+        close = self.match['close'] or tag.lower() in self.autoclose_tags
+
+        return tag, ids, classes, attrs, close, bool(self.match['content'])
+
+    def render_element(self):
+        tag, ids, classes, attrs, close, has_content = self._render_element()
+
+        if 'id' in attrs:
+            ids.append(attrs.pop('id'))
+        if 'class' in attrs:
+            classes.append(attrs.pop('class'))
+
+        starttag = '<%s' % tag
+        if ids:
+            starttag += ' id="%s"' % '_'.join(ids)
+        if classes:
+            starttag += ' class="%s"' % ' '.join(classes)
+        for key, value in sorted(attrs.items(), key=lambda x: x[0]):
+            starttag += ' %s="%s"' % (key, value)
+
+        if close:
+            starttag += ' /'
+        starttag += '>'
+
+        content = ''
+        endtag = ''
+        if self.match['content']:
+            if self.match['var']:
+                content += self.template_variable(self.match['content'][1:])
+            else:
+                content += self.match['content'][1:]
+
+            if not close:
+                endtag = '</%s>' % tag
+
+        elif not close:
+            endtag = '</%s>' % tag
+
+        return starttag, content, endtag
+
+    def render_html_comment(self):
+        return '<!-- ', self.match['html_comment'][1:], ' -->'
+
+    def render_html_cond_comment(self):
+        return '<!--%s>' % self.match['html_cond_comment'][1:], \
+               '', \
+               '<![endif]-->'
+
+    def render_plain(self):
+        return '', self.plain, ''
+
+    def render_variable(self):
+        return '', self.template_variable(self.match['variable'][2:]), ''
+
+    def render_tag(self):
+        start, end = self.template_tag(
+            self.tag_name(),
+            self.match['tag'][2:]
+        )
+        return start, '', end
+
+    def tag_name(self):
+        return self.match['tag'].split(' ')[1]
+
+    def template_variable(self, name):
+        return '{{ %s }}' % name
+
+    def template_tag(self, name, content):
+        start = '{%% %s %%}' % content
+        end = ''
+        if name in self.tags_to_close:
+            end = '{%% end%s %%}' % name
+        return start, end
+
+class Compiler(object):
+    class ParseError(Exception): pass
+
+    re_indent = regex.compile(r'^(?<indent>\s*)')
+    re_haml = regex.compile(r'''
+        ^(?<indent>\s*)
+        (?:
+            (?<element>[%.#][a-zA-Z0-9_-]+)+
+            (?:{\s*
+                (?:(?<key>[a-zA-Z][a-zA-Z0-9_-]*)
+                   \s*=\s*
+                   (?<value>[^,]+)
+
+                   (?:\s*,\s*
+                      (?<key2>[a-zA-Z][a-zA-Z0-9_-]*)
+                      \s*=\s*
+                      (?<value2>[^,]+)
+                   )*
+                )?
+               \s*}
+            )?
+            (?<consume_around>>)?
+            (?<consume_within><)?
+            (?:(?<close>/)|(?<var>=)?(?<content> .*)?)
+        |   (?<filter>:[a-zA-Z][a-zA-Z0-9_-]*)
+        |   (?<doctype>!.*)
+        |   (?<html_cond_comment>/\[.*\])
+        |   (?<html_comment>/.*)
+        |   (?<comment>-\#.*)
+        |   (?<variable>= .*)
+        |   (?<tag>- .*)
+        |   \\?(?<plain>.*)
+        )
+    ''', regex.VERBOSE)
+
+    def __init__(self, stream, outstream, nodeklass=None):
+        if nodeklass is None:
+            nodeklass = Node
+
+        self.nodeklass = nodeklass
+
+        self.stream = stream
+        self.outstream = outstream
+
+        self.__iter = None
+        self.__redo_last = False
+
+    def compile(self):
+        rootnode = self.nodeklass('', self.re_haml.match(''))
+        rootnode.children = self._compile()
+        for child, kwargs in rootnode.iter_children():
+            child.render(self.outstream, **kwargs)
+
+    def _compile(self, level=-1, parent=None):
+        nodes = []
+        for last, curr, next in self.parse_iter():
+            indent = curr.m['indent']
+            empty = len(indent) == len(curr.line_stripped)
+            if len(indent) <= level and not empty:
+                # return if indent changed (and not due to an empty line)
+                self.parse_iter_redo_last()
+                return nodes
+
+            if curr.m['filter']:
+                node = self.nodeklass(
+                    self.filter(curr.m['filter'][1:], indent),
+                    curr.m
+                )
+
+            elif empty:
+                node = self.nodeklass(curr.line_stripped, curr.m)
+
+            else:
+                node = self.nodeklass('', curr.m)
+                if not node.closed():
+                    node.children = self._compile(level=len(indent))
+            nodes.append(node)
+
+        return nodes
+
+    def filter(self, name, indent):
+        f = getattr(self, 'filter_%s' % name)
+        content = ''
+        for last, curr, next in self.parse_iter():
+            if len(curr.m['indent']) <= len(indent):
+                self.parse_iter_redo_last()
+                break
+            content += curr.line
+        return f(indent, content)
+
+    def filter_css(self, indent, content):
+        return '%s<style type="text/css">\n%s\n%s</style>\n' % (
+            indent, content, indent
+        )
+
+    def filter_plain(self, indent, content):
+        return content
+
+    def filter_javascript(self, indent, content):
+        return '%s<script type="text/javascript">\n%s\n%s</script>\n' % (
+            indent, content, indent
+        )
+
+    def _parse_iter(self):
+        self.__redo_last = False
+        last = util.DictAttr()
+
+        curr = util.DictAttr()
+        curr.line = self.stream.readline()
+        curr.line_stripped = curr.line.rstrip('\r\n')
+        curr.m = self.re_haml.match(curr.line)
+
+        count = 0
+        while curr.line:
+            if self.__redo_last:
+                self.__redo_last = False
+
+            else:
+                next = util.DictAttr()
+                next.line = self.stream.readline()
+                next.line_stripped = next.line.rstrip('\r\n')
+                next.m = self.re_haml.match(next.line_stripped)
+
+            count += 1
+            if count > 100:
+                break
+            yield last, curr, next
+
+            if not self.__redo_last:
+                last = curr
+                curr = next
+
+        next = util.DictAttr()
+
+        yield last, curr, next
+
+        self.__iter = None
+
+    def parse_iter(self):
+        if self.__iter is None:
+            self.__iter = self._parse_iter()
+        return self.__iter
+
+    def parse_iter_redo_last(self):
+        self.__redo_last = True
+
+if '__main__' == __name__:
+    try:
+        from cStringIO import StringIO
+    except ImportError:
+        from StringIO import StringIO
+
+    stream = StringIO('''
+!html
+- load util
+%html
+    %head
+        %title title of the page
+        %link{rel='stylesheet', type='text/css', src='static/css/base.css'}
+        %link{rel='stylesheet', type='text/css', src=STATIC_URL + 'css/base.css'}/
+        /[if IE]
+            %link{rel='stylesheet', type='text/css', src=STATIC_URL + 'css/ie.css'}/
+    %body
+        /html comment
+        #header header
+        %p hello world
+        -# comment
+        %p second paragraph
+        %p.footnote
+        %blockquote<
+            %div
+                Foo!
+        %img
+
+        %img
+        %img>
+        %img
+
+        %p<= "Foo\\nBar"
+
+        %img
+        %pre><
+            foo
+            bar
+        %img
+
+        - if True
+            %p
+                = ONE
+                = TWO
+                = THREE
+        %ul
+            %li= one
+            %li= two
+            %li= three
+        /
+            %div inside
+            %div multi
+            %div line
+            %div comment
+        /
+            text inside
+            multi
+            line
+            comment
+        %p
+            text inside of a
+            paragraph block tag
+            \:javascript
+        %p
+            <div class="hmm">inline html</div>
+        #footer footer
+        :javascript
+            $(function() {
+                alert("hello world!");
+            });
+            // hello
+        %script{type='text/javascript'}
+            var a = 1;
+            // hello
+'''.lstrip())
+    import sys
+    Compiler(stream, sys.stdout).compile()
+from django.template import TemplateDoesNotExist
+from django.template.loaders.filesystem import Loader as DjFSLoader
+from django.template.loaders.app_directories import Loader as DjAppLoader
+
+from haml.compiler import Compiler
+
+try:
+    from cStringIO import StringIO
+except ImportError:
+    from StringIO import StringIO
+
+class FSLoader(DjFSLoader):
+    is_usable = True
+
+    def load_template_source(self, template_name, template_dirs=None):
+        data, fn = super(FSLoader, self) \
+            .load_template_source(template_name, template_dirs)
+
+        if not template_name.endswith('.haml'):
+            return data, fn
+
+        out = StringIO()
+        # TODO nasty open(fn) workaround .decode(settings.FILE_CHARSET) issue.
+        # regex in compiler module stops matching... unsure why
+        Compiler(open(fn), out).compile()
+
+        return out.getvalue(), fn
+    load_template_source.is_usable = True
+
+class AppLoader(DjAppLoader):
+    is_usable = True
+
+    def load_template_source(self, template_name, template_dirs=None):
+        data, fn = super(FSLoader, self) \
+            .load_template_source(template_name, template_dirs)
+
+        if not template_name.endswith('.haml'):
+            return data, fn
+
+        out = StringIO()
+        # TODO nasty open(fn) workaround .decode(settings.FILE_CHARSET) issue.
+        # regex in compiler module stops matching... unsure why
+        Compiler(open(fn), out).compile()
+
+        return out.getvalue(), fn
+    load_template_source.is_usable = True
+try:
+    from cStringIO import StringIO
+except ImportError:
+    from StringIO import StringIO
+
+import regex
+import unittest
+
+from haml import compiler
+
+class TestRendering(unittest.TestCase):
+    re_newline = regex.compile(r'\n')
+
+    cases = (
+        ('!html\n', '<!DOCTYPE html>\n\n'),
+        ('%html\n', '<html>\n\n</html>\n'),
+        ("%link.class{rel='stylesheet', type='text/css', src=STATIC_URL + 'css/ie.css'}",
+         '<link class="class" rel="stylesheet" src="{{ STATIC_URL }}css/ie.css" type="text/css" />\n\n'
+        ),
+        ('''
+        /[if IE]
+            %link{rel='stylesheet', type='text/css', src=STATIC_URL + 'css/ie.css'}
+        '''.strip(),
+        '''<!--[if IE]>
+            <link rel="stylesheet" src="{{ STATIC_URL }}css/ie.css" type="text/css" />
+
+<![endif]-->
+'''
+        ),
+        ('/html comment', '<!-- html comment -->\n\n'),
+        ('-# comment', '\n'),
+        ('''
+/
+    multi
+    line
+    comment
+'''.lstrip(), '''
+<!-- 
+    multi
+    line
+    comment
+
+ -->
+'''.lstrip()),
+        ('%title title of the page', '<title>title of the page</title>\n\n'),
+        ('#header header', '<div id="header">header</div>\n\n'),
+        ('.myclass a class', '<div class="myclass">a class</div>\n\n'),
+        ('%p.footnote a footnote', '<p class="footnote">a footnote</p>\n\n'),
+        ('%p= VARIABLE', '<p>{{ VARIABLE }}</p>\n\n'),
+        ('%selfclose/', '<selfclose />\n\n'),
+        ('\%plain text line', '%plain text line\n\n'),
+        ('''
+%blockquote<
+    %div
+        foo
+''', '''
+<blockquote><div>
+        foo
+
+</div></blockquote>
+'''),
+        ('''
+%img
+%img>
+%img
+''', '''
+<img /><img /><img />
+
+'''),
+        ('''
+%img
+%pre><
+    foo
+    bar
+%img
+''', '''
+<img /><pre>foo
+    bar</pre><img />
+
+'''),
+        ('''
+:javascript
+    $(function() {
+        alert("hello world!");
+    });
+    // hello
+''', '''
+<script type="text/javascript">
+    $(function() {
+        alert("hello world!");
+    });
+    // hello
+
+</script>
+
+'''),
+        ('%p= "Foo\\nBar"', '<p>{{ "Foo\\nBar" }}</p>\n\n'),
+        ('''
+%p
+    {{ var }}
+''', '''
+<p>
+    {{ var }}
+
+</p>
+'''),
+        ('- load util', '{% load util %}\n\n'),
+        ('''
+- if True
+    text
+''', '''
+{% if True %}
+    text
+
+{% endif %}
+'''),
+        ('''
+:plain
+    / not a comment
+    -# will render
+    %p
+    :javascript
+        another depth
+''', '''
+    / not a comment
+    -# will render
+    %p
+    :javascript
+        another depth
+
+'''),
+        ('''
+:css
+    * {margin: 0; padding: 0;}
+    .class {
+        content: ".";
+    }
+''', '''
+<style type="text/css">
+    * {margin: 0; padding: 0;}
+    .class {
+        content: ".";
+    }
+
+</style>
+
+'''),
+        ('''!html
+- load util
+%html
+    %head
+        %title title of the page
+        %link{rel='stylesheet', type='text/css', src='static/css/base.css'}
+        %link{rel='stylesheet', type='text/css', src=STATIC_URL + 'css/base.css'}/
+        /[if IE]
+            %link{rel='stylesheet', type='text/css', src=STATIC_URL + 'css/ie.css'}/
+    %body
+        /html comment
+        #header header
+        %p hello world
+        -# comment
+        %p second paragraph
+        %p.footnote
+        %blockquote<
+            %div
+                Foo!
+        %img
+
+        %img
+        %img>
+        %img
+
+        %p<= "Foo\\nBar"
+
+        %img
+        %pre><
+            foo
+            bar
+        %img
+
+        - if True
+            %p
+                = ONE
+                = TWO
+                = THREE
+        %ul
+            %li= one
+            %li= two
+            %li= three
+        /
+            %div inside
+            %div multi
+            %div line
+            %div comment
+        /
+            text inside
+            multi
+            line
+            comment
+        %p
+            text inside of a
+            paragraph block tag
+            \:javascript
+        %p
+            <div class="hmm">inline html</div>
+        #footer footer
+        :javascript
+            $(function() {
+                alert("hello world!");
+            });
+            // hello
+        %script{type='text/javascript'}
+            var a = 1;
+            // hello
+''', '''<!DOCTYPE html>
+{% load util %}
+<html>
+    <head>
+        <title>title of the page</title>
+        <link rel="stylesheet" src="static/css/base.css" type="text/css" />
+        <link rel="stylesheet" src="{{ STATIC_URL }}css/base.css" type="text/css" />
+        <!--[if IE]>
+            <link rel="stylesheet" src="{{ STATIC_URL }}css/ie.css" type="text/css" />
+        <![endif]-->
+    </head>
+    <body>
+        <!-- html comment -->
+        <div id="header">header</div>
+        <p>hello world</p>
+                <p>second paragraph</p>
+        <p class="footnote"></p>
+        <blockquote><div>
+                Foo!
+        </div></blockquote>
+        <img />
+
+        <img /><img /><img />
+
+        <p>{{ "Foo\\nBar" }}</p>
+
+        <img /><pre>foo
+            bar</pre><img />
+
+        {% if True %}
+            <p>
+                {{ ONE }}
+                {{ TWO }}
+                {{ THREE }}
+            </p>
+        {% endif %}
+        <ul>
+            <li>{{ one }}</li>
+            <li>{{ two }}</li>
+            <li>{{ three }}</li>
+        </ul>
+        <!-- 
+            <div>inside</div>
+            <div>multi</div>
+            <div>line</div>
+            <div>comment</div>
+         -->
+        <!-- 
+            text inside
+            multi
+            line
+            comment
+         -->
+        <p>
+            text inside of a
+            paragraph block tag
+            :javascript
+        </p>
+        <p>
+            <div class="hmm">inline html</div>
+        </p>
+        <div id="footer">footer</div>
+        <script type="text/javascript">
+            $(function() {
+                alert("hello world!");
+            });
+            // hello
+
+        </script>
+
+        <script type="text/javascript">
+            var a = 1;
+            <!-- / hello -->
+
+        </script>
+    </body>
+</html>
+'''),
+    )
+
+    def compile_template(self, template):
+        stream = StringIO(template)
+        outstream = StringIO()
+        compiler.Compiler(stream, outstream).compile()
+        return outstream.getvalue()
+
+    def test_all(self):
+        fail = False
+        for template, final in self.cases:
+            test_value = self.compile_template(template)
+            if test_value != final:
+                if fail:
+                    print "\n=====================================\n"
+                print 'template:\n' + self.re_newline.sub(r'$' + '\n', template)
+                print 'compiled:\n' + self.re_newline.sub(r'$' + '\n', test_value)
+                print 'expected:\n' + self.re_newline.sub(r'$' + '\n', final)
+                fail = True
+        if fail:
+            raise AssertionError('Expected != Test Got')
+
+if __name__ == '__main__':
+    unittest.main()
+class DictAttr(dict):
+    def __getattr__(self, name):
+        return self[name]
+
+    def __setattr__(self, name, value):
+        self[name] = value
+regex==0.1.20110524

sass/__init__.py

Empty file added.
+from setuptools import setup
+
+setup(
+    name='haml-sass',
+    version='0.5',
+    packages=['haml', ],
+    author='Dan LaMotte',
+    author_email='lamotte85@gmail.com',
+    description='haml for python (sass coming soon)',
+    keywords='haml sass django template',
+    url='https://bitbucket.org/dlamotte/haml-sass',
+)