318
|
1 // Copyright Toru Niina 2019.
|
|
2 // Distributed under the MIT License.
|
|
3 #ifndef TOML11_SERIALIZER_HPP
|
|
4 #define TOML11_SERIALIZER_HPP
|
|
5 #include <cmath>
|
|
6 #include <cstdio>
|
|
7
|
|
8 #include <limits>
|
|
9
|
|
10 #if defined(_WIN32)
|
|
11 #include <locale.h>
|
|
12 #elif defined(__APPLE__) || defined(__FreeBSD__)
|
|
13 #include <xlocale.h>
|
|
14 #elif defined(__linux__)
|
|
15 #include <locale.h>
|
|
16 #endif
|
|
17
|
|
18 #include "lexer.hpp"
|
|
19 #include "value.hpp"
|
|
20
|
|
21 namespace toml
|
|
22 {
|
|
23
|
|
24 // This function serialize a key. It checks a string is a bare key and
|
|
25 // escapes special characters if the string is not compatible to a bare key.
|
|
26 // ```cpp
|
|
27 // std::string k("non.bare.key"); // the key itself includes `.`s.
|
|
28 // std::string formatted = toml::format_key(k);
|
|
29 // assert(formatted == "\"non.bare.key\"");
|
|
30 // ```
|
|
31 //
|
|
32 // This function is exposed to make it easy to write a user-defined serializer.
|
|
33 // Since toml restricts characters available in a bare key, generally a string
|
|
34 // should be escaped. But checking whether a string needs to be surrounded by
|
|
35 // a `"` and escaping some special character is boring.
|
|
36 template<typename charT, typename traits, typename Alloc>
|
|
37 std::basic_string<charT, traits, Alloc>
|
|
38 format_key(const std::basic_string<charT, traits, Alloc>& k)
|
|
39 {
|
|
40 if(k.empty())
|
|
41 {
|
|
42 return std::string("\"\"");
|
|
43 }
|
|
44
|
|
45 // check the key can be a bare (unquoted) key
|
|
46 detail::location loc(k, std::vector<char>(k.begin(), k.end()));
|
|
47 detail::lex_unquoted_key::invoke(loc);
|
|
48 if(loc.iter() == loc.end())
|
|
49 {
|
|
50 return k; // all the tokens are consumed. the key is unquoted-key.
|
|
51 }
|
|
52
|
|
53 //if it includes special characters, then format it in a "quoted" key.
|
|
54 std::basic_string<charT, traits, Alloc> serialized("\"");
|
|
55 for(const char c : k)
|
|
56 {
|
|
57 switch(c)
|
|
58 {
|
|
59 case '\\': {serialized += "\\\\"; break;}
|
|
60 case '\"': {serialized += "\\\""; break;}
|
|
61 case '\b': {serialized += "\\b"; break;}
|
|
62 case '\t': {serialized += "\\t"; break;}
|
|
63 case '\f': {serialized += "\\f"; break;}
|
|
64 case '\n': {serialized += "\\n"; break;}
|
|
65 case '\r': {serialized += "\\r"; break;}
|
|
66 default: {
|
|
67 if (c >= 0x00 && c < 0x20)
|
|
68 {
|
|
69 std::array<char, 7> buf;
|
|
70 std::snprintf(buf.data(), buf.size(), "\\u00%02x", static_cast<int>(c));
|
|
71 serialized += buf.data();
|
|
72 }
|
|
73 else
|
|
74 {
|
|
75 serialized += c;
|
|
76 }
|
|
77 break;
|
|
78 }
|
|
79 }
|
|
80 }
|
|
81 serialized += "\"";
|
|
82 return serialized;
|
|
83 }
|
|
84
|
|
85 template<typename charT, typename traits, typename Alloc>
|
|
86 std::basic_string<charT, traits, Alloc>
|
|
87 format_keys(const std::vector<std::basic_string<charT, traits, Alloc>>& keys)
|
|
88 {
|
|
89 if(keys.empty())
|
|
90 {
|
|
91 return std::string("\"\"");
|
|
92 }
|
|
93
|
|
94 std::basic_string<charT, traits, Alloc> serialized;
|
|
95 for(const auto& ky : keys)
|
|
96 {
|
|
97 serialized += format_key(ky);
|
|
98 serialized += charT('.');
|
|
99 }
|
|
100 serialized.pop_back(); // remove the last dot '.'
|
|
101 return serialized;
|
|
102 }
|
|
103
|
|
104 template<typename Value>
|
|
105 struct serializer
|
|
106 {
|
|
107 static_assert(detail::is_basic_value<Value>::value,
|
|
108 "toml::serializer is for toml::value and its variants, "
|
|
109 "toml::basic_value<...>.");
|
|
110
|
|
111 using value_type = Value;
|
|
112 using key_type = typename value_type::key_type ;
|
|
113 using comment_type = typename value_type::comment_type ;
|
|
114 using boolean_type = typename value_type::boolean_type ;
|
|
115 using integer_type = typename value_type::integer_type ;
|
|
116 using floating_type = typename value_type::floating_type ;
|
|
117 using string_type = typename value_type::string_type ;
|
|
118 using local_time_type = typename value_type::local_time_type ;
|
|
119 using local_date_type = typename value_type::local_date_type ;
|
|
120 using local_datetime_type = typename value_type::local_datetime_type ;
|
|
121 using offset_datetime_type = typename value_type::offset_datetime_type;
|
|
122 using array_type = typename value_type::array_type ;
|
|
123 using table_type = typename value_type::table_type ;
|
|
124
|
|
125 serializer(const std::size_t w = 80u,
|
|
126 const int float_prec = std::numeric_limits<toml::floating>::max_digits10,
|
|
127 const bool can_be_inlined = false,
|
|
128 const bool no_comment = false,
|
|
129 std::vector<toml::key> ks = {},
|
|
130 const bool value_has_comment = false)
|
|
131 : can_be_inlined_(can_be_inlined), no_comment_(no_comment),
|
|
132 value_has_comment_(value_has_comment && !no_comment),
|
|
133 float_prec_(float_prec), width_(w), keys_(std::move(ks))
|
|
134 {}
|
|
135 ~serializer() = default;
|
|
136
|
|
137 std::string operator()(const boolean_type& b) const
|
|
138 {
|
|
139 return b ? "true" : "false";
|
|
140 }
|
|
141 std::string operator()(const integer_type i) const
|
|
142 {
|
|
143 #if defined(_WIN32)
|
|
144 _configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
|
|
145 const std::string original_locale(setlocale(LC_NUMERIC, nullptr));
|
|
146 setlocale(LC_NUMERIC, "C");
|
|
147 #elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__linux__)
|
|
148 const auto c_locale = newlocale(LC_NUMERIC_MASK, "C", locale_t(0));
|
|
149 locale_t original_locale(0);
|
|
150 if(c_locale != locale_t(0))
|
|
151 {
|
|
152 original_locale = uselocale(c_locale);
|
|
153 }
|
|
154 #endif
|
|
155
|
|
156 const auto str = std::to_string(i);
|
|
157
|
|
158 #if defined(_WIN32)
|
|
159 setlocale(LC_NUMERIC, original_locale.c_str());
|
|
160 _configthreadlocale(_DISABLE_PER_THREAD_LOCALE);
|
|
161 #elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__linux__)
|
|
162 if(original_locale != locale_t(0))
|
|
163 {
|
|
164 uselocale(original_locale);
|
|
165 }
|
|
166 #endif
|
|
167 return str;
|
|
168 }
|
|
169 std::string operator()(const floating_type f) const
|
|
170 {
|
|
171 if(std::isnan(f))
|
|
172 {
|
|
173 if(std::signbit(f))
|
|
174 {
|
|
175 return std::string("-nan");
|
|
176 }
|
|
177 else
|
|
178 {
|
|
179 return std::string("nan");
|
|
180 }
|
|
181 }
|
|
182 else if(!std::isfinite(f))
|
|
183 {
|
|
184 if(std::signbit(f))
|
|
185 {
|
|
186 return std::string("-inf");
|
|
187 }
|
|
188 else
|
|
189 {
|
|
190 return std::string("inf");
|
|
191 }
|
|
192 }
|
|
193
|
|
194 // set locale to "C".
|
|
195 // To make it thread-local, we use OS-specific features.
|
|
196 // If we set process-global locale, it can break other thread that also
|
|
197 // outputs something simultaneously.
|
|
198 #if defined(_WIN32)
|
|
199 _configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
|
|
200 const std::string original_locale(setlocale(LC_NUMERIC, nullptr));
|
|
201 setlocale(LC_NUMERIC, "C");
|
|
202 #elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__linux__)
|
|
203 const auto c_locale = newlocale(LC_NUMERIC_MASK, "C", locale_t(0));
|
|
204 locale_t original_locale(0);
|
|
205 if(c_locale != locale_t(0))
|
|
206 {
|
|
207 original_locale = uselocale(c_locale);
|
|
208 }
|
|
209 #endif
|
|
210
|
|
211 const auto fmt = "%.*g";
|
|
212 const auto bsz = std::snprintf(nullptr, 0, fmt, this->float_prec_, f);
|
|
213 // +1 for null character(\0)
|
|
214 std::vector<char> buf(static_cast<std::size_t>(bsz + 1), '\0');
|
|
215 std::snprintf(buf.data(), buf.size(), fmt, this->float_prec_, f);
|
|
216
|
|
217 // restore the original locale
|
|
218 #if defined(_WIN32)
|
|
219 setlocale(LC_NUMERIC, original_locale.c_str());
|
|
220 _configthreadlocale(_DISABLE_PER_THREAD_LOCALE);
|
|
221 #elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__linux__)
|
|
222 if(original_locale != locale_t(0))
|
|
223 {
|
|
224 uselocale(original_locale);
|
|
225 }
|
|
226 #endif
|
|
227
|
|
228 std::string token(buf.begin(), std::prev(buf.end()));
|
|
229 if(!token.empty() && token.back() == '.') // 1. => 1.0
|
|
230 {
|
|
231 token += '0';
|
|
232 }
|
|
233
|
|
234 const auto e = std::find_if(
|
|
235 token.cbegin(), token.cend(), [](const char c) noexcept -> bool {
|
|
236 return c == 'e' || c == 'E';
|
|
237 });
|
|
238 const auto has_exponent = (token.cend() != e);
|
|
239 const auto has_fraction = (token.cend() != std::find(
|
|
240 token.cbegin(), token.cend(), '.'));
|
|
241
|
|
242 if(!has_exponent && !has_fraction)
|
|
243 {
|
|
244 // the resulting value does not have any float specific part!
|
|
245 token += ".0";
|
|
246 }
|
|
247 return token;
|
|
248 }
|
|
249 std::string operator()(const string_type& s) const
|
|
250 {
|
|
251 if(s.kind == string_t::basic)
|
|
252 {
|
|
253 if((std::find(s.str.cbegin(), s.str.cend(), '\n') != s.str.cend() ||
|
|
254 std::find(s.str.cbegin(), s.str.cend(), '\"') != s.str.cend()) &&
|
|
255 this->width_ != (std::numeric_limits<std::size_t>::max)())
|
|
256 {
|
|
257 // if linefeed or double-quote is contained,
|
|
258 // make it multiline basic string.
|
|
259 const auto escaped = this->escape_ml_basic_string(s.str);
|
|
260 std::string open("\"\"\"");
|
|
261 std::string close("\"\"\"");
|
|
262 if(escaped.find('\n') != std::string::npos ||
|
|
263 this->width_ < escaped.size() + 6)
|
|
264 {
|
|
265 // if the string body contains newline or is enough long,
|
|
266 // add newlines after and before delimiters.
|
|
267 open += "\n";
|
|
268 close = std::string("\\\n") + close;
|
|
269 }
|
|
270 return open + escaped + close;
|
|
271 }
|
|
272
|
|
273 // no linefeed. try to make it oneline-string.
|
|
274 std::string oneline = this->escape_basic_string(s.str);
|
|
275 if(oneline.size() + 2 < width_ || width_ < 2)
|
|
276 {
|
|
277 const std::string quote("\"");
|
|
278 return quote + oneline + quote;
|
|
279 }
|
|
280
|
|
281 // the line is too long compared to the specified width.
|
|
282 // split it into multiple lines.
|
|
283 std::string token("\"\"\"\n");
|
|
284 while(!oneline.empty())
|
|
285 {
|
|
286 if(oneline.size() < width_)
|
|
287 {
|
|
288 token += oneline;
|
|
289 oneline.clear();
|
|
290 }
|
|
291 else if(oneline.at(width_-2) == '\\')
|
|
292 {
|
|
293 token += oneline.substr(0, width_-2);
|
|
294 token += "\\\n";
|
|
295 oneline.erase(0, width_-2);
|
|
296 }
|
|
297 else
|
|
298 {
|
|
299 token += oneline.substr(0, width_-1);
|
|
300 token += "\\\n";
|
|
301 oneline.erase(0, width_-1);
|
|
302 }
|
|
303 }
|
|
304 return token + std::string("\\\n\"\"\"");
|
|
305 }
|
|
306 else // the string `s` is literal-string.
|
|
307 {
|
|
308 if(std::find(s.str.cbegin(), s.str.cend(), '\n') != s.str.cend() ||
|
|
309 std::find(s.str.cbegin(), s.str.cend(), '\'') != s.str.cend() )
|
|
310 {
|
|
311 std::string open("'''");
|
|
312 if(this->width_ + 6 < s.str.size())
|
|
313 {
|
|
314 open += '\n'; // the first newline is ignored by TOML spec
|
|
315 }
|
|
316 const std::string close("'''");
|
|
317 return open + s.str + close;
|
|
318 }
|
|
319 else
|
|
320 {
|
|
321 const std::string quote("'");
|
|
322 return quote + s.str + quote;
|
|
323 }
|
|
324 }
|
|
325 }
|
|
326
|
|
327 std::string operator()(const local_date_type& d) const
|
|
328 {
|
|
329 std::ostringstream oss;
|
|
330 oss << d;
|
|
331 return oss.str();
|
|
332 }
|
|
333 std::string operator()(const local_time_type& t) const
|
|
334 {
|
|
335 std::ostringstream oss;
|
|
336 oss << t;
|
|
337 return oss.str();
|
|
338 }
|
|
339 std::string operator()(const local_datetime_type& dt) const
|
|
340 {
|
|
341 std::ostringstream oss;
|
|
342 oss << dt;
|
|
343 return oss.str();
|
|
344 }
|
|
345 std::string operator()(const offset_datetime_type& odt) const
|
|
346 {
|
|
347 std::ostringstream oss;
|
|
348 oss << odt;
|
|
349 return oss.str();
|
|
350 }
|
|
351
|
|
352 std::string operator()(const array_type& v) const
|
|
353 {
|
|
354 if(v.empty())
|
|
355 {
|
|
356 return std::string("[]");
|
|
357 }
|
|
358 if(this->is_array_of_tables(v))
|
|
359 {
|
|
360 return make_array_of_tables(v);
|
|
361 }
|
|
362
|
|
363 // not an array of tables. normal array.
|
|
364 // first, try to make it inline if none of the elements have a comment.
|
|
365 if( ! this->has_comment_inside(v))
|
|
366 {
|
|
367 const auto inl = this->make_inline_array(v);
|
|
368 if(inl.size() < this->width_ &&
|
|
369 std::find(inl.cbegin(), inl.cend(), '\n') == inl.cend())
|
|
370 {
|
|
371 return inl;
|
|
372 }
|
|
373 }
|
|
374
|
|
375 // if the length exceeds this->width_, print multiline array.
|
|
376 // key = [
|
|
377 // # ...
|
|
378 // 42,
|
|
379 // ...
|
|
380 // ]
|
|
381 std::string token;
|
|
382 std::string current_line;
|
|
383 token += "[\n";
|
|
384 for(const auto& item : v)
|
|
385 {
|
|
386 if( ! item.comments().empty() && !no_comment_)
|
|
387 {
|
|
388 // if comment exists, the element must be the only element in the line.
|
|
389 // e.g. the following is not allowed.
|
|
390 // ```toml
|
|
391 // array = [
|
|
392 // # comment for what?
|
|
393 // 1, 2, 3, 4, 5
|
|
394 // ]
|
|
395 // ```
|
|
396 if(!current_line.empty())
|
|
397 {
|
|
398 if(current_line.back() != '\n')
|
|
399 {
|
|
400 current_line += '\n';
|
|
401 }
|
|
402 token += current_line;
|
|
403 current_line.clear();
|
|
404 }
|
|
405 for(const auto& c : item.comments())
|
|
406 {
|
|
407 token += '#';
|
|
408 token += c;
|
|
409 token += '\n';
|
|
410 }
|
|
411 token += toml::visit(*this, item);
|
|
412 if(!token.empty() && token.back() == '\n') {token.pop_back();}
|
|
413 token += ",\n";
|
|
414 continue;
|
|
415 }
|
|
416 std::string next_elem;
|
|
417 if(item.is_table())
|
|
418 {
|
|
419 serializer ser(*this);
|
|
420 ser.can_be_inlined_ = true;
|
|
421 ser.width_ = (std::numeric_limits<std::size_t>::max)();
|
|
422 next_elem += toml::visit(ser, item);
|
|
423 }
|
|
424 else
|
|
425 {
|
|
426 next_elem += toml::visit(*this, item);
|
|
427 }
|
|
428
|
|
429 // comma before newline.
|
|
430 if(!next_elem.empty() && next_elem.back() == '\n') {next_elem.pop_back();}
|
|
431
|
|
432 // if current line does not exceeds the width limit, continue.
|
|
433 if(current_line.size() + next_elem.size() + 1 < this->width_)
|
|
434 {
|
|
435 current_line += next_elem;
|
|
436 current_line += ',';
|
|
437 }
|
|
438 else if(current_line.empty())
|
|
439 {
|
|
440 // if current line was empty, force put the next_elem because
|
|
441 // next_elem is not splittable
|
|
442 token += next_elem;
|
|
443 token += ",\n";
|
|
444 // current_line is kept empty
|
|
445 }
|
|
446 else // reset current_line
|
|
447 {
|
|
448 assert(current_line.back() == ',');
|
|
449 token += current_line;
|
|
450 token += '\n';
|
|
451 current_line = next_elem;
|
|
452 current_line += ',';
|
|
453 }
|
|
454 }
|
|
455 if(!current_line.empty())
|
|
456 {
|
|
457 if(!current_line.empty() && current_line.back() != '\n')
|
|
458 {
|
|
459 current_line += '\n';
|
|
460 }
|
|
461 token += current_line;
|
|
462 }
|
|
463 token += "]\n";
|
|
464 return token;
|
|
465 }
|
|
466
|
|
467 // templatize for any table-like container
|
|
468 std::string operator()(const table_type& v) const
|
|
469 {
|
|
470 // if an element has a comment, then it can't be inlined.
|
|
471 // table = {# how can we write a comment for this? key = "value"}
|
|
472 if(this->can_be_inlined_ && !(this->has_comment_inside(v)))
|
|
473 {
|
|
474 std::string token;
|
|
475 if(!this->keys_.empty())
|
|
476 {
|
|
477 token += format_key(this->keys_.back());
|
|
478 token += " = ";
|
|
479 }
|
|
480 token += this->make_inline_table(v);
|
|
481 if(token.size() < this->width_ &&
|
|
482 token.end() == std::find(token.begin(), token.end(), '\n'))
|
|
483 {
|
|
484 return token;
|
|
485 }
|
|
486 }
|
|
487
|
|
488 std::string token;
|
|
489 if(!keys_.empty())
|
|
490 {
|
|
491 token += '[';
|
|
492 token += format_keys(keys_);
|
|
493 token += "]\n";
|
|
494 }
|
|
495 token += this->make_multiline_table(v);
|
|
496 return token;
|
|
497 }
|
|
498
|
|
499 private:
|
|
500
|
|
501 std::string escape_basic_string(const std::string& s) const
|
|
502 {
|
|
503 //XXX assuming `s` is a valid utf-8 sequence.
|
|
504 std::string retval;
|
|
505 for(const char c : s)
|
|
506 {
|
|
507 switch(c)
|
|
508 {
|
|
509 case '\\': {retval += "\\\\"; break;}
|
|
510 case '\"': {retval += "\\\""; break;}
|
|
511 case '\b': {retval += "\\b"; break;}
|
|
512 case '\t': {retval += "\\t"; break;}
|
|
513 case '\f': {retval += "\\f"; break;}
|
|
514 case '\n': {retval += "\\n"; break;}
|
|
515 case '\r': {retval += "\\r"; break;}
|
|
516 default :
|
|
517 {
|
|
518 if((0x00 <= c && c <= 0x08) || (0x0A <= c && c <= 0x1F) || c == 0x7F)
|
|
519 {
|
|
520 retval += "\\u00";
|
|
521 retval += char(48 + (c / 16));
|
|
522 retval += char((c % 16 < 10 ? 48 : 55) + (c % 16));
|
|
523 }
|
|
524 else
|
|
525 {
|
|
526 retval += c;
|
|
527 }
|
|
528 }
|
|
529 }
|
|
530 }
|
|
531 return retval;
|
|
532 }
|
|
533
|
|
534 std::string escape_ml_basic_string(const std::string& s) const
|
|
535 {
|
|
536 std::string retval;
|
|
537 for(auto i=s.cbegin(), e=s.cend(); i!=e; ++i)
|
|
538 {
|
|
539 switch(*i)
|
|
540 {
|
|
541 case '\\': {retval += "\\\\"; break;}
|
|
542 // One or two consecutive "s are allowed.
|
|
543 // Later we will check there are no three consecutive "s.
|
|
544 // case '\"': {retval += "\\\""; break;}
|
|
545 case '\b': {retval += "\\b"; break;}
|
|
546 case '\t': {retval += "\\t"; break;}
|
|
547 case '\f': {retval += "\\f"; break;}
|
|
548 case '\n': {retval += "\n"; break;}
|
|
549 case '\r':
|
|
550 {
|
|
551 if(std::next(i) != e && *std::next(i) == '\n')
|
|
552 {
|
|
553 retval += "\r\n";
|
|
554 ++i;
|
|
555 }
|
|
556 else
|
|
557 {
|
|
558 retval += "\\r";
|
|
559 }
|
|
560 break;
|
|
561 }
|
|
562 default :
|
|
563 {
|
|
564 const auto c = *i;
|
|
565 if((0x00 <= c && c <= 0x08) || (0x0A <= c && c <= 0x1F) || c == 0x7F)
|
|
566 {
|
|
567 retval += "\\u00";
|
|
568 retval += char(48 + (c / 16));
|
|
569 retval += char((c % 16 < 10 ? 48 : 55) + (c % 16));
|
|
570 }
|
|
571 else
|
|
572 {
|
|
573 retval += c;
|
|
574 }
|
|
575 }
|
|
576
|
|
577 }
|
|
578 }
|
|
579 // Only 1 or 2 consecutive `"`s are allowed in multiline basic string.
|
|
580 // 3 consecutive `"`s are considered as a closing delimiter.
|
|
581 // We need to check if there are 3 or more consecutive `"`s and insert
|
|
582 // backslash to break them down into several short `"`s like the `str6`
|
|
583 // in the following example.
|
|
584 // ```toml
|
|
585 // str4 = """Here are two quotation marks: "". Simple enough."""
|
|
586 // # str5 = """Here are three quotation marks: """.""" # INVALID
|
|
587 // str5 = """Here are three quotation marks: ""\"."""
|
|
588 // str6 = """Here are fifteen quotation marks: ""\"""\"""\"""\"""\"."""
|
|
589 // ```
|
|
590 auto found_3_quotes = retval.find("\"\"\"");
|
|
591 while(found_3_quotes != std::string::npos)
|
|
592 {
|
|
593 retval.replace(found_3_quotes, 3, "\"\"\\\"");
|
|
594 found_3_quotes = retval.find("\"\"\"");
|
|
595 }
|
|
596 return retval;
|
|
597 }
|
|
598
|
|
599 // if an element of a table or an array has a comment, it cannot be inlined.
|
|
600 bool has_comment_inside(const array_type& a) const noexcept
|
|
601 {
|
|
602 // if no_comment is set, comments would not be written.
|
|
603 if(this->no_comment_) {return false;}
|
|
604
|
|
605 for(const auto& v : a)
|
|
606 {
|
|
607 if(!v.comments().empty()) {return true;}
|
|
608 }
|
|
609 return false;
|
|
610 }
|
|
611 bool has_comment_inside(const table_type& t) const noexcept
|
|
612 {
|
|
613 // if no_comment is set, comments would not be written.
|
|
614 if(this->no_comment_) {return false;}
|
|
615
|
|
616 for(const auto& kv : t)
|
|
617 {
|
|
618 if(!kv.second.comments().empty()) {return true;}
|
|
619 }
|
|
620 return false;
|
|
621 }
|
|
622
|
|
623 std::string make_inline_array(const array_type& v) const
|
|
624 {
|
|
625 assert(!has_comment_inside(v));
|
|
626 std::string token;
|
|
627 token += '[';
|
|
628 bool is_first = true;
|
|
629 for(const auto& item : v)
|
|
630 {
|
|
631 if(is_first) {is_first = false;} else {token += ',';}
|
|
632 token += visit(serializer(
|
|
633 (std::numeric_limits<std::size_t>::max)(), this->float_prec_,
|
|
634 /* inlined */ true, /*no comment*/ false, /*keys*/ {},
|
|
635 /*has_comment*/ !item.comments().empty()), item);
|
|
636 }
|
|
637 token += ']';
|
|
638 return token;
|
|
639 }
|
|
640
|
|
641 std::string make_inline_table(const table_type& v) const
|
|
642 {
|
|
643 assert(!has_comment_inside(v));
|
|
644 assert(this->can_be_inlined_);
|
|
645 std::string token;
|
|
646 token += '{';
|
|
647 bool is_first = true;
|
|
648 for(const auto& kv : v)
|
|
649 {
|
|
650 // in inline tables, trailing comma is not allowed (toml-lang #569).
|
|
651 if(is_first) {is_first = false;} else {token += ',';}
|
|
652 token += format_key(kv.first);
|
|
653 token += '=';
|
|
654 token += visit(serializer(
|
|
655 (std::numeric_limits<std::size_t>::max)(), this->float_prec_,
|
|
656 /* inlined */ true, /*no comment*/ false, /*keys*/ {},
|
|
657 /*has_comment*/ !kv.second.comments().empty()), kv.second);
|
|
658 }
|
|
659 token += '}';
|
|
660 return token;
|
|
661 }
|
|
662
|
|
663 std::string make_multiline_table(const table_type& v) const
|
|
664 {
|
|
665 std::string token;
|
|
666
|
|
667 // print non-table elements first.
|
|
668 // ```toml
|
|
669 // [foo] # a table we're writing now here
|
|
670 // key = "value" # <- non-table element, "key"
|
|
671 // # ...
|
|
672 // [foo.bar] # <- table element, "bar"
|
|
673 // ```
|
|
674 // because after printing [foo.bar], the remaining non-table values will
|
|
675 // be assigned into [foo.bar], not [foo]. Those values should be printed
|
|
676 // earlier.
|
|
677 for(const auto& kv : v)
|
|
678 {
|
|
679 if(kv.second.is_table() || is_array_of_tables(kv.second))
|
|
680 {
|
|
681 continue;
|
|
682 }
|
|
683
|
|
684 token += write_comments(kv.second);
|
|
685
|
|
686 const auto key_and_sep = format_key(kv.first) + " = ";
|
|
687 const auto residual_width = (this->width_ > key_and_sep.size()) ?
|
|
688 this->width_ - key_and_sep.size() : 0;
|
|
689 token += key_and_sep;
|
|
690 token += visit(serializer(residual_width, this->float_prec_,
|
|
691 /*can be inlined*/ true, /*no comment*/ false, /*keys*/ {},
|
|
692 /*has_comment*/ !kv.second.comments().empty()), kv.second);
|
|
693
|
|
694 if(token.back() != '\n')
|
|
695 {
|
|
696 token += '\n';
|
|
697 }
|
|
698 }
|
|
699
|
|
700 // normal tables / array of tables
|
|
701
|
|
702 // after multiline table appeared, the other tables cannot be inline
|
|
703 // because the table would be assigned into the table.
|
|
704 // [foo]
|
|
705 // ...
|
|
706 // bar = {...} # <- bar will be a member of [foo].
|
|
707 bool multiline_table_printed = false;
|
|
708 for(const auto& kv : v)
|
|
709 {
|
|
710 if(!kv.second.is_table() && !is_array_of_tables(kv.second))
|
|
711 {
|
|
712 continue; // other stuff are already serialized. skip them.
|
|
713 }
|
|
714
|
|
715 std::vector<toml::key> ks(this->keys_);
|
|
716 ks.push_back(kv.first);
|
|
717
|
|
718 auto tmp = visit(serializer(this->width_, this->float_prec_,
|
|
719 !multiline_table_printed, this->no_comment_, ks,
|
|
720 /*has_comment*/ !kv.second.comments().empty()), kv.second);
|
|
721
|
|
722 // If it is the first time to print a multi-line table, it would be
|
|
723 // helpful to separate normal key-value pair and subtables by a
|
|
724 // newline.
|
|
725 // (this checks if the current key-value pair contains newlines.
|
|
726 // but it is not perfect because multi-line string can also contain
|
|
727 // a newline. in such a case, an empty line will be written) TODO
|
|
728 if((!multiline_table_printed) &&
|
|
729 std::find(tmp.cbegin(), tmp.cend(), '\n') != tmp.cend())
|
|
730 {
|
|
731 multiline_table_printed = true;
|
|
732 token += '\n'; // separate key-value pairs and subtables
|
|
733
|
|
734 token += write_comments(kv.second);
|
|
735 token += tmp;
|
|
736
|
|
737 // care about recursive tables (all tables in each level prints
|
|
738 // newline and there will be a full of newlines)
|
|
739 if(tmp.substr(tmp.size() - 2, 2) != "\n\n" &&
|
|
740 tmp.substr(tmp.size() - 4, 4) != "\r\n\r\n" )
|
|
741 {
|
|
742 token += '\n';
|
|
743 }
|
|
744 }
|
|
745 else
|
|
746 {
|
|
747 token += write_comments(kv.second);
|
|
748 token += tmp;
|
|
749 token += '\n';
|
|
750 }
|
|
751 }
|
|
752 return token;
|
|
753 }
|
|
754
|
|
755 std::string make_array_of_tables(const array_type& v) const
|
|
756 {
|
|
757 // if it's not inlined, we need to add `[[table.key]]`.
|
|
758 // but if it can be inlined, we can format it as the following.
|
|
759 // ```
|
|
760 // table.key = [
|
|
761 // {...},
|
|
762 // # comment
|
|
763 // {...},
|
|
764 // ]
|
|
765 // ```
|
|
766 // This function checks if inlinization is possible or not, and then
|
|
767 // format the array-of-tables in a proper way.
|
|
768 //
|
|
769 // Note about comments:
|
|
770 //
|
|
771 // If the array itself has a comment (value_has_comment_ == true), we
|
|
772 // should try to make it inline.
|
|
773 // ```toml
|
|
774 // # comment about array
|
|
775 // array = [
|
|
776 // # comment about table element
|
|
777 // {of = "table"}
|
|
778 // ]
|
|
779 // ```
|
|
780 // If it is formatted as a multiline table, the two comments becomes
|
|
781 // indistinguishable.
|
|
782 // ```toml
|
|
783 // # comment about array
|
|
784 // # comment about table element
|
|
785 // [[array]]
|
|
786 // of = "table"
|
|
787 // ```
|
|
788 // So we need to try to make it inline, and it force-inlines regardless
|
|
789 // of the line width limit.
|
|
790 // It may fail if the element of a table has comment. In that case,
|
|
791 // the array-of-tables will be formatted as a multiline table.
|
|
792 if(this->can_be_inlined_ || this->value_has_comment_)
|
|
793 {
|
|
794 std::string token;
|
|
795 if(!keys_.empty())
|
|
796 {
|
|
797 token += format_key(keys_.back());
|
|
798 token += " = ";
|
|
799 }
|
|
800
|
|
801 bool failed = false;
|
|
802 token += "[\n";
|
|
803 for(const auto& item : v)
|
|
804 {
|
|
805 // if an element of the table has a comment, the table
|
|
806 // cannot be inlined.
|
|
807 if(this->has_comment_inside(item.as_table()))
|
|
808 {
|
|
809 failed = true;
|
|
810 break;
|
|
811 }
|
|
812 // write comments for the table itself
|
|
813 token += write_comments(item);
|
|
814
|
|
815 const auto t = this->make_inline_table(item.as_table());
|
|
816
|
|
817 if(t.size() + 1 > width_ || // +1 for the last comma {...},
|
|
818 std::find(t.cbegin(), t.cend(), '\n') != t.cend())
|
|
819 {
|
|
820 // if the value itself has a comment, ignore the line width limit
|
|
821 if( ! this->value_has_comment_)
|
|
822 {
|
|
823 failed = true;
|
|
824 break;
|
|
825 }
|
|
826 }
|
|
827 token += t;
|
|
828 token += ",\n";
|
|
829 }
|
|
830
|
|
831 if( ! failed)
|
|
832 {
|
|
833 token += "]\n";
|
|
834 return token;
|
|
835 }
|
|
836 // if failed, serialize them as [[array.of.tables]].
|
|
837 }
|
|
838
|
|
839 std::string token;
|
|
840 for(const auto& item : v)
|
|
841 {
|
|
842 token += write_comments(item);
|
|
843 token += "[[";
|
|
844 token += format_keys(keys_);
|
|
845 token += "]]\n";
|
|
846 token += this->make_multiline_table(item.as_table());
|
|
847 }
|
|
848 return token;
|
|
849 }
|
|
850
|
|
851 std::string write_comments(const value_type& v) const
|
|
852 {
|
|
853 std::string retval;
|
|
854 if(this->no_comment_) {return retval;}
|
|
855
|
|
856 for(const auto& c : v.comments())
|
|
857 {
|
|
858 retval += '#';
|
|
859 retval += c;
|
|
860 retval += '\n';
|
|
861 }
|
|
862 return retval;
|
|
863 }
|
|
864
|
|
865 bool is_array_of_tables(const value_type& v) const
|
|
866 {
|
|
867 if(!v.is_array() || v.as_array().empty()) {return false;}
|
|
868 return is_array_of_tables(v.as_array());
|
|
869 }
|
|
870 bool is_array_of_tables(const array_type& v) const
|
|
871 {
|
|
872 // Since TOML v0.5.0, heterogeneous arrays are allowed. So we need to
|
|
873 // check all the element in an array to check if the array is an array
|
|
874 // of tables.
|
|
875 return std::all_of(v.begin(), v.end(), [](const value_type& elem) {
|
|
876 return elem.is_table();
|
|
877 });
|
|
878 }
|
|
879
|
|
880 private:
|
|
881
|
|
882 bool can_be_inlined_;
|
|
883 bool no_comment_;
|
|
884 bool value_has_comment_;
|
|
885 int float_prec_;
|
|
886 std::size_t width_;
|
|
887 std::vector<toml::key> keys_;
|
|
888 };
|
|
889
|
|
890 template<typename C,
|
|
891 template<typename ...> class M, template<typename ...> class V>
|
|
892 std::string
|
|
893 format(const basic_value<C, M, V>& v, std::size_t w = 80u,
|
|
894 int fprec = std::numeric_limits<toml::floating>::max_digits10,
|
|
895 bool no_comment = false, bool force_inline = false)
|
|
896 {
|
|
897 using value_type = basic_value<C, M, V>;
|
|
898 // if value is a table, it is considered to be a root object.
|
|
899 // the root object can't be an inline table.
|
|
900 if(v.is_table())
|
|
901 {
|
|
902 std::ostringstream oss;
|
|
903 if(!v.comments().empty())
|
|
904 {
|
|
905 oss << v.comments();
|
|
906 oss << '\n'; // to split the file comment from the first element
|
|
907 }
|
|
908 const auto serialized = visit(serializer<value_type>(w, fprec, false, no_comment), v);
|
|
909 oss << serialized;
|
|
910 return oss.str();
|
|
911 }
|
|
912 return visit(serializer<value_type>(w, fprec, force_inline), v);
|
|
913 }
|
|
914
|
|
915 namespace detail
|
|
916 {
|
|
917 template<typename charT, typename traits>
|
|
918 int comment_index(std::basic_ostream<charT, traits>&)
|
|
919 {
|
|
920 static const int index = std::ios_base::xalloc();
|
|
921 return index;
|
|
922 }
|
|
923 } // detail
|
|
924
|
|
925 template<typename charT, typename traits>
|
|
926 std::basic_ostream<charT, traits>&
|
|
927 nocomment(std::basic_ostream<charT, traits>& os)
|
|
928 {
|
|
929 // by default, it is zero. and by default, it shows comments.
|
|
930 os.iword(detail::comment_index(os)) = 1;
|
|
931 return os;
|
|
932 }
|
|
933
|
|
934 template<typename charT, typename traits>
|
|
935 std::basic_ostream<charT, traits>&
|
|
936 showcomment(std::basic_ostream<charT, traits>& os)
|
|
937 {
|
|
938 // by default, it is zero. and by default, it shows comments.
|
|
939 os.iword(detail::comment_index(os)) = 0;
|
|
940 return os;
|
|
941 }
|
|
942
|
|
943 template<typename charT, typename traits, typename C,
|
|
944 template<typename ...> class M, template<typename ...> class V>
|
|
945 std::basic_ostream<charT, traits>&
|
|
946 operator<<(std::basic_ostream<charT, traits>& os, const basic_value<C, M, V>& v)
|
|
947 {
|
|
948 using value_type = basic_value<C, M, V>;
|
|
949
|
|
950 // get status of std::setw().
|
|
951 const auto w = static_cast<std::size_t>(os.width());
|
|
952 const int fprec = static_cast<int>(os.precision());
|
|
953 os.width(0);
|
|
954
|
|
955 // by default, iword is initialized by 0. And by default, toml11 outputs
|
|
956 // comments. So `0` means showcomment. 1 means nocommnet.
|
|
957 const bool no_comment = (1 == os.iword(detail::comment_index(os)));
|
|
958
|
|
959 if(!no_comment && v.is_table() && !v.comments().empty())
|
|
960 {
|
|
961 os << v.comments();
|
|
962 os << '\n'; // to split the file comment from the first element
|
|
963 }
|
|
964 // the root object can't be an inline table. so pass `false`.
|
|
965 const auto serialized = visit(serializer<value_type>(w, fprec, no_comment, false), v);
|
|
966 os << serialized;
|
|
967
|
|
968 // if v is a non-table value, and has only one comment, then
|
|
969 // put a comment just after a value. in the following way.
|
|
970 //
|
|
971 // ```toml
|
|
972 // key = "value" # comment.
|
|
973 // ```
|
|
974 //
|
|
975 // Since the top-level toml object is a table, one who want to put a
|
|
976 // non-table toml value must use this in a following way.
|
|
977 //
|
|
978 // ```cpp
|
|
979 // toml::value v;
|
|
980 // std::cout << "user-defined-key = " << v << std::endl;
|
|
981 // ```
|
|
982 //
|
|
983 // In this case, it is impossible to put comments before key-value pair.
|
|
984 // The only way to preserve comments is to put all of them after a value.
|
|
985 if(!no_comment && !v.is_table() && !v.comments().empty())
|
|
986 {
|
|
987 os << " #";
|
|
988 for(const auto& c : v.comments()) {os << c;}
|
|
989 }
|
|
990 return os;
|
|
991 }
|
|
992
|
|
993 } // toml
|
|
994 #endif// TOML11_SERIALIZER_HPP
|