# HG changeset patch # User Paul Boddie # Date 1534193574 -7200 # Node ID bec7e083676d4ea1c515aef74afec5bd92013cb4 # Parent 3745c9e20358fcd5f78ced3212ce26b2ba3252d7 Fixed table of contents heading level tests where minimum or maximum levels are omitted. Introduced block replacement and splitting when tables of contents appear within blocks, as they are likely to do. This prevents document validity issues when serialised as HTML. diff -r 3745c9e20358 -r bec7e083676d moinformat/macros/toc.py --- a/moinformat/macros/toc.py Mon Aug 13 17:55:16 2018 +0200 +++ b/moinformat/macros/toc.py Mon Aug 13 22:52:54 2018 +0200 @@ -20,7 +20,28 @@ """ from moinformat.macros.common import Macro -from moinformat.tree.moin import Container, Heading, Link, List, ListItem, Text +from moinformat.tree.moin import Block, Container, Heading, Link, List, \ + ListItem, Text + +def in_range(min_level, level, max_level): + + """ + Test that 'min_level' <= 'level' <= 'max_level', only imposing tests + involving limits not set to None. + """ + + return (min_level is None or min_level <= level) and \ + (max_level is None or level <= max_level) + +def above_minimum(min_level, level, max_level): + + """ + Test that 'min_level' < 'level' <= 'max_level', only imposing tests + involving limits not set to None. + """ + + return (min_level is None or min_level < level) and \ + (max_level is None or level <= max_level) class TableOfContents(Macro): @@ -81,7 +102,7 @@ # Ignore levels outside the range of interest. - if not (min_level <= level <= max_level): + if not in_range(min_level, level, max_level): continue # Determine whether the heading should be generated at this @@ -133,7 +154,7 @@ # Retain a list at the minimum level. - if min_level < level <= max_level: + if above_minimum(min_level, level, max_level): lists.pop() level -= 1 @@ -145,17 +166,52 @@ # Add the heading as an item. - if min_level <= level <= max_level: + if in_range(min_level, level, max_level): + indent = level - 1 nodes = self.get_entry(heading) item = ListItem(nodes, indent, marker, space, None) items.append(item) - # Replace the macro node's children with the top-level list. - # The macro cannot be replaced because it will be appearing inline. + # Replace the macro node with the top-level list. + + self.insert_table(lists[0]) + + def insert_table(self, content): + + "Insert the given 'content' into the document." + + macro = self.node + parent = macro.parent + region = macro.region + + # Replace the macro if it is not inside a block. + # NOTE: This attempts to avoid blocks being used in inline-only contexts + # NOTE: but may not be successful in every case. + + if not isinstance(parent, Block) or parent is region: + parent.replace(macro, content) - self.node.nodes = lists and [lists[0]] or [] + # Split any block containing the macro into preceding and following + # parts. + + else: + following = parent.split_at(macro) + + # Insert any non-empty following block. + + if not following.whitespace_only(): + region.insert_after(parent, following) + + # Insert the new content. + + region.insert_after(parent, content) + + # Remove any empty preceding block. + + if parent.whitespace_only(): + region.remove(parent) def find_headings(self, node, headings): diff -r 3745c9e20358 -r bec7e083676d moinformat/parsers/moin.py --- a/moinformat/parsers/moin.py Mon Aug 13 17:55:16 2018 +0200 +++ b/moinformat/parsers/moin.py Mon Aug 13 22:52:54 2018 +0200 @@ -559,7 +559,7 @@ # interpret the individual arguments. arglist = args and args.split(",") or [] - macro = Macro(name, arglist, region.append_point()) + macro = Macro(name, arglist, region.append_point(), region) region.append_inline(macro) # Record the macro for later processing. diff -r 3745c9e20358 -r bec7e083676d moinformat/tree/moin.py --- a/moinformat/tree/moin.py Mon Aug 13 17:55:16 2018 +0200 +++ b/moinformat/tree/moin.py Mon Aug 13 22:52:54 2018 +0200 @@ -69,6 +69,13 @@ def empty(self): return not self.nodes + def insert_after(self, old, new): + + "Insert after 'old' in the children the 'new' node." + + index = self.nodes.index(old) + self.nodes.insert(index + 1, new) + def node(self, index): try: return self.nodes[index] @@ -106,6 +113,12 @@ if text: self.append(text) + def remove(self, node): + + "Remove 'node' from the children." + + self.nodes.remove(node) + def replace(self, old, new): "Replace 'old' with 'new' in the children." @@ -113,6 +126,21 @@ i = self.nodes.index(old) self.nodes[i] = new + def split_at(self, node): + + """ + Split the container at 'node', returning a new container holding the + nodes following 'node' that are moved from this container. + """ + + i = self.nodes.index(node) + following = self.__class__(self.nodes[i+1:]) + + # Remove the node and the following parts from this container. + + del self.nodes[i:] + return following + def text_content(self): """ @@ -130,6 +158,12 @@ return "".join(l) + def whitespace_only(self): + + "Return whether the container provides only whitespace text." + + return not self.text_content().strip() + def __str__(self): return self.prettyprint() @@ -508,14 +542,15 @@ "Macro details." - def __init__(self, name, args, parent, nodes=None): + def __init__(self, name, args, parent, region, nodes=None): Container.__init__(self, nodes or []) self.name = name + self.args = args self.parent = parent - self.args = args + self.region = region def __repr__(self): - return "Macro(%r, %r, %r, %r)" % (self.name, self.args, self.parent, self.nodes) + return "Macro(%r, %r, %r, %r, %r)" % (self.name, self.args, self.parent, self.region, self.nodes) def prettyprint(self, indent=""): l = ["%sMacro: name=%r args=%r" % (indent, self.name, self.args)]