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 |