UltimateZoneBuilder/Source/Plugins/UDBScript/docs/create-docs.py
biwa 5b2b149b40
UDBScript version 5 (#819)
Improved UDBScript to version 5:

- Added Plane class
- Added BlockMap, BlockEntry, and BlackMapQueryResult classes
- Sector class
  - Added getLabelPositions method to get the position of sector labels (where tags, effects etc. are displayed)
- Added support for JavaScript BigInt for UDMF fields. This means it's not necessary anymore to use UniValue to assign integers to new UDMF fields. Instead it can be done like this: sector.fields.my_int_field = 1n;
- Added type information file (udbscript.d.ts)
2022-11-13 01:15:17 +01:00

384 lines
No EOL
16 KiB
Python

from multiprocessing.forkserver import connect_to_new_process
import xmltodict
import glob
import pprint
import re
from pathlib import Path
pp = pprint.PrettyPrinter(indent=4)
def get_param_text_from_xml(data, name):
if isinstance(data['param'], list):
for p in data['param']:
if p['@name'] == name:
return p['#text']
else:
return data['param']['#text']
return '*missing*'
def gen_dts_function(data, isclass):
outstr = f'\t\t/**\n'
if 'summary' in data['xml']:
summary = data['xml']['summary'].split('\n')[0]
outstr += f'\t\t * {summary}\n'
for p in data['parameters']:
outstr += f'\t\t * @param {p["name"]} {get_param_text_from_xml(data["xml"], p["name"])}\n'
if 'returns' in data['xml']:
outstr += f'\t\t * @returns {data["xml"]["returns"]}\n'
outstr += f'\t\t */\n'
outstr += f'\t\t'
if not isclass:
outstr += 'function '
outstr += f'{data["name"]}('
for p in data['parameters']:
outstr += f'{p["name"]}: {convert_type_to_js(p["type"])}'
#if p['default'] is not None:
# outstr += f' = {p["default"]}'
outstr += ', '
if outstr.endswith(', '):
outstr = outstr[:-2]
if data['returntype'] is None:
outstr += ');'
else:
outstr += f'): {convert_type_to_js(data["returntype"])};'
return outstr
def gen_dts_property(data, isclass):
outstr = f'\t\t/**\n'
if 'summary' in data['xml']:
summary = data['xml']['summary'].split('\n')[0]
outstr += f'\t\t * {summary}\n'
outstr += f'\t\t */\n'
outstr += f'\t\t'
if 'fakedtstype' in data['xml']:
returntype = data['xml']['fakedtstype']
else:
returntype = data['returntype']
if not isclass:
outstr += 'let '
outstr += f'{data["name"]}: {convert_type_to_js(returntype)};'
return outstr
def gen_dts_enum(data):
outstr = f'\t\t/**\n'
if 'summary' in data['xml']:
summary = data['xml']['summary'].split('\n')[0]
outstr += f'\t\t * {summary}\n'
outstr += f'\t\t */\n'
outstr += f'\t\tenum {data["name"]} {{\n'
for e in data['xml']['enum']:
outstr += f'\t\t\t/**\n'
outstr += f'\t\t\t * {e["#text"]}\n'
outstr += f'\t\t\t */\n'
outstr += f'\t\t\t{e["@name"]},\n'
outstr += '\t\t}\n'
return outstr
def convert_type_to_js(text):
if '[]' in text:
arr = '[]'
else:
arr = ''
if text == 'double' or text == 'float' or 'int' in text:
return 'number' + arr
elif text == 'bool':
return 'boolean' + arr
elif text == 'object' or text == 'ExpandoObject':
return 'any' + arr
return text
def determine_text_type(text):
#print('----------')
#print(f'text: {text}')
signature = text.replace('public ', '').replace('static ', '').replace('override', '').replace('Wrapper', '')
#print(f'signature: {signature}')
parameters = []
if 'internal' in signature:
return 'internal', None, None, None
if 'private' in signature:
return 'private', None, None, None
if 'class ' in text or 'struct ' in text:
return 'global', None, None, None
if signature.strip().startswith('enum'):
return 'enums', re.sub(r'[^\s]+\s+', r'', signature), None, None
if '(' not in text:
returntype = signature.split(' ', 1)[0].strip()
return 'properties', re.sub(r'[^\s]+\s+', r'', signature).rstrip(';'), None, returntype
signaturefields = signature.split('(')
if signaturefields[1] != ')':
for sf in signaturefields[1].rstrip(')').split(','):
#print(f'### {sf}')
ptype, pname = sf.strip().split(' ', 1)
if '=' in pname:
defaultvalue = pname.split('=')[1].strip()
pname = pname.split('=')[0].strip()
else:
defaultvalue = None
parameters.append({ 'name': pname, 'type': ptype, 'default': defaultvalue })
#print('parametertypes:')
#for pt in parametertypes:
# print(f'\t{pt}')
returntype = signaturefields[0].strip().split(' ')[0]
name = re.sub(r'[^\s]+\s+', r'', signaturefields[0].strip())
#print(f'name: {name}')
#for p in parameters:
# print(f'pname: {p["name"]}, ptype: {p["type"]}')
signature = re.sub(r'[^\s]+\s+', r'', signaturefields[0]) + '(' + re.sub(r'([^\s]+) ([^,]+)(,?\s*)', r'\2\3', signaturefields[1])
#print(f'signature: {signature}')
fields = text.split()
if fields[0] == 'public' and ('Wrapper(' in fields[1] or 'QueryOptions(' in fields[1]):
return 'constructors', name, parameters, returntype
elif fields[1] == 'static':
return 'staticmethods', name, parameters, returntype
return 'methods', name, parameters, returntype
def get_sorted_comment_texts(texts):
text = ''
for t in sorted(texts.keys()):
text += texts[t]
return text
def parse_attributes_line(line, attributes):
mo = re.match(r'\[(.+?)\((.+?)\)\]', line)
if mo is None:
return
attr_name = mo.group(1)
attr_vals = mo.group(2)
if attr_name not in attributes:
attributes[attr_name] = {}
for attr in attr_vals.split(','):
mo = re.match(r'(.+)=(.+)', attr)
if mo is None:
return
attributes[attr_name][mo.group(1).strip()] = mo.group(2).strip()
topics = {
'GameConfiguration': { 'files': [ '../API/GameConfigurationWrapper.cs' ], 'asnamespace': True },
'Angle2D': { 'files': [ '../API/Angle2DWrapper.cs' ], 'asnamespace': True },
'BlockEntry' : { 'files': [ '../API/BlockEntryWrapper.cs' ] },
'BlockMapQueryResult' : { 'files': [ '../API/BlockMapQueryResult.cs' ] },
'BlockMap' : { 'files': [ '../API/BlockMapWrapper.cs' ] },
'Data': { 'files': [ '../API/DataWrapper.cs' ], 'asnamespace': True },
'ImageInfo': { 'files': [ '../API/ImageInfo.cs' ] },
'Line2D': { 'files': [ '../API/Line2DWrapper.cs' ] },
'Linedef': { 'files': [ '../API/LinedefWrapper.cs', '../API/MapElementWrapper.cs' ] },
'Map': { 'files': [ '../API/MapWrapper.cs' ], 'asnamespace': True },
'Plane': { 'files': [ '../API/PlaneWrapper.cs' ]},
'Sector': { 'files': [ '../API/SectorWrapper.cs', '../API/MapElementWrapper.cs' ] },
'Sidedef': { 'files': [ '../API/SidedefWrapper.cs', '../API/MapElementWrapper.cs' ] },
'Thing': { 'files': [ '../API/ThingWrapper.cs', '../API/MapElementWrapper.cs' ] },
'UDB': { 'files': [ '../API/UDBWrapper.cs' ] },
'Vector2D': { 'files': [ '../API/Vector2DWrapper.cs' ] },
'Vector3D': { 'files': [ '../API/Vector3DWrapper.cs' ] },
'Vertex': { 'files': [ '../API/VertexWrapper.cs', '../API/MapElementWrapper.cs' ] },
'VisualCamera': { 'files': [ '../API/VisualCameraWrapper.cs' ] },
'QueryOptions': { 'files': [ '../QueryOptions.cs' ] },
}
dtsdata = {}
for topic in topics:
dtsd = {
'properties': [],
'constructors': [],
'methods': [],
'staticmethods': [],
'enums': []
}
texts = {
'global': '',
'properties': {},
'constructors': {},
'methods': {},
'staticmethods': {},
'enums': {}
}
memberattributes = {}
for filename in topics[topic]['files']:
topicname = filename.split('\\')[-1].replace('Wrapper.cs', '')
with open(filename, 'r') as file:
xmltext = ''
parsingcomment = False
incodeblock = False
for line in file:
line = line.strip()
if line.startswith('['):
parse_attributes_line(line, memberattributes)
elif line.startswith('///'):
parsingcomment = True
line = re.sub(r'^\s', r'', line.lstrip('/'))
if line.startswith('```'):
if incodeblock:
xmltext += '```\n'
incodeblock = False
else:
xmltext += '\n```js\n'
incodeblock = True
else:
xmltext += line + '\n'
elif parsingcomment is True:
commenttext = ''
d = xmltodict.parse('<d>' + xmltext + '</d>')['d']
summary = d['summary']
texttype, signature, parameters, returntype = determine_text_type(line)
if texttype == 'global':
texts['global'] = f'{summary}\n'
elif texttype != 'internal' and texttype != 'private':
if texttype == 'properties':
dtsd['properties'].append({
'xml': d,
'name': signature,
'returntype': returntype
})
elif texttype == 'constructors':
dtsd['constructors'].append({
'xml': d,
'name': 'constructor',
'returntype': None,
'parameters': parameters
})
elif texttype == 'methods':
dtsd['methods'].append({
'xml': d,
'name': signature,
'returntype': returntype,
'parameters': parameters
})
elif texttype == 'staticmethods':
dtsd['staticmethods'].append({
'xml': d,
'name': signature,
'returntype': returntype,
'parameters': parameters
})
elif texttype == 'enums':
dtsd['enums'].append({
'xml': d,
'name': signature
})
commenttext += '\n---\n'
if 'version' in d:
commenttext += f'<span style="float:right;font-weight:normal;font-size:66%">Version: {d["version"]}</span>\n'
if 'UDBScriptSettings' in memberattributes:
commenttext += f'<span style="float:right;font-weight:normal;font-size:66%">Version: {memberattributes["UDBScriptSettings"]["MinVersion"]}</span>\n'
commenttext += f'### {signature}'
if parameters is not None:
commenttext += '('
for param in parameters:
commenttext += f'{param["name"]}: {param["type"]}, '
if commenttext.endswith(', '):
commenttext = commenttext[:-2]
commenttext += ')'
commenttext += '\n'
commenttext += f'{summary}\n'
if 'param' in d:
commenttext += '#### Parameters\n'
if isinstance(d['param'], list):
for p in d['param']:
text = '*missing*'
if '#text' in p:
text = p['#text']
commenttext += f'* {p["@name"]}: {text}\n'
else:
text ='*missing*'
if '#text' in d['param']:
text = d['param']['#text'].replace('```', '\n```\n')
commenttext += f'* {d["param"]["@name"]}: {text}\n'
if 'enum' in d:
commenttext += '#### Options\n'
if isinstance(d['enum'], list):
for p in d['enum']:
text = '*missing*'
if '#text' in p:
text = p['#text']
commenttext += f'* {p["@name"]}: {text}\n'
else:
text ='*missing*'
if '#text' in d['enum']:
text = d['enum']['#text'].replace('```', '\n```\n')
commenttext += f'* {d["enum"]["@name"]}: {text}\n'
if 'returns' in d:
commenttext += '#### Return value\n'
text = '*missing*'
if d['returns'] is not None:
text = d['returns']
commenttext += f'{text}\n'
if signature not in texts[texttype]:
texts[texttype][signature] = ''
texts[texttype][signature] += commenttext
xmltext = ''
memberattributes = {}
parsingcomment = False
dtsdata[topic] = dtsd
outfile = open(f'htmldoc/docs/{topic}.md', 'w')
outfile.write(f'# {topic}\n\n')
outfile.write(f'{texts["global"]}')
if len(texts["constructors"]) > 0:
outfile.write(f'## Constructors\n{get_sorted_comment_texts(texts["constructors"])}')
if len(texts["staticmethods"]) > 0:
outfile.write(f'## Static methods\n{get_sorted_comment_texts(texts["staticmethods"])}')
if len(texts["properties"]) > 0:
outfile.write(f'## Properties\n{get_sorted_comment_texts(texts["properties"])}')
if len(texts["methods"]) > 0:
outfile.write(f'## Methods\n{get_sorted_comment_texts(texts["methods"])}')
if len(texts['enums']) > 0:
outfile.write(f'## Enums\n{get_sorted_comment_texts(texts["enums"])}')
outfile.close()
# Create the .d.ts file
dtsoutstr = 'declare namespace UDB {\n'
for key in dtsdata:
if key == 'UDB':
for m in dtsdata[key]['methods']:
dtsoutstr += (gen_dts_function(m, False) + '\n')[1:]
else:
if 'asnamespace' in topics[key] and topics[key]['asnamespace'] is True:
blocktype = 'namespace'
isclass = False
else:
blocktype = 'class'
isclass = True
if len(dtsdata[key]['constructors']) > 0 or len(dtsdata[key]['methods']) > 0 or len(dtsdata[key]['properties']) > 0:
dtsoutstr += f'\t{blocktype} {key} {{\n'
# constructors
for c in dtsdata[key]['constructors']:
dtsoutstr += gen_dts_function(c, isclass) + '\n'
# methods
for m in dtsdata[key]['methods']:
dtsoutstr += gen_dts_function(m, isclass) + '\n'
# properties
for p in dtsdata[key]['properties']:
if not (p['name'] in topics and p['name'] == p['returntype']):
dtsoutstr += gen_dts_property(p, isclass) + '\n'
else:
print(f'ignoring {p["name"]} in {key} - returntype {p["returntype"]}')
dtsoutstr += '\t}\n'
# static methods and enums
if len(dtsdata[key]['staticmethods']) > 0 or len(dtsdata[key]['enums']) > 0:
dtsoutstr += f'\tnamespace {key} {{\n'
if len(dtsdata[key]['staticmethods']) > 0:
for m in dtsdata[key]['staticmethods']:
dtsoutstr += gen_dts_function(m, False) + '\n'
if len(dtsdata[key]['enums']) > 0:
for e in dtsdata[key]['enums']:
dtsoutstr += gen_dts_enum(e) + '\n'
dtsoutstr += '\t}\n'
dtsoutstr += '}\n'
dtsfile = Path('../../../../Build/UDBScript/udbscript.d.ts')
if not dtsfile.parent.exists():
dtsfile.mkdir(parents=True, exist_ok=True)
dtsfile.write_text(dtsoutstr)