def test_shortcut_id(self): line = '#myid' node = Parser.parse_line(line) self.assertIsInstance(node, nodes.HtmlNode) self.assertFalse(node.has_children()) self.assertEqual('div', node.tag) self.assertEqual({'id': 'myid'}, node.attributes)
def test_jinja_line(self): line = '-extends "base.haml"' node = Parser.parse_line(line) self.assertIsInstance(node, nodes.JinjaNode) self.assertEqual('extends', node.tag) self.assertEqual('"base.haml"', node.data) self.assertFalse(node.has_children())
def test_single_html_tag(self): line = '%div' node = Parser.parse_line(line) self.assertFalse(node.has_children()) self.assertIsInstance(node, nodes.HtmlNode) self.assertEqual('div', node.tag) self.assertEqual({}, node.attributes)
def test_shortcut_class(self): line = '.cls' node = Parser.parse_line(line) self.assertIsInstance(node, nodes.HtmlNode) self.assertFalse(node.has_children()) self.assertEqual('div', node.tag) self.assertEqual({'class': 'cls'}, node.attributes)
def from_haml(cls, haml): """Given a line of HAML markup, return the correct HtmlNode.""" # You can omit the % if and only if the line starts with a '.' or '#'. if haml and not haml.startswith('%') and haml[0] in ('.', '#'): haml = '%div' + haml match = cls.TAG_REGEX.match(haml) if not match: raise ValueError('Text did not match %s' % cls.TAG_REGEX.pattern) # Create the node with the proper tag. tag = match.group('tag') if tag in cls.SELF_CLOSING_TAGS: node = SelfClosingHtmlNode(tag=tag) else: node = cls(tag=tag) # Update the condensed property on the node. node.condensed = (match.group('condensed') == '-') # Handle shortcut attributes ('.cls#id') shortcut_attrs = match.group('shortcut_attrs') if shortcut_attrs: # Splits into ['.', 'cls', '#', 'id'] parts = re.split(r'([\.#])', shortcut_attrs)[1:] # Zips together into [('.', 'cls'), ('#', 'id')] parts = zip(parts[0::2], parts[1::2]) for prefix, value in parts: if prefix == '.': node.add_attribute('class', value) elif prefix == '#': node.add_attribute('id', value) # Handle regular attributes ('(a="1", b="2")') attrs = match.group('attrs') if attrs: # Splits apart by commas, but not commas within quotes. attr_pairs = re.compile(r'(?:[^,"]|"[^"]*")+').findall(attrs[1:-1]) if any(_.count('"') != 2 for _ in attr_pairs): raise ValueError('Mismatched quotes (or missing comma) in attributes!') # Breaks pair strings into [(key, value), ...] but only split apart by # the first equal sign. attr_pairs = [pair.strip().split('=', 1) for pair in attr_pairs] for (key, value) in attr_pairs: if not value.startswith('"') or not value.endswith('"'): raise ValueError( 'Invalid attribute provided: "%s" for key "%s"' % (value, key)) node.add_attribute(key, value[1:-1]) # Handle in-line content. nested = (match.group('nested') or False) content = (match.group('content') or '').strip() # If we have nested tags, there should definitely be content. if nested and not content: raise ValueError('Illegal nesting of tags.') # If we are nesting tags, parse the content as a separate HAML line and # append the parsed child to the current node. if nested: from pyhaml_jinja.parser import Parser child = Parser.parse_line(content.strip()) node.add_child(child) # If we aren't nesting and just have content, append it to the node if # possible. elif content: if not node.children_allowed(): raise ValueError('Inline content ("%s") not permitted on node %s' % ( content, node)) node.add_child(TextNode(content)) return node
def test_empty_line(self): line = '' node = Parser.parse_line(line) self.assertIsInstance(node, nodes.EmptyNode)
def test_text(self): line = 'this is some text' node = Parser.parse_line(line) self.assertIsInstance(node, nodes.TextNode) self.assertEqual('this is some text', node.data)
def from_haml(cls, haml): """Given a line of HAML markup parse it into a Jinja node.""" match = cls.TAG_REGEX.match(haml) if not match: raise ValueError('Text did not match %s' % cls.TAG_REGEX.pattern) # Create the node with the proper tag. tag = match.group('tag') if tag in cls.SELF_CLOSING_TAGS: node = SelfClosingJinjaNode(tag=tag) else: node = cls(tag=tag) data = (match.group('data') or '').strip() # To handle nested expressions, we need to be sure that colons are only # split apart when they aren't inside brackets, braces, or quotes (both # single and double). # To do this, we'll just go through "data" and keep a stack of where we # are. The first colon we find that is outside the stack is a splitter. stack = [] chars = { '[': ']', '(': ')', '{': '}', '\'': '\'', '"': '"', } for index, char in enumerate(data): # If we found a colon and the stack is empty, treat it as the splitter # for a nested tag. Split, re-parse the rest, attach as a child, and # update data to what it should be. Then break out of the loop. if not stack and char == ':': from pyhaml_jinja.parser import Parser child = Parser.parse_line(data[index + 1:].strip()) node.add_child(child) data = data[:index] break # If we might be able to close an open tag. if char in chars.values(): # The stack is not empty and this tag closes it. if stack and char == stack[-1]: stack.pop() continue # The stack is not empty, but this tag didn't close it, AND it's not a # valid opener. Throw an error. elif stack and char not in chars.keys(): raise ValueError('Found unexpected closing tag "%s".' % char) # If we're opening, add the proper closing tag to the stack. if char in chars.keys(): stack.append(chars.get(char)) # TODO: Figure out what we do with backslash escaping characters. node.data = data return node
def from_haml(cls, haml): """Given a line of HAML markup, return the correct HtmlNode.""" # You can omit the % if and only if the line starts with a '.' or '#'. if haml and not haml.startswith('%') and haml[0] in ('.', '#'): haml = '%div' + haml match = cls.TAG_REGEX.match(haml) if not match: raise ValueError('Text did not match %s' % cls.TAG_REGEX.pattern) # Create the node with the proper tag. tag = match.group('tag') if tag in cls.SELF_CLOSING_TAGS: node = SelfClosingHtmlNode(tag=tag) else: node = cls(tag=tag) # Update the condensed property on the node. node.condensed = (match.group('condensed') == '-') # Handle shortcut attributes ('.cls#id') shortcut_attrs = match.group('shortcut_attrs') if shortcut_attrs: # Splits into ['.', 'cls', '#', 'id'] parts = re.split(r'([\.#])', shortcut_attrs)[1:] # Zips together into [('.', 'cls'), ('#', 'id')] parts = zip(parts[0::2], parts[1::2]) for prefix, value in parts: if prefix == '.': node.add_attribute('class', value) elif prefix == '#': node.add_attribute('id', value) # Handle regular attributes ('(a="1", b="2")') attrs = match.group('attrs') if attrs: # Splits apart by commas, but not commas within quotes. attr_pairs = re.compile(r'(?:[^,"]|"[^"]*")+').findall(attrs[1:-1]) if any(_.count('"') != 2 for _ in attr_pairs): raise ValueError( 'Mismatched quotes (or missing comma) in attributes!') # Breaks pair strings into [(key, value), ...] but only split apart by # the first equal sign. attr_pairs = [pair.strip().split('=', 1) for pair in attr_pairs] for (key, value) in attr_pairs: if not value.startswith('"') or not value.endswith('"'): raise ValueError( 'Invalid attribute provided: "%s" for key "%s"' % (value, key)) node.add_attribute(key, value[1:-1]) # Handle in-line content. nested = (match.group('nested') or False) content = (match.group('content') or '').strip() # If we have nested tags, there should definitely be content. if nested and not content: raise ValueError('Illegal nesting of tags.') # If we are nesting tags, parse the content as a separate HAML line and # append the parsed child to the current node. if nested: from pyhaml_jinja.parser import Parser child = Parser.parse_line(content.strip()) node.add_child(child) # If we aren't nesting and just have content, append it to the node if # possible. elif content: if not node.children_allowed(): raise ValueError( 'Inline content ("%s") not permitted on node %s' % (content, node)) node.add_child(TextNode(content)) return node
def from_haml(cls, haml): """Given a line of HAML markup parse it into a Jinja node.""" match = cls.TAG_REGEX.match(haml) if not match: raise ValueError('Text did not match %s' % cls.TAG_REGEX.pattern) # Create the node with the proper tag. tag = match.group('tag') if tag in cls.SELF_CLOSING_TAGS: node = SelfClosingJinjaNode(tag=tag) else: node = cls(tag=tag) data = (match.group('data') or '').strip() # To handle nested expressions, we need to be sure that colons are only # split apart when they aren't inside brackets, braces, or quotes (both # single and double). # To do this, we'll just go through "data" and keep a stack of where we # are. The first colon we find that is outside the stack is a splitter. stack = [] chars = { '[': ']', '(': ')', '{': '}', '\'': '\'', '"': '"', } for index, char in enumerate(data): # If we found a colon and the stack is empty, treat it as the splitter # for a nested tag. Split, re-parse the rest, attach as a child, and # update data to what it should be. Then break out of the loop. if not stack and char == ':': from pyhaml_jinja.parser import Parser child = Parser.parse_line(data[index+1:].strip()) node.add_child(child) data = data[:index] break # If we might be able to close an open tag. if char in chars.values(): # The stack is not empty and this tag closes it. if stack and char == stack[-1]: stack.pop() continue # The stack is not empty, but this tag didn't close it, AND it's not a # valid opener. Throw an error. elif stack and char not in chars.keys(): raise ValueError('Found unexpected closing tag "%s".' % char) # If we're opening, add the proper closing tag to the stack. if char in chars.keys(): stack.append(chars.get(char)) # TODO: Figure out what we do with backslash escaping characters. node.data = data return node