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