"""autotoc.py -- Create a table of contents from HTML headings.


USAGE

autotoc.py infile outfile

For use in a Python script, create an AutoToC instance::

    toc = AutoToC()

Then you can process strings with the process() method,
like that::

    input = open(infile).read()
    output = toc.process(input)
    open(outfile, 'w').write(output)

Insert the following directive in your HTML page at all places where you
want an automatically created table of contents to be inserted::

    <!-- contents -->

You can customize the title of the table of contents, the maximum depth
of headings to be regarded, a number of headings to be skipped and the
name of the class for the div element::

    <!-- contents(title='The table of contents:', classname='toc') -->
    <!-- contents(depth=3, skip=2) -->

By default, title='Contents', depth=6, skip=0 and classname='contents'.


DOWNLOAD

This script is part of Webware for Python.
You can download the latest version from the GitHub repository
(https://github.com/Cito/w4py/blob/master/DocSupport/autotoc.py).


COPYRIGHT

Copyright (c) 2005 by Christoph Zwerschke.
Licensed under the Open Software License version 2.1.
"""

import sys
import re

__version__ = '0.2'


class ToC(object):
    """Auxiliary class representing a table of contents."""

    def __init__(self,
            title='Contents', depth=6, skip=0, classname='contents'):
        self._title = title
        if depth not in range(1, 7):
            depth = 6
        self._depth = depth
        self._skip = skip
        self._classname = classname
        self._pattern = None

    def make_html(self, headings, mindepth=1):
        """Make a table of contents from headings."""
        toc = ['<div class="%s">' % self._classname]
        if self._title:
            toc.append('\n<h%d>%s</h%d>' % (mindepth, self._title, mindepth))
        maxdepth = self._depth
        curdepth = mindepth - 1
        level = 0
        for heading in headings[self._skip:]:
            depth = heading._depth
            if not mindepth <= depth <= maxdepth:
                continue
            if depth != curdepth:
                if depth > curdepth:
                    toc.append('\n<ul>')
                    level += 1
                elif level > 0:
                    toc.append('</li>\n</ul>')
                    level -= 1
                curdepth = depth
            toc.append('\n<li>' + heading.make_entry())
        while level > 0:
            toc.append('</li>\n</ul>')
            level -= 1
        toc.append('\n</div>')
        return ''.join(toc)


class Heading(object):
    """Auxiliary class representing a heading."""

    def __init__(self, depth, title, name):
        self._depth = depth
        self._title = title
        self._name = name
        self._pattern = None
        self._replace = None

    def make_entry(self):
        """Make an entry in the table of contents."""
        if self._name:
            return '<a href="#%s">%s</a>' % (self._name, self._title)
        else:
            return self._title


class AutoToC(object):
    """Main class for automatic creation of table(s) of contents.

    Provides only one method process().
    """

    def __init__(self, depth=6):
        """Initialize the AutoToC processor.

        You may define a maximum depth here already.
        """
        if depth not in range(1, 7):
            depth = 6
        self._depth = depth
        self._whitespace_pattern = re.compile('\s+')
        self._toc_directive = re.compile(
            '(?P<pattern><!-- contents'
            '(?P<args>\(.*?\))? -->)')
        self._heading_pattern = re.compile(
            '(?P<pattern>(?:<a\s+id\s*=\s*["\'](?P<name>.*?)'
            '["\']\s*>\s*(?:</a>\s*)?)?<h(?P<depth>[1-6])(\s.*?)?>'
            '\s*(?P<title>.*?)\s*</h(?P=depth)>)', re.IGNORECASE)

    def process(self, input):
        """Create table(s) of contents and put them where indicated.

        Input and output are strings.
        """
        # Read in all contents directives:
        tocs_found = self._toc_directive.findall(input)
        if not tocs_found:
            return input  # no table of contents directive in the input
        maxdepth = 0
        tocs = []
        group = self._toc_directive.groupindex
        for toc_found in tocs_found:
            p = toc_found[group['args'] - 1]
            if not p:
                p = '()'
            toc = eval('ToC' + p)
            toc._pattern = toc_found[group['pattern'] - 1]
            tocs.append(toc)
            if maxdepth <= toc._depth:
                maxdepth = toc._depth
        # Read in all headings:
        headings_found = self._heading_pattern.findall(input)
        if not headings_found:
            return input  # no headings in the input
        mindepth = 6
        headings = []
        names = {}
        names_created = False
        depths = {}
        group = self._heading_pattern.groupindex
        for heading_found in headings_found:
            names[heading_found[group['name'] - 1]] = 1
        for heading_found in headings_found:
            depth = int(heading_found[group['depth'] - 1])
            # truncate at max depth
            if depth > maxdepth:
                continue
            # get min depth with at least 2 headings
            if depth < mindepth:
                if depth in depths:
                    mindepth = depth
                else:
                    depths[depth] = 1
            title = heading_found[group['title'] - 1]
            name = heading_found[group['name'] - 1]
            if name:
                name_created = False
            else:  # no name given
                name = self._make_name(title)  # create one
                if name in names:  # make sure it is unique
                    n = names[name] + 1
                    while '%s-%d' % (name, n) in names:
                        n += 1
                    name = '%s-%d' % (name, n)
                    names[name] = n
                else:
                    names[name] = 1
                names_created = name_created = True
            heading = Heading(depth, title, name)
            heading._pattern = heading_found[group['pattern'] - 1]
            heading._name_created = name_created
            headings.append(heading)
        del names
        # Add missing link targets:
        if names_created:
            last_pos = 0
            output = []
            for heading in headings:
                pos = input.find(heading._pattern, last_pos)
                assert pos >= 0, 'Cannot find %r.' % heading._pattern
                output.append(input[last_pos:pos])
                last_pos = pos
                pos += len(heading._pattern)
                if heading._name_created:
                    output.append('<a id="%s"></a>' % heading._name)
                output.append(input[last_pos:pos])
                last_pos = pos
            output.append(input[last_pos:])
            input = ''.join(output)
            del output
        # Create table(s) of contents:
        last_pos = 0
        output = []
        for toc in tocs:
            pos = input.find(toc._pattern, last_pos)
            assert pos >= 0, 'Cannot find %r.' % toc._pattern
            output.append(input[last_pos:pos])
            last_pos = pos + len(toc._pattern)
            output.append(toc.make_html(headings, mindepth))
        output.append(input[last_pos:])
        return ''.join(output)

    # Auxiliary functions

    def _make_name(self, title):
        """Create a name from the title"""
        return self._whitespace_pattern.sub('-', title).lower()


def main(args):
    try:
        infile, outfile = args
    except Exception:
        print __doc__
        sys.exit(2)
    toc = AutoToC()
    open(outfile, 'w').write(toc.process(open(infile).read()))


if __name__ == '__main__':
    main(sys.argv[1:])
