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.serialisers.common import make_id 24 from moinformat.tree.moin import Container, Heading, Link, List, ListItem, Text 25 26 class TableOfContents(Macro): 27 28 "A table of contents macro." 29 30 name = "TableOfContents" 31 32 def evaluate(self): 33 34 "Evaluate the macro, producing a table of contents." 35 36 arglist = [] 37 _defaults = [None] * 2 38 39 for arg, default in map(None, self.node.args, _defaults): 40 if arg is not None: 41 try: 42 arg = max(1, int(arg.strip())) 43 except ValueError: 44 arg = None 45 arglist.append(arg) 46 47 self.make_table(arglist[0], arglist[1]) 48 49 def make_table(self, min_level=None, max_level=None): 50 51 """ 52 Make a table of contents with the given 'min_level' and 'max_level' of 53 headings. 54 """ 55 56 headings = [] 57 self.find_headings(self.doc, headings) 58 59 if not headings: 60 return 61 62 # Common list features. 63 64 marker = "1." 65 space = " " 66 num = "1" 67 68 # Start with no lists, no current item. 69 70 lists = [] 71 item = None 72 level = 0 73 74 for heading in headings: 75 new_level = heading.level 76 77 # Create new lists if the level increases. 78 79 if new_level > level: 80 while level < new_level: 81 level += 1 82 83 # Ignore levels outside the range of interest. 84 85 if not (min_level <= level <= max_level): 86 continue 87 88 # Determine whether the heading should be generated at this 89 # level or whether there are intermediate levels being 90 # produced. 91 92 nodes = level == new_level and self.get_entry(heading) or [] 93 indent = level - 1 94 95 # Create a new item for the heading or sublists. 96 97 new_item = ListItem(nodes, indent, marker, space, None) 98 99 # Either revive an existing list. 100 101 if level == min_level and lists: 102 new_list = lists[-1] 103 new_items = new_list.nodes 104 105 # Or make a list and add an item to it. 106 107 else: 108 new_items = [] 109 new_list = List(new_items, indent, marker, num) 110 111 # Add the list to the current item, if any. 112 113 if item: 114 item.nodes.append(new_list) 115 116 # Record the new list. 117 118 lists.append(new_list) 119 120 # Add the item to the new or revived list. 121 122 new_items.append(new_item) 123 124 # Reference the new list's items and current item. 125 126 items = new_items 127 item = new_item 128 129 else: 130 # Retrieve an existing list if the level decreases. 131 132 if new_level < level: 133 while level > new_level: 134 135 # Retain a list at the minimum level. 136 137 if min_level < level <= max_level: 138 lists.pop() 139 140 level -= 1 141 142 # Obtain the existing list and the current item. 143 144 items = lists[-1].nodes 145 item = items[-1] 146 147 # Add the heading as an item. 148 149 if min_level <= level <= max_level: 150 indent = level - 1 151 nodes = self.get_entry(heading) 152 153 item = ListItem(nodes, indent, marker, space, None) 154 items.append(item) 155 156 # Replace the macro node's children with the top-level list. 157 # The macro cannot be replaced because it will be appearing inline. 158 159 self.node.nodes = lists and [lists[0]] or [] 160 161 def find_headings(self, node, headings): 162 163 "Find headings under 'node', adding them to the 'headings' list." 164 165 if node.nodes: 166 for n in node.nodes: 167 if isinstance(n, Heading): 168 headings.append(n) 169 elif isinstance(n, Container): 170 self.find_headings(n, headings) 171 172 def get_entry(self, heading): 173 174 "Return nodes for an entry involving 'heading'." 175 176 target = make_id(heading.text_content()) 177 return [Link(heading.nodes[:], "#%s" % target), Text("\n")] 178 179 macro = TableOfContents 180 181 # vim: tabstop=4 expandtab shiftwidth=4