This repository has been archived on 2023-07-11. You can view files and clone it, but cannot push or open issues or pull requests.
chocolate-doom-wii/man/docgen
2016-10-30 18:40:00 -04:00

509 lines
12 KiB
Python
Executable file

#!/usr/bin/env python
#
# Chocolate Doom self-documentation tool. This works similar to javadoc
# or doxygen, but documents command line parameters and configuration
# file values, generating documentation in Unix manpage, wikitext and
# plain text forms.
#
# Comments are read from the source code in the following form:
#
# //!
# // @arg <extra arguments>
# // @category Category
# // @platform <some platform that the parameter is specific to>
# //
# // Long description of the parameter
# //
#
# something_involving = M_CheckParm("-param");
#
# For configuration file values:
#
# //! @begin_config_file myconfig
#
# //!
# // Description of the configuration file value.
# //
#
# CONFIG_VARIABLE_INT(my_variable, c_variable),
#
import sys
import os
import re
import glob
import getopt
INCLUDE_STATEMENT_RE = re.compile("@include\s+(\S+)")
# Find the maximum width of a list of parameters (for plain text output)
def parameter_list_width(params):
w = 0
for p in params:
pw = len(p.name) + 5
if p.args:
pw += len(p.args)
if pw > w:
w = pw
return w
class ConfigFile:
def __init__(self, filename):
self.filename = filename
self.variables = []
def add_variable(self, variable):
self.variables.append(variable)
def manpage_output(self):
result = ".SH CONFIGURATION VARIABLES\n"
for v in self.variables:
result += ".TP\n"
result += v.manpage_output()
return result
def plaintext_output(self):
result = ""
w = parameter_list_width(self.variables)
for p in self.variables:
result += p.plaintext_output(w)
result = result.rstrip() + "\n"
return result
class Category:
def __init__(self, description):
self.description = description
self.params = []
def add_param(self, param):
self.params.append(param)
# Plain text output
def plaintext_output(self):
result = "=== %s ===\n\n" % self.description
self.params.sort()
w = parameter_list_width(self.params)
for p in self.params:
if p.should_show():
result += p.plaintext_output(w)
result = result.rstrip() + "\n"
return result
def manpage_output(self):
result = ".SH " + self.description.upper() + "\n"
self.params.sort()
for p in self.params:
if p.should_show():
result += ".TP\n"
result += p.manpage_output()
return result
def wiki_output(self):
result = "=== %s ===\n" % self.description
self.params.sort()
for p in self.params:
if p.should_show():
result += "; " + p.wiki_output() + "\n"
# Escape special HTML characters
result = result.replace("&", "&amp;")
result = result.replace("<", "&lt;")
result = result.replace(">", "&gt;")
return result
categories = {
None: Category("General options"),
"video": Category("Display options"),
"demo": Category("Demo options"),
"net": Category("Networking options"),
"mod": Category("Dehacked and WAD merging"),
"compat": Category("Compatibility"),
}
wikipages = []
config_files = {}
# Show options that are in Vanilla Doom? Or only new options?
show_vanilla_options = True
class Parameter:
def __lt__(self, other):
return self.name < other.name
def __init__(self):
self.text = ""
self.name = ""
self.args = None
self.platform = None
self.category = None
self.vanilla_option = False
self.games = None
def should_show(self):
return not self.vanilla_option or show_vanilla_options
def add_text(self, text):
if len(text) <= 0:
pass
elif text[0] == "@":
match = re.match('@(\S+)\s*(.*)', text)
if not match:
raise "Malformed option line: %s" % text
option_type = match.group(1)
data = match.group(2)
if option_type == "arg":
self.args = data
elif option_type == "platform":
self.platform = data
elif option_type == "category":
self.category = data
elif option_type == "vanilla":
self.vanilla_option = True
elif option_type == "game":
self.games = re.split(r'\s+', data.strip())
else:
raise "Unknown option type '%s'" % option_type
else:
self.text += text + " "
def _games_only_text(self, pattern="(%s only)"):
if not match_game and self.games:
games_list = ", ".join(map(str.capitalize, self.games))
return " " + (pattern % games_list)
else:
return ""
def manpage_output(self):
result = self.name
if self.args:
result += " " + self.args
result = '\\fB' + result + '\\fR'
result += "\n"
if self.platform:
result += "[%s only] " % self.platform
escaped = re.sub('\\\\', '\\\\\\\\', self.text)
result += escaped + self._games_only_text() + "\n"
return result
def wiki_output(self):
result = self.name
if self.args:
result += " " + self.args
result += ": "
result += add_wiki_links(self.text)
if self.platform:
result += "'''(%s only)'''" % self.platform
result += self._games_only_text("'''(%s only)'''")
return result
def plaintext_output(self, w):
# Build the first line, with the argument on
line = " " + self.name
if self.args:
line += " " + self.args
# pad up to the plaintext width
line += " " * (w - len(line))
# Build the description text
description = self.text
if self.platform:
description += " (%s only)" % self.platform
description += self._games_only_text()
# Build the complete text for the argument
# Split the description into words and add a word at a time
result = ""
for word in re.split('\s+', description):
# Break onto the next line?
if len(line) + len(word) + 1 > 75:
result += line + "\n"
line = " " * w
# Add another word
line += word + " "
result += line + "\n\n"
return result
# Read list of wiki pages
def read_wikipages():
f = open("wikipages")
try:
for line in f:
line = line.rstrip()
line = re.sub('\#.*$', '', line)
if not re.match('^\s*$', line):
wikipages.append(line)
finally:
f.close()
# Add wiki page links
def add_wiki_links(text):
for pagename in wikipages:
page_re = re.compile('(%s)' % pagename, re.IGNORECASE)
# text = page_re.sub("SHOES", text)
text = page_re.sub('[[\\1]]', text)
return text
def add_parameter(param, line, config_file):
# If we're only targeting a particular game, check this is one of
# the ones we're targeting.
if match_game and param.games and match_game not in param.games:
return
# Is this documenting a command line parameter?
match = re.search('(M_CheckParm(WithArgs)|M_ParmExists)?\s*\(\s*"(.*?)"',
line)
if match:
param.name = match.group(3)
categories[param.category].add_param(param)
return
# Documenting a configuration file variable?
match = re.search('CONFIG_VARIABLE_\S+\s*\(\s*(\S+?)\),', line)
if match:
param.name = match.group(1)
config_file.add_variable(param)
return
raise Exception(param.text)
def process_file(file):
current_config_file = None
f = open(file)
try:
param = None
waiting_for_checkparm = False
for line in f:
line = line.rstrip()
# Ignore empty lines
if re.match('\s*$', line):
continue
# Currently reading a doc comment?
if param:
# End of doc comment
if not re.match('\s*//', line):
waiting_for_checkparm = True
# The first non-empty line after the documentation comment
# ends must contain the thing being documented.
if waiting_for_checkparm:
add_parameter(param, line, current_config_file)
param = None
else:
# More documentation text
munged_line = re.sub('\s*\/\/\s*', '', line, 1)
munged_line = re.sub('\s*$', '', munged_line)
param.add_text(munged_line)
# Check for start of a doc comment
if re.search("//!", line):
match = re.search("@begin_config_file\s*(\S+)", line)
if match:
# Beginning a configuration file
tagname = match.group(1)
current_config_file = ConfigFile(tagname)
config_files[tagname] = current_config_file
else:
# Start of a normal comment
param = Parameter()
waiting_for_checkparm = False
finally:
f.close()
def process_files(path):
# Process all C source files.
if os.path.isdir(path):
files = glob.glob(path + "/*.c")
for file in files:
process_file(file)
else:
# Special case to allow a single file to be specified as a target
process_file(path)
def print_template(template_file, content):
f = open(template_file)
try:
for line in f:
match = INCLUDE_STATEMENT_RE.search(line)
if match:
filename = match.group(1)
print_template(filename, content)
else:
line = line.replace("@content", content)
print(line.rstrip())
finally:
f.close()
def manpage_output(targets, template_file):
content = ""
for t in targets:
content += t.manpage_output() + "\n"
content = content.replace("-", "\\-")
print_template(template_file, content)
def wiki_output(targets, template):
read_wikipages()
for t in targets:
print(t.wiki_output())
def plaintext_output(targets, template_file):
content = ""
for t in targets:
content += t.plaintext_output() + "\n"
print_template(template_file, content)
def usage():
print("Usage: %s [-V] [-c tag] [-g game] ( -m | -w | -p ) <dir>..." \
% sys.argv[0])
print(" -c : Provide documentation for the specified configuration file")
print(" (matches the given tag name in the source file)")
print(" -m : Manpage output")
print(" -w : Wikitext output")
print(" -p : Plaintext output")
print(" -V : Don't show Vanilla Doom options")
print(" -g : Only document options for specified game.")
sys.exit(0)
# Parse command line
opts, args = getopt.getopt(sys.argv[1:], "m:wp:c:g:V")
output_function = None
template = None
doc_config_file = None
match_game = None
for opt in opts:
if opt[0] == "-m":
output_function = manpage_output
template = opt[1]
elif opt[0] == "-w":
output_function = wiki_output
elif opt[0] == "-p":
output_function = plaintext_output
template = opt[1]
elif opt[0] == "-V":
show_vanilla_options = False
elif opt[0] == "-c":
doc_config_file = opt[1]
elif opt[0] == "-g":
match_game = opt[1]
if output_function == None or len(args) < 1:
usage()
else:
# Process specified files
for path in args:
process_files(path)
# Build a list of things to document
documentation_targets = []
if doc_config_file:
documentation_targets.append(config_files[doc_config_file])
else:
documentation_targets.append(categories[None])
for c in categories:
if c != None:
documentation_targets.append(categories[c])
# Generate the output
output_function(documentation_targets, template)