Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python-style conditional constructs in attribute dictionaries #148

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Empty file modified hamlpy/__init__.py
100755 → 100644
Empty file.
175 changes: 145 additions & 30 deletions hamlpy/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand Down Expand Up @@ -33,6 +51,26 @@ class Element(object):
ATTRIBUTE_REGEX = re.compile(r'(?P<pre>\{\s*|,\s*)%s\s*:\s*%s' % (_ATTRIBUTE_KEY_REGEX, _ATTRIBUTE_VALUE_REGEX), re.UNICODE)
DJANGO_VARIABLE_REGEX = re.compile(r'^\s*=\s(?P<variable>[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
Expand Down Expand Up @@ -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
Expand All @@ -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 = ''
Expand All @@ -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

Expand All @@ -121,31 +157,110 @@ def _parse_attribute_dictionary(self, attribute_dict_string):
# Put double quotes around key
attribute_dict_string = re.sub(self.ATTRIBUTE_REGEX, '\g<pre>"\g<key>":\g<val>', 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<variable>}}', 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)
#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))
(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<variable>}}', 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
15 changes: 15 additions & 0 deletions hamlpy/hamlpy.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,28 @@ 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

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
Expand Down
Loading