From 0abeb54a28df3ef7028149210dabb9f18c627df4 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 7 Apr 2017 18:30:54 +0200 Subject: [PATCH] Write a documentation generator script --- docs/.gitignore | 4 + docs/css/docs.css | 103 ++++++++++++++++ docs/generate.py | 291 +++++++++++++++++++++++++++++++++++++++++++++ docs/img/arrow.svg | 35 ++++++ 4 files changed, 433 insertions(+) create mode 100644 docs/.gitignore create mode 100644 docs/css/docs.css create mode 100644 docs/generate.py create mode 100644 docs/img/arrow.svg diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..46f739f0 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,4 @@ +constructors/ +functions/ +types/ +core/ diff --git a/docs/css/docs.css b/docs/css/docs.css new file mode 100644 index 00000000..3ac8a3e0 --- /dev/null +++ b/docs/css/docs.css @@ -0,0 +1,103 @@ +body { + font-family: 'Nunito', sans-serif; + color: #333; + background-color:#fff; + font-size: 16px; +} + +a { + color: #42aaed; + text-decoration: none; +} + +pre { + font-family: 'Space Mono', monospace; + padding: 8px; + color: #567; + background: #f0f4f8; + border-radius: 0; + overflow-x: auto; +} + +a:hover { + color: #64bbdd; + text-decoration: underline; +} + +table { + width: 100%; + max-width: 100%; +} + +table td { + border-top: 1px solid #eee; + padding: 8px; +} + +.horizontal { + margin-bottom: 16px; + list-style: none; + background: #f0f4f8; + border-radius: 4px; + padding: 8px 16px; +} + +.horizontal li { + display: inline-block; + margin: 0 8px 0 0; +} + +.horizontal img { + display: inline-block; + margin: 0 8px -2px 0; +} + +h1 { + font-size: 24px; +} + +h3 { + font-size: 20px; +} + +#main_div { + padding: 20px 0; + max-width: 800px; + margin: 0 auto; +} + +pre::-webkit-scrollbar { + visibility: visible; + display: block; + height: 12px; +} + +pre::-webkit-scrollbar-track:horizontal { + background: #def; + border-radius: 0; + height: 12px; +} + +pre::-webkit-scrollbar-thumb:horizontal { + background: #bdd; + border-radius: 0; + height: 12px; +} + +@media (max-width: 640px) { + h1 { + font-size: 18px; + } + h3 { + font-size: 16px; + } + + #dev_page_content_wrap { + padding-top: 12px; + } + + #dev_page_title { + margin-top: 10px; + margin-bottom: 20px; + } +} diff --git a/docs/generate.py b/docs/generate.py new file mode 100644 index 00000000..5f110dbf --- /dev/null +++ b/docs/generate.py @@ -0,0 +1,291 @@ +import os +import re +import sys + +# Small trick so importing telethon_generator works +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from telethon_generator.parser import TLParser, TLObject + + +# TLObject -> hypertext formatted code +def write_code(file, tlobject, filename): + """Writes the code for the given 'tlobject' to the 'file' handle with hyperlinks, + using 'filename' as the file from which the relative links should work""" + + # Write the function or type and its ID + if tlobject.namespace: + file.write(tlobject.namespace) + file.write('.') + + file.write(tlobject.name) + file.write('#') + file.write(hex(tlobject.id)[2:].rjust(8, '0')) + + # Write all the arguments (or do nothing if there's none) + for arg in tlobject.args: + file.write(' ') + + # "Opening" modifiers + if arg.generic_definition: + file.write('{') + + # Argument name + file.write(arg.name) + file.write(':') + + # "Opening" modifiers + if arg.is_flag: + file.write('flags.%d?' % arg.flag_index) + + if arg.is_generic: + file.write('!') + + if arg.is_vector: + file.write('Vector<' % get_path_for_type('vector', relative_to=filename)) + + # Argument type + if arg.type: + file.write('%s' % arg.type) + else: + file.write('#') + + # "Closing" modifiers + if arg.is_vector: + file.write('>') + + if arg.generic_definition: + file.write('}') + + # Now write the resulting type (result from a function, or type for a constructor) + file.write(' = %s' % tlobject.result) + + +# TLObject -> Python class name +def get_class_name(tlobject): + """Gets the class name following the Python style guidelines, in ThisClassFormat""" + # Courtesy of http://stackoverflow.com/a/31531797/4759433 + result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), tlobject.name) + + # Replace '_' with '' once again to make sure it doesn't appear on the name + result = result[:1].upper() + result[1:].replace('_', '') + + # If it's a function, let it end with "Request" to identify them more easily + if tlobject.is_function: + result += 'Request' + + return result + + +# TLObject -> filename +def get_file_name(tlobject, add_extension=False): + """Gets the file name in file_name_format.html for the given TLObject. + Only its name may also be given if the full TLObject is not available""" + if isinstance(tlobject, TLObject): + name = tlobject.name + else: + name = tlobject + + # Courtesy of http://stackoverflow.com/a/1176023/4759433 + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + result = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + if add_extension: + return result + '.html' + else: + return result + + +def get_create_path_for(tlobject): + """Gets the file path (and creates the parent directories) + for the given 'tlobject', relative to nothing; only its local path""" + + # Determine the output directory + out_dir = 'functions' if tlobject.is_function else 'constructors' + + if tlobject.namespace: + out_dir = os.path.join(out_dir, tlobject.namespace) + + # Ensure that it exists + os.makedirs(out_dir, exist_ok=True) + + # Return the resulting filename + return os.path.join(out_dir, get_file_name(tlobject, add_extension=True)) + + +def get_path_for_type(type, relative_to): + """Similar to getting the path for a TLObject, it might not be possible + to have the TLObject itself but rather its name (the type); + this method works in the same way, returning a relative path""" + if type.lower() in {'int', 'long', 'int128', 'int256', 'double', + 'vector', 'string', 'bool', 'true', 'bytes', 'date'}: + path = 'core/index.html#%s' % type.lower() + + elif '.' in type: + # If it's not a core type, then it has to be a custom Telegram type + namespace, name = type.split('.') + path = 'types/%s/%s' % (namespace, get_file_name(name, add_extension=True)) + else: + path = 'types/%s' % get_file_name(type, add_extension=True) + + return get_relative_path(path, relative_to) + + +# Destination path from the current position -> relative to the given path +def get_relative_path(destination, relative_to): + if os.path.isfile(relative_to): + relative_to = os.path.dirname(relative_to) + + return os.path.relpath(destination, start=relative_to) + + +def get_relative_paths(original, relative_to): + """Converts the dictionary of 'original' paths to relative paths + starting from the given 'relative_to' file""" + return {k: get_relative_path(v, relative_to) for k, v in original.items()} + + +def generate_documentation(scheme_file): + """Generates the documentation HTML files from from scheme.tl to /functions and /types""" + original_paths = { + 'css': 'css/docs.css', + 'arrow': 'img/arrow.svg', + 'index_all': 'core/index.html', + 'index_types': 'types/index.html', + 'index_functions': 'functions/index.html', + 'index_constructors': 'constructors/index.html' + } + + tlobjects = tuple(TLParser.parse_file(scheme_file)) + + # First write the functions and the available constructors + for tlobject in tlobjects: + filename = get_create_path_for(tlobject) + + # Determine the relative paths for this file + paths = get_relative_paths(original_paths, relative_to=filename) + + with open(filename, 'w', encoding='utf-8') as file: + file.write(''' + + + + ''') + + # Let the page title be the same as the class name for this object + file.write(get_class_name(tlobject)) + + file.write(''' + + + + + +
+

') + + # Body title, again the class name + file.write(get_class_name(tlobject)) + + file.write('

') + + # Is it listed under functions or under types? + file.write('
---')
+            file.write('functions' if tlobject.is_function else 'types')
+            file.write('---\n')
+
+            write_code(file, tlobject, filename=filename)
+
+            file.write('
') + + file.write('

') + file.write('Parameters' if tlobject.is_function else 'Members') + file.write('

') + + # Sort the arguments in the same way they're sorted on the generated code (flags go last) + args = sorted([a for a in tlobject.args if + not a.flag_indicator and not a.generic_definition], + key=lambda a: a.is_flag) + if args: + # Writing parameters + file.write('') + + for arg in args: + file.write('') + + # Name + file.write('') + + # Type + file.write('' % arg.type) + + # Description + file.write('') + file.write('') + + file.write('
') + file.write(arg.name) + file.write('%s') + if arg.is_vector: + file.write('A list must be supplied for this argument. ') + + if arg.is_generic: + file.write('A different MTProtoRequest must be supplied for this argument. ') + + if arg.is_flag: + file.write('This argument can be omitted. ') + + file.write('
') + else: + if tlobject.is_function: + file.write('

This request takes no input parameters.

') + else: + file.write('

This type has no members.

') + + file.write('
') + + # TODO Explain the difference between functions, types and constructors + # TODO Write the available types, listing the available constructors for each + # TODO Write index.html for every sub-folder (functions/, types/ and constructors/) as well as sub-namespaces + # TODO Write the core/index.html containing the core types + + +if __name__ == '__main__': + print('Generating documentation...') + generate_documentation('../telethon_generator/scheme.tl') + print('Done.') diff --git a/docs/img/arrow.svg b/docs/img/arrow.svg new file mode 100644 index 00000000..1e131224 --- /dev/null +++ b/docs/img/arrow.svg @@ -0,0 +1,35 @@ + + + + + + + image/svg+xml + + + + + + + + +