Apply re-formatting to C++ in src/ 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 by the GROMACS development team.
5  * Copyright (c) 2017,2018,2019,2020, by the GROMACS development team, led by
6  * Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
7  * and including many others, as listed in the AUTHORS file in the
8  * top-level source directory and at http://www.gromacs.org.
9  *
10  * GROMACS is free software; you can redistribute it and/or
11  * modify it under the terms of the GNU Lesser General Public License
12  * as published by the Free Software Foundation; either version 2.1
13  * of the License, or (at your option) any later version.
14  *
15  * GROMACS is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18  * Lesser General Public License for more details.
19  *
20  * You should have received a copy of the GNU Lesser General Public
21  * License along with GROMACS; if not, see
22  * http://www.gnu.org/licenses, or write to the Free Software Foundation,
23  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
24  *
25  * If you want to redistribute modifications to GROMACS, please
26  * consider that scientific software is very special. Version
27  * control is crucial - bugs must be traceable. We will be happy to
28  * consider code for inclusion in the official distribution, but
29  * derived work must not be called official GROMACS. Details are found
30  * in the README & COPYING files - if they are missing, get the
31  * official version at http://www.gromacs.org.
32  *
33  * To help us fund GROMACS development, we humbly ask that you cite
34  * the research papers on the package. Check out http://www.gromacs.org.
35  */
36 /*! \internal \file
37  * \brief
38  * Implements gmx::HelpWriterContext.
39  *
40  * \author Teemu Murtola <teemu.murtola@gmail.com>
41  * \ingroup module_onlinehelp
42  */
43 #include "gmxpre.h"
44
45 #include "helpwritercontext.h"
46
47 #include <cctype>
48
49 #include <algorithm>
50 #include <memory>
51 #include <string>
52 #include <vector>
53
54 #include "gromacs/onlinehelp/helpformat.h"
55 #include "gromacs/utility/exceptions.h"
56 #include "gromacs/utility/gmxassert.h"
57 #include "gromacs/utility/programcontext.h"
58 #include "gromacs/utility/stringutil.h"
59 #include "gromacs/utility/textwriter.h"
60
61 #include "rstparser.h"
62
63 namespace gmx
64 {
65
66 namespace
67 {
68
69 //! \internal \addtogroup module_onlinehelp
70 //! \{
71
72 //! Characters used for reStructuredText title underlining.
73 const char g_titleChars[] = "=-^*~+#'_.";
74
75 struct t_sandr
76 {
77     const char* search;
78     const char* replace;
79 };
80
81 /* The order of these arrays is significant. Text search and replace
82  * for each element occurs in order, so earlier changes can induce
83  * subsequent changes even though the original text might not appear
84  * to invoke the latter changes.
85  * TODO: Get rid of this behavior. It makes it very difficult to manage
86  * replacements coming from multiple sources (e.g., hyperlinks).*/
87
88 //! List of replacements for console output.
89 const t_sandr sandrTty[] = {
90     { "\\*", "*" },          { "\\=", "=" },         { "[REF]", "" },       { "[ref]", "" },
91     { "[TT]", "" },          { "[tt]", "" },         { "[BB]", "" },        { "[bb]", "" },
92     { "[IT]", "" },          { "[it]", "" },         { "[MATH]", "" },      { "[math]", "" },
93     { "[CHEVRON]", "<" },    { "[chevron]", ">" },   { "[MAG]", "|" },      { "[mag]", "|" },
94     { "[INT]", "integral" }, { "[FROM]", " from " }, { "[from]", "" },      { "[TO]", " to " },
95     { "[to]", " of" },       { "[int]", "" },        { "[SUM]", "sum" },    { "[sum]", "" },
96     { "[SUB]", "_" },        { "[sub]", "" },        { "[SQRT]", "sqrt(" }, { "[sqrt]", ")" },
97     { "[EXP]", "exp(" },     { "[exp]", ")" },       { "[LN]", "ln(" },     { "[ln]", ")" },
98     { "[LOG]", "log(" },     { "[log]", ")" },       { "[COS]", "cos(" },   { "[cos]", ")" },
99     { "[SIN]", "sin(" },     { "[sin]", ")" },       { "[TAN]", "tan(" },   { "[tan]", ")" },
100     { "[COSH]", "cosh(" },   { "[cosh]", ")" },      { "[SINH]", "sinh(" }, { "[sinh]", ")" },
101     { "[TANH]", "tanh(" },   { "[tanh]", ")" },      { "[PAR]", "\n\n" },   { "[GRK]", "" },
102     { "[grk]", "" }
103 };
104
105 //! List of replacements for reStructuredText output.
106 const t_sandr sandrRst[] = {
107     { "[TT]", "``" },        { "[tt]", "``" },       { "[BB]", "**" },      { "[bb]", "**" },
108     { "[IT]", "*" },         { "[it]", "*" },        { "[MATH]", "" },      { "[math]", "" },
109     { "[CHEVRON]", "<" },    { "[chevron]", ">" },   { "[MAG]", "\\|" },    { "[mag]", "\\|" },
110     { "[INT]", "integral" }, { "[FROM]", " from " }, { "[from]", "" },      { "[TO]", " to " },
111     { "[to]", " of" },       { "[int]", "" },        { "[SUM]", "sum" },    { "[sum]", "" },
112     { "[SUB]", "_" },        { "[sub]", "" },        { "[SQRT]", "sqrt(" }, { "[sqrt]", ")" },
113     { "[EXP]", "exp(" },     { "[exp]", ")" },       { "[LN]", "ln(" },     { "[ln]", ")" },
114     { "[LOG]", "log(" },     { "[log]", ")" },       { "[COS]", "cos(" },   { "[cos]", ")" },
115     { "[SIN]", "sin(" },     { "[sin]", ")" },       { "[TAN]", "tan(" },   { "[tan]", ")" },
116     { "[COSH]", "cosh(" },   { "[cosh]", ")" },      { "[SINH]", "sinh(" }, { "[sinh]", ")" },
117     { "[TANH]", "tanh(" },   { "[tanh]", ")" },      { "[PAR]", "\n\n" },   { "[GRK]", "" },
118     { "[grk]", "" }
119 };
120
121 /*! \brief
122  * Replaces all entries from a list of replacements.
123  */
124 std::string repall(const std::string& s, int nsr, const t_sandr sa[])
125 {
126     std::string result(s);
127     for (int i = 0; i < nsr; ++i)
128     {
129         result = replaceAll(result, sa[i].search, sa[i].replace);
130     }
131     return result;
132 }
133
134 /*! \brief
135  * Replaces all entries from a list of replacements.
136  */
137 template<size_t nsr>
138 std::string repall(const std::string& s, const t_sandr (&sa)[nsr])
139 {
140     return repall(s, nsr, sa);
141 }
142
143 /*! \brief
144  * Custom output interface for HelpWriterContext::Impl::processMarkup().
145  *
146  * Provides an interface that is used to implement different types of output
147  * from HelpWriterContext::Impl::processMarkup().
148  */
149 class IWrapper
150 {
151 public:
152     virtual ~IWrapper() {}
153
154     /*! \brief
155      * Provides the wrapping settings.
156      *
157      * HelpWriterContext::Impl::processMarkup() may provide some default
158      * values for the settings if they are not set; this is the reason the
159      * return value is not const.
160      */
161     virtual TextLineWrapperSettings& settings() = 0;
162     //! Appends the given string to output.
163     virtual void wrap(const std::string& text) = 0;
164 };
165
166 /*! \brief
167  * Wraps markup output into a single string.
168  */
169 class WrapperToString : public IWrapper
170 {
171 public:
172     //! Creates a wrapper with the given settings.
173     explicit WrapperToString(const TextLineWrapperSettings& settings) : wrapper_(settings) {}
174
175     TextLineWrapperSettings& settings() override { return wrapper_.settings(); }
176     void wrap(const std::string& text) override { result_.append(wrapper_.wrapToString(text)); }
177     //! Returns the result string.
178     const std::string& result() const { return result_; }
179
180 private:
181     TextLineWrapper wrapper_;
182     std::string     result_;
183 };
184
185 /*! \brief
186  * Wraps markup output into a vector of string (one line per element).
187  */
188 class WrapperToVector : public IWrapper
189 {
190 public:
191     //! Creates a wrapper with the given settings.
192     explicit WrapperToVector(const TextLineWrapperSettings& settings) : wrapper_(settings) {}
193
194     TextLineWrapperSettings& settings() override { return wrapper_.settings(); }
195     void                     wrap(const std::string& text) override
196     {
197         const std::vector<std::string>& lines = wrapper_.wrapToVector(text);
198         result_.insert(result_.end(), lines.begin(), lines.end());
199     }
200     //! Returns a vector with the output lines.
201     const std::vector<std::string>& result() const { return result_; }
202
203 private:
204     TextLineWrapper          wrapper_;
205     std::vector<std::string> result_;
206 };
207
208 /*! \brief
209  * Makes the string uppercase.
210  *
211  * \param[in] text  Input text.
212  * \returns   \p text with all characters transformed to uppercase.
213  * \throws    std::bad_alloc if out of memory.
214  */
215 std::string toUpperCase(const std::string& text)
216 {
217     std::string result(text);
218     std::transform(result.begin(), result.end(), result.begin(), toupper);
219     return result;
220 }
221
222 /*! \brief
223  * Removes extra newlines from reStructuredText.
224  *
225  * \param[in] text  Input text.
226  * \returns   \p text with all sequences of more than two newlines replaced
227  *     with just two newlines.
228  * \throws    std::bad_alloc if out of memory.
229  */
230 std::string removeExtraNewlinesRst(const std::string& text)
231 {
232     // Start from 2, so that all newlines in the beginning get stripped off.
233     int         newlineCount = 2;
234     std::string result;
235     result.reserve(text.length());
236     for (size_t i = 0; i < text.length(); ++i)
237     {
238         if (text[i] == '\n')
239         {
240             ++newlineCount;
241             if (newlineCount > 2)
242             {
243                 continue;
244             }
245         }
246         else
247         {
248             newlineCount = 0;
249         }
250         result.push_back(text[i]);
251     }
252     size_t last = result.find_last_not_of('\n');
253     if (last != std::string::npos)
254     {
255         result.resize(last + 1);
256     }
257     return result;
258 }
259
260 //! \}
261
262 } // namespace
263
264 /********************************************************************
265  * HelpLinks::Impl
266  */
267
268 /*! \internal \brief
269  * Private implementation class for HelpLinks.
270  *
271  * \ingroup module_onlinehelp
272  */
273 class HelpLinks::Impl
274 {
275 public:
276     struct LinkItem
277     {
278         LinkItem(const std::string& linkName, const std::string& replacement) :
279             linkName(linkName),
280             replacement(replacement)
281         {
282         }
283         std::string linkName;
284         std::string replacement;
285     };
286
287     //! Shorthand for a list of links.
288     typedef std::vector<LinkItem> LinkList;
289
290     //! Initializes empty links with the given format.
291     explicit Impl(HelpOutputFormat format) : format_(format) {}
292
293     //! List of links.
294     LinkList links_;
295     //! Output format for which the links are formatted.
296     HelpOutputFormat format_;
297 };
298
299 /********************************************************************
300  * HelpLinks
301  */
302
303 HelpLinks::HelpLinks(HelpOutputFormat format) : impl_(new Impl(format)) {}
304
305 HelpLinks::~HelpLinks() {}
306
307 void HelpLinks::addLink(const std::string& linkName, const std::string& targetName, const std::string& displayName)
308 {
309     std::string replacement;
310     switch (impl_->format_)
311     {
312         case eHelpOutputFormat_Console: replacement = repall(displayName, sandrTty); break;
313         case eHelpOutputFormat_Rst: replacement = targetName; break;
314         default: GMX_RELEASE_ASSERT(false, "Output format not implemented for links");
315     }
316     impl_->links_.emplace_back(linkName, replacement);
317 }
318
319 /********************************************************************
320  * HelpWriterContext::Impl
321  */
322
323 /*! \internal \brief
324  * Private implementation class for HelpWriterContext.
325  *
326  * \ingroup module_onlinehelp
327  */
328 class HelpWriterContext::Impl
329 {
330 public:
331     /*! \brief
332      * Shared, non-modifiable state for context objects.
333      *
334      * Contents of this structure are shared between all context objects
335      * that are created from a common parent.
336      * This state should not be modified after construction.
337      *
338      * \ingroup module_onlinehelp
339      */
340     class SharedState
341     {
342     public:
343         //! Initializes the state with the given parameters.
344         SharedState(TextWriter* writer, HelpOutputFormat format, const HelpLinks* links) :
345             file_(*writer),
346             format_(format),
347             links_(links)
348         {
349         }
350
351         /*! \brief
352          * Returns a formatter for formatting options lists for console
353          * output.
354          *
355          * The formatter is lazily initialized on first access.
356          */
357         TextTableFormatter& consoleOptionsFormatter() const
358         {
359             GMX_RELEASE_ASSERT(format_ == eHelpOutputFormat_Console,
360                                "Accessing console formatter for non-console output");
361             if (!consoleOptionsFormatter_)
362             {
363                 consoleOptionsFormatter_ = std::make_unique<TextTableFormatter>();
364                 consoleOptionsFormatter_->setFirstColumnIndent(1);
365                 consoleOptionsFormatter_->addColumn(nullptr, 7, false);
366                 consoleOptionsFormatter_->addColumn(nullptr, 18, false);
367                 consoleOptionsFormatter_->addColumn(nullptr, 16, false);
368                 consoleOptionsFormatter_->addColumn(nullptr, 28, false);
369             }
370             return *consoleOptionsFormatter_;
371         }
372
373         //! Writer for writing the help.
374         TextWriter& file_;
375         //! Output format for the help output.
376         HelpOutputFormat format_;
377         //! Links to use.
378         const HelpLinks* links_;
379
380     private:
381         //! Formatter for console output options.
382         // Never releases ownership.
383         mutable std::unique_ptr<TextTableFormatter> consoleOptionsFormatter_;
384
385         GMX_DISALLOW_COPY_AND_ASSIGN(SharedState);
386     };
387
388     struct ReplaceItem
389     {
390         ReplaceItem(const std::string& search, const std::string& replace) :
391             search(search),
392             replace(replace)
393         {
394         }
395         std::string search;
396         std::string replace;
397     };
398
399     //! Smart pointer type for managing the shared state.
400     typedef std::shared_ptr<const SharedState> StatePointer;
401     //! Shorthand for a list of markup/other replacements.
402     typedef std::vector<ReplaceItem> ReplaceList;
403
404     //! Initializes the context with the given state and section depth.
405     Impl(const StatePointer& state, int sectionDepth) : state_(state), sectionDepth_(sectionDepth)
406     {
407     }
408     //! Copies the context.
409     Impl(const Impl&) = default;
410
411     //! Adds a new replacement.
412     void addReplacement(const std::string& search, const std::string& replace)
413     {
414         replacements_.emplace_back(search, replace);
415     }
416
417     //! Replaces links in a given string.
418     std::string replaceLinks(const std::string& input) const;
419
420     /*! \brief
421      * Process markup and wrap lines within a block of text.
422      *
423      * \param[in] text     Text to process.
424      * \param     wrapper  Object used to wrap the text.
425      *
426      * The \p wrapper should take care of either writing the text to output
427      * or providing an interface for the caller to retrieve the output.
428      */
429     void processMarkup(const std::string& text, IWrapper* wrapper) const;
430
431     //! Constant state shared by all child context objects.
432     StatePointer state_;
433     //! List of markup/other replacements.
434     ReplaceList replacements_;
435     //! Number of subsections above this context.
436     int sectionDepth_;
437
438 private:
439     GMX_DISALLOW_ASSIGN(Impl);
440 };
441
442 std::string HelpWriterContext::Impl::replaceLinks(const std::string& input) const
443 {
444     std::string result(input);
445     if (state_->links_ != nullptr)
446     {
447         HelpLinks::Impl::LinkList::const_iterator link;
448         for (link = state_->links_->impl_->links_.begin(); 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