Mercurial > hg
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 +