Ticket #2594: template_whitespace.patch

File template_whitespace.patch, 11.9 KB (added by SmileyChris, 9 years ago)

Alternate version

  • django/template/__init__.py

     
    6666TOKEN_TEXT = 0
    6767TOKEN_VAR = 1
    6868TOKEN_BLOCK = 2
     69TOKEN_WHITESPACE = 3
     70TOKEN_MULTIPART = 4
    6971
    7072# template syntax constants
    7173FILTER_SEPARATOR = '|'
     
    8587UNKNOWN_SOURCE="<unknown source>"
    8688
    8789# match a variable or block tag and capture the entire tag, including start/end delimiters
    88 tag_re = re.compile('(%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END),
     90tag_re = re.compile('((?:(?<![^\n])[\t ]*(?:%s.*?%s|%s.*?%s)[\t ]*\n)|(?:%s.*?%s|%s.*?%s))' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END),
     91                                          re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END),
     92                                          re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END),
    8993                                          re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END)))
     94whitespace_tag_re = re.compile('(%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END),
     95                                          re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END)))
     96ends_with_newline_re = re.compile('\n[\t ]*$')
    9097
    9198# global dictionary of libraries that have been loaded using get_library
    9299libraries = {}
     
    163170
    164171class Token(object):
    165172    def __init__(self, token_type, contents):
    166         "The token_type must be TOKEN_TEXT, TOKEN_VAR or TOKEN_BLOCK"
     173        """
     174        The token_type must be one of the following: TOKEN_TEXT, TOKEN_VAR,
     175        TOKEN_BLOCK, TOKEN_WHITESPACE, TOKEN_MULTIPART.
     176        """
    167177        self.token_type, self.contents = token_type, contents
    168178
    169179    def __str__(self):
    170         return '<%s token: "%s...">' % \
    171             ({TOKEN_TEXT: 'Text', TOKEN_VAR: 'Var', TOKEN_BLOCK: 'Block'}[self.token_type],
    172             self.contents[:20].replace('\n', ''))
     180        if self.token_type == TOKEN_MULTIPART:
     181            contents = ', '.join([str(token) for token in self.contents])
     182        elif self.token_type == TOKEN_WHITESPACE:
     183            contents = self.contents.replace('\n', '\\n')
     184        else:
     185            contents = self.contents.replace('\n', '')
     186        if len(self.contents) > 23:
     187            contents = contents[:20] + '...'
     188        return '<%s token: "%s">' % \
     189            ({TOKEN_TEXT: 'Text',
     190              TOKEN_VAR: 'Var',
     191              TOKEN_BLOCK: 'Block',
     192              TOKEN_WHITESPACE: 'Whitespace',
     193              TOKEN_MULTIPART: 'Multipart'}[self.token_type],
     194            contents)
    173195
    174196    def split_contents(self):
    175197        return list(smart_split(self.contents))
     
    187209
    188210    def create_token(self,token_string):
    189211        "Convert the given token string into a new Token object and return it"
    190         if token_string.startswith(VARIABLE_TAG_START):
    191             token = Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip())
    192         elif token_string.startswith(BLOCK_TAG_START):
    193             token = Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip())
     212        token_group = whitespace_tag_re.split(token_string)
     213        multipart = len(token_group) > 1
     214        if multipart:
     215            # If there content in the last group then this is a whitespaced
     216            # token.
     217            has_whitespace = token_group[-1]
     218            if not has_whitespace:
     219                token_group = filter(None, token_group)
     220                multipart = len(token_group) > 1
     221                if not multipart:
     222                    token_string = token_group[0].strip()
     223        if multipart:
     224            token = Token(TOKEN_MULTIPART, token_group)
     225            if has_whitespace:
     226                token.whitespace = True
     227            self.multipart_token(token)
    194228        else:
    195             token = Token(TOKEN_TEXT, token_string)
     229            if token_string.startswith(VARIABLE_TAG_START):
     230                token = Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip())
     231            elif token_string.startswith(BLOCK_TAG_START):
     232                token = Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip())
     233            else:
     234                token = Token(TOKEN_TEXT, token_string)
    196235        return token
     236   
     237    def multipart_token(self, token):
     238        token.contents = [self.create_token(token_string) for token_string in token.contents]
     239        if getattr(token, 'whitespace', None):
     240            before = token.contents[0]
     241            after = token.contents[-1]
     242            before.token_type = TOKEN_WHITESPACE
     243            before.start = True
     244            after.token_type = TOKEN_WHITESPACE
     245            before.start = False
    197246
    198247class DebugLexer(Lexer):
    199248    def __init__(self, template_string, origin):
     
    217266    def create_token(self, token_string, source):
    218267        token = super(DebugLexer, self).create_token(token_string)
    219268        token.source = source
     269        if token.token_type == TOKEN_MULTIPART:
     270            token.contents = [self.create_token(inner_token, source) for inner_token in token.contents]
     271            if getattr(token, 'whitespace', None):
     272                before = token.contents[0]
     273                after = token.contents[-1]
     274                before.token_type = TOKEN_WHITESPACE
     275                before.start = True
     276                after.token_type = TOKEN_WHITESPACE
     277                after.start = False
    220278        return token
    221279
     280    def multipart_token(self, token):
     281        pass
     282
    222283class Parser(object):
    223284    def __init__(self, tokens):
    224285        self.tokens = tokens
     
    232293        nodelist = self.create_nodelist()
    233294        while self.tokens:
    234295            token = self.next_token()
    235             if token.token_type == TOKEN_TEXT:
     296            if token.token_type == TOKEN_WHITESPACE:
     297                if token.start:
     298                    node_type = WhitespaceStartNode
     299                else:
     300                    node_type = WhitespaceEndNode
     301                self.extend_nodelist(nodelist, node_type(token.contents), token)
     302            elif token.token_type == TOKEN_TEXT:
    236303                self.extend_nodelist(nodelist, TextNode(token.contents), token)
    237304            elif token.token_type == TOKEN_VAR:
    238305                if not token.contents:
     
    307374        pass
    308375
    309376    def next_token(self):
    310         return self.tokens.pop(0)
     377        token = self.tokens.pop(0)
     378        if token.token_type == TOKEN_MULTIPART:
     379            token_group = list(token.contents)
     380            # Since we are prepending, we want the last token in first.
     381            token_group.reverse()
     382            map(self.prepend_token, token_group)
     383            token = self.next_token()
     384        return token
    311385
    312386    def prepend_token(self, token):
    313387        self.tokens.insert(0, token)
    314388
    315389    def delete_first_token(self):
    316         del self.tokens[0]
     390        self.next_token()
    317391
    318392    def add_library(self, lib):
    319393        self.tags.update(lib.tags)
     
    683757class NodeList(list):
    684758    def render(self, context):
    685759        bits = []
     760        whitespace = None
     761        whitespace_needed = False
     762        open_whitespace_block = False
    686763        for node in self:
    687764            if isinstance(node, Node):
    688                 bits.append(self.render_node(node, context))
     765                bit = self.render_node(node, context)
    689766            else:
    690                 bits.append(node)
     767                bit = node
     768            if isinstance(node, WhitespaceStartNode):
     769                # Remember the starting white space, but don't use it yet.
     770                whitespace = bit
     771                bit = None
     772                whitespace_start_position = len(bits)
     773                open_whitespace_block = True
     774                whitespace_needed = False
     775            elif isinstance(node, WhitespaceEndNode):
     776                if not whitespace_needed:
     777                    # If there is stored starting white space and this is the
     778                    # end of the line, then the white space and this new line
     779                    # can be dropped.
     780                    open_whitespace_block = False
     781                elif ends_with_newline_re.search(bits[-1]):
     782                    # If the content ended in a new line, don't use the
     783                    # surrounding white space.
     784                    open_whitespace_block = False
     785                    # Alternate behaviour could be to just strip the second
     786                    # new line:
     787                    #bit = bit[:-1]
     788                if open_whitespace_block:
     789                    # Insert the starting white space.
     790                    if whitespace:
     791                        bits.insert(whitespace_start_position, whitespace)
     792                    open_whitespace_block = False
     793                else:
     794                    # Drop the new line.
     795                    bit = None
     796            if bit:
     797                bits.append(bit)
     798                if open_whitespace_block and not whitespace_needed:
     799                    whitespace_needed = True
    691800        return ''.join(bits)
    692801
    693802    def get_nodes_by_type(self, nodetype):
     
    756865            raise
    757866        return self.encode_output(output)
    758867
     868class WhitespaceStartNode(TextNode):
     869    def __repr__(self):
     870        return "<Whitespace Start Node: %r>" % self.s[:25]
     871
     872class WhitespaceEndNode(TextNode):
     873    def __repr__(self):
     874        return "<Whitespace End Node: %r>" % self.s[:25]
     875
    759876def generic_tag_compiler(params, defaults, name, node_class, parser, token):
    760877    "Returns a template.Node subclass."
    761878    bits = token.split_contents()[1:]
  • django/template/defaulttags.py

     
    114114                'parentloop': parentloop,
    115115            }
    116116            context[self.loopvar] = item
    117             for node in self.nodelist_loop:
    118                 nodelist.append(node.render(context))
     117            nodelist.extend(self.nodelist_loop.render(context))
    119118        context.pop()
    120119        return nodelist.render(context)
    121120
  • django/template/loader_tags.py

     
    5959        else:
    6060            return get_template_from_string(source, origin, parent)
    6161
     62    def get_parent_extendsnode(self, compiled_parent):
     63        """
     64        Returns the ExtendsNode for the parent template.
     65        None is returned if a match is not found in the first 10 nodes.
     66        """
     67        for node in compiled_parent.nodelist[:10]:
     68            if isinstance(node, ExtendsNode):
     69                return node
     70        return None
     71
    6272    def render(self, context):
    6373        compiled_parent = self.get_parent(context)
    64         parent_is_child = isinstance(compiled_parent.nodelist[0], ExtendsNode)
     74        parent_extendsnode = self.get_parent_extendsnode(compiled_parent)
    6575        parent_blocks = dict([(n.name, n) for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)])
    6676        for block_node in self.nodelist.get_nodes_by_type(BlockNode):
    6777            # Check for a BlockNode with this node's name, and replace it if found.
     
    7282                # parent block might be defined in the parent's *parent*, so we
    7383                # add this BlockNode to the parent's ExtendsNode nodelist, so
    7484                # it'll be checked when the parent node's render() is called.
    75                 if parent_is_child:
    76                     compiled_parent.nodelist[0].nodelist.append(block_node)
     85                if parent_extendsnode:
     86                    parent_extendsnode.nodelist.append(block_node)
    7787            else:
    7888                # Keep any existing parents and add a new one. Used by BlockNode.
    7989                parent_block.parent = block_node.parent
Back to Top