comparison doc/gendoc.py @ 52016:1f5974f8f730

doc: refactor gendoc for better reusability This change separates the gathering of commands/topics/etc from the logic of printing their documentation out.
author Ludovic Chabant <ludovic@chabant.com>
date Mon, 09 Oct 2023 22:11:21 -0700
parents 76387080f238
children 2a875530a023
comparison
equal deleted inserted replaced
52015:a47f09da8bd1 52016:1f5974f8f730
6 6
7 7
8 import os 8 import os
9 import sys 9 import sys
10 import textwrap 10 import textwrap
11 import argparse
11 12
12 try: 13 try:
13 import msvcrt 14 import msvcrt
14 15
15 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) 16 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
113 d[b'synopsis'] = s.strip() 114 d[b'synopsis'] = s.strip()
114 115
115 return d 116 return d
116 117
117 118
118 def showdoc(ui): 119 def showdoc(ui, debugcmds=False):
119 # print options 120 # print options
120 ui.write(minirst.section(_(b"Options"))) 121 ui.write(minirst.section(_(b"Options")))
121 multioccur = False 122 multioccur = False
122 for optstr, desc in get_opts(globalopts): 123 for optstr, desc in get_opts(globalopts):
123 ui.write(b"%s\n %s\n\n" % (optstr, desc)) 124 ui.write(b"%s\n %s\n\n" % (optstr, desc))
127 ui.write(_(b"\n[+] marked option can be specified multiple times\n")) 128 ui.write(_(b"\n[+] marked option can be specified multiple times\n"))
128 ui.write(b"\n") 129 ui.write(b"\n")
129 130
130 # print cmds 131 # print cmds
131 ui.write(minirst.section(_(b"Commands"))) 132 ui.write(minirst.section(_(b"Commands")))
132 commandprinter(ui, table, minirst.subsection, minirst.subsubsection) 133 commandprinter(
134 ui,
135 table,
136 minirst.subsection,
137 minirst.subsubsection,
138 debugcmds=debugcmds,
139 )
133 140
134 # print help topics 141 # print help topics
135 # The config help topic is included in the hgrc.5 man page. 142 # The config help topic is included in the hgrc.5 man page.
136 helpprinter(ui, helptable, minirst.section, exclude=[b'config']) 143 topics = findtopics(helptable, exclude=[b'config'])
144 helpprinter(ui, topics, minirst.section)
137 145
138 ui.write(minirst.section(_(b"Extensions"))) 146 ui.write(minirst.section(_(b"Extensions")))
139 ui.write( 147 ui.write(
140 _( 148 _(
141 b"This section contains help for extensions that are " 149 b"This section contains help for extensions that are "
164 commandprinter( 172 commandprinter(
165 ui, 173 ui,
166 cmdtable, 174 cmdtable,
167 minirst.subsubsubsection, 175 minirst.subsubsubsection,
168 minirst.subsubsubsubsection, 176 minirst.subsubsubsubsection,
177 debugcmds=debugcmds,
169 ) 178 )
170 179
171 180
172 def showtopic(ui, topic): 181 def gettopicstable():
173 extrahelptable = [ 182 extrahelptable = [
174 ([b"common"], b'', loaddoc(b'common'), help.TOPIC_CATEGORY_MISC), 183 ([b"common"], b'', loaddoc(b'common'), help.TOPIC_CATEGORY_MISC),
175 ([b"hg.1"], b'', loaddoc(b'hg.1'), help.TOPIC_CATEGORY_CONFIG), 184 ([b"hg.1"], b'', loaddoc(b'hg.1'), help.TOPIC_CATEGORY_CONFIG),
176 ([b"hg-ssh.8"], b'', loaddoc(b'hg-ssh.8'), help.TOPIC_CATEGORY_CONFIG), 185 ([b"hg-ssh.8"], b'', loaddoc(b'hg-ssh.8'), help.TOPIC_CATEGORY_CONFIG),
177 ( 186 (
179 b'', 188 b'',
180 loaddoc(b'hgignore.5'), 189 loaddoc(b'hgignore.5'),
181 help.TOPIC_CATEGORY_CONFIG, 190 help.TOPIC_CATEGORY_CONFIG,
182 ), 191 ),
183 ([b"hgrc.5"], b'', loaddoc(b'hgrc.5'), help.TOPIC_CATEGORY_CONFIG), 192 ([b"hgrc.5"], b'', loaddoc(b'hgrc.5'), help.TOPIC_CATEGORY_CONFIG),
193 ([b"hg-ssh.8.gendoc"], b'', b'', help.TOPIC_CATEGORY_CONFIG),
184 ( 194 (
185 [b"hgignore.5.gendoc"], 195 [b"hgignore.5.gendoc"],
186 b'', 196 b'',
187 loaddoc(b'hgignore'), 197 loaddoc(b'hgignore'),
188 help.TOPIC_CATEGORY_CONFIG, 198 help.TOPIC_CATEGORY_CONFIG,
192 b'', 202 b'',
193 loaddoc(b'config'), 203 loaddoc(b'config'),
194 help.TOPIC_CATEGORY_CONFIG, 204 help.TOPIC_CATEGORY_CONFIG,
195 ), 205 ),
196 ] 206 ]
197 helpprinter(ui, helptable + extrahelptable, None, include=[topic]) 207 return helptable + extrahelptable
198 208
199 209
200 def helpprinter(ui, helptable, sectionfunc, include=[], exclude=[]): 210 def findtopics(helptable, include=[], exclude=[]):
211 """Find topics whose names match the given include/exclude rules
212
213 Note that exclude rules take precedence over include rules.
214 """
215 found = []
201 for h in helptable: 216 for h in helptable:
202 names, sec, doc = h[0:3] 217 names, sec, doc = h[0:3]
203 if exclude and names[0] in exclude: 218 if exclude and names[0] in exclude:
204 continue 219 continue
205 if include and names[0] not in include: 220 if include and names[0] not in include:
206 continue 221 continue
222 found.append((names, sec, doc))
223 return found
224
225
226 def showtopic(ui, topic, wraptpl=False):
227 """Render a help topic
228
229 Args:
230 ui: the UI object to output to
231 topic: the topic name to output
232 wraptpl: whether to wrap the output in the individual help topic
233 pages' header/footer
234 """
235 found = findtopics(gettopicstable(), include=[topic])
236 if not found:
237 ui.write_err(_(b"ERROR: no such topic: %s\n") % topic)
238 sys.exit(1)
239
240 if wraptpl:
241 header = _rendertpl(
242 'topicheader.txt',
243 {'topicname': topic, 'topictitle': minirst.section(found[0][1])},
244 )
245 ui.write(header.encode())
246 helpprinter(ui, found, None)
247 return True
248
249
250 def helpprinter(ui, topics, sectionfunc):
251 """Print a help topic
252
253 Args:
254 ui: the UI object to output to
255 topics: a list of help topics to output
256 sectionfunc: a callback to write the section title
257 """
258 for h in topics:
259 names, sec, doc = h[0:3]
207 for name in names: 260 for name in names:
208 ui.write(b".. _%s:\n" % name) 261 ui.write(b".. _%s:\n" % name)
209 ui.write(b"\n") 262 ui.write(b"\n")
210 if sectionfunc: 263 if sectionfunc:
211 ui.write(sectionfunc(sec)) 264 ui.write(sectionfunc(sec))
213 doc = doc(ui) 266 doc = doc(ui)
214 ui.write(doc) 267 ui.write(doc)
215 ui.write(b"\n") 268 ui.write(b"\n")
216 269
217 270
218 def commandprinter(ui, cmdtable, sectionfunc, subsectionfunc): 271 def commandprinter(ui, cmdtable, sectionfunc, subsectionfunc, debugcmds=False):
219 """Render restructuredtext describing a list of commands and their 272 """Render restructuredtext describing a list of commands and their
220 documentations, grouped by command category. 273 documentations, grouped by command category.
221 274
222 Args: 275 Args:
223 ui: UI object to write the output to 276 ui: UI object to write the output to
234 '[-p|-d|-s] [-f] [-r] [REV...]' 287 '[-p|-d|-s] [-f] [-r] [REV...]'
235 ) 288 )
236 sectionfunc: minirst function to format command category headers 289 sectionfunc: minirst function to format command category headers
237 subsectionfunc: minirst function to format command headers 290 subsectionfunc: minirst function to format command headers
238 """ 291 """
239 h = {} 292 h = allcommandnames(cmdtable, debugcmds=debugcmds)
240 for c, attr in cmdtable.items():
241 f = c.split(b"|")[0]
242 f = f.lstrip(b"^")
243 h[f] = c
244 cmds = h.keys() 293 cmds = h.keys()
245 294
246 def helpcategory(cmd): 295 def helpcategory(cmd):
247 """Given a canonical command name from `cmds` (above), retrieve its 296 """Given a canonical command name from `cmds` (above), retrieve its
248 help category. If helpcategory is None, default to CATEGORY_NONE. 297 help category. If helpcategory is None, default to CATEGORY_NONE.
275 # For now, the category header is at the same level as the headers for 324 # For now, the category header is at the same level as the headers for
276 # the commands in the category; this is fixed in the next commit. 325 # the commands in the category; this is fixed in the next commit.
277 ui.write(sectionfunc(help.CATEGORY_NAMES[category])) 326 ui.write(sectionfunc(help.CATEGORY_NAMES[category]))
278 # Print each command in the category 327 # Print each command in the category
279 for f in sorted(categorycmds): 328 for f in sorted(categorycmds):
280 if f.startswith(b"debug"):
281 continue
282 d = get_cmd(h[f], cmdtable) 329 d = get_cmd(h[f], cmdtable)
283 ui.write(subsectionfunc(d[b'cmd'])) 330 ui.write(subsectionfunc(d[b'cmd']))
284 # short description 331 # short description
285 ui.write(d[b'desc'][0]) 332 ui.write(d[b'desc'][0])
286 # synopsis 333 # synopsis
291 # synopsis 338 # synopsis
292 ui.write(b" %s\n" % line) 339 ui.write(b" %s\n" % line)
293 ui.write(b'\n') 340 ui.write(b'\n')
294 # description 341 # description
295 ui.write(b"%s\n\n" % d[b'desc'][1]) 342 ui.write(b"%s\n\n" % d[b'desc'][1])
343
296 # options 344 # options
297 opt_output = list(d[b'opts']) 345 def _optsection(s):
298 if opt_output: 346 return b"%s:\n\n" % s
299 opts_len = max([len(line[0]) for line in opt_output]) 347
300 ui.write(_(b"Options:\n\n")) 348 _optionsprinter(ui, d, _optsection)
301 multioccur = False
302 for optstr, desc in opt_output:
303 if desc:
304 s = b"%-*s %s" % (opts_len, optstr, desc)
305 else:
306 s = optstr
307 ui.write(b"%s\n" % s)
308 if optstr.endswith(b"[+]>"):
309 multioccur = True
310 if multioccur:
311 ui.write(
312 _(
313 b"\n[+] marked option can be specified"
314 b" multiple times\n"
315 )
316 )
317 ui.write(b"\n")
318 # aliases 349 # aliases
319 if d[b'aliases']: 350 if d[b'aliases']:
320 # Note the empty comment, this is required to separate this 351 # Note the empty comment, this is required to separate this
321 # (which should be a blockquote) from any preceding things (such 352 # (which should be a blockquote) from any preceding things (such
322 # as a definition list). 353 # as a definition list).
323 ui.write( 354 ui.write(
324 _(b"..\n\n aliases: %s\n\n") % b" ".join(d[b'aliases']) 355 _(b"..\n\n aliases: %s\n\n") % b" ".join(d[b'aliases'])
325 ) 356 )
326 357
327 358
359 def _optionsprinter(ui, cmd, sectionfunc):
360 """Outputs the list of options for a given command object"""
361 opt_output = list(cmd[b'opts'])
362 if opt_output:
363 opts_len = max([len(line[0]) for line in opt_output])
364 ui.write(sectionfunc(_(b"Options")))
365 multioccur = False
366 for optstr, desc in opt_output:
367 if desc:
368 s = b"%-*s %s" % (opts_len, optstr, desc)
369 else:
370 s = optstr
371 ui.write(b"%s\n" % s)
372 if optstr.endswith(b"[+]>"):
373 multioccur = True
374 if multioccur:
375 ui.write(
376 _(b"\n[+] marked option can be specified multiple times\n")
377 )
378 ui.write(b"\n")
379
380
381 def allcommandnames(cmdtable, debugcmds=False):
382 """Get a collection of all command names in the given command table
383
384 Args:
385 cmdtable: the command table to get the names from
386 debugcmds: whether to include debug commands
387
388 Returns a dictionary where the keys are the main command names, and the
389 values are the "raw" names (in the form of `name|alias1|alias2`).
390 """
391 allcmdnames = {}
392 for rawnames, attr in cmdtable.items():
393 mainname = rawnames.split(b"|")[0].lstrip(b"^")
394 if not debugcmds and mainname.startswith(b"debug"):
395 continue
396 allcmdnames[mainname] = rawnames
397 return allcmdnames
398
399
328 def allextensionnames(): 400 def allextensionnames():
401 """Get a set of all known extension names"""
329 return set(extensions.enabled().keys()) | set(extensions.disabled().keys()) 402 return set(extensions.enabled().keys()) | set(extensions.disabled().keys())
330 403
331 404
332 if __name__ == "__main__": 405 if __name__ == "__main__":
333 doc = b'hg.1.gendoc' 406 parser = argparse.ArgumentParser(
334 if len(sys.argv) > 1: 407 prog='gendoc', description="Generate mercurial documentation files"
335 doc = encoding.strtolocal(sys.argv[1]) 408 )
409 parser.add_argument('doc', default='hg.1.gendoc', nargs='?')
410 parser.add_argument(
411 '-d',
412 '--debug-cmds',
413 action='store_true',
414 help="Show debug commands in help pages",
415 )
416 args = parser.parse_args()
417
418 doc = encoding.strtolocal(args.doc)
419 debugcmds = args.debug_cmds
336 420
337 ui = uimod.ui.load() 421 ui = uimod.ui.load()
338 # Trigger extensions to load. This is disabled by default because it uses 422 # Trigger extensions to load. This is disabled by default because it uses
339 # the current user's configuration, which is often not what is wanted. 423 # the current user's configuration, which is often not what is wanted.
340 if encoding.environ.get(b'GENDOC_LOAD_CONFIGURED_EXTENSIONS', b'0') != b'0': 424 if encoding.environ.get(b'GENDOC_LOAD_CONFIGURED_EXTENSIONS', b'0') != b'0':
341 extensions.loadall(ui) 425 extensions.loadall(ui)
342 426
427 # ui.debugflag determines if the help module returns debug commands to us.
428 ui.debugflag = debugcmds
429
343 if doc == b'hg.1.gendoc': 430 if doc == b'hg.1.gendoc':
344 showdoc(ui) 431 showdoc(ui)
345 else: 432 else:
346 showtopic(ui, encoding.strtolocal(sys.argv[1])) 433 showtopic(ui, doc)