blob: 47e75d874b496d9fff8424540f7a5fabc5d648b6 [file] [log] [blame]
#!/usr/bin/env python3
# def2doc.py creates texi library documentation for all exported procedures.
# Contributed by Gaius Mulley <gaius.mulley@southwales.ac.uk>.
# 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-pim', '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()