1  #!/usr/bin/env python3
       2  
       3  # def2doc.py creates texi library documentation for all exported procedures.
       4  # Contributed by Gaius Mulley <gaius.mulley@southwales.ac.uk>.
       5  
       6  # Copyright (C) 2000-2023 Free Software Foundation, Inc.
       7  # This file is part of GNU Modula-2.
       8  #
       9  # GNU Modula-2 is free software; you can redistribute it and/or modify
      10  # it under the terms of the GNU General Public License as published by
      11  # the Free Software Foundation; either version 3, or (at your option)
      12  # any later version.
      13  #
      14  # GNU Modula-2 is distributed in the hope that it will be useful,
      15  # but WITHOUT ANY WARRANTY; without even the implied warranty of
      16  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      17  # GNU General Public License for more details.
      18  #
      19  # You should have received a copy of the GNU General Public License
      20  # along with GNU Modula-2; see the file COPYING.  If not, write to the
      21  # Free Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
      22  # 02110-1301, USA.
      23  #
      24  
      25  import argparse
      26  import os
      27  import sys
      28  
      29  Base_Libs = ['gm2-libs', 'Base libraries', 'Basic M2F compatible libraries']
      30  
      31  PIM_Log_Desc = 'PIM and Logitech 3.0 compatible libraries'
      32  PIM_Log = ['gm2-libs-log', 'PIM and Logitech 3.0 Compatible', PIM_Log_Desc]
      33  PIM_Cor_Desc = 'PIM compatible process support'
      34  PIM_Cor = ['gm2-libs-coroutines', 'PIM coroutine support', PIM_Cor_Desc]
      35  ISO_Libs = ['gm2-libs-iso', 'M2 ISO Libraries', 'ISO defined libraries']
      36  
      37  library_classifications = [Base_Libs, PIM_Log, PIM_Cor, ISO_Libs]
      38  
      39  # state_states
      40  state_none, state_var, state_type, state_const = range(4)
      41  # block states
      42  block_none, block_code, block_text, block_index = range(4)
      43  
      44  
      45  class ESC[4;38;5;81mstate:
      46      def __init__(self):
      47          self._state_state = state_none
      48          self._block = block_none
      49  
      50      def get_state(self):
      51          return self._state_state
      52  
      53      def set_state(self, value):
      54          self._state_state = value
      55  
      56      def is_const(self):
      57          return self._state_state == state_const
      58  
      59      def is_type(self):
      60          return self._state_state == state_type
      61  
      62      def is_var(self):
      63          return self._state_state == state_var
      64  
      65      def get_block(self):
      66          return self._block
      67  
      68      def _change_block(self, new_block):
      69          if self._block != new_block:
      70              self._block = new_block
      71              self._emit_block_desc()
      72  
      73      def _emit_block_desc(self):
      74          if self._block == block_code:
      75              output.write('.. code-block:: modula2\n')
      76          elif self._block == block_index:
      77              output.write('.. index::\n')
      78  
      79      def to_code(self):
      80          self._change_block(block_code)
      81  
      82      def to_index(self):
      83          self._change_block(block_index)
      84  
      85  
      86  def init_state():
      87      global state_obj
      88      state_obj = state()
      89  
      90  
      91  def emit_node(name, nxt, previous, up):
      92      if args.texinfo:
      93          output.write('@node ' + name + ', ' + nxt + ', ')
      94          output.write(previous + ', ' + up + '\n')
      95      elif args.sphinx:
      96          output.write('@c @node ' + name + ', ' + nxt + ', ')
      97          output.write(previous + ', ' + up + '\n')
      98  
      99  
     100  def emit_section(name):
     101      if args.texinfo:
     102          output.write('@section ' + name + '\n')
     103      elif args.sphinx:
     104          output.write(name + '\n')
     105          output.write('=' * len(name) + '\n')
     106  
     107  
     108  def emit_sub_section(name):
     109      if args.texinfo:
     110          output.write('@subsection ' + name + '\n')
     111      elif args.sphinx:
     112          output.write(name + '\n')
     113          output.write('-' * len(name) + '\n')
     114  
     115  
     116  def display_library_class():
     117      # display_library_class displays a node for a library directory and invokes
     118      # a routine to summarize each module.
     119      global args
     120      previous = ''
     121      nxt = library_classifications[1][1]
     122      i = 0
     123      lib = library_classifications[i]
     124      while True:
     125          emit_node(lib[1], nxt, previous, args.up)
     126          emit_section(lib[1])
     127          output.write('\n')
     128          display_modules(lib[1], lib[0], args.builddir, args.sourcedir)
     129          output.write('\n')
     130          output.write('@c ' + '-' * 60 + '\n')
     131          previous = lib[1]
     132          i += 1
     133          if i == len(library_classifications):
     134              break
     135          lib = library_classifications[i]
     136          if i+1 == len(library_classifications):
     137              nxt = ''
     138          else:
     139              nxt = library_classifications[i+1][1]
     140  
     141  
     142  def display_menu():
     143      # display_menu displays the top level menu for library documentation.
     144      output.write('@menu\n')
     145      for lib in library_classifications:
     146          output.write('* ' + lib[1] + '::' + lib[2] + '\n')
     147      output.write('@end menu\n')
     148      output.write('\n')
     149      output.write('@c ' + '=' * 60 + '\n')
     150      output.write('\n')
     151  
     152  
     153  def remote_initial_comments(file, line):
     154      # remote_initial_comments removes any (* *) at the top
     155      # of the definition module.
     156      while (line.find('*)') == -1):
     157          line = file.readline()
     158  
     159  
     160  def removeable_field(line):
     161      # removeable_field - returns True if a comment field should be removed
     162      # from the definition module.
     163      field_list = ['Author', 'Last edit', 'LastEdit', 'Last update',
     164                    'Date', 'Title', 'Revision']
     165      for field in field_list:
     166          if (line.find(field) != -1) and (line.find(':') != -1):
     167              return True
     168      ignore_list = ['System', 'SYSTEM']
     169      for ignore_field in ignore_list:
     170          if line.find(ignore_field) != -1:
     171              if line.find(':') != -1:
     172                  if line.find('Description:') == -1:
     173                      return True
     174      return False
     175  
     176  
     177  def remove_fields(file, line):
     178      # remove_fields removes Author/Date/Last edit/SYSTEM/Revision
     179      # fields from a comment within the start of a definition module.
     180      while (line.find('*)') == -1):
     181          if not removeable_field(line):
     182              line = line.rstrip().replace('{', '@{').replace('}', '@}')
     183              output.write(line + '\n')
     184          line = file.readline()
     185      output.write(line.rstrip() + '\n')
     186  
     187  
     188  def emit_index(entry, tag):
     189      global state_obj
     190      if args.texinfo:
     191          if tag == '':
     192              output.write('@findex ' + entry.rstrip() + '\n')
     193          else:
     194              output.write('@findex ' + entry.rstrip() + ' ' + tag + '\n')
     195      elif args.sphinx:
     196          if tag == '':
     197              state_obj.to_index()
     198              output.write(' ' * 3 + entry.rstrip() + '\n')
     199          else:
     200              state_obj.to_index()
     201              output.write(' ' * 3 + 'pair: ' + entry.rstrip() + '; ' + tag + '\n')
     202  
     203  
     204  def check_index(line):
     205      # check_index - create an index entry for a PROCEDURE, TYPE, CONST or VAR.
     206      global state_obj
     207  
     208      words = line.split()
     209      procedure = ''
     210      if (len(words) > 1) and (words[0] == 'PROCEDURE'):
     211          state_obj.set_state(state_none)
     212          if (words[1] == '__BUILTIN__') and (len(words) > 2):
     213              procedure = words[2]
     214          else:
     215              procedure = words[1]
     216      if (len(line) > 1) and (line[0:2] == '(*'):
     217          state_obj.set_state(state_none)
     218      elif line == 'VAR':
     219          state_obj.set_state(state_var)
     220          return
     221      elif line == 'TYPE':
     222          state_obj.set_state(state_type)
     223          return
     224      elif line == 'CONST':
     225          state_obj.set_state(state_const)
     226      if state_obj.is_var():
     227          words = line.split(',')
     228          for word in words:
     229              word = word.lstrip()
     230              if word != '':
     231                  if word.find(':') == -1:
     232                      emit_index(word, '(var)')
     233                  elif len(word) > 0:
     234                      var = word.split(':')
     235                      if len(var) > 0:
     236                          emit_index(var[0], '(var)')
     237      if state_obj.is_type():
     238          words = line.lstrip()
     239          if words.find('=') != -1:
     240              word = words.split('=')
     241              if (len(word[0]) > 0) and (word[0][0] != '_'):
     242                  emit_index(word[0].rstrip(), '(type)')
     243          else:
     244              word = words.split()
     245              if (len(word) > 1) and (word[1] == ';'):
     246                  # hidden type
     247                  if (len(word[0]) > 0) and (word[0][0] != '_'):
     248                      emit_index(word[0].rstrip(), '(type)')
     249      if state_obj.is_const():
     250          words = line.split(';')
     251          for word in words:
     252              word = word.lstrip()
     253              if word != '':
     254                  if word.find('=') != -1:
     255                      var = word.split('=')
     256                      if len(var) > 0:
     257                          emit_index(var[0], '(const)')
     258      if procedure != '':
     259          name = procedure.split('(')
     260          if name[0] != '':
     261              proc = name[0]
     262              if proc[-1] == ';':
     263                  proc = proc[:-1]
     264              if proc != '':
     265                  emit_index(proc, '')
     266  
     267  def demangle_system_datatype(line, indent):
     268      # The spaces in front align in the export qualified list.
     269      indent += len ('EXPORT QUALIFIED ')
     270      line = line.replace('@SYSTEM_DATATYPES@',
     271                          '\n' + indent * ' ' + 'Target specific data types.')
     272      line = line.replace('@SYSTEM_TYPES@',
     273                          '(* Target specific data types.  *)')
     274      return line
     275  
     276  
     277  def emit_texinfo_content(f, line):
     278      global state_obj
     279      output.write(line.rstrip() + '\n')
     280      line = f.readline()
     281      if len(line.rstrip()) == 0:
     282          output.write('\n')
     283          line = f.readline()
     284          if (line.find('(*') != -1):
     285              remove_fields(f, line)
     286          else:
     287              output.write(line.rstrip() + '\n')
     288      else:
     289          output.write(line.rstrip() + '\n')
     290      line = f.readline()
     291      while line:
     292          line = line.rstrip()
     293          check_index(line)
     294          line = line.replace('{', '@{').replace('}', '@}')
     295          line = demangle_system_datatype(line, 0)
     296          output.write(line + '\n')
     297          line = f.readline()
     298      return f
     299  
     300  
     301  def emit_sphinx_content(f, line):
     302      global state_obj
     303      state_obj.to_code()
     304      indentation = 4
     305      indent = ' ' * indentation
     306      output.write(indent + line.rstrip() + '\n')
     307      line = f.readline()
     308      if len(line.rstrip()) == 0:
     309          output.write('\n')
     310          line = f.readline()
     311          if (line.find('(*') != -1):
     312              remove_fields(f, line)
     313          else:
     314              output.write(indent + line.rstrip() + '\n')
     315      else:
     316          output.write(indent + line.rstrip() + '\n')
     317      line = f.readline()
     318      while line:
     319          line = line.rstrip()
     320          check_index(line)
     321          state_obj.to_code()
     322          line = demangle_system_datatype(line, indentation)
     323          output.write(indent + line + '\n')
     324          line = f.readline()
     325      return f
     326  
     327  
     328  def emit_example_content(f, line):
     329      if args.texinfo:
     330          return emit_texinfo_content(f, line)
     331      elif args.sphinx:
     332          return emit_sphinx_content(f, line)
     333  
     334  
     335  def emit_example_begin():
     336      if args.texinfo:
     337          output.write('@example\n')
     338  
     339  
     340  def emit_example_end():
     341      if args.texinfo:
     342          output.write('@end example\n')
     343  
     344  
     345  def emit_page(need_page):
     346      if need_page and args.texinfo:
     347          output.write('@page\n')
     348  
     349  
     350  def parse_definition(dir_, source, build, file, need_page):
     351      # parse_definition reads a definition module and creates
     352      # indices for procedures, constants, variables and types.
     353      output.write('\n')
     354      with open(find_file(dir_, build, source, file), 'r') as f:
     355          init_state()
     356          line = f.readline()
     357          while (line.find('(*') != -1):
     358              remote_initial_comments(f, line)
     359              line = f.readline()
     360          while (line.find('DEFINITION') == -1):
     361              line = f.readline()
     362          emit_example_begin()
     363          f = emit_example_content(f, line)
     364          emit_example_end()
     365          emit_page(need_page)
     366  
     367  
     368  def parse_modules(up, dir_, build, source, list_of_modules):
     369      previous = ''
     370      i = 0
     371      if len(list_of_modules) > 1:
     372          nxt = dir_ + '/' + list_of_modules[1][:-4]
     373      else:
     374          nxt = ''
     375      while i < len(list_of_modules):
     376          emit_node(dir_ + '/' + list_of_modules[i][:-4], nxt, previous, up)
     377          emit_sub_section(dir_ + '/' + list_of_modules[i][:-4])
     378          parse_definition(dir_, source, build, list_of_modules[i], True)
     379          output.write('\n')
     380          previous = dir_ + '/' + list_of_modules[i][:-4]
     381          i = i + 1
     382          if i+1 < len(list_of_modules):
     383              nxt = dir_ + '/' + list_of_modules[i+1][:-4]
     384          else:
     385              nxt = ''
     386  
     387  
     388  def do_cat(name):
     389      # do_cat displays the contents of file, name, to stdout
     390      with open(name, 'r') as file:
     391          line = file.readline()
     392          while line:
     393              output.write(line.rstrip() + '\n')
     394              line = file.readline()
     395  
     396  
     397  def module_menu(dir_, build, source):
     398      # module_menu generates a simple menu for all definition modules
     399      # in dir
     400      output.write('@menu\n')
     401      list_of_files = []
     402      if os.path.exists(os.path.join(source, dir_)):
     403          list_of_files += os.listdir(os.path.join(source, dir_))
     404      if os.path.exists(os.path.join(source, dir_)):
     405          list_of_files += os.listdir(os.path.join(build, dir_))
     406      list_of_files = list(dict.fromkeys(list_of_files).keys())
     407      list_of_files.sort()
     408      for file in list_of_files:
     409          if found_file(dir_, build, source, file):
     410              if (len(file) > 4) and (file[-4:] == '.def'):
     411                  output.write('* ' + dir_ + '/' + file[:-4] + '::' + file + '\n')
     412      output.write('@end menu\n')
     413      output.write('\n')
     414  
     415  
     416  def check_directory(dir_, build, source):
     417      # check_directory - returns True if dir exists in either build or source.
     418      if os.path.isdir(build) and os.path.exists(os.path.join(build, dir_)):
     419          return True
     420      elif os.path.isdir(source) and os.path.exists(os.path.join(source, dir_)):
     421          return True
     422      else:
     423          return False
     424  
     425  
     426  def found_file(dir_, build, source, file):
     427      # found_file return True if file is found in build/dir/file or
     428      # source/dir/file.
     429      name = os.path.join(os.path.join(build, dir_), file)
     430      if os.path.exists(name):
     431          return True
     432      name = os.path.join(os.path.join(source, dir_), file)
     433      if os.path.exists(name):
     434          return True
     435      return False
     436  
     437  
     438  def find_file(dir_, build, source, file):
     439      # find_file return the path to file searching in build/dir/file
     440      # first then source/dir/file.
     441      name1 = os.path.join(os.path.join(build, dir_), file)
     442      if os.path.exists(name1):
     443          return name1
     444      name2 = os.path.join(os.path.join(source, dir_), file)
     445      if os.path.exists(name2):
     446          return name2
     447      sys.stderr.write('file cannot be found in either ' + name1)
     448      sys.stderr.write(' or ' + name2 + '\n')
     449      os.sys.exit(1)
     450  
     451  
     452  def display_modules(up, dir_, build, source):
     453      # display_modules walks though the files in dir and parses
     454      # definition modules and includes README.texi
     455      if check_directory(dir_, build, source):
     456          if args.texinfo:
     457              ext = '.texi'
     458          elif args.sphinx:
     459              ext = '.rst'
     460          else:
     461              ext = ''
     462          if found_file(dir_, build, source, 'README' + ext):
     463              do_cat(find_file(dir_, build, source, 'README' + ext))
     464          module_menu(dir_, build, source)
     465          list_of_files = []
     466          if os.path.exists(os.path.join(source, dir_)):
     467              list_of_files += os.listdir(os.path.join(source, dir_))
     468          if os.path.exists(os.path.join(source, dir_)):
     469              list_of_files += os.listdir(os.path.join(build, dir_))
     470          list_of_files = list(dict.fromkeys(list_of_files).keys())
     471          list_of_files.sort()
     472          list_of_modules = []
     473          for file in list_of_files:
     474              if found_file(dir_, build, source, file):
     475                  if (len(file) > 4) and (file[-4:] == '.def'):
     476                      list_of_modules += [file]
     477          list_of_modules.sort()
     478          parse_modules(up, dir_, build, source, list_of_modules)
     479      else:
     480          line = 'directory ' + dir_ + ' not found in either '
     481          line += build + ' or ' + source
     482          sys.stderr.write(line + '\n')
     483  
     484  
     485  def display_copyright():
     486      output.write('@c Copyright (C) 2000-2023 Free Software Foundation, Inc.\n')
     487      output.write('@c This file is part of GNU Modula-2.\n')
     488      output.write("""
     489  @c Permission is granted to copy, distribute and/or modify this document
     490  @c under the terms of the GNU Free Documentation License, Version 1.2 or
     491  @c any later version published by the Free Software Foundation.
     492  """)
     493  
     494  
     495  def collect_args():
     496      parser = argparse.ArgumentParser()
     497      parser.add_argument('-v', '--verbose', help='generate progress messages',
     498                          action='store_true')
     499      parser.add_argument('-b', '--builddir', help='set the build directory',
     500                          default='.', action='store')
     501      parser.add_argument('-f', '--inputfile', help='set the input file',
     502                          default=None, action='store')
     503      parser.add_argument('-o', '--outputfile', help='set the output file',
     504                          default=None, action='store')
     505      parser.add_argument('-s', '--sourcedir', help='set the source directory',
     506                          default='.', action='store')
     507      parser.add_argument('-t', '--texinfo',
     508                          help='generate texinfo documentation',
     509                          default=False, action='store_true')
     510      parser.add_argument('-u', '--up', help='set the up node',
     511                          default='', action='store')
     512      parser.add_argument('-x', '--sphinx', help='generate sphinx documentation',
     513                          default=False, action='store_true')
     514      args = parser.parse_args()
     515      return args
     516  
     517  
     518  def handle_file():
     519      if args.inputfile is None:
     520          display_copyright()
     521          display_menu()
     522          display_library_class()
     523      else:
     524          parse_definition('.', args.sourcedir, args.builddir,
     525                           args.inputfile, False)
     526  
     527  
     528  def main():
     529      global args, output
     530      args = collect_args()
     531      if args.outputfile is None:
     532          output = sys.stdout
     533          handle_file()
     534      else:
     535          with open(args.outputfile, 'w') as output:
     536              handle_file()
     537  
     538  
     539  main()