mirror of
https://github.com/ToruNiina/toml11.git
synced 2024-11-09 22:30:07 +00:00
d2937ff4e1
Previously a key like: "a\u0000\u0001b" = 1 Would get written with literal control characters, rather than escapes: "a<00><01>b" = 1 The "valid/key/quoted-unicode" test from toml-test would fail with this, although it seems they're not run automatically(?) Can also reproduce with something like: % cat test.cpp #include <toml.hpp> #include <iostream> int main() { const auto data = toml::parse("test.toml"); std::cout << data << "\n"; return 0; } % cat test.toml "a\u0000\u0001b" = "a\u0000\u0001b" % c++ -I. test.cpp % ./a.out "ab" = "a\u0000\u0001b" % ./a.out | hexdump -C 00000000 22 61 00 01 62 22 20 3d 20 22 61 5c 75 30 30 30 |"a..b" = "a\u000| 00000010 30 5c 75 30 30 30 31 62 22 0a 0a |0\u0001b"..|
995 lines
33 KiB
C++
995 lines
33 KiB
C++
// Copyright Toru Niina 2019.
|
|
// Distributed under the MIT License.
|
|
#ifndef TOML11_SERIALIZER_HPP
|
|
#define TOML11_SERIALIZER_HPP
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
|
|
#include <limits>
|
|
|
|
#if defined(_WIN32)
|
|
#include <locale.h>
|
|
#elif defined(__APPLE__) || defined(__FreeBSD__)
|
|
#include <xlocale.h>
|
|
#elif defined(__linux__)
|
|
#include <locale.h>
|
|
#endif
|
|
|
|
#include "lexer.hpp"
|
|
#include "value.hpp"
|
|
|
|
namespace toml
|
|
{
|
|
|
|
// This function serialize a key. It checks a string is a bare key and
|
|
// escapes special characters if the string is not compatible to a bare key.
|
|
// ```cpp
|
|
// std::string k("non.bare.key"); // the key itself includes `.`s.
|
|
// std::string formatted = toml::format_key(k);
|
|
// assert(formatted == "\"non.bare.key\"");
|
|
// ```
|
|
//
|
|
// This function is exposed to make it easy to write a user-defined serializer.
|
|
// Since toml restricts characters available in a bare key, generally a string
|
|
// should be escaped. But checking whether a string needs to be surrounded by
|
|
// a `"` and escaping some special character is boring.
|
|
template<typename charT, typename traits, typename Alloc>
|
|
std::basic_string<charT, traits, Alloc>
|
|
format_key(const std::basic_string<charT, traits, Alloc>& k)
|
|
{
|
|
if(k.empty())
|
|
{
|
|
return std::string("\"\"");
|
|
}
|
|
|
|
// check the key can be a bare (unquoted) key
|
|
detail::location loc(k, std::vector<char>(k.begin(), k.end()));
|
|
detail::lex_unquoted_key::invoke(loc);
|
|
if(loc.iter() == loc.end())
|
|
{
|
|
return k; // all the tokens are consumed. the key is unquoted-key.
|
|
}
|
|
|
|
//if it includes special characters, then format it in a "quoted" key.
|
|
std::basic_string<charT, traits, Alloc> serialized("\"");
|
|
for(const char c : k)
|
|
{
|
|
switch(c)
|
|
{
|
|
case '\\': {serialized += "\\\\"; break;}
|
|
case '\"': {serialized += "\\\""; break;}
|
|
case '\b': {serialized += "\\b"; break;}
|
|
case '\t': {serialized += "\\t"; break;}
|
|
case '\f': {serialized += "\\f"; break;}
|
|
case '\n': {serialized += "\\n"; break;}
|
|
case '\r': {serialized += "\\r"; break;}
|
|
default: {
|
|
if (c >= 0x00 && c < 0x20)
|
|
{
|
|
std::array<char, 7> buf;
|
|
std::snprintf(buf.data(), buf.size(), "\\u00%02x", static_cast<int>(c));
|
|
serialized += buf.data();
|
|
}
|
|
else
|
|
{
|
|
serialized += c;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
serialized += "\"";
|
|
return serialized;
|
|
}
|
|
|
|
template<typename charT, typename traits, typename Alloc>
|
|
std::basic_string<charT, traits, Alloc>
|
|
format_keys(const std::vector<std::basic_string<charT, traits, Alloc>>& keys)
|
|
{
|
|
if(keys.empty())
|
|
{
|
|
return std::string("\"\"");
|
|
}
|
|
|
|
std::basic_string<charT, traits, Alloc> serialized;
|
|
for(const auto& ky : keys)
|
|
{
|
|
serialized += format_key(ky);
|
|
serialized += charT('.');
|
|
}
|
|
serialized.pop_back(); // remove the last dot '.'
|
|
return serialized;
|
|
}
|
|
|
|
template<typename Value>
|
|
struct serializer
|
|
{
|
|
static_assert(detail::is_basic_value<Value>::value,
|
|
"toml::serializer is for toml::value and its variants, "
|
|
"toml::basic_value<...>.");
|
|
|
|
using value_type = Value;
|
|
using key_type = typename value_type::key_type ;
|
|
using comment_type = typename value_type::comment_type ;
|
|
using boolean_type = typename value_type::boolean_type ;
|
|
using integer_type = typename value_type::integer_type ;
|
|
using floating_type = typename value_type::floating_type ;
|
|
using string_type = typename value_type::string_type ;
|
|
using local_time_type = typename value_type::local_time_type ;
|
|
using local_date_type = typename value_type::local_date_type ;
|
|
using local_datetime_type = typename value_type::local_datetime_type ;
|
|
using offset_datetime_type = typename value_type::offset_datetime_type;
|
|
using array_type = typename value_type::array_type ;
|
|
using table_type = typename value_type::table_type ;
|
|
|
|
serializer(const std::size_t w = 80u,
|
|
const int float_prec = std::numeric_limits<toml::floating>::max_digits10,
|
|
const bool can_be_inlined = false,
|
|
const bool no_comment = false,
|
|
std::vector<toml::key> ks = {},
|
|
const bool value_has_comment = false)
|
|
: can_be_inlined_(can_be_inlined), no_comment_(no_comment),
|
|
value_has_comment_(value_has_comment && !no_comment),
|
|
float_prec_(float_prec), width_(w), keys_(std::move(ks))
|
|
{}
|
|
~serializer() = default;
|
|
|
|
std::string operator()(const boolean_type& b) const
|
|
{
|
|
return b ? "true" : "false";
|
|
}
|
|
std::string operator()(const integer_type i) const
|
|
{
|
|
#if defined(_WIN32)
|
|
_configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
|
|
const std::string original_locale(setlocale(LC_NUMERIC, nullptr));
|
|
setlocale(LC_NUMERIC, "C");
|
|
#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__linux__)
|
|
const auto c_locale = newlocale(LC_NUMERIC_MASK, "C", locale_t(0));
|
|
locale_t original_locale(0);
|
|
if(c_locale != locale_t(0))
|
|
{
|
|
original_locale = uselocale(c_locale);
|
|
}
|
|
#endif
|
|
|
|
const auto str = std::to_string(i);
|
|
|
|
#if defined(_WIN32)
|
|
setlocale(LC_NUMERIC, original_locale.c_str());
|
|
_configthreadlocale(_DISABLE_PER_THREAD_LOCALE);
|
|
#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__linux__)
|
|
if(original_locale != locale_t(0))
|
|
{
|
|
uselocale(original_locale);
|
|
}
|
|
#endif
|
|
return str;
|
|
}
|
|
std::string operator()(const floating_type f) const
|
|
{
|
|
if(std::isnan(f))
|
|
{
|
|
if(std::signbit(f))
|
|
{
|
|
return std::string("-nan");
|
|
}
|
|
else
|
|
{
|
|
return std::string("nan");
|
|
}
|
|
}
|
|
else if(!std::isfinite(f))
|
|
{
|
|
if(std::signbit(f))
|
|
{
|
|
return std::string("-inf");
|
|
}
|
|
else
|
|
{
|
|
return std::string("inf");
|
|
}
|
|
}
|
|
|
|
// set locale to "C".
|
|
// To make it thread-local, we use OS-specific features.
|
|
// If we set process-global locale, it can break other thread that also
|
|
// outputs something simultaneously.
|
|
#if defined(_WIN32)
|
|
_configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
|
|
const std::string original_locale(setlocale(LC_NUMERIC, nullptr));
|
|
setlocale(LC_NUMERIC, "C");
|
|
#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__linux__)
|
|
const auto c_locale = newlocale(LC_NUMERIC_MASK, "C", locale_t(0));
|
|
locale_t original_locale(0);
|
|
if(c_locale != locale_t(0))
|
|
{
|
|
original_locale = uselocale(c_locale);
|
|
}
|
|
#endif
|
|
|
|
const auto fmt = "%.*g";
|
|
const auto bsz = std::snprintf(nullptr, 0, fmt, this->float_prec_, f);
|
|
// +1 for null character(\0)
|
|
std::vector<char> buf(static_cast<std::size_t>(bsz + 1), '\0');
|
|
std::snprintf(buf.data(), buf.size(), fmt, this->float_prec_, f);
|
|
|
|
// restore the original locale
|
|
#if defined(_WIN32)
|
|
setlocale(LC_NUMERIC, original_locale.c_str());
|
|
_configthreadlocale(_DISABLE_PER_THREAD_LOCALE);
|
|
#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__linux__)
|
|
if(original_locale != locale_t(0))
|
|
{
|
|
uselocale(original_locale);
|
|
}
|
|
#endif
|
|
|
|
std::string token(buf.begin(), std::prev(buf.end()));
|
|
if(!token.empty() && token.back() == '.') // 1. => 1.0
|
|
{
|
|
token += '0';
|
|
}
|
|
|
|
const auto e = std::find_if(
|
|
token.cbegin(), token.cend(), [](const char c) noexcept -> bool {
|
|
return c == 'e' || c == 'E';
|
|
});
|
|
const auto has_exponent = (token.cend() != e);
|
|
const auto has_fraction = (token.cend() != std::find(
|
|
token.cbegin(), token.cend(), '.'));
|
|
|
|
if(!has_exponent && !has_fraction)
|
|
{
|
|
// the resulting value does not have any float specific part!
|
|
token += ".0";
|
|
}
|
|
return token;
|
|
}
|
|
std::string operator()(const string_type& s) const
|
|
{
|
|
if(s.kind == string_t::basic)
|
|
{
|
|
if((std::find(s.str.cbegin(), s.str.cend(), '\n') != s.str.cend() ||
|
|
std::find(s.str.cbegin(), s.str.cend(), '\"') != s.str.cend()) &&
|
|
this->width_ != (std::numeric_limits<std::size_t>::max)())
|
|
{
|
|
// if linefeed or double-quote is contained,
|
|
// make it multiline basic string.
|
|
const auto escaped = this->escape_ml_basic_string(s.str);
|
|
std::string open("\"\"\"");
|
|
std::string close("\"\"\"");
|
|
if(escaped.find('\n') != std::string::npos ||
|
|
this->width_ < escaped.size() + 6)
|
|
{
|
|
// if the string body contains newline or is enough long,
|
|
// add newlines after and before delimiters.
|
|
open += "\n";
|
|
close = std::string("\\\n") + close;
|
|
}
|
|
return open + escaped + close;
|
|
}
|
|
|
|
// no linefeed. try to make it oneline-string.
|
|
std::string oneline = this->escape_basic_string(s.str);
|
|
if(oneline.size() + 2 < width_ || width_ < 2)
|
|
{
|
|
const std::string quote("\"");
|
|
return quote + oneline + quote;
|
|
}
|
|
|
|
// the line is too long compared to the specified width.
|
|
// split it into multiple lines.
|
|
std::string token("\"\"\"\n");
|
|
while(!oneline.empty())
|
|
{
|
|
if(oneline.size() < width_)
|
|
{
|
|
token += oneline;
|
|
oneline.clear();
|
|
}
|
|
else if(oneline.at(width_-2) == '\\')
|
|
{
|
|
token += oneline.substr(0, width_-2);
|
|
token += "\\\n";
|
|
oneline.erase(0, width_-2);
|
|
}
|
|
else
|
|
{
|
|
token += oneline.substr(0, width_-1);
|
|
token += "\\\n";
|
|
oneline.erase(0, width_-1);
|
|
}
|
|
}
|
|
return token + std::string("\\\n\"\"\"");
|
|
}
|
|
else // the string `s` is literal-string.
|
|
{
|
|
if(std::find(s.str.cbegin(), s.str.cend(), '\n') != s.str.cend() ||
|
|
std::find(s.str.cbegin(), s.str.cend(), '\'') != s.str.cend() )
|
|
{
|
|
std::string open("'''");
|
|
if(this->width_ + 6 < s.str.size())
|
|
{
|
|
open += '\n'; // the first newline is ignored by TOML spec
|
|
}
|
|
const std::string close("'''");
|
|
return open + s.str + close;
|
|
}
|
|
else
|
|
{
|
|
const std::string quote("'");
|
|
return quote + s.str + quote;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string operator()(const local_date_type& d) const
|
|
{
|
|
std::ostringstream oss;
|
|
oss << d;
|
|
return oss.str();
|
|
}
|
|
std::string operator()(const local_time_type& t) const
|
|
{
|
|
std::ostringstream oss;
|
|
oss << t;
|
|
return oss.str();
|
|
}
|
|
std::string operator()(const local_datetime_type& dt) const
|
|
{
|
|
std::ostringstream oss;
|
|
oss << dt;
|
|
return oss.str();
|
|
}
|
|
std::string operator()(const offset_datetime_type& odt) const
|
|
{
|
|
std::ostringstream oss;
|
|
oss << odt;
|
|
return oss.str();
|
|
}
|
|
|
|
std::string operator()(const array_type& v) const
|
|
{
|
|
if(v.empty())
|
|
{
|
|
return std::string("[]");
|
|
}
|
|
if(this->is_array_of_tables(v))
|
|
{
|
|
return make_array_of_tables(v);
|
|
}
|
|
|
|
// not an array of tables. normal array.
|
|
// first, try to make it inline if none of the elements have a comment.
|
|
if( ! this->has_comment_inside(v))
|
|
{
|
|
const auto inl = this->make_inline_array(v);
|
|
if(inl.size() < this->width_ &&
|
|
std::find(inl.cbegin(), inl.cend(), '\n') == inl.cend())
|
|
{
|
|
return inl;
|
|
}
|
|
}
|
|
|
|
// if the length exceeds this->width_, print multiline array.
|
|
// key = [
|
|
// # ...
|
|
// 42,
|
|
// ...
|
|
// ]
|
|
std::string token;
|
|
std::string current_line;
|
|
token += "[\n";
|
|
for(const auto& item : v)
|
|
{
|
|
if( ! item.comments().empty() && !no_comment_)
|
|
{
|
|
// if comment exists, the element must be the only element in the line.
|
|
// e.g. the following is not allowed.
|
|
// ```toml
|
|
// array = [
|
|
// # comment for what?
|
|
// 1, 2, 3, 4, 5
|
|
// ]
|
|
// ```
|
|
if(!current_line.empty())
|
|
{
|
|
if(current_line.back() != '\n')
|
|
{
|
|
current_line += '\n';
|
|
}
|
|
token += current_line;
|
|
current_line.clear();
|
|
}
|
|
for(const auto& c : item.comments())
|
|
{
|
|
token += '#';
|
|
token += c;
|
|
token += '\n';
|
|
}
|
|
token += toml::visit(*this, item);
|
|
if(!token.empty() && token.back() == '\n') {token.pop_back();}
|
|
token += ",\n";
|
|
continue;
|
|
}
|
|
std::string next_elem;
|
|
if(item.is_table())
|
|
{
|
|
serializer ser(*this);
|
|
ser.can_be_inlined_ = true;
|
|
ser.width_ = (std::numeric_limits<std::size_t>::max)();
|
|
next_elem += toml::visit(ser, item);
|
|
}
|
|
else
|
|
{
|
|
next_elem += toml::visit(*this, item);
|
|
}
|
|
|
|
// comma before newline.
|
|
if(!next_elem.empty() && next_elem.back() == '\n') {next_elem.pop_back();}
|
|
|
|
// if current line does not exceeds the width limit, continue.
|
|
if(current_line.size() + next_elem.size() + 1 < this->width_)
|
|
{
|
|
current_line += next_elem;
|
|
current_line += ',';
|
|
}
|
|
else if(current_line.empty())
|
|
{
|
|
// if current line was empty, force put the next_elem because
|
|
// next_elem is not splittable
|
|
token += next_elem;
|
|
token += ",\n";
|
|
// current_line is kept empty
|
|
}
|
|
else // reset current_line
|
|
{
|
|
assert(current_line.back() == ',');
|
|
token += current_line;
|
|
token += '\n';
|
|
current_line = next_elem;
|
|
current_line += ',';
|
|
}
|
|
}
|
|
if(!current_line.empty())
|
|
{
|
|
if(!current_line.empty() && current_line.back() != '\n')
|
|
{
|
|
current_line += '\n';
|
|
}
|
|
token += current_line;
|
|
}
|
|
token += "]\n";
|
|
return token;
|
|
}
|
|
|
|
// templatize for any table-like container
|
|
std::string operator()(const table_type& v) const
|
|
{
|
|
// if an element has a comment, then it can't be inlined.
|
|
// table = {# how can we write a comment for this? key = "value"}
|
|
if(this->can_be_inlined_ && !(this->has_comment_inside(v)))
|
|
{
|
|
std::string token;
|
|
if(!this->keys_.empty())
|
|
{
|
|
token += format_key(this->keys_.back());
|
|
token += " = ";
|
|
}
|
|
token += this->make_inline_table(v);
|
|
if(token.size() < this->width_ &&
|
|
token.end() == std::find(token.begin(), token.end(), '\n'))
|
|
{
|
|
return token;
|
|
}
|
|
}
|
|
|
|
std::string token;
|
|
if(!keys_.empty())
|
|
{
|
|
token += '[';
|
|
token += format_keys(keys_);
|
|
token += "]\n";
|
|
}
|
|
token += this->make_multiline_table(v);
|
|
return token;
|
|
}
|
|
|
|
private:
|
|
|
|
std::string escape_basic_string(const std::string& s) const
|
|
{
|
|
//XXX assuming `s` is a valid utf-8 sequence.
|
|
std::string retval;
|
|
for(const char c : s)
|
|
{
|
|
switch(c)
|
|
{
|
|
case '\\': {retval += "\\\\"; break;}
|
|
case '\"': {retval += "\\\""; break;}
|
|
case '\b': {retval += "\\b"; break;}
|
|
case '\t': {retval += "\\t"; break;}
|
|
case '\f': {retval += "\\f"; break;}
|
|
case '\n': {retval += "\\n"; break;}
|
|
case '\r': {retval += "\\r"; break;}
|
|
default :
|
|
{
|
|
if((0x00 <= c && c <= 0x08) || (0x0A <= c && c <= 0x1F) || c == 0x7F)
|
|
{
|
|
retval += "\\u00";
|
|
retval += char(48 + (c / 16));
|
|
retval += char((c % 16 < 10 ? 48 : 55) + (c % 16));
|
|
}
|
|
else
|
|
{
|
|
retval += c;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
std::string escape_ml_basic_string(const std::string& s) const
|
|
{
|
|
std::string retval;
|
|
for(auto i=s.cbegin(), e=s.cend(); i!=e; ++i)
|
|
{
|
|
switch(*i)
|
|
{
|
|
case '\\': {retval += "\\\\"; break;}
|
|
// One or two consecutive "s are allowed.
|
|
// Later we will check there are no three consecutive "s.
|
|
// case '\"': {retval += "\\\""; break;}
|
|
case '\b': {retval += "\\b"; break;}
|
|
case '\t': {retval += "\\t"; break;}
|
|
case '\f': {retval += "\\f"; break;}
|
|
case '\n': {retval += "\n"; break;}
|
|
case '\r':
|
|
{
|
|
if(std::next(i) != e && *std::next(i) == '\n')
|
|
{
|
|
retval += "\r\n";
|
|
++i;
|
|
}
|
|
else
|
|
{
|
|
retval += "\\r";
|
|
}
|
|
break;
|
|
}
|
|
default :
|
|
{
|
|
const auto c = *i;
|
|
if((0x00 <= c && c <= 0x08) || (0x0A <= c && c <= 0x1F) || c == 0x7F)
|
|
{
|
|
retval += "\\u00";
|
|
retval += char(48 + (c / 16));
|
|
retval += char((c % 16 < 10 ? 48 : 55) + (c % 16));
|
|
}
|
|
else
|
|
{
|
|
retval += c;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
// Only 1 or 2 consecutive `"`s are allowed in multiline basic string.
|
|
// 3 consecutive `"`s are considered as a closing delimiter.
|
|
// We need to check if there are 3 or more consecutive `"`s and insert
|
|
// backslash to break them down into several short `"`s like the `str6`
|
|
// in the following example.
|
|
// ```toml
|
|
// str4 = """Here are two quotation marks: "". Simple enough."""
|
|
// # str5 = """Here are three quotation marks: """.""" # INVALID
|
|
// str5 = """Here are three quotation marks: ""\"."""
|
|
// str6 = """Here are fifteen quotation marks: ""\"""\"""\"""\"""\"."""
|
|
// ```
|
|
auto found_3_quotes = retval.find("\"\"\"");
|
|
while(found_3_quotes != std::string::npos)
|
|
{
|
|
retval.replace(found_3_quotes, 3, "\"\"\\\"");
|
|
found_3_quotes = retval.find("\"\"\"");
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
// if an element of a table or an array has a comment, it cannot be inlined.
|
|
bool has_comment_inside(const array_type& a) const noexcept
|
|
{
|
|
// if no_comment is set, comments would not be written.
|
|
if(this->no_comment_) {return false;}
|
|
|
|
for(const auto& v : a)
|
|
{
|
|
if(!v.comments().empty()) {return true;}
|
|
}
|
|
return false;
|
|
}
|
|
bool has_comment_inside(const table_type& t) const noexcept
|
|
{
|
|
// if no_comment is set, comments would not be written.
|
|
if(this->no_comment_) {return false;}
|
|
|
|
for(const auto& kv : t)
|
|
{
|
|
if(!kv.second.comments().empty()) {return true;}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::string make_inline_array(const array_type& v) const
|
|
{
|
|
assert(!has_comment_inside(v));
|
|
std::string token;
|
|
token += '[';
|
|
bool is_first = true;
|
|
for(const auto& item : v)
|
|
{
|
|
if(is_first) {is_first = false;} else {token += ',';}
|
|
token += visit(serializer(
|
|
(std::numeric_limits<std::size_t>::max)(), this->float_prec_,
|
|
/* inlined */ true, /*no comment*/ false, /*keys*/ {},
|
|
/*has_comment*/ !item.comments().empty()), item);
|
|
}
|
|
token += ']';
|
|
return token;
|
|
}
|
|
|
|
std::string make_inline_table(const table_type& v) const
|
|
{
|
|
assert(!has_comment_inside(v));
|
|
assert(this->can_be_inlined_);
|
|
std::string token;
|
|
token += '{';
|
|
bool is_first = true;
|
|
for(const auto& kv : v)
|
|
{
|
|
// in inline tables, trailing comma is not allowed (toml-lang #569).
|
|
if(is_first) {is_first = false;} else {token += ',';}
|
|
token += format_key(kv.first);
|
|
token += '=';
|
|
token += visit(serializer(
|
|
(std::numeric_limits<std::size_t>::max)(), this->float_prec_,
|
|
/* inlined */ true, /*no comment*/ false, /*keys*/ {},
|
|
/*has_comment*/ !kv.second.comments().empty()), kv.second);
|
|
}
|
|
token += '}';
|
|
return token;
|
|
}
|
|
|
|
std::string make_multiline_table(const table_type& v) const
|
|
{
|
|
std::string token;
|
|
|
|
// print non-table elements first.
|
|
// ```toml
|
|
// [foo] # a table we're writing now here
|
|
// key = "value" # <- non-table element, "key"
|
|
// # ...
|
|
// [foo.bar] # <- table element, "bar"
|
|
// ```
|
|
// because after printing [foo.bar], the remaining non-table values will
|
|
// be assigned into [foo.bar], not [foo]. Those values should be printed
|
|
// earlier.
|
|
for(const auto& kv : v)
|
|
{
|
|
if(kv.second.is_table() || is_array_of_tables(kv.second))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
token += write_comments(kv.second);
|
|
|
|
const auto key_and_sep = format_key(kv.first) + " = ";
|
|
const auto residual_width = (this->width_ > key_and_sep.size()) ?
|
|
this->width_ - key_and_sep.size() : 0;
|
|
token += key_and_sep;
|
|
token += visit(serializer(residual_width, this->float_prec_,
|
|
/*can be inlined*/ true, /*no comment*/ false, /*keys*/ {},
|
|
/*has_comment*/ !kv.second.comments().empty()), kv.second);
|
|
|
|
if(token.back() != '\n')
|
|
{
|
|
token += '\n';
|
|
}
|
|
}
|
|
|
|
// normal tables / array of tables
|
|
|
|
// after multiline table appeared, the other tables cannot be inline
|
|
// because the table would be assigned into the table.
|
|
// [foo]
|
|
// ...
|
|
// bar = {...} # <- bar will be a member of [foo].
|
|
bool multiline_table_printed = false;
|
|
for(const auto& kv : v)
|
|
{
|
|
if(!kv.second.is_table() && !is_array_of_tables(kv.second))
|
|
{
|
|
continue; // other stuff are already serialized. skip them.
|
|
}
|
|
|
|
std::vector<toml::key> ks(this->keys_);
|
|
ks.push_back(kv.first);
|
|
|
|
auto tmp = visit(serializer(this->width_, this->float_prec_,
|
|
!multiline_table_printed, this->no_comment_, ks,
|
|
/*has_comment*/ !kv.second.comments().empty()), kv.second);
|
|
|
|
// If it is the first time to print a multi-line table, it would be
|
|
// helpful to separate normal key-value pair and subtables by a
|
|
// newline.
|
|
// (this checks if the current key-value pair contains newlines.
|
|
// but it is not perfect because multi-line string can also contain
|
|
// a newline. in such a case, an empty line will be written) TODO
|
|
if((!multiline_table_printed) &&
|
|
std::find(tmp.cbegin(), tmp.cend(), '\n') != tmp.cend())
|
|
{
|
|
multiline_table_printed = true;
|
|
token += '\n'; // separate key-value pairs and subtables
|
|
|
|
token += write_comments(kv.second);
|
|
token += tmp;
|
|
|
|
// care about recursive tables (all tables in each level prints
|
|
// newline and there will be a full of newlines)
|
|
if(tmp.substr(tmp.size() - 2, 2) != "\n\n" &&
|
|
tmp.substr(tmp.size() - 4, 4) != "\r\n\r\n" )
|
|
{
|
|
token += '\n';
|
|
}
|
|
}
|
|
else
|
|
{
|
|
token += write_comments(kv.second);
|
|
token += tmp;
|
|
token += '\n';
|
|
}
|
|
}
|
|
return token;
|
|
}
|
|
|
|
std::string make_array_of_tables(const array_type& v) const
|
|
{
|
|
// if it's not inlined, we need to add `[[table.key]]`.
|
|
// but if it can be inlined, we can format it as the following.
|
|
// ```
|
|
// table.key = [
|
|
// {...},
|
|
// # comment
|
|
// {...},
|
|
// ]
|
|
// ```
|
|
// This function checks if inlinization is possible or not, and then
|
|
// format the array-of-tables in a proper way.
|
|
//
|
|
// Note about comments:
|
|
//
|
|
// If the array itself has a comment (value_has_comment_ == true), we
|
|
// should try to make it inline.
|
|
// ```toml
|
|
// # comment about array
|
|
// array = [
|
|
// # comment about table element
|
|
// {of = "table"}
|
|
// ]
|
|
// ```
|
|
// If it is formatted as a multiline table, the two comments becomes
|
|
// indistinguishable.
|
|
// ```toml
|
|
// # comment about array
|
|
// # comment about table element
|
|
// [[array]]
|
|
// of = "table"
|
|
// ```
|
|
// So we need to try to make it inline, and it force-inlines regardless
|
|
// of the line width limit.
|
|
// It may fail if the element of a table has comment. In that case,
|
|
// the array-of-tables will be formatted as a multiline table.
|
|
if(this->can_be_inlined_ || this->value_has_comment_)
|
|
{
|
|
std::string token;
|
|
if(!keys_.empty())
|
|
{
|
|
token += format_key(keys_.back());
|
|
token += " = ";
|
|
}
|
|
|
|
bool failed = false;
|
|
token += "[\n";
|
|
for(const auto& item : v)
|
|
{
|
|
// if an element of the table has a comment, the table
|
|
// cannot be inlined.
|
|
if(this->has_comment_inside(item.as_table()))
|
|
{
|
|
failed = true;
|
|
break;
|
|
}
|
|
// write comments for the table itself
|
|
token += write_comments(item);
|
|
|
|
const auto t = this->make_inline_table(item.as_table());
|
|
|
|
if(t.size() + 1 > width_ || // +1 for the last comma {...},
|
|
std::find(t.cbegin(), t.cend(), '\n') != t.cend())
|
|
{
|
|
// if the value itself has a comment, ignore the line width limit
|
|
if( ! this->value_has_comment_)
|
|
{
|
|
failed = true;
|
|
break;
|
|
}
|
|
}
|
|
token += t;
|
|
token += ",\n";
|
|
}
|
|
|
|
if( ! failed)
|
|
{
|
|
token += "]\n";
|
|
return token;
|
|
}
|
|
// if failed, serialize them as [[array.of.tables]].
|
|
}
|
|
|
|
std::string token;
|
|
for(const auto& item : v)
|
|
{
|
|
token += write_comments(item);
|
|
token += "[[";
|
|
token += format_keys(keys_);
|
|
token += "]]\n";
|
|
token += this->make_multiline_table(item.as_table());
|
|
}
|
|
return token;
|
|
}
|
|
|
|
std::string write_comments(const value_type& v) const
|
|
{
|
|
std::string retval;
|
|
if(this->no_comment_) {return retval;}
|
|
|
|
for(const auto& c : v.comments())
|
|
{
|
|
retval += '#';
|
|
retval += c;
|
|
retval += '\n';
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
bool is_array_of_tables(const value_type& v) const
|
|
{
|
|
if(!v.is_array() || v.as_array().empty()) {return false;}
|
|
return is_array_of_tables(v.as_array());
|
|
}
|
|
bool is_array_of_tables(const array_type& v) const
|
|
{
|
|
// Since TOML v0.5.0, heterogeneous arrays are allowed. So we need to
|
|
// check all the element in an array to check if the array is an array
|
|
// of tables.
|
|
return std::all_of(v.begin(), v.end(), [](const value_type& elem) {
|
|
return elem.is_table();
|
|
});
|
|
}
|
|
|
|
private:
|
|
|
|
bool can_be_inlined_;
|
|
bool no_comment_;
|
|
bool value_has_comment_;
|
|
int float_prec_;
|
|
std::size_t width_;
|
|
std::vector<toml::key> keys_;
|
|
};
|
|
|
|
template<typename C,
|
|
template<typename ...> class M, template<typename ...> class V>
|
|
std::string
|
|
format(const basic_value<C, M, V>& v, std::size_t w = 80u,
|
|
int fprec = std::numeric_limits<toml::floating>::max_digits10,
|
|
bool no_comment = false, bool force_inline = false)
|
|
{
|
|
using value_type = basic_value<C, M, V>;
|
|
// if value is a table, it is considered to be a root object.
|
|
// the root object can't be an inline table.
|
|
if(v.is_table())
|
|
{
|
|
std::ostringstream oss;
|
|
if(!v.comments().empty())
|
|
{
|
|
oss << v.comments();
|
|
oss << '\n'; // to split the file comment from the first element
|
|
}
|
|
const auto serialized = visit(serializer<value_type>(w, fprec, false, no_comment), v);
|
|
oss << serialized;
|
|
return oss.str();
|
|
}
|
|
return visit(serializer<value_type>(w, fprec, force_inline), v);
|
|
}
|
|
|
|
namespace detail
|
|
{
|
|
template<typename charT, typename traits>
|
|
int comment_index(std::basic_ostream<charT, traits>&)
|
|
{
|
|
static const int index = std::ios_base::xalloc();
|
|
return index;
|
|
}
|
|
} // detail
|
|
|
|
template<typename charT, typename traits>
|
|
std::basic_ostream<charT, traits>&
|
|
nocomment(std::basic_ostream<charT, traits>& os)
|
|
{
|
|
// by default, it is zero. and by default, it shows comments.
|
|
os.iword(detail::comment_index(os)) = 1;
|
|
return os;
|
|
}
|
|
|
|
template<typename charT, typename traits>
|
|
std::basic_ostream<charT, traits>&
|
|
showcomment(std::basic_ostream<charT, traits>& os)
|
|
{
|
|
// by default, it is zero. and by default, it shows comments.
|
|
os.iword(detail::comment_index(os)) = 0;
|
|
return os;
|
|
}
|
|
|
|
template<typename charT, typename traits, typename C,
|
|
template<typename ...> class M, template<typename ...> class V>
|
|
std::basic_ostream<charT, traits>&
|
|
operator<<(std::basic_ostream<charT, traits>& os, const basic_value<C, M, V>& v)
|
|
{
|
|
using value_type = basic_value<C, M, V>;
|
|
|
|
// get status of std::setw().
|
|
const auto w = static_cast<std::size_t>(os.width());
|
|
const int fprec = static_cast<int>(os.precision());
|
|
os.width(0);
|
|
|
|
// by default, iword is initialized by 0. And by default, toml11 outputs
|
|
// comments. So `0` means showcomment. 1 means nocommnet.
|
|
const bool no_comment = (1 == os.iword(detail::comment_index(os)));
|
|
|
|
if(!no_comment && v.is_table() && !v.comments().empty())
|
|
{
|
|
os << v.comments();
|
|
os << '\n'; // to split the file comment from the first element
|
|
}
|
|
// the root object can't be an inline table. so pass `false`.
|
|
const auto serialized = visit(serializer<value_type>(w, fprec, no_comment, false), v);
|
|
os << serialized;
|
|
|
|
// if v is a non-table value, and has only one comment, then
|
|
// put a comment just after a value. in the following way.
|
|
//
|
|
// ```toml
|
|
// key = "value" # comment.
|
|
// ```
|
|
//
|
|
// Since the top-level toml object is a table, one who want to put a
|
|
// non-table toml value must use this in a following way.
|
|
//
|
|
// ```cpp
|
|
// toml::value v;
|
|
// std::cout << "user-defined-key = " << v << std::endl;
|
|
// ```
|
|
//
|
|
// In this case, it is impossible to put comments before key-value pair.
|
|
// The only way to preserve comments is to put all of them after a value.
|
|
if(!no_comment && !v.is_table() && !v.comments().empty())
|
|
{
|
|
os << " #";
|
|
for(const auto& c : v.comments()) {os << c;}
|
|
}
|
|
return os;
|
|
}
|
|
|
|
} // toml
|
|
#endif// TOML11_SERIALIZER_HPP
|