D9135: doc: Generate separate commands/topics/extension web pages.

ludovicchabant (Ludovic Chabant) phabricator at mercurial-scm.org
Wed Sep 30 21:35:32 UTC 2020


ludovicchabant created this revision.
Herald added a reviewer: hg-reviewers.
Herald added a subscriber: mercurial-patches.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D9135

AFFECTED FILES
  .hgignore
  doc/Makefile
  doc/gendoc.py
  doc/templates/cmdheader.txt
  doc/templates/extheader.txt
  doc/templates/topicheader.txt

CHANGE DETAILS

diff --git a/doc/templates/topicheader.txt b/doc/templates/topicheader.txt
new file mode 100644
--- /dev/null
+++ b/doc/templates/topicheader.txt
@@ -0,0 +1,4 @@
+.. _topic-%(topicname)s:
+
+%(topictitle)s
+
diff --git a/doc/templates/extheader.txt b/doc/templates/extheader.txt
new file mode 100644
--- /dev/null
+++ b/doc/templates/extheader.txt
@@ -0,0 +1,4 @@
+.. _ext-%(extname)s:
+
+%(exttitle)s
+
diff --git a/doc/templates/cmdheader.txt b/doc/templates/cmdheader.txt
new file mode 100644
--- /dev/null
+++ b/doc/templates/cmdheader.txt
@@ -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
+
diff --git a/doc/gendoc.py b/doc/gendoc.py
--- a/doc/gendoc.py
+++ b/doc/gendoc.py
@@ -131,7 +131,8 @@
 
     # print help topics
     # The config help topic is included in the hgrc.5 man page.
-    helpprinter(ui, helptable, minirst.section, exclude=[b'config'])
+    topics = findtopics(helptable, exclude=[b'config'])
+    helpprinter(ui, topics, minirst.section)
 
     ui.write(minirst.section(_(b"Extensions")))
     ui.write(
@@ -166,7 +167,162 @@
             )
 
 
-def showtopic(ui, topic):
+def showcommandlist(ui):
+    # ui.verbose = True
+    cmdnames = allcommandnames(table, debugcmds=False)
+    for mainname, allnames in cmdnames.items():
+        ui.write(mainname)
+        ui.write(b" ")
+
+
+def showtopiclist(ui):
+    for topic in helptable:
+        ui.write(topic[0][0])
+        ui.write(b" ")
+
+
+def showextensionlist(ui):
+    for extensionname in allextensionnames():
+        ui.write(extensionname)
+        ui.write(b" ")
+
+
+def showhelpindex(ui):
+    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)
+                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):
+    ui.verbose = True
+    cmdnames = allcommandnames(table, debugcmds=False)
+    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, longdoc
+
+
+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),
         ([b"hg.1"], b'', loaddoc(b'hg.1'), help.TOPIC_CATEGORY_CONFIG),
@@ -179,6 +335,12 @@
         ),
         ([b"hgrc.5"], b'', loaddoc(b'hgrc.5'), help.TOPIC_CATEGORY_CONFIG),
         (
+            [b"hg-ssh.8.gendoc"],
+            b'',
+            b'',
+            help.TOPIC_CATEGORY_CONFIG
+        ),
+        (
             [b"hgignore.5.gendoc"],
             b'',
             loaddoc(b'hgignore'),
@@ -191,16 +353,40 @@
             help.TOPIC_CATEGORY_CONFIG,
         ),
     ]
-    helpprinter(ui, helptable + extrahelptable, None, include=[topic])
+    return helptable + extrahelptable
 
 
-def helpprinter(ui, helptable, sectionfunc, include=[], exclude=[]):
+def findtopics(helptable, include=[], exclude=[]):
+    found = []
     for h in helptable:
         names, sec, doc = h[0:3]
         if exclude and names[0] in exclude:
             continue
         if include and names[0] not in include:
             continue
+        found.append((names, sec, doc))
+    return found
+
+
+def showtopic(ui, topic, wraptpl=False):
+    found = findtopics(gettopicstable(), include=[topic])
+    if not found:
+        raise Exception("ERROR: no such topic: %s\n" % topic)
+
+    ui.verbose = True
+    if wraptpl:
+        header = _rendertpl('topicheader.txt', {
+            'topicname': topic,
+            'topictitle': minirst.section(found[0][1])
+        })
+        ui.write(header.encode())
+    helpprinter(ui, found, None)
+    return True
+
+
+def helpprinter(ui, topics, sectionfunc):
+    for h in topics:
+        names, sec, doc = h[0:3]
         for name in names:
             ui.write(b".. _%s:\n" % name)
         ui.write(b"\n")
@@ -212,6 +398,25 @@
         ui.write(b"\n")
 
 
+def showextension(ui, extensionname):
+    mod = extensions.load(ui, extensionname, None)
+
+    ui.verbose = True
+    header = _rendertpl('extheader.txt', {
+        'extname': extensionname,
+        'exttitle': minirst.section(extensionname)
+    })
+    ui.write(header.encode())
+
+    shortdoc, longdoc = _splitdoc(mod)
+    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)
+
+
 def commandprinter(ui, cmdtable, sectionfunc, subsectionfunc):
     """Render restructuredtext describing a list of commands and their
     documentations, grouped by command category.
@@ -233,88 +438,63 @@
       sectionfunc: minirst function to format command category headers
       subsectionfunc: minirst function to format command headers
     """
-    h = {}
-    for c, attr in cmdtable.items():
-        f = c.split(b"|")[0]
-        f = f.lstrip(b"^")
-        h[f] = c
-    cmds = h.keys()
+    cmdnames = allcommandnames(cmdtable)
+    for f in sorted(cmdnames.keys()):
+        d = get_cmd(cmdnames[f], cmdtable)
+        _onecommandprinter(ui, d, sectionfunc)
+
 
-    def helpcategory(cmd):
-        """Given a canonical command name from `cmds` (above), retrieve its
-        help category. If helpcategory is None, default to CATEGORY_NONE.
-        """
-        fullname = h[cmd]
-        details = cmdtable[fullname]
-        helpcategory = details[0].helpcategory
-        return helpcategory or help.registrar.command.CATEGORY_NONE
+def _optionsprinter(ui, cmd, sectionfunc):
+    opt_output = list(cmd[b'opts'])
+    if opt_output:
+        opts_len = max([len(line[0]) for line in opt_output])
+        ui.write(sectionfunc(_(b"Options")))
+        multioccur = False
+        for optstr, desc in opt_output:
+            if desc:
+                s = b"%-*s  %s" % (opts_len, optstr, desc)
+            else:
+                s = optstr
+            ui.write(b"%s\n" % s)
+            if optstr.endswith(b"[+]>"):
+                multioccur = True
+        if multioccur:
+            ui.write(_(b"\n[+] marked option can be specified"
+                       b" multiple times\n"))
+        ui.write(b"\n")
+
 
-    cmdsbycategory = {category: [] for category in help.CATEGORY_ORDER}
-    for cmd in cmds:
-        # If a command category wasn't registered, the command won't get
-        # rendered below, so we raise an AssertionError.
-        if helpcategory(cmd) not in cmdsbycategory:
-            raise AssertionError(
-                "The following command did not register its (category) in "
-                "help.CATEGORY_ORDER: %s (%s)" % (cmd, helpcategory(cmd))
-            )
-        cmdsbycategory[helpcategory(cmd)].append(cmd)
+def _onecommandprinter(ui, cmd, sectionfunc):
+    ui.write(sectionfunc(cmd[b'cmd']))
+    # short description
+    ui.write(cmd[b'desc'][0])
+    # synopsis
+    ui.write(b"::\n\n")
+    synopsislines = cmd[b'synopsis'].splitlines()
+    for line in synopsislines:
+        # some commands (such as rebase) have a multi-line
+        # synopsis
+        ui.write(b"   %s\n" % line)
+    ui.write(b'\n')
+    # description
+    ui.write(b"%s\n\n" % cmd[b'desc'][1])
+    # options
+    def _optsection(s):
+        return b"%s:\n\n" % s
+    _optionsprinter(ui, cmd, _optsection)
+    # aliases
+    if cmd[b'aliases']:
+        ui.write(_(b"    aliases: %s\n\n") % b" ".join(cmd[b'aliases']))
 
-    # Print the help for each command. We present the commands grouped by
-    # category, and we use help.CATEGORY_ORDER as a guide for a helpful order
-    # in which to present the categories.
-    for category in help.CATEGORY_ORDER:
-        categorycmds = cmdsbycategory[category]
-        if not categorycmds:
-            # Skip empty categories
+
+def allcommandnames(cmdtable, debugcmds=False):
+    allcmdnames = {}
+    for cmdnames, attr in cmdtable.items():
+        mainname = cmdnames.split(b"|")[0].lstrip(b"^")
+        if not debugcmds and mainname.startswith(b"debug"):
             continue
-        # Print a section header for the category.
-        # For now, the category header is at the same level as the headers for
-        # the commands in the category; this is fixed in the next commit.
-        ui.write(sectionfunc(help.CATEGORY_NAMES[category]))
-        # Print each command in the category
-        for f in sorted(categorycmds):
-            if f.startswith(b"debug"):
-                continue
-            d = get_cmd(h[f], cmdtable)
-            ui.write(subsectionfunc(d[b'cmd']))
-            # short description
-            ui.write(d[b'desc'][0])
-            # synopsis
-            ui.write(b"::\n\n")
-            synopsislines = d[b'synopsis'].splitlines()
-            for line in synopsislines:
-                # some commands (such as rebase) have a multi-line
-                # synopsis
-                ui.write(b"   %s\n" % line)
-            ui.write(b'\n')
-            # description
-            ui.write(b"%s\n\n" % d[b'desc'][1])
-            # options
-            opt_output = list(d[b'opts'])
-            if opt_output:
-                opts_len = max([len(line[0]) for line in opt_output])
-                ui.write(_(b"Options:\n\n"))
-                multioccur = False
-                for optstr, desc in opt_output:
-                    if desc:
-                        s = b"%-*s  %s" % (opts_len, optstr, desc)
-                    else:
-                        s = optstr
-                    ui.write(b"%s\n" % s)
-                    if optstr.endswith(b"[+]>"):
-                        multioccur = True
-                if multioccur:
-                    ui.write(
-                        _(
-                            b"\n[+] marked option can be specified"
-                            b" multiple times\n"
-                        )
-                    )
-                ui.write(b"\n")
-            # aliases
-            if d[b'aliases']:
-                ui.write(_(b"    aliases: %s\n\n") % b" ".join(d[b'aliases']))
+        allcmdnames[mainname] = cmdnames
+    return allcmdnames
 
 
 def allextensionnames():
@@ -327,7 +507,21 @@
         doc = encoding.strtolocal(sys.argv[1])
 
     ui = uimod.ui.load()
-    if doc == b'hg.1.gendoc':
+    if doc == b'commandlist':
+        showcommandlist(ui)
+    elif doc == b'topiclist':
+        showtopiclist(ui)
+    elif doc == b'extensionlist':
+        showextensionlist(ui)
+    elif doc == b'index':
+        showhelpindex(ui)
+    elif doc == b'hg.1.gendoc':
         showdoc(ui)
+    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:])
     else:
-        showtopic(ui, encoding.strtolocal(sys.argv[1]))
+        showtopic(ui, doc)
diff --git a/doc/Makefile b/doc/Makefile
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -1,8 +1,11 @@
 SOURCES=$(notdir $(wildcard ../mercurial/helptext/*.[0-9].txt))
 MAN=$(SOURCES:%.txt=%)
-HTML=$(SOURCES:%.txt=%.html)
+HTML=$(SOURCES:%.txt=%.html) index.html
 GENDOC=gendoc.py ../mercurial/commands.py ../mercurial/help.py \
 	../mercurial/helptext/*.txt ../hgext/*.py ../hgext/*/__init__.py
+HGCMDTPL=templates/cmdheader.txt
+TOPICTPL=templates/topicheader.txt
+EXTTPL=templates/extheader.txt
 PREFIX=/usr/local
 MANDIR=$(PREFIX)/share/man
 INSTALL=install -m 644
@@ -11,6 +14,91 @@
 
 export HGENCODING=UTF-8
 
+.PHONY: all man html install clean
+
+# Generate a list of hg commands and extensions.
+commandlist.txt: $(GENDOC)
+	${PYTHON} gendoc.py commandlist > $@.tmp
+	mv $@.tmp $@
+
+topiclist.txt: $(GENDOC)
+	${PYTHON} gendoc.py topiclist > $@.tmp
+	mv $@.tmp $@
+
+extensionlist.txt: $(GENDOC)
+	${PYTHON} gendoc.py extensionlist > $@.tmp
+	mv $@.tmp $@
+
+# Generate a sub-Makefile that can build the RST/man/html doc for
+# each hg command.
+define RuleAllCommandsTemplate
+HG_COMMANDS=$(1)
+all-commands: $$(HG_COMMANDS:%=hg-%.gendoc.txt)
+endef
+
+define RuleAllTopicsTemplate
+HG_TOPICS=$(1)
+all-topics: $$(HG_TOPICS:%=%.gendoc.txt)
+endef
+
+define RuleAllExtensionsTemplate
+HG_EXTENSIONS=$(1)
+all-extensions: $$(HG_EXTENSIONS:%=%.gendoc.txt)
+endef
+
+define RuleCommandTemplate
+hg-$C.gendoc.txt: $$(GENDOC) $$(HGCMDTPL)
+	$$(PYTHON) gendoc.py cmd-$C > $$@.tmp
+	mv $$@.tmp $$@
+endef
+
+define RuleTopicTemplate
+topic-$T.gendoc.txt: $$(GENDOC) $$(TOPICTPL)
+	$$(PYTHON) gendoc.py topic-$T > $$@.tmp
+	mv $$@.tmp $$@
+endef
+
+define RuleExtensionTemplate
+ext-$E.gendoc.txt: $$(GENDOC) $$(EXTTPL)
+	$$(PYTHON) gendoc.py ext-$E > $$@.tmp
+	mv $$@.tmp $$@
+endef
+
+# Note that this type of stuff is only supported by GNU Make 4 and above.
+# Check the version you have installed if this rule fails.
+CommandsTopicsExtensions.mk: commandlist.txt topiclist.txt extensionlist.txt Makefile
+	$(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 $@
+
+# Include the sub-Makefile.
+# NOTE: since we define (above) a rule for this sub-Makefile, make will
+# automatically try to generate it if it's not up to date, and then 
+# will restart itself using the newly generated file.
+ifeq (,$(filter clean,$(MAKECMDGOALS)))
+-include CommandsTopicsExtensions.mk
+endif
+
+# If the sub-Makefile is available, add all the hg commands/topics/extensions
+# to the list of things to generate html pages for.
+ifdef HG_COMMANDS
+HTML+=$(HG_COMMANDS:%=hg-%.html)
+endif
+
+ifdef HG_TOPICS
+HTML+=$(HG_TOPICS:%=topic-%.html)
+endif
+
+ifdef HG_EXTENSIONS
+HTML+=$(HG_EXTENSIONS:%=ext-%.html)
+endif
+
+
 all: man html
 
 man: $(MAN)
@@ -22,14 +110,26 @@
 	${PYTHON} gendoc.py "$(basename $@)" > $@.tmp
 	mv $@.tmp $@
 
+index.gendoc.txt: $(GENDOC)
+	$(PYTHON) gendoc.py index > $@.tmp
+	mv $@.tmp $@
+
 %: %.txt %.gendoc.txt common.txt
 	$(PYTHON) runrst hgmanpage $(RSTARGS) --halt warning \
 	  --strip-elements-with-class htmlonly $*.txt $*
 
+%: %.gendoc.txt common.txt
+	$(PYTHON) runrst hgmanpage $(RSTARGS) --halt warning \
+	  --strip-elements-with-class htmlonly $*.gendoc.txt $*
+
 %.html: %.txt %.gendoc.txt common.txt
 	$(PYTHON) runrst html $(RSTARGS) --halt warning \
 	  --link-stylesheet --stylesheet-path style.css $*.txt $*.html
 
+%.html: %.gendoc.txt common.txt
+	$(PYTHON) runrst html $(RSTARGS) --halt warning \
+	  --link-stylesheet --stylesheet-path style.css $*.gendoc.txt $*.html
+
 MANIFEST: man html
 # tracked files are already in the main MANIFEST
 	$(RM) $@
@@ -45,4 +145,4 @@
 	done
 
 clean:
-	$(RM) $(MAN) $(HTML) common.txt $(SOURCES) $(SOURCES:%.txt=%.gendoc.txt) MANIFEST
+	$(RM) *[0-9] *.html common.txt commandlist.txt topiclist.txt extensionlist.txt $(SOURCES) MANIFEST CommandsTopicsExtensions.mk *.gendoc.txt
diff --git a/.hgignore b/.hgignore
--- a/.hgignore
+++ b/.hgignore
@@ -35,10 +35,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/*.{x,ht}ml
+doc/*.gendoc.txt
+doc/hg-*.html
+doc/topic-*.html
+doc/ext-*.html
 MANIFEST
 MANIFEST.in
 patches



To: ludovicchabant, #hg-reviewers
Cc: mercurial-patches, mercurial-devel


More information about the Mercurial-devel mailing list