#!/usr/bin/env python3 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 from __future__ import print_function import urllib.parse import glob import os import sys verbose = '--verbose' in sys.argv dump = '--dump' in sys.argv internal = '--internal' in sys.argv plain_output = '--plain-output' in sys.argv if plain_output: plain_file = open('plain_text_out.txt', 'w+') in_code = None paths = ['include/libtorrent/*.hpp', 'include/libtorrent/kademlia/*.hpp', 'include/libtorrent/extensions/*.hpp'] if internal: paths.append('include/libtorrent/aux_/*.hpp') files = [] for p in paths: files.extend(glob.glob(os.path.join('..', p))) functions = [] classes = [] enums = [] constants = {} # maps filename to overview description overviews = {} # maps names -> URL symbols = {} global orphaned_export # some files that need pre-processing to turn symbols into # links into the reference documentation preprocess_rst = \ { 'manual.rst': 'manual-ref.rst', 'tuning.rst': 'tuning-ref.rst', 'upgrade_to_1.2.rst': 'upgrade_to_1.2-ref.rst', 'settings.rst': 'settings-ref.rst' } # some pre-defined sections from the main manual symbols = \ { "queuing_": "manual-ref.html#queuing", "fast-resume_": "manual-ref.html#fast-resume", "storage-allocation_": "manual-ref.html#storage-allocation", "alerts_": "manual-ref.html#alerts", "upnp-and-nat-pmp_": "manual-ref.html#upnp-and-nat-pmp", "http-seeding_": "manual-ref.html#http-seeding", "metadata-from-peers_": "manual-ref.html#metadata-from-peers", "magnet-links_": "manual-ref.html#magnet-links", "ssl-torrents_": "manual-ref.html#ssl-torrents", "dynamic-loading-of-torrent-files_": "manual-ref.html#dynamic-loading-of-torrent-files", "session-statistics_": "manual-ref.html#session-statistics", "peer-classes_": "manual-ref.html#peer-classes" } # parse out names of settings, and add them to the symbols list, to get cross # references working with open('../src/settings_pack.cpp') as f: for line in f: line = line.strip() if not line.startswith('SET('): continue name = line.split('(')[1].split(',')[0] symbols['settings_pack::' + name] = 'reference-Settings.html#' + name static_links = \ { ".. _`BEP 3`: https://www.bittorrent.org/beps/bep_0003.html", ".. _`BEP 17`: https://www.bittorrent.org/beps/bep_0017.html", ".. _`BEP 19`: https://www.bittorrent.org/beps/bep_0019.html", ".. _`BEP 42`: https://www.bittorrent.org/beps/bep_0042.html", ".. _`rate based choking`: manual-ref.html#rate-based-choking", } anon_index = 0 category_mapping = { 'ed25519.hpp': 'ed25519', 'session.hpp': 'Session', 'session_handle.hpp': 'Session', 'add_torrent_params.hpp': 'Core', 'session_status.hpp': 'Session', 'session_stats.hpp': 'Session', 'session_params.hpp': 'Session', 'error_code.hpp': 'Error Codes', 'storage.hpp': 'Custom Storage', 'storage_defs.hpp': 'Storage', 'file_storage.hpp': 'Storage', 'file_pool.hpp': 'Custom Storage', 'extensions.hpp': 'Plugins', 'ut_metadata.hpp': 'Plugins', 'ut_pex.hpp': 'Plugins', 'ut_trackers.hpp': 'Plugins', 'smart_ban.hpp': 'Plugins', 'create_torrent.hpp': 'Create Torrents', 'alert.hpp': 'Alerts', 'alert_types.hpp': 'Alerts', 'bencode.hpp': 'Bencoding', 'lazy_entry.hpp': 'Bencoding', 'bdecode.hpp': 'Bdecoding', 'entry.hpp': 'Bencoding', 'time.hpp': 'Time', 'escape_string.hpp': 'Utility', 'enum_net.hpp': 'Network', 'broadcast_socket.hpp': 'Network', 'socket.hpp': 'Network', 'address.hpp': 'Network', 'socket_io.hpp': 'Network', 'bitfield.hpp': 'Utility', 'sha1_hash.hpp': 'Utility', 'hasher.hpp': 'Utility', 'hasher512.hpp': 'Utility', 'identify_client.hpp': 'Utility', 'ip_filter.hpp': 'Filter', 'session_settings.hpp': 'Settings', 'settings_pack.hpp': 'Settings', 'operations.hpp': 'Alerts', 'disk_buffer_holder.hpp': 'Custom Storage', 'alert_dispatcher.hpp': 'Alerts', } category_fun_mapping = { 'min_memory_usage()': 'Settings', 'high_performance_seed()': 'Settings', 'cache_status': 'Core', } def categorize_symbol(name, filename): f = os.path.split(filename)[1] if name.endswith('_category()') \ or name.endswith('_error_code') \ or name.endswith('error_code_enum') \ or name.endswith('errors'): return 'Error Codes' if name in category_fun_mapping: return category_fun_mapping[name] if f in category_mapping: return category_mapping[f] if filename.startswith('libtorrent/kademlia/'): return 'DHT' return 'Core' def suppress_warning(filename, name): f = os.path.split(filename)[1] if f != 'alert_types.hpp': return False # if name.endswith('_alert') or name == 'message()': return True # return False def first_item(itr): for i in itr: return i return None def is_visible(desc): if desc.strip().startswith('hidden'): return False if internal: return True if desc.strip().startswith('internal'): return False return True def highlight_signature(s): name = s.split('(', 1) name2 = name[0].split(' ') if len(name2[-1]) == 0: return s # make the name of the function bold name2[-1] = '**' + name2[-1] + '** ' # if there is a return value, make sure we preserve pointer types if len(name2) > 1: name2[0] = name2[0].replace('*', '\\*') name[0] = ' '.join(name2) # we have to escape asterisks, since this is rendered into # a parsed literal in rst name[1] = name[1].replace('*', '\\*') # we also have to escape colons name[1] = name[1].replace(':', '\\:') # escape trailing underscores name[1] = name[1].replace('_', '\\_') # comments in signatures are italic name[1] = name[1].replace('/\\*', '*/\\*') name[1] = name[1].replace('\\*/', '\\*/*') return '('.join(name) def highlight_name(s): if '=' in s: splitter = ' = ' elif '{' in s: splitter = '{' else: return s name = s.split(splitter, 1) name2 = name[0].split(' ') if len(name2[-1]) == 0: return s name2[-1] = '**' + name2[-1] + '** ' name[0] = ' '.join(name2) return splitter.join(name) def html_sanitize(s): ret = '' for i in s: if i == '<': ret += '<' elif i == '>': ret += '>' elif i == '&': ret += '&' else: ret += i return ret def looks_like_namespace(line): line = line.strip() if line.startswith('namespace'): return True return False def looks_like_blank(line): line = line.split('//')[0] line = line.replace('{', '') line = line.replace('}', '') line = line.replace('[', '') line = line.replace(']', '') line = line.replace(';', '') line = line.strip() return len(line) == 0 def looks_like_variable(line): line = line.split('//')[0] line = line.strip() if ' ' not in line and '\t' not in line: return False if line.startswith('friend '): return False if line.startswith('enum '): return False if line.startswith(','): return False if line.startswith(':'): return False if line.startswith('typedef'): return False if line.startswith('using'): return False if ' = ' in line: return True if line.endswith(';'): return True return False def looks_like_constant(line): line = line.strip() if line.startswith('inline'): line = line.split('inline')[1] line = line.strip() if not line.startswith('constexpr'): return False line = line.split('constexpr')[1] return looks_like_variable(line) def looks_like_forward_decl(line): line = line.split('//')[0] line = line.strip() if not line.endswith(';'): return False if '{' in line: return False if '}' in line: return False if line.startswith('friend '): return True if line.startswith('struct '): return True if line.startswith('class '): return True return False def looks_like_function(line): if line.startswith('friend'): return False if '::' in line.split('(')[0].split(' ')[-1]: return False if line.startswith(','): return False if line.startswith(':'): return False return '(' in line def parse_function(lno, lines, filename): start_paren = 0 end_paren = 0 signature = '' global orphaned_export orphaned_export = False while lno < len(lines): line = lines[lno].strip() lno += 1 if line.startswith('//'): continue start_paren += line.count('(') end_paren += line.count(')') sig_line = line.replace('TORRENT_EXPORT ', '') \ .replace('TORRENT_EXTRA_EXPORT', '') \ .replace('TORRENT_COUNTER_NOEXCEPT', '').strip() if signature != '': sig_line = '\n ' + sig_line signature += sig_line if verbose: print('fun %s' % line) if start_paren > 0 and start_paren == end_paren: if signature[-1] != ';': # we also need to consume the function body start_paren = 0 end_paren = 0 for i in range(len(signature)): if signature[i] == '(': start_paren += 1 elif signature[i] == ')': end_paren += 1 if start_paren > 0 and start_paren == end_paren: for k in range(i, len(signature)): if signature[k] == ':' or signature[k] == '{': signature = signature[0:k].strip() break break lno = consume_block(lno - 1, lines) signature += ';' ret = [{'file': filename[11:], 'signatures': set([signature]), 'names': set( [signature.split('(')[0].split(' ')[-1].strip() + '()'])}, lno] if first_item(ret[0]['names']) == '()': return [None, lno] return ret if len(signature) > 0: print('\x1b[31mFAILED TO PARSE FUNCTION\x1b[0m %s\nline: %d\nfile: %s' % (signature, lno, filename)) return [None, lno] def add_desc(line): # plain output prints just descriptions and filters out c++ code. # it's used to run spell checker over if plain_output: for s in line.split('\n'): # if the first character is a space, strip it if len(s) > 0 and s[0] == ' ': s = s[1:] global in_code if in_code is not None and not s.startswith(in_code) and len(s) > 1: in_code = None if s.strip().startswith('.. code::'): in_code = s.split('.. code::')[0] + '\t' # strip out C++ code from the plain text output since it's meant for # running spell checking over if not s.strip().startswith('.. ') and in_code is None: plain_file.write(s + '\n') def parse_class(lno, lines, filename): start_brace = 0 end_brace = 0 name = '' funs = [] fields = [] enums = [] state = 'public' context = '' class_type = 'struct' blanks = 0 decl = '' while lno < len(lines): line = lines[lno].strip() decl += lines[lno].replace('TORRENT_EXPORT ', '') \ .replace('TORRENT_EXTRA_EXPORT', '') \ .replace('TORRENT_COUNTER_NOEXCEPT', '').split('{')[0].strip() if '{' in line: break if verbose: print('class %s' % line) lno += 1 if decl.startswith('class'): state = 'private' class_type = 'class' name = decl.split(':')[0].replace('class ', '').replace('struct ', '').replace('final', '').strip() global orphaned_export orphaned_export = False while lno < len(lines): line = lines[lno].strip() lno += 1 if line == '': blanks += 1 context = '' continue if line.startswith('/*'): lno = consume_comment(lno - 1, lines) continue if line.startswith('#'): lno = consume_ifdef(lno - 1, lines, True) continue if 'TORRENT_DEFINE_ALERT' in line: if verbose: print('xx %s' % line) blanks += 1 continue if 'TORRENT_DEPRECATED' in line: if verbose: print('xx %s' % line) blanks += 1 continue if line.startswith('//'): if verbose: print('desc %s' % line) line = line[2:] if len(line) and line[0] == ' ': line = line[1:] context += line + '\n' continue start_brace += line.count('{') end_brace += line.count('}') if line == 'private:': state = 'private' elif line == 'protected:': state = 'protected' elif line == 'public:': state = 'public' if start_brace > 0 and start_brace == end_brace: return [{'file': filename[11:], 'enums': enums, 'fields':fields, 'type': class_type, 'name': name, 'decl': decl, 'fun': funs}, lno] if state != 'public' and not internal: if verbose: print('private %s' % line) blanks += 1 continue if start_brace - end_brace > 1: if verbose: print('scope %s' % line) blanks += 1 continue if looks_like_function(line): current_fun, lno = parse_function(lno - 1, lines, filename) if current_fun is not None and is_visible(context): if context == '' and blanks == 0 and len(funs): funs[-1]['signatures'].update(current_fun['signatures']) funs[-1]['names'].update(current_fun['names']) else: if 'TODO: ' in context: print('TODO comment in public documentation: %s:%d' % (filename, lno)) sys.exit(1) current_fun['desc'] = context add_desc(context) if context == '' and not suppress_warning(filename, first_item(current_fun['names'])): print('WARNING: member function "%s" is not documented: \x1b[34m%s:%d\x1b[0m' % (name + '::' + first_item(current_fun['names']), filename, lno)) funs.append(current_fun) context = '' blanks = 0 continue if looks_like_variable(line): if 'constexpr static' in line: print('ERROR: found "constexpr static", use "static constexpr" instead for consistency!\n%s:%d\n%s' % (filename, lno, line)) sys.exit(1) if verbose: print('var %s' % line) if not is_visible(context): continue line = line.split('//')[0].strip() # the name may look like this: # std::uint8_t fails : 7; # int scrape_downloaded = -1; # static constexpr peer_flags_t interesting{0x1}; n = line.split('=')[0].split('{')[0].strip().split(' : ')[0].split(' ')[-1].split(':')[0].split(';')[0] if context == '' and blanks == 0 and len(fields): fields[-1]['names'].append(n) fields[-1]['signatures'].append(line) else: if context == '' and not suppress_warning(filename, n): print('WARNING: field "%s" is not documented: \x1b[34m%s:%d\x1b[0m' % (name + '::' + n, filename, lno)) add_desc(context) fields.append({'signatures': [line], 'names': [n], 'desc': context}) context = '' blanks = 0 continue if line.startswith('enum '): if verbose: print('enum %s' % line) if not is_visible(context): consume_block(lno - 1, lines) else: enum, lno = parse_enum(lno - 1, lines, filename) if enum is not None: if 'TODO: ' in context: print('TODO comment in public documentation: %s:%d' % (filename, lno)) sys.exit(1) enum['desc'] = context add_desc(context) if context == '' and not suppress_warning(filename, enum['name']): print('WARNING: enum "%s" is not documented: \x1b[34m%s:%d\x1b[0m' % (name + '::' + enum['name'], filename, lno)) enums.append(enum) context = '' continue context = '' if verbose: if looks_like_forward_decl(line) \ or looks_like_blank(line) \ or looks_like_namespace(line): print('-- %s' % line) else: print('?? %s' % line) if len(name) > 0: print('\x1b[31mFAILED TO PARSE CLASS\x1b[0m %s\nfile: %s:%d' % (name, filename, lno)) return [None, lno] def parse_constant(lno, lines, filename): line = lines[lno].strip() if verbose: print('const %s' % line) line = line.split('=')[0] if 'constexpr' in line: line = line.split('constexpr')[1] if '{' in line and '}' in line: line = line.split('{')[0] t, name = line.strip().split(' ') return [{'file': filename[11:], 'type': t, 'name': name}, lno + 1] def parse_enum(lno, lines, filename): start_brace = 0 end_brace = 0 global anon_index line = lines[lno].strip() name = line.replace('enum ', '').replace('class ', '').split(':')[0].split('{')[0].strip() if len(name) == 0: if not internal: print('WARNING: anonymous enum at: \x1b[34m%s:%d\x1b[0m' % (filename, lno)) lno = consume_block(lno - 1, lines) return [None, lno] name = 'anonymous_enum_%d' % anon_index anon_index += 1 values = [] context = '' if '{' not in line: if verbose: print('enum %s' % lines[lno]) lno += 1 val = 0 while lno < len(lines): line = lines[lno].strip() lno += 1 if line.startswith('//'): if verbose: print('desc %s' % line) line = line[2:] if len(line) and line[0] == ' ': line = line[1:] context += line + '\n' continue if line.startswith('#'): lno = consume_ifdef(lno - 1, lines) continue start_brace += line.count('{') end_brace += line.count('}') if '{' in line: line = line.split('{')[1] line = line.split('}')[0] if len(line): if verbose: print('enumv %s' % lines[lno - 1]) for v in line.split(','): v = v.strip() if v.startswith('//'): break if v == '': continue valstr = '' try: if '=' in v: val = int(v.split('=')[1].strip(), 0) valstr = str(val) except Exception: pass if '=' in v: v = v.split('=')[0].strip() if is_visible(context): add_desc(context) values.append({'name': v.strip(), 'desc': context, 'val': valstr}) if verbose: print('enumv %s' % valstr) context = '' val += 1 else: if verbose: print('?? %s' % lines[lno - 1]) if start_brace > 0 and start_brace == end_brace: return [{'file': filename[11:], 'name': name, 'values': values}, lno] if len(name) > 0: print('\x1b[31mFAILED TO PARSE ENUM\x1b[0m %s\nline: %d\nfile: %s' % (name, lno, filename)) return [None, lno] def consume_block(lno, lines): start_brace = 0 end_brace = 0 while lno < len(lines): line = lines[lno].strip() if verbose: print('xx %s' % line) lno += 1 start_brace += line.count('{') end_brace += line.count('}') if start_brace > 0 and start_brace == end_brace: break return lno def consume_comment(lno, lines): while lno < len(lines): line = lines[lno].strip() if verbose: print('xx %s' % line) lno += 1 if '*/' in line: break return lno def trim_define(line): return line.replace('#ifndef', '').replace('#ifdef', '') \ .replace('#if', '').replace('defined', '') \ .replace('TORRENT_ABI_VERSION == 1', '') \ .replace('||', '').replace('&&', '').replace('(', '').replace(')', '') \ .replace('!', '').replace('\\', '').strip() def consume_ifdef(lno, lines, warn_on_ifdefs=False): line = lines[lno].strip() lno += 1 start_if = 1 end_if = 0 if verbose: print('prep %s' % line) if warn_on_ifdefs and line.strip().startswith('#if'): while line.endswith('\\'): lno += 1 line += lines[lno].strip() if verbose: print('prep %s' % lines[lno].trim()) define = trim_define(line) if 'TORRENT_' in define and 'TORRENT_ABI_VERSION' not in define: print('\x1b[31mWARNING: possible ABI breakage in public struct! "%s" \x1b[34m %s:%d\x1b[0m' % (define, filename, lno)) # we've already warned once, no need to do it twice warn_on_ifdefs = False elif define != '': print('\x1b[33msensitive define in public struct: "%s"\x1b[34m %s:%d\x1b[0m' % (define, filename, lno)) if (line.startswith('#if') and ( ' TORRENT_USE_ASSERTS' in line or ' TORRENT_USE_INVARIANT_CHECKS' in line or ' TORRENT_ASIO_DEBUGGING' in line) or line == '#if TORRENT_ABI_VERSION == 1'): while lno < len(lines): line = lines[lno].strip() lno += 1 if verbose: print('prep %s' % line) if line.startswith('#endif'): end_if += 1 if line.startswith('#if'): start_if += 1 if line == '#else' and start_if - end_if == 1: break if start_if - end_if == 0: break return lno else: while line.endswith('\\') and lno < len(lines): line = lines[lno].strip() lno += 1 if verbose: print('prep %s' % line) return lno for filename in files: h = open(filename) lines = h.read().split('\n') if verbose: print('\n=== %s ===\n' % filename) blanks = 0 lno = 0 global orphaned_export orphaned_export = False while lno < len(lines): line = lines[lno].strip() if orphaned_export: print('ERROR: TORRENT_EXPORT without function or class!\n%s:%d\n%s' % (filename, lno, line)) sys.exit(1) lno += 1 if line == '': blanks += 1 context = '' continue if 'TORRENT_EXPORT' in line.split() \ and 'ifndef TORRENT_EXPORT' not in line \ and 'define TORRENT_DEPRECATED_EXPORT TORRENT_EXPORT' not in line \ and 'define TORRENT_EXPORT' not in line \ and 'for TORRENT_EXPORT' not in line \ and 'TORRENT_EXPORT TORRENT_CFG' not in line \ and 'extern TORRENT_EXPORT ' not in line \ and 'struct TORRENT_EXPORT ' not in line: orphaned_export = True if verbose: print('maybe orphaned: %s\n' % line) if line.startswith('//') and line[2:].strip() == 'OVERVIEW': # this is a section overview current_overview = '' while lno < len(lines): line = lines[lno].strip() lno += 1 if not line.startswith('//'): # end of overview overviews[filename[11:]] = current_overview current_overview = '' break line = line[2:] if line.startswith(' '): line = line[1:] current_overview += line + '\n' if line.startswith('//'): if verbose: print('desc %s' % line) line = line[2:] if len(line) and line[0] == ' ': line = line[1:] context += line + '\n' continue if line.startswith('/*'): lno = consume_comment(lno - 1, lines) continue if line.startswith('#'): lno = consume_ifdef(lno - 1, lines) continue if (line == 'namespace detail {' or line == 'namespace aux {' or line == 'namespace libtorrent { namespace aux {') \ and not internal: lno = consume_block(lno - 1, lines) continue if ('namespace aux' in line or 'namespace detail' in line) and \ '//' not in line.split('namespace')[0] and \ '}' not in line.split('namespace')[1]: print('ERROR: whitespace preceding namespace declaration: %s:%d' % (filename, lno)) sys.exit(1) if 'TORRENT_DEPRECATED' in line: if ('class ' in line or 'struct ' in line) and ';' not in line: lno = consume_block(lno - 1, lines) context = '' blanks += 1 if verbose: print('xx %s' % line) continue if looks_like_constant(line): if 'constexpr static' in line: print('ERROR: found "constexpr static", use "static constexpr" instead for consistency!\n%s:%d\n%s' % (filename, lno, line)) sys.exit(1) current_constant, lno = parse_constant(lno - 1, lines, filename) if current_constant is not None and is_visible(context): if 'TODO: ' in context: print('TODO comment in public documentation: %s:%d' % (filename, lno)) sys.exit(1) current_constant['desc'] = context add_desc(context) if context == '': print('WARNING: constant "%s" is not documented: \x1b[34m%s:%d\x1b[0m' % (current_constant['name'], filename, lno)) t = current_constant['type'] if t in constants: constants[t].append(current_constant) else: constants[t] = [current_constant] continue if 'TORRENT_EXPORT ' in line or line.startswith('inline ') or line.startswith('template') or internal: if line.startswith('class ') or line.startswith('struct '): if not line.endswith(';'): current_class, lno = parse_class(lno - 1, lines, filename) if current_class is not None and is_visible(context): if 'TODO: ' in context: print('TODO comment in public documentation: %s:%d' % (filename, lno)) sys.exit(1) current_class['desc'] = context add_desc(context) if context == '': print('WARNING: class "%s" is not documented: \x1b[34m%s:%d\x1b[0m' % (current_class['name'], filename, lno)) classes.append(current_class) context = '' blanks += 1 continue if looks_like_function(line): current_fun, lno = parse_function(lno - 1, lines, filename) if current_fun is not None and is_visible(context): if context == '' and blanks == 0 and len(functions): functions[-1]['signatures'].update(current_fun['signatures']) functions[-1]['names'].update(current_fun['names']) else: if 'TODO: ' in context: print('TODO comment in public documentation: %s:%d' % (filename, lno)) sys.exit(1) current_fun['desc'] = context add_desc(context) if context == '': print('WARNING: function "%s" is not documented: \x1b[34m%s:%d\x1b[0m' % (first_item(current_fun['names']), filename, lno)) functions.append(current_fun) context = '' blanks = 0 continue if ('enum class ' not in line and 'class ' in line or 'struct ' in line) and ';' not in line: lno = consume_block(lno - 1, lines) context = '' blanks += 1 continue if line.startswith('enum '): if not is_visible(context): consume_block(lno - 1, lines) else: current_enum, lno = parse_enum(lno - 1, lines, filename) if current_enum is not None and is_visible(context): if 'TODO: ' in context: print('TODO comment in public documentation: %s:%d' % (filename, lno)) sys.exit(1) current_enum['desc'] = context add_desc(context) if context == '': print('WARNING: enum "%s" is not documented: \x1b[34m%s:%d\x1b[0m' % (current_enum['name'], filename, lno)) enums.append(current_enum) context = '' blanks += 1 continue blanks += 1 if verbose: if looks_like_forward_decl(line) \ or looks_like_blank(line) \ or looks_like_namespace(line): print('-- %s' % line) else: print('?? %s' % line) context = '' h.close() # ==================================================================== # # RENDER PART # # ==================================================================== def new_category(cat): return {'classes': [], 'functions': [], 'enums': [], 'filename': 'reference-%s.rst' % cat.replace(' ', '_'), 'constants': {}} if dump: if verbose: print('\n===============================\n') for c in classes: print('\x1b[4m%s\x1b[0m %s\n{' % (c['type'], c['name'])) for f in c['fun']: for s in f['signatures']: print(' %s' % s.replace('\n', '\n ')) if len(c['fun']) > 0 and len(c['fields']) > 0: print('') for f in c['fields']: for s in f['signatures']: print(' %s' % s) if len(c['fields']) > 0 and len(c['enums']) > 0: print('') for e in c['enums']: print(' \x1b[4menum\x1b[0m %s\n {' % e['name']) for v in e['values']: print(' %s' % v['name']) print(' };') print('};\n') for f in functions: print('%s' % f['signature']) for e in enums: print('\x1b[4menum\x1b[0m %s\n{' % e['name']) for v in e['values']: print(' %s' % v['name']) print('};') for t, c in constants: print('\x1b[4mconstant\x1b[0m %s %s\n' % (e['type'], e['name'])) categories = {} for c in classes: cat = categorize_symbol(c['name'], c['file']) if cat not in categories: categories[cat] = new_category(cat) if c['file'] in overviews: categories[cat]['overview'] = overviews[c['file']] filename = categories[cat]['filename'].replace('.rst', '.html') + '#' categories[cat]['classes'].append(c) symbols[c['name']] = filename + c['name'] for f in c['fun']: for n in f['names']: symbols[n] = filename + n symbols[c['name'] + '::' + n] = filename + n for f in c['fields']: for n in f['names']: symbols[c['name'] + '::' + n] = filename + n for e in c['enums']: symbols[e['name']] = filename + e['name'] symbols[c['name'] + '::' + e['name']] = filename + e['name'] for v in e['values']: # symbols[v['name']] = filename + v['name'] symbols[e['name'] + '::' + v['name']] = filename + v['name'] symbols[c['name'] + '::' + v['name']] = filename + v['name'] for f in functions: cat = categorize_symbol(first_item(f['names']), f['file']) if cat not in categories: categories[cat] = new_category(cat) if f['file'] in overviews: categories[cat]['overview'] = overviews[f['file']] for n in f['names']: symbols[n] = categories[cat]['filename'].replace('.rst', '.html') + '#' + n categories[cat]['functions'].append(f) for e in enums: cat = categorize_symbol(e['name'], e['file']) if cat not in categories: categories[cat] = new_category(cat) categories[cat]['enums'].append(e) filename = categories[cat]['filename'].replace('.rst', '.html') + '#' symbols[e['name']] = filename + e['name'] for v in e['values']: symbols[e['name'] + '::' + v['name']] = filename + v['name'] for t, c in constants.items(): for const in c: cat = categorize_symbol(t, const['file']) if cat not in categories: categories[cat] = new_category(cat) if t not in categories[cat]['constants']: categories[cat]['constants'][t] = [const] else: categories[cat]['constants'][t].append(const) filename = categories[cat]['filename'].replace('.rst', '.html') + '#' symbols[t + '::' + const['name']] = filename + t + '::' + const['name'] symbols[t] = filename + t def print_declared_in(out, o): out.write('Declared in "%s"\n\n' % print_link(o['file'], '../include/%s' % o['file'])) print(dump_link_targets(), file=out) # returns RST marked up string def linkify_symbols(string): lines = string.split('\n') ret = [] in_literal = False lno = 0 return_string = '' for line in lines: lno += 1 # don't touch headlines, i.e. lines whose # next line entirely contains one of =, - or . if (lno < len(lines) - 1): next_line = lines[lno] else: next_line = '' if '.. include:: ' in line: return_string += '\n'.join(ret) ret = [line] return_string += dump_link_targets() + '\n' continue if len(next_line) > 0 and lines[lno].replace('=', ''). \ replace('-', '').replace('.', '') == '': ret.append(line) continue if line.startswith('|'): ret.append(line) continue if in_literal and not line.startswith('\t') and not line == '': # print(' end literal: "%s"' % line) in_literal = False if in_literal: # print(' literal: "%s"' % line) ret.append(line) continue if line.strip() == '.. parsed-literal::' or \ line.strip().startswith('.. code::') or \ (not line.strip().startswith('..') and line.endswith('::')): # print(' start literal: "%s"' % line) in_literal = True words = line.split(' ') for i in range(len(words)): # it's important to preserve leading # tabs, since that's relevant for # rst markup leading = '' w = words[i] if len(w) == 0: continue while len(w) > 0 and \ w[0] in ['\t', ' ', '(', '[', '{']: leading += w[0] w = w[1:] # preserve commas and dots at the end w = w.strip() trailing = '' if len(w) == 0: continue while len(w) > 1 and w[-1] in ['.', ',', ')'] and w[-2:] != '()': trailing = w[-1] + trailing w = w[:-1] link_name = w # print(w) if len(w) == 0: continue if link_name[-1] == '_': link_name = link_name[:-1] if w in symbols: link_name = link_name.replace('-', ' ') # print(' found %s -> %s' % (w, link_name)) words[i] = leading + print_link(link_name, symbols[w]) + trailing ret.append(' '.join(words)) return_string += '\n'.join(ret) return return_string link_targets = [] def print_link(name, target): global link_targets link_targets.append(target) return "`%s`__" % name def dump_link_targets(indent=''): global link_targets ret = '\n' for link in link_targets: ret += '%s__ %s\n' % (indent, link) link_targets = [] return ret def heading(string, c, indent=''): string = string.strip() return '\n' + indent + string + '\n' + indent + (c * len(string)) + '\n' def render_enums(out, enums, print_declared_reference, header_level): for e in enums: print('.. raw:: html\n', file=out) print('\t' % e['name'], file=out) print('', file=out) dump_report_issue('enum ' + e['name'], out) print(heading('enum %s' % e['name'], header_level), file=out) print_declared_in(out, e) width = [len('name'), len('value'), len('description')] for i in range(len(e['values'])): e['values'][i]['desc'] = linkify_symbols(e['values'][i]['desc']) for v in e['values']: width[0] = max(width[0], len(v['name'])) width[1] = max(width[1], len(v['val'])) for d in v['desc'].split('\n'): width[2] = max(width[2], len(d)) print('+-' + ('-' * width[0]) + '-+-' + ('-' * width[1]) + '-+-' + ('-' * width[2]) + '-+', file=out) print('| ' + 'name'.ljust(width[0]) + ' | ' + 'value'.ljust(width[1]) + ' | ' + 'description'.ljust(width[2]) + ' |', file=out) print('+=' + ('=' * width[0]) + '=+=' + ('=' * width[1]) + '=+=' + ('=' * width[2]) + '=+', file=out) for v in e['values']: d = v['desc'].split('\n') if len(d) == 0: d = [''] print('| ' + v['name'].ljust(width[0]) + ' | ' + v['val'].ljust(width[1]) + ' | ' + d[0].ljust(width[2]) + ' |', file=out) for s in d[1:]: print('| ' + (' ' * width[0]) + ' | ' + (' ' * width[1]) + ' | ' + s.ljust(width[2]) + ' |', file=out) print('+-' + ('-' * width[0]) + '-+-' + ('-' * width[1]) + '-+-' + ('-' * width[2]) + '-+', file=out) print('', file=out) print(dump_link_targets(), file=out) sections = \ { 'Core': 0, 'DHT': 0, 'Session': 0, 'Settings': 0, 'Bencoding': 1, 'Bdecoding': 1, 'Filter': 1, 'Error Codes': 1, 'Create Torrents': 1, 'ed25519': 2, 'Utility': 2, 'Storage': 2, 'Custom Storage': 2, 'Plugins': 2, 'Alerts': 3 } def print_toc(out, categories, s): for cat in categories: if (s != 2 and cat not in sections) or \ (cat in sections and sections[cat] != s): continue print('\t.. rubric:: %s\n' % cat, file=out) if 'overview' in categories[cat]: print('\t| overview__', file=out) for c in categories[cat]['classes']: print('\t| ' + print_link(c['name'], symbols[c['name']]), file=out) for f in categories[cat]['functions']: for n in f['names']: print('\t| ' + print_link(n, symbols[n]), file=out) for e in categories[cat]['enums']: print('\t| ' + print_link(e['name'], symbols[e['name']]), file=out) for t, c in categories[cat]['constants'].items(): print('\t| ' + print_link(t, symbols[t]), file=out) print('', file=out) if 'overview' in categories[cat]: print('\t__ %s#overview' % categories[cat]['filename'].replace('.rst', '.html'), file=out) print(dump_link_targets('\t'), file=out) def dump_report_issue(h, out): print(('.. raw:: html\n\n\t[report issue]\n\n').format( urllib.parse.quote_plus(h), urllib.parse.quote_plus('Documentation under heading "' + h + '" could be improved')), file=out) out = open('reference.rst', 'w+') out.write('''======================= reference documentation ======================= ''') out.write('`single-page version`__\n\n__ single-page-ref.html\n\n') for i in range(4): out.write('.. container:: main-toc\n\n') print_toc(out, categories, i) out.close() for cat in categories: out = open(categories[cat]['filename'], 'w+') classes = categories[cat]['classes'] functions = categories[cat]['functions'] enums = categories[cat]['enums'] constants = categories[cat]['constants'] out.write('''.. include:: header.rst `home`__ __ reference.html %s .. contents:: Table of contents :depth: 2 :backlinks: none ''' % heading(cat, '=')) if 'overview' in categories[cat]: out.write('%s\n' % linkify_symbols(categories[cat]['overview'])) for c in classes: print('.. raw:: html\n', file=out) print('\t' % c['name'], file=out) print('', file=out) dump_report_issue('class ' + c['name'], out) out.write('%s\n' % heading(c['name'], '-')) print_declared_in(out, c) c['desc'] = linkify_symbols(c['desc']) out.write('%s\n' % c['desc']) print(dump_link_targets(), file=out) print('\n.. parsed-literal::\n\t', file=out) block = '\n%s\n{\n' % c['decl'] for f in c['fun']: for s in f['signatures']: block += ' %s\n' % highlight_signature(s.replace('\n', '\n ')) if len(c['fun']) > 0 and len(c['enums']) > 0: block += '\n' first = True for e in c['enums']: if not first: block += '\n' first = False block += ' enum %s\n {\n' % e['name'] for v in e['values']: block += ' %s,\n' % v['name'] block += ' };\n' if len(c['fun']) + len(c['enums']) > 0 and len(c['fields']): block += '\n' for f in c['fields']: for s in f['signatures']: block += ' %s\n' % highlight_name(s) block += '};' print(block.replace('\n', '\n\t') + '\n', file=out) for f in c['fun']: if f['desc'] == '': continue print('.. raw:: html\n', file=out) for n in f['names']: print('\t' % n, file=out) print('', file=out) h = ' '.join(f['names']) dump_report_issue('%s::[%s]' % (c['name'], h), out) print(heading(h, '.'), file=out) block = '.. parsed-literal::\n\n' for s in f['signatures']: block += highlight_signature(s.replace('\n', '\n ')) + '\n' print('%s\n' % block.replace('\n', '\n\t'), file=out) f['desc'] = linkify_symbols(f['desc']) print('%s' % f['desc'], file=out) print(dump_link_targets(), file=out) render_enums(out, c['enums'], False, '.') for f in c['fields']: if f['desc'] == '': continue print('.. raw:: html\n', file=out) for n in f['names']: print('\t' % n, file=out) print('', file=out) h = ' '.join(f['names']) dump_report_issue('%s::[%s]' % (c['name'], h), out) print(h, file=out) f['desc'] = linkify_symbols(f['desc']) print('\t%s' % f['desc'].replace('\n', '\n\t'), file=out) print(dump_link_targets(), file=out) for f in functions: print('.. raw:: html\n', file=out) for n in f['names']: print('\t' % n, file=out) print('', file=out) h = ' '.join(f['names']) dump_report_issue(h, out) print(heading(h, '-'), file=out) print_declared_in(out, f) block = '.. parsed-literal::\n\n' for s in f['signatures']: block += highlight_signature(s) + '\n' print('%s\n' % block.replace('\n', '\n\t'), file=out) print(linkify_symbols(f['desc']), file=out) print(dump_link_targets(), file=out) render_enums(out, enums, True, '-') for t, c in constants.items(): print('.. raw:: html\n', file=out) print('\t\n' % t, file=out) dump_report_issue(t, out) print(heading(t, '-'), file=out) print_declared_in(out, c[0]) for v in c: print('.. raw:: html\n', file=out) print('\t\n' % (t, v['name']), file=out) print(v['name'], file=out) v['desc'] = linkify_symbols(v['desc']) print('\t%s' % v['desc'].replace('\n', '\n\t'), file=out) print(dump_link_targets('\t'), file=out) print('', file=out) print(dump_link_targets(), file=out) for i in static_links: print(i, file=out) out.close() # for s in symbols: # print(s) for i, o in list(preprocess_rst.items()): f = open(i, 'r') out = open(o, 'w+') print('processing %s -> %s' % (i, o)) link = linkify_symbols(f.read()) print(link, end=' ', file=out) print(dump_link_targets(), file=out) out.close() f.close()