From 5ce58234aafc1a68861f168b06d65bad22f48f82 Mon Sep 17 00:00:00 2001
From: Michael Hanselmann <hansmi@google.com>
Date: Tue, 8 Jan 2013 16:21:29 +0100
Subject: [PATCH] Link man pages in documentation
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This patch depends on β€œOption to include man pages in documentation”. In
the documentation build including man pages, all β€œ:manpage:`…`”
references are converted to links. For man pages not provided by Ganeti,
Sphinx' standard formatting is used.

A small dance is necessary to hook into Sphinx' processing of man page
roles and to generate automatically resolved links. The code converts
β€œ:manpage:`…`” for known man pages to the data structure equivalent of
β€œ:doc:`$name($section) <man-$name>`”. Additionally it checks the section
numbers and formatting of references (in all builds).

Signed-off-by: Michael Hanselmann <hansmi@google.com>
Reviewed-by: Guido Trotter <ultrotter@google.com>
---
 Makefile.am             |  12 ++++-
 lib/build/sphinx_ext.py | 117 +++++++++++++++++++++++++++++++++++++++-
 2 files changed, 127 insertions(+), 2 deletions(-)

diff --git a/Makefile.am b/Makefile.am
index ca4da8250..2d130ef69 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -929,7 +929,10 @@ man_MANS = \
 	man/htools.1 \
 	man/mon-collector.7
 
-manrst = $(patsubst %.1,%.rst,$(patsubst %.7,%.rst,$(patsubst %.8,%.rst,$(man_MANS))))
+# Remove extensions from all filenames in man_MANS
+mannoext = $(patsubst %.1,%,$(patsubst %.7,%,$(patsubst %.8,%,$(man_MANS))))
+
+manrst = $(patsubst %,%.rst,$(mannoext))
 manhtml = $(patsubst %.rst,%.html,$(manrst))
 mangen = $(patsubst %.rst,%.gen,$(manrst))
 maninput = \
@@ -1407,6 +1410,13 @@ lib/_autoconf.py: Makefile | stamp-directories
 	  echo "ENABLE_SPLIT_QUERY = $(ENABLE_SPLIT_QUERY)"; \
 	  echo "ENABLE_RESTRICTED_COMMANDS = $(ENABLE_RESTRICTED_COMMANDS)"; \
 	  echo "ENABLE_MONITORING = $(ENABLE_MONITORING)"; \
+## Write dictionary with man page name as the key and the section number as the
+## value
+	  echo "MAN_PAGES = {"; \
+	  for i in $(notdir $(man_MANS)); do \
+	    echo "$$i" | sed -re 's/^(.*)\.([0-9]+)$$/  "\1": \2,/g'; \
+	  done; \
+	  echo "}"; \
 	} > $@
 
 lib/_vcsversion.py: Makefile vcs-version | stamp-directories
diff --git a/lib/build/sphinx_ext.py b/lib/build/sphinx_ext.py
index 3e6f3ee0f..d72bc9777 100644
--- a/lib/build/sphinx_ext.py
+++ b/lib/build/sphinx_ext.py
@@ -23,17 +23,29 @@
 
 """
 
+import re
 from cStringIO import StringIO
 
 import docutils.statemachine
 import docutils.nodes
 import docutils.utils
+import docutils.parsers.rst
 
 import sphinx.errors
 import sphinx.util.compat
+import sphinx.roles
+import sphinx.addnodes
 
 s_compat = sphinx.util.compat
 
+try:
+  # Access to a protected member of a client class
+  # pylint: disable=W0212
+  orig_manpage_role = docutils.parsers.rst.roles._roles["manpage"]
+except (AttributeError, ValueError, KeyError), err:
+  # Normally the "manpage" role is registered by sphinx/roles.py
+  raise Exception("Can't find reST role named 'manpage': %s" % err)
+
 from ganeti import constants
 from ganeti import compat
 from ganeti import errors
@@ -42,10 +54,21 @@ from ganeti import opcodes
 from ganeti import ht
 from ganeti import rapi
 from ganeti import luxi
+from ganeti import _autoconf
 
 import ganeti.rapi.rlib2 # pylint: disable=W0611
 
 
+#: Regular expression for man page names
+_MAN_RE = re.compile(r"^(?P<name>[-\w_]+)\((?P<section>\d+)\)$")
+
+
+class ReSTError(Exception):
+  """Custom class for generating errors in Sphinx.
+
+  """
+
+
 def _GetCommonParamNames():
   """Builds a list of parameters common to all opcodes.
 
@@ -310,14 +333,106 @@ def BuildValuesDoc(values):
     yield "  %s" % doc
 
 
-# TODO: Implement Sphinx directive for query fields
+def _ManPageNodeClass(*args, **kwargs):
+  """Generates a pending XRef like a ":doc:`...`" reference.
+
+  """
+  # Type for sphinx/environment.py:BuildEnvironment.resolve_references
+  kwargs["reftype"] = "doc"
+
+  # Force custom title
+  kwargs["refexplicit"] = True
+
+  return sphinx.addnodes.pending_xref(*args, **kwargs)
+
+
+class _ManPageXRefRole(sphinx.roles.XRefRole):
+  def __init__(self):
+    """Initializes this class.
+
+    """
+    sphinx.roles.XRefRole.__init__(self, nodeclass=_ManPageNodeClass,
+                                   warn_dangling=True)
+
+    assert not hasattr(self, "converted"), \
+      "Sphinx base class gained an attribute named 'converted'"
+
+    self.converted = None
+
+  def process_link(self, env, refnode, has_explicit_title, title, target):
+    """Specialization for man page links.
+
+    """
+    if has_explicit_title:
+      raise ReSTError("Setting explicit title is not allowed for man pages")
+
+    # Check format and extract name and section
+    m = _MAN_RE.match(title)
+    if not m:
+      raise ReSTError("Man page reference '%s' does not match regular"
+                      " expression '%s'" % (title, _MAN_RE.pattern))
+
+    name = m.group("name")
+    section = int(m.group("section"))
+
+    wanted_section = _autoconf.MAN_PAGES.get(name, None)
+
+    if not (wanted_section is None or wanted_section == section):
+      raise ReSTError("Referenced man page '%s' has section number %s, but the"
+                      " reference uses section %s" %
+                      (name, wanted_section, section))
+
+    self.converted = bool(wanted_section is not None and
+                          env.app.config.enable_manpages)
+
+    if self.converted:
+      # Create link to known man page
+      return (title, "man-%s" % name)
+    else:
+      # No changes
+      return (title, target)
+
+
+def _ManPageRole(typ, rawtext, text, lineno, inliner, # pylint: disable=W0102
+                 options={}, content=[]):
+  """Custom role for man page references.
+
+  Converts man pages to links if enabled during the build.
+
+  """
+  xref = _ManPageXRefRole()
+
+  assert ht.TNone(xref.converted)
+
+  # Check if it's a known man page
+  try:
+    result = xref(typ, rawtext, text, lineno, inliner,
+                  options=options, content=content)
+  except ReSTError, err:
+    msg = inliner.reporter.error(str(err), line=lineno)
+    return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
+
+  assert ht.TBool(xref.converted)
+
+  # Return if the conversion was successful (i.e. the man page was known and
+  # conversion was enabled)
+  if xref.converted:
+    return result
+
+  # Fallback if man page links are disabled or an unknown page is referenced
+  return orig_manpage_role(typ, rawtext, text, lineno, inliner,
+                           options=options, content=content)
 
 
 def setup(app):
   """Sphinx extension callback.
 
   """
+  # TODO: Implement Sphinx directive for query fields
   app.add_directive("opcode_params", OpcodeParams)
   app.add_directive("opcode_result", OpcodeResult)
   app.add_directive("pyassert", PythonAssert)
   app.add_role("pyeval", PythonEvalRole)
+
+  app.add_config_value("enable_manpages", False, True)
+  app.add_role("manpage", _ManPageRole)
-- 
GitLab