8d5a44903be1664c72c3b4578c3a5f5107dab949
[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, 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 <string>
50 #include <vector>
51
52 #include <boost/shared_ptr.hpp>
53
54 #include "gromacs/utility/exceptions.h"
55 #include "gromacs/utility/file.h"
56 #include "gromacs/utility/gmxassert.h"
57 #include "gromacs/utility/programcontext.h"
58 #include "gromacs/utility/stringutil.h"
59
60 namespace gmx
61 {
62
63 namespace
64 {
65
66 //! \internal \addtogroup module_onlinehelp
67 //! \{
68
69 //! Characters used for reStructuredText title underlining.
70 const char g_titleChars[] = "=-^*~+#'_.";
71
72 struct t_sandr
73 {
74     const char *search;
75     const char *replace;
76 };
77
78 /* The order of these arrays is significant. Text search and replace
79  * for each element occurs in order, so earlier changes can induce
80  * subsequent changes even though the original text might not appear
81  * to invoke the latter changes.
82  * TODO: Get rid of this behavior. It makes it very difficult to manage
83  * replacements coming from multiple sources (e.g., hyperlinks).*/
84
85 //! List of replacements for console output.
86 const t_sandr sandrTty[] = {
87     { "\\*", "*" },
88     { "\\=", "=" },
89     { "[REF]", "" },
90     { "[ref]", "" },
91     { "[TT]", "" },
92     { "[tt]", "" },
93     { "[BB]", "" },
94     { "[bb]", "" },
95     { "[IT]", "" },
96     { "[it]", "" },
97     { "[MATH]", "" },
98     { "[math]", "" },
99     { "[CHEVRON]", "<" },
100     { "[chevron]", ">" },
101     { "[MAG]", "|" },
102     { "[mag]", "|" },
103     { "[INT]", "integral" },
104     { "[FROM]", " from " },
105     { "[from]", "" },
106     { "[TO]", " to " },
107     { "[to]", " of" },
108     { "[int]", "" },
109     { "[SUM]", "sum" },
110     { "[sum]", "" },
111     { "[SUB]", "_" },
112     { "[sub]", "" },
113     { "[SQRT]", "sqrt(" },
114     { "[sqrt]", ")" },
115     { "[EXP]", "exp(" },
116     { "[exp]", ")" },
117     { "[LN]", "ln(" },
118     { "[ln]", ")" },
119     { "[LOG]", "log(" },
120     { "[log]", ")" },
121     { "[COS]", "cos(" },
122     { "[cos]", ")" },
123     { "[SIN]", "sin(" },
124     { "[sin]", ")" },
125     { "[TAN]", "tan(" },
126     { "[tan]", ")" },
127     { "[COSH]", "cosh(" },
128     { "[cosh]", ")" },
129     { "[SINH]", "sinh(" },
130     { "[sinh]", ")" },
131     { "[TANH]", "tanh(" },
132     { "[tanh]", ")" },
133     { "[PAR]", "\n\n" },
134     { "[BR]", "\n"},
135     { "[GRK]", "" },
136     { "[grk]", "" }
137 };
138
139 //! List of replacements for reStructuredText output.
140 const t_sandr sandrRst[] = {
141     { "[TT]", "``" },
142     { "[tt]", "``" },
143     { "[BB]", "**" },
144     { "[bb]", "**" },
145     { "[IT]", "*" },
146     { "[it]", "*" },
147     { "[MATH]", "" },
148     { "[math]", "" },
149     { "[CHEVRON]", "<" },
150     { "[chevron]", ">" },
151     { "[MAG]", "\\|" },
152     { "[mag]", "\\|" },
153     { "[INT]", "integral" },
154     { "[FROM]", " from " },
155     { "[from]", "" },
156     { "[TO]", " to " },
157     { "[to]", " of" },
158     { "[int]", "" },
159     { "[SUM]", "sum" },
160     { "[sum]", "" },
161     { "[SUB]", "_" },
162     { "[sub]", "" },
163     { "[SQRT]", "sqrt(" },
164     { "[sqrt]", ")" },
165     { "[EXP]", "exp(" },
166     { "[exp]", ")" },
167     { "[LN]", "ln(" },
168     { "[ln]", ")" },
169     { "[LOG]", "log(" },
170     { "[log]", ")" },
171     { "[COS]", "cos(" },
172     { "[cos]", ")" },
173     { "[SIN]", "sin(" },
174     { "[sin]", ")" },
175     { "[TAN]", "tan(" },
176     { "[tan]", ")" },
177     { "[COSH]", "cosh(" },
178     { "[cosh]", ")" },
179     { "[SINH]", "sinh(" },
180     { "[sinh]", ")" },
181     { "[TANH]", "tanh(" },
182     { "[tanh]", ")" },
183     { "[PAR]", "\n\n" },
184     // [BR] is fundamentally incompatible with rst
185     { "[BR]", "\n\n"},
186     { "[GRK]", "" },
187     { "[grk]", "" }
188 };
189
190 /*! \brief
191  * Replaces all entries from a list of replacements.
192  */
193 std::string repall(const std::string &s, int nsr, const t_sandr sa[])
194 {
195     std::string result(s);
196     for (int i = 0; i < nsr; ++i)
197     {
198         result = replaceAll(result, sa[i].search, sa[i].replace);
199     }
200     return result;
201 }
202
203 /*! \brief
204  * Replaces all entries from a list of replacements.
205  */
206 template <size_t nsr>
207 std::string repall(const std::string &s, const t_sandr (&sa)[nsr])
208 {
209     return repall(s, nsr, sa);
210 }
211
212 /*! \brief
213  * Custom output interface for HelpWriterContext::Impl::processMarkup().
214  *
215  * Provides an interface that is used to implement different types of output
216  * from HelpWriterContext::Impl::processMarkup().
217  */
218 class WrapperInterface
219 {
220     public:
221         virtual ~WrapperInterface() {}
222
223         /*! \brief
224          * Provides the wrapping settings.
225          *
226          * HelpWriterContext::Impl::processMarkup() may provide some default
227          * values for the settings if they are not set; this is the reason the
228          * return value is not const.
229          */
230         virtual TextLineWrapperSettings &settings() = 0;
231         //! Appends the given string to output.
232         virtual void wrap(const std::string &text)  = 0;
233 };
234
235 /*! \brief
236  * Wraps markup output into a single string.
237  */
238 class WrapperToString : public WrapperInterface
239 {
240     public:
241         //! Creates a wrapper with the given settings.
242         explicit WrapperToString(const TextLineWrapperSettings &settings)
243             : wrapper_(settings)
244         {
245         }
246
247         virtual TextLineWrapperSettings &settings()
248         {
249             return wrapper_.settings();
250         }
251         virtual void wrap(const std::string &text)
252         {
253             result_.append(wrapper_.wrapToString(text));
254         }
255         //! Returns the result string.
256         const std::string &result() const { return result_; }
257
258     private:
259         TextLineWrapper         wrapper_;
260         std::string             result_;
261 };
262
263 /*! \brief
264  * Wraps markup output into a vector of string (one line per element).
265  */
266 class WrapperToVector : public WrapperInterface
267 {
268     public:
269         //! Creates a wrapper with the given settings.
270         explicit WrapperToVector(const TextLineWrapperSettings &settings)
271             : wrapper_(settings)
272         {
273         }
274
275         virtual TextLineWrapperSettings &settings()
276         {
277             return wrapper_.settings();
278         }
279         virtual void wrap(const std::string &text)
280         {
281             const std::vector<std::string> &lines = wrapper_.wrapToVector(text);
282             result_.insert(result_.end(), lines.begin(), lines.end());
283         }
284         //! Returns a vector with the output lines.
285         const std::vector<std::string> &result() const { return result_; }
286
287     private:
288         TextLineWrapper          wrapper_;
289         std::vector<std::string> result_;
290 };
291
292 /*! \brief
293  * Makes the string uppercase.
294  *
295  * \param[in] text  Input text.
296  * \returns   \p text with all characters transformed to uppercase.
297  * \throws    std::bad_alloc if out of memory.
298  */
299 std::string toUpperCase(const std::string &text)
300 {
301     std::string result(text);
302     std::transform(result.begin(), result.end(), result.begin(), toupper);
303     return result;
304 }
305
306 /*! \brief
307  * Removes extra newlines from reStructuredText.
308  *
309  * \param[in] text  Input text.
310  * \returns   \p text with all sequences of more than two newlines replaced
311  *     with just two newlines.
312  *
313  * Does not throw.
314  */
315 std::string removeExtraNewlinesRst(const std::string &text)
316 {
317     // Start from 2, so that all newlines in the beginning get stripped off.
318     int         newlineCount = 2;
319     std::string result;
320     result.reserve(text.length());
321     for (size_t i = 0; i < text.length(); ++i)
322     {
323         if (text[i] == '\n')
324         {
325             ++newlineCount;
326             if (newlineCount > 2)
327             {
328                 continue;
329             }
330         }
331         else
332         {
333             newlineCount = 0;
334         }
335         result.push_back(text[i]);
336     }
337     size_t last = result.find_last_not_of('\n');
338     if (last != std::string::npos)
339     {
340         result.resize(last + 1);
341     }
342     return result;
343 }
344
345 //! \}
346
347 }   // namespace
348
349 /********************************************************************
350  * HelpLinks::Impl
351  */
352
353 /*! \internal \brief
354  * Private implementation class for HelpLinks.
355  *
356  * \ingroup module_onlinehelp
357  */
358 class HelpLinks::Impl
359 {
360     public:
361         struct LinkItem
362         {
363             LinkItem(const std::string &linkName,
364                      const std::string &replacement)
365                 : linkName(linkName), replacement(replacement)
366             {
367             }
368             std::string         linkName;
369             std::string         replacement;
370         };
371
372         //! Shorthand for a list of links.
373         typedef std::vector<LinkItem> LinkList;
374
375         //! Initializes empty links with the given format.
376         explicit Impl(HelpOutputFormat format) : format_(format)
377         {
378         }
379
380         //! List of links.
381         LinkList          links_;
382         //! Output format for which the links are formatted.
383         HelpOutputFormat  format_;
384 };
385
386 /********************************************************************
387  * HelpLinks
388  */
389
390 HelpLinks::HelpLinks(HelpOutputFormat format) : impl_(new Impl(format))
391 {
392 }
393
394 HelpLinks::~HelpLinks()
395 {
396 }
397
398 void HelpLinks::addLink(const std::string &linkName,
399                         const std::string &targetName,
400                         const std::string &displayName)
401 {
402     std::string replacement;
403     switch (impl_->format_)
404     {
405         case eHelpOutputFormat_Console:
406             replacement = repall(displayName, sandrTty);
407             break;
408         case eHelpOutputFormat_Rst:
409             replacement = targetName;
410             break;
411         default:
412             GMX_RELEASE_ASSERT(false, "Output format not implemented for links");
413     }
414     impl_->links_.push_back(Impl::LinkItem(linkName, replacement));
415 }
416
417 /********************************************************************
418  * HelpWriterContext::Impl
419  */
420
421 /*! \internal \brief
422  * Private implementation class for HelpWriterContext.
423  *
424  * \ingroup module_onlinehelp
425  */
426 class HelpWriterContext::Impl
427 {
428     public:
429         /*! \brief
430          * Shared, non-modifiable state for context objects.
431          *
432          * Contents of this structure are shared between all context objects
433          * that are created from a common parent.
434          * This state should not be modified after construction.
435          *
436          * \ingroup module_onlinehelp
437          */
438         struct SharedState
439         {
440             //! Initializes the state with the given parameters.
441             SharedState(File *file, HelpOutputFormat format,
442                         const HelpLinks *links)
443                 : file_(*file), format_(format), links_(links)
444             {
445             }
446
447             //! Output file to which the help is written.
448             File                   &file_;
449             //! Output format for the help output.
450             HelpOutputFormat        format_;
451             //! Links to use.
452             const HelpLinks        *links_;
453         };
454
455         struct ReplaceItem
456         {
457             ReplaceItem(const std::string &search,
458                         const std::string &replace)
459                 : search(search), replace(replace)
460             {
461             }
462             std::string         search;
463             std::string         replace;
464         };
465
466         //! Smart pointer type for managing the shared state.
467         typedef boost::shared_ptr<const SharedState> StatePointer;
468         //! Shorthand for a list of markup/other replacements.
469         typedef std::vector<ReplaceItem> ReplaceList;
470
471         //! Initializes the context with the given state and section depth.
472         Impl(const StatePointer &state, int sectionDepth)
473             : state_(state), sectionDepth_(sectionDepth)
474         {
475             initDefaultReplacements();
476         }
477
478         //! Initializes default replacements for the chosen output format.
479         void initDefaultReplacements();
480         //! Adds a new replacement.
481         void addReplacement(const std::string &search,
482                             const std::string &replace)
483         {
484             replacements_.push_back(ReplaceItem(search, replace));
485         }
486
487         //! Replaces links in a given string.
488         std::string replaceLinks(const std::string &input) const;
489
490         /*! \brief
491          * Process markup and wrap lines within a block of text.
492          *
493          * \param[in] text     Text to process.
494          * \param     wrapper  Object used to wrap the text.
495          *
496          * The \p wrapper should take care of either writing the text to output
497          * or providing an interface for the caller to retrieve the output.
498          */
499         void processMarkup(const std::string &text,
500                            WrapperInterface  *wrapper) const;
501
502         //! Constant state shared by all child context objects.
503         StatePointer            state_;
504         //! List of markup/other replacements.
505         ReplaceList             replacements_;
506         //! Number of subsections above this context.
507         int                     sectionDepth_;
508
509     private:
510         GMX_DISALLOW_ASSIGN(Impl);
511 };
512
513 void HelpWriterContext::Impl::initDefaultReplacements()
514 {
515     const char *program = getProgramContext().programName();
516     addReplacement("[PROGRAM]", program);
517 }
518
519 std::string HelpWriterContext::Impl::replaceLinks(const std::string &input) const
520 {
521     std::string result(input);
522     if (state_->links_ != NULL)
523     {
524         HelpLinks::Impl::LinkList::const_iterator link;
525         for (link  = state_->links_->impl_->links_.begin();
526              link != state_->links_->impl_->links_.end(); ++link)
527         {
528             result = replaceAllWords(result, link->linkName, link->replacement);
529         }
530     }
531     return result;
532 }
533
534 void HelpWriterContext::Impl::processMarkup(const std::string &text,
535                                             WrapperInterface  *wrapper) const
536 {
537     std::string result(text);
538     for (ReplaceList::const_iterator i = replacements_.begin();
539          i != replacements_.end(); ++i)
540     {
541         result = replaceAll(result, i->search, i->replace);
542     }
543     switch (state_->format_)
544     {
545         case eHelpOutputFormat_Console:
546         {
547             const int   baseFirstLineIndent = wrapper->settings().firstLineIndent();
548             const int   baseIndent          = wrapper->settings().indent();
549             result = repall(result, sandrTty);
550             result = replaceLinks(result);
551             std::string paragraph;
552             paragraph.reserve(result.length());
553             size_t      i             = 0;
554             int         nextBreakSize = 0;
555             bool        bLiteral      = false;
556             while (i < result.length())
557             {
558                 while (i < result.length() && result[i] == '\n')
559                 {
560                     ++i;
561                 }
562                 if (i == result.length())
563                 {
564                     break;
565                 }
566                 const int breakSize     = nextBreakSize;
567                 int       currentLine   = 0;
568                 bool      bLineStart    = true;
569                 int       currentIndent = 0;
570                 int       firstIndent   = 0;
571                 int       indent        = 0;
572                 paragraph.clear();
573                 for (;; ++i)
574                 {
575                     if (result[i] == '\n' || i == result.length())
576                     {
577                         if (currentLine == 0)
578                         {
579                             firstIndent = currentIndent;
580                         }
581                         else if (currentLine == 1)
582                         {
583                             indent = currentIndent;
584                         }
585                         ++currentLine;
586                         bLineStart    = true;
587                         currentIndent = 0;
588                         if (i + 1 >= result.length() || result[i + 1] == '\n')
589                         {
590                             nextBreakSize = 2;
591                             break;
592                         }
593                         if (!bLiteral)
594                         {
595                             if (!std::isspace(result[i - 1]))
596                             {
597                                 paragraph.push_back(' ');
598                             }
599                             continue;
600                         }
601                     }
602                     else if (bLineStart)
603                     {
604                         if (std::isspace(result[i]))
605                         {
606                             ++currentIndent;
607                             continue;
608                         }
609                         else if (i + 1 < result.length()
610                                  && result[i] == '*' && result[i + 1] == ' ')
611                         {
612                             if (currentLine > 0)
613                             {
614                                 while (i > 0 && result[i - 1] != '\n')
615                                 {
616                                     --i;
617                                 }
618                                 paragraph     = stripString(paragraph);
619                                 nextBreakSize = 1;
620                                 break;
621                             }
622                             indent = currentIndent + 2;
623                         }
624                         bLineStart = false;
625                     }
626                     paragraph.push_back(result[i]);
627                 }
628                 if (endsWith(paragraph, "::"))
629                 {
630                     bLiteral = true;
631                     if (paragraph.length() == 2)
632                     {
633                         if (breakSize == 0)
634                         {
635                             nextBreakSize = 0;
636                         }
637                         continue;
638                     }
639                     if (paragraph[paragraph.length() - 3] == ' ')
640                     {
641                         paragraph.resize(paragraph.length() - 3);
642                     }
643                     else
644                     {
645                         paragraph.resize(paragraph.length() - 1);
646                     }
647                 }
648                 else
649                 {
650                     bLiteral = false;
651                 }
652                 if (breakSize > 0)
653                 {
654                     wrapper->wrap(std::string(breakSize, '\n'));
655                 }
656                 wrapper->settings().setFirstLineIndent(baseFirstLineIndent + firstIndent);
657                 wrapper->settings().setIndent(baseIndent + indent);
658                 wrapper->wrap(paragraph);
659                 wrapper->settings().setFirstLineIndent(baseFirstLineIndent);
660                 wrapper->settings().setIndent(baseIndent);
661             }
662             break;
663         }
664         case eHelpOutputFormat_Rst:
665         {
666             result = repall(result, sandrRst);
667             result = replaceLinks(result);
668             result = replaceAll(result, "[REF]", "");
669             result = replaceAll(result, "[ref]", "");
670             result = removeExtraNewlinesRst(result);
671             wrapper->wrap(result);
672             break;
673         }
674         default:
675             GMX_THROW(InternalError("Invalid help output format"));
676     }
677 }
678
679 /********************************************************************
680  * HelpWriterContext
681  */
682
683 HelpWriterContext::HelpWriterContext(File *file, HelpOutputFormat format)
684     : impl_(new Impl(Impl::StatePointer(new Impl::SharedState(file, format, NULL)), 0))
685 {
686 }
687
688 HelpWriterContext::HelpWriterContext(File *file, HelpOutputFormat format,
689                                      const HelpLinks *links)
690     : impl_(new Impl(Impl::StatePointer(new Impl::SharedState(file, format, links)), 0))
691 {
692     if (links != NULL)
693     {
694         GMX_RELEASE_ASSERT(links->impl_->format_ == format,
695                            "Links must have the same output format as the context");
696     }
697 }
698
699 HelpWriterContext::HelpWriterContext(Impl *impl)
700     : impl_(impl)
701 {
702 }
703
704 HelpWriterContext::HelpWriterContext(const HelpWriterContext &other)
705     : impl_(new Impl(*other.impl_))
706 {
707 }
708
709 HelpWriterContext::~HelpWriterContext()
710 {
711 }
712
713 void HelpWriterContext::setReplacement(const std::string &search,
714                                        const std::string &replace)
715 {
716     impl_->addReplacement(search, replace);
717 }
718
719 HelpOutputFormat HelpWriterContext::outputFormat() const
720 {
721     return impl_->state_->format_;
722 }
723
724 File &HelpWriterContext::outputFile() const
725 {
726     return impl_->state_->file_;
727 }
728
729 void HelpWriterContext::enterSubSection(const std::string &title)
730 {
731     GMX_RELEASE_ASSERT(impl_->sectionDepth_ - 1 < static_cast<int>(std::strlen(g_titleChars)),
732                        "Too deeply nested subsections");
733     writeTitle(title);
734     ++impl_->sectionDepth_;
735 }
736
737 std::string
738 HelpWriterContext::substituteMarkupAndWrapToString(
739         const TextLineWrapperSettings &settings, const std::string &text) const
740 {
741     WrapperToString wrapper(settings);
742     impl_->processMarkup(text, &wrapper);
743     return wrapper.result();
744 }
745
746 std::vector<std::string>
747 HelpWriterContext::substituteMarkupAndWrapToVector(
748         const TextLineWrapperSettings &settings, const std::string &text) const
749 {
750     WrapperToVector wrapper(settings);
751     impl_->processMarkup(text, &wrapper);
752     return wrapper.result();
753 }
754
755 void HelpWriterContext::writeTitle(const std::string &title) const
756 {
757     if (title.empty())
758     {
759         return;
760     }
761     File &file = outputFile();
762     switch (outputFormat())
763     {
764         case eHelpOutputFormat_Console:
765             file.writeLine(toUpperCase(title));
766             file.writeLine();
767             break;
768         case eHelpOutputFormat_Rst:
769             file.writeLine(title);
770             file.writeLine(std::string(title.length(),
771                                        g_titleChars[impl_->sectionDepth_]));
772             break;
773         default:
774             GMX_THROW(NotImplementedError(
775                               "This output format is not implemented"));
776     }
777 }
778
779 void HelpWriterContext::writeTextBlock(const std::string &text) const
780 {
781     TextLineWrapperSettings settings;
782     if (outputFormat() == eHelpOutputFormat_Console)
783     {
784         settings.setLineLength(78);
785     }
786     outputFile().writeLine(substituteMarkupAndWrapToString(settings, text));
787 }
788
789 void HelpWriterContext::writeOptionListStart() const
790 {
791 }
792
793 void HelpWriterContext::writeOptionItem(const std::string &name,
794                                         const std::string &args,
795                                         const std::string &description) const
796 {
797     File &file = outputFile();
798     switch (outputFormat())
799     {
800         case eHelpOutputFormat_Console:
801             // TODO: Generalize this when there is need for it; the current,
802             // special implementation is in CommandLineHelpWriter.
803             GMX_THROW(NotImplementedError("Option item formatting for console output not implemented"));
804             break;
805         case eHelpOutputFormat_Rst:
806         {
807             file.writeLine(formatString("``%s`` %s", name.c_str(), args.c_str()));
808             TextLineWrapperSettings settings;
809             settings.setIndent(4);
810             file.writeLine(substituteMarkupAndWrapToString(settings, description));
811             break;
812         }
813         default:
814             GMX_THROW(NotImplementedError(
815                               "This output format is not implemented"));
816     }
817 }
818
819 void HelpWriterContext::writeOptionListEnd() const
820 {
821 }
822
823 } // namespace gmx