diff --git a/bottle.py b/bottle.py index f99f46a50..a78b488e6 100644 --- a/bottle.py +++ b/bottle.py @@ -3415,15 +3415,19 @@ class StplParser(object): _re_inl = _re_tok.replace('|\\n','') # We re-use this string pattern later # 2: Comments (until end of line, but not the newline itself) _re_tok += '|(#.*)' - # 3,4: Keywords that start or continue a python block (only start of line) + # 3,4: Open and close grouping tokens + _re_tok += '|([\[\{\(])' + _re_tok += '|([\]\}\)])' + # 5,6: Keywords that start or continue a python block (only start of line) _re_tok += '|^([ \\t]*(?:if|for|while|with|try|def|class)\\b)' \ '|^([ \\t]*(?:elif|else|except|finally)\\b)' - # 5: Our special 'end' keyword (but only if it stands alone) + # 7: Our special 'end' keyword (but only if it stands alone) _re_tok += '|((?:^|;)[ \\t]*end[ \\t]*(?=(?:%(block_close)s[ \\t]*)?\\r?$|;|#))' - # 6: A customizable end-of-code-block template token (only end of line) + # 8: A customizable end-of-code-block template token (only end of line) _re_tok += '|(%(block_close)s[ \\t]*(?=$))' - # 7: And finally, a single newline. The 8th token is 'everything else' + # 9: And finally, a single newline. The 10th token is 'everything else' _re_tok += '|(\\r?\\n)' + # Match the start tokens of code areas in a template _re_split = '(?m)^[ \t]*(\\\\?)((%(line_start)s)|(%(block_start)s))(%%?)' # Match inline statements (may contain python strings) @@ -3437,6 +3441,7 @@ def __init__(self, source, syntax=None, encoding='utf8'): self.code_buffer, self.text_buffer = [], [] self.lineno, self.offset = 1, 0 self.indent, self.indent_mod = 0, 0 + self.paren_depth = 0 def get_syntax(self): ''' Tokens as a space separated string (default: <% %> % {{ }}) ''' @@ -3493,8 +3498,8 @@ def read_code(self, multiline): return code_line += self.source[self.offset:self.offset+m.start()] self.offset += m.end() - _str, _com, _blk1, _blk2, _end, _cend, _nl = m.groups() - if code_line and (_blk1 or _blk2): # a if b else c + _str, _com, _po, _pc, _blk1, _blk2, _end, _cend, _nl = m.groups() + if (code_line or self.paren_depth > 0) and (_blk1 or _blk2): # a if b else c code_line += _blk1 or _blk2 continue if _str: # Python string @@ -3503,6 +3508,15 @@ def read_code(self, multiline): comment = _com if multiline and _com.strip().endswith(self._tokens[1]): multiline = False # Allow end-of-block in comments + elif _po: # open parenthesis + self.paren_depth += 1 + code_line += _po + elif _pc: # close parenthesis + if self.paren_depth > 0: + # we could check for matching parentheses here, but it's + # easier to leave that to python - just check counts + self.paren_depth -= 1 + code_line += _pc elif _blk1: # Start-block keyword (if/for/while/def/try/...) code_line, self.indent_mod = _blk1, -1 self.indent += 1 diff --git a/test/test_stpl.py b/test/test_stpl.py index 4c38f0705..75ca42b56 100755 --- a/test/test_stpl.py +++ b/test/test_stpl.py @@ -375,6 +375,18 @@ def test_multiline_strings_in_code_line(self): ''' self.assertRenders(source, result) + def test_multiline_comprehensions_in_code_line(self): + self.assertRenders(source=''' + % a = [ + % (i + 1) + % for i in range(5) + % if i%2 == 0 + % ] + {{a}} + ''', result=''' + [1, 3, 5] + ''') + if __name__ == '__main__': #pragma: no cover unittest.main()