]>
Commit | Line | Data |
---|---|---|
1eee94d3 GM |
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 | ||
a945c346 | 6 | # Copyright (C) 2000-2024 Free Software Foundation, Inc. |
1eee94d3 GM |
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' | |
8804eb0b | 32 | PIM_Log = ['gm2-libs-log', 'PIM and Logitech 3.0 Compatible', PIM_Log_Desc] |
1eee94d3 GM |
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 state: | |
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(): | |
a945c346 | 486 | output.write('@c Copyright (C) 2000-2024 Free Software Foundation, Inc.\n') |
1eee94d3 GM |
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() |