#!/usr/bin/env python3 # def2doc.py creates texi library documentation for all exported procedures. # Contributed by Gaius Mulley . # Copyright (C) 2000-2023 Free Software Foundation, Inc. # This file is part of GNU Modula-2. # # GNU Modula-2 is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3, or (at your option) # any later version. # # GNU Modula-2 is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with GNU Modula-2; see the file COPYING. If not, write to the # Free Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. # import argparse import os import sys Base_Libs = ['gm2-libs', 'Base libraries', 'Basic M2F compatible libraries'] PIM_Log_Desc = 'PIM and Logitech 3.0 compatible libraries' PIM_Log = ['gm2-libs-log', 'PIM and Logitech 3.0 Compatible', PIM_Log_Desc] PIM_Cor_Desc = 'PIM compatible process support' PIM_Cor = ['gm2-libs-coroutines', 'PIM coroutine support', PIM_Cor_Desc] ISO_Libs = ['gm2-libs-iso', 'M2 ISO Libraries', 'ISO defined libraries'] library_classifications = [Base_Libs, PIM_Log, PIM_Cor, ISO_Libs] # state_states state_none, state_var, state_type, state_const = range(4) # block states block_none, block_code, block_text, block_index = range(4) class state: def __init__(self): self._state_state = state_none self._block = block_none def get_state(self): return self._state_state def set_state(self, value): self._state_state = value def is_const(self): return self._state_state == state_const def is_type(self): return self._state_state == state_type def is_var(self): return self._state_state == state_var def get_block(self): return self._block def _change_block(self, new_block): if self._block != new_block: self._block = new_block self._emit_block_desc() def _emit_block_desc(self): if self._block == block_code: output.write('.. code-block:: modula2\n') elif self._block == block_index: output.write('.. index::\n') def to_code(self): self._change_block(block_code) def to_index(self): self._change_block(block_index) def init_state(): global state_obj state_obj = state() def emit_node(name, nxt, previous, up): if args.texinfo: output.write('@node ' + name + ', ' + nxt + ', ') output.write(previous + ', ' + up + '\n') elif args.sphinx: output.write('@c @node ' + name + ', ' + nxt + ', ') output.write(previous + ', ' + up + '\n') def emit_section(name): if args.texinfo: output.write('@section ' + name + '\n') elif args.sphinx: output.write(name + '\n') output.write('=' * len(name) + '\n') def emit_sub_section(name): if args.texinfo: output.write('@subsection ' + name + '\n') elif args.sphinx: output.write(name + '\n') output.write('-' * len(name) + '\n') def display_library_class(): # display_library_class displays a node for a library directory and invokes # a routine to summarize each module. global args previous = '' nxt = library_classifications[1][1] i = 0 lib = library_classifications[i] while True: emit_node(lib[1], nxt, previous, args.up) emit_section(lib[1]) output.write('\n') display_modules(lib[1], lib[0], args.builddir, args.sourcedir) output.write('\n') output.write('@c ' + '-' * 60 + '\n') previous = lib[1] i += 1 if i == len(library_classifications): break lib = library_classifications[i] if i+1 == len(library_classifications): nxt = '' else: nxt = library_classifications[i+1][1] def display_menu(): # display_menu displays the top level menu for library documentation. output.write('@menu\n') for lib in library_classifications: output.write('* ' + lib[1] + '::' + lib[2] + '\n') output.write('@end menu\n') output.write('\n') output.write('@c ' + '=' * 60 + '\n') output.write('\n') def remote_initial_comments(file, line): # remote_initial_comments removes any (* *) at the top # of the definition module. while (line.find('*)') == -1): line = file.readline() def removeable_field(line): # removeable_field - returns True if a comment field should be removed # from the definition module. field_list = ['Author', 'Last edit', 'LastEdit', 'Last update', 'Date', 'Title', 'Revision'] for field in field_list: if (line.find(field) != -1) and (line.find(':') != -1): return True ignore_list = ['System', 'SYSTEM'] for ignore_field in ignore_list: if line.find(ignore_field) != -1: if line.find(':') != -1: if line.find('Description:') == -1: return True return False def remove_fields(file, line): # remove_fields removes Author/Date/Last edit/SYSTEM/Revision # fields from a comment within the start of a definition module. while (line.find('*)') == -1): if not removeable_field(line): line = line.rstrip().replace('{', '@{').replace('}', '@}') output.write(line + '\n') line = file.readline() output.write(line.rstrip() + '\n') def emit_index(entry, tag): global state_obj if args.texinfo: if tag == '': output.write('@findex ' + entry.rstrip() + '\n') else: output.write('@findex ' + entry.rstrip() + ' ' + tag + '\n') elif args.sphinx: if tag == '': state_obj.to_index() output.write(' ' * 3 + entry.rstrip() + '\n') else: state_obj.to_index() output.write(' ' * 3 + 'pair: ' + entry.rstrip() + '; ' + tag + '\n') def check_index(line): # check_index - create an index entry for a PROCEDURE, TYPE, CONST or VAR. global state_obj words = line.split() procedure = '' if (len(words) > 1) and (words[0] == 'PROCEDURE'): state_obj.set_state(state_none) if (words[1] == '__BUILTIN__') and (len(words) > 2): procedure = words[2] else: procedure = words[1] if (len(line) > 1) and (line[0:2] == '(*'): state_obj.set_state(state_none) elif line == 'VAR': state_obj.set_state(state_var) return elif line == 'TYPE': state_obj.set_state(state_type) return elif line == 'CONST': state_obj.set_state(state_const) if state_obj.is_var(): words = line.split(',') for word in words: word = word.lstrip() if word != '': if word.find(':') == -1: emit_index(word, '(var)') elif len(word) > 0: var = word.split(':') if len(var) > 0: emit_index(var[0], '(var)') if state_obj.is_type(): words = line.lstrip() if words.find('=') != -1: word = words.split('=') if (len(word[0]) > 0) and (word[0][0] != '_'): emit_index(word[0].rstrip(), '(type)') else: word = words.split() if (len(word) > 1) and (word[1] == ';'): # hidden type if (len(word[0]) > 0) and (word[0][0] != '_'): emit_index(word[0].rstrip(), '(type)') if state_obj.is_const(): words = line.split(';') for word in words: word = word.lstrip() if word != '': if word.find('=') != -1: var = word.split('=') if len(var) > 0: emit_index(var[0], '(const)') if procedure != '': name = procedure.split('(') if name[0] != '': proc = name[0] if proc[-1] == ';': proc = proc[:-1] if proc != '': emit_index(proc, '') def demangle_system_datatype(line, indent): # The spaces in front align in the export qualified list. indent += len ('EXPORT QUALIFIED ') line = line.replace('@SYSTEM_DATATYPES@', '\n' + indent * ' ' + 'Target specific data types.') line = line.replace('@SYSTEM_TYPES@', '(* Target specific data types. *)') return line def emit_texinfo_content(f, line): global state_obj output.write(line.rstrip() + '\n') line = f.readline() if len(line.rstrip()) == 0: output.write('\n') line = f.readline() if (line.find('(*') != -1): remove_fields(f, line) else: output.write(line.rstrip() + '\n') else: output.write(line.rstrip() + '\n') line = f.readline() while line: line = line.rstrip() check_index(line) line = line.replace('{', '@{').replace('}', '@}') line = demangle_system_datatype(line, 0) output.write(line + '\n') line = f.readline() return f def emit_sphinx_content(f, line): global state_obj state_obj.to_code() indentation = 4 indent = ' ' * indentation output.write(indent + line.rstrip() + '\n') line = f.readline() if len(line.rstrip()) == 0: output.write('\n') line = f.readline() if (line.find('(*') != -1): remove_fields(f, line) else: output.write(indent + line.rstrip() + '\n') else: output.write(indent + line.rstrip() + '\n') line = f.readline() while line: line = line.rstrip() check_index(line) state_obj.to_code() line = demangle_system_datatype(line, indentation) output.write(indent + line + '\n') line = f.readline() return f def emit_example_content(f, line): if args.texinfo: return emit_texinfo_content(f, line) elif args.sphinx: return emit_sphinx_content(f, line) def emit_example_begin(): if args.texinfo: output.write('@example\n') def emit_example_end(): if args.texinfo: output.write('@end example\n') def emit_page(need_page): if need_page and args.texinfo: output.write('@page\n') def parse_definition(dir_, source, build, file, need_page): # parse_definition reads a definition module and creates # indices for procedures, constants, variables and types. output.write('\n') with open(find_file(dir_, build, source, file), 'r') as f: init_state() line = f.readline() while (line.find('(*') != -1): remote_initial_comments(f, line) line = f.readline() while (line.find('DEFINITION') == -1): line = f.readline() emit_example_begin() f = emit_example_content(f, line) emit_example_end() emit_page(need_page) def parse_modules(up, dir_, build, source, list_of_modules): previous = '' i = 0 if len(list_of_modules) > 1: nxt = dir_ + '/' + list_of_modules[1][:-4] else: nxt = '' while i < len(list_of_modules): emit_node(dir_ + '/' + list_of_modules[i][:-4], nxt, previous, up) emit_sub_section(dir_ + '/' + list_of_modules[i][:-4]) parse_definition(dir_, source, build, list_of_modules[i], True) output.write('\n') previous = dir_ + '/' + list_of_modules[i][:-4] i = i + 1 if i+1 < len(list_of_modules): nxt = dir_ + '/' + list_of_modules[i+1][:-4] else: nxt = '' def do_cat(name): # do_cat displays the contents of file, name, to stdout with open(name, 'r') as file: line = file.readline() while line: output.write(line.rstrip() + '\n') line = file.readline() def module_menu(dir_, build, source): # module_menu generates a simple menu for all definition modules # in dir output.write('@menu\n') list_of_files = [] if os.path.exists(os.path.join(source, dir_)): list_of_files += os.listdir(os.path.join(source, dir_)) if os.path.exists(os.path.join(source, dir_)): list_of_files += os.listdir(os.path.join(build, dir_)) list_of_files = list(dict.fromkeys(list_of_files).keys()) list_of_files.sort() for file in list_of_files: if found_file(dir_, build, source, file): if (len(file) > 4) and (file[-4:] == '.def'): output.write('* ' + dir_ + '/' + file[:-4] + '::' + file + '\n') output.write('@end menu\n') output.write('\n') def check_directory(dir_, build, source): # check_directory - returns True if dir exists in either build or source. if os.path.isdir(build) and os.path.exists(os.path.join(build, dir_)): return True elif os.path.isdir(source) and os.path.exists(os.path.join(source, dir_)): return True else: return False def found_file(dir_, build, source, file): # found_file return True if file is found in build/dir/file or # source/dir/file. name = os.path.join(os.path.join(build, dir_), file) if os.path.exists(name): return True name = os.path.join(os.path.join(source, dir_), file) if os.path.exists(name): return True return False def find_file(dir_, build, source, file): # find_file return the path to file searching in build/dir/file # first then source/dir/file. name1 = os.path.join(os.path.join(build, dir_), file) if os.path.exists(name1): return name1 name2 = os.path.join(os.path.join(source, dir_), file) if os.path.exists(name2): return name2 sys.stderr.write('file cannot be found in either ' + name1) sys.stderr.write(' or ' + name2 + '\n') os.sys.exit(1) def display_modules(up, dir_, build, source): # display_modules walks though the files in dir and parses # definition modules and includes README.texi if check_directory(dir_, build, source): if args.texinfo: ext = '.texi' elif args.sphinx: ext = '.rst' else: ext = '' if found_file(dir_, build, source, 'README' + ext): do_cat(find_file(dir_, build, source, 'README' + ext)) module_menu(dir_, build, source) list_of_files = [] if os.path.exists(os.path.join(source, dir_)): list_of_files += os.listdir(os.path.join(source, dir_)) if os.path.exists(os.path.join(source, dir_)): list_of_files += os.listdir(os.path.join(build, dir_)) list_of_files = list(dict.fromkeys(list_of_files).keys()) list_of_files.sort() list_of_modules = [] for file in list_of_files: if found_file(dir_, build, source, file): if (len(file) > 4) and (file[-4:] == '.def'): list_of_modules += [file] list_of_modules.sort() parse_modules(up, dir_, build, source, list_of_modules) else: line = 'directory ' + dir_ + ' not found in either ' line += build + ' or ' + source sys.stderr.write(line + '\n') def display_copyright(): output.write('@c Copyright (C) 2000-2023 Free Software Foundation, Inc.\n') output.write('@c This file is part of GNU Modula-2.\n') output.write(""" @c Permission is granted to copy, distribute and/or modify this document @c under the terms of the GNU Free Documentation License, Version 1.2 or @c any later version published by the Free Software Foundation. """) def collect_args(): parser = argparse.ArgumentParser() parser.add_argument('-v', '--verbose', help='generate progress messages', action='store_true') parser.add_argument('-b', '--builddir', help='set the build directory', default='.', action='store') parser.add_argument('-f', '--inputfile', help='set the input file', default=None, action='store') parser.add_argument('-o', '--outputfile', help='set the output file', default=None, action='store') parser.add_argument('-s', '--sourcedir', help='set the source directory', default='.', action='store') parser.add_argument('-t', '--texinfo', help='generate texinfo documentation', default=False, action='store_true') parser.add_argument('-u', '--up', help='set the up node', default='', action='store') parser.add_argument('-x', '--sphinx', help='generate sphinx documentation', default=False, action='store_true') args = parser.parse_args() return args def handle_file(): if args.inputfile is None: display_copyright() display_menu() display_library_class() else: parse_definition('.', args.sourcedir, args.builddir, args.inputfile, False) def main(): global args, output args = collect_args() if args.outputfile is None: output = sys.stdout handle_file() else: with open(args.outputfile, 'w') as output: handle_file() main()