1 #!/usr/bin/env python 2 3 """ 4 Table of contents macro. 5 6 Copyright (C) 2018 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from moinformat.macros.common import Macro 23 from moinformat.tree.moin import Container, Heading, Link, List, ListItem, Text 24 25 class TableOfContents(Macro): 26 27 "A table of contents macro." 28 29 name = "TableOfContents" 30 31 def evaluate(self): 32 33 "Evaluate the macro, producing a table of contents." 34 35 arglist = [] 36 _defaults = [None] * 2 37 38 for arg, default in map(None, self.node.args, _defaults): 39 if arg is not None: 40 try: 41 arg = max(1, int(arg.strip())) 42 except ValueError: 43 arg = None 44 arglist.append(arg) 45 46 self.make_table(arglist[0], arglist[1]) 47 48 def make_table(self, min_level=None, max_level=None): 49 50 """ 51 Make a table of contents with the given 'min_level' and 'max_level' of 52 headings. 53 """ 54 55 headings = [] 56 self.find_headings(self.doc, headings) 57 58 if not headings: 59 return 60 61 # Common list features. 62 63 marker = "1." 64 space = " " 65 num = "1" 66 67 # Start with no lists, no current item. 68 69 lists = [] 70 item = None 71 level = 0 72 73 for heading in headings: 74 new_level = heading.level 75 76 # Create new lists if the level increases. 77 78 if new_level > level: 79 while level < new_level: 80 level += 1 81 82 # Ignore levels outside the range of interest. 83 84 if not (min_level <= level <= max_level): 85 continue 86 87 # Determine whether the heading should be generated at this 88 # level or whether there are intermediate levels being 89 # produced. 90 91 nodes = level == new_level and self.get_entry(heading) or [] 92 indent = level - 1 93 94 # Create a new item for the heading or sublists. 95 96 new_item = ListItem(nodes, indent, marker, space, None) 97 98 # Either revive an existing list. 99 100 if level == min_level and lists: 101 new_list = lists[-1] 102 new_items = new_list.nodes 103 104 # Or make a list and add an item to it. 105 106 else: 107 new_items = [] 108 new_list = List(new_items, indent, marker, num) 109 110 # Add the list to the current item, if any. 111 112 if item: 113 item.nodes.append(new_list) 114 115 # Record the new list. 116 117 lists.append(new_list) 118 119 # Add the item to the new or revived list. 120 121 new_items.append(new_item) 122 123 # Reference the new list's items and current item. 124 125 items = new_items 126 item = new_item 127 128 else: 129 # Retrieve an existing list if the level decreases. 130 131 if new_level < level: 132 while level > new_level: 133 134 # Retain a list at the minimum level. 135 136 if min_level < level <= max_level: 137 lists.pop() 138 139 level -= 1 140 141 # Obtain the existing list and the current item. 142 143 items = lists[-1].nodes 144 item = items[-1] 145 146 # Add the heading as an item. 147 148 if min_level <= level <= max_level: 149 indent = level - 1 150 nodes = self.get_entry(heading) 151 152 item = ListItem(nodes, indent, marker, space, None) 153 items.append(item) 154 155 # Replace the macro node's children with the top-level list. 156 # The macro cannot be replaced because it will be appearing inline. 157 158 self.node.nodes = lists and [lists[0]] or [] 159 160 def find_headings(self, node, headings): 161 162 "Find headings under 'node', adding them to the 'headings' list." 163 164 if node.nodes: 165 for n in node.nodes: 166 if isinstance(n, Heading): 167 headings.append(n) 168 elif isinstance(n, Container): 169 self.find_headings(n, headings) 170 171 def get_entry(self, heading): 172 173 "Return nodes for an entry involving 'heading'." 174 175 return [Link(heading.nodes[:], "#%s" % heading.identifier), Text("\n")] 176 177 macro = TableOfContents 178 179 # vim: tabstop=4 expandtab shiftwidth=4