Mercurial > public > mercurial-scm > hg
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) |