Mercurial > minori
comparison dep/toml11/toml/serializer.hpp @ 318:3b355fa948c7
config: use TOML instead of INI
unfortunately, INI is not enough, and causes some paths including
semicolons to break with our current storage of the library folders.
so, I decided to switch to TOML which does support real arrays...
| author | Paper <paper@paper.us.eu.org> |
|---|---|
| date | Wed, 12 Jun 2024 05:25:41 -0400 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 317:b1f4d1867ab1 | 318:3b355fa948c7 |
|---|---|
| 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 |
