ModelHandle/tools/generate_docs.py
2023-01-14 12:57:00 +01:00

464 lines
17 KiB
Python

import os
import sys
import shutil
import zipfile
import json
import subprocess
import platform
import html
import re
from lib import tools_lib as Tools
class HtmlGenerator:
def __init__ (self, eol):
self.html = ''
self.eol = eol
def AddText (self, content):
self.html += content
def AddLine (self, content):
self.AddText (content + self.eol)
def AddTag (self, tagName, content):
self.AddLine ('<{0}>{1}</{0}>'.format (tagName, content))
def AddTagWithClass (self, tagName, className, content):
self.AddLine ('<{0} class="{1}">{2}</{0}>'.format (tagName, className, content))
def AddTagWithAttributes (self, tagName, attributes, content):
line = '<{0}'.format (tagName);
if len (attributes) > 0:
attributeStrings = map (lambda x : '{0}="{1}"'.format (x[0], x[1]), attributes)
line += ' ' + ' '.join (attributeStrings)
line += '>{1}</{0}>'.format (tagName, content)
self.AddLine (line)
def BeginTag (self, tagName):
self.AddLine ('<{0}>'.format (tagName))
def BeginTagWithClass (self, tagName, className):
self.AddLine ('<{0} class="{1}">'.format (tagName, className))
def EndTag (self, tagName):
self.AddLine ('</{0}>'.format (tagName))
def GetHtml (self):
return self.html
def CleanUpText (text):
if text == None:
return ''
invalidChars = ['\r', '\n', '\t']
for invalidChar in invalidChars:
text = text.replace (invalidChar, ' ')
text = html.escape (text)
return text
def GenerateLink (entityName, entityLink):
target = '_blank' if entityLink.startswith ('http') else '_self'
return '<a href="{1}" target="{2}">{0}</a>'.format (entityName, entityLink, target)
def FinalizeType (text, entityLinks):
text = CleanUpText (text)
arrayMatch = re.match ('Array\.&lt;(.+)&gt', text)
if arrayMatch != None:
matchedName = arrayMatch.group (1)
if matchedName in entityLinks:
return GenerateLink (matchedName, entityLinks[matchedName]) + '[]'
else:
return arrayMatch.group (1) + '[]'
if text in entityLinks:
return GenerateLink (text, entityLinks[text])
return text
def FinalizeDescription (text, entityLinks):
text = CleanUpText (text)
links = re.findall ('{@link (.+)}', text)
for link in links:
if link in entityLinks:
text = text.replace ('{@link ' + link + '}', GenerateLink (link, entityLinks[link]))
else:
text = text.replace ('{@link ' + link + '}', link)
return text
class NavigationGroup:
def __init__ (self, name, sort):
self.name = name
self.sort = sort
self.links = []
def AddLink (self, name, url):
self.links.append ({
'name' : name,
'url' : url
})
class Navigation:
def __init__ (self):
self.groups = []
self.entityLinks = {}
def AddGroup (self, group):
self.groups.append (group)
def AddEntityLink (self, name, url):
self.entityLinks[name] = url
def GenerateHtml (self, eol):
generator = HtmlGenerator (eol)
for group in self.groups:
if len (group.links) == 0:
continue
generator.BeginTagWithClass ('div', 'navigation_section')
generator.AddTagWithClass ('div', 'navigation_title', group.name)
finalLinks = group.links
if group.sort:
finalLinks = sorted (group.links, key = lambda x : x['name'])
for link in finalLinks:
linkHtml = GenerateLink (link['name'], link['url'])
generator.AddTagWithAttributes ('div', [('id', 'nav-' + link['name']), ('class', 'navigation_item')], linkHtml)
generator.EndTag ('div')
return generator.GetHtml ()
class EnumMemberDoc:
def __init__ (self, name, description):
self.name = name
self.description = description
class EnumDoc:
def __init__ (self, name, description):
self.name = name
self.description = description
self.members = []
def AddMember (self, member):
self.members.append (member)
def GenerateHtml (self, navigation, eol):
generator = HtmlGenerator (eol)
generator.AddTag ('h1', self.name)
generator.AddTagWithClass ('div', 'description', FinalizeDescription (self.description, navigation.entityLinks))
if len (self.members) > 0:
generator.AddTag ('h2', 'Values')
for member in self.members:
generator.BeginTagWithClass ('div', 'parameter_header')
generator.AddTagWithClass ('span', 'parameter_name', member.name)
generator.EndTag ('div')
generator.BeginTagWithClass ('div', 'parameter_main')
generator.AddTagWithClass ('div', 'parameter_description', FinalizeDescription (member.description, navigation.entityLinks))
generator.EndTag ('div')
return generator.GetHtml ()
class ParameterDoc:
def __init__ (self, name, types, isOptional, description):
self.name = name
self.types = types
self.isOptional = isOptional
self.description = description
self.subParameters = []
def AddSubParameter (self, parameter):
self.subParameters.append (parameter)
class ReturnsDoc:
def __init__ (self, types, description):
self.types = types
self.description = description
class MethodDoc:
def __init__ (self, name, description, parameters, returns):
self.name = name
self.description = description
self.parameters = parameters
self.returns = returns
def AddParameter (self, parameter):
self.parameters.append (parameter)
def GenerateHtml (self, navigation, eol):
generator = HtmlGenerator (eol)
generator.AddTag ('h1', self.name)
GenerateMethodHtml (self, generator, navigation, False)
return generator.GetHtml ()
class ClassDoc:
def __init__ (self, name, description):
self.name = name
self.description = description
self.constructor = None
self.methods = []
def SetConstructor (self, constructor):
self.constructor = constructor
def AddMethod (self, method):
self.methods.append (method)
def GenerateHtml (self, navigation, eol):
generator = HtmlGenerator (eol)
generator.AddTag ('h1', self.name)
generator.AddTagWithClass ('div', 'description', FinalizeDescription (self.description, navigation.entityLinks))
if self.constructor != None:
generator.AddTag ('h2', 'Constructor')
GenerateMethodHtml (self.constructor, generator, navigation, True)
if len (self.methods) > 0:
generator.AddTag ('h2', 'Methods')
for method in self.methods:
GenerateMethodHtml (method, generator, navigation, False)
return generator.GetHtml ()
def GenerateParameterTypesHtml (paramTypes, generator, navigation):
for i in range (0, len (paramTypes)):
paramType = paramTypes[i]
paramTypeHtml = FinalizeType (paramType, navigation.entityLinks)
generator.AddTagWithClass ('span', 'type parameter_type', paramTypeHtml)
if (i < len (paramTypes) - 1):
generator.AddTagWithClass ('span', 'parameter_type_separator', '|')
def GenerateParameterListHtml (parameters, generator, navigation):
for param in parameters:
generator.BeginTagWithClass ('div', 'parameter_header')
generator.AddTagWithClass ('span', 'parameter_name', param.name)
GenerateParameterTypesHtml (param.types, generator, navigation)
if param.isOptional:
generator.AddTagWithClass ('span', 'parameter_attributes', '(optional)')
generator.EndTag ('div')
generator.BeginTagWithClass ('div', 'parameter_main')
generator.AddTagWithClass ('div', 'parameter_description', FinalizeDescription (param.description, navigation.entityLinks))
if len (param.subParameters) > 0:
GenerateParameterListHtml (param.subParameters, generator, navigation)
generator.EndTag ('div')
def GenerateMethodHtml (method, generator, navigation, isConstructor):
paramNames = map (lambda x : x.name, method.parameters)
methodSignature = method.name + ' (' + ', '.join (paramNames) + ')'
if isConstructor:
methodSignature = 'new ' + methodSignature
generator.BeginTagWithClass ('div', 'method_container')
generator.AddTagWithAttributes ('div', [('id', method.name), ('class', 'method_signature')], methodSignature)
if method.description != None:
generator.AddTagWithClass ('div', 'method_title', 'Description')
generator.AddTagWithClass ('div', 'method_description', FinalizeDescription (method.description, navigation.entityLinks))
if method.parameters != None and len (method.parameters) > 0:
generator.AddTagWithClass ('div', 'method_title', 'Parameters')
GenerateParameterListHtml (method.parameters, generator, navigation)
if method.returns != None:
generator.AddTagWithClass ('div', 'method_title', 'Returns')
generator.BeginTagWithClass ('div', 'method_returns')
if method.returns.types != None:
GenerateParameterTypesHtml (method.returns.types, generator, navigation)
if method.returns.description != None:
generator.AddTagWithClass ('span', 'return_description', FinalizeDescription (method.returns.description, navigation.entityLinks))
generator.EndTag ('div')
generator.EndTag ('div')
def GetDictValue (dict, key):
if not key in dict:
return None
return dict[key]
def GetDocumentedDoclets (doclets):
documented = []
for doclet in doclets:
kind = doclet['kind']
if kind not in ['class', 'function', 'constant', 'member']:
continue
if 'undocumented' in doclet and doclet['undocumented'] == True:
continue
documented.append (doclet)
return documented
def GetParametersFromDoclet (doclet):
parameters = []
paramNamespaceToDoc = {}
if not 'params' in doclet:
return parameters
for param in doclet['params']:
paramName = GetDictValue (param, 'name')
paramIsOptional = 'optional' in param and param['optional'] == True
paramNameParts = paramName.split ('.')
paramTypes = None
if 'type' in param:
paramTypes = param['type']['names']
if len (paramNameParts) == 1:
paramDoc = ParameterDoc (
paramName,
paramTypes,
paramIsOptional,
GetDictValue (param, 'description')
)
parameters.append (paramDoc)
paramNamespaceToDoc[paramName] = paramDoc
else:
paramDoc = ParameterDoc (
paramNameParts[len (paramNameParts) - 1],
paramTypes,
paramIsOptional,
GetDictValue (param, 'description')
)
paramNamespace = '.'.join (paramNameParts[0:-1])
paramNamespaceToDoc[paramNamespace].AddSubParameter (paramDoc)
paramNamespaceToDoc[paramName] = paramDoc
return parameters
def GetReturnsFromDoclet (doclet):
if not 'returns' in doclet:
return None
assert (len (doclet['returns']) == 1)
returns = doclet['returns'][0]
returnType = None
if 'type' in returns:
returnType = returns['type']['names']
return ReturnsDoc (
returnType,
GetDictValue (returns, 'description')
)
def BuildHierarchy (doclets):
hierarchy = {
'classes' : [],
'functions' : [],
'enums' : []
}
classNameToDoc = {}
enumNameToDoc = {}
for doclet in doclets:
kind = doclet['kind']
name = doclet['name']
parameters = GetParametersFromDoclet (doclet)
returns = GetReturnsFromDoclet (doclet)
description = GetDictValue (doclet, 'description')
if kind == 'class':
classDoc = ClassDoc (name, doclet['classdesc'])
constructorDoc = MethodDoc (name, description, parameters, returns)
classDoc.SetConstructor (constructorDoc)
hierarchy['classes'].append (classDoc)
classNameToDoc[name] = classDoc
elif kind == 'function':
if 'memberof' in doclet:
parentName = doclet['memberof']
if parentName in classNameToDoc:
classDoc = classNameToDoc[parentName]
methodDoc = MethodDoc (name, description, parameters, returns)
classDoc.AddMethod (methodDoc)
else:
methodDoc = MethodDoc (name, description, parameters, returns)
hierarchy['functions'].append (methodDoc)
elif kind == 'constant':
if 'isEnum' in doclet and doclet['isEnum'] == True:
enumDoc = EnumDoc (name, description)
hierarchy['enums'].append (enumDoc)
enumNameToDoc[name] = enumDoc
elif kind == 'member':
parentName = doclet['memberof']
if parentName in enumNameToDoc:
enumDoc = enumNameToDoc[parentName]
memberDoc = EnumMemberDoc (name, description)
enumDoc.AddMember (memberDoc)
return hierarchy
def CreateFromTemplate (templateHtmlPath, resultHtmlPath, navigation, title, content, eol):
shutil.copy (templateHtmlPath, resultHtmlPath)
Tools.ReplaceStringsInFile (resultHtmlPath, [
('$$$TITLE$$$', title),
('$$$NAVIGATION$$$', navigation.GenerateHtml (eol)),
('$$$MAIN$$$', content)
])
def BuildNavigation (pageGroups, hierarchy):
navigation = Navigation ()
for pageGroup in pageGroups:
navGroup = NavigationGroup (pageGroup['name'], False)
for page in pageGroup['pages']:
navGroup.AddLink (page['name'], page['url'])
navigation.AddGroup (navGroup)
classesGroup = NavigationGroup ('Classes', True)
for classDoc in hierarchy['classes']:
navigation.AddEntityLink (classDoc.name, classDoc.name + '.html')
classesGroup.AddLink (classDoc.name, classDoc.name + '.html')
navigation.AddGroup (classesGroup)
functionsGroup = NavigationGroup ('Functions', True)
for methodDoc in hierarchy['functions']:
navigation.AddEntityLink (methodDoc.name, methodDoc.name + '.html')
functionsGroup.AddLink (methodDoc.name, methodDoc.name + '.html')
navigation.AddGroup (functionsGroup)
enumsGroup = NavigationGroup ('Enums', True)
for enumDoc in hierarchy['enums']:
navigation.AddEntityLink (enumDoc.name, enumDoc.name + '.html')
enumsGroup.AddLink (enumDoc.name, enumDoc.name + '.html')
navigation.AddGroup (enumsGroup)
return navigation
def BuildDocumentationFiles (navigation, pageGroups, hierarchy, sourceDir, resultDir):
templateHtmlPath = os.path.join (sourceDir, 'Template.html')
eol = Tools.GetEOLCharFromFile (templateHtmlPath)
for pageGroup in pageGroups:
for page in pageGroup['pages']:
if page['url'].startswith ('http'):
continue
sourceHtmlPath = os.path.join (sourceDir, page['url'])
pageHtmlPath = os.path.join (resultDir, page['url'])
pageContent = '<div class="page">' + eol + Tools.GetFileContent (sourceHtmlPath) + eol + '</div>'
CreateFromTemplate (templateHtmlPath, pageHtmlPath, navigation, page['name'], pageContent, eol)
for classDoc in hierarchy['classes']:
classHtmlPath = os.path.join (resultDir, classDoc.name + '.html')
docContent = classDoc.GenerateHtml (navigation, eol)
CreateFromTemplate (templateHtmlPath, classHtmlPath, navigation, classDoc.name, docContent, eol)
for methodDoc in hierarchy['functions']:
methodHtmlPath = os.path.join (resultDir, methodDoc.name + '.html')
docContent = methodDoc.GenerateHtml (navigation, eol)
CreateFromTemplate (templateHtmlPath, methodHtmlPath, navigation, methodDoc.name, docContent, eol)
for enumDoc in hierarchy['enums']:
enumHtmlPath = os.path.join (resultDir, enumDoc.name + '.html')
docContent = enumDoc.GenerateHtml (navigation, eol)
CreateFromTemplate (templateHtmlPath, enumHtmlPath, navigation, enumDoc.name, docContent, eol)
def Main (argv):
toolsDir = os.path.dirname (os.path.abspath (__file__))
rootDir = os.path.dirname (toolsDir)
os.chdir (rootDir)
shell = True
if platform.system () != 'Windows':
shell = False
result = subprocess.run (['jsdoc', '-c', 'tools/jsdoc.json'], stdout = subprocess.PIPE, shell = shell)
resultJson = json.loads (result.stdout)
resultDir = os.path.join (rootDir, 'docs')
sourceDir = os.path.join (resultDir, 'source')
for fileName in os.listdir (resultDir):
filePath = os.path.join (resultDir, fileName)
if not os.path.isdir (filePath):
os.remove (filePath)
config = None
with open (os.path.join (sourceDir, 'config.json')) as configJson:
config = json.load (configJson)
doclets = GetDocumentedDoclets (resultJson)
pageGroups = config['page_groups']
hierarchy = BuildHierarchy (doclets)
navigation = BuildNavigation (pageGroups, hierarchy)
for name in config['external_refs']:
navigation.AddEntityLink (name, config['external_refs'][name])
BuildDocumentationFiles (navigation, pageGroups, hierarchy, sourceDir, resultDir)
return 0
sys.exit (Main (sys.argv))