Skip to content
This repository has been archived by the owner on Aug 17, 2023. It is now read-only.

Commit

Permalink
Merge 22dfa7d into cc7695b
Browse files Browse the repository at this point in the history
  • Loading branch information
rowanseymour committed Dec 1, 2016
2 parents cc7695b + 22dfa7d commit 5e35efd
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 122 deletions.
22 changes: 14 additions & 8 deletions hamlpy/hamlpy.py
Expand Up @@ -8,26 +8,32 @@
VALID_EXTENSIONS = ['haml', 'hamlpy']


class Compiler:
DEFAULT_OPTIONS = {
'attr_wrapper': '\'', # how to render attribute values, e.g. foo='bar'
'django_inline_style': True, # support both #{...} and ={...}
'debug_tree': False
}


class Compiler:
def __init__(self, options_dict=None):
options_dict = options_dict or {}
self.debug_tree = options_dict.pop('debug_tree', False)
self.options_dict = options_dict
self.options = DEFAULT_OPTIONS.copy()
if options_dict:
self.options.update(options_dict)

def process(self, raw_text):
split_text = raw_text.split('\n')
return self.process_lines(split_text)

def process_lines(self, haml_lines):
root = RootNode(**self.options_dict)
root = RootNode(self.options)
line_iter = iter(haml_lines)

haml_node=None
for line_number, line in enumerate(line_iter):
node_lines = line

if not root.parent_of(HamlNode(line)).inside_filter_node():
if not root.parent_of(HamlNode(line, self.options)).inside_filter_node():
if line.count('{') - line.count('}') == 1:
start_multiline = line_number # For exception handling

Expand All @@ -42,11 +48,11 @@ def process_lines(self, haml_lines):
if haml_node is not None and len(node_lines.strip()) == 0:
haml_node.newlines += 1
else:
haml_node = create_node(node_lines)
haml_node = create_node(node_lines, self.options)
if haml_node:
root.add_node(haml_node)

if self.options_dict and self.options_dict.get('debug_tree'):
if self.options['debug_tree']:
return root.debug_tree()
else:
return root.render()
Expand Down
5 changes: 5 additions & 0 deletions hamlpy/hamlpy_watcher.py
Expand Up @@ -51,6 +51,8 @@ def __call__(self, parser, namespace, values, option_string=None):
help='Add self closing tag. eg. --tag macro:endmacro')
arg_parser.add_argument('--attr-wrapper', dest='attr_wrapper', type=str, choices=('"', "'"), default="'", action='store',
help="The character that should wrap element attributes. This defaults to ' (an apostrophe).")
arg_parser.add_argument('--django-inline', dest='django_inline', action='store_true',
help="Whether to support ={...} syntax for inline variables in addition to #{...}")
arg_parser.add_argument('--jinja', default=False, action='store_true',
help='Makes the necessary changes to be used with Jinja2.')
arg_parser.add_argument('--once', default=False, action='store_true',
Expand Down Expand Up @@ -91,6 +93,9 @@ def watch_folder():

if args.attr_wrapper:
compiler_args['attr_wrapper'] = args.attr_wrapper

if args.django_inline:
compiler_args['django_inline_style'] = args.django_inline

if args.jinja:
for k in ('ifchanged', 'ifequal', 'ifnotequal', 'autoescape', 'blocktrans',
Expand Down
135 changes: 79 additions & 56 deletions hamlpy/nodes.py
Expand Up @@ -40,9 +40,6 @@ class NotAvailableError(Exception):
VARIABLE = '='
TAG = '-'

INLINE_VARIABLE = re.compile(r'(?<!\\)([#=]\{\s*(.+?)\s*\})')
ESCAPED_INLINE_VARIABLE = re.compile(r'\\([#=]\{\s*(.+?)\s*\})')

COFFEESCRIPT_FILTERS = [':coffeescript', ':coffee']
JAVASCRIPT_FILTER = ':javascript'
CSS_FILTER = ':css'
Expand All @@ -57,71 +54,95 @@ class NotAvailableError(Exception):

HAML_ESCAPE = '\\'

def create_node(haml_line):

_inline_variable_regexes = None


def get_inline_variable_regex(options):
"""
Generates regular expressions for inline variables and escaped inline variables, based on compiler options
"""
global _inline_variable_regexes

if not _inline_variable_regexes:
prefixes = ['=', '#'] if options['django_inline_style'] else ['#']
prefixes = ''.join(prefixes)
_inline_variable_regexes = (
re.compile(r'(?<!\\)([' + prefixes + r']\{\s*(.+?)\s*\})'),
re.compile(r'\\([' + prefixes + r']\{\s*(.+?)\s*\})')
)
return _inline_variable_regexes


def create_node(haml_line, options):
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)
inline_var_regex, escaped_var_regex = get_inline_variable_regex(options)

if re.match(inline_var_regex, stripped_line) or re.match(escaped_var_regex, stripped_line):
return PlaintextNode(haml_line, options)

if stripped_line[0] == HAML_ESCAPE:
return PlaintextNode(haml_line)
return PlaintextNode(haml_line, options)

if stripped_line.startswith(DOCTYPE):
return DoctypeNode(haml_line)
return DoctypeNode(haml_line, options)

if stripped_line[0] in ELEMENT_CHARACTERS:
return ElementNode(haml_line)
return ElementNode(haml_line, options)

if stripped_line[0:len(CONDITIONAL_COMMENT)] == CONDITIONAL_COMMENT:
return ConditionalCommentNode(haml_line)
return ConditionalCommentNode(haml_line, options)

if stripped_line[0] == HTML_COMMENT:
return CommentNode(haml_line)
return CommentNode(haml_line, options)

for comment_prefix in HAML_COMMENTS:
if stripped_line.startswith(comment_prefix):
return HamlCommentNode(haml_line)
return HamlCommentNode(haml_line, options)

if stripped_line[0] == VARIABLE:
return VariableNode(haml_line)
return VariableNode(haml_line, options)

if stripped_line[0] == TAG:
return TagNode(haml_line)
return TagNode(haml_line, options)

if stripped_line == JAVASCRIPT_FILTER:
return JavascriptFilterNode(haml_line)
return JavascriptFilterNode(haml_line, options)

if stripped_line in COFFEESCRIPT_FILTERS:
return CoffeeScriptFilterNode(haml_line)
return CoffeeScriptFilterNode(haml_line, options)

if stripped_line == CSS_FILTER:
return CssFilterNode(haml_line)
return CssFilterNode(haml_line, options)

if stripped_line == STYLUS_FILTER:
return StylusFilterNode(haml_line)
return StylusFilterNode(haml_line, options)

if stripped_line == PLAIN_FILTER:
return PlainFilterNode(haml_line)
return PlainFilterNode(haml_line, options)

if stripped_line == PYTHON_FILTER:
return PythonFilterNode(haml_line)
return PythonFilterNode(haml_line, options)

if stripped_line == CDATA_FILTER:
return CDataFilterNode(haml_line)
return CDataFilterNode(haml_line, options)

if stripped_line == PYGMENTS_FILTER:
return PygmentsFilterNode(haml_line)
return PygmentsFilterNode(haml_line, options)

if stripped_line == MARKDOWN_FILTER:
return MarkdownFilterNode(haml_line)
return MarkdownFilterNode(haml_line, options)

return PlaintextNode(haml_line)
return PlaintextNode(haml_line, options)

class TreeNode(object):
''' Generic parent/child tree class'''
"""
Generic parent/child tree class
"""
def __init__(self):
self.parent = None
self.children = []
Expand All @@ -141,8 +162,11 @@ def add_child(self, child):
self.children.append(child)

class RootNode(TreeNode):
def __init__(self, attr_wrapper="'"):
TreeNode.__init__(self)
def __init__(self, options):
super(RootNode, self).__init__()

self.options = options

self.indentation = -2
# Number of empty lines to render after node
self.newlines = 0
Expand All @@ -153,14 +177,6 @@ def __init__(self, attr_wrapper="'"):
# Indicates that a node does not render anything (for whitespace removal)
self.empty_node = False

# Options
self.attr_wrapper = attr_wrapper

def add_child(self, child):
'''Add child node, and copy all options to it'''
super(RootNode, self).add_child(child)
child.attr_wrapper = self.attr_wrapper

def render(self):
# Render (sets self.before and self.after)
self._render_children()
Expand Down Expand Up @@ -231,16 +247,20 @@ def __repr__(self):
return '(%s)' % (self.__class__)

class HamlNode(RootNode):
def __init__(self, haml):
RootNode.__init__(self)
def __init__(self, haml, options):
super(HamlNode, self).__init__(options)

RootNode.__init__(self, options)
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))

def replace_inline_variables(self, content):
content = re.sub(INLINE_VARIABLE, r'{{ \2 }}', content)
content = re.sub(ESCAPED_INLINE_VARIABLE, r'\1', content)
inline_var_regex, escaped_var_regex = get_inline_variable_regex(self.options)

content = re.sub(inline_var_regex, r'{{ \2 }}', content)
content = re.sub(escaped_var_regex, r'\1', content)
return content

def __repr__(self):
Expand All @@ -263,12 +283,13 @@ def _render(self):

class ElementNode(HamlNode):
'''Node which represents a HTML tag'''
def __init__(self, haml):
HamlNode.__init__(self, haml)
def __init__(self, haml, options):
super(ElementNode, self).__init__(haml, options)

self.django_variable = False

def _render(self):
self.element = Element(self.haml, self.attr_wrapper)
self.element = Element(self.haml, self.options['attr_wrapper'])
self.django_variable = self.element.django_variable
self.before = self._render_before(self.element)
self.after = self._render_after(self.element)
Expand Down Expand Up @@ -391,10 +412,11 @@ def _render(self):

parts = doctype.split()
if parts and parts[0] == "XML":
attr_wrapper = self.options['attr_wrapper']
encoding = parts[1] if len(parts) > 1 else 'utf-8'
self.before = "<?xml version=%s1.0%s encoding=%s%s%s ?>" % (
self.attr_wrapper, self.attr_wrapper,
self.attr_wrapper, encoding, self.attr_wrapper,
attr_wrapper, attr_wrapper,
attr_wrapper, encoding, attr_wrapper,
)
else:
types = {
Expand All @@ -418,8 +440,9 @@ def _post_render(self):
pass

class VariableNode(ElementNode):
def __init__(self, haml):
ElementNode.__init__(self, haml)
def __init__(self, haml, options):
super(VariableNode, self).__init__(haml, options)

self.django_variable = True

def _render(self):
Expand Down Expand Up @@ -455,8 +478,9 @@ class TagNode(HamlNode):
'for':'empty',
'with':'with'}

def __init__(self, haml):
HamlNode.__init__(self, haml)
def __init__(self, haml, options):
super(TagNode, self).__init__(haml, options)

self.tag_statement = self.haml.lstrip(TAG).strip()
self.tag_name = self.tag_statement.split(' ')[0]

Expand All @@ -478,7 +502,6 @@ def _render(self):
def should_contain(self, node):
return isinstance(node, TagNode) and node.tag_name in self.may_contain.get(self.tag_name, '')


class FilterNode(HamlNode):
def add_node(self, node):
self.add_child(node)
Expand All @@ -502,10 +525,10 @@ def _post_render(self):
# Don't post-render children of filter nodes as we don't want them to be interpreted as HAML
pass


class PlainFilterNode(FilterNode):
def __init__(self, haml):
FilterNode.__init__(self, haml)
def __init__(self, haml, options):
super(PlainFilterNode, self).__init__(haml, options)

self.empty_node = True

def _render(self):
Expand Down Expand Up @@ -541,7 +564,7 @@ def _render(self):
class JavascriptFilterNode(FilterNode):
def _render(self):
self.before = '<script type=%(attr_wrapper)stext/javascript%(attr_wrapper)s>\n// <![CDATA[%(new_lines)s' % {
'attr_wrapper': self.attr_wrapper,
'attr_wrapper': self.options['attr_wrapper'],
'new_lines': self.render_newlines(),
}
self.after = '// ]]>\n</script>\n'
Expand All @@ -550,7 +573,7 @@ def _render(self):
class CoffeeScriptFilterNode(FilterNode):
def _render(self):
self.before = '<script type=%(attr_wrapper)stext/coffeescript%(attr_wrapper)s>\n#<![CDATA[%(new_lines)s' % {
'attr_wrapper': self.attr_wrapper,
'attr_wrapper': self.options['attr_wrapper'],
'new_lines': self.render_newlines(),
}
self.after = '#]]>\n</script>\n'
Expand All @@ -559,7 +582,7 @@ def _render(self):
class CssFilterNode(FilterNode):
def _render(self):
self.before = '<style type=%(attr_wrapper)stext/css%(attr_wrapper)s>\n/*<![CDATA[*/%(new_lines)s' % {
'attr_wrapper': self.attr_wrapper,
'attr_wrapper': self.options['attr_wrapper'],
'new_lines': self.render_newlines(),
}
self.after = '/*]]>*/\n</style>\n'
Expand All @@ -568,7 +591,7 @@ def _render(self):
class StylusFilterNode(FilterNode):
def _render(self):
self.before = '<style type=%(attr_wrapper)stext/stylus%(attr_wrapper)s>\n/*<![CDATA[*/%(new_lines)s' % {
'attr_wrapper': self.attr_wrapper,
'attr_wrapper': self.options['attr_wrapper'],
'new_lines': self.render_newlines(),
}
self.after = '/*]]>*/\n</style>\n'
Expand Down

0 comments on commit 5e35efd

Please sign in to comment.