changeset 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 745409f94f0c
files .hgignore doc/Makefile doc/gendoc.py doc/runrst doc/templates/cmdheader.txt doc/templates/extheader.txt doc/templates/topicheader.txt
diffstat 7 files changed, 533 insertions(+), 7 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Mon Oct 09 22:11:21 2023 -0700
+++ b/.hgignore	Mon Oct 09 22:14:24 2023 -0700
@@ -40,10 +40,17 @@
 dist
 packages
 doc/common.txt
+doc/commandlist.txt
+doc/extensionlist.txt
+doc/topiclist.txt
+doc/*.mk
 doc/*.[0-9]
 doc/*.[0-9].txt
 doc/*.[0-9].gendoc.txt
 doc/*.[0-9].{x,ht}ml
+doc/build
+doc/html
+doc/man
 MANIFEST
 MANIFEST.in
 patches
--- a/doc/Makefile	Mon Oct 09 22:11:21 2023 -0700
+++ b/doc/Makefile	Mon Oct 09 22:14:24 2023 -0700
@@ -3,6 +3,7 @@
 HTML=$(SOURCES:%.txt=%.html)
 GENDOC=gendoc.py ../mercurial/commands.py ../mercurial/help.py \
 	../mercurial/helptext/*.txt ../hgext/*.py ../hgext/*/__init__.py
+RUNRST=runrst
 PREFIX=/usr/local
 MANDIR=$(PREFIX)/share/man
 INSTALL=install -m 644
@@ -14,10 +15,150 @@
 else
 PYTHON?=python3
 endif
+
 RSTARGS=
+GENDOCARGS=
+GENDOCCMD=$(PYTHON) gendoc.py $(GENDOCARGS)
+
+# Output directories for individual help pages.
+MANOUT=man
+HTMLOUT=html
+BUILDDIR=build
 
 export HGENCODING=UTF-8
 
+.PHONY: all man html install clean knownrefs
+
+# Generate a list of hg commands and extensions.
+commandlist.txt: $(GENDOC)
+	${GENDOCCMD} commandlist > $@.tmp
+	mv $@.tmp $@
+
+topiclist.txt: $(GENDOC)
+	${GENDOCCMD} topiclist > $@.tmp
+	mv $@.tmp $@
+
+extensionlist.txt: $(GENDOC)
+	${GENDOCCMD} extensionlist > $@.tmp
+	mv $@.tmp $@
+
+# Build target for running runrst more easily by hand
+knownrefs: commandlist.txt topiclist.txt extensionlist.txt
+
+BUILDFILES=commandlist.txt topiclist.txt extensionlist.txt
+
+# We want to generate a sub-Makefile that can build the RST/man/html doc for
+# each hg command. Here are templates that we'll use to generate this
+# sub-Makefile.
+HGCMDTPL=templates/cmdheader.txt
+TOPICTPL=templates/topicheader.txt
+EXTTPL=templates/extheader.txt
+
+define RuleAllCommandsTemplate
+HG_COMMANDS=$(1)
+all-commands: $$(HG_COMMANDS:%=$$(BUILDDIR)/hg-%.gendoc.txt)
+endef
+
+define RuleAllTopicsTemplate
+HG_TOPICS=$(1)
+all-topics: $$(HG_TOPICS:%=$$(BUILDDIR)/%.gendoc.txt)
+endef
+
+define RuleAllExtensionsTemplate
+HG_EXTENSIONS=$(1)
+all-extensions: $$(HG_EXTENSIONS:%=$$(BUILDDIR)/%.gendoc.txt)
+endef
+
+define RuleCommandTemplate
+$$(BUILDDIR)/hg-$C.gendoc.txt: $$(GENDOC) $$(HGCMDTPL)
+	mkdir -p $$(@D)
+	$${GENDOCCMD} cmd-$C > $$@.tmp
+	mv $$@.tmp $$@
+endef
+
+define RuleTopicTemplate
+$$(BUILDDIR)/topic-$T.gendoc.txt: $$(GENDOC) $$(TOPICTPL)
+	mkdir -p $$(@D)
+	$${GENDOCCMD} topic-$T > $$@.tmp
+	mv $$@.tmp $$@
+endef
+
+define RuleExtensionTemplate
+$$(BUILDDIR)/ext-$E.gendoc.txt: $$(GENDOC) $$(EXTTPL)
+	mkdir -p $$(@D)
+	$${GENDOCCMD} ext-$E > $$@.tmp
+	mv $$@.tmp $$@
+endef
+
+# Actually generate the sub-Makefile.
+# The $file function is only supported by GNU Make 4 and above.
+CommandsTopicsExtensions.mk: commandlist.txt topiclist.txt extensionlist.txt Makefile
+ifeq (4.0,$(firstword $(sort $(MAKE_VERSION) 4.0)))
+	$(file > $@.tmp,# Generated by Makefile)
+	$(file >> $@.tmp,$(call RuleAllCommandsTemplate,$(file < commandlist.txt)))
+	$(file >> $@.tmp,$(call RuleAllTopicsTemplate,$(file < topiclist.txt)))
+	$(file >> $@.tmp,$(call RuleAllExtensionsTemplate,$(file < extensionlist.txt)))
+	$(foreach C,$(file < commandlist.txt),$(file >> $@.tmp,$(RuleCommandTemplate)))
+	$(foreach T,$(file < topiclist.txt),$(file >> $@.tmp,$(RuleTopicTemplate)))
+	$(foreach E,$(file < extensionlist.txt),$(file >> $@.tmp,$(RuleExtensionTemplate)))
+	mv $@.tmp $@
+else
+	@echo "You are running make ${MAKE_VERSION} but you need make 4.0 or above"
+endif
+
+BUILDFILES+=CommandsTopicsExtensions.mk
+
+# Include the sub-Makefile that contains rules for generating each individual
+# command/help-topic/extension help page. This sub-Makefile is created by the
+# rule above (CommandsTopicsExtensions.mk) which in turn is created from the
+# plain-text lists of commands/help-topics/extensions.
+#
+# Any time the source code changes, these plain-text lists and this
+# sub-Makefile will get regenerated. Make will then restart itself to take
+# into account the rules inside the sub-Makefile.
+#
+# We want to avoid doing all this work for targets that we know don't need it
+# however. For example, running `make clean` would only generate these files
+# in order to delete them immediately. As a result, we don't include the
+# sub-Makefile (and therefore don't require generating it) if clean is one of
+# the targets. This might not do what we want when other targets are specified
+# but it's most likely what we want.
+ifeq (,$(filter clean,$(MAKECMDGOALS)))
+-include CommandsTopicsExtensions.mk
+endif
+
+# If the sub-Makefile is available, add all the hg commands, help-topics, and
+# extensions to the list of things to generate html and man pages for.
+#
+# Naming convention:
+# - commands: hg-foo (html and man)
+# - help topics: topic-foo (html), hgfoo (man)
+# - extensions: ext-foo (html), hgext-foo (man)
+#
+# Man pages for commands are in section 1 (user commands), topics and 
+# extensions are in section 7 (miscellanea)
+#
+# NOTE: topics and extension are temporarily disabled for man pages because
+# they make docutils' RST converter crash.
+ifdef HG_COMMANDS
+HTML+=$(HG_COMMANDS:%=$(HTMLOUT)/hg-%.html)
+MAN+=$(HG_COMMANDS:%=$(MANOUT)/hg-%.1)
+endif
+
+ifdef HG_TOPICS
+HTML+=$(HG_TOPICS:%=$(HTMLOUT)/topic-%.html)
+#MAN+=$(HG_TOPICS:%=$(MANOUT)/hg%.7)
+endif
+
+ifdef HG_EXTENSIONS
+HTML+=$(HG_EXTENSIONS:%=$(HTMLOUT)/ext-%.html)
+#MAN+=$(HG_EXTENSIONS:%=$(MANOUT)/hgext-%.7)
+endif
+
+# Also add the HTML index page
+HTML+=$(HTMLOUT)/index.html
+
+
 all: man html
 
 man: $(MAN)
@@ -26,17 +167,45 @@
 
 # This logic is duplicated in setup.py:hgbuilddoc()
 common.txt $(SOURCES) $(SOURCES:%.txt=%.gendoc.txt): $(GENDOC)
-	${PYTHON} gendoc.py "$(basename $@)" > $@.tmp
+	${GENDOCCMD} "$(basename $@)" > $@.tmp
 	mv $@.tmp $@
 
-%: %.txt %.gendoc.txt common.txt
+%: %.txt %.gendoc.txt common.txt $(RUNRST)
 	$(PYTHON) runrst hgmanpage $(RSTARGS) --halt warning \
 	  --strip-elements-with-class htmlonly $*.txt $*
 
-%.html: %.txt %.gendoc.txt common.txt
+%.html: %.txt %.gendoc.txt common.txt $(RUNRST)
 	$(PYTHON) runrst html $(RSTARGS) --halt warning \
 	  --link-stylesheet --stylesheet-path style.css $*.txt $*.html
 
+# Rules for index page and individual command/help-topic/extension pages
+# Because the naming isn't the same between html and man pages, we need to
+# break down man pages rules a bit more.
+$(BUILDDIR)/index.gendoc.txt: $(GENDOC)
+	mkdir -p $(@D)
+	${GENDOCCMD} index > $@.tmp
+	mv $@.tmp $@
+
+$(MANOUT)/hg-%.1: $(BUILDDIR)/hg-%.gendoc.txt common.txt $(RUNRST)
+	mkdir -p $(@D)
+	$(PYTHON) runrst hgmanpage --hg-individual-pages $(RSTARGS) --halt warning \
+	  --strip-elements-with-class htmlonly $(BUILDDIR)/hg-$*.gendoc.txt $@
+
+$(MANOUT)/hg%.7: $(BUILDDIR)/topic-%.gendoc.txt common.txt $(RUNRST)
+	mkdir -p $(@D)
+	$(PYTHON) runrst hgmanpage --hg-individual-pages $(RSTARGS) --halt warning \
+	  --strip-elements-with-class htmlonly $(BUILDDIR)/topic-$*.gendoc.txt $@
+
+$(MANOUT)/hgext-%.7: $(BUILDDIR)/ext-%.gendoc.txt common.txt $(RUNRST)
+	mkdir -p $(@D)
+	$(PYTHON) runrst hgmanpage --hg-individual-pages $(RSTARGS) --halt warning \
+	  --strip-elements-with-class htmlonly $(BUILDDIR)/ext-$*.gendoc.txt $@
+
+$(HTMLOUT)/%.html: $(BUILDDIR)/%.gendoc.txt common.txt $(RUNRST)
+	mkdir -p $(@D)
+	$(PYTHON) runrst html --hg-individual-pages $(RSTARGS) --halt warning \
+	  --link-stylesheet --stylesheet-path style.css $(BUILDDIR)/$*.gendoc.txt $@
+
 MANIFEST: man html
 # tracked files are already in the main MANIFEST
 	$(RM) $@
@@ -51,5 +220,9 @@
 	  $(INSTALL) $$i "$(DESTDIR)$(MANDIR)"/$$subdir ; \
 	done
 
+# The clean target explicitly doesn't bother with the sub-Makefile, so we don't
+# know anything about all the command/topic/extension targets and files.
+# $(HTML) only has the basic topics, so we need to delete $(HTMLOUT)/*.html and
+# other similar files "by hand" here.
 clean:
-	$(RM) $(MAN) $(HTML) common.txt $(SOURCES) $(SOURCES:%.txt=%.gendoc.txt) MANIFEST
+	$(RM) $(MAN) $(HTML) common.txt $(SOURCES) MANIFEST *.gendoc.txt $(BUILDFILES) $(BUILDDIR)/*.gendoc.* $(HTMLOUT)/*.html
--- 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)
--- a/doc/runrst	Mon Oct 09 22:11:21 2023 -0700
+++ b/doc/runrst	Mon Oct 09 22:14:24 2023 -0700
@@ -13,6 +13,7 @@
 """
 
 
+import re
 import sys
 
 try:
@@ -31,13 +32,63 @@
     )
     sys.exit(-1)
 
+# Whether we are rendering a help page for a single topic.
+# If false, we are rendering a monolithic page with all topics together.
+is_individual_pages_mode = False
+
+
+def make_cmd_ref_uri(cmd):
+    if is_individual_pages_mode:
+        return "hg-%s.html" % cmd
+    else:
+        return "hg.1.html#%s" % cmd
+
+
+known_refs = None
+
+
+def load_known_refs(fname):
+    try:
+        with open(fname, 'r') as fp:
+            text = fp.read()
+            return re.split(r'[ \n]+', text)
+    except OSError:
+        sys.stderr.write(
+            "abort: couldn't find '%', please run documentation generation "
+            "through the Makefile, or run 'make knownrefs'\n"
+        )
+        sys.exit(-1)
+
+
+def find_known_ref(ref):
+    global known_refs
+    if known_refs is None:
+        cmds = load_known_refs('commandlist.txt')
+        topics = load_known_refs('topiclist.txt')
+        exts = load_known_refs('extensionlist.txt')
+        known_refs = {'hg': cmds, 'topic': topics, 'ext': exts}
+    for reftype, refnames in known_refs.items():
+        if ref in refnames:
+            return reftype
+    return None
+
+
+def make_any_ref_uri(ref):
+    if is_individual_pages_mode:
+        # Try to find if ref is a command, topic, or extension. If not,
+        # reference the anchor in the main hg.1 help page.
+        reftype = find_known_ref(ref)
+        if reftype:
+            return '%s-%s.html' % (reftype, ref)
+    return "hg.1.html#%s" % ref
+
 
 def role_hg(name, rawtext, text, lineno, inliner, options=None, content=None):
     text = "hg " + utils.unescape(text)
     linktext = nodes.literal(rawtext, text)
     parts = text.split()
     cmd, args = parts[1], parts[2:]
-    refuri = "hg.1.html#%s" % cmd
+    refuri = make_cmd_ref_uri(cmd)
     if cmd == 'help' and args:
         if args[0] == 'config':
             # :hg:`help config`
@@ -48,9 +99,9 @@
         elif len(args) >= 2 and args[0] == '-c':
             # :hg:`help -c COMMAND ...` is equivalent to :hg:`COMMAND`
             # (mainly for :hg:`help -c config`)
-            refuri = "hg.1.html#%s" % args[1]
+            refuri = make_cmd_ref_uri(args[1])
         else:
-            refuri = "hg.1.html#%s" % args[0]
+            refuri = make_any_ref_uri(args[0])
     node = nodes.reference(rawtext, '', linktext, refuri=refuri)
     return [node], []
 
@@ -65,4 +116,8 @@
     writer = sys.argv[1]
     del sys.argv[1]
 
+    if sys.argv[1] == '--hg-individual-pages':
+        is_individual_pages_mode = True
+        del sys.argv[1]
+
     core.publish_cmdline(writer_name=writer)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/templates/cmdheader.txt	Mon Oct 09 22:14:24 2023 -0700
@@ -0,0 +1,22 @@
+.. _hg-%(cmdname)s.1:
+
+%(cmdtitle)s
+
+%(cmdshortdesc)s
+
+.. contents::
+   :backlinks: top
+   :class: htmlonly
+   :depth: 1
+
+Synopsis
+--------
+
+::
+
+   %(cmdsynopsis)s
+
+Description
+-----------
+%(cmdlongdesc)s
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/templates/extheader.txt	Mon Oct 09 22:14:24 2023 -0700
@@ -0,0 +1,9 @@
+.. _ext-%(extname)s:
+
+%(exttitle)s
+
+.. contents::
+   :backlinks: top
+   :class: htmlonly
+   :depth: 2
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/templates/topicheader.txt	Mon Oct 09 22:14:24 2023 -0700
@@ -0,0 +1,9 @@
+.. _topic-%(topicname)s:
+
+%(topictitle)s
+
+.. contents::
+   :backlinks: top
+   :class: htmlonly
+   :depth: 2
+