Apply clang-format to source tree
[alexxy/gromacs.git] / src / gromacs / onlinehelp / helpwritercontext.cpp
1 /*
2  * This file is part of the GROMACS molecular simulation package.
3  *
4  * Copyright (c) 2012,2013,2014,2015,2016,2017,2018,2019, by the GROMACS development team, led by
5  * Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
6  * and including many others, as listed in the AUTHORS file in the
7  * top-level source directory and at http://www.gromacs.org.
8  *
9  * GROMACS is free software; you can redistribute it and/or
10  * modify it under the terms of the GNU Lesser General Public License
11  * as published by the Free Software Foundation; either version 2.1
12  * of the License, or (at your option) any later version.
13  *
14  * GROMACS is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17  * Lesser General Public License for more details.
18  *
19  * You should have received a copy of the GNU Lesser General Public
20  * License along with GROMACS; if not, see
21  * http://www.gnu.org/licenses, or write to the Free Software Foundation,
22  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
23  *
24  * If you want to redistribute modifications to GROMACS, please
25  * consider that scientific software is very special. Version
26  * control is crucial - bugs must be traceable. We will be happy to
27  * consider code for inclusion in the official distribution, but
28  * derived work must not be called official GROMACS. Details are found
29  * in the README & COPYING files - if they are missing, get the
30  * official version at http://www.gromacs.org.
31  *
32  * To help us fund GROMACS development, we humbly ask that you cite
33  * the research papers on the package. Check out http://www.gromacs.org.
34  */
35 /*! \internal \file
36  * \brief
37  * Implements gmx::HelpWriterContext.
38  *
39  * \author Teemu Murtola <teemu.murtola@gmail.com>
40  * \ingroup module_onlinehelp
41  */
42 #include "gmxpre.h"
43
44 #include "helpwritercontext.h"
45
46 #include <cctype>
47
48 #include <algorithm>
49 #include <memory>
50 #include <string>
51 #include <vector>
52
53 #include "gromacs/onlinehelp/helpformat.h"
54 #include "gromacs/utility/exceptions.h"
55 #include "gromacs/utility/gmxassert.h"
56 #include "gromacs/utility/programcontext.h"
57 #include "gromacs/utility/stringutil.h"
58 #include "gromacs/utility/textwriter.h"
59
60 #include "rstparser.h"
61
62 namespace gmx
63 {
64
65 namespace
66 {
67
68 //! \internal \addtogroup module_onlinehelp
69 //! \{
70
71 //! Characters used for reStructuredText title underlining.
72 const char g_titleChars[] = "=-^*~+#'_.";
73
74 struct t_sandr
75 {
76     const char* search;
77     const char* replace;
78 };
79
80 /* The order of these arrays is significant. Text search and replace
81  * for each element occurs in order, so earlier changes can induce
82  * subsequent changes even though the original text might not appear
83  * to invoke the latter changes.
84  * TODO: Get rid of this behavior. It makes it very difficult to manage
85  * replacements coming from multiple sources (e.g., hyperlinks).*/
86
87 //! List of replacements for console output.
88 const t_sandr sandrTty[] = {
89     { "\\*", "*" },          { "\\=", "=" },         { "[REF]", "" },       { "[ref]", "" },
90     { "[TT]", "" },          { "[tt]", "" },         { "[BB]", "" },        { "[bb]", "" },
91     { "[IT]", "" },          { "[it]", "" },         { "[MATH]", "" },      { "[math]", "" },
92     { "[CHEVRON]", "<" },    { "[chevron]", ">" },   { "[MAG]", "|" },      { "[mag]", "|" },
93     { "[INT]", "integral" }, { "[FROM]", " from " }, { "[from]", "" },      { "[TO]", " to " },
94     { "[to]", " of" },       { "[int]", "" },        { "[SUM]", "sum" },    { "[sum]", "" },
95     { "[SUB]", "_" },        { "[sub]", "" },        { "[SQRT]", "sqrt(" }, { "[sqrt]", ")" },
96     { "[EXP]", "exp(" },     { "[exp]", ")" },       { "[LN]", "ln(" },     { "[ln]", ")" },
97     { "[LOG]", "log(" },     { "[log]", ")" },       { "[COS]", "cos(" },   { "[cos]", ")" },
98     { "[SIN]", "sin(" },     { "[sin]", ")" },       { "[TAN]", "tan(" },   { "[tan]", ")" },
99     { "[COSH]", "cosh(" },   { "[cosh]", ")" },      { "[SINH]", "sinh(" }, { "[sinh]", ")" },
100     { "[TANH]", "tanh(" },   { "[tanh]", ")" },      { "[PAR]", "\n\n" },   { "[GRK]", "" },
101     { "[grk]", "" }
102 };
103
104 //! List of replacements for reStructuredText output.
105 const t_sandr sandrRst[] = {
106     { "[TT]", "``" },        { "[tt]", "``" },       { "[BB]", "**" },      { "[bb]", "**" },
107     { "[IT]", "*" },         { "[it]", "*" },        { "[MATH]", "" },      { "[math]", "" },
108     { "[CHEVRON]", "<" },    { "[chevron]", ">" },   { "[MAG]", "\\|" },    { "[mag]", "\\|" },
109     { "[INT]", "integral" }, { "[FROM]", " from " }, { "[from]", "" },      { "[TO]", " to " },
110     { "[to]", " of" },       { "[int]", "" },        { "[SUM]", "sum" },    { "[sum]", "" },
111     { "[SUB]", "_" },        { "[sub]", "" },        { "[SQRT]", "sqrt(" }, { "[sqrt]", ")" },
112     { "[EXP]", "exp(" },     { "[exp]", ")" },       { "[LN]", "ln(" },     { "[ln]", ")" },
113     { "[LOG]", "log(" },     { "[log]", ")" },       { "[COS]", "cos(" },   { "[cos]", ")" },
114     { "[SIN]", "sin(" },     { "[sin]", ")" },       { "[TAN]", "tan(" },   { "[tan]", ")" },
115     { "[COSH]", "cosh(" },   { "[cosh]", ")" },      { "[SINH]", "sinh(" }, { "[sinh]", ")" },
116     { "[TANH]", "tanh(" },   { "[tanh]", ")" },      { "[PAR]", "\n\n" },   { "[GRK]", "" },
117     { "[grk]", "" }
118 };
119
120 /*! \brief
121  * Replaces all entries from a list of replacements.
122  */
123 std::string repall(const std::string& s, int nsr, const t_sandr sa[])
124 {
125     std::string result(s);
126     for (int i = 0; i < nsr; ++i)
127     {
128         result = replaceAll(result, sa[i].search, sa[i].replace);
129     }
130     return result;
131 }
132
133 /*! \brief
134  * Replaces all entries from a list of replacements.
135  */
136 template<size_t nsr>
137 std::string repall(const std::string& s, const t_sandr (&sa)[nsr])
138 {
139     return repall(s, nsr, sa);
140 }
141
142 /*! \brief
143  * Custom output interface for HelpWriterContext::Impl::processMarkup().
144  *
145  * Provides an interface that is used to implement different types of output
146  * from HelpWriterContext::Impl::processMarkup().
147  */
148 class IWrapper
149 {
150 public:
151     virtual ~IWrapper() {}
152
153     /*! \brief
154      * Provides the wrapping settings.
155      *
156      * HelpWriterContext::Impl::processMarkup() may provide some default
157      * values for the settings if they are not set; this is the reason the
158      * return value is not const.
159      */
160     virtual TextLineWrapperSettings& settings() = 0;
161     //! Appends the given string to output.
162     virtual void wrap(const std::string& text) = 0;
163 };
164
165 /*! \brief
166  * Wraps markup output into a single string.
167  */
168 class WrapperToString : public IWrapper
169 {
170 public:
171     //! Creates a wrapper with the given settings.
172     explicit WrapperToString(const TextLineWrapperSettings& settings) : wrapper_(settings) {}
173
174     TextLineWrapperSettings& settings() override { return wrapper_.settings(); }
175     void wrap(const std::string& text) override { result_.append(wrapper_.wrapToString(text)); }
176     //! Returns the result string.
177     const std::string& result() const { return result_; }
178
179 private:
180     TextLineWrapper wrapper_;
181     std::string     result_;
182 };
183
184 /*! \brief
185  * Wraps markup output into a vector of string (one line per element).
186  */
187 class WrapperToVector : public IWrapper
188 {
189 public:
190     //! Creates a wrapper with the given settings.
191     explicit WrapperToVector(const TextLineWrapperSettings& settings) : wrapper_(settings) {}
192
193     TextLineWrapperSettings& settings() override { return wrapper_.settings(); }
194     void                     wrap(const std::string& text) override
195     {
196         const std::vector<std::string>& lines = wrapper_.wrapToVector(text);
197         result_.insert(result_.end(), lines.begin(), lines.end());
198     }
199     //! Returns a vector with the output lines.
200     const std::vector<std::string>& result() const { return result_; }
201
202 private:
203     TextLineWrapper          wrapper_;
204     std::vector<std::string> result_;
205 };
206
207 /*! \brief
208  * Makes the string uppercase.
209  *
210  * \param[in] text  Input text.
211  * \returns   \p text with all characters transformed to uppercase.
212  * \throws    std::bad_alloc if out of memory.
213  */
214 std::string toUpperCase(const std::string& text)
215 {
216     std::string result(text);
217     std::transform(result.begin(), result.end(), result.begin(), toupper);
218     return result;
219 }
220
221 /*! \brief
222  * Removes extra newlines from reStructuredText.
223  *
224  * \param[in] text  Input text.
225  * \returns   \p text with all sequences of more than two newlines replaced
226  *     with just two newlines.
227  * \throws    std::bad_alloc if out of memory.
228  */
229 std::string removeExtraNewlinesRst(const std::string& text)
230 {
231     // Start from 2, so that all newlines in the beginning get stripped off.
232     int         newlineCount = 2;
233     std::string result;
234     result.reserve(text.length());
235     for (size_t i = 0; i < text.length(); ++i)
236     {
237         if (text[i] == '\n')
238         {
239             ++newlineCount;
240             if (newlineCount > 2)
241             {
242                 continue;
243             }
244         }
245         else
246         {
247             newlineCount = 0;
248         }
249         result.push_back(text[i]);
250     }
251     size_t last = result.find_last_not_of('\n');
252     if (last != std::string::npos)
253     {
254         result.resize(last + 1);
255     }
256     return result;
257 }
258
259 //! \}
260
261 } // namespace
262
263 /********************************************************************
264  * HelpLinks::Impl
265  */
266
267 /*! \internal \brief
268  * Private implementation class for HelpLinks.
269  *
270  * \ingroup module_onlinehelp
271  */
272 class HelpLinks::Impl
273 {
274 public:
275     struct LinkItem
276     {
277         LinkItem(const std::string& linkName, const std::string& replacement) :
278             linkName(linkName),
279             replacement(replacement)
280         {
281         }
282         std::string linkName;
283         std::string replacement;
284     };
285
286     //! Shorthand for a list of links.
287     typedef std::vector<LinkItem> LinkList;
288
289     //! Initializes empty links with the given format.
290     explicit Impl(HelpOutputFormat format) : format_(format) {}
291
292     //! List of links.
293     LinkList links_;
294     //! Output format for which the links are formatted.
295     HelpOutputFormat format_;
296 };
297
298 /********************************************************************
299  * HelpLinks
300  */
301
302 HelpLinks::HelpLinks(HelpOutputFormat format) : impl_(new Impl(format)) {}
303
304 HelpLinks::~HelpLinks() {}
305
306 void HelpLinks::addLink(const std::string& linkName, const std::string& targetName, const std::string& displayName)
307 {
308     std::string replacement;
309     switch (impl_->format_)
310     {
311         case eHelpOutputFormat_Console: replacement = repall(displayName, sandrTty); break;
312         case eHelpOutputFormat_Rst: replacement = targetName; break;
313         default: GMX_RELEASE_ASSERT(false, "Output format not implemented for links");
314     }
315     impl_->links_.emplace_back(linkName, replacement);
316 }
317
318 /********************************************************************
319  * HelpWriterContext::Impl
320  */
321
322 /*! \internal \brief
323  * Private implementation class for HelpWriterContext.
324  *
325  * \ingroup module_onlinehelp
326  */
327 class HelpWriterContext::Impl
328 {
329 public:
330     /*! \brief
331      * Shared, non-modifiable state for context objects.
332      *
333      * Contents of this structure are shared between all context objects
334      * that are created from a common parent.
335      * This state should not be modified after construction.
336      *
337      * \ingroup module_onlinehelp
338      */
339     class SharedState
340     {
341     public:
342         //! Initializes the state with the given parameters.
343         SharedState(TextWriter* writer, HelpOutputFormat format, const HelpLinks* links) :
344             file_(*writer),
345             format_(format),
346             links_(links)
347         {
348         }
349
350         /*! \brief
351          * Returns a formatter for formatting options lists for console
352          * output.
353          *
354          * The formatter is lazily initialized on first access.
355          */
356         TextTableFormatter& consoleOptionsFormatter() const
357         {
358             GMX_RELEASE_ASSERT(format_ == eHelpOutputFormat_Console,
359                                "Accessing console formatter for non-console output");
360             if (!consoleOptionsFormatter_)
361             {
362                 consoleOptionsFormatter_ = std::make_unique<TextTableFormatter>();
363                 consoleOptionsFormatter_->setFirstColumnIndent(1);
364                 consoleOptionsFormatter_->addColumn(nullptr, 7, false);
365                 consoleOptionsFormatter_->addColumn(nullptr, 18, false);
366                 consoleOptionsFormatter_->addColumn(nullptr, 16, false);
367                 consoleOptionsFormatter_->addColumn(nullptr, 28, false);
368             }
369             return *consoleOptionsFormatter_;
370         }
371
372         //! Writer for writing the help.
373         TextWriter& file_;
374         //! Output format for the help output.
375         HelpOutputFormat format_;
376         //! Links to use.
377         const HelpLinks* links_;
378
379     private:
380         //! Formatter for console output options.
381         // Never releases ownership.
382         mutable std::unique_ptr<TextTableFormatter> consoleOptionsFormatter_;
383
384         GMX_DISALLOW_COPY_AND_ASSIGN(SharedState);
385     };
386
387     struct ReplaceItem
388     {
389         ReplaceItem(const std::string& search, const std::string& replace) :
390             search(search),
391             replace(replace)
392         {
393         }
394         std::string search;
395         std::string replace;
396     };
397
398     //! Smart pointer type for managing the shared state.
399     typedef std::shared_ptr<const SharedState> StatePointer;
400     //! Shorthand for a list of markup/other replacements.
401     typedef std::vector<ReplaceItem> ReplaceList;
402
403     //! Initializes the context with the given state and section depth.
404     Impl(const StatePointer& state, int sectionDepth) : state_(state), sectionDepth_(sectionDepth)
405     {
406     }
407     //! Copies the context.
408     Impl(const Impl&) = default;
409
410     //! Adds a new replacement.
411     void addReplacement(const std::string& search, const std::string& replace)
412     {
413         replacements_.emplace_back(search, replace);
414     }
415
416     //! Replaces links in a given string.
417     std::string replaceLinks(const std::string& input) const;
418
419     /*! \brief
420      * Process markup and wrap lines within a block of text.
421      *
422      * \param[in] text     Text to process.
423      * \param     wrapper  Object used to wrap the text.
424      *
425      * The \p wrapper should take care of either writing the text to output
426      * or providing an interface for the caller to retrieve the output.
427      */
428     void processMarkup(const std::string& text, IWrapper* wrapper) const;
429
430     //! Constant state shared by all child context objects.
431     StatePointer state_;
432     //! List of markup/other replacements.
433     ReplaceList replacements_;
434     //! Number of subsections above this context.
435     int sectionDepth_;
436
437 private:
438     GMX_DISALLOW_ASSIGN(Impl);
439 };
440
441 std::string HelpWriterContext::Impl::replaceLinks(const std::string& input) const
442 {
443     std::string result(input);
444     if (state_->links_ != nullptr)
445     {
446         HelpLinks::Impl::LinkList::const_iterator link;
447         for (link = state_->links_->impl_->links_.begin();
448              link != state_->links_->impl_->links_.end(); ++link)
449         {
450             result = replaceAllWords(result, link->linkName, link->replacement);
451         }
452     }
453     return result;
454 }
455
456 void HelpWriterContext::Impl::processMarkup(const std::string& text, IWrapper* wrapper) const
457 {
458     std::string result(text);
459     for (ReplaceList::const_iterator i = replacements_.begin(); i != replacements_.end(); ++i)
460     {
461         result = replaceAll(result, i->search, i->replace);
462     }
463     switch (state_->format_)
464     {
465         case eHelpOutputFormat_Console:
466         {
467             const int baseFirstLineIndent = wrapper->settings().firstLineIndent();
468             const int baseIndent          = wrapper->settings().indent();
469             result                        = repall(result, sandrTty);
470             result                        = replaceLinks(result);
471             std::string paragraph;
472             paragraph.reserve(result.length());
473             RstParagraphIterator iter(result);
474             while (iter.nextParagraph())
475             {
476                 iter.getParagraphText(&paragraph);
477                 wrapper->settings().setFirstLineIndent(baseFirstLineIndent + iter.firstLineIndent());
478                 wrapper->settings().setIndent(baseIndent + iter.indent());
479                 wrapper->wrap(paragraph);
480             }
481             wrapper->settings().setFirstLineIndent(baseFirstLineIndent);
482             wrapper->settings().setIndent(baseIndent);
483             break;
484         }
485         case eHelpOutputFormat_Rst:
486         {
487             result = repall(result, sandrRst);
488             result = replaceLinks(result);
489             result = replaceAll(result, "[REF]", "");
490             result = replaceAll(result, "[ref]", "");
491             result = removeExtraNewlinesRst(result);
492             wrapper->wrap(result);
493             break;
494         }
495         default: GMX_THROW(InternalError("Invalid help output format"));
496     }
497 }
498
499 /********************************************************************
500  * HelpWriterContext
501  */
502
503 HelpWriterContext::HelpWriterContext(TextWriter* writer, HelpOutputFormat format) :
504     impl_(new Impl(Impl::StatePointer(new Impl::SharedState(writer, format, nullptr)), 0))
505 {
506 }
507
508 HelpWriterContext::HelpWriterContext(TextWriter* writer, HelpOutputFormat format, const HelpLinks* links) :
509     impl_(new Impl(Impl::StatePointer(new Impl::SharedState(writer, format, links)), 0))
510 {
511     if (links != nullptr)
512     {
513         GMX_RELEASE_ASSERT(links->impl_->format_ == format,
514                            "Links must have the same output format as the context");
515     }
516 }
517
518 HelpWriterContext::HelpWriterContext(Impl* impl) : impl_(impl) {}
519
520 HelpWriterContext::HelpWriterContext(const HelpWriterContext& other) : impl_(new Impl(*other.impl_))
521 {
522 }
523
524 HelpWriterContext::~HelpWriterContext() {}
525
526 void HelpWriterContext::setReplacement(const std::string& search, const std::string& replace)
527 {
528     impl_->addReplacement(search, replace);
529 }
530
531 HelpOutputFormat HelpWriterContext::outputFormat() const
532 {
533     return impl_->state_->format_;
534 }
535
536 TextWriter& HelpWriterContext::outputFile() const
537 {
538     return impl_->state_->file_;
539 }
540
541 void HelpWriterContext::enterSubSection(const std::string& title)
542 {
543     GMX_RELEASE_ASSERT(impl_->sectionDepth_ - 1 < static_cast<int>(std::strlen(g_titleChars)),
544                        "Too deeply nested subsections");
545     writeTitle(title);
546     ++impl_->sectionDepth_;
547 }
548
549 std::string HelpWriterContext::substituteMarkupAndWrapToString(const TextLineWrapperSettings& settings,
550                                                                const std::string& text) const
551 {
552     WrapperToString wrapper(settings);
553     impl_->processMarkup(text, &wrapper);
554     return wrapper.result();
555 }
556
557 std::vector<std::string> HelpWriterContext::substituteMarkupAndWrapToVector(const TextLineWrapperSettings& settings,
558                                                                             const std::string& text) const
559 {
560     WrapperToVector wrapper(settings);
561     impl_->processMarkup(text, &wrapper);
562     return wrapper.result();
563 }
564
565 void HelpWriterContext::writeTitle(const std::string& title) const
566 {
567     if (title.empty())
568     {
569         return;
570     }
571     TextWriter& file = outputFile();
572     file.ensureEmptyLine();
573     switch (outputFormat())
574     {
575         case eHelpOutputFormat_Console: file.writeLine(toUpperCase(title)); break;
576         case eHelpOutputFormat_Rst:
577             file.writeLine(title);
578             file.writeLine(std::string(title.length(), g_titleChars[impl_->sectionDepth_]));
579             break;
580         default: GMX_THROW(NotImplementedError("This output format is not implemented"));
581     }
582     file.ensureEmptyLine();
583 }
584
585 void HelpWriterContext::writeTextBlock(const std::string& text) const
586 {
587     TextLineWrapperSettings settings;
588     if (outputFormat() == eHelpOutputFormat_Console)
589     {
590         settings.setLineLength(78);
591     }
592     outputFile().writeLine(substituteMarkupAndWrapToString(settings, text));
593 }
594
595 void HelpWriterContext::paragraphBreak() const
596 {
597     outputFile().ensureEmptyLine();
598 }
599
600 void HelpWriterContext::writeOptionListStart() const {}
601
602 void HelpWriterContext::writeOptionItem(const std::string& name,
603                                         const std::string& value,
604                                         const std::string& defaultValue,
605                                         const std::string& info,
606                                         const std::string& description) const
607 {
608     TextWriter& file = outputFile();
609     switch (outputFormat())
610     {
611         case eHelpOutputFormat_Console:
612         {
613             TextTableFormatter& formatter(impl_->state_->consoleOptionsFormatter());
614             formatter.clear();
615             formatter.addColumnLine(0, name);
616             formatter.addColumnLine(1, value);
617             if (!defaultValue.empty())
618             {
619                 formatter.addColumnLine(2, "(" + defaultValue + ")");
620             }
621             if (!info.empty())
622             {
623                 formatter.addColumnLine(3, "(" + info + ")");
624             }
625             TextLineWrapperSettings settings;
626             settings.setIndent(11);
627             settings.setLineLength(78);
628             std::string formattedDescription = substituteMarkupAndWrapToString(settings, description);
629             file.writeLine(formatter.formatRow());
630             file.writeLine(formattedDescription);
631             break;
632         }
633         case eHelpOutputFormat_Rst:
634         {
635             std::string args(value);
636             if (!defaultValue.empty())
637             {
638                 args.append(" (");
639                 args.append(defaultValue);
640                 args.append(")");
641             }
642             if (!info.empty())
643             {
644                 args.append(" (");
645                 args.append(info);
646                 args.append(")");
647             }
648             file.writeLine(formatString("``%s`` %s", name.c_str(), args.c_str()));
649             TextLineWrapperSettings settings;
650             settings.setIndent(4);
651             file.writeLine(substituteMarkupAndWrapToString(settings, description));
652             break;
653         }
654         default: GMX_THROW(NotImplementedError("This output format is not implemented"));
655     }
656 }
657
658 void HelpWriterContext::writeOptionListEnd() const {}
659
660 } // namespace gmx