Remove PrivateImplPointer in favour of std::unique_ptr
[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),
281             replacement(replacement)
282         {
283         }
284         std::string linkName;
285         std::string replacement;
286     };
287
288     //! Shorthand for a list of links.
289     typedef std::vector<LinkItem> LinkList;
290
291     //! Initializes empty links with the given format.
292     explicit Impl(HelpOutputFormat format) : format_(format) {}
293
294     //! List of links.
295     LinkList links_;
296     //! Output format for which the links are formatted.
297     HelpOutputFormat format_;
298 };
299
300 /********************************************************************
301  * HelpLinks
302  */
303
304 HelpLinks::HelpLinks(HelpOutputFormat format) : impl_(new Impl(format)) {}
305
306 HelpLinks::~HelpLinks() {}
307
308 void HelpLinks::addLink(const std::string& linkName, const std::string& targetName, const std::string& displayName)
309 {
310     std::string replacement;
311     switch (impl_->format_)
312     {
313         case eHelpOutputFormat_Console: replacement = repall(displayName, sandrTty); break;
314         case eHelpOutputFormat_Rst: replacement = targetName; break;
315         default: GMX_RELEASE_ASSERT(false, "Output format not implemented for links");
316     }
317     impl_->links_.emplace_back(linkName, replacement);
318 }
319
320 /********************************************************************
321  * HelpWriterContext::Impl
322  */
323
324 /*! \internal \brief
325  * Private implementation class for HelpWriterContext.
326  *
327  * \ingroup module_onlinehelp
328  */
329 class HelpWriterContext::Impl
330 {
331 public:
332     /*! \brief
333      * Shared, non-modifiable state for context objects.
334      *
335      * Contents of this structure are shared between all context objects
336      * that are created from a common parent.
337      * This state should not be modified after construction.
338      *
339      * \ingroup module_onlinehelp
340      */
341     class SharedState
342     {
343     public:
344         //! Initializes the state with the given parameters.
345         SharedState(TextWriter* writer, HelpOutputFormat format, const HelpLinks* links) :
346             file_(*writer),
347             format_(format),
348             links_(links)
349         {
350         }
351
352         /*! \brief
353          * Returns a formatter for formatting options lists for console
354          * output.
355          *
356          * The formatter is lazily initialized on first access.
357          */
358         TextTableFormatter& consoleOptionsFormatter() const
359         {
360             GMX_RELEASE_ASSERT(format_ == eHelpOutputFormat_Console,
361                                "Accessing console formatter for non-console output");
362             if (!consoleOptionsFormatter_)
363             {
364                 consoleOptionsFormatter_ = std::make_unique<TextTableFormatter>();
365                 consoleOptionsFormatter_->setFirstColumnIndent(1);
366                 consoleOptionsFormatter_->addColumn(nullptr, 7, false);
367                 consoleOptionsFormatter_->addColumn(nullptr, 18, false);
368                 consoleOptionsFormatter_->addColumn(nullptr, 16, false);
369                 consoleOptionsFormatter_->addColumn(nullptr, 28, false);
370             }
371             return *consoleOptionsFormatter_;
372         }
373
374         //! Writer for writing the help.
375         TextWriter& file_;
376         //! Output format for the help output.
377         HelpOutputFormat format_;
378         //! Links to use.
379         const HelpLinks* links_;
380
381     private:
382         //! Formatter for console output options.
383         // Never releases ownership.
384         mutable std::unique_ptr<TextTableFormatter> consoleOptionsFormatter_;
385
386         GMX_DISALLOW_COPY_AND_ASSIGN(SharedState);
387     };
388
389     struct ReplaceItem
390     {
391         ReplaceItem(const std::string& search, const std::string& replace) :
392             search(search),
393             replace(replace)
394         {
395         }
396         std::string search;
397         std::string replace;
398     };
399
400     //! Smart pointer type for managing the shared state.
401     typedef std::shared_ptr<const SharedState> StatePointer;
402     //! Shorthand for a list of markup/other replacements.
403     typedef std::vector<ReplaceItem> ReplaceList;
404
405     //! Initializes the context with the given state and section depth.
406     Impl(const StatePointer& state, int sectionDepth) : state_(state), sectionDepth_(sectionDepth)
407     {
408     }
409     //! Copies the context.
410     Impl(const Impl&) = default;
411
412     //! Adds a new replacement.
413     void addReplacement(const std::string& search, const std::string& replace)
414     {
415         replacements_.emplace_back(search, replace);
416     }
417
418     //! Replaces links in a given string.
419     std::string replaceLinks(const std::string& input) const;
420
421     /*! \brief
422      * Process markup and wrap lines within a block of text.
423      *
424      * \param[in] text     Text to process.
425      * \param     wrapper  Object used to wrap the text.
426      *
427      * The \p wrapper should take care of either writing the text to output
428      * or providing an interface for the caller to retrieve the output.
429      */
430     void processMarkup(const std::string& text, IWrapper* wrapper) const;
431
432     //! Constant state shared by all child context objects.
433     StatePointer state_;
434     //! List of markup/other replacements.
435     ReplaceList replacements_;
436     //! Number of subsections above this context.
437     int sectionDepth_;
438
439 private:
440     GMX_DISALLOW_ASSIGN(Impl);
441 };
442
443 std::string HelpWriterContext::Impl::replaceLinks(const std::string& input) const
444 {
445     std::string result(input);
446     if (state_->links_ != nullptr)
447     {
448         HelpLinks::Impl::LinkList::const_iterator link;
449         for (link = state_->links_->impl_->links_.begin(); link != state_->links_->impl_->links_.end(); ++link)
450         {
451             result = replaceAllWords(result, link->linkName, link->replacement);
452         }
453     }
454     return result;
455 }
456
457 void HelpWriterContext::Impl::processMarkup(const std::string& text, IWrapper* wrapper) const
458 {
459     std::string result(text);
460     for (ReplaceList::const_iterator i = replacements_.begin(); i != replacements_.end(); ++i)
461     {
462         result = replaceAll(result, i->search, i->replace);
463     }
464     switch (state_->format_)
465     {
466         case eHelpOutputFormat_Console:
467         {
468             const int baseFirstLineIndent = wrapper->settings().firstLineIndent();
469             const int baseIndent          = wrapper->settings().indent();
470             result                        = repall(result, sandrTty);
471             result                        = replaceLinks(result);
472             std::string paragraph;
473             paragraph.reserve(result.length());
474             RstParagraphIterator iter(result);
475             while (iter.nextParagraph())
476             {
477                 iter.getParagraphText(&paragraph);
478                 wrapper->settings().setFirstLineIndent(baseFirstLineIndent + iter.firstLineIndent());
479                 wrapper->settings().setIndent(baseIndent + iter.indent());
480                 wrapper->wrap(paragraph);
481             }
482             wrapper->settings().setFirstLineIndent(baseFirstLineIndent);
483             wrapper->settings().setIndent(baseIndent);
484             break;
485         }
486         case eHelpOutputFormat_Rst:
487         {
488             result = repall(result, sandrRst);
489             result = replaceLinks(result);
490             result = replaceAll(result, "[REF]", "");
491             result = replaceAll(result, "[ref]", "");
492             result = removeExtraNewlinesRst(result);
493             wrapper->wrap(result);
494             break;
495         }
496         default: GMX_THROW(InternalError("Invalid help output format"));
497     }
498 }
499
500 /********************************************************************
501  * HelpWriterContext
502  */
503
504 HelpWriterContext::HelpWriterContext(TextWriter* writer, HelpOutputFormat format) :
505     impl_(new Impl(Impl::StatePointer(new Impl::SharedState(writer, format, nullptr)), 0))
506 {
507 }
508
509 HelpWriterContext::HelpWriterContext(TextWriter* writer, HelpOutputFormat format, const HelpLinks* links) :
510     impl_(new Impl(Impl::StatePointer(new Impl::SharedState(writer, format, links)), 0))
511 {
512     if (links != nullptr)
513     {
514         GMX_RELEASE_ASSERT(links->impl_->format_ == format,
515                            "Links must have the same output format as the context");
516     }
517 }
518
519 HelpWriterContext::HelpWriterContext(Impl* impl) : impl_(impl) {}
520
521 HelpWriterContext::HelpWriterContext(const HelpWriterContext& other) : impl_(new Impl(*other.impl_))
522 {
523 }
524
525 HelpWriterContext::~HelpWriterContext() {}
526
527 void HelpWriterContext::setReplacement(const std::string& search, const std::string& replace)
528 {
529     impl_->addReplacement(search, replace);
530 }
531
532 HelpOutputFormat HelpWriterContext::outputFormat() const
533 {
534     return impl_->state_->format_;
535 }
536
537 TextWriter& HelpWriterContext::outputFile() const
538 {
539     return impl_->state_->file_;
540 }
541
542 void HelpWriterContext::enterSubSection(const std::string& title)
543 {
544     GMX_RELEASE_ASSERT(impl_->sectionDepth_ - 1 < static_cast<int>(std::strlen(g_titleChars)),
545                        "Too deeply nested subsections");
546     writeTitle(title);
547     ++impl_->sectionDepth_;
548 }
549
550 std::string HelpWriterContext::substituteMarkupAndWrapToString(const TextLineWrapperSettings& settings,
551                                                                const std::string& text) const
552 {
553     WrapperToString wrapper(settings);
554     impl_->processMarkup(text, &wrapper);
555     return wrapper.result();
556 }
557
558 std::vector<std::string> HelpWriterContext::substituteMarkupAndWrapToVector(const TextLineWrapperSettings& settings,
559                                                                             const std::string& text) const
560 {
561     WrapperToVector wrapper(settings);
562     impl_->processMarkup(text, &wrapper);
563     return wrapper.result();
564 }
565
566 void HelpWriterContext::writeTitle(const std::string& title) const
567 {
568     if (title.empty())
569     {
570         return;
571     }
572     TextWriter& file = outputFile();
573     file.ensureEmptyLine();
574     switch (outputFormat())
575     {
576         case eHelpOutputFormat_Console: file.writeLine(toUpperCase(title)); break;
577         case eHelpOutputFormat_Rst:
578             file.writeLine(title);
579             file.writeLine(std::string(title.length(), g_titleChars[impl_->sectionDepth_]));
580             break;
581         default: GMX_THROW(NotImplementedError("This output format is not implemented"));
582     }
583     file.ensureEmptyLine();
584 }
585
586 void HelpWriterContext::writeTextBlock(const std::string& text) const
587 {
588     TextLineWrapperSettings settings;
589     if (outputFormat() == eHelpOutputFormat_Console)
590     {
591         settings.setLineLength(78);
592     }
593     outputFile().writeLine(substituteMarkupAndWrapToString(settings, text));
594 }
595
596 void HelpWriterContext::paragraphBreak() const
597 {
598     outputFile().ensureEmptyLine();
599 }
600
601 void HelpWriterContext::writeOptionListStart() const {}
602
603 void HelpWriterContext::writeOptionItem(const std::string& name,
604                                         const std::string& value,
605                                         const std::string& defaultValue,
606                                         const std::string& info,
607                                         const std::string& description) const
608 {
609     TextWriter& file = outputFile();
610     switch (outputFormat())
611     {
612         case eHelpOutputFormat_Console:
613         {
614             TextTableFormatter& formatter(impl_->state_->consoleOptionsFormatter());
615             formatter.clear();
616             formatter.addColumnLine(0, name);
617             formatter.addColumnLine(1, value);
618             if (!defaultValue.empty())
619             {
620                 formatter.addColumnLine(2, "(" + defaultValue + ")");
621             }
622             if (!info.empty())
623             {
624                 formatter.addColumnLine(3, "(" + info + ")");
625             }
626             TextLineWrapperSettings settings;
627             settings.setIndent(11);
628             settings.setLineLength(78);
629             std::string formattedDescription = substituteMarkupAndWrapToString(settings, description);
630             file.writeLine(formatter.formatRow());
631             file.writeLine(formattedDescription);
632             break;
633         }
634         case eHelpOutputFormat_Rst:
635         {
636             std::string args(value);
637             if (!defaultValue.empty())
638             {
639                 args.append(" (");
640                 args.append(defaultValue);
641                 args.append(")");
642             }
643             if (!info.empty())
644             {
645                 args.append(" (");
646                 args.append(info);
647                 args.append(")");
648             }
649             file.writeLine(formatString("``%s`` %s", name.c_str(), args.c_str()));
650             TextLineWrapperSettings settings;
651             settings.setIndent(4);
652             file.writeLine(substituteMarkupAndWrapToString(settings, description));
653             break;
654         }
655         default: GMX_THROW(NotImplementedError("This output format is not implemented"));
656     }
657 }
658
659 void HelpWriterContext::writeOptionListEnd() const {}
660
661 } // namespace gmx