Add other missing files to dist.
[chocolate-doom.git] / man / docgen
blob73e8c85248bafe41c4e4d064606e6ca2d1f89f58
1 #!/usr/bin/env python
2 #
3 # Chocolate Doom self-documentation tool. This works similar to javadoc
4 # or doxygen, but documents command line parameters and configuration
5 # file values, generating documentation in Unix manpage, wikitext and
6 # plain text forms.
8 # Comments are read from the source code in the following form:
10 # //!
11 # // @arg <extra arguments>
12 # // @category Category
13 # // @platform <some platform that the parameter is specific to>
14 # //
15 # // Long description of the parameter
16 # //
18 # something_involving = M_CheckParm("-param");
20 # For configuration file values:
22 # //! @begin_config_file myconfig.cfg
24 # //!
25 # // Description of the configuration file value.
26 # //
28 # CONFIG_VARIABLE_INT(my_variable, c_variable),
31 import sys
32 import os
33 import re
34 import glob
35 import getopt
37 # Find the maximum width of a list of parameters (for plain text output)
39 def parameter_list_width(params):
40 w = 0
42 for p in params:
43 pw = len(p.name) + 5
45 if p.args:
46 pw += len(p.args)
48 if pw > w:
49 w = pw
51 return w
53 class ConfigFile:
54 def __init__(self, filename):
55 self.filename = filename
56 self.variables = []
58 def add_variable(self, variable):
59 self.variables.append(variable)
61 def manpage_output(self):
62 result = ".SH CONFIGURATION VARIABLES\n"
64 for v in self.variables:
65 result += ".TP\n"
66 result += v.manpage_output()
68 return result
70 def plaintext_output(self):
71 result = ""
73 w = parameter_list_width(self.variables)
75 for p in self.variables:
76 result += p.plaintext_output(w)
78 result = result.rstrip() + "\n"
80 return result
82 class Category:
83 def __init__(self, description):
84 self.description = description
85 self.params = []
87 def add_param(self, param):
88 self.params.append(param)
90 # Plain text output
92 def plaintext_output(self):
93 result = "=== %s ===\n\n" % self.description
95 self.params.sort()
97 w = parameter_list_width(self.params)
99 for p in self.params:
100 if p.should_show():
101 result += p.plaintext_output(w)
103 result = result.rstrip() + "\n"
105 return result
107 def manpage_output(self):
108 result = ".SH " + self.description.upper() + "\n"
110 self.params.sort()
112 for p in self.params:
113 if p.should_show():
114 result += ".TP\n"
115 result += p.manpage_output()
117 return result
119 def wiki_output(self):
120 result = "=== %s ===\n" % self.description
122 self.params.sort()
124 for p in self.params:
125 if p.should_show():
126 result += "; " + p.wiki_output() + "\n"
128 # Escape special HTML characters
130 result = result.replace("&", "&amp;")
131 result = result.replace("<", "&lt;")
132 result = result.replace(">", "&gt;")
134 return result
136 categories = {
137 None: Category("General options"),
138 "video": Category("Display options"),
139 "demo": Category("Demo options"),
140 "net": Category("Networking options"),
141 "mod": Category("Dehacked and WAD merging"),
142 "compat": Category("Compatibility"),
145 wikipages = []
146 config_files = {}
148 # Show options that are in Vanilla Doom? Or only new options?
150 show_vanilla_options = True
152 class Parameter:
153 def __cmp__(self, other):
154 if self.name < other.name:
155 return -1
156 else:
157 return 1
159 def __init__(self):
160 self.text = ""
161 self.name = ""
162 self.args = None
163 self.platform = None
164 self.category = None
165 self.vanilla_option = False
167 def should_show(self):
168 return not self.vanilla_option or show_vanilla_options
170 def add_text(self, text):
171 if len(text) <= 0:
172 pass
173 elif text[0] == "@":
174 match = re.match('@(\S+)\s*(.*)', text)
176 if not match:
177 raise "Malformed option line: %s" % text
179 option_type = match.group(1)
180 data = match.group(2)
182 if option_type == "arg":
183 self.args = data
184 elif option_type == "platform":
185 self.platform = data
186 elif option_type == "category":
187 self.category = data
188 elif option_type == "vanilla":
189 self.vanilla_option = True
190 else:
191 raise "Unknown option type '%s'" % option_type
193 else:
194 self.text += text + " "
196 def manpage_output(self):
197 result = self.name
199 if self.args:
200 result += " " + self.args
202 result = '\\fB' + result + '\\fR'
204 result += "\n"
206 if self.platform:
207 result += "[%s only] " % self.platform
209 escaped = re.sub('\\\\', '\\\\\\\\', self.text)
211 result += escaped + "\n"
213 return result
215 def wiki_output(self):
216 result = self.name
218 if self.args:
219 result += " " + self.args
221 result += ": "
223 result += add_wiki_links(self.text)
225 if self.platform:
226 result += "'''(%s only)'''" % self.platform
228 return result
230 def plaintext_output(self, w):
232 # Build the first line, with the argument on
234 line = " " + self.name
235 if self.args:
236 line += " " + self.args
238 # pad up to the plaintext width
240 line += " " * (w - len(line))
242 # Build the description text
244 description = self.text
246 if self.platform:
247 description += " (%s only)" % self.platform
249 # Build the complete text for the argument
250 # Split the description into words and add a word at a time
252 result = ""
253 for word in re.split('\s+', description):
255 # Break onto the next line?
257 if len(line) + len(word) + 1 > 75:
258 result += line + "\n"
259 line = " " * w
261 # Add another word
263 line += word + " "
265 result += line + "\n\n"
267 return result
269 # Read list of wiki pages
271 def read_wikipages():
272 f = open("wikipages")
274 try:
275 for line in f:
276 line = line.rstrip()
278 line = re.sub('\#.*$', '', line)
280 if not re.match('^\s*$', line):
281 wikipages.append(line)
282 finally:
283 f.close()
285 # Add wiki page links
287 def add_wiki_links(text):
288 for pagename in wikipages:
289 page_re = re.compile('(%s)' % pagename, re.IGNORECASE)
290 # text = page_re.sub("SHOES", text)
291 text = page_re.sub('[[\\1]]', text)
293 return text
295 def add_parameter(param, line, config_file):
297 # Is this documenting a command line parameter?
299 match = re.search('M_CheckParm\s*\(\s*"(.*?)"\s*\)', line)
301 if match:
302 param.name = match.group(1)
303 categories[param.category].add_param(param)
304 return
306 # Documenting a configuration file variable?
308 match = re.search('CONFIG_VARIABLE_\S+\s*\(\s*(\S+?),', line)
310 if match:
311 param.name = match.group(1)
312 config_file.add_variable(param)
313 return
315 raise Exception(param.text)
317 def process_file(file):
319 current_config_file = None
321 f = open(file)
323 try:
324 param = None
325 waiting_for_checkparm = False
327 for line in f:
328 line = line.rstrip()
330 # Ignore empty lines
332 if re.match('\s*$', line):
333 continue
335 # Currently reading a doc comment?
337 if param:
338 # End of doc comment
340 if not re.match('\s*//', line):
341 waiting_for_checkparm = True
343 # The first non-empty line after the documentation comment
344 # ends must contain the thing being documented.
346 if waiting_for_checkparm:
347 add_parameter(param, line, current_config_file)
348 param = None
349 else:
350 # More documentation text
352 munged_line = re.sub('\s*\/\/\s*', '', line, 1)
353 munged_line = re.sub('\s*$', '', munged_line)
354 param.add_text(munged_line)
356 # Check for start of a doc comment
358 if re.search("//!", line):
359 match = re.search("@begin_config_file\s*(\S+)", line)
361 if match:
362 # Beginning a configuration file
363 filename = match.group(1)
364 current_config_file = ConfigFile(filename)
365 config_files[filename] = current_config_file
366 else:
367 # Start of a normal comment
368 param = Parameter()
369 waiting_for_checkparm = False
370 finally:
371 f.close()
373 def process_files(dir):
374 # Process all C source files.
376 if os.path.isdir(dir):
377 files = glob.glob(dir + "/*.c")
379 for file in files:
380 process_file(file)
381 else:
382 # Special case to allow a single file to be specified as a target
384 process_file(dir)
386 def print_template(template_file, content):
387 f = open(template_file)
389 try:
390 for line in f:
391 line = line.replace("@content", content)
392 print line.rstrip()
394 finally:
395 f.close()
397 def manpage_output(targets, template_file):
399 content = ""
401 for t in targets:
402 content += t.manpage_output() + "\n"
404 print_template(template_file, content)
406 def wiki_output(targets, template):
407 read_wikipages()
409 for t in targets:
410 print t.wiki_output()
412 def plaintext_output(targets, template_file):
414 content = ""
416 for t in targets:
417 content += t.plaintext_output() + "\n"
419 print_template(template_file, content)
421 def usage():
422 print "Usage: %s [-V] [-c filename ]( -m | -w | -p ) <directory>" \
423 % sys.argv[0]
424 print " -c : Provide documentation for the specified configuration file"
425 print " -m : Manpage output"
426 print " -w : Wikitext output"
427 print " -p : Plaintext output"
428 print " -V : Don't show Vanilla Doom options"
429 sys.exit(0)
431 # Parse command line
433 opts, args = getopt.getopt(sys.argv[1:], "m:wp:c:V")
435 output_function = None
436 template = None
437 doc_config_file = None
439 for opt in opts:
440 if opt[0] == "-m":
441 output_function = manpage_output
442 template = opt[1]
443 elif opt[0] == "-w":
444 output_function = wiki_output
445 elif opt[0] == "-p":
446 output_function = plaintext_output
447 template = opt[1]
448 elif opt[0] == "-V":
449 show_vanilla_options = False
450 elif opt[0] == "-c":
451 doc_config_file = opt[1]
453 if output_function == None or len(args) != 1:
454 usage()
455 else:
457 # Process specified files
459 process_files(args[0])
461 # Build a list of things to document
463 documentation_targets = []
465 if doc_config_file:
466 documentation_targets.append(config_files[doc_config_file])
467 else:
468 documentation_targets.append(categories[None])
470 for c in categories:
471 if c != None:
472 documentation_targets.append(categories[c])
474 # Generate the output
476 output_function(documentation_targets, template)