1 /*
2 * This file is part of the GROMACS molecular simulation package.
3 *
4 * Copyright (c) 2012,2013,2014,2016,2017 by the GROMACS development team.
5 * Copyright (c) 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 functions in helpformat.h.
39 *
40 * \author Teemu Murtola <teemu.murtola@gmail.com>
41 * \ingroup module_onlinehelp
42 */
43 #include "gmxpre.h"
44
45 #include "helpformat.h"
46
47 #include <algorithm>
48 #include <string>
49 #include <vector>
50
51 #include "gromacs/onlinehelp/helpwritercontext.h"
52 #include "gromacs/utility/gmxassert.h"
53 #include "gromacs/utility/stringutil.h"
54
55 namespace gmx
56 {
57
58 /********************************************************************
59 * TextTableFormatter::Impl
60 */
61
62 /*! \internal \brief
63 * Private implementation class for TextTableFormatter.
64 *
65 * \ingroup module_onlinehelp
66 */
67 class TextTableFormatter::Impl
68 {
69 public:
70 /*! \internal \brief
71 * Manages a single column for TextTableFormatter.
72 *
73 * \ingroup module_onlinehelp
74 */
75 struct ColumnData
76 {
77 //! Initializes a text table column with given values.
ColumnDatagmx::TextTableFormatter::Impl::ColumnData78 ColumnData(const char* title, int width, bool bWrap) :
79 title_(title != nullptr ? title : ""),
80 width_(width),
81 bWrap_(bWrap),
82 firstLine_(0),
83 nextLineIndex_(0),
84 nextLineOffset_(0)
85 {
86 GMX_ASSERT(width >= 0, "Negative width not possible");
87 GMX_ASSERT(title_.length() <= static_cast<size_t>(width),
88 "Title too long for column width");
89 }
90
91 //! Returns the title of the column.
titlegmx::TextTableFormatter::Impl::ColumnData92 const std::string& title() const { return title_; }
93 //! Returns the width of the column.
widthgmx::TextTableFormatter::Impl::ColumnData94 int width() const { return width_; }
95 /*! \brief
96 * Returns the first line offset for the current row.
97 *
98 * Note that the return value may be outside the printed lines if
99 * there is no text.
100 */
firstLinegmx::TextTableFormatter::Impl::ColumnData101 int firstLine() const { return firstLine_; }
102
103 /*! \brief
104 * Resets the formatting state.
105 *
106 * After this call, textForNextLine() and hasLinesRemaining() can
107 * be used to format the lines for the column.
108 */
startFormattinggmx::TextTableFormatter::Impl::ColumnData109 void startFormatting()
110 {
111 nextLineIndex_ = (!lines_.empty() ? -firstLine_ : 0);
112 nextLineOffset_ = 0;
113 }
114 //! Whether there are lines remaining for textForNextLine().
hasLinesRemaininggmx::TextTableFormatter::Impl::ColumnData115 bool hasLinesRemaining() const { return nextLineIndex_ < ssize(lines_); }
116 /*! \brief
117 * Returns the text for the next line.
118 *
119 * \param[in] columnWidth Width to wrap the text to.
120 * \returns Text for the next line, or empty string if there is
121 * no text for this column.
122 */
textForNextLinegmx::TextTableFormatter::Impl::ColumnData123 std::string textForNextLine(int columnWidth)
124 {
125 if (nextLineIndex_ < 0 || !hasLinesRemaining())
126 {
127 ++nextLineIndex_;
128 return std::string();
129 }
130 if (bWrap_)
131 {
132 TextLineWrapperSettings settings;
133 settings.setLineLength(columnWidth);
134 TextLineWrapper wrapper(settings);
135 const std::string& currentLine = lines_[nextLineIndex_];
136 const size_t prevOffset = nextLineOffset_;
137 const size_t nextOffset = wrapper.findNextLine(currentLine, prevOffset);
138 if (nextOffset >= currentLine.size())
139 {
140 ++nextLineIndex_;
141 nextLineOffset_ = 0;
142 }
143 else
144 {
145 nextLineOffset_ = nextOffset;
146 }
147 return wrapper.formatLine(currentLine, prevOffset, nextOffset);
148 }
149 else
150 {
151 return lines_[nextLineIndex_++];
152 }
153 }
154
155 //! Statit data: title of the column.
156 std::string title_;
157 //! Static data: width of the column.
158 int width_;
159 //! Static data: whether to automatically wrap input text.
160 bool bWrap_;
161 //! First line offset for the current row.
162 int firstLine_;
163 //! Text lines for the current row.
164 std::vector<std::string> lines_;
165 //! Formatting state: index in `lines_` for the next line.
166 int nextLineIndex_;
167 //! Formatting state: offset within line `nextLineIndex_` for the next line.
168 size_t nextLineOffset_;
169 };
170
171 //! Container type for column data.
172 typedef std::vector<ColumnData> ColumnList;
173
174 //! Initializes data for an empty formatter.
175 Impl();
176
177 /*! \brief
178 * Convenience method for checked access to data for a column.
179 *
180 * \param[in] index Zero-based column index.
181 * \returns \c columns_[index]
182 */
columnData(int index)183 ColumnData& columnData(int index)
184 {
185 GMX_ASSERT(index >= 0 && index < ssize(columns_), "Invalid column index");
186 return columns_[index];
187 }
188 //! \copydoc columnData()
columnData(int index) const189 const ColumnData& columnData(int index) const
190 {
191 return const_cast<Impl*>(this)->columnData(index);
192 }
193
194 //! Container for column data.
195 ColumnList columns_;
196 //! Indentation before the first column.
197 int firstColumnIndent_;
198 //! Indentation before the last column if folded.
199 int foldLastColumnToNextLineIndent_;
200 //! If true, no output has yet been produced.
201 bool bFirstRow_;
202 //! If true, a header will be printed before the first row.
203 bool bPrintHeader_;
204 };
205
Impl()206 TextTableFormatter::Impl::Impl() :
207 firstColumnIndent_(0),
208 foldLastColumnToNextLineIndent_(-1),
209 bFirstRow_(true),
210 bPrintHeader_(false)
211 {
212 }
213
214 /********************************************************************
215 * TextTableFormatter
216 */
217
TextTableFormatter()218 TextTableFormatter::TextTableFormatter() : impl_(new Impl) {}
219
~TextTableFormatter()220 TextTableFormatter::~TextTableFormatter() {}
221
addColumn(const char * title,int width,bool bWrap)222 void TextTableFormatter::addColumn(const char* title, int width, bool bWrap)
223 {
224 if (title != nullptr && title[0] != '\0')
225 {
226 impl_->bPrintHeader_ = true;
227 }
228 impl_->columns_.emplace_back(title, width, bWrap);
229 }
230
setFirstColumnIndent(int indent)231 void TextTableFormatter::setFirstColumnIndent(int indent)
232 {
233 GMX_RELEASE_ASSERT(indent >= 0, "Negative indentation not allowed");
234 impl_->firstColumnIndent_ = indent;
235 }
236
setFoldLastColumnToNextLine(int indent)237 void TextTableFormatter::setFoldLastColumnToNextLine(int indent)
238 {
239 impl_->foldLastColumnToNextLineIndent_ = indent;
240 }
241
didOutput() const242 bool TextTableFormatter::didOutput() const
243 {
244 return !impl_->bFirstRow_;
245 }
246
clear()247 void TextTableFormatter::clear()
248 {
249 Impl::ColumnList::iterator i;
250 for (i = impl_->columns_.begin(); i != impl_->columns_.end(); ++i)
251 {
252 i->firstLine_ = 0;
253 i->lines_.clear();
254 }
255 }
256
addColumnLine(int index,const std::string & text)257 void TextTableFormatter::addColumnLine(int index, const std::string& text)
258 {
259 Impl::ColumnData& column = impl_->columnData(index);
260 TextLineWrapper wrapper;
261 std::vector<std::string> lines(wrapper.wrapToVector(text));
262 column.lines_.insert(column.lines_.end(), lines.begin(), lines.end());
263 }
264
addColumnHelpTextBlock(int index,const HelpWriterContext & context,const std::string & text)265 void TextTableFormatter::addColumnHelpTextBlock(int index,
266 const HelpWriterContext& context,
267 const std::string& text)
268 {
269 Impl::ColumnData& column = impl_->columnData(index);
270 TextLineWrapperSettings settings;
271 // TODO: If in the future, there is actually a coupling between the markup
272 // and the wrapping, this must be postponed into formatRow(), where we do
273 // the actual line wrapping.
274 std::vector<std::string> lines(context.substituteMarkupAndWrapToVector(settings, text));
275 column.lines_.insert(column.lines_.end(), lines.begin(), lines.end());
276 }
277
setColumnFirstLineOffset(int index,int firstLine)278 void TextTableFormatter::setColumnFirstLineOffset(int index, int firstLine)
279 {
280 GMX_ASSERT(firstLine >= 0, "Invalid first line");
281 Impl::ColumnData& column = impl_->columnData(index);
282 column.firstLine_ = firstLine;
283 }
284
formatRow()285 std::string TextTableFormatter::formatRow()
286 {
287 std::string result;
288 Impl::ColumnList::iterator column;
289 // Print a header if this is the first line.
290 if (impl_->bPrintHeader_ && impl_->bFirstRow_)
291 {
292 size_t totalWidth = 0;
293 result.append(impl_->firstColumnIndent_, ' ');
294 for (column = impl_->columns_.begin(); column != impl_->columns_.end(); ++column)
295 {
296 std::string title(column->title());
297 if (column != impl_->columns_.end() - 1)
298 {
299 title.resize(column->width() + 1, ' ');
300 totalWidth += title.length();
301 }
302 else
303 {
304 totalWidth += std::min(column->width(), static_cast<int>(title.length() + 13));
305 }
306 result.append(title);
307 }
308 result.append("\n");
309 result.append(impl_->firstColumnIndent_, ' ');
310 result.append(totalWidth, '-');
311 result.append("\n");
312 }
313
314 // Format all the lines, one column at a time.
315 std::vector<std::string> lines;
316 std::vector<std::string> columnLines;
317 int currentWidth = 0;
318 bool bFoldLastColumn = false;
319 for (column = impl_->columns_.begin(); column != impl_->columns_.end(); ++column)
320 {
321 // Format the column into columnLines.
322 column->startFormatting();
323 columnLines.clear();
324 columnLines.reserve(lines.size());
325 for (size_t line = 0; column->hasLinesRemaining(); ++line)
326 {
327 int columnWidth = column->width();
328 if (line < lines.size())
329 {
330 const int overflow = static_cast<int>(lines[line].length()) - currentWidth;
331 if (overflow > 0)
332 {
333 if (overflow > columnWidth && column->bWrap_)
334 {
335 columnLines.emplace_back();
336 continue;
337 }
338 columnWidth -= overflow;
339 }
340 }
341 columnLines.push_back(column->textForNextLine(columnWidth));
342 }
343 if (column == impl_->columns_.end() - 1 && impl_->foldLastColumnToNextLineIndent_ >= 0
344 && columnLines.size() >= lines.size() + column->lines_.size())
345 {
346 bFoldLastColumn = true;
347 currentWidth += column->width();
348 break;
349 }
350 // Add columnLines into lines.
351 if (lines.size() < columnLines.size())
352 {
353 lines.resize(columnLines.size());
354 }
355 for (size_t line = 0; line < columnLines.size(); ++line)
356 {
357 if (column != impl_->columns_.begin() && !columnLines[line].empty())
358 {
359 lines[line].append(" ");
360 if (static_cast<int>(lines[line].length()) < currentWidth)
361 {
362 lines[line].resize(currentWidth, ' ');
363 }
364 }
365 lines[line].append(columnLines[line]);
366 }
367 currentWidth += column->width() + 1;
368 }
369
370 // Construct the result by concatenating all the lines.
371 std::vector<std::string>::const_iterator line;
372 for (line = lines.begin(); line != lines.end(); ++line)
373 {
374 result.append(impl_->firstColumnIndent_, ' ');
375 result.append(*line);
376 result.append("\n");
377 }
378
379 if (bFoldLastColumn)
380 {
381 Impl::ColumnList::reference lastColumn = impl_->columns_.back();
382 const int totalIndent = impl_->firstColumnIndent_ + impl_->foldLastColumnToNextLineIndent_;
383 lastColumn.startFormatting();
384 currentWidth -= impl_->foldLastColumnToNextLineIndent_;
385 while (lastColumn.hasLinesRemaining())
386 {
387 result.append(totalIndent, ' ');
388 result.append(lastColumn.textForNextLine(currentWidth));
389 result.append("\n");
390 }
391 }
392
393 impl_->bFirstRow_ = false;
394 clear();
395 return result;
396 }
397
398 } // namespace gmx
399