Thea
TextOutputStream.cpp
1 //============================================================================
2 //
3 // This file is part of the Thea toolkit.
4 //
5 // This software is distributed under the BSD license, as detailed in the
6 // accompanying LICENSE.txt file. Portions are derived from other works:
7 // their respective licenses and copyright information are reproduced in
8 // LICENSE.txt and/or in the relevant source files.
9 //
10 // Author: Siddhartha Chaudhuri
11 // First version: 2013
12 //
13 //============================================================================
14 
15 /*
16  ORIGINAL HEADER
17 
18  @file TextOutputStream.cpp
19 
20  @maintainer Morgan McGuire, http://graphics.cs.williams.edu
21  @created 2004-06-21
22  @edited 2010-03-14
23 
24  Copyright 2000-2010, Morgan McGuire.
25  All rights reserved.
26 */
27 
28 #include "TextOutputStream.hpp"
29 #include "FilePath.hpp"
30 #include "FileSystem.hpp"
31 #include "Math.hpp"
32 #include <stdio.h>
33 
34 namespace Thea {
35 
37 : wordWrap(WordWrap::WITHOUT_BREAKING),
38  allowWordWrapInsideDoubleQuotes(false),
39  numColumns(80),
40  spacesPerIndent(4),
41  convertNewlines(true),
42  trueSymbol("true"),
43  falseSymbol("false")
44 {
45 #ifdef THEA_WINDOWS
46  newlineStyle = NewlineStyle::WINDOWS;
47 #else
48  newlineStyle = NewlineStyle::UNIX;
49 #endif
50 }
51 
53 : NamedObject("<memory>"),
54  startingNewLine(true),
55  currentColumn(0),
56  inDQuote(false),
57  path("<memory>"),
58  indentLevel(0),
59  m_ok(true),
60  changed(false)
61 {
62  setOptions(opt);
63 }
64 
65 TextOutputStream::TextOutputStream(std::string const & path_, TextOutputStream::Settings const & opt)
66 : NamedObject(FilePath::objectName(path_)),
67  startingNewLine(true),
68  currentColumn(0),
69  inDQuote(false),
70  path(FileSystem::resolve(path_)),
71  indentLevel(0),
72  m_ok(true),
73  changed(false)
74 {
75  setOptions(opt);
76 
77  // Verify ability to write to disk
78  _commit(false, true);
79 }
80 
82 {
83  if (path != "<memory>")
84  commit(true);
85 }
86 
87 bool
89 {
90  return m_ok;
91 }
92 
93 bool
95 {
96  return _commit(flush, false);
97 }
98 
99 bool
100 TextOutputStream::_commit(bool flush, bool force)
101 {
102  // If there is already an error, the commit fails
103  if (!m_ok)
104  return false;
105 
106  // Is there anything new to write?
107  if (!force && !changed)
108  return true;
109 
110  // Nothing to commit for memory streams
111  if (path == "<memory>")
112  return true;
113 
114  // Make sure the directory exists
115  std::string dir = FilePath::parent(path);
116  if (!FileSystem::exists(dir))
117  if (!FileSystem::createDirectory(dir))
118  {
119  THEA_ERROR << "TextOutputStream: Could not create parent directory of '" << path << "'";
120  m_ok = false;
121  }
122 
123  FILE * f = nullptr;
124  if (m_ok)
125  {
126  f = fopen(path.c_str(), "wb");
127  if (!f)
128  {
129  THEA_ERROR << "TextOutputStream: Could not open \'" + path + "\' for writing";
130  m_ok = false;
131  }
132  }
133 
134  if (m_ok)
135  {
136  fwrite(&data[0], 1, data.size(), f);
137 
138  if (flush)
139  fflush(f);
140 
141  fclose(f);
142 
143  changed = false;
144  }
145 
146  return m_ok;
147 }
148 
149 void
151 {
152  out = std::string(data.data(), data.size());
153 }
154 
155 std::string
157 {
158  std::string str;
159  commitToString(str);
160  return str;
161 }
162 
163 void
164 TextOutputStream::setIndentLevel(int i)
165 {
166  indentLevel = i;
167 
168  // If there were more pops than pushes, don't let that take us below 0 indent.
169  // Don't ever indent more than the number of columns.
170  indentSpaces = Math::clamp(options.spacesPerIndent * indentLevel,
171  0,
172  options.numColumns - 1);
173 }
174 
175 void
176 TextOutputStream::setOptions(Settings const & _opt)
177 {
178  if (options.numColumns < 2)
179  throw Error(getNameStr() + ": Must specify at least 2 columns in options");
180 
181  options = _opt;
182 
183  setIndentLevel(indentLevel);
184  newline = (options.newlineStyle == NewlineStyle::WINDOWS ? "\r\n" : "\n");
185 }
186 
187 void
189 {
190  setIndentLevel(indentLevel + 1);
191 }
192 
193 void
195 {
196  setIndentLevel(indentLevel - 1);
197 }
198 
199 namespace TextOutputStreamInternal {
200 
201 static std::string
202 escape(std::string const & str)
203 {
204  std::string result = "";
205 
206  for (size_t i = 0; i < str.length(); ++i)
207  {
208  char c = str.at(i);
209 
210  switch (c)
211  {
212  case '\0':
213  result += "\\0";
214  break;
215 
216  case '\r':
217  result += "\\r";
218  break;
219 
220  case '\n':
221  result += "\\n";
222  break;
223 
224  case '\t':
225  result += "\\t";
226  break;
227 
228  case '\\':
229  result += "\\\\";
230  break;
231 
232  default:
233  result += c;
234  }
235  }
236 
237  return result;
238 }
239 
240 } // namespace TextOutputStreamInternal
241 
242 void
243 TextOutputStream::writeString(std::string const & str)
244 {
245  // Convert special characters to escape sequences
246  this->printf("\"%s\"", TextOutputStreamInternal::escape(str).c_str());
247 }
248 
249 void
251 {
252  this->printf("%s ", b ? options.trueSymbol.c_str() : options.falseSymbol.c_str());
253 }
254 
255 void
257 {
258  this->printf("%f ", n);
259 }
260 
261 void
262 TextOutputStream::writeSymbol(std::string const & str)
263 {
264  if (!str.empty())
265  {
266  // TODO: check for legal symbols?
267  this->printf("%s ", str.c_str());
268  }
269 }
270 
272  std::string const & a,
273  std::string const & b,
274  std::string const & c,
275  std::string const & d,
276  std::string const & e,
277  std::string const & f)
278 {
279  writeSymbol(a);
280  writeSymbol(b);
281  writeSymbol(c);
282  writeSymbol(d);
283  writeSymbol(e);
284  writeSymbol(f);
285 }
286 
287 void
288 TextOutputStream::printf(char const * fmt, ...)
289 {
290  va_list arg_list;
291  va_start(arg_list, fmt);
292  this->vprintf(fmt, arg_list);
293  va_end(arg_list);
294 }
295 
296 void
297 TextOutputStream::vprintf(char const * fmt, va_list arg_list)
298 {
299  std::string str = vformat(fmt, arg_list);
300  std::string clean;
301  convertNewlines(str, clean);
302  wordWrapIndentAppend(clean);
303 }
304 
305 void
306 TextOutputStream::convertNewlines(std::string const & in, std::string & out)
307 {
308  // TODO: can be significantly optimized in cases where
309  // single characters are copied in order by walking through
310  // the array and copying substrings as needed.
311  if (options.convertNewlines)
312  {
313  out = "";
314 
315  for (size_t i = 0; i < in.size(); ++i)
316  {
317  if (in[i] == '\n')
318  {
319  // Unix newline
320  out += newline;
321  }
322  else if ((in[i] == '\r') && (i + 1 < in.size()) && (in[i + 1] == '\n'))
323  {
324  // Windows newline
325  out += newline;
326  ++i;
327  }
328  else
329  {
330  out += in[i];
331  }
332  }
333  }
334  else
335  {
336  out = in;
337  }
338 }
339 
340 void
342 {
343  for (size_t i = 0; i < newline.size(); ++i)
344  {
345  indentAppend(newline[i]);
346  }
347 }
348 
349 void
351 {
352  for (int i = 0; i < numLines; ++i)
353  {
354  writeNewline();
355  }
356 }
357 
358 void
359 TextOutputStream::wordWrapIndentAppend(std::string const & str)
360 {
361  // TODO: keep track of the last space character we saw so we don't
362  // have to always search.
363  if ((options.wordWrap == WordWrap::NONE) ||
364  (currentColumn + (int)str.size() <= options.numColumns))
365  {
366  // No word-wrapping is needed
367 
368  // Add one character at a time.
369  // TODO: optimize for strings without newlines to add multiple
370  // characters.
371  for (size_t i = 0; i < str.size(); ++i)
372  {
373  indentAppend(str[i]);
374  }
375 
376  return;
377  }
378 
379  // Number of columns to wrap against
380  int cols = options.numColumns - indentSpaces;
381 
382  // Copy forward until we exceed the column size,
383  // and then back up and try to insert newlines as needed.
384  for (size_t i = 0; i < str.size(); ++i)
385  {
386  indentAppend(str[i]);
387 
388  if ((str[i] == '\r') && (i + 1 < str.size()) && (str[i + 1] == '\n'))
389  {
390  // \r\n, we need to hit the \n to enter word wrapping.
391  ++i;
392  indentAppend(str[i]);
393  }
394 
395  if (currentColumn >= cols)
396  {
397  debugAssertM(str[i] != '\n' && str[i] != '\r',
398  "Should never enter word-wrapping on a newline character");
399  // True when we're allowed to treat a space as a space.
400  bool unquotedSpace = options.allowWordWrapInsideDoubleQuotes || ! inDQuote;
401  // Cases:
402  //
403  // 1. Currently in a series of spaces that ends with a newline
404  // strip all spaces and let the newline
405  // flow through.
406  //
407  // 2. Currently in a series of spaces that does not end with a newline
408  // strip all spaces and replace them with single newline
409  //
410  // 3. Not in a series of spaces
411  // search backwards for a space, then execute case 2.
412  // Index of most recent space
413  size_t lastSpace = data.size() - 1;
414  // How far back we had to look for a space
415  size_t k = 0;
416  size_t maxLookBackward = (size_t)(currentColumn - indentSpaces);
417 
418  // Search backwards (from current character), looking for a space.
419  while ((k < maxLookBackward) &&
420  (lastSpace > 0) &&
421  (! ((data[lastSpace] == ' ') && unquotedSpace)))
422  {
423  --lastSpace;
424  ++k;
425 
426  if ((data[lastSpace] == '\"') && !options.allowWordWrapInsideDoubleQuotes)
427  {
428  unquotedSpace = ! unquotedSpace;
429  }
430  }
431 
432  if (k == maxLookBackward)
433  {
434  // We couldn't find a series of spaces
435  if (options.wordWrap == WordWrap::ALWAYS)
436  {
437  // Strip the last character we wrote, force a newline,
438  // and replace the last character;
439  data.pop_back();
440  writeNewline();
441  indentAppend(str[i]);
442  }
443  else
444  {
445  // Must be Settings::WRAP_WITHOUT_BREAKING
446  //
447  // Don't write the newline; we'll come back to
448  // the word wrap code after writing another character
449  }
450  }
451  else
452  {
453  // We found a series of spaces. If they continue
454  // to the new string, strip spaces off both. Otherwise
455  // strip spaces from data only and insert a newline.
456  // Find the start of the spaces. firstSpace is the index of the
457  // first non-space, looking backwards from lastSpace.
458  size_t firstSpace = lastSpace;
459 
460  while ((k < maxLookBackward) &&
461  (firstSpace > 0) &&
462  (data[firstSpace] == ' '))
463  {
464  --firstSpace;
465  ++k;
466  }
467 
468  if (k == maxLookBackward)
469  {
470  ++firstSpace;
471  }
472 
473  if (lastSpace == data.size() - 1)
474  {
475  // Spaces continued up to the new string
476  data.resize(firstSpace + 1);
477  writeNewline();
478 
479  // Delete the spaces from the new string
480  while ((i < str.size() - 1) && (str[i + 1] == ' '))
481  {
482  ++i;
483  }
484  }
485  else
486  {
487  // Spaces were somewhere in the middle of the old string.
488  // replace them with a newline.
489  // Copy over the characters that should be saved
490  Array<char> temp;
491 
492  for (size_t j = lastSpace + 1; j < data.size(); ++j)
493  {
494  char c = data[j];
495 
496  if (c == '\"')
497  {
498  // Undo changes to quoting (they will be re-done
499  // when we paste these characters back on).
500  inDQuote = !inDQuote;
501  }
502 
503  temp.push_back(c);
504  }
505 
506  // Remove those characters and replace with a newline.
507  data.resize(firstSpace + 1);
508  writeNewline();
509 
510  // Write them back
511  for (size_t j = 0; j < temp.size(); ++j)
512  {
513  indentAppend(temp[j]);
514  }
515 
516  // We are now free to continue adding from the
517  // new string, which may or may not begin with spaces.
518  } // if spaces included new string
519  } // if hit indent
520  } // if line exceeded
521  } // iterate over str
522 
523  changed = true;
524 }
525 
526 void
527 TextOutputStream::indentAppend(char c)
528 {
529  if (startingNewLine)
530  {
531  for (int j = 0; j < indentSpaces; ++j)
532  {
533  data.push_back(' ');
534  }
535 
536  startingNewLine = false;
537  currentColumn = indentSpaces;
538  }
539 
540  data.push_back(c);
541 
542  // Don't increment the column count on return character
543  // newline is taken care of below.
544  if (c != '\r')
545  {
546  ++currentColumn;
547  }
548 
549  if (c == '\"')
550  {
551  inDQuote = ! inDQuote;
552  }
553 
554  startingNewLine = (c == '\n');
555 
556  if (startingNewLine)
557  {
558  currentColumn = 0;
559  }
560 
561  changed = true;
562 }
563 
564 } // namespace Thea
int spacesPerIndent
Number of spaces in each indent.
bool ok() const
True if no errors have been encountered.
void writeNewline()
Write a newline character(s).
std::string commitToString()
Commit the buffered data to a string and return it.
~TextOutputStream()
Destructor.
void vprintf(char const *fmt, va_list arg_list)
Use C-style vprintf syntax to write to the stream.
Root namespace for the Thea library.
void printf(char const *fmt,...)
Use C-style printf syntax to write to the stream.
void pushIndent()
Increase indent level by one.
Filesystem operations.
Definition: FileSystem.hpp:27
Settings()
Constructs the default settings.
An object wrapping a name string.
Definition: NamedObject.hpp:58
static bool exists(std::string const &path)
Check if a file or directory exists.
Definition: FileSystem.cpp:26
std::string vformat(char const *fmt, va_list arg_ptr)
Produces a string from arguments in the style of printf, can be called with the argument list from a ...
Definition: StringAlg.cpp:414
T clamp(T const &x, U const &lo, V const &hi)
Clamp a number to lie in the range [lo, hi] (inclusive).
Definition: Math.hpp:328
static bool createDirectory(std::string const &path)
Create a directory, including all necessary parents (equivalent to "mkdir -p").
Definition: FileSystem.cpp:60
std::string wordWrap(std::string const &input, intx num_cols, char const *newline=NEWLINE)
Produces a new string that is the input string wrapped at a certain number of columns (where the line...
Definition: StringAlg.cpp:167
Output configuration optionss.
std::string const & getNameStr() const
Access the name string directly, for efficiency.
Definition: NamedObject.hpp:98
int numColumns
Number of columns for word wrapping.
bool commit(bool flush=true)
Write the buffered data to disk.
void popIndent()
Decrease indent level by one.
void writeNumber(double n)
Write a double precision number, followed by a space.
static std::string parent(std::string const &path)
Get the path to the immediate parent of an object.
Definition: FilePath.cpp:68
void writeSymbol(std::string const &str)
Write a symbol that can be read by TextInputStream, followed by a space.
Word-wrap settings (enum class).
void writeBoolean(bool b)
Write a boolean value, followed by a space.
std::runtime_error Error
An error class.
Definition: Error.hpp:27
std::vector< T, Alloc > Array
Dynamically resizable array.
Definition: Array.hpp:25
TextOutputStream(std::string const &path_, Settings const &settings_=Settings::defaults())
Construct a stream that writes to a file.
Operations on file paths.
Definition: FilePath.hpp:31
void writeSymbols(std::string const &a, std::string const &b="", std::string const &c="", std::string const &d="", std::string const &e="", std::string const &f="")
Convenience function for writing multiple symbols in a row, separated by spaces, e.g. writeSymbols("name", "=").
NewlineStyle newlineStyle
Style of newline used by word wrapping and by (optionsal) conversion.
void writeString(std::string const &str)
Write a quoted string.
void writeNewlines(int numLines)
Write several newline character(s).
void debugAssertM(CondT const &test, MessageT const &msg)
Check if a test condition is true, and immediately abort the program with an error code if not...
Definition: Common.hpp:52