diff --git a/hamlpy/__init__.py b/hamlpy/__init__.py old mode 100755 new mode 100644 diff --git a/hamlpy/elements.py b/hamlpy/elements.py index 3bb5d3d..979ee64 100644 --- a/hamlpy/elements.py +++ b/hamlpy/elements.py @@ -2,6 +2,24 @@ import sys from types import NoneType +class Conditional(object): + + """Data structure for a conditional construct in attribute dictionaries""" + + NOTHING = object() + + def __init__(self, test, body, orelse=NOTHING): + self.test = test + self.body = body + self.orelse = orelse + + def __repr__(self): + if self.orelse is self.NOTHING: + attrs = [self.test, self.body] + else: + attrs = [self.test, self.body, self.orelse] + return "<%s@0X%X %r>" % (self.__class__.__name__, id(self), attrs) + class Element(object): """contains the pieces of an element and can populate itself from haml element text""" @@ -33,6 +51,26 @@ class Element(object): ATTRIBUTE_REGEX = re.compile(r'(?P
\{\s*|,\s*)%s\s*:\s*%s' % (_ATTRIBUTE_KEY_REGEX, _ATTRIBUTE_VALUE_REGEX), re.UNICODE)
     DJANGO_VARIABLE_REGEX = re.compile(r'^\s*=\s(?P[a-zA-Z_][a-zA-Z0-9._-]*)\s*$')
 
+    # Attribute dictionary parsing
+    ATTRKEY_REGEX = re.compile(r"\s*(%s|%s)\s*:\s*" % (
+        _SINGLE_QUOTE_STRING_LITERAL_REGEX, _DOUBLE_QUOTE_STRING_LITERAL_REGEX),
+        re.UNICODE)
+    _VALUE_LIST_REGEX = r"\[\s*(?:(?:%s|%s|None(?!\w)|\d+)\s*,?\s*)*\]" % (
+        _SINGLE_QUOTE_STRING_LITERAL_REGEX, _DOUBLE_QUOTE_STRING_LITERAL_REGEX)
+    _VALUE_TUPLE_REGEX = r"\(\s*(?:(?:%s|%s|None(?!\w)|\d+)\s*,?\s*)*\)" % (
+        _SINGLE_QUOTE_STRING_LITERAL_REGEX, _DOUBLE_QUOTE_STRING_LITERAL_REGEX)
+    ATTRVAL_REGEX = re.compile(r"None(?!\w)|%s|%s|%s|%s|\d+" % (
+        _SINGLE_QUOTE_STRING_LITERAL_REGEX, _DOUBLE_QUOTE_STRING_LITERAL_REGEX,
+        _VALUE_LIST_REGEX, _VALUE_TUPLE_REGEX), re.UNICODE)
+
+    CONDITION_REGEX = re.compile(r"(%s|%s|%s|%s|(?!,| else ).)+" % (
+        _SINGLE_QUOTE_STRING_LITERAL_REGEX, _DOUBLE_QUOTE_STRING_LITERAL_REGEX,
+        _VALUE_LIST_REGEX, _VALUE_TUPLE_REGEX), re.UNICODE)
+
+    NEWLINE_REGEX = re.compile("[\r\n]+")
+
+    DJANGO_TAG_REGEX = re.compile("({%|%})")
+
 
     def __init__(self, haml, attr_wrapper="'"):
         self.haml = haml
@@ -66,7 +104,7 @@ def _parse_haml(self):
 
     def _parse_class_from_attributes_dict(self):
         clazz = self.attributes_dict.get('class', '')
-        if not isinstance(clazz, str):
+        if not isinstance(clazz, basestring):
             clazz = ''
             for one_class in self.attributes_dict.get('class'):
                 clazz += ' ' + one_class
@@ -82,7 +120,7 @@ def _parse_id(self, id_haml):
     def _parse_id_dict(self, id_dict):
         text = ''
         id_dict = self.attributes_dict.get('id')
-        if isinstance(id_dict, str):
+        if isinstance(id_dict, basestring):
             text = '_' + id_dict
         else:
             text = ''
@@ -96,23 +134,21 @@ def _escape_attribute_quotes(self, v):
         '''
         escaped = []
         inside_tag = False
-        for i, _ in enumerate(v):
-            if v[i:i + 2] == '{%':
+        escape = "\\" + self.attr_wrapper
+        for ss in self.DJANGO_TAG_REGEX.split(v):
+            if ss == "{%":
                 inside_tag = True
-            elif v[i:i + 2] == '%}':
+            elif ss == "%}":
                 inside_tag = False
-
-            if v[i] == self.attr_wrapper and not inside_tag:
-                escaped.append('\\')
-
-            escaped.append(v[i])
-
+            if not inside_tag:
+                ss = ss.replace(self.attr_wrapper, escape)
+            escaped.append(ss)
         return ''.join(escaped)
 
     def _parse_attribute_dictionary(self, attribute_dict_string):
         attributes_dict = {}
         if (attribute_dict_string):
-            attribute_dict_string = attribute_dict_string.replace('\n', ' ')
+            attribute_dict_string = self.NEWLINE_REGEX.sub(" ", attribute_dict_string)
             try:
                 # converting all allowed attributes to python dictionary style
 
@@ -121,24 +157,29 @@ def _parse_attribute_dictionary(self, attribute_dict_string):
                 # Put double quotes around key
                 attribute_dict_string = re.sub(self.ATTRIBUTE_REGEX, '\g
"\g":\g', attribute_dict_string)
                 # Parse string as dictionary
-                attributes_dict = eval(attribute_dict_string)
-                for k, v in attributes_dict.items():
-                    if k != 'id' and k != 'class':
-                        if isinstance(v, NoneType):
-                            self.attributes += "%s " % (k,)
-                        elif isinstance(v, int) or isinstance(v, float):
-                            self.attributes += "%s=%s " % (k, self.attr_wrap(v))
-                        else:
-                            # DEPRECATED: Replace variable in attributes (e.g. "= somevar") with Django version ("{{somevar}}")
-                            v = re.sub(self.DJANGO_VARIABLE_REGEX, '{{\g}}', attributes_dict[k])
-                            if v != attributes_dict[k]:
-                                sys.stderr.write("\n---------------------\nDEPRECATION WARNING: %s" % self.haml.lstrip() + \
-                                                 "\nThe Django attribute variable feature is deprecated and may be removed in future versions." +
-                                                 "\nPlease use inline variables ={...} instead.\n-------------------\n")
-
-                            attributes_dict[k] = v
-                            v = v.decode('utf-8')
-                            self.attributes += "%s=%s " % (k, self.attr_wrap(self._escape_attribute_quotes(v)))
+                for (key, val) in self.parse_attr(attribute_dict_string[1:-1]):
+                    if isinstance(val, Conditional):
+                        if key not in ("id", "class"):
+                            self.attributes += "{%% %s %%} " % val.test
+                        value = "{%% %s %%}%s" % (val.test,
+                            self.add_attr(key, val.body))
+                        while isinstance(val.orelse, Conditional):
+                            val = val.orelse
+                            if key not in ("id", "class"):
+                                self.attributes += "{%% el%s %%} " % val.test
+                            value += "{%% el%s %%}%s" % (val.test,
+                                self.add_attr(key, val.body))
+                        if val.orelse is not val.NOTHING:
+                            if key not in ("id", "class"):
+                                self.attributes += "{% else %} "
+                            value += "{%% else %%}%s" % self.add_attr(key,
+                                val.orelse)
+                        if key not in ("id", "class"):
+                            self.attributes += "{% endif %}"
+                        value += "{% endif %}"
+                    else:
+                        value = self.add_attr(key, val)
+                    attributes_dict[key] = value
                 self.attributes = self.attributes.strip()
             except Exception, e:
                 raise Exception('failed to decode: %s' % attribute_dict_string)
@@ -146,6 +187,80 @@ def _parse_attribute_dictionary(self, attribute_dict_string):
 
         return attributes_dict
 
+    def parse_attr(self, string):
+        """Generate (key, value) pairs from attributes dictionary string"""
+        string = string.strip()
+        while string:
+            match = self.ATTRKEY_REGEX.match(string)
+            if not match:
+                raise SyntaxError("Dictionary key expected at %r" % string)
+            key = eval(match.group(1))
+            (val, string) = self.parse_attribute_value(string[match.end():])
+            if string.startswith(","):
+                string = string[1:].lstrip()
+            yield (key, val)
+
+    def parse_attribute_value(self, string):
+        """Parse an attribute value from dictionary string
+
+        Return a (value, tail) pair where tail is remainder of the string.
 
+        """
+        match = self.ATTRVAL_REGEX.match(string)
+        if not match:
+            raise SyntaxError("Dictionary value expected at %r" % string)
+        val = eval(match.group(0))
+        string = string[match.end():].lstrip()
+        if string.startswith("if "):
+            match = self.CONDITION_REGEX.match(string)
+            # Note: cannot fail.  At least the "if" word must match.
+            condition = match.group(0)
+            string = string[len(condition):].lstrip()
+            if string.startswith("else "):
+                (orelse, string) = self.parse_attribute_value(
+                    string[5:].lstrip())
+                val = Conditional(condition, val, orelse)
+            else:
+                val = Conditional(condition, val)
+        return (val, string)
 
+    def add_attr(self, key, value):
+        """Add attribute definition to self.attributes
 
+        For "id" and "class" attributes, return attribute value
+        (possibly modified by replacing deprecated syntax).
+
+        For other attributes, return the "key=value" string
+        appropriate for the value type and also add this string
+        to self.attributes.
+
+        """
+        if isinstance(value, basestring):
+            # DEPRECATED: Replace variable in attributes (e.g. "= somevar") with Django version ("{{somevar}}")
+            newval = re.sub(self.DJANGO_VARIABLE_REGEX, '{{\g}}', value)
+            if newval != value:
+                sys.stderr.write("""
+---------------------
+DEPRECATION WARNING: %s
+The Django attribute variable feature is deprecated
+and may be removed in future versions.
+Please use inline variables ={...} instead.
+-------------------
+""" % self.haml.lstrip())
+
+            value = newval.decode('utf-8')
+        if key in ("id", "class"):
+            return value
+        if isinstance(value, NoneType):
+            attr = "%s" % key
+        elif isinstance(value, int) or isinstance(value, float):
+            attr = "%s=%s" % (key, self.attr_wrap(value))
+        elif isinstance(value, basestring):
+            attr = "%s=%s" % (key,
+                self.attr_wrap(self._escape_attribute_quotes(value)))
+        else:
+            raise ValueError(
+                "Non-scalar value %r (type %s) passed for HTML attribute %r"
+                % (value, type(value), key))
+        self.attributes += attr + " "
+        return attr
diff --git a/hamlpy/hamlpy.py b/hamlpy/hamlpy.py
old mode 100755
new mode 100644
index 4aa5037..60e8c08
--- a/hamlpy/hamlpy.py
+++ b/hamlpy/hamlpy.py
@@ -24,6 +24,18 @@ def process_lines(self, haml_lines):
         for line_number, line in enumerate(line_iter):
             node_lines = line
 
+            # support for line breaks ("\" symbol at the end of line)
+            while node_lines.rstrip().endswith("\\"):
+                node_lines = node_lines.rstrip()[:-1]
+                try:
+                    line = line_iter.next()
+                except StopIteration:
+                    raise Exception(
+                        "Line break symbol '\\' found at the last line %s" \
+                        % line_number
+                    )
+                node_lines += line
+
             if not root.parent_of(HamlNode(line)).inside_filter_node():
                 if line.count('{') - line.count('}') == 1:
                     start_multiline=line_number # For exception handling
@@ -31,6 +43,9 @@ def process_lines(self, haml_lines):
                     while line.count('{') - line.count('}') != -1:
                         try:
                             line = line_iter.next()
+                            # support for line breaks inside Node parameters
+                            if line.rstrip().endswith("\\"):
+                                line = line.rstrip()[:-1]
                         except StopIteration:
                             raise Exception('No closing brace found for multi-line HAML beginning at line %s' % (start_multiline+1))
                         node_lines += line
diff --git a/hamlpy/nodes.py b/hamlpy/nodes.py
index a5e7e4a..3c9e592 100644
--- a/hamlpy/nodes.py
+++ b/hamlpy/nodes.py
@@ -50,69 +50,6 @@ class NotAvailableError(Exception):
 
 HAML_ESCAPE = '\\'
 
-def create_node(haml_line):
-    stripped_line = haml_line.strip()
-
-    if len(stripped_line) == 0:
-        return None
-
-    if re.match(INLINE_VARIABLE, stripped_line) or re.match(ESCAPED_INLINE_VARIABLE, stripped_line):
-        return PlaintextNode(haml_line)
-
-    if stripped_line[0] == HAML_ESCAPE:
-        return PlaintextNode(haml_line)
-
-    if stripped_line.startswith(DOCTYPE):
-        return DoctypeNode(haml_line)
-
-    if stripped_line[0] in ELEMENT_CHARACTERS:
-        return ElementNode(haml_line)
-
-    if stripped_line[0:len(CONDITIONAL_COMMENT)] == CONDITIONAL_COMMENT:
-        return ConditionalCommentNode(haml_line)
-
-    if stripped_line[0] == HTML_COMMENT:
-        return CommentNode(haml_line)
-
-    for comment_prefix in HAML_COMMENTS:
-        if stripped_line.startswith(comment_prefix):
-            return HamlCommentNode(haml_line)
-
-    if stripped_line[0] == VARIABLE:
-        return VariableNode(haml_line)
-
-    if stripped_line[0] == TAG:
-        return TagNode(haml_line)
-
-    if stripped_line == JAVASCRIPT_FILTER:
-        return JavascriptFilterNode(haml_line)
-
-    if stripped_line in COFFEESCRIPT_FILTERS:
-        return CoffeeScriptFilterNode(haml_line)
-
-    if stripped_line == CSS_FILTER:
-        return CssFilterNode(haml_line)
-
-    if stripped_line == STYLUS_FILTER:
-        return StylusFilterNode(haml_line)
-
-    if stripped_line == PLAIN_FILTER:
-        return PlainFilterNode(haml_line)
-
-    if stripped_line == PYTHON_FILTER:
-        return PythonFilterNode(haml_line)
-
-    if stripped_line == CDATA_FILTER:
-        return CDataFilterNode(haml_line)
-
-    if stripped_line == PYGMENTS_FILTER:
-        return PygmentsFilterNode(haml_line)
-
-    if stripped_line == MARKDOWN_FILTER:
-        return MarkdownFilterNode(haml_line)
-
-    return PlaintextNode(haml_line)
-
 class TreeNode(object):
     ''' Generic parent/child tree class'''
     def __init__(self):
@@ -203,8 +140,13 @@ def add_node(self, node):
             self.add_child(node)
 
     def _should_go_inside_last_node(self, node):
-        return len(self.children) > 0 and (node.indentation > self.children[-1].indentation
-            or (node.indentation == self.children[-1].indentation and self.children[-1].should_contain(node)))
+        if self.children:
+            _child = self.children[-1]
+            if node.indentation > _child.indentation:
+                return True
+            elif node.indentation == _child.indentation:
+                return _child.should_contain(node)
+        return False
 
     def should_contain(self, node):
         return False
@@ -226,10 +168,16 @@ def __repr__(self):
 class HamlNode(RootNode):
     def __init__(self, haml):
         RootNode.__init__(self)
-        self.haml = haml.strip()
-        self.raw_haml = haml
-        self.indentation = (len(haml) - len(haml.lstrip()))
-        self.spaces = ''.join(haml[0] for i in range(self.indentation))
+        if haml:
+            self.haml = haml.strip()
+            self.raw_haml = haml
+            self.indentation = (len(haml) - len(haml.lstrip()))
+            self.spaces = haml[0] * self.indentation
+        else:
+            # When the string is empty, we cannot build self.spaces.
+            # All other attributes have trivial values, no need to compute.
+            self.haml = self.raw_haml = self.spaces = ""
+            self.indentation = 0
 
     def replace_inline_variables(self, content):
         content = re.sub(INLINE_VARIABLE, r'{{ \2 }}', content)
@@ -445,6 +393,7 @@ class TagNode(HamlNode):
                    'ifchanged':'else',
                    'ifequal':'else',
                    'ifnotequal':'else',
+                   'blocktrans':'plural',
                    'for':'empty',
                    'with':'with'}
 
@@ -604,3 +553,57 @@ def _render(self):
             self.before += markdown( ''.join(lines))
         else:
             self.after = self.render_newlines()
+
+LINE_NODES = {
+    HAML_ESCAPE: PlaintextNode,
+    ELEMENT: ElementNode,
+    ID: ElementNode,
+    CLASS: ElementNode,
+    HTML_COMMENT: CommentNode,
+    VARIABLE: VariableNode,
+    TAG: TagNode,
+}
+
+SCRIPT_FILTERS = {
+    JAVASCRIPT_FILTER: JavascriptFilterNode,
+    ':coffeescript': CoffeeScriptFilterNode,
+    ':coffee': CoffeeScriptFilterNode,
+    CSS_FILTER: CssFilterNode,
+    STYLUS_FILTER: StylusFilterNode,
+    PLAIN_FILTER: PlainFilterNode,
+    PYTHON_FILTER: PythonFilterNode,
+    CDATA_FILTER: CDataFilterNode,
+    PYGMENTS_FILTER: PygmentsFilterNode,
+    MARKDOWN_FILTER: MarkdownFilterNode,
+}
+
+def create_node(haml_line):
+    stripped_line = haml_line.strip()
+
+    if len(stripped_line) == 0:
+        return None
+
+    if INLINE_VARIABLE.match(stripped_line) \
+    or ESCAPED_INLINE_VARIABLE.match(stripped_line):
+        return PlaintextNode(haml_line)
+
+    if stripped_line.startswith(DOCTYPE):
+        return DoctypeNode(haml_line)
+
+    if stripped_line.startswith(CONDITIONAL_COMMENT):
+        return ConditionalCommentNode(haml_line)
+
+    for comment_prefix in HAML_COMMENTS:
+        if stripped_line.startswith(comment_prefix):
+            return HamlCommentNode(haml_line)
+
+    # Note: HAML_COMMENTS start with the same characters
+    # as TAG and VARIABLE prefixes, so comments must be processed first.
+    line_node = LINE_NODES.get(stripped_line[0], None)
+    if line_node is not None:
+        return line_node(haml_line)
+    line_node = SCRIPT_FILTERS.get(stripped_line, None)
+    if line_node is not None:
+        return line_node(haml_line)
+    return PlaintextNode(haml_line)
+
diff --git a/hamlpy/template/loaders.py b/hamlpy/template/loaders.py
index 4d80c09..60eefb4 100644
--- a/hamlpy/template/loaders.py
+++ b/hamlpy/template/loaders.py
@@ -24,14 +24,9 @@ class TemplateDoesNotExist(Exception):
 
 
 def get_haml_loader(loader):
-    if hasattr(loader, 'Loader'):
-        baseclass = loader.Loader
-    else:
-        class baseclass(object):
-            def load_template_source(self, *args, **kwargs):
-                return loader.load_template_source(*args, **kwargs)
-
-    class Loader(baseclass):
+    class Loader(loader.Loader):
+
+        # load_template_source is deprecated in v1.9. Use get_contents instead.
         def load_template_source(self, template_name, *args, **kwargs):
             name, _extension = os.path.splitext(template_name)
             # os.path.splitext always returns a period at the start of extension
@@ -57,6 +52,16 @@ def load_template_source(self, template_name, *args, **kwargs):
         def _generate_template_name(self, name, extension="hamlpy"):
             return "%s.%s" % (name, extension)
 
+        def get_contents(self, origin):
+            contents = super(Loader, self).get_contents(origin)
+            # template_name is lookup name, name is file path.
+            # Should we check extension in name instead of template_name?
+            extension = os.path.splitext(origin.template_name)[1].lstrip(".")
+            if extension in hamlpy.VALID_EXTENSIONS:
+                hamlParser = hamlpy.Compiler(options_dict=options_dict)
+                contents = hamlParser.process(contents)
+            return contents
+
     return Loader
 
 
diff --git a/hamlpy/template/utils.py b/hamlpy/template/utils.py
index c9187e8..cb3d399 100644
--- a/hamlpy/template/utils.py
+++ b/hamlpy/template/utils.py
@@ -1,6 +1,5 @@
-import imp
-from os import listdir
-from os.path import dirname, splitext
+from os.path import dirname
+from pkgutil import iter_modules
 
 try:
   from django.template import loaders
@@ -8,24 +7,19 @@
 except ImportError, e:
   _django_available = False
 
-MODULE_EXTENSIONS = tuple([suffix[0] for suffix in imp.get_suffixes()])
-
 def get_django_template_loaders():
     if not _django_available:
         return []
-    return [(loader.__name__.rsplit('.',1)[1], loader) 
+    return [(loader.__name__.rsplit('.',1)[1], loader)
                 for loader in get_submodules(loaders)
                 if hasattr(loader, 'Loader')]
-        
+
 def get_submodules(package):
     submodules = ("%s.%s" % (package.__name__, module)
                 for module in package_contents(package))
-    return [__import__(module, {}, {}, [module.rsplit(".", 1)[-1]]) 
+    return [__import__(module, {}, {}, [module.rsplit(".", 1)[-1]])
                 for module in submodules]
 
 def package_contents(package):
-    package_path = dirname(loaders.__file__)
-    contents = set([splitext(module)[0]
-            for module in listdir(package_path)
-            if module.endswith(MODULE_EXTENSIONS)])
-    return contents
+    package_path = dirname(package.__file__)
+    return set([name for (ldr, name, ispkg) in iter_modules([package_path])])
diff --git a/hamlpy/templatize.py b/hamlpy/templatize.py
index 9581109..afc2bda 100644
--- a/hamlpy/templatize.py
+++ b/hamlpy/templatize.py
@@ -6,7 +6,7 @@
 """
 
 try:
-    from django.utils.translation import trans_real
+    from django.utils import translation
     _django_available = True
 except ImportError, e:
     _django_available = False
@@ -16,16 +16,15 @@
 
 
 def decorate_templatize(func):
-    def templatize(src, origin=None):
+    def templatize(src, origin=None, **kwargs):
         #if the template has no origin file then do not attempt to parse it with haml
         if origin:
             #if the template has a source file, then only parse it if it is haml
             if os.path.splitext(origin)[1].lower() in ['.'+x.lower() for x in hamlpy.VALID_EXTENSIONS]:
                 hamlParser = hamlpy.Compiler()
-                html = hamlParser.process(src.decode('utf-8'))
-                src = html.encode('utf-8')
-        return func(src, origin)
+                src = hamlParser.process(src)
+        return func(src, origin=origin, **kwargs)
     return templatize
 
 if _django_available:
-    trans_real.templatize = decorate_templatize(trans_real.templatize)
+    translation.templatize = decorate_templatize(translation.templatize)
diff --git a/hamlpy/test/hamlpy_test.py b/hamlpy/test/hamlpy_test.py
old mode 100755
new mode 100644
index 5601c96..238bae8
--- a/hamlpy/test/hamlpy_test.py
+++ b/hamlpy/test/hamlpy_test.py
@@ -57,6 +57,24 @@ def test_dictionaries_can_by_pythonic(self):
         result = hamlParser.process(haml)
         self.assertEqual(html, result.replace('\n', ''))
 
+    def test_dictionaries_allow_conditionals(self):
+        for (haml, html) in (
+            ("%img{'src': 'hello' if coming}",
+             ""),
+            ("%img{'src': 'hello' if coming else 'goodbye' }",
+             ""),
+            ("%item{'a': 'one' if b == 1 else 'two' if b == [1, 2] else None}",
+             ""),
+            # For id and class attributes, conditions work on individual parts
+            # of the value (more parts can be added from HAML tag).
+            ("%div{'id': 'No1' if tree is TheLarch, 'class': 'quite-a-long-way-away'}",
+             "
"), + ("%div{'id': 'dog_kennel' if assisant.name == 'Mr Lambert' else 'mattress'}", + "
"), + ): + hamlParser = hamlpy.Compiler() + result = hamlParser.process(haml) + self.assertEqual(html, result.replace('\n', '')) def test_html_comments_rendered_properly(self): haml = '/ some comment' @@ -295,6 +313,12 @@ def test_filters_render_escaped_backslash(self): result = hamlParser.process(haml) eq_(html, result) + @raises(Exception) + def test_throws_exception_when_break_last_line(self): + haml = '-width a=1 \\' + hamlParser = hamlpy.Compiler() + result = hamlParser.process(haml) + def test_xml_namespaces(self): haml = "%fb:tag\n content" html = "\n content\n\n" @@ -312,7 +336,7 @@ def test_attr_wrapper(self): hamlParser = hamlpy.Compiler(options_dict={'attr_wrapper': '"'}) result = hamlParser.process(haml) self.assertEqual(result, - ''' + '''
diff --git a/hamlpy/test/template_compare_test.py b/hamlpy/test/template_compare_test.py index 3d20485..8fbbcbf 100644 --- a/hamlpy/test/template_compare_test.py +++ b/hamlpy/test/template_compare_test.py @@ -13,34 +13,40 @@ def test_nuke_outer_whitespace(self): def test_comparing_simple_templates(self): self._compare_test_files('simple') - + def test_mixed_id_and_classes_using_dictionary(self): self._compare_test_files('classIdMixtures') - + def test_self_closing_tags_close(self): self._compare_test_files('selfClosingTags') - + def test_nested_html_comments(self): self._compare_test_files('nestedComments') - + def test_haml_comments(self): self._compare_test_files('hamlComments') - + def test_implicit_divs(self): self._compare_test_files('implicitDivs') - + def test_django_combination_of_tags(self): self._compare_test_files('djangoCombo') - + def test_self_closing_django(self): self._compare_test_files('selfClosingDjango') - + def test_nested_django_tags(self): self._compare_test_files('nestedDjangoTags') - + + def test_line_break(self): + self._compare_test_files('lineBreak') + + def test_line_break_in_node_params(self): + self._compare_test_files('lineBreakInNode') + def test_filters(self): self._compare_test_files('filters') - + def test_filters_markdown(self): try: import markdown @@ -81,7 +87,7 @@ def _print_diff(self, s1, s2): line = 1 col = 1 - + for i, _ in enumerate(shorter): if len(shorter) <= i + 1: print 'Ran out of characters to compare!' @@ -109,19 +115,19 @@ def _print_diff(self, s1, s2): def _compare_test_files(self, name): haml_lines = codecs.open('templates/' + name + '.hamlpy', encoding = 'utf-8').readlines() html = open('templates/' + name + '.html').read() - + haml_compiler = hamlpy.Compiler() parsed = haml_compiler.process_lines(haml_lines) # Ignore line ending differences parsed = parsed.replace('\r', '') html = html.replace('\r', '') - + if parsed != html: print '\nHTML (actual): ' print '\n'.join(["%d. %s" % (i + 1, l) for i, l in enumerate(parsed.split('\n')) ]) self._print_diff(parsed, html) eq_(parsed, html) - + if __name__ == '__main__': unittest.main() diff --git a/hamlpy/test/templates/lineBreak.hamlpy b/hamlpy/test/templates/lineBreak.hamlpy new file mode 100644 index 0000000..41a2e3f --- /dev/null +++ b/hamlpy/test/templates/lineBreak.hamlpy @@ -0,0 +1,7 @@ +-with \ + a=10 \ + b=20 c=56 \ + d=30 \ + e=43 + + %div{'class': 'row'} diff --git a/hamlpy/test/templates/lineBreak.html b/hamlpy/test/templates/lineBreak.html new file mode 100644 index 0000000..63599cd --- /dev/null +++ b/hamlpy/test/templates/lineBreak.html @@ -0,0 +1,5 @@ +{% with a=10 b=20 c=56 d=30 e=43 %} + +
+{% endwith %} + diff --git a/hamlpy/test/templates/lineBreakInNode.hamlpy b/hamlpy/test/templates/lineBreakInNode.hamlpy new file mode 100644 index 0000000..f101a77 --- /dev/null +++ b/hamlpy/test/templates/lineBreakInNode.hamlpy @@ -0,0 +1,4 @@ +%div{ + 'id' \ + : "row-id" +} diff --git a/hamlpy/test/templates/lineBreakInNode.html b/hamlpy/test/templates/lineBreakInNode.html new file mode 100644 index 0000000..95510e7 --- /dev/null +++ b/hamlpy/test/templates/lineBreakInNode.html @@ -0,0 +1 @@ +
diff --git a/hamlpy/test/test_elements.py b/hamlpy/test/test_elements.py index 119fae9..8079433 100644 --- a/hamlpy/test/test_elements.py +++ b/hamlpy/test/test_elements.py @@ -7,29 +7,29 @@ class TestElement(object): def test_attribute_value_not_quoted_when_looks_like_key(self): sut = Element('') s1 = sut._parse_attribute_dictionary('''{name:"viewport", content:"width:device-width, initial-scale:1, minimum-scale:1, maximum-scale:1"}''') - eq_(s1['content'], 'width:device-width, initial-scale:1, minimum-scale:1, maximum-scale:1') - eq_(s1['name'], 'viewport') + eq_(s1['content'], "content='width:device-width, initial-scale:1, minimum-scale:1, maximum-scale:1'") + eq_(s1['name'], "name='viewport'") sut = Element('') s1 = sut._parse_attribute_dictionary('''{style:"a:x, b:'y', c:1, e:3"}''') - eq_(s1['style'], "a:x, b:'y', c:1, e:3") + eq_(s1['style'], "style='a:x, b:\\'y\\', c:1, e:3'") sut = Element('') s1 = sut._parse_attribute_dictionary('''{style:"a:x, b:'y', c:1, d:\\"dk\\", e:3"}''') - eq_(s1['style'], '''a:x, b:'y', c:1, d:"dk", e:3''') + eq_(s1['style'], "style='a:x, b:\\'y\\', c:1, d:\"dk\", e:3'") sut = Element('') s1 = sut._parse_attribute_dictionary('''{style:'a:x, b:\\'y\\', c:1, d:"dk", e:3'}''') - eq_(s1['style'], '''a:x, b:'y', c:1, d:"dk", e:3''') + eq_(s1['style'], "style='a:x, b:\\'y\\', c:1, d:\"dk\", e:3'") def test_dashes_work_in_attribute_quotes(self): sut = Element('') s1 = sut._parse_attribute_dictionary('''{"data-url":"something", "class":"blah"}''') - eq_(s1['data-url'],'something') + eq_(s1['data-url'], "data-url='something'") eq_(s1['class'], 'blah') s1 = sut._parse_attribute_dictionary('''{data-url:"something", class:"blah"}''') - eq_(s1['data-url'],'something') + eq_(s1['data-url'], "data-url='something'") eq_(s1['class'], 'blah') def test_escape_quotes_except_django_tags(self): @@ -45,43 +45,43 @@ def test_attributes_parse(self): sut = Element('') s1 = sut._parse_attribute_dictionary('''{a:'something',"b":None,'c':2}''') - eq_(s1['a'],'something') - eq_(s1['b'],None) - eq_(s1['c'],2) + eq_(s1['a'], "a='something'") + eq_(s1['b'], "b") + eq_(s1['c'], "c='2'") - eq_(sut.attributes, "a='something' c='2' b") + eq_(sut.attributes, "a='something' b c='2'") def test_pulls_tag_name_off_front(self): sut = Element('%div.class') eq_(sut.tag, 'div') - + def test_default_tag_is_div(self): sut = Element('.class#id') eq_(sut.tag, 'div') - + def test_parses_id(self): sut = Element('%div#someId.someClass') eq_(sut.id, 'someId') - + sut = Element('#someId.someClass') eq_(sut.id, 'someId') - + def test_no_id_gives_empty_string(self): sut = Element('%div.someClass') eq_(sut.id, '') - + def test_parses_class(self): sut = Element('%div#someId.someClass') eq_(sut.classes, 'someClass') - + def test_properly_parses_multiple_classes(self): sut = Element('%div#someId.someClass.anotherClass') eq_(sut.classes, 'someClass anotherClass') - + def test_no_class_gives_empty_string(self): sut = Element('%div#someId') eq_(sut.classes, '') - + def test_attribute_dictionary_properly_parses(self): sut = Element("%html{'xmlns':'http://www.w3.org/1999/xhtml', 'xml:lang':'en', 'lang':'en'}") assert "xmlns='http://www.w3.org/1999/xhtml'" in sut.attributes @@ -92,45 +92,45 @@ def test_id_and_class_dont_go_in_attributes(self): sut = Element("%div{'class':'hello', 'id':'hi'}") assert 'class=' not in sut.attributes assert 'id=' not in sut.attributes - + def test_attribute_merges_classes_properly(self): sut = Element("%div.someClass.anotherClass{'class':'hello'}") assert 'someClass' in sut.classes assert 'anotherClass' in sut.classes assert 'hello' in sut.classes - + def test_attribute_merges_ids_properly(self): sut = Element("%div#someId{'id':'hello'}") eq_(sut.id, 'someId_hello') - + def test_can_use_arrays_for_id_in_attributes(self): sut = Element("%div#someId{'id':['more', 'andMore']}") eq_(sut.id, 'someId_more_andMore') - + def test_self_closes_a_self_closing_tag(self): sut = Element(r"%br") assert sut.self_close - + def test_does_not_close_a_non_self_closing_tag(self): sut = Element("%div") assert sut.self_close == False - + def test_can_close_a_non_self_closing_tag(self): sut = Element("%div/") assert sut.self_close - + def test_properly_detects_django_tag(self): sut = Element("%div= $someVariable") assert sut.django_variable - + def test_knows_when_its_not_django_tag(self): sut = Element("%div Some Text") assert sut.django_variable == False - + def test_grabs_inline_tag_content(self): sut = Element("%div Some Text") eq_(sut.inline_content, 'Some Text') - + def test_multiline_attributes(self): sut = Element("""%link{'rel': 'stylesheet', 'type': 'text/css', 'href': '/long/url/to/stylesheet/resource.css'}""") diff --git a/reference.md b/reference.md index 542bdb0..bac133f 100644 --- a/reference.md +++ b/reference.md @@ -9,6 +9,7 @@ - [Attributes: {}](#attributes-) - [Attributes without values (Boolean attributes)](#attributes-without-values-boolean-attributes) - ['class' and 'id' attributes](#class-and-id-attributes) + - [Conditional attributes](#conditional-attributes) - [Class and ID: . and #](#class-and-id--and-) - [Implicit div elements](#implicit-div-elements) - [Self-Closing Tags: /](#self-closing-tags-) @@ -115,7 +116,39 @@ The 'class' and 'id' attributes can also be specified as a Python tuple whose el is compiled to:
Content
- + +#### Conditional attributes + +Attribute dictionaries support Python-style conditional expressions for attribute values: + + KEY : VALUE if CONDITION [else OTHER-VALUE] + +For example: + + %img{'src': 'hello' if coming else 'goodbye' } + +is compiled to: + + + +The 'else' part may be omitted, for example: + + %div{'id': 'No1' if tree is TheLarch} + +The 'else' part also may contain conditional expression: + + 'score': '29.9' if name == 'St Stephan' else '29.3' if name == 'Richard III' + +For the 'class' and 'id' attributes conditional expressions are processed in a different way: condition tags are placed inside the value rather than around the whole attribute. That is done so because these attributes may get additional value parts from [HAML syntax](#class-and-id--and-). The downside is that conditional expression cannot remove 'class' or 'id' attribute altogether, as it happens with common attributes. Example: + + %div{'id': 'dog_kennel' if assisant.name == 'Mr Lambert' else 'mattress', + 'class': 'the-larch' if tree is quite_a_long_way_away} + +is rendered to: + +
+ ### Class and ID: . and # The period and pound sign are borrowed from CSS. They are used as shortcuts to specify the class and id attributes of an element, respectively. Multiple class names can be specified by chaining class names together with periods. They are placed immediately after a tag and before an attribute dictionary. For example: diff --git a/setup.py b/setup.py index 58de695..dc44a43 100644 --- a/setup.py +++ b/setup.py @@ -2,8 +2,8 @@ # Note to Jesse - only push sdist to PyPi, bdist seems to always break pip installer setup(name='hamlpy', - version = '0.82.2', - download_url = 'git@github.com:jessemiller/HamlPy.git', + version = '0.82.2.3', + download_url = 'git@github.com:a1s/HamlPy.git', packages = ['hamlpy', 'hamlpy.template'], author = 'Jesse Miller', author_email = 'millerjesse@gmail.com',