3 # def2doc.py creates texi library documentation for all exported procedures.
4 # Contributed by Gaius Mulley <gaius.mulley@southwales.ac.uk>.
6 # Copyright (C) 2000-2024 Free Software Foundation, Inc.
7 # This file is part of GNU Modula-2.
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)
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.
19 # You should have received a copy of the GNU General Public License
20 # along with GCC; see the file COPYING3. If not see
21 # <http://www.gnu.org/licenses/>.
28 Base_Libs
= ['gm2-libs', 'Base libraries', 'Basic M2F compatible libraries']
30 PIM_Log_Desc
= 'PIM and Logitech 3.0 compatible libraries'
31 PIM_Log
= ['gm2-libs-log', 'PIM and Logitech 3.0 Compatible', PIM_Log_Desc
]
32 PIM_Cor_Desc
= 'PIM compatible process support'
33 PIM_Cor
= ['gm2-libs-coroutines', 'PIM coroutine support', PIM_Cor_Desc
]
34 ISO_Libs
= ['gm2-libs-iso', 'M2 ISO Libraries', 'ISO defined libraries']
36 library_classifications
= [Base_Libs
, PIM_Log
, PIM_Cor
, ISO_Libs
]
39 state_none
, state_var
, state_type
, state_const
= range(4)
41 block_none
, block_code
, block_text
, block_index
= range(4)
46 self
._state
_state
= state_none
47 self
._block
= block_none
50 return self
._state
_state
52 def set_state(self
, value
):
53 self
._state
_state
= value
56 return self
._state
_state
== state_const
59 return self
._state
_state
== state_type
62 return self
._state
_state
== state_var
67 def _change_block(self
, new_block
):
68 if self
._block
!= new_block
:
69 self
._block
= new_block
70 self
._emit
_block
_desc
()
72 def _emit_block_desc(self
):
73 if self
._block
== block_code
:
74 output
.write('.. code-block:: modula2\n')
75 elif self
._block
== block_index
:
76 output
.write('.. index::\n')
79 self
._change
_block
(block_code
)
82 self
._change
_block
(block_index
)
90 def emit_node(name
, nxt
, previous
, up
):
92 output
.write('@node ' + name
+ ', ' + nxt
+ ', ')
93 output
.write(previous
+ ', ' + up
+ '\n')
95 output
.write('@c @node ' + name
+ ', ' + nxt
+ ', ')
96 output
.write(previous
+ ', ' + up
+ '\n')
99 def emit_section(name
):
101 output
.write('@section ' + name
+ '\n')
103 output
.write(name
+ '\n')
104 output
.write('=' * len(name
) + '\n')
107 def emit_sub_section(name
):
109 output
.write('@subsection ' + name
+ '\n')
111 output
.write(name
+ '\n')
112 output
.write('-' * len(name
) + '\n')
115 def display_library_class():
116 # display_library_class displays a node for a library directory and invokes
117 # a routine to summarize each module.
120 nxt
= library_classifications
[1][1]
122 lib
= library_classifications
[i
]
124 emit_node(lib
[1], nxt
, previous
, args
.up
)
127 display_modules(lib
[1], lib
[0], args
.builddir
, args
.sourcedir
)
129 output
.write('@c ' + '-' * 60 + '\n')
132 if i
== len(library_classifications
):
134 lib
= library_classifications
[i
]
135 if i
+1 == len(library_classifications
):
138 nxt
= library_classifications
[i
+1][1]
142 # display_menu displays the top level menu for library documentation.
143 output
.write('@menu\n')
144 for lib
in library_classifications
:
145 output
.write('* ' + lib
[1] + '::' + lib
[2] + '\n')
146 output
.write('@end menu\n')
148 output
.write('@c ' + '=' * 60 + '\n')
152 def remote_initial_comments(file, line
):
153 # remote_initial_comments removes any (* *) at the top
154 # of the definition module.
155 while (line
.find('*)') == -1):
156 line
= file.readline()
159 def removeable_field(line
):
160 # removeable_field - returns True if a comment field should be removed
161 # from the definition module.
162 field_list
= ['Author', 'Last edit', 'LastEdit', 'Last update',
163 'Date', 'Title', 'Revision']
164 for field
in field_list
:
165 if (line
.find(field
) != -1) and (line
.find(':') != -1):
167 ignore_list
= ['System', 'SYSTEM']
168 for ignore_field
in ignore_list
:
169 if line
.find(ignore_field
) != -1:
170 if line
.find(':') != -1:
171 if line
.find('Description:') == -1:
176 def remove_fields(file, line
):
177 # remove_fields removes Author/Date/Last edit/SYSTEM/Revision
178 # fields from a comment within the start of a definition module.
179 while (line
.find('*)') == -1):
180 if not removeable_field(line
):
181 line
= line
.rstrip().replace('{', '@{').replace('}', '@}')
182 output
.write(line
+ '\n')
183 line
= file.readline()
184 output
.write(line
.rstrip() + '\n')
187 def emit_index(entry
, tag
):
191 output
.write('@findex ' + entry
.rstrip() + '\n')
193 output
.write('@findex ' + entry
.rstrip() + ' ' + tag
+ '\n')
197 output
.write(' ' * 3 + entry
.rstrip() + '\n')
200 output
.write(' ' * 3 + 'pair: ' + entry
.rstrip() + '; ' + tag
+ '\n')
203 def check_index(line
):
204 # check_index - create an index entry for a PROCEDURE, TYPE, CONST or VAR.
209 if (len(words
) > 1) and (words
[0] == 'PROCEDURE'):
210 state_obj
.set_state(state_none
)
211 if (words
[1] == '__BUILTIN__') and (len(words
) > 2):
215 if (len(line
) > 1) and (line
[0:2] == '(*'):
216 state_obj
.set_state(state_none
)
218 state_obj
.set_state(state_var
)
221 state_obj
.set_state(state_type
)
223 elif line
== 'CONST':
224 state_obj
.set_state(state_const
)
225 if state_obj
.is_var():
226 words
= line
.split(',')
230 if word
.find(':') == -1:
231 emit_index(word
, '(var)')
233 var
= word
.split(':')
235 emit_index(var
[0], '(var)')
236 if state_obj
.is_type():
237 words
= line
.lstrip()
238 if words
.find('=') != -1:
239 word
= words
.split('=')
240 if (len(word
[0]) > 0) and (word
[0][0] != '_'):
241 emit_index(word
[0].rstrip(), '(type)')
244 if (len(word
) > 1) and (word
[1] == ';'):
246 if (len(word
[0]) > 0) and (word
[0][0] != '_'):
247 emit_index(word
[0].rstrip(), '(type)')
248 if state_obj
.is_const():
249 words
= line
.split(';')
253 if word
.find('=') != -1:
254 var
= word
.split('=')
256 emit_index(var
[0], '(const)')
258 name
= procedure
.split('(')
266 def demangle_system_datatype(line
, indent
):
267 # The spaces in front align in the export qualified list.
268 indent
+= len ('EXPORT QUALIFIED ')
269 line
= line
.replace('@SYSTEM_DATATYPES@',
270 '\n' + indent
* ' ' + 'Target specific data types.')
271 line
= line
.replace('@SYSTEM_TYPES@',
272 '(* Target specific data types. *)')
276 def emit_texinfo_content(f
, line
):
278 output
.write(line
.rstrip() + '\n')
280 if len(line
.rstrip()) == 0:
283 if (line
.find('(*') != -1):
284 remove_fields(f
, line
)
286 output
.write(line
.rstrip() + '\n')
288 output
.write(line
.rstrip() + '\n')
293 line
= line
.replace('{', '@{').replace('}', '@}')
294 line
= demangle_system_datatype(line
, 0)
295 output
.write(line
+ '\n')
300 def emit_sphinx_content(f
, line
):
304 indent
= ' ' * indentation
305 output
.write(indent
+ line
.rstrip() + '\n')
307 if len(line
.rstrip()) == 0:
310 if (line
.find('(*') != -1):
311 remove_fields(f
, line
)
313 output
.write(indent
+ line
.rstrip() + '\n')
315 output
.write(indent
+ line
.rstrip() + '\n')
321 line
= demangle_system_datatype(line
, indentation
)
322 output
.write(indent
+ line
+ '\n')
327 def emit_example_content(f
, line
):
329 return emit_texinfo_content(f
, line
)
331 return emit_sphinx_content(f
, line
)
334 def emit_example_begin():
336 output
.write('@example\n')
339 def emit_example_end():
341 output
.write('@end example\n')
344 def emit_page(need_page
):
345 if need_page
and args
.texinfo
:
346 output
.write('@page\n')
349 def parse_definition(dir_
, source
, build
, file, need_page
):
350 # parse_definition reads a definition module and creates
351 # indices for procedures, constants, variables and types.
353 with
open(find_file(dir_
, build
, source
, file), 'r') as f
:
356 while (line
.find('(*') != -1):
357 remote_initial_comments(f
, line
)
359 while (line
.find('DEFINITION') == -1):
362 f
= emit_example_content(f
, line
)
367 def parse_modules(up
, dir_
, build
, source
, list_of_modules
):
370 if len(list_of_modules
) > 1:
371 nxt
= dir_
+ '/' + list_of_modules
[1][:-4]
374 while i
< len(list_of_modules
):
375 emit_node(dir_
+ '/' + list_of_modules
[i
][:-4], nxt
, previous
, up
)
376 emit_sub_section(dir_
+ '/' + list_of_modules
[i
][:-4])
377 parse_definition(dir_
, source
, build
, list_of_modules
[i
], True)
379 previous
= dir_
+ '/' + list_of_modules
[i
][:-4]
381 if i
+1 < len(list_of_modules
):
382 nxt
= dir_
+ '/' + list_of_modules
[i
+1][:-4]
388 # do_cat displays the contents of file, name, to stdout
389 with
open(name
, 'r') as file:
390 line
= file.readline()
392 output
.write(line
.rstrip() + '\n')
393 line
= file.readline()
396 def module_menu(dir_
, build
, source
):
397 # module_menu generates a simple menu for all definition modules
399 output
.write('@menu\n')
401 if os
.path
.exists(os
.path
.join(source
, dir_
)):
402 list_of_files
+= os
.listdir(os
.path
.join(source
, dir_
))
403 if os
.path
.exists(os
.path
.join(source
, dir_
)):
404 list_of_files
+= os
.listdir(os
.path
.join(build
, dir_
))
405 list_of_files
= list(dict.fromkeys(list_of_files
).keys())
407 for file in list_of_files
:
408 if found_file(dir_
, build
, source
, file):
409 if (len(file) > 4) and (file[-4:] == '.def'):
410 output
.write('* ' + dir_
+ '/' + file[:-4] + '::' + file + '\n')
411 output
.write('@end menu\n')
415 def check_directory(dir_
, build
, source
):
416 # check_directory - returns True if dir exists in either build or source.
417 if os
.path
.isdir(build
) and os
.path
.exists(os
.path
.join(build
, dir_
)):
419 elif os
.path
.isdir(source
) and os
.path
.exists(os
.path
.join(source
, dir_
)):
425 def found_file(dir_
, build
, source
, file):
426 # found_file return True if file is found in build/dir/file or
428 name
= os
.path
.join(os
.path
.join(build
, dir_
), file)
429 if os
.path
.exists(name
):
431 name
= os
.path
.join(os
.path
.join(source
, dir_
), file)
432 if os
.path
.exists(name
):
437 def find_file(dir_
, build
, source
, file):
438 # find_file return the path to file searching in build/dir/file
439 # first then source/dir/file.
440 name1
= os
.path
.join(os
.path
.join(build
, dir_
), file)
441 if os
.path
.exists(name1
):
443 name2
= os
.path
.join(os
.path
.join(source
, dir_
), file)
444 if os
.path
.exists(name2
):
446 sys
.stderr
.write('file cannot be found in either ' + name1
)
447 sys
.stderr
.write(' or ' + name2
+ '\n')
451 def display_modules(up
, dir_
, build
, source
):
452 # display_modules walks though the files in dir and parses
453 # definition modules and includes README.texi
454 if check_directory(dir_
, build
, source
):
461 if found_file(dir_
, build
, source
, 'README' + ext
):
462 do_cat(find_file(dir_
, build
, source
, 'README' + ext
))
463 module_menu(dir_
, build
, source
)
465 if os
.path
.exists(os
.path
.join(source
, dir_
)):
466 list_of_files
+= os
.listdir(os
.path
.join(source
, dir_
))
467 if os
.path
.exists(os
.path
.join(source
, dir_
)):
468 list_of_files
+= os
.listdir(os
.path
.join(build
, dir_
))
469 list_of_files
= list(dict.fromkeys(list_of_files
).keys())
472 for file in list_of_files
:
473 if found_file(dir_
, build
, source
, file):
474 if (len(file) > 4) and (file[-4:] == '.def'):
475 list_of_modules
+= [file]
476 list_of_modules
.sort()
477 parse_modules(up
, dir_
, build
, source
, list_of_modules
)
479 line
= 'directory ' + dir_
+ ' not found in either '
480 line
+= build
+ ' or ' + source
481 sys
.stderr
.write(line
+ '\n')
484 def display_copyright():
485 output
.write('@c Copyright (C) 2000-2024 Free Software Foundation, Inc.\n')
486 output
.write('@c This file is part of GNU Modula-2.\n')
488 @c Permission is granted to copy, distribute and/or modify this document
489 @c under the terms of the GNU Free Documentation License, Version 1.2 or
490 @c any later version published by the Free Software Foundation.
495 parser
= argparse
.ArgumentParser()
496 parser
.add_argument('-v', '--verbose', help='generate progress messages',
498 parser
.add_argument('-b', '--builddir', help='set the build directory',
499 default
='.', action
='store')
500 parser
.add_argument('-f', '--inputfile', help='set the input file',
501 default
=None, action
='store')
502 parser
.add_argument('-o', '--outputfile', help='set the output file',
503 default
=None, action
='store')
504 parser
.add_argument('-s', '--sourcedir', help='set the source directory',
505 default
='.', action
='store')
506 parser
.add_argument('-t', '--texinfo',
507 help='generate texinfo documentation',
508 default
=False, action
='store_true')
509 parser
.add_argument('-u', '--up', help='set the up node',
510 default
='', action
='store')
511 parser
.add_argument('-x', '--sphinx', help='generate sphinx documentation',
512 default
=False, action
='store_true')
513 args
= parser
.parse_args()
518 if args
.inputfile
is None:
521 display_library_class()
523 parse_definition('.', args
.sourcedir
, args
.builddir
,
524 args
.inputfile
, False)
529 args
= collect_args()
530 if args
.outputfile
is None:
534 with
open(args
.outputfile
, 'w') as output
: