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(">") or result.endswith(">\n")) - + def test_dictionaries_support_arrays_for_id(self): haml = "%div{'id':('itemType', '5')}" html = "
" hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) self.assertEqual(html, result.replace('\n', '')) - + def test_dictionaries_can_by_pythonic(self): haml = "%div{'id':['Article','1'], 'class':['article','entry','visible']} Booyaka" html = "
Booyaka
" hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) self.assertEqual(html, result.replace('\n', '')) - - + + def test_html_comments_rendered_properly(self): haml = '/ some comment' html = "" @@ -60,17 +67,17 @@ def test_html_comments_rendered_properly(self): def test_conditional_comments_rendered_properly(self): haml = "/[if IE]\n %h1 You use a shitty browser" - html = "" + html = "\n" hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result) - + def test_single_line_conditional_comments_rendered_properly(self): haml = "/[if IE] You use a shitty browser" - html = "" + html = "\n" hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) - eq_(html, result) + eq_(html, result) def test_django_variables_on_tag_render_properly(self): haml = '%div= story.tease' @@ -78,48 +85,48 @@ def test_django_variables_on_tag_render_properly(self): hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result.replace('\n', '')) - + def test_stand_alone_django_variables_render(self): haml = '= story.tease' html = '{{ story.tease }}' hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result.replace('\n', '')) - + def test_stand_alone_django_tags_render(self): haml = '- extends "something.html"' html = '{% extends "something.html" %}' hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result.replace('\n', '')) - + def test_if_else_django_tags_render(self): haml = '- if something\n %p hello\n- else\n %p goodbye' html = '{% if something %}\n

hello

\n{% else %}\n

goodbye

\n{% endif %}\n' hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result) - - @raises(TypeError) + + @raises(TypeError) def test_throws_exception_when_trying_to_close_django(self): haml = '- endfor' hamlParser = hamlpy.Compiler() - hamlParser.process(haml) - + result = hamlParser.process(haml) + def test_handles_dash_in_class_name_properly(self): haml = '.header.span-24.last' html = "
\n" hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result) - + def test_handles_multiple_attributes_in_dict(self): haml = "%div{'id': ('article', '3'), 'class': ('newest', 'urgent')} Content" html = "
Content
\n" hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result) - + def test_inline_variables_are_parsed_correctly(self): haml = "={greeting} #{name}, how are you ={date}?" html = "{{ greeting }} {{ name }}, how are you {{ date }}?\n" @@ -127,6 +134,13 @@ def test_inline_variables_are_parsed_correctly(self): result = hamlParser.process(haml) eq_(html, result) + def test_inline_variables_can_use_filter_characters(self): + haml = "={value|center:\"15\"}" + html = "{{ value|center:\"15\" }}\n" + hamlParser = hamlpy.Compiler() + result = hamlParser.process(haml) + eq_(html, result) + def test_inline_variables_in_attributes_are_parsed_correctly(self): haml = "%a{'b': '={greeting} test'} blah" html = "blah\n" @@ -149,8 +163,8 @@ def test_inline_variables_in_attributes_work_in_class(self): eq_(html, result) def test_inline_variables_in_attributes_are_escaped_correctly(self): - haml = "%a{'b': '\\\\={greeting} test'} blah" - html = "blah\n" + haml = "%a{'b': '\\\\={greeting} test', title: \"It can't be removed\"} blah" + html = "blah\n" hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result) @@ -161,84 +175,84 @@ def test_inline_variables_escaping_works(self): hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result) - + def test_inline_variables_escaping_works_at_start_of_line(self): haml = "\\={name}, how are you?" html = "={name}, how are you?\n" hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result) - + def test_inline_variables_with_hash_escaping_works_at_start_of_line(self): haml = "\\#{name}, how are you?" html = "#{name}, how are you?\n" hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result) - + def test_inline_variables_work_at_start_of_line(self): haml = "={name}, how are you?" html = "{{ name }}, how are you?\n" hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result) - + def test_inline_variables_with_hash_work_at_start_of_line(self): haml = "#{name}, how are you?" html = "{{ name }}, how are you?\n" hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result) - + def test_inline_variables_with_special_characters_are_parsed_correctly(self): haml = "%h1 Hello, #{person.name}, how are you?" html = "

Hello, {{ person.name }}, how are you?

\n" hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result) - + def test_plain_text(self): haml = "This should be plain text\n This should be indented" html = "This should be plain text\n This should be indented\n" hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) - eq_(html, result) - + eq_(html, result) + def test_plain_text_with_indenting(self): haml = "This should be plain text" html = "This should be plain text\n" hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result) - + def test_escaped_haml(self): haml = "\\= Escaped" html = "= Escaped\n" hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result) - + def test_utf8_with_regular_text(self): haml = u"%a{'href':'', 'title':'링크(Korean)'} Some Link" html = u"Some Link\n" hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result) - + def test_python_filter(self): haml = ":python\n for i in range(0, 5): print \"

item \%s

\" % i" html = '

item \\0

\n

item \\1

\n

item \\2

\n

item \\3

\n

item \\4

\n' hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result) - + def test_doctype_html5(self): haml = '!!! 5' html = '' hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result.replace('\n', '')) - + def test_doctype_xhtml(self): haml = '!!!' html = '' @@ -265,7 +279,7 @@ def test_plain_filter_with_indentation(self): html = "-This should be plain text\n.This should be more\n This should be indented\n" hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) - eq_(html, result) + eq_(html, result) def test_plain_filter_with_no_children(self): haml = ":plain\nNothing" @@ -274,33 +288,42 @@ def test_plain_filter_with_no_children(self): result = hamlParser.process(haml) eq_(html, result) - def test_xml_namespaces(self): - haml = "%fb:tag\n content" - html = "\n content\n\n" + def test_filters_render_escaped_backslash(self): + haml = ":plain\n \\Something" + html = "\\Something\n" hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result) - - def test_pygments_filter(self): - haml = ''' - :highlight - print "hi" - - if x: - print "y": - else: - print "z": - ''' - html = '\n
print "hi"' \
-                + '\n\nif x:' \
-                + '\n    print "y":' \
-                + '\nelse:' \
-                + '\n    print "z":' \
-                + '\n
\n' - + + def test_xml_namespaces(self): + haml = "%fb:tag\n content" + html = "\n content\n\n" hamlParser = hamlpy.Compiler() result = hamlParser.process(haml) eq_(html, result) - + + def test_attr_wrapper(self): + haml = """ +%html{'xmlns':'http://www.w3.org/1999/xhtml', 'xml:lang':'en', 'lang':'en'} + %body#main + %div.wrap + %a{:href => '/'} +:javascript""" + hamlParser = hamlpy.Compiler(options_dict={'attr_wrapper': '"'}) + result = hamlParser.process(haml) + self.assertEqual(result, + ''' + +
+ +
+ + + +''') + if __name__ == '__main__': unittest.main() diff --git a/hamlpy/test/loader_test.py b/hamlpy/test/loader_test.py new file mode 100644 index 0000000..27643aa --- /dev/null +++ b/hamlpy/test/loader_test.py @@ -0,0 +1,74 @@ +import unittest +import sys + +try: + from django.conf import settings + + settings.configure(DEBUG=True, TEMPLATE_DEBUG=True) +except ImportError, e: + pass + +from hamlpy.template.loaders import get_haml_loader, TemplateDoesNotExist + +class DummyLoader(object): + """ + A dummy template loader that only loads templates from self.templates + """ + templates = { + "in_dict.txt" : "in_dict content", + "loader_test.hamlpy" : "loader_test content", + } + def __init__(self, *args, **kwargs): + self.Loader = self.__class__ + + def load_template_source(self, template_name, *args, **kwargs): + try: + return (self.templates[template_name], "test:%s" % template_name) + except KeyError: + raise TemplateDoesNotExist(template_name) + +class LoaderTest(unittest.TestCase): + """ + Tests for the django template loader. + + A dummy template loader is used that loads only from a dictionary of templates. + """ + + def setUp(self): + dummy_loader = DummyLoader() + + hamlpy_loader_class = get_haml_loader(dummy_loader) + self.hamlpy_loader = hamlpy_loader_class() + + def _test_assert_exception(self, template_name): + try: + self.hamlpy_loader.load_template_source(template_name) + except TemplateDoesNotExist: + self.assertTrue(True) + else: + self.assertTrue(False, '\'%s\' should not be loaded by the hamlpy tempalte loader.' % template_name) + + def test_file_not_in_dict(self): + # not_in_dict.txt doesn't exit, so we're expecting an exception + self._test_assert_exception('not_in_dict.hamlpy') + + def test_file_in_dict(self): + # in_dict.txt in in dict, but with an extension not supported by + # the loader, so we expect an exception + self._test_assert_exception('in_dict.txt') + + def test_file_should_load(self): + # loader_test.hamlpy is in the dict, so it should load fine + try: + self.hamlpy_loader.load_template_source('loader_test.hamlpy') + except TemplateDoesNotExist: + self.assertTrue(False, '\'loader_test.hamlpy\' should be loaded by the hamlpy tempalte loader, but it was not.') + else: + self.assertTrue(True) + + def test_file_different_extension(self): + # loader_test.hamlpy is in dict, but we're going to try + # to load loader_test.txt + # we expect an exception since the extension is not supported by + # the loader + self._test_assert_exception('loader_test.txt') diff --git a/hamlpy/test/regression.py b/hamlpy/test/regression.py new file mode 100644 index 0000000..ba21e6f --- /dev/null +++ b/hamlpy/test/regression.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +import unittest +from nose.tools import eq_, raises +from hamlpy import hamlpy + +class RegressionTest(unittest.TestCase): + # Regression test for Github Issue 92 + def test_haml_comment_nodes_dont_post_render_children(self): + haml = ''' + -# My comment + #my_div + my text + test + ''' + html = "test" + hamlParser = hamlpy.Compiler() + result = hamlParser.process(haml) + eq_(html, result.strip()) + + def test_whitespace_after_attribute_key(self): + haml = '%form{id : "myform"}' + html = "
" + hamlParser = hamlpy.Compiler() + result = hamlParser.process(haml) + eq_(html, result.strip()) + + def test_for_newline_after_conditional_comment(self): + haml = '/[if lte IE 7]\n\ttest\n#test' + html = "\n
" + hamlParser = hamlpy.Compiler() + result = hamlParser.process(haml) + eq_(html, result.strip()) diff --git a/hamlpy/test/template_compare_test.py b/hamlpy/test/template_compare_test.py index 23ad78d..3d20485 100644 --- a/hamlpy/test/template_compare_test.py +++ b/hamlpy/test/template_compare_test.py @@ -6,10 +6,10 @@ class TestTemplateCompare(unittest.TestCase): def test_nuke_inner_whitespace(self): - self._compare_test_files('nukeInnerWhitespace') + self._compare_test_files('nukeInnerWhiteSpace') def test_nuke_outer_whitespace(self): - self._compare_test_files('nukeOuterWhitespace') + self._compare_test_files('nukeOuterWhiteSpace') def test_comparing_simple_templates(self): self._compare_test_files('simple') @@ -41,6 +41,23 @@ def test_nested_django_tags(self): def test_filters(self): self._compare_test_files('filters') + def test_filters_markdown(self): + try: + import markdown + self._compare_test_files('filtersMarkdown') + except ImportError: + pass + + def test_filters_pygments(self): + try: + import pygments + if pygments.__version__ == '1.6': + self._compare_test_files('filtersPygments16') + else: + self._compare_test_files('filtersPygments') + except ImportError: + pass + def test_nested_if_else_blocks(self): self._compare_test_files('nestedIfElseBlocks') @@ -57,52 +74,52 @@ def test_whitespace_preservation(self): self._compare_test_files('whitespacePreservation') def _print_diff(self, s1, s2): - if len(s1)>len(s2): - shorter=s2 + if len(s1) > len(s2): + shorter = s2 else: - shorter=s1 + shorter = s1 - line=1 - col=1 + line = 1 + col = 1 for i, _ in enumerate(shorter): - if len(shorter) <= i+1: + if len(shorter) <= i + 1: print 'Ran out of characters to compare!' - print 'Actual len=%d'%len(s1) - print 'Expected len=%d'%len(s2) + print 'Actual len=%d' % len(s1) + print 'Expected len=%d' % len(s2) break if s1[i] != s2[i]: print 'Difference begins at line', line, 'column', col - actual_line = s1.splitlines()[line-1] - expected_line = s2.splitlines()[line-1] + actual_line = s1.splitlines()[line - 1] + expected_line = s2.splitlines()[line - 1] print 'HTML (actual, len=%2d) : %s' % (len(actual_line), actual_line) print 'HTML (expected, len=%2d) : %s' % (len(expected_line), expected_line) print 'Character code (actual) : %d (%s)' % (ord(s1[i]), s1[i]) print 'Character code (expected): %d (%s)' % (ord(s2[i]), s2[i]) break - if shorter[i]=='\n': + if shorter[i] == '\n': line += 1 - col=1 + col = 1 else: - col+=1 + col += 1 else: print "No Difference Found" def _compare_test_files(self, name): - haml_lines = codecs.open('templates/'+name+'.hamlpy', encoding='utf-8').readlines() - html = open('templates/'+name+'.html').read() + 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','') + 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')) ]) + print '\n'.join(["%d. %s" % (i + 1, l) for i, l in enumerate(parsed.split('\n')) ]) self._print_diff(parsed, html) eq_(parsed, html) diff --git a/hamlpy/test/templates/classIdMixtures.hamlpy b/hamlpy/test/templates/classIdMixtures.hamlpy index 52ba3ca..06be022 100644 --- a/hamlpy/test/templates/classIdMixtures.hamlpy +++ b/hamlpy/test/templates/classIdMixtures.hamlpy @@ -1,2 +1,2 @@ %div#Article.article.entry{'id':'123', 'class':'true'} - Now this is interesting \ No newline at end of file + Now this is interesting diff --git a/hamlpy/test/templates/filters.hamlpy b/hamlpy/test/templates/filters.hamlpy index 76b838c..72d7669 100644 --- a/hamlpy/test/templates/filters.hamlpy +++ b/hamlpy/test/templates/filters.hamlpy @@ -16,10 +16,3 @@ a=1 for i in range(5): print a+i -:markdown - hello - no paragraph - - New paragraph - - test \ No newline at end of file diff --git a/hamlpy/test/templates/filters.html b/hamlpy/test/templates/filters.html index a1e35c9..fab181f 100644 --- a/hamlpy/test/templates/filters.html +++ b/hamlpy/test/templates/filters.html @@ -24,8 +24,3 @@ 3 4 5 -

hello -no paragraph

-

New paragraph

-
test
-
\ No newline at end of file diff --git a/hamlpy/test/templates/filtersMarkdown.hamlpy b/hamlpy/test/templates/filtersMarkdown.hamlpy new file mode 100644 index 0000000..becf36a --- /dev/null +++ b/hamlpy/test/templates/filtersMarkdown.hamlpy @@ -0,0 +1,10 @@ +:markdown + hello + no paragraph + + line break + follow + + New paragraph + + code block diff --git a/hamlpy/test/templates/filtersMarkdown.html b/hamlpy/test/templates/filtersMarkdown.html new file mode 100644 index 0000000..1b1a9a8 --- /dev/null +++ b/hamlpy/test/templates/filtersMarkdown.html @@ -0,0 +1,7 @@ +

hello +no paragraph

+

line break
+follow

+

New paragraph

+
code block
+
\ No newline at end of file diff --git a/hamlpy/test/templates/filtersPygments.hamlpy b/hamlpy/test/templates/filtersPygments.hamlpy new file mode 100644 index 0000000..4dc3852 --- /dev/null +++ b/hamlpy/test/templates/filtersPygments.hamlpy @@ -0,0 +1,7 @@ +:highlight + print "hi" + + if x: + print "y": + else: + print "z": diff --git a/hamlpy/test/templates/filtersPygments.html b/hamlpy/test/templates/filtersPygments.html new file mode 100644 index 0000000..2b87052 --- /dev/null +++ b/hamlpy/test/templates/filtersPygments.html @@ -0,0 +1,8 @@ + +
print "hi"
+
+if x:
+    print "y":
+else:
+    print "z":
+
diff --git a/hamlpy/test/templates/filtersPygments16.hamlpy b/hamlpy/test/templates/filtersPygments16.hamlpy new file mode 100644 index 0000000..4dc3852 --- /dev/null +++ b/hamlpy/test/templates/filtersPygments16.hamlpy @@ -0,0 +1,7 @@ +:highlight + print "hi" + + if x: + print "y": + else: + print "z": diff --git a/hamlpy/test/templates/filtersPygments16.html b/hamlpy/test/templates/filtersPygments16.html new file mode 100644 index 0000000..380bf83 --- /dev/null +++ b/hamlpy/test/templates/filtersPygments16.html @@ -0,0 +1,8 @@ + +
print "hi"
+
+if x:
+    print "y":
+else:
+    print "z":
+
diff --git a/hamlpy/test/templates/nukeOuterWhiteSpace.html b/hamlpy/test/templates/nukeOuterWhiteSpace.html index c95a95a..171d1b3 100644 --- a/hamlpy/test/templates/nukeOuterWhiteSpace.html +++ b/hamlpy/test/templates/nukeOuterWhiteSpace.html @@ -13,7 +13,8 @@

+ +