diff --git a/src/coders/commons.cpp b/src/coders/commons.cpp index e915b6a6..b79dc14e 100644 --- a/src/coders/commons.cpp +++ b/src/coders/commons.cpp @@ -108,6 +108,10 @@ bool BasicParser::hasNext() { return pos < source.length(); } +size_t BasicParser::remain() const { + return source.length() - pos; +} + bool BasicParser::isNext(const std::string& substring) { if (source.length() - pos < substring.length()) { return false; @@ -221,10 +225,6 @@ std::string_view BasicParser::readUntilEOL() { std::string BasicParser::parseName() { char c = peek(); if (!is_identifier_start(c)) { - if (c == '"') { - pos++; - return parseString(c); - } throw error("identifier expected"); } int start = pos; @@ -234,6 +234,18 @@ std::string BasicParser::parseName() { return std::string(source.substr(start, pos - start)); } +std::string BasicParser::parseXmlName() { + char c = peek(); + if (!is_json_identifier_start(c)) { + throw error("identifier expected"); + } + int start = pos; + while (hasNext() && is_json_identifier_part(source[pos])) { + pos++; + } + return std::string(source.substr(start, pos - start)); +} + int64_t BasicParser::parseSimpleInt(int base) { char c = peek(); int index = hexchar2int(c); @@ -349,36 +361,16 @@ std::string BasicParser::parseString(char quote, bool closeRequired) { continue; } switch (c) { - case 'n': - ss << '\n'; - break; - case 'r': - ss << '\r'; - break; - case 'b': - ss << '\b'; - break; - case 't': - ss << '\t'; - break; - case 'f': - ss << '\f'; - break; - case '\'': - ss << '\\'; - break; - case '"': - ss << '"'; - break; - case '\\': - ss << '\\'; - break; - case '/': - ss << '/'; - break; - case '\n': - pos++; - continue; + case 'n': ss << '\n'; break; + case 'r': ss << '\r'; break; + case 'b': ss << '\b'; break; + case 't': ss << '\t'; break; + case 'f': ss << '\f'; break; + case '\'': ss << '\\'; break; + case '"': ss << '"'; break; + case '\\': ss << '\\'; break; + case '/': ss << '/'; break; + case '\n': continue; default: throw error( "'\\" + std::string({c}) + "' is an illegal escape" diff --git a/src/coders/commons.hpp b/src/coders/commons.hpp index e5bb3bc1..abd4c01a 100644 --- a/src/coders/commons.hpp +++ b/src/coders/commons.hpp @@ -30,14 +30,22 @@ inline bool is_whitespace(int c) { } inline bool is_identifier_start(int c) { - return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || - c == '.'; + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_'; } inline bool is_identifier_part(int c) { return is_identifier_start(c) || is_digit(c) || c == '-'; } +inline bool is_json_identifier_start(int c) { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || + c == '.'; +} + +inline bool is_json_identifier_part(int c) { + return is_json_identifier_start(c) || is_digit(c) || c == '-'; +} + inline int hexchar2int(int c) { if (c >= '0' && c <= '9') { return c - '0'; @@ -99,7 +107,9 @@ public: std::string_view readUntil(char c); std::string_view readUntilEOL(); std::string parseName(); + std::string parseXmlName(); bool hasNext(); + size_t remain() const; char peek(); char peekInLine(); char peekNoJump(); diff --git a/src/coders/json.cpp b/src/coders/json.cpp index 8fb6a1e2..9c189998 100644 --- a/src/coders/json.cpp +++ b/src/coders/json.cpp @@ -178,7 +178,8 @@ dv::value Parser::parseObject() { skipLine(); continue; } - std::string key = parseName(); + expect('"'); + std::string key = parseString('"'); char next = peek(); if (next != ':') { throw error("':' expected"); diff --git a/src/coders/toml.cpp b/src/coders/toml.cpp index 9dbf3e6e..080df90e 100644 --- a/src/coders/toml.cpp +++ b/src/coders/toml.cpp @@ -26,33 +26,151 @@ class TomlReader : BasicParser { } } - dv::value& getSection(const std::string& section) { - if (section.empty()) { - return root; - } - size_t offset = 0; - auto rootMap = &root; - do { - size_t index = section.find('.', offset); - if (index == std::string::npos) { - auto map = rootMap->at(section); - if (!map) { - return rootMap->object(section); + // modified version of BaseParser.parseString + // todo: extract common part + std::string parseMultilineString() { + pos += 2; + char next = peek(); + + std::stringstream ss; + while (hasNext()) { + char c = source[pos]; + if (c == '"' && remain() >= 2 && + source[pos+1] == '"' && + source[pos+2] == '"') { + pos += 3; + return ss.str(); + } + if (c == '\\') { + pos++; + c = nextChar(); + if (c >= '0' && c <= '7') { + pos--; + ss << (char)parseSimpleInt(8); + continue; } - return *map; + switch (c) { + case 'n': ss << '\n'; break; + case 'r': ss << '\r'; break; + case 'b': ss << '\b'; break; + case 't': ss << '\t'; break; + case 'f': ss << '\f'; break; + case '\'': ss << '\\'; break; + case '"': ss << '"'; break; + case '\\': ss << '\\'; break; + case '/': ss << '/'; break; + case '\n': continue; + default: + throw error( + "'\\" + std::string({c}) + "' is an illegal escape" + ); + } + continue; } - auto subsection = section.substr(offset, index); - auto map = rootMap->at(subsection); - if (!map) { - rootMap = &rootMap->object(subsection); - } else { - rootMap = &*map; - } - offset = index + 1; - } while (true); + ss << c; + pos++; + } + throw error("unexpected end"); } - void readSection(const std::string& section, dv::value& map) { + dv::value parseValue() { + char c = peek(); + if (is_digit(c)) { + int start = pos; + // parse numeric literal + auto value = parseNumber(1); + if (hasNext() && peekNoJump() == '-') { + while (hasNext()) { + c = source[pos]; + if (!is_digit(c) && c != ':' && c != '.' && c != '-' && + c != 'T' && c != 'Z') { + break; + } + pos++; + } + return std::string(source.substr(start, pos - start)); + } + return value; + } else if (c == '-' || c == '+') { + int sign = c == '-' ? -1 : 1; + pos++; + // parse numeric literal + return parseNumber(sign); + } else if (is_identifier_start(c)) { + // parse keywords + std::string keyword = parseName(); + if (keyword == "true" || keyword == "false") { + return keyword == "true"; + } else if (keyword == "inf") { + return INFINITY; + } else if (keyword == "nan") { + return NAN; + } + throw error("unknown keyword " + util::quote(keyword)); + } else if (c == '"' || c == '\'') { + pos++; + if (remain() >= 2 && + c == '"' && + source[pos] == '"' && + source[pos+1] == '"') { + return parseMultilineString(); + } + return parseString(c); + } else if (c == '[') { + // parse array + pos++; + dv::list_t values; + while (peek() != ']') { + values.push_back(parseValue()); + if (peek() != ']') { + expect(','); + } + } + pos++; + return dv::value(std::move(values)); + } else if (c == '{') { + // parse inline table + pos++; + auto table = dv::object(); + while (peek() != '}') { + auto key = parseName(); + expect('='); + table[key] = parseValue(); + if (peek() != '}') { + expect(','); + } + } + pos++; + return table; + } else { + throw error("feature is not supported"); + } + } + + dv::value& parseLValue(dv::value& root) { + dv::value* lvalue = &root; + while (hasNext()) { + char c = peek(); + std::string name; + if (c == '\'' || c == '"') { + pos++; + name = parseString(c); + } else { + name = parseName(); + } + if (lvalue->getType() == dv::value_type::none) { + *lvalue = dv::object(); + } + lvalue = &(*lvalue)[name]; + if (peek() != '.') { + break; + } + pos++; + } + return *lvalue; + } + + void readSection(dv::value& map, dv::value& root) { while (hasNext()) { skipWhitespace(); if (!hasNext()) { @@ -60,43 +178,43 @@ class TomlReader : BasicParser { } char c = nextChar(); if (c == '[') { - std::string name = parseName(); - pos++; - readSection(name, getSection(name)); + if (hasNext() && peek() == '[') { + pos++; + // parse list of tables + dv::value& list = parseLValue(root); + if (list == nullptr) { + list = dv::list(); + } else if (!list.isList()) { + throw error("target is not an array"); + } + expect(']'); + expect(']'); + dv::value section = dv::object(); + readSection(section, root); + list.add(std::move(section)); + return; + } + // parse table + dv::value& section = parseLValue(root); + if (section == nullptr) { + section = dv::object(); + } else if (!section.isObject()) { + throw error("target is not a table"); + } + expect(']'); + readSection(section, root); return; } pos--; - std::string name = parseName(); + dv::value& lvalue = parseLValue(map); expect('='); - c = peek(); - if (is_digit(c)) { - map[name] = parseNumber(1); - } else if (c == '-' || c == '+') { - int sign = c == '-' ? -1 : 1; - pos++; - map[name] = parseNumber(sign); - } else if (is_identifier_start(c)) { - std::string identifier = parseName(); - if (identifier == "true" || identifier == "false") { - map[name] = identifier == "true"; - } else if (identifier == "inf") { - map[name] = INFINITY; - } else if (identifier == "nan") { - map[name] = NAN; - } - } else if (c == '"' || c == '\'') { - pos++; - map[name] = parseString(c); - } else { - throw error("feature is not supported"); - } + lvalue = parseValue(); expectNewLine(); } } public: TomlReader(std::string_view file, std::string_view source) - : BasicParser(file, source) { - root = dv::object(); + : BasicParser(file, source), root(dv::object()) { } dv::value read() { @@ -104,7 +222,7 @@ public: if (!hasNext()) { return std::move(root); } - readSection("", root); + readSection(root, root); return std::move(root); } }; @@ -113,6 +231,7 @@ void toml::parse( SettingsHandler& handler, std::string_view file, std::string_view source ) { auto map = parse(file, source); + for (const auto& [sectionName, sectionMap] : map.asObject()) { if (!sectionMap.isObject()) { continue; diff --git a/src/data/dv.hpp b/src/data/dv.hpp index 369f27a0..94d96453 100644 --- a/src/data/dv.hpp +++ b/src/data/dv.hpp @@ -196,6 +196,9 @@ namespace dv { value(std::shared_ptr v) noexcept { this->operator=(std::move(v)); } + value(list_t values) { + this->operator=(std::make_shared(std::move(values))); + } value(const value& v) noexcept : type(value_type::none) { this->operator=(v); diff --git a/src/engine.cpp b/src/engine.cpp index 538721ea..a7b94bbb 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -9,6 +9,7 @@ #include "coders/imageio.hpp" #include "coders/json.hpp" #include "coders/toml.hpp" +#include "coders/commons.hpp" #include "content/Content.hpp" #include "content/ContentBuilder.hpp" #include "content/ContentLoader.hpp" @@ -116,7 +117,12 @@ void Engine::loadSettings() { if (fs::is_regular_file(settings_file)) { logger.info() << "loading settings"; std::string text = files::read_string(settings_file); - toml::parse(settingsHandler, settings_file.string(), text); + try { + toml::parse(settingsHandler, settings_file.string(), text); + } catch (const parsing_error& err) { + logger.error() << err.errorLog(); + throw; + } } } diff --git a/src/window/Events.cpp b/src/window/Events.cpp index 67c305cf..092e4a0d 100644 --- a/src/window/Events.cpp +++ b/src/window/Events.cpp @@ -196,22 +196,25 @@ void Events::loadBindings( const std::string& filename, const std::string& source ) { auto map = toml::parse(filename, source); - for (auto& [key, value] : map.asObject()) { - auto [prefix, codename] = util::split_at(value.asString(), ':'); - inputtype type; - int code; - if (prefix == "key") { - type = inputtype::keyboard; - code = static_cast(input_util::keycode_from(codename)); - } else if (prefix == "mouse") { - type = inputtype::mouse; - code = static_cast(input_util::mousecode_from(codename)); - } else { - logger.error() - << "unknown input type: " << prefix << " (binding " - << util::quote(key) << ")"; - continue; + for (auto& [sectionName, section] : map.asObject()) { + for (auto& [name, value] : section.asObject()) { + auto key = sectionName + "." + name; + auto [prefix, codename] = util::split_at(value.asString(), ':'); + inputtype type; + int code; + if (prefix == "key") { + type = inputtype::keyboard; + code = static_cast(input_util::keycode_from(codename)); + } else if (prefix == "mouse") { + type = inputtype::mouse; + code = static_cast(input_util::mousecode_from(codename)); + } else { + logger.error() + << "unknown input type: " << prefix << " (binding " + << util::quote(key) << ")"; + continue; + } + Events::bind(key, type, code); } - Events::bind(key, type, code); } } diff --git a/test/coders/toml.cpp b/test/coders/toml.cpp new file mode 100644 index 00000000..e8a63f19 --- /dev/null +++ b/test/coders/toml.cpp @@ -0,0 +1,95 @@ +#include "coders/toml.hpp" + +#include + +#include "data/dv.hpp" +#include "util/stringutil.hpp" +#include "util/Buffer.hpp" +#include "coders/commons.hpp" + +TEST(TOML, EncodeDecode) { + const std::string name = "TOML-encoder"; + const int bytesSize = 20; + const int year = 2019; + const float score = 3.141592; + const bool visible = true; + dv::objects::Bytes srcBytes(bytesSize); + for (int i = 0; i < bytesSize; i ++) { + srcBytes[i] = rand(); + } + + std::string text; + { + auto object = dv::object(); + object["name"] = name; + object["year"] = year; + object["score"] = score; + object["visible"] = visible; + object["data"] = srcBytes; + + text = toml::stringify(object, ""); + std::cout << text << std::endl; + } + try { + auto object = toml::parse("", text); + EXPECT_EQ(object["name"].asString(), name); + EXPECT_EQ(object["year"].asInteger(), year); + EXPECT_FLOAT_EQ(object["score"].asNumber(), score); + EXPECT_EQ(object["visible"].asBoolean(), visible); + auto b64string = object["data"].asString(); + + auto bytes = util::base64_decode(b64string); + EXPECT_EQ(bytes.size(), bytesSize); + for (int i = 0; i < bytesSize; i++) { + EXPECT_EQ(bytes[i], srcBytes[i]); + } + } catch (const parsing_error& err) { + std::cerr << err.errorLog() << std::endl; + throw; + } +} + +// Modified example from toml.io +inline std::string SRC_EXAMPLE = + "# This is a TOML document\n" + "\n" + "title = \"TOML Example\"\n" + "\n" + "[owner]\n" + "name = \"Tom Preston-Werner\"\n" + "dob = 1979-05-27T07:32:00-08:00\n" + "\n" + "[database]\n" + "enabled = true\n" + "ports = [ 8000, 8001, 8002 ]\n" + "data = [ [\"delta\", \"phi\"], [3.14] ]\n" + "temp_targets = { cpu = 79.5, case = 72.0 }\n" + "\n" + "[servers]\n" + "\n" + "[servers.alpha]\n" + "ip = \"10.0.0.1\"\n" + "role = \"frontend\"\n" + "\n" + "[servers.beta]\n" + "ip = \"10.0.0.2\"\n" + "role = \"\"\"back\\\n" + "end\"\"\"\n" + "\n" + "[[users]]\n" + "name = \"noname\"\n" + "\n" + "[[users]]\n" + "name = \"user1\"\n" + "suspended = true\n"; + +TEST(TOML, ExampleCode) { + try { + std::cout << SRC_EXAMPLE << std::endl; + auto object = toml::parse("", SRC_EXAMPLE); + std::cout << object << std::endl; + } catch (const parsing_error& err) { + std::cerr << err.errorLog() << std::endl; + throw; + } +}