verto/processors/ConditionalProcessor.py
from markdown.blockprocessors import BlockProcessor
from verto.errors.TagNotMatchedError import TagNotMatchedError
from verto.processors.utils import etree, parse_arguments, blocks_to_string
from verto.utils.HtmlParser import HtmlParser
from verto.utils.HtmlSerializer import HtmlSerializer
from collections import OrderedDict
import re
class ConditionalProcessor(BlockProcessor):
''' Searches a Document for conditional tags e.g.
{conditonal flag condition="<condition>"} The processor matches
the following `elif` and `else` statements in the document and
parses them via the provided html template.
'''
def __init__(self, ext, *args, **kwargs):
'''
Args:
ext: An instance of the VertoExtension.
'''
super().__init__(*args, **kwargs)
self.processor = 'conditional'
self.pattern = re.compile(ext.processor_info[self.processor]['pattern'])
self.arguments = ext.processor_info[self.processor]['arguments']
template_name = ext.processor_info.get('template_name', self.processor)
self.template = ext.jinja_templates[template_name]
self.template_parameters = ext.processor_info[self.processor].get('template_parameters', None)
def test(self, parent, block):
''' Tests if the block if it contains any type of conditional
types.
Args:
parent: The parent element of the html tree.
blocks: The markdown blocks to until a new tag is found.
Returns:
Return true if any conditional tag is found.
'''
return self.pattern.search(block) is not None
def run(self, parent, blocks):
''' Replaces all conditionals with the given html template.
Allows for recursively defined if statements.
Args:
lines: A list of lines of the Markdown document to be
converted.
Returns:
Markdown document with comments removed.
Raises:
TagNotMatchedError: When a condition tags does not have
a matching start tag, or a start tag does not have
a matching end tag.
'''
block = blocks.pop(0)
context = dict()
start_tag = self.pattern.search(block)
is_if = tag_starts_with('if', start_tag.group('args'))
# elif or else before an if conditional
if not is_if:
string = ''
if tag_starts_with('elif', start_tag.group('args')):
string = 'elif'
elif tag_starts_with('else', start_tag.group('args')):
string = 'else'
elif tag_starts_with('end', start_tag.group('args')):
'end'
else:
string = 'unrecognised'
msg = '{} conditional found before if'.format(string)
raise TagNotMatchedError(self.processor, block, msg)
# Put left overs back on blocks, should be empty though
if block[:start_tag.start()].strip() != '':
self.parser.parseChunk(parent, block[:start_tag.start()])
if block[start_tag.end():].strip() != '':
blocks.insert(0, block[start_tag.end():])
# Process if statement
argument_values = parse_arguments(self.processor, start_tag.group('args'), self.arguments)
if_expression = argument_values['condition']
next_tag, block, content_blocks = self.get_content(blocks)
if_content = self.parse_blocks(content_blocks)
context['if_expression'] = if_expression
context['if_content'] = if_content
# Process elif statements
elifs = OrderedDict()
while next_tag is not None and tag_starts_with('elif', next_tag.group('args')):
argument_values = parse_arguments(self.processor, next_tag.group('args'), self.arguments)
elif_expression = argument_values['condition']
next_tag, block, content_blocks = self.get_content(blocks)
content = self.parse_blocks(content_blocks)
elifs[elif_expression] = content
context['elifs'] = elifs
# Process else statement
has_else = next_tag is not None and tag_starts_with('else', next_tag.group('args'))
else_content = ''
if has_else:
argument_values = parse_arguments(self.processor, next_tag.group('args'), self.arguments)
next_tag, block, content_blocks = self.get_content(blocks)
else_content = self.parse_blocks(content_blocks)
context['has_else'] = has_else
context['else_content'] = else_content
if (next_tag is None or (next_tag is not None and not tag_starts_with('end', next_tag.group('args')))):
msg = 'end conditional not found'
raise TagNotMatchedError(self.processor, block, msg)
# Render template and compile into an element
html_string = self.template.render(context)
parser = HtmlParser()
parser.feed(html_string).close()
parent.append(parser.get_root())
def get_content(self, blocks):
''' Recursively parses blocks into an element tree, returning
a string of the output.
Args:
blocks: The markdown blocks to until a new tag is found.
Returns:
The next tag (regex match) the current block (string) and
the content of the blocks (list of strings).
Raises:
TagNotMatchedError: When a sibling conditional is not
closed.
'''
next_tag = None
content_blocks = []
the_rest = None
inner_if_tags = 0
inner_end_tags = 0
is_elif, is_else = False, False
while len(blocks) > 0:
block = blocks.pop(0)
# Do we have either a start or end tag
next_tag = self.pattern.search(block)
is_if = next_tag is not None and tag_starts_with('if', next_tag.group('args'))
is_elif = next_tag is not None and tag_starts_with('elif', next_tag.group('args'))
is_else = next_tag is not None and tag_starts_with('else', next_tag.group('args'))
is_end = next_tag is not None and tag_starts_with('end', next_tag.group('args'))
# Keep track of how many inner boxed-text start tags we have seen
if is_if:
inner_if_tags += 1
if inner_if_tags != inner_end_tags:
if is_end:
inner_end_tags += 1
elif is_elif or is_else or is_end:
if block[:next_tag.start()].strip() != '':
content_blocks.append(block[:next_tag.start()])
the_rest = block[next_tag.end():]
break
content_blocks.append(block)
if the_rest and the_rest.strip() != '':
blocks.insert(0, the_rest) # Keep anything off the end, should be empty though
if inner_if_tags != inner_end_tags:
raise TagNotMatchedError(self.processor, block, 'no end tag found to close start tag')
return next_tag, block, content_blocks
def parse_blocks(self, blocks):
'''Recursively parses blocks into an element tree,
returning a string of the output.
Args:
blocks: The markdown blocks to process.
Returns:
A string of the element tree which was created.
'''
# Parse all the inner content of the boxed-text tags
content_tree = etree.Element('content')
self.parser.parseChunk(content_tree, blocks_to_string(blocks))
# Convert parsed element tree back into html text for rendering
content = ''
for child in content_tree:
content += HtmlSerializer.tostring(child)
return content
def tag_starts_with(argument_key, arguments, default=False):
'''Search for the given argument in a string of all arguments,
treating the argument as a flag only.
Args:
argument_key: the name of the argument.
arguments: a string of the argument inputs.
default: the default value if not found.
Returns:
True if argument is found, otherwise None.
'''
result = re.search(r'(^|\s+){}($|\s)'.format(argument_key), arguments)
if result:
return True
else:
return default