Browse Source

Panda3D Documentation Generator

Josh Yelon 20 years ago
parent
commit
80ac41f598
1 changed files with 701 additions and 0 deletions
  1. 701 0
      direct/src/directscripts/gendocs.py

+ 701 - 0
direct/src/directscripts/gendocs.py

@@ -0,0 +1,701 @@
+########################################################################
+#
+# Documentation generator for panda.  There are three
+# major subsystems:
+#
+# * The module that reads interrogate databases.
+#
+# * The module that reads python source-files.
+#
+# * 
+# 
+########################################################################
+
+import os,sys,parser,symbol,token,types,re
+
+########################################################################
+#
+# assorted utility functions
+#
+########################################################################
+
+SECHEADER = re.compile("^[A-Z][a-z]+\s*:")
+JUNKHEADER = re.compile("^((Function)|(Access))\s*:")
+
+def readFile(fn):
+    try:
+        srchandle = open(fn, "r")
+        data = srchandle.read()
+        srchandle.close()
+        return data
+    except:
+        sys.exit("Cannot read "+fn)
+
+def writeFile(wfile,data):
+    try:
+        dsthandle = open(wfile, "wb")
+        dsthandle.write(data)
+        dsthandle.close()
+    except:
+        sys.exit("Cannot write "+wfile)
+
+def findFiles(dir, ext, ign, list):
+    for file in os.listdir(dir):
+        full = dir + "/" + file
+        if (ign.has_key(full)==0) and (ign.has_key(file)==0):
+            if (os.path.isfile(full)):
+                if (file.endswith(ext)):
+                    list.append(full)
+            elif (os.path.isdir(full)):
+                findFiles(full, ext, ign, list)
+
+def textToHTML(comment,sep,delsection=None):
+    sections = [""]
+    included = {}
+    for line in comment.split("\n"):
+        line = line.lstrip(" ").lstrip(sep).lstrip(" ").rstrip("\r").rstrip(" ")
+        if (line == ""):
+            sections.append("")
+        elif (SECHEADER.match(line)):
+            sections.append(line)
+        else:
+            sections[-1] = sections[-1] + " " + line
+    total = ""
+    for sec in sections:
+        if (sec != ""):
+            sec = sec.replace("&","&")
+            sec = sec.replace("<","&lt;")
+            sec = sec.replace(">","&gt;")
+            sec = sec.replace("  "," ")
+            sec = sec.replace("  "," ")
+            if (delsection != None) and (delsection.match(sec)):
+                included[sec] = 1
+            if (included.has_key(sec)==0):
+                included[sec] = 1
+                total = total + sec + "<br>\n"
+    return total
+
+def linkToPage(page, text):
+    return '<a href="/apiref.php?page=' + page + '">' + text + '</a>'
+
+def convertToPythonFn(fn):
+    result = ""
+    lastc = 0
+    for c in fn:
+        if (c!="_"):
+            if (lastc=="_"):
+                result = result + c.upper()
+            else:
+                result = result + c
+        lastc = c
+    return result
+
+def getPandaVersion():
+    try:
+        f = file("dtool/PandaVersion.pp","r")
+        pattern = re.compile('^[ \t]*[#][ \t]*define[ \t]+PANDA_VERSION[ \t]+([0-9]+)[ \t]+([0-9]+)[ \t]+([0-9]+)')
+        for line in f:
+            match = pattern.match(line,0)
+            if (match):
+                version = match.group(1)+"."+match.group(2)+"."+match.group(3)
+                break
+        f.close()
+    except: sys.exit("Cannot read version number from dtool/PandaVersion.pp")
+    return version
+
+########################################################################
+#
+# Interrogate Database Tokenizer
+#
+########################################################################
+
+class InterrogateTokenizer:
+    """
+    A big string, with a "parse pointer", and routines to
+    extract integers and strings.  The token syntax is that
+    used by interrogate databases.
+    """
+
+    def __init__(self,fn):
+        self.fn = fn
+        self.pos = 0
+        self.data = readFile(fn)
+
+    def readint(self):
+        neg = 0
+        while (self.data[self.pos].isspace()):
+            self.pos += 1
+        if (self.data[self.pos] == "-"):
+            neg = 1
+            self.pos += 1
+        if (self.data[self.pos].isdigit()==0):
+            print "File position "+str(self.pos)
+            print "Text: "+self.data[self.pos:self.pos+50]
+            sys.exit("Syntax error in interrogate file format 0")
+        value = 0
+        while (self.data[self.pos].isdigit()):
+            value = value*10 + int(self.data[self.pos])
+            self.pos += 1
+        if (neg): value = -value
+        return value
+
+    def readstring(self):
+        length = self.readint()
+        if (self.data[self.pos].isspace()==0):
+            sys.exit("Syntax error in interrogate file format 1")
+        self.pos += 1
+        body = self.data[self.pos:self.pos+length]
+        if (len(body) != length):
+            sys.exit("Syntax error in interrogate file format 2")
+        self.pos += length
+        return body
+
+########################################################################
+#
+# Interrogate Database Storage/Parsing
+#
+########################################################################
+
+def parseInterrogateIntVec(tokzr):
+    length = tokzr.readint()
+    result = []
+    for i in range(length):
+        result.append(tokzr.readint())
+    return result
+
+class InterrogateFunction:
+    def __init__(self, tokzr, db):
+        self.db = db
+        self.index = tokzr.readint()
+        self.componentname = tokzr.readstring()
+        self.flags = tokzr.readint()
+        self.classindex = tokzr.readint()
+        self.scopedname = tokzr.readstring()
+        self.cwrappers = parseInterrogateIntVec(tokzr)
+        self.pythonwrappers = parseInterrogateIntVec(tokzr)
+        self.comment = tokzr.readstring()
+        self.prototype = tokzr.readstring()
+
+class InterrogateEnumValue:
+    def __init__(self, tokzr):
+        self.name = tokzr.readstring()
+        self.scopedname = tokzr.readstring()
+        self.value = tokzr.readint()
+
+class InterrogateDerivation:
+    def __init__(self, tokzr):
+        self.flags = tokzr.readint()
+        self.base = tokzr.readint()
+        self.upcast = tokzr.readint()
+        self.downcast = tokzr.readint()
+
+class InterrogateType:
+    def __init__(self, tokzr, db):
+        self.db = db
+        self.index = tokzr.readint()
+        self.componentname = tokzr.readstring()
+        self.flags = tokzr.readint()
+        self.scopedname = tokzr.readstring()
+        self.truename = tokzr.readstring()
+        self.outerclass = tokzr.readint()
+        self.atomictype = tokzr.readint()
+        self.wrappedtype = tokzr.readint()
+        self.constructors = parseInterrogateIntVec(tokzr)
+        self.destructor = tokzr.readint()
+        self.elements = parseInterrogateIntVec(tokzr)
+        self.methods = parseInterrogateIntVec(tokzr)
+        self.casts = parseInterrogateIntVec(tokzr)
+        self.derivations = []
+        nderivations = tokzr.readint()        
+        for i in range(nderivations):
+            self.derivations.append(InterrogateDerivation(tokzr))
+        self.enumvalues = []
+        nenumvalues = tokzr.readint()
+        for i in range(nenumvalues):
+            self.enumvalues.append(InterrogateEnumValue(tokzr))
+        self.nested = parseInterrogateIntVec(tokzr)
+        self.comment = tokzr.readstring()
+
+class InterrogateParameter:
+    def __init__(self, tokzr):
+        self.name = tokzr.readstring()
+        self.parameterflags = tokzr.readint()
+        self.type = tokzr.readint()
+
+class InterrogateWrapper:
+    def __init__(self, tokzr, db):
+        self.db = db
+        self.index = tokzr.readint()
+        self.componentname = tokzr.readstring()
+        self.flags = tokzr.readint()
+        self.function = tokzr.readint()
+        self.returntype = tokzr.readint()
+        self.returnvaluedestructor = tokzr.readint()
+        self.uniquename = tokzr.readstring()
+        self.parameters = []
+        nparameters = tokzr.readint()
+        for i in range(nparameters):
+            self.parameters.append(InterrogateParameter(tokzr))
+
+class InterrogateDatabase:
+    def __init__(self, tokzr):
+        self.fn = tokzr.fn
+        self.magic = tokzr.readint()
+        version1 = tokzr.readint()
+        version2 = tokzr.readint()
+        if (version1 != 2) or (version2 != 2):
+            sys.exit("This program only understands interrogate file format 2.2")
+        self.library = tokzr.readstring()
+        self.libhash = tokzr.readstring()
+        self.module = tokzr.readstring()
+        self.functions = {}
+        self.wrappers = {}
+        self.types = {}
+        self.namedtypes = {}
+        count_functions = tokzr.readint()
+        for i in range(count_functions):
+            fn = InterrogateFunction(tokzr, self)
+            self.functions[fn.index] = fn
+        count_wrappers = tokzr.readint()
+        for i in range(count_wrappers):
+            wr = InterrogateWrapper(tokzr, self)
+            self.wrappers[wr.index] = wr
+        count_types = tokzr.readint()
+        for i in range(count_types):
+            tp = InterrogateType(tokzr, self)
+            self.types[tp.index] = tp
+            self.namedtypes[tp.scopedname] = tp
+
+########################################################################
+#
+# Pattern Matching for Python Parse Trees
+#
+########################################################################
+
+def printTree(tree,indent):
+    spacing = "                                                        "[:indent]
+    if isinstance(tree,types.TupleType) and isinstance(tree[0], types.IntType):
+        if symbol.sym_name.has_key(tree[0]):
+            for i in range(len(tree)):
+                if (i==0):
+                    print spacing + "(symbol." + symbol.sym_name[tree[0]] + ","
+                else:
+                    printTree(tree[i], indent+1)
+            print spacing + "),"
+        elif token.tok_name.has_key(tree[0]):
+            print spacing + "(token." + token.tok_name[tree[0]] + ", '" + tree[1] + "'),"
+        else:
+            print spacing + str(tree)
+    else:
+        print spacing + str(tree)
+
+
+COMPOUND_STMT_PATTERN = (
+    symbol.stmt,
+    (symbol.compound_stmt, ['compound'])
+    )
+
+
+DOCSTRING_STMT_PATTERN = (
+    symbol.stmt,
+    (symbol.simple_stmt,
+     (symbol.small_stmt,
+      (symbol.expr_stmt,
+       (symbol.testlist,
+        (symbol.test,
+         (symbol.and_test,
+          (symbol.not_test,
+           (symbol.comparison,
+            (symbol.expr,
+             (symbol.xor_expr,
+              (symbol.and_expr,
+               (symbol.shift_expr,
+                (symbol.arith_expr,
+                 (symbol.term,
+                  (symbol.factor,
+                   (symbol.power,
+                    (symbol.atom,
+                     (token.STRING, ['docstring'])
+                     )))))))))))))))),
+     (token.NEWLINE, '')
+     ))
+
+DERIVATION_PATTERN = (
+    symbol.test,
+    (symbol.and_test,
+     (symbol.not_test,
+      (symbol.comparison,
+       (symbol.expr,
+        (symbol.xor_expr,
+         (symbol.and_expr,
+          (symbol.shift_expr,
+           (symbol.arith_expr,
+            (symbol.term,
+             (symbol.factor,
+              (symbol.power,
+               (symbol.atom,
+                (token.NAME, ['classname'])
+   )))))))))))))
+
+
+
+class ParseTreeInfo:
+    docstring = ''
+    name = ''
+
+    def __init__(self, tree, name):
+        """
+        The code can be a string (in which case it is parsed), or it
+        can be in parse tree form already.
+        """
+        self.name = name
+        self.class_info = {}
+        self.function_info = {}
+        self.derivs = {}
+        if isinstance(tree, types.StringType):
+            try:
+                tree = parser.suite(tree+"\n").totuple()
+                if (tree):
+                    found, vars = self.match(DOCSTRING_STMT_PATTERN, tree[1])
+                    if found:
+                        self.docstring = vars["docstring"]
+            except:
+                print "CAUTION --- Parse failed: "+name
+        if isinstance(tree, types.TupleType):
+            self.extract_info(tree)
+
+    def match(self, pattern, data, vars=None):
+        """
+        pattern
+            Pattern to match against, possibly containing variables.
+        data
+            Data to be checked and against which variables are extracted.
+        vars
+            Dictionary of variables which have already been found.  If not
+            provided, an empty dictionary is created.
+        
+        The `pattern' value may contain variables of the form ['varname']
+        which are allowed to parseTreeMatch anything.  The value that is
+        parseTreeMatched is returned as part of a dictionary which maps
+        'varname' to the parseTreeMatched value.  'varname' is not required
+        to be a string object, but using strings makes patterns and the code
+        which uses them more readable.  This function returns two values: a
+        boolean indicating whether a parseTreeMatch was found and a
+        dictionary mapping variable names to their associated values.
+        """
+        if vars is None:
+            vars = {}
+        if type(pattern) is types.ListType:       # 'variables' are ['varname']
+            vars[pattern[0]] = data
+            return 1, vars
+        if type(pattern) is not types.TupleType:
+            return (pattern == data), vars
+        if len(data) != len(pattern):
+            return 0, vars
+        for pattern, data in map(None, pattern, data):
+            same, vars = self.match(pattern, data, vars)
+            if not same:
+                break
+        return same, vars
+
+    def extract_info(self, tree):
+        # extract docstring
+        found = 0
+        if len(tree) == 2:
+            found, vars = self.match(DOCSTRING_STMT_PATTERN[1], tree[1])
+        elif len(tree) >= 4:
+            found, vars = self.match(DOCSTRING_STMT_PATTERN, tree[3])
+        if found:
+            self.docstring = eval(vars['docstring'])
+        # discover inner definitions
+        for node in tree[1:]:
+            found, vars = self.match(COMPOUND_STMT_PATTERN, node)
+            if found:
+                cstmt = vars['compound']
+                if cstmt[0] == symbol.funcdef:
+                    name = cstmt[2][1]
+                    self.function_info[name] = ParseTreeInfo(cstmt and cstmt[-1] or None, name)
+                    self.function_info[name].prototype = self.extract_tokens("", cstmt[3])
+                elif cstmt[0] == symbol.classdef:
+                    name = cstmt[2][1]
+                    self.class_info[name] = ParseTreeInfo(cstmt and cstmt[-1] or None, name)
+                    self.extract_derivs(self.class_info[name],cstmt)
+
+    def extract_derivs(self, classinfo, tree):
+        if (len(tree)==8):
+            derivs = tree[4]
+            for deriv in derivs[1:]:
+                found, vars = self.match(DERIVATION_PATTERN, deriv)
+                if (found):
+                    classinfo.derivs[vars["classname"]] = 1
+
+    def extract_tokens(self, str, tree):
+        if (isinstance(tree, types.TupleType)):
+            if (token.tok_name.has_key(tree[0])):
+                str = str + tree[1]
+                if (tree[1]==","): str=str+" "
+            elif (symbol.sym_name.has_key(tree[0])):
+                for sub in tree[1:]:
+                    str = self.extract_tokens(str, sub)
+        return str
+
+########################################################################
+#
+# The code database contains:
+#
+#  - a list of InterrogateDatabase objects representing C++ modules.
+#  - a list of ParseTreeInfo objects representing python modules.
+#
+# Collectively, these make up all the data about all the code.  
+# 
+########################################################################
+
+class CodeDatabase:
+    def __init__(self, cxxlist, pylist):
+        self.types = {}
+        self.funcs = {}
+        self.goodtypes = {}
+        self.globalfn = []
+        for cxx in cxxlist:
+            print "Reading source file "+cxx
+            tokzr = InterrogateTokenizer(cxx)
+            idb = InterrogateDatabase(tokzr)
+            for type in idb.types.values():
+                if (type.flags & 8192) or (self.types.has_key(type.scopedname)==0):
+                    self.types[type.scopedname] = type
+                if (type.flags & 8192) and (type.atomictype == 0) and (type.scopedname.count(" ")==0) and (type.scopedname.count(":")==0):
+                    self.goodtypes[type.scopedname] = type
+            for func in idb.functions.values():
+                type = idb.types.get(func.classindex)
+                func.pyname = convertToPythonFn(func.componentname)
+                if (type == None):
+                    self.funcs["GLOBAL."+func.pyname] = func
+                    self.globalfn.append("GLOBAL."+func.pyname)
+                else:
+                    self.funcs[type.scopedname+"."+func.pyname] = func
+        for py in pylist:
+            print "Reading source file "+py
+            pyinf = ParseTreeInfo(readFile(py), py)
+            for type in pyinf.class_info.keys():
+                typinf = pyinf.class_info[type]
+                self.types[type] = typinf
+                self.goodtypes[type] = typinf
+                for func in typinf.function_info.keys():
+                    self.funcs[type+"."+func] = typinf.function_info[func]
+            for func in pyinf.function_info.keys():
+                self.funcs["GLOBAL."+func] = pyinf.function_info[func]
+                self.globalfn.append("GLOBAL."+func)
+
+    def getClassList(self):
+        return self.goodtypes.keys()
+
+    def getGlobalFunctionList(self):
+        return self.globalfn
+
+    def getClassComment(self, cn):
+        type = self.types.get(cn)
+        if (isinstance(type, InterrogateType)):
+            return textToHTML(type.comment,"/")
+        elif (isinstance(type, ParseTreeInfo)):
+            return textToHTML(type.docstring,"#")
+        else:
+            return ""
+
+    def getClassParents(self, cn):
+        type = self.types.get(cn)
+        if (isinstance(type, InterrogateType)):
+            parents = []
+            for deriv in type.derivations:
+                basetype = type.db.types[deriv.base]
+                parents.append(basetype.scopedname)
+            return parents
+        elif (isinstance(type, ParseTreeInfo)):
+            return type.derivs.keys()
+        else:
+            return []
+
+    def buildInheritance(self, inheritance, cn):
+        if (inheritance.count(cn) == 0):
+            inheritance.append(cn)
+            for parent in self.getClassParents(cn):
+                self.buildInheritance(inheritance, parent)
+
+    def getInheritance(self, cn):
+        inheritance = []
+        self.buildInheritance(inheritance, cn)
+        return inheritance
+
+    def getClassMethods(self, cn):
+        type = self.types.get(cn)
+        result = []
+        if (isinstance(type, InterrogateType)):
+            for method in type.methods:
+                func = type.db.functions[method]
+                if (func.classindex == type.index):
+                    result.append(type.scopedname+"."+func.pyname)
+        elif (isinstance(type, ParseTreeInfo)):
+            for method in type.function_info.keys():
+                result.append(type.name + "." + method)
+        return result
+
+    def getFunctionName(self, fn):
+        func = self.funcs.get(fn)
+        if (isinstance(func, InterrogateFunction)):
+            return func.pyname
+        elif (isinstance(func, ParseTreeInfo)):
+            return func.name
+        else:
+            return fn
+
+    def getFunctionPrototype(self, fn):
+        func = self.funcs.get(fn)
+        if (isinstance(func, InterrogateFunction)):
+            proto = func.prototype
+            proto = proto.replace(" inline "," ")
+            if (proto.startswith("inline ")): proto = proto[7:]
+            proto = proto.replace("basic_string< char >", "string")
+            return textToHTML(proto,"")
+        elif (isinstance(func, ParseTreeInfo)):
+            return textToHTML("def "+func.name+func.prototype,"")
+        return fn
+
+    def getFunctionComment(self, fn):
+        func = self.funcs.get(fn)
+        if (isinstance(func, InterrogateFunction)):
+            return textToHTML(func.comment, "/", JUNKHEADER)
+        elif (isinstance(func, ParseTreeInfo)):
+            return textToHTML(func.docstring, "#")
+        return fn
+
+########################################################################
+#
+# The "Class Rename Dictionary" - Yech.
+#
+########################################################################
+
+CLASS_RENAME_DICT = {
+ "Loader"                    : "PandaLoader" ,
+ "String"                    : "CString" ,
+ "LMatrix4f"                 : "Mat4" ,
+ "LMatrix3f"                 : "Mat3" ,
+ "LVecBase4f"                : "VBase4" ,
+ "LVector4f"                 : "Vec4" ,
+ "LPoint4f"                  : "Point4" ,
+ "LVecBase3f"                : "VBase3" ,
+ "LVector3f"                 : "Vec3" ,
+ "LPoint3f"                  : "Point3" ,
+ "LVecBase2f"                : "VBase2" ,
+ "LVector2f"                 : "Vec2" ,
+ "LPoint2f"                  : "Point2" ,
+ "LQuaternionf"              : "Quat" ,
+ "LMatrix4d"                 : "Mat4D" ,
+ "LMatrix3d"                 : "Mat3D" ,
+ "LVecBase4d"                : "VBase4D" ,
+ "LVector4d"                 : "Vec4D" ,
+ "LPoint4d"                  : "Point4D" ,
+ "LVecBase3d"                : "VBase3D" ,
+ "LVector3d"                 : "Vec3D" ,
+ "LPoint3d"                  : "Point3D" ,
+ "LVecBase2d"                : "VBase2D" ,
+ "LVector2d"                 : "Vec2D" ,
+ "LPoint2d"                  : "Point2D" ,
+ "LQuaterniond"              : "QuatD" ,
+ "Plane"                     : "PlaneBase" ,
+ "Planef"                    : "Plane" ,
+ "Planed"                    : "PlaneD" ,
+ "Frustum"                   : "FrustumBase" ,
+ "Frustumf"                  : "Frustum" ,
+ "Frustumd"                  : "FrustumD" 
+}
+
+
+########################################################################
+#
+# Main Program
+#
+########################################################################
+
+def getCodeDatabase():
+    ignore = {}
+    ignore["__init__.py"] = 1
+    ignore["direct/src/directscripts"] = 1
+    ignore["direct/src/extensions"] = 1
+    ignore["direct/src/extensions_native"] = 1
+    ignore["direct/src/ffi"] = 1
+    cxxfiles = []
+    pyfiles = []
+    findFiles("built/pandac/input", ".in", ignore, cxxfiles)
+    findFiles("direct",             ".py", ignore, pyfiles)
+    return CodeDatabase(cxxfiles, pyfiles)
+
+
+def generateFunctionDocs(code, method):
+    name = code.getFunctionName(method)
+    proto = code.getFunctionPrototype(method)
+    comment = code.getFunctionComment(method)
+    if (comment == ""): comment = "Undocumented function.<br>\n"
+    chunk = '<table class="codecomment1" width="100%"><tr><td>' + "\n"
+    chunk = chunk + '<a name="' + name + '"><b>' + name + "</b></a><br>\n"
+    chunk = chunk + proto + "<br>\n"
+    chunk = chunk + comment
+    chunk = chunk + "</td></tr></table>\n"
+    return chunk
+
+def generateDocs(code):
+    dir = "built/pandac/docs"
+    classes = code.getClassList()[:]
+    classes.sort()
+    xclasses = classes[:]
+    for type in classes:
+        print "Generating page for class "+type
+        body = "<h1>" + type + "</h1>\n"
+        comment = code.getClassComment(type)
+        body = body + "<ul>\n" + comment + "</ul>\n\n"
+        inheritance = code.getInheritance(type)
+        body = body + "<h2>Inheritance:</h2>\n<ul>\n"
+        for inh in inheritance:
+            line = "  " + linkToPage(inh,inh) + " : "
+            for parent in code.getClassParents(inh):
+                line = line + linkToPage(parent,parent) + " "
+            body = body + line + "<br>\n"
+        body = body + "</ul>\n"
+        for sclass in inheritance:
+            methods = code.getClassMethods(sclass)[:]
+            methods.sort()
+            if (len(methods) > 0):
+                body = body + "<h2>Methods of "+sclass+":</h2>\n<ul>\n"
+                for method in methods:
+                    fn = code.getFunctionName(method)
+                    body = body + '<a href="#' + fn + '">' + fn + "</a><br>\n"
+                body = body + "</ul>\n"
+        body = body + "</ul>\n"
+        for sclass in inheritance:
+            methods = code.getClassMethods(sclass)[:]
+            methods.sort()
+            for method in methods:
+                body = body + generateFunctionDocs(code, method)
+        writeFile(dir + "/" + type + ".html", body)
+        if (CLASS_RENAME_DICT.has_key(type)):
+            modtype = CLASS_RENAME_DICT[type]
+            writeFile(dir + "/" + modtype + ".html", body)
+            xclasses.append(modtype)
+    xclasses.sort()
+    pversion = getPandaVersion()
+    index = "<h1>List of Classes - Panda " + pversion + "</h1>\n<ul>\n"
+    for type in xclasses:
+        index = index + linkToPage(type,type) + "<br>\n"
+    index = index + "</ul>\n"
+    index = index + "<h1>List of Global Functions - Panda " + pversion + "</h1>\n<ul>\n"
+    fnlist = code.getGlobalFunctionList()[:]
+    fnlist.sort()
+    for func in fnlist:
+        fn = code.getFunctionName(func)
+        index = index + '<a href="#' + fn + '">' + fn + "</a><br>\n"
+    index = index + "</ul>\n"
+    for func in fnlist:
+        index = index + generateFunctionDocs(code, func)
+    writeFile(dir + "/index.html", index)
+
+
+generateDocs(getCodeDatabase())
+