#!/usr/bin/env python
"""Generate HTML documentation from live Python objects."""

__version__ = "Ka-Ping Yee <ping@lfw.org>, 29 May 2000"

import sys, string, re, inspect

# ---------------------------------------------------- formatting utilities
def htmlescape(str):
    return string.replace(string.replace(str, "&", "&amp;"), "<", "&lt;")


# added by prescod
#   physical addresses produced by repr were defeating diff and they are 
#   ugly anyhow
def smartRepr( obj ):
    rep=repr( obj )
    match=re.match( r"<(.+?) at ......>", rep )
    if match:
        return "<"+match.group(1)+">"
    else:
        return rep

def htmlrepr(object):
    return htmlescape(smartRepr(object))

def preformat(str):
    str = htmlescape(string.expandtabs(str))
    return string.replace(string.replace(str, " ", "&nbsp;"), "\n", "<br>\n")

def multicolumn(list, format, cols=4):
    results = ["<table width=\"100%\"><tr>"]
    rows = (len(list)+cols-1)/cols

    for col in range(cols):
        results.append("<td width=\"%d%%\" valign=top>" % (100/cols))
        for i in range(rows*col, rows*col+rows):
            if i < len(list):
                results.append(format(list[i]) + "<br>")
        results.append("</td>")
    results.append("</tr></table>")
    return results

def heading(title, fgcol, bgcol, extras=""):
    return ["""
<p><table width="100%%" cellspacing=0 cellpadding=0 border=0>
<tr bgcolor="%s"><td colspan=3 valign=bottom><small><small><br></small></small
><font color="%s" face="helvetica, arial">&nbsp;%s</font></td
><td align=right valign=bottom>&nbsp;%s</td></tr>
""" % (bgcol, fgcol, title, extras), "</table>"]

def section(title, fgcol, bgcol, contents, width=20,
            prelude="", marginalia=None, gap="&nbsp;&nbsp;&nbsp;"):
    if marginalia is None:
        marginalia = "&nbsp;" * width
    results = []
    results.append("""
<p><table width="100%%" cellspacing=0 cellpadding=0 border=0>
<tr bgcolor="%s"><td colspan=3 valign=bottom><small><small><br></small></small
><font color="%s" face="helvetica, arial">&nbsp;%s</font></td></tr>
""" % (bgcol, fgcol, title))
    if prelude:
        results.append("""
<tr><td bgcolor="%s">%s</td>
<td bgcolor="%s" colspan=2>%s</td></tr>
""" % (bgcol, marginalia, bgcol, prelude))
    results.append("""
<tr><td bgcolor="%s">%s</td><td>%s</td>
""" % (bgcol, marginalia, gap))

    # Alas, this horrible hack seems to be the only way to force Netscape
    # to expand the main cell consistently to the maximum available width.
    results.append("<td><small><small>" + "&nbsp; "*100 + "</small></small\n>")

    results.append(contents)
    results.append("</td></tr></table>")
    return results

def footer():
    return """
<table width="100%"><tr><td align=right>
<font face="helvetica, arial"><small><small>generated with
<strong>htmldoc</strong> by Ka-Ping Yee</a></small></small></font>
</td></tr></table>"""
        

# -------------------------------------------------------- automatic markup
import re

def namelink(name, *dicts):
    for dict in dicts:
        if dict.has_key(name):
            return "<a href=\"%s\">%s</a>" % (dict[name], name)
    return name

def classlink(object, modname, *dicts):
    name = object.__name__
    if object.__module__ != modname:
        name = object.__module__ + "." + name
    for dict in dicts:
        if dict.has_key(object):
            return "<a href=\"%s\">%s</a>" % (dict[object], name)
    return name

def modulelink(object):
    return "<a href=\"%s.html\">%s</a>" % (object.__name__, object.__name__)

def markup(text, functions={}, classes={}, methods={}, escape=htmlescape):
    """Mark up some plain text, given a context of symbols to look for.
    Each context dictionary maps object names to named anchor identifiers."""
    results = []
    here = 0
    pattern = re.compile("(self\.)?(\w+)")
    while 1:
        match = pattern.search(text, here)
        if not match: break
        start, end = match.regs[2]
        found, name = match.group(0), match.group(2)
        results.append(escape(text[here:start]))

        if text[end:end+1] == "(":
            results.append(namelink(name, methods, functions, classes))
        elif match.group(1):
            results.append("<strong>%s</strong>" % name)
        else:
            results.append(namelink(name, classes))
        here = end
    results.append(text[here:])
    return string.join(results, "")

def getdoc(object):
    result = inspect.getdoc(object)
    if not result:
        try: result = inspect.getcomments(object)
        except: pass
    return result and string.rstrip(result) + "\n" or ""

# -------------------------------------------------- type-specific routines
def document_tree(tree, modname, classes={}):
    """Produce HTML for a class tree as returned by inspect.getclasstree()."""
    results = ["<dl>\n"]
    for entry in tree:
        if type(entry) is type(()):
            c, bases = entry
            results.append("<dt><font face=\"helvetica, arial\"><small>")
            results.append(classlink(c, modname, classes))
            if bases:
                parents = []
                for base in bases:
                    parents.append(classlink(base, modname, classes))
                results.append("(" + string.join(parents, ", ") + ")")
            results.append("\n</small></font></dt>")
        elif type(entry) is type([]):
            results.append("<dd>\n")
            results.append(document_tree(entry, modname, classes))
            results.append("</dd>\n")
    results.append("</dl>\n")
    return results

def document_module(object):
    """Produce HTML documentation for a given module object."""
    name = object.__name__
    file = inspect.getfile(object)
    results = []
    head = "<br><big><big><strong>&nbsp;%s</strong></big></big>" % name
    if hasattr(object, "__version__"):
        head = head + " (version: %s)" % htmlescape(object.__version__)
    results.append(heading(head, "#ffffff", "#7799ee"))

    cadr = lambda list: list[1]
    modules = map(cadr, inspect.getmembers(object, inspect.ismodule))

    classes, cdict = [], {}
    for key, value in inspect.getmembers(object, inspect.isclass):
        if inspect.getfile(value) == file:
            classes.append(value)
            cdict[key] = cdict[value] = "#" + key
    functions, fdict = [], {}
    for key, value in inspect.getmembers(object, inspect.isroutine):
        if not inspect.isfunction(value) or inspect.getfile(value) == file:
            functions.append(value)
            fdict[key] = "#-" + key
            if inspect.isfunction(value): fdict[value] = fdict[key]

    for c in classes:
        for base in c.__bases__:
            key, modname = base.__name__, base.__module__
            if modname != name and sys.modules.has_key(modname):
                module = sys.modules[modname]
                if hasattr(module, key) and getattr(module, key) is base:
                    if not cdict.has_key(key):
                        cdict[key] = cdict[base] = modname + ".html#" + key

    doc = markup(getdoc(object), fdict, cdict, escape=preformat)
    if doc: doc = "<p><small><tt>" + doc + "</tt></small>\n\n"
    else: doc = "<p><small><em>no doc string</em></small>\n"
    results.append(doc)

    if modules:
        contents = multicolumn(modules, modulelink)
        results.append(section("<big><strong>Modules</strong></big>",
                               "#fffff", "#aa55cc", contents))

    if classes:
        contents = document_tree(inspect.getclasstree(classes), name, cdict)
        for item in classes:
            contents.append(document_class(item, fdict, cdict))
        results.append(section("<big><strong>Classes</strong></big>",
                               "#ffffff", "#ee77aa", contents))
    if functions:
        contents = []
        for item in functions:
            contents.append(document_function(item, fdict, cdict))
        results.append(section("<big><strong>Functions</strong></big>",
                               "#ffffff", "#eeaa77", contents))
    return results

def document_class(object, functions={}, classes={}):
    """Produce HTML documentation for a given class object."""
    name = object.__name__
    bases = object.__bases__
    results = []
    
    methods, mdict = [], {}
    for key, value in inspect.getmembers(object, inspect.ismethod):
        methods.append(value)
        mdict[key] = mdict[value] = "#" + name + "-" + key

    if methods:
        for item in methods:
            results.append(document_method(
                item, functions, classes, mdict, name))

    title = "<a name=\"%s\">class <strong>%s</strong></a>" % (name, name)
    if bases:
        parents = []
        for base in bases:
            parents.append(classlink(base, object.__module__, classes))
        title = title + "(%s)" % string.join(parents, ", ")
    doc = markup(getdoc(object), functions, classes, mdict, escape=preformat)
    if doc: doc = "<small><tt>" + doc + "<br>&nbsp;</tt></small>"
    else: doc = "<small><em>no doc string</em></small>"
    return section(title, "#000000", "#ffc8d8", results, 10, doc)

def document_method(object, functions={}, classes={}, methods={}, clname=""):
    """Produce HTML documentation for a given method object."""
    return document_function(
        object.im_func, functions, classes, methods, clname)

def defaultformat(object):
    return """<small><font color="#a0a0a0">=""" + \
        htmlrepr(object) + "</font></small>"

def document_function(object, functions={}, classes={}, methods={}, clname=""):
    """Produce HTML documentation for a given function object."""
    try:
        args, varargs, varkw, defaults = inspect.getargspec(object)
        argspec = inspect.formatargspec(
            args, varargs, varkw, defaults, defaultformat=defaultformat)
    except TypeError:
        argspec = "(<small><em>no arg info</em></small>)"
    
    if object.__name__ == "<lambda>":
        decl = ["<em>lambda</em> ", argspec[1:-1]]
    else:
        anchor = clname + "-" + object.__name__
        decl = ["<a name=\"%s\"\n>" % anchor,
                "<strong>%s</strong>" % object.__name__, argspec, "</a>\n"]
    doc = markup(getdoc(object), functions, classes, methods, escape=preformat)
    if doc:
        doc = string.replace(doc, "<br>\n", "</tt></small\n><dd><small><tt>")
        doc = ["<dd><small><tt>", doc, "</tt></small>"]
    else:
        doc = "<dd><small><em>no doc string</em></small>"
    return ["<dl><dt>", decl, doc, "</dl>"]

def document_builtin(object):
    """Produce HTML documentation for a given built-in function."""
    return ("<strong>%s</strong>" % object.__name__ +
            "(<small><em>no arg info</em></small>)")

# --------------------------------------------------- main dispatch routine
def document(object):
    """Generate documentation for a given object."""
    if inspect.ismodule(object): results = document_module(object)
    elif inspect.isclass(object): results = document_class(object)
    elif inspect.ismethod(object): results = document_method(object)
    elif inspect.isfunction(object): results = document_function(object)
    elif inspect.isbuiltin(object): results = document_builtin(object)
    else: raise TypeError, "don't know how to document this kind of object"
    return serialize(results)

def serialize(list):
    """Combine a list containing strings and nested lists into a single
    string.  This lets us manipulate lists until the last moment, since
    rearranging lists is faster than rearranging strings."""
    results = []
    for item in list:
        if type(item) is type(""): results.append(item)
        else: results.append(serialize(item))
    return string.join(results, "")

if __name__ == "__main__":
    import os
    modnames = []
    for arg in sys.argv[1:]:
        if os.path.isdir(arg):
            for file in os.listdir(arg):
                if file[-3:] == ".py":
                    modnames.append(file[:-3])
                elif file[-9:] == "module.so":
                    modnames.append(file[:-9])
        else:
            if arg[-3:] == ".py":
                modnames.append(arg[:-3])
            else:
                modnames.append(arg)

    for modname in modnames:
        try:
            module = __import__(modname)
        except:
            print "failed to import %s" % modname
        else:
            file = open(modname + ".html", "w")
            file.write(
"""<!doctype html public \"-//W3C//DTD HTML 4.0 Transitional//EN\">
<html><title>%s</title><body bgcolor="#ffffff">
""" % modname)
            file.write(document(module))
            file.write("</body></html>")
            file.close()
            print "wrote %s.html" % modname
