Apply clang-format-11
[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,2021, 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/classhelpers.h"
56 #include "gromacs/utility/exceptions.h"
57 #include "gromacs/utility/gmxassert.h"
58 #include "gromacs/utility/programcontext.h"
59 #include "gromacs/utility/stringutil.h"
60 #include "gromacs/utility/textwriter.h"
61
62 #include "rstparser.h"
63
64 namespace gmx
65 {
66
67 namespace
68 {
69
70 //! \internal \addtogroup module_onlinehelp
71 //! \{
72
73 //! Characters used for reStructuredText title underlining.
74 const char g_titleChars[] = "=-^*~+#'_.";
75
76 struct t_sandr
77 {
78     const char* search;
79     const char* replace;
80 };
81
82 /* The order of these arrays is significant. Text search and replace
83  * for each element occurs in order, so earlier changes can induce
84  * subsequent changes even though the original text might not appear
85  * to invoke the latter changes.
86  * TODO: Get rid of this behavior. It makes it very difficult to manage
87  * replacements coming from multiple sources (e.g., hyperlinks).*/
88
89 //! List of replacements for console output.
90 const t_sandr sandrTty[] = {
91     { "\\*", "*" },          { "\\=", "=" },         { "[REF]", "" },       { "[ref]", "" },
92     { "[TT]", "" },          { "[tt]", "" },         { "[BB]", "" },        { "[bb]", "" },
93     { "[IT]", "" },          { "[it]", "" },         { "[MATH]", "" },      { "[math]", "" },
94     { "[CHEVRON]", "<" },    { "[chevron]", ">" },   { "[MAG]", "|" },      { "[mag]", "|" },
95     { "[INT]", "integral" }, { "[FROM]", " from " }, { "[from]", "" },      { "[TO]", " to " },
96     { "[to]", " of" },       { "[int]", "" },        { "[SUM]", "sum" },    { "[sum]", "" },
97     { "[SUB]", "_" },        { "[sub]", "" },        { "[SQRT]", "sqrt(" }, { "[sqrt]", ")" },
98     { "[EXP]", "exp(" },     { "[exp]", ")" },       { "[LN]", "ln(" },     { "[ln]", ")" },
99     { "[LOG]", "log(" },     { "[log]", ")" },       { "[COS]", "cos(" },   { "[cos]", ")" },
100     { "[SIN]", "sin(" },     { "[sin]", ")" },       { "[TAN]", "tan(" },   { "[tan]", ")" },
101     { "[COSH]", "cosh(" },   { "[cosh]", ")" },      { "[SINH]", "sinh(" }, { "[sinh]", ")" },
102     { "[TANH]", "tanh(" },   { "[tanh]", ")" },      { "[PAR]", "\n\n" },   { "[GRK]", "" },
103     { "[grk]", "" }
104 };
105
106 //! List of replacements for reStructuredText output.
107 const t_sandr sandrRst[] = {
108     { "[TT]", "``" },        { "[tt]", "``" },       { "[BB]", "**" },      { "[bb]", "**" },
109     { "[IT]", "*" },         { "[it]", "*" },        { "[MATH]", "" },      { "[math]", "" },
110     { "[CHEVRON]", "<" },    { "[chevron]", ">" },   { "[MAG]", "\\|" },    { "[mag]", "\\|" },
111     { "[INT]", "integral" }, { "[FROM]", " from " }, { "[from]", "" },      { "[TO]", " to " },
112     { "[to]", " of" },       { "[int]", "" },        { "[SUM]", "sum" },    { "[sum]", "" },
113     { "[SUB]", "_" },        { "[sub]", "" },        { "[SQRT]", "sqrt(" }, { "[sqrt]", ")" },
114     { "[EXP]", "exp(" },     { "[exp]", ")" },       { "[LN]", "ln(" },     { "[ln]", ")" },
115     { "[LOG]", "log(" },     { "[log]", ")" },       { "[COS]", "cos(" },   { "[cos]", ")" },
116     { "[SIN]", "sin(" },     { "[sin]", ")" },       { "[TAN]", "tan(" },   { "[tan]", ")" },
117     { "[COSH]", "cosh(" },   { "[cosh]", ")" },      { "[SINH]", "sinh(" }, { "[sinh]", ")" },
118     { "[TANH]", "tanh(" },   { "[tanh]", ")" },      { "[PAR]", "\n\n" },   { "[GRK]", "" },
119     { "[grk]", "" }
120 };
121
122 /*! \brief
123  * Replaces all entries from a list of replacements.
124  */
125 std::string repall(const std::string& s, int nsr, const t_sandr sa[])
126 {
127     std::string result(s);
128     for (int i = 0; i < nsr; ++i)
129     {
130         result = replaceAll(result, sa[i].search, sa[i].replace);
131     }
132     return result;
133 }
134
135 /*! \brief
136  * Replaces all entries from a list of replacements.
137  */
138 template<size_t nsr>
139 std::string repall(const std::string& s, const t_sandr (&sa)[nsr])
140 {
141     return repall(s, nsr, sa);
142 }
143
144 /*! \brief
145  * Custom output interface for HelpWriterContext::Impl::processMarkup().
146  *
147  * Provides an interface that is used to implement different types of output
148  * from HelpWriterContext::Impl::processMarkup().
149  */
150 class IWrapper
151 {
152 public:
153     virtual ~IWrapper() {}
154
155     /*! \brief
156      * Provides the wrapping settings.
157      *
158      * HelpWriterContext::Impl::processMarkup() may provide some default
159      * values for the settings if they are not set; this is the reason the
160      * return value is not const.
161      */
162     virtual TextLineWrapperSettings& settings() = 0;
163     //! Appends the given string to output.
164     virtual void wrap(const std::string& text) = 0;
165 };
166
167 /*! \brief
168  * Wraps markup output into a single string.
169  */
170 class WrapperToString : public IWrapper
171 {
172 public:
173     //! Creates a wrapper with the given settings.
174     explicit WrapperToString(const TextLineWrapperSettings& settings) : wrapper_(settings) {}
175
176     TextLineWrapperSettings& settings() override { return wrapper_.settings(); }
177     void wrap(const std::string& text) override { result_.append(wrapper_.wrapToString(text)); }
178     //! Returns the result string.
179     const std::string& result() const { return result_; }
180
181 private:
182     TextLineWrapper wrapper_;
183     std::string     result_;
184 };
185
186 /*! \brief
187  * Wraps markup output into a vector of string (one line per element).
188  */
189 class WrapperToVector : public IWrapper
190 {
191 public:
192     //! Creates a wrapper with the given settings.
193     explicit WrapperToVector(const TextLineWrapperSettings& settings) : wrapper_(settings) {}
194
195     TextLineWrapperSettings& settings() override { return wrapper_.settings(); }
196     void                     wrap(const std::string& text) override
197     {
198         const std::vector<std::string>& lines = wrapper_.wrapToVector(text);
199         result_.insert(result_.end(), lines.begin(), lines.end());
200     }
201     //! Returns a vector with the output lines.
202     const std::vector<std::string>& result() const { return result_; }
203
204 private:
205     TextLineWrapper          wrapper_;
206     std::vector<std::string> result_;
207 };
208
209 /*! \brief
210  * Makes the string uppercase.
211  *
212  * \param[in] text  Input text.
213  * \returns   \p text with all characters transformed to uppercase.
214  * \throws    std::bad_alloc if out of memory.
215  */
216 std::string toUpperCase(const std::string& text)
217 {
218     std::string result(text);
219     std::transform(result.begin(), result.end(), result.begin(), toupper);
220     return result;
221 }
222
223 /*! \brief
224  * Removes extra newlines from reStructuredText.
225  *
226  * \param[in] text  Input text.
227  * \returns   \p text with all sequences of more than two newlines replaced
228  *     with just two newlines.
229  * \throws    std::bad_alloc if out of memory.
230  */
231 std::string removeExtraNewlinesRst(const std::string& text)
232 {
233     // Start from 2, so that all newlines in the beginning get stripped off.
234     int         newlineCount = 2;
235     std::string result;
236     result.reserve(text.length());
237     for (size_t i = 0; i < text.length(); ++i)
238     {
239         if (text[i] == '\n')
240         {
241             ++newlineCount;
242             if (newlineCount > 2)
243             {
244                 continue;
245             }
246         }
247         else
248         {
249             newlineCount = 0;
250         }
251         result.push_back(text[i]);
252     }
253     size_t last = result.find_last_not_of('\n');
254     if (last != std::string::npos)
255     {
256         result.resize(last + 1);
257     }
258     return result;
259 }
260
261 //! \}
262
263 } // namespace
264
265 /********************************************************************
266  * HelpLinks::Impl
267  */
268
269 /*! \internal \brief
270  * Private implementation class for HelpLinks.
271  *
272  * \ingroup module_onlinehelp
273  */
274 class HelpLinks::Impl
275 {
276 public:
277     struct LinkItem
278     {
279         LinkItem(const std::string& linkName, const std::string& replacement) :
280             linkName(linkName), 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), format_(format), links_(links)
346         {
347         }
348
349         /*! \brief
350          * Returns a formatter for formatting options lists for console
351          * output.
352          *
353          * The formatter is lazily initialized on first access.
354          */
355         TextTableFormatter& consoleOptionsFormatter() const
356         {
357             GMX_RELEASE_ASSERT(format_ == eHelpOutputFormat_Console,
358                                "Accessing console formatter for non-console output");
359             if (!consoleOptionsFormatter_)
360             {
361                 consoleOptionsFormatter_ = std::make_unique<TextTableFormatter>();
362                 consoleOptionsFormatter_->setFirstColumnIndent(1);
363                 consoleOptionsFormatter_->addColumn(nullptr, 7, false);
364                 consoleOptionsFormatter_->addColumn(nullptr, 18, false);
365                 consoleOptionsFormatter_->addColumn(nullptr, 16, false);
366                 consoleOptionsFormatter_->addColumn(nullptr, 28, false);
367             }
368             return *consoleOptionsFormatter_;
369         }
370
371         //! Writer for writing the help.
372         TextWriter& file_;
373         //! Output format for the help output.
374         HelpOutputFormat format_;
375         //! Links to use.
376         const HelpLinks* links_;
377
378     private:
379         //! Formatter for console output options.
380         // Never releases ownership.
381         mutable std::unique_ptr<TextTableFormatter> consoleOptionsFormatter_;
382
383         GMX_DISALLOW_COPY_AND_ASSIGN(SharedState);
384     };
385
386     struct ReplaceItem
387     {
388         ReplaceItem(const std::string& search, const std::string& replace) :
389             search(search), replace(replace)
390         {
391         }
392         std::string search;
393         std::string replace;
394     };
395
396     //! Smart pointer type for managing the shared state.
397     typedef std::shared_ptr<const SharedState> StatePointer;
398     //! Shorthand for a list of markup/other replacements.
399     typedef std::vector<ReplaceItem> ReplaceList;
400
401     //! Initializes the context with the given state and section depth.
402     Impl(const StatePointer& state, int sectionDepth) : state_(state), sectionDepth_(sectionDepth)
403     {
404     }
405     //! Copies the context.
406     Impl(const Impl&) = default;
407
408     //! Adds a new replacement.
409     void addReplacement(const std::string& search, const std::string& replace)
410     {
411         replacements_.emplace_back(search, replace);
412     }
413
414     //! Replaces links in a given string.
415     std::string replaceLinks(const std::string& input) const;
416
417     /*! \brief
418      * Process markup and wrap lines within a block of text.
419      *
420      * \param[in] text     Text to process.
421      * \param     wrapper  Object used to wrap the text.
422      *
423      * The \p wrapper should take care of either writing the text to output
424      * or providing an interface for the caller to retrieve the output.
425      */
426     void processMarkup(const std::string& text, IWrapper* wrapper) const;
427
428     //! Constant state shared by all child context objects.
429     StatePointer state_;
430     //! List of markup/other replacements.
431     ReplaceList replacements_;
432     //! Number of subsections above this context.
433     int sectionDepth_;
434
435 private:
436     GMX_DISALLOW_ASSIGN(Impl);
437 };
438
439 std::string HelpWriterContext::Impl::replaceLinks(const std::string& input) const
440 {
441     std::string result(input);
442     if (state_->links_ != nullptr)
443     {
444         HelpLinks::Impl::LinkList::const_iterator link;
445         for (link = state_->links_->impl_->links_.begin(); link != state_->links_->impl_->links_.end(); ++link)
446         {
447             result = replaceAllWords(result, link->linkName, link->replacement);
448         }
449     }
450     return result;
451 }
452
453 void HelpWriterContext::Impl::processMarkup(const std::string& text, IWrapper* wrapper) const
454 {
455     std::string result(text);
456     for (ReplaceList::const_iterator i = replacements_.begin(); i != replacements_.end(); ++i)
457     {
458         result = replaceAll(result, i->search, i->replace);
459     }
460     switch (state_->format_)
461     {
462         case eHelpOutputFormat_Console:
463         {
464             const int baseFirstLineIndent = wrapper->settings().firstLineIndent();
465             const int baseIndent          = wrapper->settings().indent();
466             result                        = repall(result, sandrTty);
467             result                        = replaceLinks(result);
468             std::string paragraph;
469             paragraph.reserve(result.length());
470             RstParagraphIterator iter(result);
471             while (iter.nextParagraph())
472             {
473                 iter.getParagraphText(&paragraph);
474                 wrapper->settings().setFirstLineIndent(baseFirstLineIndent + iter.firstLineIndent());
475                 wrapper->settings().setIndent(baseIndent + iter.indent());
476                 wrapper->wrap(paragraph);
477             }
478             wrapper->settings().setFirstLineIndent(baseFirstLineIndent);
479             wrapper->settings().setIndent(baseIndent);
480             break;
481         }
482         case eHelpOutputFormat_Rst:
483         {
484             result = repall(result, sandrRst);
485             result = replaceLinks(result);
486             result = replaceAll(result, "[REF]", "");
487             result = replaceAll(result, "[ref]", "");
488             result = removeExtraNewlinesRst(result);
489             wrapper->wrap(result);
490             break;
491         }
492         default: GMX_THROW(InternalError("Invalid help output format"));
493     }
494 }
495
496 /********************************************************************
497  * HelpWriterContext
498  */
499
500 HelpWriterContext::HelpWriterContext(TextWriter* writer, HelpOutputFormat format) :
501     impl_(new Impl(Impl::StatePointer(new Impl::SharedState(writer, format, nullptr)), 0))
502 {
503 }
504
505 HelpWriterContext::HelpWriterContext(TextWriter* writer, HelpOutputFormat format, const HelpLinks* links) :
506     impl_(new Impl(Impl::StatePointer(new Impl::SharedState(writer, format, links)), 0))
507 {
508     if (links != nullptr)
509     {
510         GMX_RELEASE_ASSERT(links->impl_->format_ == format,
511                            "Links must have the same output format as the context");
512     }
513 }
514
515 HelpWriterContext::HelpWriterContext(Impl* impl) : impl_(impl) {}
516
517 HelpWriterContext::HelpWriterContext(const HelpWriterContext& other) : impl_(new Impl(*other.impl_))
518 {
519 }
520
521 HelpWriterContext::~HelpWriterContext() {}
522
523 void HelpWriterContext::setReplacement(const std::string& search, const std::string& replace)
524 {
525     impl_->addReplacement(search, replace);
526 }
527
528 HelpOutputFormat HelpWriterContext::outputFormat() const
529 {
530     return impl_->state_->format_;
531 }
532
533 TextWriter& HelpWriterContext::outputFile() const
534 {
535     return impl_->state_->file_;
536 }
537
538 void HelpWriterContext::enterSubSection(const std::string& title)
539 {
540     GMX_RELEASE_ASSERT(impl_->sectionDepth_ - 1 < static_cast<int>(std::strlen(g_titleChars)),
541                        "Too deeply nested subsections");
542     writeTitle(title);
543     ++impl_->sectionDepth_;
544 }
545
546 std::string HelpWriterContext::substituteMarkupAndWrapToString(const TextLineWrapperSettings& settings,
547                                                                const std::string& text) const
548 {
549     WrapperToString wrapper(settings);
550     impl_->processMarkup(text, &wrapper);
551     return wrapper.result();
552 }
553
554 std::vector<std::string> HelpWriterContext::substituteMarkupAndWrapToVector(const TextLineWrapperSettings& settings,
555                                                                             const std::string& text) const
556 {
557     WrapperToVector wrapper(settings);
558     impl_->processMarkup(text, &wrapper);
559     return wrapper.result();
560 }
561
562 void HelpWriterContext::writeTitle(const std::string& title) const
563 {
564     if (title.empty())
565     {
566         return;
567     }
568     TextWriter& file = outputFile();
569     file.ensureEmptyLine();
570     switch (outputFormat())
571     {
572         case eHelpOutputFormat_Console: file.writeLine(toUpperCase(title)); break;
573         case eHelpOutputFormat_Rst:
574             file.writeLine(title);
575             file.writeLine(std::string(title.length(), g_titleChars[impl_->sectionDepth_]));
576             break;
577         default: GMX_THROW(NotImplementedError("This output format is not implemented"));
578     }
579     file.ensureEmptyLine();
580 }
581
582 void HelpWriterContext::writeTextBlock(const std::string& text) const
583 {
584     TextLineWrapperSettings settings;
585     if (outputFormat() == eHelpOutputFormat_Console)
586     {
587         settings.setLineLength(78);
588     }
589     outputFile().writeLine(substituteMarkupAndWrapToString(settings, text));
590 }
591
592 void HelpWriterContext::paragraphBreak() const
593 {
594     outputFile().ensureEmptyLine();
595 }
596
597 void HelpWriterContext::writeOptionListStart() const {}
598
599 void HelpWriterContext::writeOptionItem(const std::string& name,
600                                         const std::string& value,
601                                         const std::string& defaultValue,
602                                         const std::string& info,
603                                         const std::string& description) const
604 {
605     TextWriter& file = outputFile();
606     switch (outputFormat())
607     {
608         case eHelpOutputFormat_Console:
609         {
610             TextTableFormatter& formatter(impl_->state_->consoleOptionsFormatter());
611             formatter.clear();
612             formatter.addColumnLine(0, name);
613             formatter.addColumnLine(1, value);
614             if (!defaultValue.empty())
615             {
616                 formatter.addColumnLine(2, "(" + defaultValue + ")");
617             }
618             if (!info.empty())
619             {
620                 formatter.addColumnLine(3, "(" + info + ")");
621             }
622             TextLineWrapperSettings settings;
623             settings.setIndent(11);
624             settings.setLineLength(78);
625             std::string formattedDescription = substituteMarkupAndWrapToString(settings, description);
626             file.writeLine(formatter.formatRow());
627             file.writeLine(formattedDescription);
628             break;
629         }
630         case eHelpOutputFormat_Rst:
631         {
632             std::string args(value);
633             if (!defaultValue.empty())
634             {
635                 args.append(" (");
636                 args.append(defaultValue);
637                 args.append(")");
638             }
639             if (!info.empty())
640             {
641                 args.append(" (");
642                 args.append(info);
643                 args.append(")");
644             }
645             file.writeLine(formatString("``%s`` %s", name.c_str(), args.c_str()));
646             TextLineWrapperSettings settings;
647             settings.setIndent(4);
648             file.writeLine(substituteMarkupAndWrapToString(settings, description));
649             break;
650         }
651         default: GMX_THROW(NotImplementedError("This output format is not implemented"));
652     }
653 }
654
655 void HelpWriterContext::writeOptionListEnd() const {}
656
657 } // namespace gmx