Base to get all 'gmx help' contents to user guide
authorTeemu Murtola <teemu.murtola@gmail.com>
Tue, 19 May 2015 09:20:40 +0000 (12:20 +0300)
committerGerrit Code Review <gerrit@gerrit.gromacs.org>
Fri, 22 May 2015 14:53:35 +0000 (16:53 +0200)
Implement initial functionality to get all 'gmx help' contents to the
reStructuredText user guide.  Make 'gmx help -export rst' export also
the help topics, and make minimal changes to the user guide and the
selection help to get it go through Sphinx cleanly.  Subsequent changes
will clean up the documentation further.

Part of #679.

Change-Id: I66219110fac4f4acd9ff9c36a19ca10f9561ca29

12 files changed:
docs/user-guide/tools.rst
src/gromacs/commandline/cmdlinehelpmodule.cpp
src/gromacs/commandline/cmdlinehelpmodule.h
src/gromacs/commandline/cmdlinemodulemanager.cpp
src/gromacs/commandline/tests/cmdlinehelpmodule.cpp
src/gromacs/commandline/tests/cmdlinemodulemanagertest.cpp
src/gromacs/commandline/tests/refdata/CommandLineHelpModuleTest_ExportsHelp.xml
src/gromacs/onlinehelp/helptopic.cpp
src/gromacs/onlinehelp/tests/helpmanager.cpp
src/gromacs/onlinehelp/tests/mock_helptopic.cpp
src/gromacs/onlinehelp/tests/mock_helptopic.h
src/gromacs/selection/selhelp.cpp

index b34b48e169ef491bdf59130c798bc2fe585482cf..ad3755ac6e5f588de01ed614a30af7c6ea7ace2a 100644 (file)
@@ -14,6 +14,8 @@ be found at the links below:
    /programs/byname
    /programs/bytopic
 
+   /programs/selections
+
 .. toctree::
    :hidden:
    :glob:
index cbd5115d7e3292a2dda84bf115b744d0f102ec7e..e32e337cb22df32327268b1c15ad7432364b1565 100644 (file)
@@ -108,6 +108,80 @@ class CommandLineHelpModuleImpl
 namespace
 {
 
+/********************************************************************
+ * HelpExportInterface
+ */
+
+/*! \brief
+ * Callbacks for exporting help information for command-line modules.
+ *
+ * \ingroup module_commandline
+ */
+class HelpExportInterface
+{
+    public:
+        //! Shorthand for a list of modules contained in a group.
+        typedef CommandLineModuleGroupData::ModuleList ModuleGroupContents;
+
+        virtual ~HelpExportInterface() {};
+
+        /*! \brief
+         * Called once before exporting individual modules.
+         *
+         * Can, e.g., open shared output files (e.g., if the output is written
+         * into a single file, or if a separate index is required) and write
+         * headers into them.
+         */
+        virtual void startModuleExport() = 0;
+        /*! \brief
+         * Called to export the help for each module.
+         *
+         * \param[in] module      Module for which the help should be exported.
+         * \param[in] tag         Unique tag for the module (gmx-something).
+         * \param[in] displayName Display name for the module (gmx something).
+         */
+        virtual void exportModuleHelp(
+            const CommandLineModuleInterface &module,
+            const std::string                &tag,
+            const std::string                &displayName) = 0;
+        /*! \brief
+         * Called after all modules have been exported.
+         *
+         * Can close files opened in startModuleExport(), write footers to them
+         * etc.
+         */
+        virtual void finishModuleExport() = 0;
+
+        /*! \brief
+         * Called once before exporting module groups.
+         *
+         * Can, e.g., open a single output file for listing all the groups.
+         */
+        virtual void startModuleGroupExport() = 0;
+        /*! \brief
+         * Called to export the help for each module group.
+         *
+         * \param[in] title    Title for the group.
+         * \param[in] modules  List of modules in the group.
+         */
+        virtual void exportModuleGroup(const char                *title,
+                                       const ModuleGroupContents &modules) = 0;
+        /*! \brief
+         * Called after all module groups have been exported.
+         *
+         * Can close files opened in startModuleGroupExport(), write footers to them
+         * etc.
+         */
+        virtual void finishModuleGroupExport() = 0;
+
+        /*! \brief
+         * Called to export the help for a top-level topic.
+         *
+         * \param[in] topic   Topic to export.
+         */
+        virtual void exportTopic(const HelpTopicInterface &topic) = 0;
+};
+
 /********************************************************************
  * RootHelpTopic
  */
@@ -142,14 +216,40 @@ class RootHelpTopic : public CompositeHelpTopic<RootHelpText>
         {
         }
 
+        //! Adds a top-level topic and optionally marks it as exported.
+        void addTopic(HelpTopicPointer topic, bool bExported)
+        {
+            if (bExported)
+            {
+                exportedTopics_.push_back(topic->name());
+            }
+            addSubTopic(move(topic));
+        }
+        //! Exports all the top-level topics with the given exporter.
+        void exportHelp(HelpExportInterface *exporter) const;
+
         virtual void writeHelp(const HelpWriterContext &context) const;
 
     private:
         const CommandLineHelpModuleImpl  &helpModule_;
+        std::vector<std::string>          exportedTopics_;
 
         GMX_DISALLOW_COPY_AND_ASSIGN(RootHelpTopic);
 };
 
+void RootHelpTopic::exportHelp(HelpExportInterface *exporter) const
+{
+    std::vector<std::string>::const_iterator topicName;
+    for (topicName = exportedTopics_.begin();
+         topicName != exportedTopics_.end();
+         ++topicName)
+    {
+        const HelpTopicInterface *topic = findSubTopic(topicName->c_str());
+        GMX_RELEASE_ASSERT(topic != NULL, "Exported help topic no longer found");
+        exporter->exportTopic(*topic);
+    }
+}
+
 void RootHelpTopic::writeHelp(const HelpWriterContext &context) const
 {
     if (context.outputFormat() != eHelpOutputFormat_Console)
@@ -308,71 +408,8 @@ void ModuleHelpTopic::writeHelp(const HelpWriterContext & /*context*/) const
 }
 
 /********************************************************************
- * HelpExportInterface
- */
-
-/*! \brief
- * Callbacks for exporting help information for command-line modules.
- *
- * \ingroup module_commandline
+ * HelpExportReStructuredText
  */
-class HelpExportInterface
-{
-    public:
-        //! Shorthand for a list of modules contained in a group.
-        typedef CommandLineModuleGroupData::ModuleList ModuleGroupContents;
-
-        virtual ~HelpExportInterface() {};
-
-        /*! \brief
-         * Called once before exporting individual modules.
-         *
-         * Can, e.g., open shared output files (e.g., if the output is written
-         * into a single file, or if a separate index is required) and write
-         * headers into them.
-         */
-        virtual void startModuleExport() = 0;
-        /*! \brief
-         * Called to export the help for each module.
-         *
-         * \param[in] module      Module for which the help should be exported.
-         * \param[in] tag         Unique tag for the module (gmx-something).
-         * \param[in] displayName Display name for the module (gmx something).
-         */
-        virtual void exportModuleHelp(
-            const CommandLineModuleInterface &module,
-            const std::string                &tag,
-            const std::string                &displayName) = 0;
-        /*! \brief
-         * Called after all modules have been exported.
-         *
-         * Can close files opened in startModuleExport(), write footers to them
-         * etc.
-         */
-        virtual void finishModuleExport() = 0;
-
-        /*! \brief
-         * Called once before exporting module groups.
-         *
-         * Can, e.g., open a single output file for listing all the groups.
-         */
-        virtual void startModuleGroupExport() = 0;
-        /*! \brief
-         * Called to export the help for each module group.
-         *
-         * \param[in] title    Title for the group.
-         * \param[in] modules  List of modules in the group.
-         */
-        virtual void exportModuleGroup(const char                *title,
-                                       const ModuleGroupContents &modules) = 0;
-        /*! \brief
-         * Called after all module groups have been exported.
-         *
-         * Can close files opened in startModuleGroupExport(), write footers to them
-         * etc.
-         */
-        virtual void finishModuleGroupExport() = 0;
-};
 
 /*! \internal \brief
  * Adds hyperlinks to modules within this binary.
@@ -407,10 +444,6 @@ void initProgramLinks(HelpLinks *links, const CommandLineHelpModuleImpl &helpMod
     }
 }
 
-/********************************************************************
- * HelpExportReStructuredText
- */
-
 /*! \internal \brief
  * Implements export for web pages as reStructuredText.
  *
@@ -435,6 +468,8 @@ class HelpExportReStructuredText : public HelpExportInterface
                                        const ModuleGroupContents &modules);
         virtual void finishModuleGroupExport();
 
+        virtual void exportTopic(const HelpTopicInterface &topic);
+
     private:
         FileOutputRedirectorInterface  *outputRedirector_;
         HelpLinks                       links_;
@@ -571,6 +606,15 @@ void HelpExportReStructuredText::finishModuleGroupExport()
     manPagesFile_.reset();
 }
 
+void HelpExportReStructuredText::exportTopic(const HelpTopicInterface &topic)
+{
+    const std::string path("programs/" + std::string(topic.name()) + ".rst");
+    File              file(outputRedirector_->openFileForWriting(path));
+    HelpWriterContext context(&file, eHelpOutputFormat_Rst, &links_);
+    HelpManager       manager(topic, context);
+    manager.writeCurrentTopic();
+}
+
 /********************************************************************
  * HelpExportCompletion
  */
@@ -598,6 +642,8 @@ class HelpExportCompletion : public HelpExportInterface
                                        const ModuleGroupContents & /*modules*/) {}
         virtual void finishModuleGroupExport() {}
 
+        virtual void exportTopic(const HelpTopicInterface & /*topic*/) {}
+
     private:
         ShellCompletionWriter    bashWriter_;
         std::vector<std::string> modules_;
@@ -642,6 +688,7 @@ void HelpExportCompletion::finishModuleExport()
 /********************************************************************
  * CommandLineHelpModuleImpl implementation
  */
+
 CommandLineHelpModuleImpl::CommandLineHelpModuleImpl(
         const ProgramContextInterface    &programContext,
         const std::string                &binaryName,
@@ -681,6 +728,8 @@ void CommandLineHelpModuleImpl::exportHelp(HelpExportInterface *exporter) const
         exporter->exportModuleGroup((*group)->title(), (*group)->modules());
     }
     exporter->finishModuleGroupExport();
+
+    rootTopic_->exportHelp(exporter);
 }
 
 /********************************************************************
@@ -706,9 +755,9 @@ HelpTopicPointer CommandLineHelpModule::createModuleHelpTopic(
     return HelpTopicPointer(new ModuleHelpTopic(module, *impl_));
 }
 
-void CommandLineHelpModule::addTopic(HelpTopicPointer topic)
+void CommandLineHelpModule::addTopic(HelpTopicPointer topic, bool bExported)
 {
-    impl_->rootTopic_->addSubTopic(move(topic));
+    impl_->rootTopic_->addTopic(move(topic), bExported);
 }
 
 void CommandLineHelpModule::setShowHidden(bool bHidden)
@@ -731,7 +780,7 @@ void CommandLineHelpModule::setOutputRedirector(
 int CommandLineHelpModule::run(int argc, char *argv[])
 {
     // Add internal topics lazily here.
-    addTopic(HelpTopicPointer(new CommandsHelpTopic(*impl_)));
+    addTopic(HelpTopicPointer(new CommandsHelpTopic(*impl_)), false);
 
     const char *const exportFormats[] = { "rst", "completion" };
     std::string       exportFormat;
index 42f25078d6fc190e703cfd808dc535f4eb96fefc..87214a2c0192fdef74fc7d029e34aa27cf7e868f 100644 (file)
@@ -100,10 +100,12 @@ class CommandLineHelpModule : public CommandLineModuleInterface
         /*! \brief
          * Adds a top-level help topic.
          *
-         * \param[in] topic  Help topic to add.
+         * \param[in] topic     Help topic to add.
+         * \param[in] bExported Whether this topic will be directly exported to
+         *     the user guide.
          * \throws    std::bad_alloc if out of memory.
          */
-        void addTopic(HelpTopicPointer topic);
+        void addTopic(HelpTopicPointer topic, bool bExported);
         //! Sets whether hidden options will be shown in help.
         void setShowHidden(bool bHidden);
         /*! \brief
index cd2c65957569a30d5922b163519ea03876f0d566..95766c0ae7da004862729a90d99bfe21b6a7e1bf 100644 (file)
@@ -319,7 +319,7 @@ void CommandLineModuleManager::Impl::addModule(CommandLineModulePointer module)
     HelpTopicPointer helpTopic(helpModule_->createModuleHelpTopic(*module));
     modules_.insert(std::make_pair(std::string(module->name()),
                                    move(module)));
-    helpModule_->addTopic(move(helpTopic));
+    helpModule_->addTopic(move(helpTopic), false);
 }
 
 void CommandLineModuleManager::Impl::ensureHelpModuleExists()
@@ -475,7 +475,7 @@ CommandLineModuleGroup CommandLineModuleManager::addModuleGroup(
 void CommandLineModuleManager::addHelpTopic(HelpTopicPointer topic)
 {
     impl_->ensureHelpModuleExists();
-    impl_->helpModule_->addTopic(move(topic));
+    impl_->helpModule_->addTopic(move(topic), true);
 }
 
 int CommandLineModuleManager::run(int argc, char *argv[])
index 3a2af802dc8c86d8ecdcd5dc6396b77c4862032c..e5b9a139622863d985579b37ff9269a2504e1e06 100644 (file)
@@ -139,10 +139,18 @@ TEST_F(CommandLineHelpModuleTest, ExportsHelp)
         gmx::CommandLineModuleGroup group = manager().addModuleGroup("Group 2");
         group.addModule("other");
     }
+    MockHelpTopic &topic1 = addHelpTopic("topic1", "Test topic");
+    MockHelpTopic &sub1   = topic1.addSubTopic("sub1", "Subtopic 1", "Sub text");
+    MockHelpTopic &sub2   = topic1.addSubTopic("sub2", "Subtopic 2", "Sub text");
+    MockHelpTopic &topic2 = addHelpTopic("topic2", "Another topic");
     using ::testing::_;
     using ::testing::Invoke;
     EXPECT_CALL(mod1, initOptions(_)).WillOnce(Invoke(&initOptionsBasic));
     EXPECT_CALL(mod2, initOptions(_));
+    EXPECT_CALL(topic1, writeHelp(_));
+    EXPECT_CALL(sub1, writeHelp(_));
+    EXPECT_CALL(sub2, writeHelp(_));
+    EXPECT_CALL(topic2, writeHelp(_));
     int rc = 0;
     ASSERT_NO_THROW_GMX(rc = manager().run(args.argc(), args.argv()));
     ASSERT_EQ(0, rc);
index 72816979c356dcd890c4f7e7bb344d54ac2921f7..f85cabe057ec0cabe853a08aa1c26f97c32a4843 100644 (file)
@@ -177,10 +177,6 @@ MockHelpTopic &
 CommandLineModuleManagerTestBase::addHelpTopic(const char *name, const char *title)
 {
     MockHelpTopic *topic = new MockHelpTopic(name, title, "Help text");
-    using ::testing::_;
-    using ::testing::Invoke;
-    ON_CALL(*topic, writeHelp(_))
-        .WillByDefault(Invoke(topic, &MockHelpTopic::writeHelpBase));
     manager().addHelpTopic(gmx::HelpTopicPointer(topic));
     return *topic;
 }
index d8b6126b9cbc0755344c7e686e29823571fb2965..02a51bb3da9d414aeae202e7581e6cfb581d2083 100644 (file)
@@ -106,5 +106,23 @@ Group 2
 +++++++
 | ``test other`` - Second module
 
+]]></String>
+  <String Name="programs/topic1.rst"><![CDATA[
+Test topic
+==========
+Help text
+
+Subtopic 1
+----------
+Sub text
+
+Subtopic 2
+----------
+Sub text
+]]></String>
+  <String Name="programs/topic2.rst"><![CDATA[
+Another topic
+=============
+Help text
 ]]></String>
 </ReferenceData>
index 1cf1585187ef3837339d3feb0562c1026780b584..c2d88baf68f32e077630dde6ae15bb9816f2c5f0 100644 (file)
@@ -140,24 +140,32 @@ AbstractCompositeHelpTopic::writeSubTopicList(const HelpWriterContext &context,
 {
     if (context.outputFormat() != eHelpOutputFormat_Console)
     {
-        // TODO: Implement once the situation with Redmine issue #969 is more
-        // clear.
-        GMX_THROW(NotImplementedError(
-                          "Subtopic listing is not implemented for this output format"));
+        Impl::SubTopicMap::const_iterator topic;
+        for (topic = impl_->subtopics_.begin(); topic != impl_->subtopics_.end(); ++topic)
+        {
+            const char *const title = topic->second->title();
+            if (!isNullOrEmpty(title))
+            {
+                context.outputFile().writeLine();
+                HelpWriterContext subContext(context);
+                subContext.enterSubSection(title);
+                topic->second->writeHelp(subContext);
+            }
+        }
+        return true;
     }
     int maxNameLength = 0;
     Impl::SubTopicMap::const_iterator topic;
     for (topic = impl_->subtopics_.begin(); topic != impl_->subtopics_.end(); ++topic)
     {
-        const char *title = topic->second->title();
-        if (title == NULL || title[0] == '\0')
-        {
-            continue;
-        }
-        int nameLength = static_cast<int>(topic->first.length());
-        if (nameLength > maxNameLength)
+        const char *const title = topic->second->title();
+        if (!isNullOrEmpty(title))
         {
-            maxNameLength = nameLength;
+            int nameLength = static_cast<int>(topic->first.length());
+            if (nameLength > maxNameLength)
+            {
+                maxNameLength = nameLength;
+            }
         }
     }
     if (maxNameLength == 0)
@@ -172,9 +180,9 @@ AbstractCompositeHelpTopic::writeSubTopicList(const HelpWriterContext &context,
     file.writeLine(title);
     for (topic = impl_->subtopics_.begin(); topic != impl_->subtopics_.end(); ++topic)
     {
-        const char *name  = topic->first.c_str();
-        const char *title = topic->second->title();
-        if (title != NULL && title[0] != '\0')
+        const char *const name  = topic->first.c_str();
+        const char *const title = topic->second->title();
+        if (!isNullOrEmpty(title))
         {
             formatter.clear();
             formatter.addColumnLine(0, name);
index 09b22225f1c9a6b2beb304427c4bb704254bc0c9..f28b0f8e12cdca9a5bc176cd78eeaf1968c0522c 100644 (file)
@@ -1,7 +1,7 @@
 /*
  * This file is part of the GROMACS molecular simulation package.
  *
- * Copyright (c) 2012,2013,2014, by the GROMACS development team, led by
+ * Copyright (c) 2012,2013,2014,2015, by the GROMACS development team, led by
  * Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
  * and including many others, as listed in the AUTHORS file in the
  * top-level source directory and at http://www.gromacs.org.
@@ -102,10 +102,10 @@ TEST_F(HelpManagerTest, HandlesRootTopic)
 TEST_F(HelpManagerTest, HandlesSubTopics)
 {
     MockHelpTopic &first =
-        rootTopic_.addSubTopic("first", "First topic", "First topic text");
+        rootTopic_.addSubTopic("first", "First topic", NULL);
     MockHelpTopic &firstSub =
-        first.addSubTopic("firstsub", "First subtopic", "First subtopic text");
-    rootTopic_.addSubTopic("second", "Second topic", "Second topic text");
+        first.addSubTopic("firstsub", "First subtopic", NULL);
+    rootTopic_.addSubTopic("second", "Second topic", NULL);
 
     using ::testing::_;
     EXPECT_CALL(firstSub, writeHelp(_));
@@ -117,9 +117,9 @@ TEST_F(HelpManagerTest, HandlesSubTopics)
 TEST_F(HelpManagerTest, HandlesInvalidTopics)
 {
     MockHelpTopic &first =
-        rootTopic_.addSubTopic("first", "First topic", "First topic text");
-    first.addSubTopic("firstsub", "First subtopic", "First subtopic text");
-    rootTopic_.addSubTopic("second", "Second topic", "Second topic text");
+        rootTopic_.addSubTopic("first", "First topic", NULL);
+    first.addSubTopic("firstsub", "First subtopic", NULL);
+    rootTopic_.addSubTopic("second", "Second topic", NULL);
 
     ASSERT_THROW_GMX(manager_.enterTopic("unknown"), gmx::InvalidInputError);
     ASSERT_NO_THROW_GMX(manager_.enterTopic("first"));
index 5d7484d1f902fb919c0993ec7e561565488808cb..3579fc7d829f11eb2aca10145651cc02abaa4856 100644 (file)
@@ -1,7 +1,7 @@
 /*
  * This file is part of the GROMACS molecular simulation package.
  *
- * Copyright (c) 2012,2014, by the GROMACS development team, led by
+ * Copyright (c) 2012,2014,2015, by the GROMACS development team, led by
  * Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
  * and including many others, as listed in the AUTHORS file in the
  * top-level source directory and at http://www.gromacs.org.
@@ -46,6 +46,8 @@
 #include <gmock/gmock.h>
 #include <gtest/gtest.h>
 
+#include "gromacs/utility/stringutil.h"
+
 namespace gmx
 {
 namespace test
@@ -67,8 +69,15 @@ MockHelpTopic::addSubTopic(gmx::AbstractCompositeHelpTopic *parent,
 }
 
 MockHelpTopic::MockHelpTopic(const char *name, const char *title, const char *text)
-    : name_(name), title_(title), text_(text)
+    : name_(name), title_(title), text_(text != NULL ? text : "")
 {
+    if (!isNullOrEmpty(text))
+    {
+        using ::testing::_;
+        using ::testing::Invoke;
+        ON_CALL(*this, writeHelp(_))
+            .WillByDefault(Invoke(this, &MockHelpTopic::writeHelpBase));
+    }
 }
 
 MockHelpTopic::~MockHelpTopic()
index 649ff921c3e07dbaa531e432f48a314c20eab16c..d1996e0b6fb4d68211d152e80bff361196a86eb6 100644 (file)
@@ -76,8 +76,7 @@ class MockHelpTopic : public AbstractCompositeHelpTopic
          * Calls base class writeHelp() method.
          *
          * This provides the possibility for the mock to do the actual help
-         * writing.  The caller must set this as the writeHelp() action, as
-         * most tests do not want to get the output.
+         * writing.
          */
         void writeHelpBase(const HelpWriterContext &context)
         {
index 8487fafe69f59d003b334a717ef8c4bed7c8480e..02cdfcb45a8f6ed53be911b24b4d15c6d8ca7d87 100644 (file)
@@ -157,10 +157,10 @@ const char *const CmdLineHelpText::text[] = {
     "Depending on the tool, two additional command-line arguments may be",
     "available to control the behavior:",
     "",
-    " * [TT]-seltype[tt] can be used to specify the default type of",
-    "   positions to calculate for each selection.",
-    " * [TT]-selrpos[tt] can be used to specify the default type of",
-    "   positions used in selecting atoms by coordinates.",
+    "* [TT]-seltype[tt] can be used to specify the default type of",
+    "  positions to calculate for each selection.",
+    "* [TT]-selrpos[tt] can be used to specify the default type of",
+    "  positions used in selecting atoms by coordinates.",
     "",
     "See the \"positions\" subtopic for more information on these options.",
 };
@@ -227,7 +227,7 @@ const char *const ExamplesHelpText::text[] = {
     "",
 
     "Centers of mass of residues 1 to 5 and 10::",
-    ""
+    "",
     "  res_com of resnr 1 to 5 10",
     "",
 
@@ -418,25 +418,25 @@ const char *const SyntaxHelpText::text[] = {
     "would be valid.[PAR]",
 
     "Selections are composed of three main types of expressions, those that",
-    "define atoms ([TT]ATOM_EXPR[tt]s), those that define positions",
-    "([TT]POS_EXPR[tt]s), and those that evaluate to numeric values",
-    "([TT]NUM_EXPR[tt]s). Each selection should be a [TT]POS_EXPR[tt]",
+    "define atoms ([TT]ATOM_EXPR[tt]), those that define positions",
+    "([TT]POS_EXPR[tt]), and those that evaluate to numeric values",
+    "([TT]NUM_EXPR[tt]). Each selection should be a [TT]POS_EXPR[tt]",
     "or a [TT]ATOM_EXPR[tt] (the latter is automatically converted to",
     "positions). The basic rules are as follows:",
     "",
-    " * An expression like [TT]NUM_EXPR1 < NUM_EXPR2[tt] evaluates to an",
-    "   [TT]ATOM_EXPR[tt] that selects all the atoms for which the comparison",
-    "   is true.",
-    " * Atom expressions can be combined with boolean operations such as",
-    "   [TT]not ATOM_EXPR[tt], [TT]ATOM_EXPR and ATOM_EXPR[tt], or",
-    "   [TT]ATOM_EXPR or ATOM_EXPR[tt]. Parentheses can be used to alter the",
-    "   evaluation order.",
-    " * [TT]ATOM_EXPR[tt] expressions can be converted into [TT]POS_EXPR[tt]",
-    "   expressions in various ways, see the \"positions\" subtopic for more",
-    "   details.",
-    " * [TT]POS_EXPR[tt] can be converted into [TT]NUM_EXPR[tt] using syntax",
-    "   like \"x of POS_EXPR\". Currently, this is only supported for single",
-    "   positions like in expression \"x of cog of ATOM_EXPR\".",
+    "* An expression like [TT]NUM_EXPR1 < NUM_EXPR2[tt] evaluates to an",
+    "  [TT]ATOM_EXPR[tt] that selects all the atoms for which the comparison",
+    "  is true.",
+    "* Atom expressions can be combined with boolean operations such as",
+    "  [TT]not ATOM_EXPR[tt], [TT]ATOM_EXPR and ATOM_EXPR[tt], or",
+    "  [TT]ATOM_EXPR or ATOM_EXPR[tt]. Parentheses can be used to alter the",
+    "  evaluation order.",
+    "* [TT]ATOM_EXPR[tt] expressions can be converted into [TT]POS_EXPR[tt]",
+    "  expressions in various ways, see the \"positions\" subtopic for more",
+    "  details.",
+    "* [TT]POS_EXPR[tt] can be converted into [TT]NUM_EXPR[tt] using syntax",
+    "  like \"[TT]x of POS_EXPR[tt]\". Currently, this is only supported for single",
+    "  positions like in expression \"[TT]x of cog of ATOM_EXPR[tt]\".",
     "",
 
     "Some keywords select atoms based on string values such as the atom name.",
@@ -538,6 +538,17 @@ class KeywordsHelpTopic : public CompositeHelpTopic<KeywordsHelpText>
                                       const gmx_ana_selmethod_t *> >
             MethodList;
 
+        /*! \brief
+         * Prints markup for starting a list of keywords.
+         */
+        void writeKeywordListStart(const HelpWriterContext &context,
+                                   const char              *heading) const;
+        /*! \brief
+         * Prints markup for ending a list of keywords.
+         */
+        void writeKeywordListEnd(const HelpWriterContext &context,
+                                 const char              *extraInfo) const;
+
         /*! \brief
          * Prints a brief list of keywords (selection methods) available.
          *
@@ -579,45 +590,60 @@ KeywordsHelpTopic::KeywordsHelpTopic()
 
 void KeywordsHelpTopic::writeHelp(const HelpWriterContext &context) const
 {
-    if (context.outputFormat() != eHelpOutputFormat_Console)
-    {
-        GMX_THROW(NotImplementedError(
-                          "Selection help is not implemented for this output format"));
-    }
-    // TODO: The markup here is not really appropriate, and printKeywordList()
-    // still prints raw text, but these are waiting for discussion of the
-    // markup format in #969.
     context.writeTextBlock(helpText());
     context.writeTextBlock("");
 
     // Print the list of keywords
-    context.writeTextBlock("Keywords that select atoms by an integer property:");
-    context.writeTextBlock("(use in expressions or like \"atomnr 1 to 5 7 9\")");
+    writeKeywordListStart(context, "Keywords that select atoms by an integer property:");
     printKeywordList(context, INT_VALUE, false);
-    context.writeTextBlock("");
+    writeKeywordListEnd(context, "(use in expressions or like \"atomnr 1 to 5 7 9\")");
 
-    context.writeTextBlock("Keywords that select atoms by a numeric property:");
-    context.writeTextBlock("(use in expressions or like \"occupancy 0.5 to 1\")");
+    writeKeywordListStart(context, "Keywords that select atoms by a numeric property:");
     printKeywordList(context, REAL_VALUE, false);
-    context.writeTextBlock("");
+    writeKeywordListEnd(context, "(use in expressions or like \"occupancy 0.5 to 1\")");
 
-    context.writeTextBlock("Keywords that select atoms by a string property:");
-    context.writeTextBlock("(use like \"name PATTERN [PATTERN] ...\")");
+    writeKeywordListStart(context, "Keywords that select atoms by a string property:");
     printKeywordList(context, STR_VALUE, false);
-    context.writeTextBlock("");
+    writeKeywordListEnd(context, "(use like \"name PATTERN [PATTERN] ...\")");
 
-    context.writeTextBlock("Additional keywords that directly select atoms:");
+    writeKeywordListStart(context, "Additional keywords that directly select atoms:");
     printKeywordList(context, GROUP_VALUE, false);
-    context.writeTextBlock("");
+    writeKeywordListEnd(context, NULL);
 
-    context.writeTextBlock("Keywords that directly evaluate to positions:");
-    context.writeTextBlock("(see also \"positions\" subtopic)");
+    writeKeywordListStart(context, "Keywords that directly evaluate to positions:");
     printKeywordList(context, POS_VALUE, false);
-    context.writeTextBlock("");
+    writeKeywordListEnd(context, "(see also \"positions\" subtopic)");
 
-    context.writeTextBlock("Additional keywords:");
+    writeKeywordListStart(context, "Additional keywords:");
     printKeywordList(context, POS_VALUE, true);
     printKeywordList(context, NO_VALUE, true);
+    writeKeywordListEnd(context, NULL);
+}
+
+void KeywordsHelpTopic::writeKeywordListStart(const HelpWriterContext &context,
+                                              const char              *heading) const
+{
+    context.writeTextBlock(heading);
+    if (context.outputFormat() == eHelpOutputFormat_Rst)
+    {
+        context.writeTextBlock("");
+        context.writeTextBlock("::");
+        context.writeTextBlock("");
+    }
+}
+
+void KeywordsHelpTopic::writeKeywordListEnd(const HelpWriterContext &context,
+                                            const char              *extraInfo) const
+{
+    if (context.outputFormat() == eHelpOutputFormat_Rst)
+    {
+        context.writeTextBlock("");
+    }
+    if (!isNullOrEmpty(extraInfo))
+    {
+        context.writeTextBlock(extraInfo);
+    }
+    context.writeTextBlock("");
 }
 
 void KeywordsHelpTopic::printKeywordList(const HelpWriterContext &context,