Refactor rst parsing for console output
authorTeemu Murtola <teemu.murtola@gmail.com>
Tue, 2 Jun 2015 10:26:30 +0000 (13:26 +0300)
committerTeemu Murtola <teemu.murtola@gmail.com>
Thu, 4 Jun 2015 12:11:04 +0000 (14:11 +0200)
Move code from helpwritercontext.cpp into a separate class in
rstparser.*, and structure it more cleanly.  The logic is mostly the
same as earlier, but the new structure also makes it easier to fix
some issues.  In particular, since the code now first identifies what
the next paragraph is, and uses a separate loop to format it, it is much
easier to adapt the formatting to the contents of the paragraph.
Add comments for the complicated paragraph parsing logic.

To support this change, make TextLineWrapper not produce whitespace-only
lines if requested to indent an empty line.

Add tests for fixed/enhanced functionality.

Change-Id: I690dcf1578780d5fb0016d41da1e67dca96bd08c

docs/doxygen/user/onlinehelp.md
src/gromacs/onlinehelp-doc.h
src/gromacs/onlinehelp/helpwritercontext.cpp
src/gromacs/onlinehelp/rstparser.cpp [new file with mode: 0644]
src/gromacs/onlinehelp/rstparser.h [new file with mode: 0644]
src/gromacs/onlinehelp/tests/helpwritercontext.cpp
src/gromacs/onlinehelp/tests/refdata/HelpWriterContextTest_FormatsLiteralTextWithIndentation.xml [new file with mode: 0644]
src/gromacs/onlinehelp/tests/refdata/HelpWriterContextTest_FormatsTitles.xml
src/gromacs/utility/stringutil.cpp
src/gromacs/utility/tests/refdata/TextLineWrapperTest_HandlesIndentWithEmptyLines.xml [new file with mode: 0644]
src/gromacs/utility/tests/stringutil.cpp

index 3c70c6dea051067d34d2955089f7d1099719ab99..4c3e0ce4bd094be3f3185945789bc9974cc6d809 100644 (file)
@@ -57,26 +57,24 @@ output:
    reStructuredText).  This means that block quotes are also rendered
    reasonably, since they are just indented paragraphs.
  - Literal blocks, i.e., indented paragraphs where the preceding paragraph ends
-   with `::`.  Line breaks within such paragraphs are preserved (but
-   varying indentation is not currently implemented).  The rules for handling
-   the `::` are the same as in reStructuredText.
+   with `::`.  Line breaks within such paragraphs are preserved.  The rules for
+   handling the `::` are the same as in reStructuredText.
+   Multiple paragraphs within a literal block are not currently supported.
  - Titles, i.e., a line underlined by a punctuation character.
    Title formatting is currently preserved as-is, so it must be manually
    ensured that the used punctuation character properly fits into the context
    (i.e., other titles in the same generated reStructuredText document).
    Titles with both under- and overline are not currently supported.
  - Bullet lists.  Only lists that start with `*` are currently recognized.
-   Indentation for the second and subsequent lines is either 2 (if the bullet
-   text is on a single line in input), or determined from the second line in
-   the input.
+   Indentation for the second and subsequent lines is determined from
+   the first non-space character after the bullet and/or from the second line
+   in the input (if these are not the same, the minimum is used).
    Note that in reStructuredText, the `*` must not be indented with respect to
    the preceding paragraph; otherwise, the bullet list is rendered within a
-   block quote.
+   block quote.  Also, an empty line needs to precede a bullet list.
  - Enumerated lists.  Only lists that start with digits are supported (e.g.,
    `1.`).  Multi-digit numbers can be used.
-   Indentation for the second and subsequent lines is determined either from
-   the first non-space character after the period (if there is a single line in
-   the input), or from the second line in the input.
+   Indentation is determined as for bullet lists.
    Lists are not renumbered automatically.
 
 \Gromacs-specific markup
index 8a7e1f722378f8fd9bf396e96d0e603ecdfd0e1e..fd1e7a0c9f99675676af62f691b013833cf80d30 100644 (file)
@@ -44,6 +44,8 @@
  *    output formats from the same input strings and API calls.  Whenever
  *    possible, the output format should be abstracted using this interface,
  *    but in some cases code still writes out raw reStructuredText.
+ *  - rstparser.h provides the functionality to parse reStructuredText such that
+ *    it can be rewrapped for console output.
  *  - helpformat.h provides some general text-processing classes, currently
  *    focused on producing aligned tables for console output.
  *  - helptopicinterface.h, helptopic.h, and helpmanager.h provide classes for
index 680828df70bc38938f65f5a5c1d67e7a4aaea06e..0c3c37abf373baedc9d9b6a656610c08f913a4cc 100644 (file)
@@ -57,6 +57,8 @@
 #include "gromacs/utility/programcontext.h"
 #include "gromacs/utility/stringutil.h"
 
+#include "rstparser.h"
+
 namespace gmx
 {
 
@@ -338,98 +340,6 @@ std::string removeExtraNewlinesRst(const std::string &text)
     return result;
 }
 
-/*! \brief
- * Returns `true` if a list item starts in \p text at \p index.
- *
- * Does not throw.
- */
-bool startsListItem(const std::string &text, size_t index)
-{
-    if (text.length() <= index + 1)
-    {
-        return false;
-    }
-    if (text[index] == '*' && std::isspace(text[index+1]))
-    {
-        return true;
-    }
-    if (std::isdigit(text[index]))
-    {
-        while (index < text.length() && std::isdigit(text[index]))
-        {
-            ++index;
-        }
-        if (text.length() > index + 1 && text[index] == '.'
-            && std::isspace(text[index+1]))
-        {
-            return true;
-        }
-    }
-    return false;
-}
-
-/*! \brief
- * Returns `true` if a table starts in \p text at \p index.
- *
- * The function only inspects the first line for something that looks like a
- * reStructuredText table, and accepts also some malformed tables.
- * Any issues should be apparent when Sphinx parses the reStructuredText
- * export, so full validation is not done here.
- *
- * Does not throw.
- */
-bool startsTable(const std::string &text, size_t index)
-{
-    if (text[index] == '=')
-    {
-        while (index < text.length() && text[index] != '\n')
-        {
-            if (text[index] != '=' && !std::isspace(text[index]))
-            {
-                return false;
-            }
-            ++index;
-        }
-        return true;
-    }
-    else if (text[index] == '+')
-    {
-        while (index < text.length() && text[index] != '\n')
-        {
-            if (text[index] != '-' && text[index] != '+')
-            {
-                return false;
-            }
-            ++index;
-        }
-        return true;
-    }
-    return false;
-}
-
-/*! \brief
- * Returns `true` if a line in \p text starting at \p index is a title underline.
- *
- * Does not throw.
- */
-bool isTitleUnderline(const std::string &text, size_t index)
-{
-    const char firstChar = text[index];
-    if (std::ispunct(firstChar))
-    {
-        while (index < text.length() && text[index] != '\n')
-        {
-            if (text[index] != firstChar)
-            {
-                return false;
-            }
-            ++index;
-        }
-        return true;
-    }
-    return false;
-}
-
 //! \}
 
 }   // namespace
@@ -636,140 +546,18 @@ void HelpWriterContext::Impl::processMarkup(const std::string &text,
             const int   baseIndent          = wrapper->settings().indent();
             result = repall(result, sandrTty);
             result = replaceLinks(result);
-            std::string paragraph;
+            std::string          paragraph;
             paragraph.reserve(result.length());
-            size_t      i             = 0;
-            int         nextBreakSize = 0;
-            bool        bLiteral      = false;
-            while (i < result.length())
+            RstParagraphIterator iter(result);
+            while (iter.nextParagraph())
             {
-                while (i < result.length() && result[i] == '\n')
-                {
-                    ++i;
-                }
-                if (i == result.length())
-                {
-                    break;
-                }
-                const int breakSize     = nextBreakSize;
-                int       currentLine   = 0;
-                bool      bLineStart    = true;
-                int       currentIndent = 0;
-                int       firstIndent   = 0;
-                int       indent        = 0;
-                paragraph.clear();
-                for (;; ++i)
-                {
-                    if (result[i] == '\n' || i == result.length())
-                    {
-                        if (currentLine == 0)
-                        {
-                            firstIndent = currentIndent;
-                        }
-                        else if (currentLine == 1)
-                        {
-                            indent = currentIndent;
-                        }
-                        ++currentLine;
-                        bLineStart    = true;
-                        currentIndent = 0;
-                        if (i + 1 >= result.length() || result[i + 1] == '\n')
-                        {
-                            nextBreakSize = 2;
-                            break;
-                        }
-                        if (!bLiteral)
-                        {
-                            if (!std::isspace(result[i - 1]))
-                            {
-                                paragraph.push_back(' ');
-                            }
-                            continue;
-                        }
-                    }
-                    else if (bLineStart)
-                    {
-                        if (std::isspace(result[i]))
-                        {
-                            ++currentIndent;
-                            continue;
-                        }
-                        else if (startsListItem(result, i))
-                        {
-                            if (currentLine > 0)
-                            {
-                                while (i > 0 && result[i - 1] != '\n')
-                                {
-                                    --i;
-                                }
-                                paragraph     = stripString(paragraph);
-                                nextBreakSize = 1;
-                                break;
-                            }
-                            int prefixLength = 0;
-                            while (!std::isspace(result[i + prefixLength]))
-                            {
-                                ++prefixLength;
-                            }
-                            while (i + prefixLength < result.length()
-                                   && std::isspace(result[i + prefixLength]))
-                            {
-                                ++prefixLength;
-                            }
-                            indent = currentIndent + prefixLength;
-                        }
-                        else if (currentLine == 0 && startsTable(result, i))
-                        {
-                            bLiteral = true;
-                        }
-                        else if (currentLine == 1 && isTitleUnderline(result, i))
-                        {
-                            // TODO: Nicer formatting that shares
-                            // implementation with writeTitle() and honors the
-                            // nesting depths etc.
-                            if (i > 0)
-                            {
-                                paragraph[paragraph.length() - 1] = '\n';
-                            }
-                        }
-                        bLineStart = false;
-                    }
-                    paragraph.push_back(result[i]);
-                }
-                if (endsWith(paragraph, "::"))
-                {
-                    bLiteral = true;
-                    if (paragraph.length() == 2)
-                    {
-                        if (breakSize == 0)
-                        {
-                            nextBreakSize = 0;
-                        }
-                        continue;
-                    }
-                    if (paragraph[paragraph.length() - 3] == ' ')
-                    {
-                        paragraph.resize(paragraph.length() - 3);
-                    }
-                    else
-                    {
-                        paragraph.resize(paragraph.length() - 1);
-                    }
-                }
-                else
-                {
-                    bLiteral = false;
-                }
-                if (breakSize > 0)
-                {
-                    wrapper->wrap(std::string(breakSize, '\n'));
-                }
-                wrapper->settings().setFirstLineIndent(baseFirstLineIndent + firstIndent);
-                wrapper->settings().setIndent(baseIndent + indent);
+                iter.getParagraphText(&paragraph);
+                wrapper->settings().setFirstLineIndent(baseFirstLineIndent + iter.firstLineIndent());
+                wrapper->settings().setIndent(baseIndent + iter.indent());
                 wrapper->wrap(paragraph);
-                wrapper->settings().setFirstLineIndent(baseFirstLineIndent);
-                wrapper->settings().setIndent(baseIndent);
             }
+            wrapper->settings().setFirstLineIndent(baseFirstLineIndent);
+            wrapper->settings().setIndent(baseIndent);
             break;
         }
         case eHelpOutputFormat_Rst:
diff --git a/src/gromacs/onlinehelp/rstparser.cpp b/src/gromacs/onlinehelp/rstparser.cpp
new file mode 100644 (file)
index 0000000..dc5edac
--- /dev/null
@@ -0,0 +1,340 @@
+/*
+ * This file is part of the GROMACS molecular simulation package.
+ *
+ * Copyright (c) 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.
+ *
+ * GROMACS is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1
+ * of the License, or (at your option) any later version.
+ *
+ * GROMACS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GROMACS; if not, see
+ * http://www.gnu.org/licenses, or write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
+ *
+ * If you want to redistribute modifications to GROMACS, please
+ * consider that scientific software is very special. Version
+ * control is crucial - bugs must be traceable. We will be happy to
+ * consider code for inclusion in the official distribution, but
+ * derived work must not be called official GROMACS. Details are found
+ * in the README & COPYING files - if they are missing, get the
+ * official version at http://www.gromacs.org.
+ *
+ * To help us fund GROMACS development, we humbly ask that you cite
+ * the research papers on the package. Check out http://www.gromacs.org.
+ */
+/*! \internal \file
+ * \brief
+ * Implements classes from rstparser.h.
+ *
+ * \author Teemu Murtola <teemu.murtola@gmail.com>
+ * \ingroup module_onlinehelp
+ */
+#include "gmxpre.h"
+
+#include "rstparser.h"
+
+#include <cctype>
+
+#include <algorithm>
+
+#include "gromacs/utility/stringutil.h"
+
+namespace gmx
+{
+
+namespace
+{
+
+/*! \brief
+ * Counts the number of leading spaces in a text range.
+ *
+ * Does not throw.
+ */
+int countLeadingSpace(const std::string &text, size_t start, size_t end)
+{
+    for (size_t i = start; i < end; ++i)
+    {
+        if (!std::isspace(text[i]))
+        {
+            return i - start;
+        }
+    }
+    return end - start;
+}
+
+/*! \brief
+ * Returns `true` if a list item starts in \p text at \p index.
+ *
+ * Does not throw.
+ */
+bool startsListItem(const std::string &text, size_t index)
+{
+    if (text.length() <= index + 1)
+    {
+        return false;
+    }
+    if (text[index] == '*' && std::isspace(text[index+1]))
+    {
+        return true;
+    }
+    if (std::isdigit(text[index]))
+    {
+        while (index < text.length() && std::isdigit(text[index]))
+        {
+            ++index;
+        }
+        if (text.length() > index + 1 && text[index] == '.'
+            && std::isspace(text[index+1]))
+        {
+            return true;
+        }
+    }
+    return false;
+}
+
+/*! \brief
+ * Returns `true` if a table starts in \p text at \p index.
+ *
+ * The function only inspects the first line for something that looks like a
+ * reStructuredText table, and accepts also some malformed tables.
+ * Any issues should be apparent when Sphinx parses the reStructuredText
+ * export, so full validation is not done here.
+ *
+ * Does not throw.
+ */
+bool startsTable(const std::string &text, size_t index)
+{
+    if (text[index] == '=')
+    {
+        while (index < text.length() && text[index] != '\n')
+        {
+            if (text[index] != '=' && !std::isspace(text[index]))
+            {
+                return false;
+            }
+            ++index;
+        }
+        return true;
+    }
+    else if (text[index] == '+')
+    {
+        while (index < text.length() && text[index] != '\n')
+        {
+            if (text[index] != '-' && text[index] != '+')
+            {
+                return false;
+            }
+            ++index;
+        }
+        return true;
+    }
+    return false;
+}
+
+/*! \brief
+ * Returns `true` if a line in \p text starting at \p index is a title underline.
+ *
+ * Does not throw.
+ */
+bool isTitleUnderline(const std::string &text, size_t index)
+{
+    const char firstChar = text[index];
+    if (std::ispunct(firstChar))
+    {
+        while (index < text.length() && text[index] != '\n')
+        {
+            if (text[index] != firstChar)
+            {
+                return false;
+            }
+            ++index;
+        }
+        return true;
+    }
+    return false;
+}
+
+}    // namespace
+
+/********************************************************************
+ * RstParagraphIterator
+ */
+
+RstParagraphIterator::RstParagraphIterator(const std::string &text)
+    : text_(text), begin_(0), end_(0), type_(eParagraphType_Normal),
+      breakSize_(0), firstLineIndent_(0), indent_(0),
+      nextBegin_(0), nextBreakSize_(0), literalIndent_(-1)
+{
+}
+
+bool RstParagraphIterator::nextParagraph()
+{
+    begin_     = nextBegin_;
+    type_      = eParagraphType_Normal;
+    breakSize_ = nextBreakSize_;
+    // Skip leading newlines (includes those separating paragraphs).
+    while (begin_ < text_.length() && text_[begin_] == '\n')
+    {
+        ++begin_;
+    }
+    if (begin_ == text_.length())
+    {
+        end_       = begin_;
+        breakSize_ = 0;
+        nextBegin_ = begin_;
+        return false;
+    }
+    if (literalIndent_ >= 0)
+    {
+        type_ = eParagraphType_Literal;
+    }
+    // Loop over lines in input until the end of the current paragraph.
+    size_t i         = begin_;
+    int    lineCount = 0;
+    while (true)
+    {
+        const bool   bFirstLine = (lineCount == 0);
+        const size_t lineStart  = i;
+        const size_t lineEnd    = std::min(text_.find('\n', i), text_.length());
+        const int    lineIndent = countLeadingSpace(text_, lineStart, lineEnd);
+        const size_t textStart  = lineStart + lineIndent;
+        const bool   bListItem  = startsListItem(text_, textStart);
+        // Return each list item as a separate paragraph to make the behavior
+        // the same always; the item text could even contain multiple
+        // paragraphs, that would anyways produce breaks.
+        if (bListItem && !bFirstLine)
+        {
+            // Since there was no empty line in input, do not produce one in
+            // the output, either.
+            nextBreakSize_ = 1;
+            // end_ is not updated to break the paragraph before the current line.
+            break;
+        }
+        // Now we will actually use this line as part of this paragraph.
+        end_ = lineEnd;
+        ++lineCount;
+        // Update indentation.
+        if (bFirstLine)
+        {
+            firstLineIndent_ = indent_ = lineIndent;
+            if (bListItem)
+            {
+                // Find the indentation of the actual text after the
+                // bullet/number.
+                int prefixLength = 0;
+                while (!std::isspace(text_[textStart + prefixLength]))
+                {
+                    ++prefixLength;
+                }
+                while (textStart + prefixLength < text_.length()
+                       && std::isspace(text_[textStart + prefixLength]))
+                {
+                    ++prefixLength;
+                }
+                indent_ += prefixLength;
+            }
+        }
+        else
+        {
+            indent_ = std::min(indent_, lineIndent);
+        }
+        // We need to check for the title underline before checking for the
+        // paragraph break so that the title is correctly recognized.
+        if (lineCount == 2 && isTitleUnderline(text_, lineStart))
+        {
+            type_ = eParagraphType_Title;
+        }
+        // Check for end-of-input or an empty line, i.e., a normal paragraph
+        // break.
+        if (lineEnd + 1 >= text_.length() || text_[lineEnd + 1] == '\n')
+        {
+            nextBreakSize_ = 2;
+            break;
+        }
+        // Always return the title as a separate paragraph, as it requires
+        // different processing.
+        // TODO: This should allow nicer formatting that shares
+        // implementation with writeTitle() and honors the nesting depths etc.,
+        // but that is not implemented.
+        if (type_ == eParagraphType_Title)
+        {
+            // If we are here, there was no actual paragraph break, so do not
+            // produce one in the output either.
+            nextBreakSize_ = 1;
+            break;
+        }
+        // Next loop starts at the character after the newline.
+        i = lineEnd + 1;
+    }
+    nextBegin_ = end_;
+    // Check if the next paragraph should be treated as a literal paragraph,
+    // and deal with transformations for the :: marker.
+    if (end_ - begin_ >= 2 && text_.compare(end_ - 2, 2, "::") == 0)
+    {
+        literalIndent_ = indent_;
+        // Return the actual literal block if the paragraph was just an "::".
+        if (end_ - begin_ == 2)
+        {
+            // Avoid leading whitespace at the beginning; breakSize_ == 0
+            // only for the first paragraph.
+            if (breakSize_ == 0)
+            {
+                nextBreakSize_ = 0;
+            }
+            return nextParagraph();
+        }
+        // Remove one of the colons, or both if preceded by whitespace.
+        const bool bRemoveDoubleColon = (text_[end_ - 3] == ' ');
+        end_ -= (bRemoveDoubleColon ? 3 : 1);
+    }
+    else
+    {
+        literalIndent_ = -1;
+    }
+    // Treat a table like a literal block (preserve newlines).
+    if (startsTable(text_, begin_ + firstLineIndent_))
+    {
+        type_ = eParagraphType_Literal;
+    }
+    return true;
+}
+
+void RstParagraphIterator::getParagraphText(std::string *result) const
+{
+    result->clear();
+    result->reserve(end_ - begin_);
+    result->append(breakSize_, '\n');
+    const bool bPreserveNewlines = (type_ != eParagraphType_Normal);
+    size_t     i                 = begin_;
+    while (i < end_)
+    {
+        const bool   bFirstLine = (i == begin_);
+        const size_t lineStart  = i + (bFirstLine ? firstLineIndent_ : indent_);
+        const size_t lineEnd    = std::min(text_.find('\n', i), end_);
+        if (!bFirstLine)
+        {
+            if (bPreserveNewlines)
+            {
+                result->push_back('\n');
+            }
+            else if (!std::isspace((*result)[result->length() - 1]))
+            {
+                result->push_back(' ');
+            }
+        }
+        result->append(text_, lineStart, lineEnd - lineStart);
+        i = lineEnd + 1;
+    }
+}
+
+} // namespace gmx
diff --git a/src/gromacs/onlinehelp/rstparser.h b/src/gromacs/onlinehelp/rstparser.h
new file mode 100644 (file)
index 0000000..c250209
--- /dev/null
@@ -0,0 +1,147 @@
+/*
+ * This file is part of the GROMACS molecular simulation package.
+ *
+ * Copyright (c) 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.
+ *
+ * GROMACS is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1
+ * of the License, or (at your option) any later version.
+ *
+ * GROMACS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GROMACS; if not, see
+ * http://www.gnu.org/licenses, or write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
+ *
+ * If you want to redistribute modifications to GROMACS, please
+ * consider that scientific software is very special. Version
+ * control is crucial - bugs must be traceable. We will be happy to
+ * consider code for inclusion in the official distribution, but
+ * derived work must not be called official GROMACS. Details are found
+ * in the README & COPYING files - if they are missing, get the
+ * official version at http://www.gromacs.org.
+ *
+ * To help us fund GROMACS development, we humbly ask that you cite
+ * the research papers on the package. Check out http://www.gromacs.org.
+ */
+/*! \internal \file
+ * \brief
+ * Declares classes for (partial) parsing of reStructuredText.
+ *
+ * \author Teemu Murtola <teemu.murtola@gmail.com>
+ * \ingroup module_onlinehelp
+ */
+#ifndef GMX_ONLINEHELP_RSTPARSER_H
+#define GMX_ONLINEHELP_RSTPARSER_H
+
+#include <string>
+
+#include "gromacs/utility/classhelpers.h"
+
+namespace gmx
+{
+
+class TextLineWrapperSettings;
+
+/*! \internal
+ * \brief
+ * Iterator over reStructuredText paragraphs.
+ *
+ * After initialization, nextParagraph() needs to be called to access the first
+ * paragraph.  Subsequence paragraphs can be accessed by repeated calls to
+ * nextParagraph().  After the last paragraph, nextParagraph() returns `false`.
+ *
+ * After each call to nextParagraph(), other methods can be called to query
+ * details of the current paragraph.
+ *
+ * \ingroup module_onlinehelp
+ */
+class RstParagraphIterator
+{
+    public:
+        /*! \brief
+         * Initializes an iterator for given input text.
+         *
+         * Does not throw.
+         */
+        explicit RstParagraphIterator(const std::string &text);
+
+        /*! \brief
+         * Advances the iterator to the next paragraph.
+         *
+         * \returns `false` if there were no more paragraphs.
+         *
+         * Does not throw (except std::bad_alloc if std::string::compare()
+         * throws).
+         */
+        bool nextParagraph();
+
+        //! Returns the indentation for first line of this paragraph.
+        int firstLineIndent() const { return firstLineIndent_; }
+        //! Returns the indentation for subsequent lines of this paragraph.
+        int indent() const { return indent_; }
+        /*! \brief
+         * Returns the text
+         *
+         * \param[out] result  Variable to receive the paragraph text.
+         * \throws std::bad_alloc if out of memory.
+         *
+         * Indentation and internal line breaks have been stripped from the
+         * paragraph text (except for literal blocks etc.).  For literal
+         * blocks, the common indentation has been stripped and is returned in
+         * indent() instead.
+         *
+         * Leading newlines are returned to indicate necessary separation from
+         * the preceding paragraph.
+         */
+        void getParagraphText(std::string *result) const;
+
+    private:
+        enum ParagraphType
+        {
+            eParagraphType_Normal,
+            eParagraphType_Literal,
+            eParagraphType_Title
+        };
+
+        //! The text to iterate over.
+        const std::string &text_;
+
+        //! Start of the current paragraph.
+        size_t             begin_;
+        //! End of the current paragraph (C++-style iterator).
+        size_t             end_;
+        //! Type of the current paragraph.
+        ParagraphType      type_;
+        //! Number of newlines to print before the current paragraph.
+        int                breakSize_;
+        //! Indentation of the first line of this paragraph.
+        int                firstLineIndent_;
+        //! (Minimum) indentation of other lines in this paragraph.
+        int                indent_;
+
+        //! Start of the next paragrah.
+        size_t             nextBegin_;
+        //! Number of newlines to print after the current paragraph.
+        int                nextBreakSize_;
+        /*! \brief
+         * Indentation of the preceding paragraph that contained `::`.
+         *
+         * If the next paragraph is not a literal block, the value is `-1`.
+         */
+        int                literalIndent_;
+
+        GMX_DISALLOW_COPY_AND_ASSIGN(RstParagraphIterator);
+};
+
+} // namespace gmx
+
+#endif
index 1b2c4acf681baa786fe16a37d36551feb203c1ac..d41ca87f6b2a9317e3682e81a53970b71ae7aa82 100644 (file)
@@ -168,6 +168,20 @@ TEST_F(HelpWriterContextTest, FormatsLiteralTextAtBeginning)
     testFormatting(text);
 }
 
+TEST_F(HelpWriterContextTest, FormatsLiteralTextWithIndentation)
+{
+    const char *const text[] = {
+        "Sample paragraph::",
+        "",
+        "    literal block",
+        "      another indented line",
+        "",
+        "Normal paragraph",
+        "with wrapping"
+    };
+    testFormatting(text);
+}
+
 TEST_F(HelpWriterContextTest, FormatsBulletList)
 {
     const char *const text[] = {
@@ -243,12 +257,9 @@ TEST_F(HelpWriterContextTest, FormatsGridTable)
 
 TEST_F(HelpWriterContextTest, FormatsTitles)
 {
-    // Console formatting does not currently work without the paragraph breaks
-    // after the title.
     const char *const text[] = {
         "Title",
         "=====",
-        "",
         "Some text without spacing",
         "",
         "Subtitle",
diff --git a/src/gromacs/onlinehelp/tests/refdata/HelpWriterContextTest_FormatsLiteralTextWithIndentation.xml b/src/gromacs/onlinehelp/tests/refdata/HelpWriterContextTest_FormatsLiteralTextWithIndentation.xml
new file mode 100644 (file)
index 0000000..6fbc448
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/xsl" href="referencedata.xsl"?>
+<ReferenceData>
+  <String Name="Console"><![CDATA[
+Sample paragraph:
+
+    literal block
+      another indented line
+
+Normal paragraph with wrapping]]></String>
+  <String Name="reStructuredText"><![CDATA[
+Sample paragraph::
+
+    literal block
+      another indented line
+
+Normal paragraph
+with wrapping]]></String>
+</ReferenceData>
index b5eadeb566396da2921913542d02ece618540d58..3a03ef4cacb106d82e9109d9abbd2cff72b5eebb 100644 (file)
@@ -4,7 +4,6 @@
   <String Name="Console"><![CDATA[
 Title
 =====
-
 Some text without spacing
 
 Subtitle
@@ -14,7 +13,6 @@ More text]]></String>
   <String Name="reStructuredText"><![CDATA[
 Title
 =====
-
 Some text without spacing
 
 Subtitle
index dbcb951486f652c23680744fc9a30abef2ccd000..86abf82a085e9c370569609e6a6d188f994eb06f 100644 (file)
@@ -323,7 +323,11 @@ TextLineWrapper::formatLine(const std::string &input,
         --lineEnd;
     }
 
-    size_t      lineLength = lineEnd - lineStart;
+    const size_t lineLength = lineEnd - lineStart;
+    if (lineLength == 0)
+    {
+        return std::string();
+    }
     std::string result(indent, ' ');
     result.append(input, lineStart, lineLength);
     if (bContinuation && settings_.continuationChar_ != '\0')
diff --git a/src/gromacs/utility/tests/refdata/TextLineWrapperTest_HandlesIndentWithEmptyLines.xml b/src/gromacs/utility/tests/refdata/TextLineWrapperTest_HandlesIndentWithEmptyLines.xml
new file mode 100644 (file)
index 0000000..edb38af
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/xsl" href="referencedata.xsl"?>
+<ReferenceData>
+  <String Name="WrappedWithNoLimit"><![CDATA[
+  A quick brown fox jumps
+
+  over the lazy dog]]></String>
+  <String Name="WrappedAt14"><![CDATA[
+  A quick brown
+  fox jumps
+
+  over the lazy
+  dog]]></String>
+</ReferenceData>
index 5f9fb1e31a997da846fdaa56c6994bec502f671d..ba1683d06d236ae5a99f15c9506bb203c601a243 100644 (file)
@@ -238,6 +238,8 @@ TEST(ReplaceAllTest, HandlesPossibleRecursiveMatches)
 const char g_wrapText[] = "A quick brown fox jumps over the lazy dog";
 //! Test string for wrapping with embedded line breaks.
 const char g_wrapText2[] = "A quick brown fox jumps\nover the lazy dog";
+//! Test string for wrapping with embedded line breaks and an empty line.
+const char g_wrapText3[] = "A quick brown fox jumps\n\nover the lazy dog";
 //! Test string for wrapping with a long word.
 const char g_wrapTextLongWord[]
     = "A quick brown fox jumps awordthatoverflowsaline over the lazy dog";
@@ -331,6 +333,16 @@ TEST_F(TextLineWrapperTest, HandlesIndent)
     checkText(wrapper.wrapToString(g_wrapText2), "WrappedAt14");
 }
 
+TEST_F(TextLineWrapperTest, HandlesIndentWithEmptyLines)
+{
+    gmx::TextLineWrapper wrapper;
+    wrapper.settings().setIndent(2);
+
+    checkText(wrapper.wrapToString(g_wrapText3), "WrappedWithNoLimit");
+    wrapper.settings().setLineLength(16);
+    checkText(wrapper.wrapToString(g_wrapText3), "WrappedAt14");
+}
+
 TEST_F(TextLineWrapperTest, HandlesHangingIndent)
 {
     gmx::TextLineWrapper wrapper;