# -*- coding: utf-8 -*-
"""
= TracNav: The navigation bar for Trac =

This macro implements a fully customizable navigation bar for the Trac
wiki engine. The contents of the navigation bar is a wiki page itself
and can be edited like any other wiki page through the web
interface. The navigation bar supports hierarchical ordering of
topics. The design of TracNav mimics the design of the TracGuideToc
that was originally supplied with Trac. The drawback of TracGuideToc
is that it is not customizable without editing its source code and
that it does not support hierarchical ordering.


== Installation ==

To install TracNav, place the file `TracNav.py` in the `wiki-macros`
subdirectory and the accompanying `tracnav.css` file in the
`templates` subdirectory of your Trac project. Add this line
{{{
@import url(<?cs var:chrome.href ?>/site/tracnav.css);
}}}
to the `templates/site_css.cs` file of your Trac project.

The `tracnav.css` file defines the styles for displaying the
navigation bar. These styles build upon the styles for !TracGuideToc
that come with your Trac distribution. If you just install the macro
but miss to install the style file, TracNav will work but look
somewhat strange.


== Usage ==

To use TracNav, create an index page for your site and call the
TracNav macro on each page, where the navigation bar should be
displayed. The index page is a regular wiki page. The page with the
table of contents must include an unordered list of links that should
be displayed in the navigation bar.

To display the navigation bar on a page, you must call the TracNav
macro on that page an pass the name of your table of contents as
argument.


== Author and License ==

Copyright 2005, 2006
 *  Bernhard Haumacher (haui at haumacher.de)
 *  Thomas Moschny (moschny at ipd.uni-karlsruhe.de)

{{{
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
}}}

== Additional information and a life example ==

Please visit: http://svn.ipd.uka.de/trac/javaparty/wiki/TracNav
"""

__revision__ = "$Id$"

import re
from trac.wiki.api import WikiSystem
from trac.wiki.model import WikiPage
from trac.wiki.formatter import Formatter, OneLinerFormatter
from StringIO import StringIO

TRACNAVHOME = "http://svn.ipd.uka.de/trac/javaparty/wiki/TracNav"
LISTRULE = re.compile(r"^(?P<indent>[ \t\v]+)\* +(?P<rest>.*)$", re.M)
ALLOWED_MACROS = ["image"]

def get_toc(hdf, env, preview, name):
    """
    Fetch the wiki page containing the toc, if available.
    """
    if preview:
        cur_path = hdf.getValue('HTTP.PathInfo', '')
        toc_path = "/wiki/" + name
        if cur_path == toc_path:
            return hdf.getValue('args.text', '')

    if WikiSystem(env).has_page(name):
        return WikiPage(env, name).text
    else:
        return ''


class TocFormatter(OneLinerFormatter):
    """
    Basically the OneLinerFormatter, but additionally remembers the
    last wiki link.
    """
    def format_toc(self, wikitext):
        self.link = None
        out = StringIO()
        OneLinerFormatter.format(self, wikitext, out)
        return out.getvalue(), self.link

    def __init__(self, env):
        OneLinerFormatter.__init__(self, env)
        self.link = None

    def _make_link(self, namespace, target, match, label):
        if namespace == 'wiki':
            self.link = target
        return OneLinerFormatter._make_link(
            self, namespace, target, match, label)

    def _macro_formatter(self, match, fullmatch):
        name = fullmatch.group('macroname').lower()
        if name == 'br':
            return ' '
        elif name in ALLOWED_MACROS:
            # leapfrog the OneLinerFormatter
            return Formatter._macro_formatter(self, match, fullmatch)
        else:
            return ''

    # FIXME: what about _make_relative_link() ?
    # FIXME: CamelCase links are special and not handled by the Formatter...


def get_toc_entry(toc_text, env):
    """
    Parse and format the entries in toc_text.
    """
    formatter = TocFormatter(env)
    for match in LISTRULE.finditer(toc_text):
        indent = len(match.group('indent'))
        label, link = formatter.format_toc(match.group('rest'))
        yield indent, link, label


def get_toc_entry_and_indent(gen):
    """
    Filter for get_toc_entry().  The first call to next() returns the
    indentation level of the next entry (or -1 if there are no more
    entries) and the second call returns the entry itself.
    """
    while True:
        try:
            indent, link, label = gen.next()
        except StopIteration:
            yield -1
            return
        yield indent
        yield link, label        


def _parse_toc(gen, next_indent, level = 0):
    toclist = []
    if next_indent > level:
        sublist, next_indent = _parse_toc(gen, next_indent, level + 1)
        if next_indent < level: # level is empty
            return sublist, next_indent
        else:                   # broken indentation structure
            toclist.append((None, None, sublist))
    while True:
        if next_indent == level:
            (link, label), next_indent = gen.next(), gen.next()
            if next_indent > level:
                sublist, next_indent = _parse_toc(gen, next_indent, level + 1)
                toclist.append((link, label, sublist))
            else:
                toclist.append((link, label, None))
        else:
            assert next_indent < level
            return toclist, next_indent


def parse_toc(toc_text, env):
    """
    Recursively construct the toc tree using _parse_toc().
    """
    gen = get_toc_entry_and_indent(get_toc_entry(toc_text, env))
    toc, _ = _parse_toc(gen, gen.next())
    return toc
   

def execute(hdf, args, env):
    """
    Main routine of the wiki macro.
    """
    #env.log.debug("hdf: %s", hdf)
    preview = hdf.getValue('args.preview', "")
    curpage = hdf.getValue('wiki.page_name', "")

    # split the argument to get the wiki page names to include
    names = (args or "TOC").split('|')

    # Parsing the tocS
    tocs = []
    for name in names:
        toc = parse_toc(get_toc(hdf, env, preview, name), env)
        if not toc:
            toc = parse_toc(' * TOC "%s" is empty!' % name, env)
        tocs.append((name, toc))

    col = 0
    html = '%s<div class="wiki-toc trac-nav">\n' % indentation(col)
    col += 1
    html += '%s<h2><a href="%s">TracNav</a> menu</h2>' % \
            (indentation(col), TRACNAVHOME)

    for name, toc in tocs:
        (found, filtered) = filter_toc(curpage, toc, 0)
        if found:
            html += display_all(hdf, env, name, curpage, filtered, col)
        else:
            html += display_all(hdf, env, name, curpage, toc, col)
    col -= 1
    html += '%s</div>\n' % indentation(col)
    return html



def filter_toc(curpage, toc, level):
    found = 0
    result = []
    for name, title, sub in toc:
        if sub == None:
            if name == curpage:
                found = 1
            result.append((name, title, None))
        else:
            (subfound, subtoc) = filter_toc(curpage, sub, level + 1)
            if subfound:
                found = 1
            if subfound or (name == None):
                if level == 0 and name != None:
                    prepended = [(name, title, subtoc)]
                    prepended.extend(result)
                    result = prepended
                else:
                    result.append((name, title, subtoc))
            else:
                result.append((name, title, []))
    return (found, result)

def indentation(col):
    return ' ' * col

def display_all(hdf, env, name, curpage, toc, col):
    preview = hdf.getValue('args.preview', "")
    html = ''

    if (not preview) and hdf.getValue('trac.acl.WIKI_MODIFY', ''):
        html += '%s<div class="edit"><a href="%s?edit=yes">edit</a></div>\n' % \
                (indentation(col), env.href.wiki(name))
    html += '%s<ul>\n' % indentation(col)
    col += 1
    html += display(curpage, toc, 0, col)
    col -= 1
    html += '%s</ul>\n' % indentation(col)
    return html

def display(curpage, toc, depth, col):
    html = ''
    for name, title, sub in toc:
        li_style = ' style="padding-left: %dem;"' % (depth + 1)
        if sub == None:
            if name == curpage:
                cls = ' class="active"'
            else:
                cls = ''
            html += '%s<li%s%s>%s</li>\n' % \
                    (indentation(col), li_style, cls, title)
        else:
            html += '%s<li%s>\n' % (indentation(col), li_style)
            col += 1
            if name == None or sub:
                html += '%s<h4>%s</h4>\n' % \
                        (indentation(col), title)
            else:
                html += '%s<h4>%s...</h4>\n' % \
                        (indentation(col), title)
            col -= 1
            html += '%s</li>\n' % indentation(col)
            if len(sub) > 0:
                html += display(curpage, sub, depth + 1, col)
    return html

