diff doc/gendoc.py @ 52021:2a875530a023

doc: generate separate commands/topics/extension pages This change modifies gendoc.py and Makefile so that individual pages for commands, help topics, and extensions can be generated. A new index page is also generated with links to all these pages. This makes it easier to look up and search the help text of a given command or topic, instead of having to deal with the giant hg.1 "all-in-one" page. Since the list of individual pages varies based on the source code, we generate a dynamic Makefile that contains this list of files as individual targets. This gives us fine-grained control over output files. However, it greatly increases the time spent generating all help pages. It's recommended to run make with -j to make use of multi-core archs. Individual man pages are produced in doc/man, and HTML ones are in doc/html
author Ludovic Chabant <ludovic@chabant.com>
date Mon, 09 Oct 2023 22:14:24 -0700
parents 1f5974f8f730
children
line wrap: on
line diff
--- a/doc/gendoc.py	Mon Oct 09 22:11:21 2023 -0700
+++ b/doc/gendoc.py	Mon Oct 09 22:14:24 2023 -0700
@@ -178,6 +178,202 @@
             )
 
 
+def showcommandlist(ui, debugcmds=False):
+    """Render a plain text list of all command names
+
+    Args:
+        ui: the UI object to output to
+        debugcmds: whether to include debug commands
+    """
+    cmdnames = allcommandnames(table, debugcmds=debugcmds)
+    for mainname in cmdnames.keys():
+        # Make does not like semicolons in filenames (or what it
+        # considers as filenames). We use command names as targets so
+        # it applies here. For now let's skip commands with semicolons
+        # in them (at this time it only includes the `admin::verify`
+        # advanced command).
+        if b'::' in mainname:
+            continue
+        ui.write(mainname)
+        ui.write(b" ")
+
+
+def showtopiclist(ui):
+    """Render a plain text list of all help topic names
+
+    Args:
+        ui: the UI object to output to
+    """
+    for topic in helptable:
+        topicname = topic[0][0]
+        if help.filtertopic(ui, topicname):
+            continue
+        ui.write(topicname)
+        ui.write(b" ")
+
+
+def showextensionlist(ui):
+    """Render a plain text list of all extension names
+
+    Args:
+        ui: the UI object to output to
+    """
+    for extensionname in allextensionnames():
+        ui.write(extensionname)
+        ui.write(b" ")
+
+
+def showhelpindex(ui, debugcmds=False):
+    """Render restructured text for a complete mercurial help index
+
+    This index will show a list of commands, followed by a list of help topics,
+    and finally a list of extensions. These lists are split in categories and
+    ordered 'nicely' as defined by alphabetical and categeory order.
+
+    Each entry in this index is a reference to the specific help page of the
+    command, topic, or extension at hand.
+    """
+    ui.write(minirst.section(_(b"Mercurial Distributed SCM")))
+
+    missingdoc = _(b"(no help text available)")
+
+    cats, h, syns = help._getcategorizedhelpcmds(ui, table, None)
+    ui.write(minirst.subsection(_(b"Commands")))
+
+    for cat in help.CATEGORY_ORDER:
+        catfns = sorted(cats.get(cat, []))
+        if not catfns:
+            continue
+
+        catname = gettext(help.CATEGORY_NAMES[cat])
+        ui.write(minirst.subsubsection(catname))
+        for c in catfns:
+            url = b'hg-%s.html' % c
+            ui.write(b" :`%s <%s>`__: %s" % (c, url, h[c]))
+            syns[c].remove(c)
+            if syns[c]:
+                ui.write(_(b" (aliases: *%s*)") % (b', '.join(syns[c])))
+            ui.write(b"\n")
+        ui.write(b"\n\n")
+
+    ui.write(b"\n\n")
+
+    ui.write(minirst.subsection(_(b"Additional Help Topics")))
+    topiccats, topicsyns = help._getcategorizedhelptopics(ui, helptable)
+    for cat in help.TOPIC_CATEGORY_ORDER:
+        topics = topiccats.get(cat, [])
+        if not topics:
+            continue
+
+        catname = gettext(help.TOPIC_CATEGORY_NAMES[cat])
+        ui.write(minirst.subsubsection(catname))
+        for t, desc in topics:
+            url = b'topic-%s.html' % t
+            ui.write(b" :`%s <%s>`__: %s" % (t, url, desc))
+            topicsyns[t].remove(t)
+            if topicsyns[t]:
+                ui.write(_(b" (aliases: *%s*)") % (b', '.join(topicsyns[t])))
+            ui.write(b"\n")
+        ui.write(b"\n\n")
+
+    ui.write(b"\n\n")
+
+    # Add an alphabetical list of extensions, categorized by group.
+    sectionkeywords = [
+        (b"(ADVANCED)", _(b"(ADVANCED)")),
+        (b"(EXPERIMENTAL)", _(b"(EXPERIMENTAL)")),
+        (b"(DEPRECATED)", _(b"(DEPRECATED)")),
+    ]
+    extensionsections = [
+        (b"Extensions", []),
+        (b"Advanced Extensions", []),
+        (b"Experimental Extensions", []),
+        (b"Deprecated Extensions", []),
+    ]
+    for extensionname in allextensionnames():
+        mod = extensions.load(ui, extensionname, None)
+        shortdoc, longdoc = _splitdoc(mod)
+        for i, kwds in enumerate(sectionkeywords):
+            if any([kwd in shortdoc for kwd in kwds]):
+                extensionsections[i + 1][1].append(
+                    (extensionname, mod, shortdoc)
+                )
+                break
+        else:
+            extensionsections[0][1].append((extensionname, mod, shortdoc))
+    for sectiontitle, extinfos in extensionsections:
+        ui.write(minirst.subsection(_(sectiontitle)))
+        for extinfo in sorted(extinfos, key=lambda ei: ei[0]):
+            extensionname, mod, shortdoc = extinfo
+            url = b'ext-%s.html' % extensionname
+            ui.write(
+                minirst.subsubsection(b'`%s <%s>`__' % (extensionname, url))
+            )
+            ui.write(shortdoc)
+            ui.write(b'\n\n')
+            cmdtable = getattr(mod, 'cmdtable', None)
+            if cmdtable:
+                cmdnames = allcommandnames(cmdtable, debugcmds=debugcmds)
+                for f in sorted(cmdnames.keys()):
+                    d = get_cmd(cmdnames[f], cmdtable)
+                    ui.write(b':%s: ' % d[b'cmd'])
+                    ui.write(d[b'desc'][0] or (missingdoc + b"\n"))
+                    ui.write(b'\n')
+            ui.write(b'\n')
+
+
+def showcommand(ui, mainname):
+    # Always pass debugcmds=True so that we find whatever command we are told
+    # to display.
+    cmdnames = allcommandnames(table, debugcmds=True)
+    allnames = cmdnames[mainname]
+    d = get_cmd(allnames, table)
+
+    header = _rendertpl(
+        'cmdheader.txt',
+        {
+            'cmdname': mainname,
+            'cmdtitle': minirst.section(b'hg ' + mainname),
+            'cmdshortdesc': minirst.subsection(d[b'desc'][0]),
+            'cmdlongdesc': d[b'desc'][1],
+            'cmdsynopsis': d[b'synopsis'],
+        },
+    )
+    ui.write(header.encode())
+
+    _optionsprinter(ui, d, minirst.subsubsection)
+    if d[b'aliases']:
+        ui.write(minirst.subsubsection(_(b"Aliases")))
+        ui.write(b"::\n\n   ")
+        ui.write(b", ".join(d[b'aliases']))
+        ui.write(b"\n")
+
+
+def _splitdoc(obj):
+    objdoc = pycompat.getdoc(obj)
+    firstnl = objdoc.find(b'\n')
+    if firstnl > 0:
+        shortdoc = objdoc[:firstnl]
+        longdoc = objdoc[firstnl + 1 :]
+    else:
+        shortdoc = objdoc
+        longdoc = ''
+    return shortdoc.lstrip(), longdoc.lstrip()
+
+
+def _rendertpl(tplname, data):
+    tplpath = os.path.join(os.path.dirname(__file__), 'templates', tplname)
+    with open(tplpath, 'r') as f:
+        tpl = f.read()
+
+    if isinstance(tpl, bytes):
+        tpl = tpl.decode()
+    for k in data:
+        data[k] = data[k].decode()
+
+    return tpl % data
+
+
 def gettopicstable():
     extrahelptable = [
         ([b"common"], b'', loaddoc(b'common'), help.TOPIC_CATEGORY_MISC),
@@ -268,6 +464,41 @@
         ui.write(b"\n")
 
 
+def showextension(ui, extensionname, debugcmds=False):
+    """Render the help text for an extension
+
+    Args:
+        ui: the UI object to output to
+        extensionname: the name of the extension to output
+        debugcmds: whether to include the extension's debug commands, if any
+    """
+    mod = extensions.load(ui, extensionname, None)
+
+    header = _rendertpl(
+        'extheader.txt',
+        {'extname': extensionname, 'exttitle': minirst.section(extensionname)},
+    )
+    ui.write(header.encode())
+
+    shortdoc, longdoc = _splitdoc(mod)
+    if shortdoc:
+        ui.write(b"%s\n\n" % gettext(shortdoc))
+    if longdoc:
+        ui.write(minirst.subsection(_(b"Description")))
+        ui.write(b"%s\n\n" % gettext(longdoc))
+
+    cmdtable = getattr(mod, 'cmdtable', None)
+    if cmdtable:
+        ui.write(minirst.subsection(_(b'Commands')))
+        commandprinter(
+            ui,
+            cmdtable,
+            minirst.subsubsection,
+            minirst.subsubsubsection,
+            debugcmds=debugcmds,
+        )
+
+
 def commandprinter(ui, cmdtable, sectionfunc, subsectionfunc, debugcmds=False):
     """Render restructuredtext describing a list of commands and their
     documentations, grouped by command category.
@@ -427,7 +658,27 @@
     # ui.debugflag determines if the help module returns debug commands to us.
     ui.debugflag = debugcmds
 
+    # Render the 'all-in-one' giant documentation file
     if doc == b'hg.1.gendoc':
         showdoc(ui)
+    # Render a command/help-topic/extension name list (for internal use)
+    elif doc == b'commandlist':
+        showcommandlist(ui, debugcmds=debugcmds)
+    elif doc == b'topiclist':
+        showtopiclist(ui)
+    elif doc == b'extensionlist':
+        showextensionlist(ui)
+    # Render the help index/main page
+    elif doc == b'index':
+        showhelpindex(ui, debugcmds=debugcmds)
+    # Render an individual command/help-topic/extension page
+    elif doc.startswith(b'cmd-'):
+        showcommand(ui, doc[4:])
+    elif doc.startswith(b'topic-'):
+        showtopic(ui, doc[6:], wraptpl=True)
+    elif doc.startswith(b'ext-'):
+        showextension(ui, doc[4:], debugcmds=debugcmds)
+    # Render a help-topic page without any title/footer, for later inclusion
+    # into a hand-written help text file
     else:
         showtopic(ui, doc)