MoinLight

Annotated moinformat/macros/toc.py

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