diff --git a/hamlpy/__init__.py b/hamlpy/__init__.py
index b0ad793..b61a1f3 100755
--- a/hamlpy/__init__.py
+++ b/hamlpy/__init__.py
@@ -1,4 +1 @@
-try:
- import templatize
-except ImportError:
- pass
+import templatize
diff --git a/hamlpy/attribute_dict_parser.py b/hamlpy/attribute_dict_parser.py
index b02c1d3..4b48ee7 100644
--- a/hamlpy/attribute_dict_parser.py
+++ b/hamlpy/attribute_dict_parser.py
@@ -7,12 +7,12 @@
re_whitespace = re.compile(r'([ \t]+)')
re_leading_spaces = re.compile(r'^\s+', re.MULTILINE)
re_line = re.compile(r'.*')
-re_sq= re.compile(r'(.*?)(?= initial_indentation:
line=re_line.match(self.s, pos=self.ptr).group(0)
lines.append(line)
self.ptr += len(line)+1
-
+
h=hamlpy.Compiler()
html = h.process_lines(lines)
return re.sub(re_leading_spaces, ' ', html).replace('\n', '').strip()
@@ -150,12 +151,11 @@ def __parse_key(self):
if self.s[self.ptr] == ':':
self.ptr+=1
-
+
# Consume opening quote
quote=None
if self.s[self.ptr] in ("'",'"'):
quote = self.s[self.ptr]
- self.ptr += 1
# Extract key
if quote:
@@ -197,9 +197,9 @@ def parse(self):
lst.append(val)
self.ptr +=1
-
+
if self.terminator==')':
return tuple(lst)
else:
return lst
-
+
diff --git a/hamlpy/elements.py b/hamlpy/elements.py
index e2d0fbd..b26e8cf 100644
--- a/hamlpy/elements.py
+++ b/hamlpy/elements.py
@@ -3,7 +3,7 @@
class Element(object):
"""contains the pieces of an element and can populate itself from haml element text"""
-
+
self_closing_tags = ('meta', 'img', 'link', 'br', 'hr', 'input', 'source', 'track', 'area', 'base', 'col', 'command', 'embed', 'keygen', 'param', 'wbr')
ELEMENT = '%'
@@ -20,11 +20,12 @@ class Element(object):
(?P/)?
(?P=)?
(?P[^\w\.#\{].*)?
- """, re.X|re.MULTILINE|re.DOTALL)
+ """, re.X | re.MULTILINE | re.DOTALL | re.UNICODE)
- def __init__(self, haml):
+ def __init__(self, haml, attr_wrapper="'"):
self.haml = haml
+ self.attr_wrapper = attr_wrapper
self.tag = None
self.id = None
self.classes = None
@@ -35,7 +36,10 @@ def __init__(self, haml):
self.nuke_outer_whitespace = False
self.inline_content = ''
self._parse_haml()
-
+
+ def attr_wrap(self, value):
+ return '%s%s%s' % (self.attr_wrapper, value, self.attr_wrapper)
+
def _parse_haml(self):
split_tags = self.HAML_REGEX.search(self.haml).groupdict('')
@@ -46,7 +50,7 @@ def _parse_haml(self):
self.id = self._parse_id(split_tags.get('id'))
self.classes = self._parse_classes(split_tags.get('class'))
self.self_close = split_tags.get('selfclose') or self.tag in self.self_closing_tags
-
+
self.attributes = self._render_attributes(self.attributes_dict, self.id, self.classes)
self.nuke_inner_whitespace = split_tags.get('nuke_inner_whitespace') != ''
@@ -58,7 +62,7 @@ def _parse_classes(self, classes):
tag_classes = classes.lstrip(self.CLASS).replace('.', ' ')
dict_classes = self._parse_class_from_attributes_dict()
return ('%s %s' % (tag_classes, dict_classes)).strip()
-
+
def _parse_class_from_attributes_dict(self):
cla = self.attributes_dict.get('class', '')
if isinstance(cla, list) or isinstance(cla, tuple):
@@ -73,7 +77,7 @@ def _parse_id(self, id_haml):
id_text += '_'
id_text += self._parse_id_dict(self.attributes_dict['id'])
return id_text
-
+
def _parse_id_dict(self, id_dict):
id_dict = self.attributes_dict.get('id')
@@ -84,11 +88,11 @@ def _parse_id_dict(self, id_dict):
def _render_attributes(self, dict, id, classes):
attributes=[]
-
+
if len(id) > 0:
- attributes.append("id='%s'" % self.id)
+ attributes.append("id=%s" % self.attr_wrap(self.id))
if len(classes) > 0:
- attributes.append("class='%s'" % self.classes)
+ attributes.append("class=%s" % self.attr_wrap(self.classes))
for k, v in dict.items():
if k != 'id' and k != 'class':
@@ -97,24 +101,23 @@ def _render_attributes(self, dict, id, classes):
attributes.append( "%s" % (k,))
else:
value = self._escape_attribute_quotes(v)
- attributes.append( "%s='%s'" % (k, value))
-
+ attributes.append( "%s=%s" % (k, self.attr_wrap(value)))
+
return ' '.join(attributes)
-
-
- def _escape_attribute_quotes(self,v):
+
+ def _escape_attribute_quotes(self, v):
'''
- Escapes single quotes with a backslash, except those inside a Django tag
+ Escapes quotes with a backslash, except those inside a Django tag
'''
- escaped=[]
+ escaped = []
inside_tag = False
for i, _ in enumerate(v):
- if v[i:i+2] == '{%':
- inside_tag=True
- elif v[i:i+2] == '%}':
- inside_tag=False
+ if v[i:i + 2] == '{%':
+ inside_tag = True
+ elif v[i:i + 2] == '%}':
+ inside_tag = False
- if v[i]=="'" and not inside_tag:
+ if v[i] == self.attr_wrapper and not inside_tag:
escaped.append('\\')
escaped.append(v[i])
diff --git a/hamlpy/ext.py b/hamlpy/ext.py
new file mode 100644
index 0000000..096b8cc
--- /dev/null
+++ b/hamlpy/ext.py
@@ -0,0 +1,42 @@
+# coding=utf-8
+try:
+ import jinja2.ext
+ _jinja2_available = True
+except ImportError, e:
+ _jinja2_available = False
+
+import hamlpy
+import os
+
+HAML_FILE_NAME_EXTENSIONS = ['haml', 'hamlpy']
+
+
+def clean_extension(file_ext):
+ if not isinstance(file_ext, basestring):
+ raise Exception('Wrong file extension format: %r' % file_ext)
+ if len(file_ext) > 1 and file_ext.startswith('.'):
+ file_ext = file_ext[1:]
+ return file_ext.lower().strip()
+
+
+def get_file_extension(file_path):
+ file_ext = os.path.splitext(file_path)[1]
+ return clean_extension(file_ext)
+
+
+def has_any_extension(file_path, extensions):
+ file_ext = get_file_extension(file_path)
+ return file_ext and extensions and file_ext in [clean_extension(e) for e in extensions]
+
+if _jinja2_available:
+ class HamlPyExtension(jinja2.ext.Extension):
+
+ def preprocess(self, source, name, filename=None):
+ if name and has_any_extension(name, HAML_FILE_NAME_EXTENSIONS):
+ compiler = hamlpy.Compiler()
+ try:
+ return compiler.process(source)
+ except Exception as e:
+ raise jinja2.TemplateSyntaxError(e, 1, name=name, filename=filename)
+ else:
+ return source
diff --git a/hamlpy/hamlpy.py b/hamlpy/hamlpy.py
index 4dfc2ee..ebbcafb 100755
--- a/hamlpy/hamlpy.py
+++ b/hamlpy/hamlpy.py
@@ -5,19 +5,25 @@
VALID_EXTENSIONS=['haml', 'hamlpy']
class Compiler:
- def process(self, raw_text, options=None):
+
+ 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
+
+ def process(self, raw_text):
split_text = raw_text.split('\n')
- return self.process_lines(split_text, options)
+ return self.process_lines(split_text)
- def process_lines(self, haml_lines, options=None):
- root = RootNode()
+ def process_lines(self, haml_lines):
+ root = RootNode(**self.options_dict)
line_iter = iter(haml_lines)
haml_node=None
for line_number, line in enumerate(line_iter):
node_lines = line
- if root.parent_of(HamlNode(line)).should_treat_children_as_multiline():
+ if not root.parent_of(HamlNode(line)).inside_filter_node():
if line.count('{') - line.count('}') == 1:
start_multiline=line_number # For exception handling
@@ -35,8 +41,8 @@ def process_lines(self, haml_lines, options=None):
haml_node = create_node(node_lines)
if haml_node:
root.add_node(haml_node)
-
- if options and options.debug_tree:
+
+ if self.options_dict and self.options_dict.get('debug_tree'):
return root.debug_tree()
else:
return root.render()
@@ -45,19 +51,27 @@ def convert_files():
import codecs
parser = OptionParser()
- parser.add_option("-d", "--debug-tree", dest="debug_tree",
- action="store_true", help="Print the generated tree instead of the HTML")
+ parser.add_option(
+ "-d", "--debug-tree", dest="debug_tree",
+ action="store_true",
+ help="Print the generated tree instead of the HTML")
+ parser.add_option(
+ "--attr-wrapper", dest="attr_wrapper",
+ type="choice", choices=('"', "'"), default="'",
+ action="store",
+ help="The character that should wrap element attributes. "
+ "This defaults to ' (an apostrophe).")
(options, args) = parser.parse_args()
if len(args) < 1:
print "Specify the input file as the first argument."
- else:
+ else:
infile = args[0]
haml_lines = codecs.open(infile, 'r', encoding='utf-8').read().splitlines()
- compiler = Compiler()
- output = compiler.process_lines(haml_lines, options=options)
-
+ compiler = Compiler(options.__dict__)
+ output = compiler.process_lines(haml_lines)
+
if len(args) == 2:
outfile = codecs.open(args[1], 'w', encoding='utf-8')
outfile.write(output)
diff --git a/hamlpy/hamlpy_watcher.py b/hamlpy/hamlpy_watcher.py
index add5f3c..04012b1 100644
--- a/hamlpy/hamlpy_watcher.py
+++ b/hamlpy/hamlpy_watcher.py
@@ -3,47 +3,111 @@
#
# Watch a folder for files with the given extensions and call the HamlPy
# compiler if the modified time has changed since the last check.
-from time import gmtime, strftime
+from time import strftime
+import argparse
import sys
import codecs
import os
import os.path
import time
import hamlpy
+import nodes as hamlpynodes
-CHECK_INTERVAL = 3 # in seconds
-DEBUG = False # print file paths when a file is compiled
+try:
+ str = unicode
+except NameError:
+ pass
+
+class Options(object):
+ CHECK_INTERVAL = 3 # in seconds
+ DEBUG = False # print file paths when a file is compiled
+ VERBOSE = False
+ OUTPUT_EXT = '.html'
# dict of compiled files [fullpath : timestamp]
compiled = dict()
+class StoreNameValueTagPair(argparse.Action):
+ def __call__(self, parser, namespace, values, option_string = None):
+ tags = getattr(namespace, 'tags', {})
+ if tags is None:
+ tags = {}
+ for item in values:
+ n, v = item.split(':')
+ tags[n] = v
+
+ setattr(namespace, 'tags', tags)
+
+arg_parser = argparse.ArgumentParser()
+arg_parser.add_argument('-v', '--verbose', help = 'Display verbose output', action = 'store_true')
+arg_parser.add_argument('-i', '--input-extension', metavar = 'EXT', default = '.hamlpy', help = 'The file extensions to look for', type = str, nargs = '+')
+arg_parser.add_argument('-ext', '--extension', metavar = 'EXT', default = Options.OUTPUT_EXT, help = 'The output file extension. Default is .html', type = str)
+arg_parser.add_argument('-r', '--refresh', metavar = 'S', default = Options.CHECK_INTERVAL, help = 'Refresh interval for files. Default is {} seconds'.format(Options.CHECK_INTERVAL), type = int)
+arg_parser.add_argument('input_dir', help = 'Folder to watch', type = str)
+arg_parser.add_argument('output_dir', help = 'Destination folder', type = str, nargs = '?')
+arg_parser.add_argument('--tag', help = 'Add self closing tag. eg. --tag macro:endmacro', type = str, nargs = 1, action = StoreNameValueTagPair)
+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('--jinja', help = 'Makes the necessary changes to be used with Jinja2', default = False, action = 'store_true')
+
def watched_extension(extension):
"""Return True if the given extension is one of the watched extensions"""
for ext in hamlpy.VALID_EXTENSIONS:
- if extension.endswith('.'+ext):
+ if extension.endswith('.' + ext):
return True
return False
def watch_folder():
"""Main entry point. Expects one or two arguments (the watch folder + optional destination folder)."""
- argv_len = len(sys.argv)
- if argv_len in (2, 3):
- folder = os.path.realpath(sys.argv[1])
- destination = os.path.realpath(argv_len == 3 and os.path.realpath(sys.argv[2]) or folder)
-
- print "Watching %s at refresh interval %s seconds" % (folder,CHECK_INTERVAL)
- while True:
- try:
- _watch_folder(folder, destination)
- time.sleep(CHECK_INTERVAL)
- except KeyboardInterrupt:
- # allow graceful exit (no stacktrace output)
- sys.exit(0)
- pass
+ argv = sys.argv[1:] if len(sys.argv) > 1 else []
+ args = arg_parser.parse_args(sys.argv[1:])
+ compiler_args = {}
+
+ input_folder = os.path.realpath(args.input_dir)
+ if not args.output_dir:
+ output_folder = input_folder
else:
- print "Usage: hamlpy-watcher.py [destination_folder]"
+ output_folder = os.path.realpath(args.output_dir)
+
+ if args.verbose:
+ Options.VERBOSE = True
+ print "Watching {} at refresh interval {} seconds".format(input_folder, args.refresh)
+
+ if args.extension:
+ Options.OUTPUT_EXT = args.extension
+
+ if getattr(args, 'tags', False):
+ hamlpynodes.TagNode.self_closing.update(args.tags)
+
+ if args.input_extension:
+ hamlpy.VALID_EXTENSIONS += args.input_extension
+
+ if args.attr_wrapper:
+ compiler_args['attr_wrapper'] = args.attr_wrapper
+
+ if args.jinja:
+ for k in ('ifchanged', 'ifequal', 'ifnotequal', 'autoescape', 'blocktrans',
+ 'spaceless', 'comment', 'cache', 'localize', 'compress'):
+ del hamlpynodes.TagNode.self_closing[k]
+
+ hamlpynodes.TagNode.may_contain.pop(k, None)
+
+ hamlpynodes.TagNode.self_closing.update({
+ 'macro' : 'endmacro',
+ 'call' : 'endcall',
+ 'raw' : 'endraw'
+ })
+
+ hamlpynodes.TagNode.may_contain['for'] = 'else'
+
+ while True:
+ try:
+ _watch_folder(input_folder, output_folder, compiler_args)
+ time.sleep(args.refresh)
+ except KeyboardInterrupt:
+ # allow graceful exit (no stacktrace output)
+ sys.exit(0)
-def _watch_folder(folder, destination):
+def _watch_folder(folder, destination, compiler_args):
"""Compares "modified" timestamps against the "compiled" dict, calls compiler
if necessary."""
for dirpath, dirnames, filenames in os.walk(folder):
@@ -53,32 +117,33 @@ def _watch_folder(folder, destination):
fullpath = os.path.join(dirpath, filename)
subfolder = os.path.relpath(dirpath, folder)
mtime = os.stat(fullpath).st_mtime
-
+
# Create subfolders in target directory if they don't exist
compiled_folder = os.path.join(destination, subfolder)
if not os.path.exists(compiled_folder):
os.makedirs(compiled_folder)
-
+
compiled_path = _compiled_path(compiled_folder, filename)
if (not fullpath in compiled or
compiled[fullpath] < mtime or
not os.path.isfile(compiled_path)):
- compile_file(fullpath, compiled_path)
+ compile_file(fullpath, compiled_path, compiler_args)
compiled[fullpath] = mtime
def _compiled_path(destination, filename):
- return os.path.join(destination, filename[:filename.rfind('.')] + '.html')
+ return os.path.join(destination, filename[:filename.rfind('.')] + Options.OUTPUT_EXT)
-def compile_file(fullpath, outfile_name):
+def compile_file(fullpath, outfile_name, compiler_args):
"""Calls HamlPy compiler."""
- print '%s %s -> %s' % ( strftime("%H:%M:%S", gmtime()), fullpath, outfile_name )
+ if Options.VERBOSE:
+ print '%s %s -> %s' % (strftime("%H:%M:%S"), fullpath, outfile_name)
try:
- if DEBUG:
+ if Options.DEBUG:
print "Compiling %s -> %s" % (fullpath, outfile_name)
- haml_lines = codecs.open(fullpath, 'r', encoding='utf-8').read().splitlines()
- compiler = hamlpy.Compiler()
+ haml_lines = codecs.open(fullpath, 'r', encoding = 'utf-8').read().splitlines()
+ compiler = hamlpy.Compiler(compiler_args)
output = compiler.process_lines(haml_lines)
- outfile = codecs.open(outfile_name, 'w', encoding='utf-8')
+ outfile = codecs.open(outfile_name, 'w', encoding = 'utf-8')
outfile.write(output)
except Exception, e:
# import traceback
diff --git a/hamlpy/nodes.py b/hamlpy/nodes.py
index 4941a77..534d9ba 100644
--- a/hamlpy/nodes.py
+++ b/hamlpy/nodes.py
@@ -4,11 +4,22 @@
from elements import Element
-from pygments import highlight
-from pygments.formatters import HtmlFormatter
-from pygments.lexers import guess_lexer
-
-from markdown import markdown
+try:
+ from pygments import highlight
+ from pygments.formatters import HtmlFormatter
+ from pygments.lexers import guess_lexer
+ _pygments_available = True
+except ImportError, e:
+ _pygments_available = False
+
+try:
+ from markdown import markdown
+ _markdown_available = True
+except ImportError, e:
+ _markdown_available = False
+
+class NotAvailableError(Exception):
+ pass
ELEMENT = '%'
ID = '#'
@@ -22,8 +33,8 @@
VARIABLE = '='
TAG = '-'
-INLINE_VARIABLE = re.compile(r'(?0 else None
+ siblings = self.parent.children
+ index = siblings.index(self)
+ return siblings[index - 1] if index > 0 else None
def right_sibling(self):
- siblings=self.parent.children
- index=siblings.index(self)
- return siblings[index+1] if index\n"
- self.before=''
+ self.before = ''
# Rendered text at end of node, e.g. "\n
"
- self.after=''
+ self.after = ''
# Indicates that a node does not render anything (for whitespace removal)
- self.empty_node=False
+ 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)
@@ -144,7 +163,7 @@ def render(self):
return self._generate_html()
def render_newlines(self):
- return '\n'*(self.newlines+1)
+ return '\n' * (self.newlines + 1)
def parent_of(self, node):
if (self._should_go_inside_last_node(node)):
@@ -153,22 +172,22 @@ def parent_of(self, node):
else:
return self
- def should_treat_children_as_multiline(self):
+ def inside_filter_node(self):
if self.parent:
- return self.parent.should_treat_children_as_multiline()
+ return self.parent.inside_filter_node()
else:
- return True
+ return False
def _render_children(self):
for child in self.children:
child._render()
-
+
def _post_render(self):
for child in self.children:
child._post_render()
def _generate_html(self):
- output=[]
+ output = []
output.append(self.before)
for child in self.children:
output.append(child.before)
@@ -176,17 +195,17 @@ def _generate_html(self):
output.append(child.after)
output.append(self.after)
return ''.join(output)
-
+
def add_node(self, node):
if (self._should_go_inside_last_node(node)):
self.children[-1].add_node(node)
else:
self.add_child(node)
-
+
def _should_go_inside_last_node(self, node):
- return len(self.children)>0 and (node.indentation > self.children[-1].indentation
+ 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)))
-
+
def should_contain(self, node):
return False
@@ -194,9 +213,9 @@ def debug_tree(self):
return '\n'.join(self._debug_tree([self]))
def _debug_tree(self, nodes):
- output=[]
+ output = []
for n in nodes:
- output.append('%s%s' % (' '*(n.indentation+2), n))
+ output.append('%s%s' % (' ' * (n.indentation + 2), n))
if n.children:
output += self._debug_tree(n.children)
return output
@@ -204,7 +223,7 @@ def _debug_tree(self, nodes):
def __repr__(self):
return '(%s)' % (self.__class__)
-class HamlNode(RootNode):
+class HamlNode(RootNode):
def __init__(self, haml):
RootNode.__init__(self)
self.haml = haml.strip()
@@ -223,7 +242,12 @@ def __repr__(self):
class PlaintextNode(HamlNode):
'''Node that is not modified or processed when rendering'''
def _render(self):
- self.before = '%s%s' % (self.spaces, self.replace_inline_variables(self.haml))
+ text = self.replace_inline_variables(self.haml)
+ # Remove escape character unless inside filter node
+ if text and text[0] == HAML_ESCAPE and not self.inside_filter_node():
+ text = text.replace(HAML_ESCAPE, '', 1)
+
+ self.before = '%s%s' % (self.spaces, text)
if self.children:
self.before += self.render_newlines()
else:
@@ -233,11 +257,11 @@ def _render(self):
class ElementNode(HamlNode):
'''Node which represents a HTML tag'''
def __init__(self, haml):
- HamlNode.__init__(self,haml)
+ HamlNode.__init__(self, haml)
self.django_variable = False
def _render(self):
- self.element = Element(self.haml)
+ self.element = Element(self.haml, self.attr_wrapper)
self.django_variable = self.element.django_variable
self.before = self._render_before(self.element)
self.after = self._render_after(self.element)
@@ -255,7 +279,7 @@ def _render_before(self, element):
content = content.strip()
if element.self_close and not content:
- start.append(" />" )
+ start.append(" />")
elif content:
start.append(">%s" % (content))
elif self.children:
@@ -282,18 +306,18 @@ def _post_render(self):
self.after = self.after.lstrip()
if self.children:
- node=self
+ node = self
# If node renders nothing, do removal on its first child instead
- if node.children[0].empty_node==True:
- node=node.children[0]
+ if node.children[0].empty_node == True:
+ node = node.children[0]
if node.children:
- node.children[0].before=node.children[0].before.lstrip()
+ node.children[0].before = node.children[0].before.lstrip()
- node=self
- if node.children[-1].empty_node==True:
- node=node.children[-1]
+ node = self
+ if node.children[-1].empty_node == True:
+ node = node.children[-1]
if node.children:
- node.children[-1].after=node.children[-1].after.rstrip()
+ node.children[-1].after = node.children[-1].after.rstrip()
# Outer whitespace removal
if self.element.nuke_outer_whitespace:
@@ -318,10 +342,10 @@ def _post_render(self):
self.parent.after = self.parent.after.lstrip()
self.parent.newlines = 0
- super(ElementNode, self)._post_render()
+ super(ElementNode, self)._post_render()
def _render_inline_content(self, inline_content):
- if inline_content == None or len(inline_content)==0:
+ if inline_content == None or len(inline_content) == 0:
return None
if self.django_variable:
@@ -329,37 +353,40 @@ def _render_inline_content(self, inline_content):
return content
else:
return self.replace_inline_variables(inline_content)
-
-class CommentNode(HamlNode):
+
+class CommentNode(HamlNode):
def _render(self):
- self.after = "-->\n"
+ self.after = "-->\n"
if self.children:
- self.before =""
+
+ self.after = "\n"
self._render_children()
class DoctypeNode(HamlNode):
def _render(self):
doctype = self.haml.lstrip(DOCTYPE).strip()
- parts=doctype.split()
+ parts = doctype.split()
if parts and parts[0] == "XML":
encoding = parts[1] if len(parts) > 1 else 'utf-8'
- self.before = "" % encoding
+ self.before = "" % (
+ self.attr_wrapper, self.attr_wrapper,
+ self.attr_wrapper, encoding, self.attr_wrapper,
+ )
else:
types = {
"": '',
@@ -385,7 +412,7 @@ class VariableNode(ElementNode):
def __init__(self, haml):
ElementNode.__init__(self, haml)
self.django_variable = True
-
+
def _render(self):
tag_content = self.haml.lstrip(VARIABLE)
self.before = "%s%s" % (self.spaces, self._render_inline_content(tag_content))
@@ -410,21 +437,21 @@ class TagNode(HamlNode):
'cache': 'endcache',
'localize': 'endlocalize',
'compress': 'endcompress'}
- may_contain = {'if':['else', 'elif'],
+ may_contain = {'if':['else', 'elif'],
'ifchanged':'else',
'ifequal':'else',
'ifnotequal':'else',
- 'for':'empty',
+ 'for':'empty',
'with':'with'}
-
+
def __init__(self, haml):
HamlNode.__init__(self, haml)
self.tag_statement = self.haml.lstrip(TAG).strip()
self.tag_name = self.tag_statement.split(' ')[0]
-
+
if (self.tag_name in self.self_closing.values()):
raise TypeError("Do not close your Django tags manually. It will be done for you.")
-
+
def _render(self):
self.before = "%s{%% %s %%}" % (self.spaces, self.tag_statement)
if (self.tag_name in self.self_closing.keys()):
@@ -436,19 +463,19 @@ def _render(self):
else:
self.after = self.render_newlines()
self._render_children()
-
+
def should_contain(self, node):
- return isinstance(node,TagNode) and node.tag_name in self.may_contain.get(self.tag_name,'')
+ 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)
- def should_treat_children_as_multiline(self):
- return False
+ def inside_filter_node(self):
+ return True
- def _render_children_as_plain_text(self,remove_indentation=True):
+ def _render_children_as_plain_text(self, remove_indentation = True):
if self.children:
initial_indentation = len(self.children[0].spaces)
for child in self.children:
@@ -463,13 +490,13 @@ def _render_children_as_plain_text(self,remove_indentation=True):
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)
- self.empty_node=True
-
+ self.empty_node = True
+
def _render(self):
self._render_children_as_plain_text(remove_indentation=True)
@@ -502,25 +529,37 @@ def _render(self):
class JavascriptFilterNode(FilterNode):
def _render(self):
- self.before = '\n'
- self._render_children_as_plain_text(remove_indentation=False)
+ self._render_children_as_plain_text(remove_indentation = False)
class CoffeeScriptFilterNode(FilterNode):
def _render(self):
- self.before = '\n'
- self._render_children_as_plain_text(remove_indentation=False)
+ self._render_children_as_plain_text(remove_indentation = False)
class CssFilterNode(FilterNode):
def _render(self):
- self.before = '\n'
- self._render_children_as_plain_text(remove_indentation=False)
+ self._render_children_as_plain_text(remove_indentation = False)
class StylusFilterNode(FilterNode):
def _render(self):
- self.before = '\n'
self._render_children_as_plain_text()
@@ -528,11 +567,14 @@ class CDataFilterNode(FilterNode):
def _render(self):
self.before = self.spaces + '\n'
- self._render_children_as_plain_text(remove_indentation=False)
+ self._render_children_as_plain_text(remove_indentation = False)
class PygmentsFilterNode(FilterNode):
def _render(self):
if self.children:
+ if not _pygments_available:
+ raise NotAvailableError("Pygments is not available")
+
self.before = self.render_newlines()
indent_offset = len(self.children[0].spaces)
text = ''.join(''.join([c.spaces[indent_offset:], c.haml, c.render_newlines()]) for c in self.children)
@@ -543,9 +585,16 @@ def _render(self):
class MarkdownFilterNode(FilterNode):
def _render(self):
if self.children:
+ if not _markdown_available:
+ raise NotAvailableError("Markdown is not available")
self.before = self.render_newlines()[1:]
indent_offset = len(self.children[0].spaces)
- text = ''.join(''.join([c.spaces[indent_offset:], c.haml, c.render_newlines()]) for c in self.children)
- self.before += markdown(text)
+ lines = []
+ for c in self.children:
+ haml = c.raw_haml.lstrip()
+ if haml[-1] == '\n':
+ haml = haml[:-1]
+ lines.append(c.spaces[indent_offset:] + haml + c.render_newlines())
+ self.before += markdown( ''.join(lines))
else:
self.after = self.render_newlines()
diff --git a/hamlpy/template/loaders.py b/hamlpy/template/loaders.py
index 968d24f..4d80c09 100644
--- a/hamlpy/template/loaders.py
+++ b/hamlpy/template/loaders.py
@@ -1,12 +1,28 @@
import os
-from django.template import TemplateDoesNotExist
-from django.template.loaders import filesystem, app_directories
+try:
+ from django.template import TemplateDoesNotExist
+ from django.template.loaders import filesystem, app_directories
+ _django_available = True
+except ImportError, e:
+ class TemplateDoesNotExist(Exception):
+ pass
+
+ _django_available = False
from hamlpy import hamlpy
from hamlpy.template.utils import get_django_template_loaders
+# Get options from Django settings
+options_dict = {}
+
+if _django_available:
+ from django.conf import settings
+ if hasattr(settings, 'HAMLPY_ATTR_WRAPPER'):
+ options_dict.update(attr_wrapper=settings.HAMLPY_ATTR_WRAPPER)
+
+
def get_haml_loader(loader):
if hasattr(loader, 'Loader'):
baseclass = loader.Loader
@@ -17,17 +33,19 @@ def load_template_source(self, *args, **kwargs):
class Loader(baseclass):
def load_template_source(self, template_name, *args, **kwargs):
- _name, _extension = os.path.splitext(template_name)
+ name, _extension = os.path.splitext(template_name)
+ # os.path.splitext always returns a period at the start of extension
+ extension = _extension.lstrip('.')
- for extension in hamlpy.VALID_EXTENSIONS:
+ if extension in hamlpy.VALID_EXTENSIONS:
try:
haml_source, template_path = super(Loader, self).load_template_source(
- self._generate_template_name(_name, extension), *args, **kwargs
+ self._generate_template_name(name, extension), *args, **kwargs
)
except TemplateDoesNotExist:
pass
else:
- hamlParser = hamlpy.Compiler()
+ hamlParser = hamlpy.Compiler(options_dict=options_dict)
html = hamlParser.process(haml_source)
return html, template_path
@@ -45,6 +63,6 @@ def _generate_template_name(self, name, extension="hamlpy"):
haml_loaders = dict((name, get_haml_loader(loader))
for (name, loader) in get_django_template_loaders())
-
-HamlPyFilesystemLoader = get_haml_loader(filesystem)
-HamlPyAppDirectoriesLoader = get_haml_loader(app_directories)
+if _django_available:
+ HamlPyFilesystemLoader = get_haml_loader(filesystem)
+ HamlPyAppDirectoriesLoader = get_haml_loader(app_directories)
diff --git a/hamlpy/template/utils.py b/hamlpy/template/utils.py
index 46e4cf9..c9187e8 100644
--- a/hamlpy/template/utils.py
+++ b/hamlpy/template/utils.py
@@ -2,11 +2,17 @@
from os import listdir
from os.path import dirname, splitext
-from django.template import loaders
+try:
+ from django.template import loaders
+ _django_available = True
+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)
for loader in get_submodules(loaders)
if hasattr(loader, 'Loader')]
diff --git a/hamlpy/templatize.py b/hamlpy/templatize.py
index 8d715b4..9581109 100644
--- a/hamlpy/templatize.py
+++ b/hamlpy/templatize.py
@@ -1,19 +1,31 @@
"""
This module decorates the django templatize function to parse haml templates
before the translation utility extracts tags from it.
+
+--Modified to ignore non-haml files.
"""
-from django.utils.translation import trans_real
+try:
+ from django.utils.translation import trans_real
+ _django_available = True
+except ImportError, e:
+ _django_available = False
+
import hamlpy
+import os
def decorate_templatize(func):
- def templatize(src, origin=None):
- hamlParser = hamlpy.Compiler()
- html = hamlParser.process(src)
- return func(html, origin)
-
- return templatize
-
-trans_real.templatize = decorate_templatize(trans_real.templatize)
+ def templatize(src, origin=None):
+ #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)
+ return templatize
+if _django_available:
+ trans_real.templatize = decorate_templatize(trans_real.templatize)
diff --git a/hamlpy/test/attribute_dict_parser_test.py b/hamlpy/test/attribute_dict_parser_test.py
index 95c80a7..4c2956d 100644
--- a/hamlpy/test/attribute_dict_parser_test.py
+++ b/hamlpy/test/attribute_dict_parser_test.py
@@ -7,79 +7,83 @@ class TestAttributeDictParser(object):
def test_empty_dictionary(self):
dict=AttributeDictParser("{}").parse()
eq_(len(dict), 0)
-
+
def test_one_string_value(self):
dict=AttributeDictParser("{'class': 'test'}").parse()
eq_(dict.get('class'), 'test')
-
+
def test_two_string_value(self):
dict=AttributeDictParser("{'class': 'test', 'id': 'something'}").parse()
eq_(dict.get('class'), 'test')
eq_(dict.get('id'), 'something')
-
+
def test_integer_value(self):
dict=AttributeDictParser("{'class': 'test', 'data-number': 123}").parse()
eq_(dict.get('class'), 'test')
eq_(dict.get('data-number'), '123')
-
+
def test_float_value(self):
dict=AttributeDictParser("{'class': 'test', 'data-number': 123.456}").parse()
eq_(dict.get('class'), 'test')
eq_(dict.get('data-number'), '123.456')
-
+
def test_none_value(self):
dict=AttributeDictParser("{'controls': None}").parse()
eq_(dict.get('controls'), None)
-
+
def test_colon_in_key(self):
dict=AttributeDictParser("{'xml:lang': 'en'}").parse()
eq_(dict.get('xml:lang'), 'en')
-
+
def test_colon_in_value(self):
dict=AttributeDictParser("{'xmllang': 'e:n'}").parse()
eq_(dict.get('xmllang'), 'e:n')
-
+
def test_double_quotes(self):
dict=AttributeDictParser('{"class": "test", "data-number": "123"}').parse()
eq_(dict.get('class'), 'test')
eq_(dict.get('data-number'), '123')
-
+
def test_key_quotes_are_optional(self):
dict=AttributeDictParser("{class: 'test', data-number: 123}").parse()
eq_(dict.get('class'), 'test')
eq_(dict.get('data-number'), '123')
-
+
def test_whitespace_between_key_and_value(self):
dict=AttributeDictParser("{ class: 'test', data-number: 123}").parse()
eq_(dict.get('class'), 'test')
eq_(dict.get('data-number'), '123')
-
+
def test_whitespace_before_colon(self):
dict=AttributeDictParser("{ class \t: 'test'}").parse()
eq_(dict.get('class'), 'test')
-
+
+ def test_trailing_comma(self):
+ dict=AttributeDictParser("{class: 'test', }").parse()
+ eq_(dict.get('class'), 'test')
+
def test_multiline_after_values(self):
dict=AttributeDictParser("""{class: 'test',
data-number: 123}""").parse()
eq_(dict.get('class'), 'test')
eq_(dict.get('data-number'), '123')
-
+
def test_ruby_haml_arrow(self):
dict=AttributeDictParser("{'class' => 'test'}").parse()
eq_(dict.get('class'), 'test')
-
+
def test_ruby_haml_colon(self):
dict=AttributeDictParser("{ :class => 'test'}").parse()
eq_(dict.get('class'), 'test')
-
+
def test_list_value(self):
dict=AttributeDictParser("{ class: ['a','b','c']}").parse()
eq_(dict.get('class'), ['a','b','c'])
-
+
def test_tuple_value(self):
dict=AttributeDictParser("{ class: ('a','b','c')}").parse()
eq_(dict.get('class'), ('a','b','c'))
-
+
def test_attribute_value_not_quoted_when_looks_like_key(self):
dict=AttributeDictParser('{name:"viewport", content:"width:device-width, initial-scale:1, minimum-scale:1, maximum-scale:1"}').parse()
eq_(dict.get('content'), 'width:device-width, initial-scale:1, minimum-scale:1, maximum-scale:1')
@@ -100,8 +104,8 @@ def test_multiline_haml_in_attributes(self):
dict=AttributeDictParser(s).parse()
eq_(dict.get('class'), '{% if forloop.first %} link-first {% else %} {% if forloop.last %} link-last {% endif %} {% endif %}')
eq_(dict.get('href'), "{% url 'some_view' %}")
-
+
# \r\n and \n
# Curly braces in multiline HAML
# Blank lines in Multiline HAML
- # Incorrectly indented multiline HAML
\ No newline at end of file
+ # Incorrectly indented multiline HAML
diff --git a/hamlpy/test/ext_test.py b/hamlpy/test/ext_test.py
new file mode 100644
index 0000000..a1c61b1
--- /dev/null
+++ b/hamlpy/test/ext_test.py
@@ -0,0 +1,47 @@
+import unittest
+import os
+from hamlpy.ext import has_any_extension
+
+class ExtTest(unittest.TestCase):
+ """
+ Tests for methods found in ../ext.py
+ """
+
+ def test_has_any_extension(self):
+ extensions = [
+ 'hamlpy',
+ 'haml',
+ '.txt'
+ ]
+ # no directory
+ self.assertTrue(has_any_extension('dir.hamlpy', extensions))
+ self.assertTrue(has_any_extension('dir.haml', extensions))
+ self.assertTrue(has_any_extension('dir.txt', extensions))
+ self.assertFalse(has_any_extension('dir.html', extensions))
+ # with dot in filename
+ self.assertTrue(has_any_extension('dir.dot.hamlpy', extensions))
+ self.assertTrue(has_any_extension('dir.dot.haml', extensions))
+ self.assertTrue(has_any_extension('dir.dot.txt', extensions))
+ self.assertFalse(has_any_extension('dir.dot.html', extensions))
+
+ # relative path
+ self.assertTrue(has_any_extension('../dir.hamlpy', extensions))
+ self.assertTrue(has_any_extension('../dir.haml', extensions))
+ self.assertTrue(has_any_extension('../dir.txt', extensions))
+ self.assertFalse(has_any_extension('../dir.html', extensions))
+ # with dot in filename
+ self.assertTrue(has_any_extension('../dir.dot.hamlpy', extensions))
+ self.assertTrue(has_any_extension('../dir.dot.haml', extensions))
+ self.assertTrue(has_any_extension('../dir.dot.txt', extensions))
+ self.assertFalse(has_any_extension('../dir.dot.html', extensions))
+
+ # absolute paths
+ self.assertTrue(has_any_extension('/home/user/dir.hamlpy', extensions))
+ self.assertTrue(has_any_extension('/home/user/dir.haml', extensions))
+ self.assertTrue(has_any_extension('/home/user/dir.txt', extensions))
+ self.assertFalse(has_any_extension('/home/user/dir.html', extensions))
+ # with dot in filename
+ self.assertTrue(has_any_extension('/home/user/dir.dot.hamlpy', extensions))
+ self.assertTrue(has_any_extension('/home/user/dir.dot.haml', extensions))
+ self.assertTrue(has_any_extension('/home/user/dir.dot.txt', extensions))
+ self.assertFalse(has_any_extension('/home/user/dir.dot.html', extensions))
\ No newline at end of file
diff --git a/hamlpy/test/hamlpy_test.py b/hamlpy/test/hamlpy_test.py
index 9152635..5601c96 100755
--- a/hamlpy/test/hamlpy_test.py
+++ b/hamlpy/test/hamlpy_test.py
@@ -4,7 +4,7 @@
from hamlpy import hamlpy
class HamlPyTest(unittest.TestCase):
-
+
def test_applies_id_properly(self):
haml = '%div#someId Some text'
html = "Some text
"
@@ -12,13 +12,20 @@ def test_applies_id_properly(self):
result = hamlParser.process(haml)
self.assertEqual(html, result.replace('\n', ''))
+ def test_non_ascii_id_allowed(self):
+ haml = u'%div#これはテストです test'
+ html = u"test
"
+ hamlParser = hamlpy.Compiler()
+ result = hamlParser.process(haml)
+ self.assertEqual(html, result.replace('\n', ''))
+
def test_applies_class_properly(self):
haml = '%div.someClass Some text'
html = "Some text
"
hamlParser = hamlpy.Compiler()
result = hamlParser.process(haml)
self.assertEqual(html, result.replace('\n', ''))
-
+
def test_applies_multiple_classes_properly(self):
haml = '%div.someClass.anotherClass Some text'
html = "Some text
"
@@ -35,22 +42,22 @@ def test_dictionaries_define_attributes(self):
self.assertTrue("xml:lang='en'" in result)
self.assertTrue("lang='en'" in result)
self.assertTrue(result.endswith(">