5 # By Gerald Combs <gerald@wireshark.org>
6 # Based on make-wsluarm.pl by Luis E. Garcia Onatnon <luis.ontanon@gmail.com> and Hadriel Kaplan
8 # Wireshark - Network traffic analyzer
9 # By Gerald Combs <gerald@wireshark.org>
10 # Copyright 1998 Gerald Combs
12 # SPDX-License-Identifier: GPL-2.0-or-later
14 WSLUA's Reference Manual Generator
16 This reads Doxygen-style comments in C code and generates wslua API documentation
17 formatted as AsciiDoc.
19 Behavior as documented by Hadriel:
20 - Allows modules (i.e., WSLUA_MODULE) to have detailed descriptions
21 - Two (or more) line breaks in comments result in separate paragraphs
22 - Any indent with a single leading star '*' followed by space is a bulleted list item
23 reducing indent or having an extra linebreak stops the list
24 - Any indent with a leading digits-dot followed by space, i.e. "1. ", is a numbered list item
25 reducing indent or having an extra linebreak stops the list
35 from string
import Template
37 def parse_desc(description
):
39 Break up descriptions based on newlines and keywords. Some processing
40 is done for code blocks and lists, but the output is otherwise left
41 intact. Assumes the input has been stripped.
44 c_lines
= description
.strip().splitlines()
54 indent
= raw_len
- len(line
)
56 # If we find "[source,...]" then treat it as a block
57 if re
.search(r
'\[source.*\]', line
):
58 # The next line *should* be a delimiter...
59 block_delim
= next(cli
).strip()
60 line
+= f
'\n{block_delim}\n'
61 block_line
= next(cli
)
62 # XXX try except StopIteration
63 while block_line
.strip() != block_delim
:
64 # Keep eating lines until the closing delimiter.
65 # XXX Strip indent spaces?
66 line
+= block_line
+ '\n'
67 block_line
= next(cli
)
68 line
+= block_delim
+ '\n'
70 adoc_lines
.append(line
)
71 elif re
.match(r
'^\s*$', line
):
72 # line is either empty or just whitespace, and we're not in a @code block
73 # so it's the end of a previous paragraph, beginning of new one
76 # We have a regular line, not in a @code block.
79 # if line starts with "@version" or "@since", make it a "Since:"
80 if re
.match(r
'^@(version|since)\s+', line
):
81 line
= re
.sub(r
'^@(version|since)\s+', 'Since: ', line
)
82 adoc_lines
.append(line
)
84 # If line starts with single "*" and space, leave it mostly intact.
85 elif re
.match(r
'^\*\s', line
):
86 adoc_lines
+= ['', line
]
87 # keep eating until we find a blank line or end
90 while not re
.match(r
'^\s*$', line
):
93 # if this is less indented than before, break out
94 if raw_len
- len(line
) < indent
:
100 adoc_lines
.append('')
102 # if line starts with "1." and space, leave it mostly intact.
103 elif re
.match(r
'^1\.\s', line
):
104 adoc_lines
+= ['', line
]
105 # keep eating until we find a blank line or end
108 while not re
.match(r
'^\s*$', line
):
111 # if this is less indented than before, break out
112 if raw_len
- len(line
) < indent
:
116 except StopIteration:
118 adoc_lines
.append('')
120 # Just a normal line, add it to array
123 line
= re
.sub(r
'\[\[(.*)\]\]', r
'$$\1$$', line
)
126 # Strip out consecutive empty lines.
127 # This isn't strictly necessary but makes the AsciiDoc output prettier.
128 adoc_lines
= '\n'.join(adoc_lines
).splitlines()
129 adoc_lines
= [val
for idx
, val
in enumerate(adoc_lines
) if idx
== 0 or not (val
== '' and val
== adoc_lines
[idx
- 1])]
131 return '\n'.join(adoc_lines
)
135 def __init__(self
, c_file
, id, start
, name
, raw_description
):
140 if not raw_description
:
142 self
.description
= parse_desc(raw_description
)
143 self
.arguments
= [] # (name, description, optional)
144 self
.returns
= [] # description
145 self
.errors
= [] # description
146 logging
.info(f
'Created function {id} ({name}) at {start}')
148 def add_argument(self
, id, raw_name
, raw_description
, raw_optional
):
150 logging
.critical(f
'Invalid argument ID {id} in function {self.id}')
152 if not raw_description
:
155 if raw_optional
== 'OPT':
157 self
.arguments
.append((raw_name
.lower(), parse_desc(raw_description
), optional
))
159 def extract_buf(self
, buf
):
160 "Extract arguments, errors, and return values from a function's buffer."
162 # Splits "WSLUA_OPTARG_ProtoField_int8_NAME /* food */" into
163 # "OPT" (1), "ProtoField_int8" (2), "NAME" (3), ..., ..., " food " (6)
164 # Handles functions like "loadfile(filename)" too.
165 for m
in re
.finditer(r
'#define WSLUA_(OPT)?ARG_((?:[A-Za-z0-9]+_)?[a-z0-9_]+)_([A-Z0-9_]+)\s+\d+' + TRAILING_COMMENT_RE
, buf
, re
.MULTILINE|re
.DOTALL
):
166 self
.add_argument(m
.group(2), m
.group(3), m
.group(6), m
.group(1))
167 logging
.info(f
'Created arg {m.group(3)} for {self.id} at {m.start()}')
169 # Same as above, except that there is no macro but a (multi-line) comment.
170 for m
in re
.finditer(r
'/\*\s*WSLUA_(OPT)?ARG_((?:[A-Za-z0-9]+_)?[a-z0-9_]+)_([A-Z0-9_]+)\s*(.*?)\*/', buf
, re
.MULTILINE|re
.DOTALL
):
171 self
.add_argument(m
.group(2), m
.group(3), m
.group(4), m
.group(1))
172 logging
.info(f
'Created arg {m.group(3)} for {self.id} at {m.start()}')
174 for m
in re
.finditer(r
'/\*\s+WSLUA_MOREARGS\s+([A-Za-z_]+)\s+(.*?)\*/', buf
, re
.MULTILINE|re
.DOTALL
):
175 self
.add_argument(m
.group(1), '...', m
.group(2), False)
176 logging
.info(f
'Created morearg for {self.id}')
178 for m
in re
.finditer(r
'WSLUA_(FINAL_)?RETURN\(\s*.*?\s*\)\s*;' + TRAILING_COMMENT_RE
, buf
, re
.MULTILINE|re
.DOTALL
):
179 if m
.group(4) and len(m
.group(4)) > 0:
180 self
.returns
.append(m
.group(4).strip())
181 logging
.info(f
'Created return for {self.id} at {m.start()}')
183 for m
in re
.finditer(r
'/\*\s*_WSLUA_RETURNS_\s*(.*?)\*/', buf
, re
.MULTILINE|re
.DOTALL
):
184 if m
.group(1) and len(m
.group(1)) > 0:
185 self
.returns
.append(m
.group(1).strip())
186 logging
.info(f
'Created return for {self.id} at {m.start()}')
188 for m
in re
.finditer(r
'WSLUA_ERROR\s*\(\s*(([A-Z][A-Za-z]+)_)?([a-z_]+),' + QUOTED_RE
, buf
, re
.MULTILINE|re
.DOTALL
):
189 self
.errors
.append(m
.group(4).strip())
190 logging
.info(f
'Created error {m.group(4)[:10]} for {self.id} at {m.start()}')
193 # The Perl script wrapped optional args in '[]', joined them with ', ', and
194 # converted non-alphabetic characters to underscores.
195 mangled_names
= [f
'_{a}_' if optional
else a
for a
, _
, optional
in self
.arguments
]
196 section_name
= re
.sub('[^A-Za-z0-9]', '_', f
'{self.name}_{"__".join(mangled_names)}_')
197 opt_names
= [f
'[{a}]' if optional
else a
for a
, _
, optional
in self
.arguments
]
200 [#lua_fn_{section_name}]
201 ===== {self.name}({', '.join(opt_names)})
205 if len(self
.arguments
) > 0:
210 for (name
, description
, optional
) in self
.arguments
:
212 name
+= ' (optional)'
213 adoc_buf
+= f
'\n{name}::\n'
215 if len(description
) > 0:
216 adoc_buf
+= f
'\n{description}\n'
218 adoc_buf
+= f
'\n// function_arg_footer: {name}'
220 if len(self
.arguments
) > 0:
221 adoc_buf
+= '\n// end of function_args\n'
223 if len(self
.returns
) > 0:
228 for description
in self
.returns
:
229 adoc_buf
+= f
'\n{description}\n'
231 if len(self
.returns
) > 0:
232 adoc_buf
+= f
'\n// function_returns_footer: {self.name}'
234 if len(self
.errors
) > 0:
239 for description
in self
.errors
:
240 adoc_buf
+= f
'\n* {description}\n'
242 if len(self
.errors
) > 0:
243 adoc_buf
+= f
'\n// function_errors_footer: {self.name}'
245 adoc_buf
+= f
'\n// function_footer: {section_name}\n'
250 # group 1: whole trailing comment (possibly empty), e.g. " /* foo */"
251 # group 2: any leading whitespace. XXX why is this not removed using (?:...)
252 # group 3: actual comment text, e.g. " foo ".
253 TRAILING_COMMENT_RE
= r
'((\s*|[\n\r]*)/\*(.*?)\*/)?'
254 IN_COMMENT_RE
= r
'[\s\r\n]*((.*?)\s*\*/)?'
255 QUOTED_RE
= r
'"([^"]*)"'
257 # XXX We might want to create a "LuaClass" class similar to LuaFunction
258 # and move these there.
259 def extract_class_definitions(c_file
, c_buf
, module
, classes
, functions
):
260 for m
in re
.finditer(r
'WSLUA_CLASS_DEFINE(?:_BASE)?\(\s*([A-Z][a-zA-Z0-9]+).*?\);' + TRAILING_COMMENT_RE
, c_buf
, re
.MULTILINE|re
.DOTALL
):
261 raw_desc
= m
.group(4)
266 'description': parse_desc(raw_desc
),
271 classes
[name
] = mod_class
272 logging
.info(f
'Created class {name}')
275 def extract_function_definitions(c_file
, c_buf
, module
, classes
, functions
):
276 for m
in re
.finditer(r
'WSLUA_FUNCTION\s+wslua_([a-z_0-9]+)[^\{]*\{' + TRAILING_COMMENT_RE
, c_buf
, re
.MULTILINE|re
.DOTALL
):
278 functions
[id] = LuaFunction(c_file
, id, m
.start(), id, m
.group(4))
280 def extract_constructor_definitions(c_file
, c_buf
, module
, classes
, functions
):
281 for m
in re
.finditer(r
'WSLUA_CONSTRUCTOR\s+([A-Za-z0-9]+)_([a-z0-9_]+).*?\{' + TRAILING_COMMENT_RE
, c_buf
, re
.MULTILINE|re
.DOTALL
):
282 class_name
= m
.group(1)
283 id = f
'{class_name}_{m.group(2)}'
284 name
= f
'{class_name}.{m.group(2)}'
285 functions
[id] = LuaFunction(c_file
, id, m
.start(), name
, m
.group(5))
286 classes
[class_name
]['constructors'].append(id)
288 def extract_constructor_markups(c_file
, c_buf
, module
, classes
, functions
):
289 for m
in re
.finditer(r
'_WSLUA_CONSTRUCTOR_\s+([A-Za-z0-9]+)_([a-z0-9_]+)\s*(.*?)\*/', c_buf
, re
.MULTILINE|re
.DOTALL
):
290 class_name
= m
.group(1)
291 id = f
'{class_name}_{m.group(2)}'
292 name
= f
'{class_name}.{m.group(2)}'
293 functions
[id] = LuaFunction(c_file
, id, m
.start(), name
, m
.group(3))
294 classes
[class_name
]['constructors'].append(id)
296 def extract_method_definitions(c_file
, c_buf
, module
, classes
, functions
):
297 for m
in re
.finditer(r
'WSLUA_METHOD\s+([A-Za-z0-9]+)_([a-z0-9_]+)[^\{]*\{' + TRAILING_COMMENT_RE
, c_buf
, re
.MULTILINE|re
.DOTALL
):
298 class_name
= m
.group(1)
299 id = f
'{class_name}_{m.group(2)}'
300 name
= f
'{class_name.lower()}:{m.group(2)}'
301 functions
[id] = LuaFunction(c_file
, id, m
.start(), name
, m
.group(5))
302 classes
[class_name
]['methods'].append(id)
304 def extract_metamethod_definitions(c_file
, c_buf
, module
, classes
, functions
):
305 for m
in re
.finditer(r
'WSLUA_METAMETHOD\s+([A-Za-z0-9]+)(__[a-z0-9]+)[^\{]*\{' + TRAILING_COMMENT_RE
, c_buf
, re
.MULTILINE|re
.DOTALL
):
306 class_name
= m
.group(1)
307 id = f
'{class_name}{m.group(2)}'
308 name
= f
'{class_name.lower()}:{m.group(2)}'
309 functions
[id] = LuaFunction(c_file
, id, m
.start(), name
, m
.group(5))
310 classes
[class_name
]['methods'].append(id)
312 def extract_attribute_markups(c_file
, c_buf
, module
, classes
, functions
):
313 for m
in re
.finditer(r
'/\*\s+WSLUA_ATTRIBUTE\s+([A-Za-z0-9]+)_([a-z0-9_]+)\s+([A-Z]*)\s*(.*?)\*/', c_buf
, re
.MULTILINE|re
.DOTALL
):
314 class_name
= m
.group(1)
315 name
= f
'{m.group(1).lower()}.{m.group(2)}'
319 mode_desc
+= 'Retrieve only.\n'
321 mode_desc
+= 'Assign only.\n'
322 elif 'RW' in mode
or 'WR' in mode
:
323 mode_desc
+= 'Retrieve or assign.\n'
325 sys
.stderr
.write(f
'Attribute does not have a RO/WO/RW mode {mode}\n')
330 'description': parse_desc(f
'{mode_desc}\n{m.group(4)}'),
332 classes
[class_name
]['attributes'].append(attribute
)
333 logging
.info(f
'Created attribute {name} for class {class_name}')
336 parser
= argparse
.ArgumentParser(description
="WSLUA's Reference Manual Generator")
337 parser
.add_argument("c_files", nargs
='+', metavar
='C file', help="C file")
338 parser
.add_argument('--output-directory', help='Output directory')
339 parser
.add_argument('--verbose', action
='store_true', help='Show more output')
340 args
= parser
.parse_args()
342 logging
.basicConfig(format
='%(levelname)s: %(message)s', level
=logging
.DEBUG
if args
.verbose
else logging
.WARNING
)
346 for c_file
in args
.c_files
:
347 with
open(c_file
, encoding
='utf-8') as c_f
:
350 # Peek for modules vs continuations.
351 m
= re
.search(r
'WSLUA_(|CONTINUE_)MODULE\s*(\w+)', c_buf
)
353 module_name
= m
.group(2)
354 c_pair
= (os
.path
.basename(c_file
), c_buf
)
356 if m
.group(1) == 'CONTINUE_':
357 modules
[module_name
]['c'].append(c_pair
)
359 modules
[module_name
]['c'].insert(0, c_pair
)
361 modules
[module_name
] = {}
362 modules
[module_name
]['c'] = [c_pair
]
363 modules
[module_name
]['file_base'] = os
.path
.splitext(c_pair
[0])[0]
365 logging
.warning(f
'No module found in {c_file}')
368 extract_class_definitions
,
369 extract_function_definitions
,
370 extract_constructor_definitions
,
371 extract_constructor_markups
,
372 extract_method_definitions
,
373 extract_metamethod_definitions
,
374 extract_attribute_markups
,
377 for module_name
in sorted(modules
):
378 adoc_file
= f
'{modules[module_name]["file_base"]}.adoc'
379 logging
.info(f
'Writing module {module_name} to {adoc_file} from {len(modules[module_name]["c"])} input(s)')
383 # Extract our module's description.
384 m
= re
.search(r
'WSLUA_MODULE\s*[A-Z][a-zA-Z0-9]+' + IN_COMMENT_RE
, modules
[module_name
]['c'][0][1], re
.MULTILINE|re
.DOTALL
)
387 modules
[module_name
]['description'] = parse_desc(f
'{m.group(2)}')
389 # Extract module-level information from each file.
390 for (c_file
, c_buf
) in modules
[module_name
]['c']:
391 for extractor
in extractors
:
392 extractor(c_file
, c_buf
, modules
[module_name
], classes
, functions
)
394 # Extract function-level information from each file.
395 for (c_file
, c_buf
) in modules
[module_name
]['c']:
396 c_file_ids
= filter(lambda k
: functions
[k
].c_file
== c_file
, functions
.keys())
397 func_ids
= sorted(c_file_ids
, key
=lambda k
: functions
[k
].start
)
399 for next_id
in func_ids
:
400 functions
[id].extract_buf(c_buf
[functions
[id].start
:functions
[next_id
].start
])
402 functions
[id].extract_buf(c_buf
[functions
[id].start
:])
404 with
open(os
.path
.join(args
.output_directory
, adoc_file
), 'w', encoding
='utf-8') as adoc_f
:
407 [#lua_module_{module_name}]
408 === {modules[module_name]["description"]}
410 for class_name
in sorted(classes
.keys()):
411 lua_class
= classes
[class_name
]
414 [#lua_class_{class_name}]
418 if not lua_class
["description"] == '':
419 adoc_f
.write(f
'\n{lua_class["description"]}\n')
421 for constructor_id
in sorted(lua_class
['constructors'], key
=lambda id: functions
[id].start
):
422 adoc_f
.write(functions
[constructor_id
].to_adoc())
423 del functions
[constructor_id
]
425 for method_id
in sorted(lua_class
['methods'], key
=lambda id: functions
[id].start
):
426 adoc_f
.write(functions
[method_id
].to_adoc())
427 del functions
[method_id
]
429 for attribute
in lua_class
['attributes']:
430 attribute_id
= re
.sub('[^A-Za-z0-9]', '_', f
'{attribute["name"]}')
432 [#lua_class_attrib_{attribute_id}]
433 ===== {attribute["name"]}
435 {attribute["description"]}
437 // End {attribute["name"]}
441 adoc_f
.write(f
'\n// class_footer: {class_name}\n')
443 if len(functions
.keys()) > 0:
445 [#global_functions_{module_name}]
446 ==== Global Functions
449 for global_id
in sorted(functions
.keys(), key
=lambda id: functions
[id].start
):
450 adoc_f
.write(functions
[global_id
].to_adoc())
452 if len(functions
.keys()) > 0:
453 adoc_f
.write(f
'// Global function\n')
455 adoc_f
.write('// end of module\n')
457 if __name__
== '__main__':