ref: 0bf70e16ef522e27a919f19b89abfa22d56d691c
dir: /man/docgen/
#!/usr/bin/env python3 # # 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 io import sys import os import re import glob import getopt TEXT_WRAP_WIDTH = 78 INCLUDE_STATEMENT_RE = re.compile("@include\s+(\S+)") # Use appropriate stdout function for Python 2 or 3 def stdout(buf): if sys.version_info.major < 3: sys.stdout.write(buf) else: sys.stdout.buffer.write(buf) # 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 markdown_output(self): result = "## %s\n\n| Parameter | Description |\n| - | - |\n" % self.description self.params.sort() for p in self.params: if p.should_show(): result += p.markdown_output() result = result.rstrip() + "\n" return result def completion_output(self): result = "" self.params.sort() for p in self.params: if p.should_show(): result += p.completion_output(0) result = result.rstrip() 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("&", "&") result = result.replace("<", "<") result = result.replace(">", ">") return result categories = ( (None, Category("General options")), ("game", Category("Game start options")), ("video", Category("Display options")), ("net", Category("Networking options")), ("mod", Category("Dehacked and WAD merging")), ("demo", Category("Demo options")), ("compat", Category("Compatibility")), ("obscure", Category("Obscure and less-used options")), ) 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 markdown_output(self): if self.args: name = "%s %s" % (self.name, self.args) else: name = "%s" % self.name name = name.replace("|", "\\|") text = self.text if self.platform: text += " (%s only)" % self.platform text = text.replace("|", "\\|") result = "| %s | %s |\n" % (name, text) # html escape result = result.replace("<", "<") result = result.replace(">", ">") return result def plaintext_output(self, indent): # Build the first line, with the argument on start = " " + self.name if self.args: start += " " + self.args # pad up to the plaintext width start += " " * (indent - len(start)) # 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 = "" words = [word for word in re.split('\s+', description) if word] maxlen = TEXT_WRAP_WIDTH - indent outlines = [[]] for word in words: linelen = sum(len(w) + 1 for w in outlines[-1]) if linelen + len(word) > maxlen: outlines.append([]) outlines[-1].append(word) linesep = "\n" + " " * indent return (start + linesep.join(" ".join(line) for line in outlines) + "\n\n") def completion_output(self, w): result = self.name + " " return result # Read list of wiki pages def read_wikipages(): f = io.open("wikipages", encoding='UTF-8') try: for line in f: line = line.rstrip() line = re.sub('\#.*$', '', line) if not re.match(r'^\s*$', line): wikipages.append(line) finally: f.close() # Add wiki page links def add_wiki_links(text): def replace_name(m): linktext = m.group(1) if linktext == pagename: return '[[%s]]' % pagename else: return '[[%s|%s]]' % (pagename, linktext) for pagename in wikipages: text = re.sub(r'\b(%s)\b' % re.escape(pagename), replace_name, text, count=0, flags=re.IGNORECASE) 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) category = dict(categories)[param.category] 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(filename): current_config_file = None f = io.open(filename, encoding='UTF-8') 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 filename in files: process_file(filename) else: # Special case to allow a single file to be specified as a target process_file(path) def print_template(template_file, substs, content): f = io.open(template_file, encoding='UTF-8') try: for line in f: match = INCLUDE_STATEMENT_RE.search(line) if match: filename = match.group(1) filename = os.path.join(os.path.dirname(template_file), filename) print_template(filename, substs, content) else: line = line.replace("@content", content) for k,v in substs.items(): line = line.replace(k,v) stdout(line.rstrip().encode('UTF-8') + b'\n') finally: f.close() def manpage_output(targets, substs, template_file): content = "" for t in targets: content += t.manpage_output() + "\n" content = content.replace("-", "\\-") print_template(template_file, substs, content) def wiki_output(targets, _, template): read_wikipages() for t in targets: stdout(t.wiki_output().encode('UTF-8') + b'\n') def markdown_output(targets, substs, template_file): content = "" for t in targets: content += t.markdown_output() + "\n" print_template(template_file, substs, content) def plaintext_output(targets, substs, template_file): content = "" for t in targets: content += t.plaintext_output() + "\n" print_template(template_file, substs, content) def completion_output(targets, substs, template_file): content = "" for t in targets: content += t.completion_output() + "\n" print_template(template_file, substs, content) def usage(): print("Usage: %s [-V] [-c tag] [-g game] -n program_name -s package_name [ -z shortname ] ( -M | -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(" -s : Package name, e.g. Chocolate Doom (for substitution)") print(" -z : Package short-name, e.g. Chocolate (for substitution)") print(" -n : Program name, e.g. chocolate (for substitution)") print(" -M : Markdown output") print(" -m : Manpage output") print(" -w : Wikitext output") print(" -p : Plaintext output") print(" -b : Bash-Completion 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:], "n:s:z:M:m:wp:b:c:g:V") output_function = None template = None doc_config_file = None match_game = None substs = {} for opt in opts: if opt[0] == "-n": substs["@PROGRAM_SPREFIX@"] = opt[1] if opt[0] == "-s": substs["@PACKAGE_NAME@"] = opt[1] if opt[0] == "-z": substs["@PACKAGE_SHORTNAME@"] = opt[1] if opt[0] == "-m": output_function = manpage_output template = opt[1] elif opt[0] == "-M": output_function = markdown_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] == "-b": output_function = completion_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] substs["@GAME@"] = opt[1] substs["@GAME_UPPER@"] = opt[1].title() if "doom" == opt[1]: substs["@CFGFILE@"] = "default.cfg" else: substs["@CFGFILE@"] = opt[1] + ".cfg" 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 if doc_config_file: documentation_targets = [config_files[doc_config_file]] else: documentation_targets = [c for _, c in categories] # Generate the output output_function(documentation_targets, substs, template)