From fe8e9c1a09d0d461476acb79b7ca97dc92938070 Mon Sep 17 00:00:00 2001 From: alexander smishlajev Date: Sat, 21 Sep 2013 07:51:11 +0300 Subject: [PATCH 01/14] regex-based parsing for attribute dictionaries --- hamlpy/elements.py | 106 ++++++++++++++++++++++++++--------- hamlpy/test/hamlpy_test.py | 2 +- hamlpy/test/test_elements.py | 58 +++++++++---------- 3 files changed, 110 insertions(+), 56 deletions(-) diff --git a/hamlpy/elements.py b/hamlpy/elements.py index 3bb5d3d..44864fb 100644 --- a/hamlpy/elements.py +++ b/hamlpy/elements.py @@ -33,6 +33,20 @@ 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)\s*,?\s*)*\]" % (
+        _SINGLE_QUOTE_STRING_LITERAL_REGEX, _DOUBLE_QUOTE_STRING_LITERAL_REGEX)
+    _VALUE_TUPLE_REGEX = r"\(\s*(?:(?:%s|%s)\s*,?\s*)*\)" % (
+        _SINGLE_QUOTE_STRING_LITERAL_REGEX, _DOUBLE_QUOTE_STRING_LITERAL_REGEX)
+    ATTRVAL_REGEX = re.compile(r"\d+|None(?!\w)|%s|%s|%s|%s" % (
+        _SINGLE_QUOTE_STRING_LITERAL_REGEX, _DOUBLE_QUOTE_STRING_LITERAL_REGEX,
+        _VALUE_LIST_REGEX, _VALUE_TUPLE_REGEX), re.UNICODE)
+
+    NEWLINE_REGEX = re.compile("[\r\n]+")
+
 
     def __init__(self, haml, attr_wrapper="'"):
         self.haml = haml
@@ -66,7 +80,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 +96,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 = ''
@@ -112,7 +126,7 @@ def _escape_attribute_quotes(self, v):
     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,31 +135,71 @@ 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]):
+                    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)
-                #raise Exception('failed to decode: %s. Details: %s'%(attribute_dict_string, e))
+                #raise Exception('failed to decode: %s' % attribute_dict_string)
+                raise Exception('failed to decode: %s. Details: %s'%(attribute_dict_string, e))
 
         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))
+            string = string[match.end():]
+            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():].strip()
+            if string.startswith(","):
+                string = string[1:].strip()
+            yield (key, val)
+
+    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/test/hamlpy_test.py b/hamlpy/test/hamlpy_test.py
index 5601c96..59eba5b 100755
--- a/hamlpy/test/hamlpy_test.py
+++ b/hamlpy/test/hamlpy_test.py
@@ -312,7 +312,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/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'}""") From bd088dc6a1d3b90e6a6251a2ddf5187596192217 Mon Sep 17 00:00:00 2001 From: alexander smishlajev Date: Sat, 21 Sep 2013 09:18:54 +0300 Subject: [PATCH 02/14] Conditions in attribute dictionaries --- hamlpy/elements.py | 50 +++++++++++++++++++++++++++++++++++--- hamlpy/test/hamlpy_test.py | 16 ++++++++++++ reference.md | 31 ++++++++++++++++++++++- 3 files changed, 93 insertions(+), 4 deletions(-) diff --git a/hamlpy/elements.py b/hamlpy/elements.py index 44864fb..8414a96 100644 --- a/hamlpy/elements.py +++ b/hamlpy/elements.py @@ -2,6 +2,17 @@ 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 + class Element(object): """contains the pieces of an element and can populate itself from haml element text""" @@ -45,6 +56,10 @@ class Element(object): _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)+?(?=,| else |$)" % ( + _SINGLE_QUOTE_STRING_LITERAL_REGEX, _DOUBLE_QUOTE_STRING_LITERAL_REGEX), + re.UNICODE) + NEWLINE_REGEX = re.compile("[\r\n]+") @@ -136,7 +151,21 @@ def _parse_attribute_dictionary(self, attribute_dict_string): attribute_dict_string = re.sub(self.ATTRIBUTE_REGEX, '\g
"\g":\g', attribute_dict_string)
                 # Parse string as dictionary
                 for (key, val) in self.parse_attr(attribute_dict_string[1:-1]):
-                    value = self.add_attr(key, val)
+                    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))
+                        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:
@@ -158,9 +187,24 @@ def parse_attr(self, string):
             if not match:
                 raise SyntaxError("Dictionary value expected at %r" % string)
             val = eval(match.group(0))
-            string = string[match.end():].strip()
+            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 "):
+                    string = string[5:].lstrip()
+                    match = self.ATTRVAL_REGEX.match(string)
+                    if not match:
+                        raise SyntaxError(
+                            "Missing \"else\" expression at %r" % string)
+                    val = Conditional(condition, val, eval(match.group(0)))
+                    string = string[match.end():].lstrip()
+                else:
+                    val = Conditional(condition, val)
             if string.startswith(","):
-                string = string[1:].strip()
+                string = string[1:].lstrip()
             yield (key, val)
 
     def add_attr(self, key, value):
diff --git a/hamlpy/test/hamlpy_test.py b/hamlpy/test/hamlpy_test.py
index 59eba5b..d772edb 100755
--- a/hamlpy/test/hamlpy_test.py
+++ b/hamlpy/test/hamlpy_test.py
@@ -57,6 +57,22 @@ 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' }",
+             ""),
+            # 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' diff --git a/reference.md b/reference.md index 542bdb0..f50fad7 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,35 @@ 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} + +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: From 054a59f76b8be3b5e4ae985f76f97f3569c47f1c Mon Sep 17 00:00:00 2001 From: alexander smishlajev Date: Sat, 21 Sep 2013 09:30:51 +0300 Subject: [PATCH 03/14] Undo debug output of parsing exceptions --- hamlpy/elements.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hamlpy/elements.py b/hamlpy/elements.py index 8414a96..02c1707 100644 --- a/hamlpy/elements.py +++ b/hamlpy/elements.py @@ -169,8 +169,8 @@ def _parse_attribute_dictionary(self, attribute_dict_string): attributes_dict[key] = value self.attributes = self.attributes.strip() except Exception, e: - #raise Exception('failed to decode: %s' % attribute_dict_string) - raise Exception('failed to decode: %s. Details: %s'%(attribute_dict_string, e)) + raise Exception('failed to decode: %s' % attribute_dict_string) + #raise Exception('failed to decode: %s. Details: %s'%(attribute_dict_string, e)) return attributes_dict From dcbe9165ae67378b3847a52dcf96f9ff9e2a2f08 Mon Sep 17 00:00:00 2001 From: alexander smishlajev Date: Sat, 21 Sep 2013 12:55:26 +0300 Subject: [PATCH 04/14] add support for chained conditions --- hamlpy/elements.py | 71 +++++++++++++++++++++++--------------- hamlpy/test/hamlpy_test.py | 2 ++ reference.md | 4 +++ 3 files changed, 50 insertions(+), 27 deletions(-) diff --git a/hamlpy/elements.py b/hamlpy/elements.py index 02c1707..20a235d 100644 --- a/hamlpy/elements.py +++ b/hamlpy/elements.py @@ -13,6 +13,13 @@ def __init__(self, test, body, orelse=NOTHING): 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""" @@ -48,17 +55,17 @@ class Element(object): 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)\s*,?\s*)*\]" % ( + _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)\s*,?\s*)*\)" % ( + _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"\d+|None(?!\w)|%s|%s|%s|%s" % ( + 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)+?(?=,| else |$)" % ( - _SINGLE_QUOTE_STRING_LITERAL_REGEX, _DOUBLE_QUOTE_STRING_LITERAL_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]+") @@ -156,6 +163,12 @@ def _parse_attribute_dictionary(self, attribute_dict_string): 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 %} " @@ -182,31 +195,35 @@ def parse_attr(self, string): if not match: raise SyntaxError("Dictionary key expected at %r" % string) key = eval(match.group(1)) - string = string[match.end():] - 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 "): - string = string[5:].lstrip() - match = self.ATTRVAL_REGEX.match(string) - if not match: - raise SyntaxError( - "Missing \"else\" expression at %r" % string) - val = Conditional(condition, val, eval(match.group(0))) - string = string[match.end():].lstrip() - else: - val = Conditional(condition, val) + (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 diff --git a/hamlpy/test/hamlpy_test.py b/hamlpy/test/hamlpy_test.py index d772edb..06a11d3 100755 --- a/hamlpy/test/hamlpy_test.py +++ b/hamlpy/test/hamlpy_test.py @@ -63,6 +63,8 @@ def test_dictionaries_allow_conditionals(self): ""), ("%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'}", diff --git a/reference.md b/reference.md index f50fad7..bac133f 100644 --- a/reference.md +++ b/reference.md @@ -135,6 +135,10 @@ 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', From a4b152c7b4fa30b9ac1a86ad94b7f66a347f6e5a Mon Sep 17 00:00:00 2001 From: Alexander Smishlajev Date: Sun, 19 Oct 2014 15:51:29 +0300 Subject: [PATCH 05/14] Support for plular forms in {% blocktrans %} --- hamlpy/nodes.py | 1 + 1 file changed, 1 insertion(+) mode change 100644 => 100755 hamlpy/nodes.py diff --git a/hamlpy/nodes.py b/hamlpy/nodes.py old mode 100644 new mode 100755 index a5e7e4a..8a8b5bc --- a/hamlpy/nodes.py +++ b/hamlpy/nodes.py @@ -445,6 +445,7 @@ class TagNode(HamlNode): 'ifchanged':'else', 'ifequal':'else', 'ifnotequal':'else', + 'blocktrans':'plural', 'for':'empty', 'with':'with'} From 2a3ef4ca40eb5f33f2242deac213d5a4576bd859 Mon Sep 17 00:00:00 2001 From: kacah Date: Thu, 26 Feb 2015 22:50:20 +0200 Subject: [PATCH 06/14] Added line breaking functionality with '\' symbol (with tests) --- hamlpy/hamlpy.py | 12 ++++++++++ hamlpy/test/hamlpy_test.py | 6 +++++ hamlpy/test/template_compare_test.py | 31 ++++++++++++++------------ hamlpy/test/templates/lineBreak.hamlpy | 7 ++++++ hamlpy/test/templates/lineBreak.html | 5 +++++ 5 files changed, 47 insertions(+), 14 deletions(-) create mode 100644 hamlpy/test/templates/lineBreak.hamlpy create mode 100644 hamlpy/test/templates/lineBreak.html diff --git a/hamlpy/hamlpy.py b/hamlpy/hamlpy.py index 4aa5037..5a7be6f 100755 --- 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 diff --git a/hamlpy/test/hamlpy_test.py b/hamlpy/test/hamlpy_test.py index 06a11d3..238bae8 100755 --- a/hamlpy/test/hamlpy_test.py +++ b/hamlpy/test/hamlpy_test.py @@ -313,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" diff --git a/hamlpy/test/template_compare_test.py b/hamlpy/test/template_compare_test.py index 3d20485..909c470 100644 --- a/hamlpy/test/template_compare_test.py +++ b/hamlpy/test/template_compare_test.py @@ -13,34 +13,37 @@ 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_filters(self): self._compare_test_files('filters') - + def test_filters_markdown(self): try: import markdown @@ -81,7 +84,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 +112,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 %} + From 2eb1fa30827a52d8beef7a29bbcac66b0d873f97 Mon Sep 17 00:00:00 2001 From: kacah Date: Sat, 28 Feb 2015 06:25:57 +0200 Subject: [PATCH 07/14] Support for line breaks inside Node parameters ol (with tests) --- hamlpy/hamlpy.py | 3 +++ hamlpy/test/template_compare_test.py | 3 +++ hamlpy/test/templates/lineBreakInNode.hamlpy | 4 ++++ hamlpy/test/templates/lineBreakInNode.html | 1 + 4 files changed, 11 insertions(+) create mode 100644 hamlpy/test/templates/lineBreakInNode.hamlpy create mode 100644 hamlpy/test/templates/lineBreakInNode.html diff --git a/hamlpy/hamlpy.py b/hamlpy/hamlpy.py index 5a7be6f..60e8c08 100755 --- a/hamlpy/hamlpy.py +++ b/hamlpy/hamlpy.py @@ -43,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/test/template_compare_test.py b/hamlpy/test/template_compare_test.py index 909c470..8fbbcbf 100644 --- a/hamlpy/test/template_compare_test.py +++ b/hamlpy/test/template_compare_test.py @@ -41,6 +41,9 @@ def test_nested_django_tags(self): 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') 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 @@ +
From 84abeed87310daa9cbb4b704dccc149a2dd53be5 Mon Sep 17 00:00:00 2001 From: Alexander Smishlajev Date: Tue, 10 Mar 2015 12:59:24 +0200 Subject: [PATCH 08/14] Fix https://github.com/jessemiller/HamlPy/issues/149 --- hamlpy/template/utils.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) mode change 100644 => 100755 hamlpy/template/utils.py diff --git a/hamlpy/template/utils.py b/hamlpy/template/utils.py old mode 100644 new mode 100755 index c9187e8..cb3d399 --- 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])]) From 04fffd04ad85034514297e2ad184a3b3bffaccf6 Mon Sep 17 00:00:00 2001 From: Alexander Smishlajev Date: Tue, 10 Mar 2015 13:00:04 +0200 Subject: [PATCH 09/14] Version 0.82.2.1 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 58de695..aa43a80 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.1', + download_url = 'git@github.com:a1s/HamlPy.git', packages = ['hamlpy', 'hamlpy.template'], author = 'Jesse Miller', author_email = 'millerjesse@gmail.com', From 2c8ed92f3fbe4cbceec7edca44f47a130620f8c1 Mon Sep 17 00:00:00 2001 From: a1s Date: Wed, 5 Jul 2017 12:30:43 +0300 Subject: [PATCH 10/14] Compatibility fix for Django v1.11: translation.templatize moved from trans_real to separate module --- hamlpy/__init__.py | 0 hamlpy/hamlpy.py | 0 hamlpy/nodes.py | 0 hamlpy/template/utils.py | 0 hamlpy/templatize.py | 4 ++-- hamlpy/test/hamlpy_test.py | 0 setup.py | 2 +- 7 files changed, 3 insertions(+), 3 deletions(-) mode change 100755 => 100644 hamlpy/__init__.py mode change 100755 => 100644 hamlpy/hamlpy.py mode change 100755 => 100644 hamlpy/nodes.py mode change 100755 => 100644 hamlpy/template/utils.py mode change 100755 => 100644 hamlpy/test/hamlpy_test.py diff --git a/hamlpy/__init__.py b/hamlpy/__init__.py old mode 100755 new mode 100644 diff --git a/hamlpy/hamlpy.py b/hamlpy/hamlpy.py old mode 100755 new mode 100644 diff --git a/hamlpy/nodes.py b/hamlpy/nodes.py old mode 100755 new mode 100644 diff --git a/hamlpy/template/utils.py b/hamlpy/template/utils.py old mode 100755 new mode 100644 diff --git a/hamlpy/templatize.py b/hamlpy/templatize.py index 9581109..2a40491 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 @@ -28,4 +28,4 @@ def templatize(src, origin=None): 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 diff --git a/setup.py b/setup.py index aa43a80..f25f3bd 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ # Note to Jesse - only push sdist to PyPi, bdist seems to always break pip installer setup(name='hamlpy', - version = '0.82.2.1', + version = '0.82.2.2', download_url = 'git@github.com:a1s/HamlPy.git', packages = ['hamlpy', 'hamlpy.template'], author = 'Jesse Miller', From 86ec7e3ab18f394b2adce724b7ded4d5e3ff6f53 Mon Sep 17 00:00:00 2001 From: a1s Date: Thu, 6 Jul 2017 10:56:15 +0300 Subject: [PATCH 11/14] Fix template loader for Django v1.11: use .get_contents() instead of .load_template_source() --- hamlpy/template/loaders.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) 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 From ff5ebd1748ece1f012c037a838cb470f78a6ed24 Mon Sep 17 00:00:00 2001 From: a1s Date: Fri, 7 Jul 2017 14:09:56 +0300 Subject: [PATCH 12/14] Compatibility fix for Django v1.11: the source for translation.templatize() is Unicode, not bytestring --- hamlpy/templatize.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/hamlpy/templatize.py b/hamlpy/templatize.py index 2a40491..afc2bda 100644 --- a/hamlpy/templatize.py +++ b/hamlpy/templatize.py @@ -16,15 +16,14 @@ 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: From eb6891875fe8aa6f7e6894a7454a938df89ade4a Mon Sep 17 00:00:00 2001 From: a1s Date: Mon, 11 Feb 2019 16:49:54 +0200 Subject: [PATCH 13/14] Parsing speed-ups --- hamlpy/elements.py | 18 +++--- hamlpy/nodes.py | 140 +++++++++++++++++++++++---------------------- 2 files changed, 80 insertions(+), 78 deletions(-) diff --git a/hamlpy/elements.py b/hamlpy/elements.py index 20a235d..979ee64 100644 --- a/hamlpy/elements.py +++ b/hamlpy/elements.py @@ -69,6 +69,8 @@ class Element(object): NEWLINE_REGEX = re.compile("[\r\n]+") + DJANGO_TAG_REGEX = re.compile("({%|%})") + def __init__(self, haml, attr_wrapper="'"): self.haml = haml @@ -132,17 +134,15 @@ 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): diff --git a/hamlpy/nodes.py b/hamlpy/nodes.py index 8a8b5bc..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) @@ -605,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) + From 055a731b265d5966a73466dd16bc0c07ba3bf1d8 Mon Sep 17 00:00:00 2001 From: a1s Date: Mon, 11 Feb 2019 16:52:48 +0200 Subject: [PATCH 14/14] Version 0.82.2.3 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f25f3bd..dc44a43 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ # Note to Jesse - only push sdist to PyPi, bdist seems to always break pip installer setup(name='hamlpy', - version = '0.82.2.2', + version = '0.82.2.3', download_url = 'git@github.com:a1s/HamlPy.git', packages = ['hamlpy', 'hamlpy.template'], author = 'Jesse Miller',