diff options
author | Aqua-sama <aqua@iserlohn-fortress.net> | 2017-02-24 02:06:20 +0100 |
---|---|---|
committer | Aqua-sama <aqua@iserlohn-fortress.net> | 2017-02-24 02:06:20 +0100 |
commit | 21552196c529cdc4e7112d2f09a80ab81c71207a (patch) | |
tree | 5bc9fe9f55da0775384b2449f7ee543cbe7afed2 | |
parent | Download manager (diff) | |
download | smolbote-21552196c529cdc4e7112d2f09a80ab81c71207a.tar.xz |
Settings class rework
Using toml as format
-rw-r--r-- | smolbote.qbs | 2 | ||||
-rw-r--r-- | src/3rd-party/toml/LICENSE | 24 | ||||
-rw-r--r-- | src/3rd-party/toml/toml.h | 1958 | ||||
-rw-r--r-- | src/browser.cpp | 21 | ||||
-rw-r--r-- | src/browser.h | 5 | ||||
-rw-r--r-- | src/forms/blockerdialog.cpp | 5 | ||||
-rw-r--r-- | src/forms/downloaddialog.cpp | 6 | ||||
-rw-r--r-- | src/mainwindow.cpp | 27 | ||||
-rw-r--r-- | src/settings.cpp | 80 | ||||
-rw-r--r-- | src/settings.h | 25 | ||||
-rw-r--r-- | src/widgets/webviewtabbar.cpp | 15 | ||||
-rw-r--r-- | test/config.ini | 34 | ||||
-rw-r--r-- | test/config.toml | 36 |
13 files changed, 2129 insertions, 109 deletions
diff --git a/smolbote.qbs b/smolbote.qbs index 032a93e..31cb6b3 100644 --- a/smolbote.qbs +++ b/smolbote.qbs @@ -42,7 +42,7 @@ Project { // condition: qbs.targetOS.contains("windows") // } - cpp.includePaths: ['src'] + cpp.includePaths: ['src', 'src/3rd-party'] cpp.defines: { if(project.deprecatedWarnings) defines.push("QT_DEPRECATED_WARNINGS", "QT_DISABLE_DEPRECATED_BEFORE="+project.deprecatedBefore); diff --git a/src/3rd-party/toml/LICENSE b/src/3rd-party/toml/LICENSE new file mode 100644 index 0000000..1514196 --- /dev/null +++ b/src/3rd-party/toml/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2014, MAYAH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/src/3rd-party/toml/toml.h b/src/3rd-party/toml/toml.h new file mode 100644 index 0000000..ec8c489 --- /dev/null +++ b/src/3rd-party/toml/toml.h @@ -0,0 +1,1958 @@ +#ifndef TINYTOML_H_ +#define TINYTOML_H_ + +#include <algorithm> +#include <cassert> +#include <cctype> +#include <chrono> +#include <cmath> +#include <cstdint> +#include <cstdio> +#include <ctime> +#include <iomanip> +#include <istream> +#include <sstream> +#include <stdexcept> +#include <string> +#include <map> +#include <memory> +#include <utility> +#include <vector> + +namespace toml { + +// ---------------------------------------------------------------------- +// Declarations + +class Value; +typedef std::chrono::system_clock::time_point Time; +typedef std::vector<Value> Array; +typedef std::map<std::string, Value> Table; + +namespace internal { +template<typename T> struct call_traits_value { + typedef T return_type; +}; +template<typename T> struct call_traits_ref { + typedef const T& return_type; +}; +} // namespace internal + +template<typename T> struct call_traits; +template<> struct call_traits<bool> : public internal::call_traits_value<bool> {}; +template<> struct call_traits<int> : public internal::call_traits_value<int> {}; +template<> struct call_traits<int64_t> : public internal::call_traits_value<int64_t> {}; +template<> struct call_traits<double> : public internal::call_traits_value<double> {}; +template<> struct call_traits<std::string> : public internal::call_traits_ref<std::string> {}; +template<> struct call_traits<Time> : public internal::call_traits_ref<Time> {}; +template<> struct call_traits<Array> : public internal::call_traits_ref<Array> {}; +template<> struct call_traits<Table> : public internal::call_traits_ref<Table> {}; + +// A value is returned for std::vector<T>. Not reference. +// This is because a fresh vector is made. +template<typename T> struct call_traits<std::vector<T>> : public internal::call_traits_value<std::vector<T>> {}; + +// Formatting flags +enum FormatFlag { + FORMAT_NONE = 0, + FORMAT_INDENT = 1 +}; + +class Value { +public: + enum Type { + NULL_TYPE, + BOOL_TYPE, + INT_TYPE, + DOUBLE_TYPE, + STRING_TYPE, + TIME_TYPE, + ARRAY_TYPE, + TABLE_TYPE, + }; + + Value() : type_(NULL_TYPE), null_(nullptr) {} + Value(bool v) : type_(BOOL_TYPE), bool_(v) {} + Value(int v) : type_(INT_TYPE), int_(v) {} + Value(int64_t v) : type_(INT_TYPE), int_(v) {} + Value(double v) : type_(DOUBLE_TYPE), double_(v) {} + Value(const std::string& v) : type_(STRING_TYPE), string_(new std::string(v)) {} + Value(const char* v) : type_(STRING_TYPE), string_(new std::string(v)) {} + Value(const Time& v) : type_(TIME_TYPE), time_(new Time(v)) {} + Value(const Array& v) : type_(ARRAY_TYPE), array_(new Array(v)) {} + Value(const Table& v) : type_(TABLE_TYPE), table_(new Table(v)) {} + Value(std::string&& v) : type_(STRING_TYPE), string_(new std::string(std::move(v))) {} + Value(Array&& v) : type_(ARRAY_TYPE), array_(new Array(std::move(v))) {} + Value(Table&& v) : type_(TABLE_TYPE), table_(new Table(std::move(v))) {} + + Value(const Value& v); + Value(Value&& v) noexcept; + Value& operator=(const Value& v); + Value& operator=(Value&& v) noexcept; + + // Guards from unexpected Value construction. + // Someone might use a value like this: + // toml::Value v = x->find("foo"); + // But this is wrong. Without this constructor, + // value will be unexpectedly initialized with bool. + Value(const void* v) = delete; + ~Value(); + + // Retruns Value size. + // 0 for invalid value. + // The number of inner elements for array or table. + // 1 for other types. + size_t size() const; + bool empty() const; + Type type() const { return type_; } + + bool valid() const { return type_ != NULL_TYPE; } + template<typename T> bool is() const; + template<typename T> typename call_traits<T>::return_type as() const; + + friend bool operator==(const Value& lhs, const Value& rhs); + friend bool operator!=(const Value& lhs, const Value& rhs) { return !(lhs == rhs); } + + // ---------------------------------------------------------------------- + // For integer/floating value + + // Returns true if the value is int or double. + bool isNumber() const; + // Returns number. Convert to double. + double asNumber() const; + + // ---------------------------------------------------------------------- + // For Time value + + // Converts to time_t if the internal value is Time. + // We don't have as<std::time_t>(). Since time_t is basically signed long, + // it's something like a method to converting to (normal) integer. + std::time_t as_time_t() const; + + // ---------------------------------------------------------------------- + // For Table value + template<typename T> typename call_traits<T>::return_type get(const std::string&) const; + Value* set(const std::string& key, const Value& v); + // Finds a Value with |key|. |key| can contain '.' + // Note: if you would like to find a child value only, you need to use findChild. + const Value* find(const std::string& key) const; + Value* find(const std::string& key); + bool has(const std::string& key) const { return find(key) != nullptr; } + bool erase(const std::string& key); + + Value& operator[](const std::string& key); + + // Merge table. Returns true if succeeded. Otherwise, |this| might be corrupted. + // When the same key exists, it will be overwritten. + bool merge(const Value&); + + // Finds a value with |key|. It searches only children. + Value* findChild(const std::string& key); + const Value* findChild(const std::string& key) const; + // Sets a value, and returns the pointer to the created value. + // When the value having the same key exists, it will be overwritten. + Value* setChild(const std::string& key, const Value& v); + Value* setChild(const std::string& key, Value&& v); + bool eraseChild(const std::string& key); + + // ---------------------------------------------------------------------- + // For Array value + + template<typename T> typename call_traits<T>::return_type get(size_t index) const; + const Value* find(size_t index) const; + Value* find(size_t index); + Value* push(const Value& v); + Value* push(Value&& v); + + // ---------------------------------------------------------------------- + // Others + + // Writer. + static std::string spaces(int num); + static std::string escapeKey(const std::string& key); + + void write(std::ostream*, const std::string& keyPrefix = std::string(), int indent = -1) const; + void writeFormatted(std::ostream*, FormatFlag flags) const; + + friend std::ostream& operator<<(std::ostream&, const Value&); + +private: + static const char* typeToString(Type); + + template<typename T> void assureType() const; + Value* ensureValue(const std::string& key); + + template<typename T> struct ValueConverter; + + Type type_; + union { + void* null_; + bool bool_; + int64_t int_; + double double_; + std::string* string_; + Time* time_; + Array* array_; + Table* table_; + }; + + template<typename T> friend struct ValueConverter; +}; + +// parse() returns ParseResult. +struct ParseResult { + ParseResult(toml::Value v, std::string er) : + value(std::move(v)), + errorReason(std::move(er)) {} + + bool valid() const { return value.valid(); } + + toml::Value value; + std::string errorReason; +}; + +// Parses from std::istream. +ParseResult parse(std::istream&); + +// ---------------------------------------------------------------------- +// Declarations for Implementations +// You don't need to understand the below to use this library. + +#if defined(_WIN32) +// Windows does not have timegm but have _mkgmtime. +inline time_t timegm(std::tm* timeptr) +{ + return _mkgmtime(timeptr); +} +#endif +#if defined(_MSC_VER) +// Visual studio doesn't define gmtime_r, but mingw does +inline std::tm* gmtime_r(const time_t* timer, std::tm* result) +{ + gmtime_s(result, timer); + return result; +} +#endif + +namespace internal { + +enum class TokenType { + ERROR, + END_OF_FILE, + END_OF_LINE, + IDENT, + STRING, + MULTILINE_STRING, + BOOL, + INT, + DOUBLE, + TIME, + COMMA, + DOT, + EQUAL, + LBRACKET, + RBRACKET, + LBRACE, + RBRACE, +}; + +class Token { +public: + explicit Token(TokenType type) : type_(type) {} + Token(TokenType type, const std::string& v) : type_(type), str_value_(v) {} + Token(TokenType type, bool v) : type_(type), int_value_(v) {} + Token(TokenType type, std::int64_t v) : type_(type), int_value_(v) {} + Token(TokenType type, double v) : type_(type), double_value_(v) {} + Token(TokenType type, std::chrono::system_clock::time_point tp) : type_(type), time_value_(tp) {} + + TokenType type() const { return type_; } + const std::string& strValue() const { return str_value_; } + bool boolValue() const { return int_value_ != 0; } + std::int64_t intValue() const { return int_value_; } + double doubleValue() const { return double_value_; } + std::chrono::system_clock::time_point timeValue() const { return time_value_; } + +private: + TokenType type_; + std::string str_value_; + std::int64_t int_value_; + double double_value_; + std::chrono::system_clock::time_point time_value_; +}; + +class Lexer { +public: + explicit Lexer(std::istream& is) : is_(is), lineNo_(1) {} + + Token nextKeyToken(); + Token nextValueToken(); + + int lineNo() const { return lineNo_; } + +private: + bool current(char* c); + void next(); + bool consume(char c); + + Token nextToken(bool isValueToken); + + void skipUntilNewLine(); + + Token nextStringDoubleQuote(); + Token nextStringSingleQuote(); + + Token nextKey(); + Token nextValue(); + + Token parseAsTime(const std::string&); + + std::istream& is_; + int lineNo_; +}; + +class Parser { +public: + explicit Parser(std::istream& is) : lexer_(is), token_(TokenType::ERROR) { nextKey(); } + + // Parses. If failed, value should be invalid value. + // You can get the error by calling errorReason(). + Value parse(); + const std::string& errorReason(); + +private: + const Token& token() const { return token_; } + void nextKey() { token_ = lexer_.nextKeyToken(); } + void nextValue() { token_ = lexer_.nextValueToken(); } + + void skipForKey(); + void skipForValue(); + + bool consumeForKey(TokenType); + bool consumeForValue(TokenType); + bool consumeEOLorEOFForKey(); + + Value* parseGroupKey(Value* root); + + bool parseKeyValue(Value*); + bool parseKey(std::string*); + bool parseValue(Value*); + bool parseBool(Value*); + bool parseNumber(Value*); + bool parseArray(Value*); + bool parseInlineTable(Value*); + + void addError(const std::string& reason); + + Lexer lexer_; + Token token_; + std::string errorReason_; +}; + +} // namespace internal + +// ---------------------------------------------------------------------- +// Implementations + +inline ParseResult parse(std::istream& is) +{ + internal::Parser parser(is); + toml::Value v = parser.parse(); + + if (v.valid()) + return ParseResult(std::move(v), std::string()); + + return ParseResult(std::move(v), std::move(parser.errorReason())); +} + +inline std::string format(std::stringstream& ss) +{ + return ss.str(); +} + +template<typename T, typename... Args> +std::string format(std::stringstream& ss, T&& t, Args&&... args) +{ + ss << std::forward<T>(t); + return format(ss, std::forward<Args>(args)...); +} + +template<typename... Args> +#if defined(_MSC_VER) +__declspec(noreturn) +#else +[[noreturn]] +#endif +void failwith(Args&&... args) +{ + std::stringstream ss; + throw std::runtime_error(format(ss, std::forward<Args>(args)...)); +} + +namespace internal { + +inline std::string removeDelimiter(const std::string& s) +{ + std::string r; + for (char c : s) { + if (c == '_') + continue; + r += c; + } + return r; +} + +inline std::string unescape(const std::string& codepoint) +{ + std::uint32_t x; + std::uint8_t buf[8]; + std::stringstream ss(codepoint); + + ss >> std::hex >> x; + + if (x <= 0x7FUL) { + // 0xxxxxxx + buf[0] = 0x00 | ((x >> 0) & 0x7F); + buf[1] = '\0'; + } else if (x <= 0x7FFUL) { + // 110yyyyx 10xxxxxx + buf[0] = 0xC0 | ((x >> 6) & 0xDF); + buf[1] = 0x80 | ((x >> 0) & 0xBF); + buf[2] = '\0'; + } else if (x <= 0xFFFFUL) { + // 1110yyyy 10yxxxxx 10xxxxxx + buf[0] = 0xE0 | ((x >> 12) & 0xEF); + buf[1] = 0x80 | ((x >> 6) & 0xBF); + buf[2] = 0x80 | ((x >> 0) & 0xBF); + buf[3] = '\0'; + } else if (x <= 0x10FFFFUL) { + // 11110yyy 10yyxxxx 10xxxxxx 10xxxxxx + buf[0] = 0xF0 | ((x >> 18) & 0xF7); + buf[1] = 0x80 | ((x >> 12) & 0xBF); + buf[2] = 0x80 | ((x >> 6) & 0xBF); + buf[3] = 0x80 | ((x >> 0) & 0xBF); + buf[4] = '\0'; + } else { + buf[0] = '\0'; + } + + return reinterpret_cast<char*>(buf); +} + +// Returns true if |s| is integer. +// [+-]?\d+(_\d+)* +inline bool isInteger(const std::string& s) +{ + if (s.empty()) + return false; + + std::string::size_type p = 0; + if (s[p] == '+' || s[p] == '-') + ++p; + + while (p < s.size() && '0' <= s[p] && s[p] <= '9') { + ++p; + if (p < s.size() && s[p] == '_') { + ++p; + if (!(p < s.size() && '0' <= s[p] && s[p] <= '9')) + return false; + } + } + + return p == s.size(); +} + +// Returns true if |s| is double. +// [+-]? (\d+(_\d+)*)? (\.\d+(_\d+)*)? ([eE] [+-]? \d+(_\d+)*)? +// 1----------- 2------------- 3---------------------- +// 2 or (1 and 3) should exist. +inline bool isDouble(const std::string& s) +{ + if (s.empty()) + return false; + + std::string::size_type p = 0; + if (s[p] == '+' || s[p] == '-') + ++p; + + bool ok = false; + while (p < s.size() && '0' <= s[p] && s[p] <= '9') { + ++p; + ok = true; + + if (p < s.size() && s[p] == '_') { + ++p; + if (!(p < s.size() && '0' <= s[p] && s[p] <= '9')) + return false; + } + } + + if (p < s.size() && s[p] == '.') + ++p; + + while (p < s.size() && '0' <= s[p] && s[p] <= '9') { + ++p; + ok = true; + + if (p < s.size() && s[p] == '_') { + ++p; + if (!(p < s.size() && '0' <= s[p] && s[p] <= '9')) + return false; + } + } + + if (!ok) + return false; + + ok = false; + if (p < s.size() && (s[p] == 'e' || s[p] == 'E')) { + ++p; + if (p < s.size() && (s[p] == '+' || s[p] == '-')) + ++p; + while (p < s.size() && '0' <= s[p] && s[p] <= '9') { + ++p; + ok = true; + + if (p < s.size() && s[p] == '_') { + ++p; + if (!(p < s.size() && '0' <= s[p] && s[p] <= '9')) + return false; + } + } + if (!ok) + return false; + } + + return p == s.size(); +} + +// static +inline std::string escapeString(const std::string& s) +{ + std::stringstream ss; + for (size_t i = 0; i < s.size(); ++i) { + switch (s[i]) { + case '\n': ss << "\\n"; break; + case '\r': ss << "\\r"; break; + case '\t': ss << "\\t"; break; + case '\"': ss << "\\\""; break; + case '\'': ss << "\\\'"; break; + case '\\': ss << "\\\\"; break; + default: ss << s[i]; break; + } + } + + return ss.str(); +} + +} // namespace internal + +// ---------------------------------------------------------------------- +// Lexer + +namespace internal { + +inline bool Lexer::current(char* c) +{ + int x = is_.peek(); + if (x == EOF) + return false; + *c = static_cast<char>(x); + return true; +} + +inline void Lexer::next() +{ + int x = is_.get(); + if (x == '\n') + ++lineNo_; +} + +inline bool Lexer::consume(char c) +{ + char x; + if (!current(&x)) + return false; + if (x != c) + return false; + next(); + return true; +} + +inline void Lexer::skipUntilNewLine() +{ + char c; + while (current(&c)) { + if (c == '\n') + return; + next(); + } +} + +inline Token Lexer::nextStringDoubleQuote() +{ + if (!consume('"')) + return Token(TokenType::ERROR, std::string("string didn't start with '\"'")); + + std::string s; + char c; + bool multiline = false; + + if (current(&c) && c == '"') { + next(); + if (!current(&c) || c != '"') { + // OK. It's empty string. + return Token(TokenType::STRING, std::string()); + } + + next(); + // raw string literal started. + // Newline just after """ should be ignored. + while (current(&c) && (c == ' ' || c == '\t')) + next(); + if (current(&c) && c == '\n') + next(); + multiline = true; + } + + while (current(&c)) { + next(); + if (c == '\\') { + if (!current(&c)) + return Token(TokenType::ERROR, std::string("string has unknown escape sequence")); + next(); + switch (c) { + case 't': c = '\t'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 'u': + case 'U': { + int size = c == 'u' ? 4 : 8; + std::string codepoint; + for (int i = 0; i < size; ++i) { + if (current(&c) && (('0' <= c && c <= '9') || ('A' <= c && c <= 'F') || ('a' <= c && c <= 'f'))) { + codepoint += c; + next(); + } else { + return Token(TokenType::ERROR, std::string("string has unknown escape sequence")); + } + } + s += unescape(codepoint); + continue; + } + case '"': c = '"'; break; + case '\'': c = '\''; break; + case '\\': c = '\\'; break; + case '\n': + while (current(&c) && (c == ' ' || c == '\t' || c == '\r' || c == '\n')) { + next(); + } + continue; + default: + return Token(TokenType::ERROR, std::string("string has unknown escape sequence")); + } + } else if (c == '"') { + if (multiline) { + if (current(&c) && c == '"') { + next(); + if (current(&c) && c == '"') { + next(); + return Token(TokenType::MULTILINE_STRING, s); + } else { + s += '"'; + s += '"'; + continue; + } + } else { + s += '"'; + continue; + } + } else { + return Token(TokenType::STRING, s); + } + } + + s += c; + } + + return Token(TokenType::ERROR, std::string("string didn't end")); +} + +inline Token Lexer::nextStringSingleQuote() +{ + if (!consume('\'')) + return Token(TokenType::ERROR, std::string("string didn't start with '\''?")); + + std::string s; + char c; + + if (current(&c) && c == '\'') { + next(); + if (!current(&c) || c != '\'') { + // OK. It's empty string. + return Token(TokenType::STRING, std::string()); + } + next(); + // raw string literal started. + // Newline just after """ should be ignored. + if (current(&c) && c == '\n') + next(); + + while (current(&c)) { + if (c == '\'') { + next(); + if (current(&c) && c == '\'') { + next(); + if (current(&c) && c == '\'') { + next(); + return Token(TokenType::MULTILINE_STRING, s); + } else { + s += '\''; + s += '\''; + continue; + } + } else { + s += '\''; + continue; + } + } + + next(); + s += c; + continue; + } + + return Token(TokenType::ERROR, std::string("string didn't end with '\'\'\'' ?")); + } + + while (current(&c)) { + next(); + if (c == '\'') { + return Token(TokenType::STRING, s); + } + + s += c; + } + + return Token(TokenType::ERROR, std::string("string didn't end with '\''?")); +} + +inline Token Lexer::nextKey() +{ + std::string s; + char c; + while (current(&c) && (isalnum(c) || c == '_' || c == '-')) { + s += c; + next(); + } + + if (s.empty()) + return Token(TokenType::ERROR, std::string("Unknown key format")); + + return Token(TokenType::IDENT, s); +} + +inline Token Lexer::nextValue() +{ + std::string s; + char c; + + if (current(&c) && isalpha(c)) { + s += c; + next(); + while (current(&c) && isalpha(c)) { + s += c; + next(); + } + + if (s == "true") + return Token(TokenType::BOOL, true); + if (s == "false") + return Token(TokenType::BOOL, false); + return Token(TokenType::ERROR, std::string("Unknown ident: ") + s); + } + + while (current(&c) && (('0' <= c && c <= '9') || c == '.' || c == 'e' || c == 'E' || + c == 'T' || c == 'Z' || c == '_' || c == ':' || c == '-' || c == '+')) { + next(); + s += c; + } + + if (isInteger(s)) { + std::stringstream ss(removeDelimiter(s)); + std::int64_t x; + ss >> x; + return Token(TokenType::INT, x); + } + + if (isDouble(s)) { + std::stringstream ss(removeDelimiter(s)); + double d; + ss >> d; + return Token(TokenType::DOUBLE, d); + } + + return parseAsTime(s); +} + +inline Token Lexer::parseAsTime(const std::string& str) +{ + const char* s = str.c_str(); + + int n; + int YYYY, MM, DD; + if (sscanf(s, "%d-%d-%d%n", &YYYY, &MM, &DD, &n) != 3) + return Token(TokenType::ERROR, std::string("Invalid token")); + + if (s[n] == '\0') { + std::tm t; + t.tm_sec = 0; + t.tm_min = 0; + t.tm_hour = 0; + t.tm_mday = DD; + t.tm_mon = MM - 1; + t.tm_year = YYYY - 1900; + auto tp = std::chrono::system_clock::from_time_t(timegm(&t)); + return Token(TokenType::TIME, tp); + } + + if (s[n] != 'T') + return Token(TokenType::ERROR, std::string("Invalid token")); + + s = s + n + 1; + + int hh, mm; + double ss; // double for fraction + if (sscanf(s, "%d:%d:%lf%n", &hh, &mm, &ss, &n) != 3) + return Token(TokenType::ERROR, std::string("Invalid token")); + + std::tm t; + t.tm_sec = static_cast<int>(ss); + t.tm_min = mm; + t.tm_hour = hh; + t.tm_mday = DD; + t.tm_mon = MM - 1; + t.tm_year = YYYY - 1900; + auto tp = std::chrono::system_clock::from_time_t(timegm(&t)); + ss -= static_cast<int>(ss); + // TODO(mayah): workaround GCC 4.9.3 on cygwin does not have std::round, but round(). + tp += std::chrono::microseconds(static_cast<std::int64_t>(round(ss * 1000000))); + + if (s[n] == '\0') + return Token(TokenType::TIME, tp); + + if (s[n] == 'Z' && s[n + 1] == '\0') + return Token(TokenType::TIME, tp); + + s = s + n; + // offset + // [+/-]%d:%d + char pn; + int oh, om; + if (sscanf(s, "%c%d:%d", &pn, &oh, &om) != 3) + return Token(TokenType::ERROR, std::string("Invalid token")); + + if (pn != '+' && pn != '-') + return Token(TokenType::ERROR, std::string("Invalid token")); + + if (pn == '+') { + tp -= std::chrono::hours(oh); + tp -= std::chrono::minutes(om); + } else { + tp += std::chrono::hours(oh); + tp += std::chrono::minutes(om); + } + + return Token(TokenType::TIME, tp); +} + +inline Token Lexer::nextKeyToken() +{ + return nextToken(false); +} + +inline Token Lexer::nextValueToken() +{ + return nextToken(true); +} + +inline Token Lexer::nextToken(bool isValueToken) +{ + char c; + while (current(&c)) { + if (c == ' ' || c == '\t' || c == '\r') { + next(); + continue; + } + + if (c == '#') { + skipUntilNewLine(); + continue; + } + + switch (c) { + case '\n': + next(); + return Token(TokenType::END_OF_LINE); + case '=': + next(); + return Token(TokenType::EQUAL); + case '{': + next(); + return Token(TokenType::LBRACE); + case '}': + next(); + return Token(TokenType::RBRACE); + case '[': + next(); + return Token(TokenType::LBRACKET); + case ']': + next(); + return Token(TokenType::RBRACKET); + case ',': + next(); + return Token(TokenType::COMMA); + case '.': + next(); + return Token(TokenType::DOT); + case '\"': + return nextStringDoubleQuote(); + case '\'': + return nextStringSingleQuote(); + default: + if (isValueToken) { + return nextValue(); + } else { + return nextKey(); + } + } + } + + return Token(TokenType::END_OF_FILE); +} + +} // namespace internal + +// ---------------------------------------------------------------------- + +// static +inline const char* Value::typeToString(Value::Type type) +{ + switch (type) { + case NULL_TYPE: return "null"; + case BOOL_TYPE: return "bool"; + case INT_TYPE: return "int"; + case DOUBLE_TYPE: return "double"; + case STRING_TYPE: return "string"; + case TIME_TYPE: return "time"; + case ARRAY_TYPE: return "array"; + case TABLE_TYPE: return "table"; + default: return "unknown"; + } +} + +inline Value::Value(const Value& v) : + type_(v.type_) +{ + switch (v.type_) { + case NULL_TYPE: null_ = v.null_; break; + case BOOL_TYPE: bool_ = v.bool_; break; + case INT_TYPE: int_ = v.int_; break; + case DOUBLE_TYPE: double_ = v.double_; break; + case STRING_TYPE: string_ = new std::string(*v.string_); break; + case TIME_TYPE: time_ = new Time(*v.time_); break; + case ARRAY_TYPE: array_ = new Array(*v.array_); break; + case TABLE_TYPE: table_ = new Table(*v.table_); break; + default: + assert(false); + type_ = NULL_TYPE; + null_ = nullptr; + } +} + +inline Value::Value(Value&& v) noexcept : + type_(v.type_) +{ + switch (v.type_) { + case NULL_TYPE: null_ = v.null_; break; + case BOOL_TYPE: bool_ = v.bool_; break; + case INT_TYPE: int_ = v.int_; break; + case DOUBLE_TYPE: double_ = v.double_; break; + case STRING_TYPE: string_ = v.string_; break; + case TIME_TYPE: time_ = v.time_; break; + case ARRAY_TYPE: array_ = v.array_; break; + case TABLE_TYPE: table_ = v.table_; break; + default: + assert(false); + type_ = NULL_TYPE; + null_ = nullptr; + } + + v.type_ = NULL_TYPE; + v.null_ = nullptr; +} + +inline Value& Value::operator=(const Value& v) +{ + if (this == &v) + return *this; + + this->~Value(); + + type_ = v.type_; + switch (v.type_) { + case NULL_TYPE: null_ = v.null_; break; + case BOOL_TYPE: bool_ = v.bool_; break; + case INT_TYPE: int_ = v.int_; break; + case DOUBLE_TYPE: double_ = v.double_; break; + case STRING_TYPE: string_ = new std::string(*v.string_); break; + case TIME_TYPE: time_ = new Time(*v.time_); break; + case ARRAY_TYPE: array_ = new Array(*v.array_); break; + case TABLE_TYPE: table_ = new Table(*v.table_); break; + default: + assert(false); + type_ = NULL_TYPE; + null_ = nullptr; + } + + return *this; +} + +inline Value& Value::operator=(Value&& v) noexcept +{ + if (this == &v) + return *this; + + this->~Value(); + + type_ = v.type_; + switch (v.type_) { + case NULL_TYPE: null_ = v.null_; break; + case BOOL_TYPE: bool_ = v.bool_; break; + case INT_TYPE: int_ = v.int_; break; + case DOUBLE_TYPE: double_ = v.double_; break; + case STRING_TYPE: string_ = v.string_; break; + case TIME_TYPE: time_ = v.time_; break; + case ARRAY_TYPE: array_ = v.array_; break; + case TABLE_TYPE: table_ = v.table_; break; + default: + assert(false); + type_ = NULL_TYPE; + null_ = nullptr; + } + + v.type_ = NULL_TYPE; + v.null_ = nullptr; + return *this; +} + +inline Value::~Value() +{ + switch (type_) { + case STRING_TYPE: + delete string_; + break; + case TIME_TYPE: + delete time_; + break; + case ARRAY_TYPE: + delete array_; + break; + case TABLE_TYPE: + delete table_; + break; + default: + break; + } +} + +inline size_t Value::size() const +{ + switch (type_) { + case NULL_TYPE: + return 0; + case ARRAY_TYPE: + return array_->size(); + case TABLE_TYPE: + return table_->size(); + default: + return 1; + } +} + +inline bool Value::empty() const +{ + return size() == 0; +} + +template<> struct Value::ValueConverter<bool> +{ + bool is(const Value& v) { return v.type() == Value::BOOL_TYPE; } + bool to(const Value& v) { v.assureType<bool>(); return v.bool_; } + +}; +template<> struct Value::ValueConverter<int64_t> +{ + bool is(const Value& v) { return v.type() == Value::INT_TYPE; } + int64_t to(const Value& v) { v.assureType<int64_t>(); return v.int_; } +}; +template<> struct Value::ValueConverter<int> +{ + bool is(const Value& v) { return v.type() == Value::INT_TYPE; } + int to(const Value& v) { v.assureType<int>(); return static_cast<int>(v.int_); } +}; +template<> struct Value::ValueConverter<double> +{ + bool is(const Value& v) { return v.type() == Value::DOUBLE_TYPE; } + double to(const Value& v) { v.assureType<double>(); return v.double_; } +}; +template<> struct Value::ValueConverter<std::string> +{ + bool is(const Value& v) { return v.type() == Value::STRING_TYPE; } + const std::string& to(const Value& v) { v.assureType<std::string>(); return *v.string_; } +}; +template<> struct Value::ValueConverter<Time> +{ + bool is(const Value& v) { return v.type() == Value::TIME_TYPE; } + const Time& to(const Value& v) { v.assureType<Time>(); return *v.time_; } +}; +template<> struct Value::ValueConverter<Array> +{ + bool is(const Value& v) { return v.type() == Value::ARRAY_TYPE; } + const Array& to(const Value& v) { v.assureType<Array>(); return *v.array_; } +}; +template<> struct Value::ValueConverter<Table> +{ + bool is(const Value& v) { return v.type() == Value::TABLE_TYPE; } + const Table& to(const Value& v) { v.assureType<Table>(); return *v.table_; } +}; + +template<typename T> +struct Value::ValueConverter<std::vector<T>> +{ + bool is(const Value& v) + { + if (v.type() != Value::ARRAY_TYPE) + return false; + const Array& array = v.as<Array>(); + if (array.empty()) + return true; + return array.front().is<T>(); + } + + std::vector<T> to(const Value& v) + { + const Array& array = v.as<Array>(); + if (array.empty()) + return std::vector<T>(); + array.front().assureType<T>(); + + std::vector<T> result; + for (const auto& element : array) { + result.push_back(element.as<T>()); + } + + return result; + } +}; + +namespace internal { +template<typename T> inline const char* type_name(); +template<> inline const char* type_name<bool>() { return "bool"; } +template<> inline const char* type_name<int>() { return "int"; } +template<> inline const char* type_name<int64_t>() { return "int64_t"; } +template<> inline const char* type_name<double>() { return "double"; } +template<> inline const char* type_name<std::string>() { return "string"; } +template<> inline const char* type_name<toml::Time>() { return "time"; } +template<> inline const char* type_name<toml::Array>() { return "array"; } +template<> inline const char* type_name<toml::Table>() { return "table"; } +} // namespace internal + +template<typename T> +inline void Value::assureType() const +{ + if (!is<T>()) + failwith("type error: this value is ", typeToString(type_), " but ", internal::type_name<T>(), " was requested"); +} + +template<typename T> +inline bool Value::is() const +{ + return ValueConverter<T>().is(*this); +} + +template<typename T> +inline typename call_traits<T>::return_type Value::as() const +{ + return ValueConverter<T>().to(*this); +} + +inline bool Value::isNumber() const +{ + return is<int>() || is<double>(); +} + +inline double Value::asNumber() const +{ + if (is<int>()) + return as<int>(); + if (is<double>()) + return as<double>(); + + failwith("type error: this value is ", typeToString(type_), " but number is requested"); +} + +inline std::time_t Value::as_time_t() const +{ + return std::chrono::system_clock::to_time_t(as<Time>()); +} + +inline std::string Value::spaces(int num) +{ + if (num <= 0) + return std::string(); + + return std::string(num, ' '); +} + +inline std::string Value::escapeKey(const std::string& key) +{ + auto position = std::find_if(key.begin(), key.end(), [](char c) -> bool { + if (std::isalnum(c) || c == '_' || c == '-') + return false; + return true; + }); + + if (position != key.end()) { + std::string escaped = "\""; + for (const char& c : key) { + if (c == '\\' || c == '"') + escaped += '\\'; + escaped += c; + } + escaped += "\""; + + return escaped; + } + + return key; +} + +inline void Value::write(std::ostream* os, const std::string& keyPrefix, int indent) const +{ + switch (type_) { + case NULL_TYPE: + failwith("null type value is not a valid value"); + break; + case BOOL_TYPE: + (*os) << (bool_ ? "true" : "false"); + break; + case INT_TYPE: + (*os) << int_; + break; + case DOUBLE_TYPE: { + (*os) << std::fixed << std::showpoint << double_; + break; + } + case STRING_TYPE: + (*os) << '"' << internal::escapeString(*string_) << '"'; + break; + case TIME_TYPE: { + time_t tt = std::chrono::system_clock::to_time_t(*time_); + std::tm t; + gmtime_r(&tt, &t); + char buf[256]; + sprintf(buf, "%04d-%02d-%02dT%02d:%02d:%02dZ", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec); + (*os) << buf; + break; + } + case ARRAY_TYPE: + (*os) << '['; + for (size_t i = 0; i < array_->size(); ++i) { + if (i) + (*os) << ", "; + (*array_)[i].write(os, keyPrefix, -1); + } + (*os) << ']'; + break; + case TABLE_TYPE: + for (const auto& kv : *table_) { + if (kv.second.is<Table>()) + continue; + if (kv.second.is<Array>() && kv.second.size() > 0 && kv.second.find(0)->is<Table>()) + continue; + (*os) << spaces(indent) << escapeKey(kv.first) << " = "; + kv.second.write(os, keyPrefix, indent >= 0 ? indent + 1 : indent); + (*os) << '\n'; + } + for (const auto& kv : *table_) { + if (kv.second.is<Table>()) { + std::string key(keyPrefix); + if (!keyPrefix.empty()) + key += "."; + key += escapeKey(kv.first); + (*os) << "\n" << spaces(indent) << "[" << key << "]\n"; + kv.second.write(os, key, indent >= 0 ? indent + 1 : indent); + } + if (kv.second.is<Array>() && kv.second.size() > 0 && kv.second.find(0)->is<Table>()) { + std::string key(keyPrefix); + if (!keyPrefix.empty()) + key += "."; + key += escapeKey(kv.first); + for (const auto& v : kv.second.as<Array>()) { + (*os) << "\n" << spaces(indent) << "[[" << key << "]]\n"; + v.write(os, key, indent >= 0 ? indent + 1 : indent); + } + } + } + break; + default: + failwith("writing unknown type"); + break; + } +} + +inline void Value::writeFormatted(std::ostream* os, FormatFlag flags) const +{ + int indent = flags & FORMAT_INDENT ? 0 : -1; + + write(os, std::string(), indent); +} + +// static +inline FormatFlag operator|(FormatFlag lhs, FormatFlag rhs) +{ + return static_cast<FormatFlag>(static_cast<int>(lhs) | static_cast<int>(rhs)); +} + +// static +inline std::ostream& operator<<(std::ostream& os, const toml::Value& v) +{ + v.write(&os); + return os; +} + +// static +inline bool operator==(const Value& lhs, const Value& rhs) +{ + if (lhs.type() != rhs.type()) + return false; + + switch (lhs.type()) { + case Value::Type::NULL_TYPE: + return true; + case Value::Type::BOOL_TYPE: + return lhs.bool_ == rhs.bool_; + case Value::Type::INT_TYPE: + return lhs.int_ == rhs.int_; + case Value::Type::DOUBLE_TYPE: + return lhs.double_ == rhs.double_; + case Value::Type::STRING_TYPE: + return *lhs.string_ == *rhs.string_; + case Value::Type::TIME_TYPE: + return *lhs.time_ == *rhs.time_; + case Value::Type::ARRAY_TYPE: + return *lhs.array_ == *rhs.array_; + case Value::Type::TABLE_TYPE: + return *lhs.table_ == *rhs.table_; + default: + failwith("unknown type"); + } +} + +template<typename T> +inline typename call_traits<T>::return_type Value::get(const std::string& key) const +{ + if (!is<Table>()) + failwith("type must be table to do get(key)."); + + const Value* obj = find(key); + if (!obj) + failwith("key ", key, " was not found."); + + return obj->as<T>(); +} + +inline const Value* Value::find(const std::string& key) const +{ + if (!is<Table>()) + return nullptr; + + std::istringstream ss(key); + internal::Lexer lexer(ss); + + const Value* current = this; + while (true) { + internal::Token t = lexer.nextKeyToken(); + if (!(t.type() == internal::TokenType::IDENT || t.type() == internal::TokenType::STRING)) + return nullptr; + + std::string part = t.strValue(); + t = lexer.nextKeyToken(); + if (t.type() == internal::TokenType::DOT) { + current = current->findChild(part); + if (!current || !current->is<Table>()) + return nullptr; + } else if (t.type() == internal::TokenType::END_OF_FILE) { + return current->findChild(part); + } else { + return nullptr; + } + } +} + +inline Value* Value::find(const std::string& key) +{ + return const_cast<Value*>(const_cast<const Value*>(this)->find(key)); +} + +inline bool Value::merge(const toml::Value& v) +{ + if (this == &v) + return true; + if (!is<Table>() || !v.is<Table>()) + return false; + + for (const auto& kv : *v.table_) { + if (Value* tmp = find(kv.first)) { + // If both are table, we merge them. + if (tmp->is<Table>() && kv.second.is<Table>()) { + if (!tmp->merge(kv.second)) + return false; + } else { + setChild(kv.first, kv.second); + } + } else { + setChild(kv.first, kv.second); + } + } + + return true; +} + +inline Value* Value::set(const std::string& key, const Value& v) +{ + Value* result = ensureValue(key); + *result = v; + return result; +} + +inline Value* Value::setChild(const std::string& key, const Value& v) +{ + if (!valid()) + *this = Value((Table())); + + if (!is<Table>()) + failwith("type must be table to do set(key, v)."); + + (*table_)[key] = v; + return &(*table_)[key]; +} + +inline Value* Value::setChild(const std::string& key, Value&& v) +{ + if (!valid()) + *this = Value((Table())); + + if (!is<Table>()) + failwith("type must be table to do set(key, v)."); + + (*table_)[key] = std::move(v); + return &(*table_)[key]; +} + +inline bool Value::erase(const std::string& key) +{ + if (!is<Table>()) + return false; + + std::istringstream ss(key); + internal::Lexer lexer(ss); + + Value* current = this; + while (true) { + internal::Token t = lexer.nextKeyToken(); + if (!(t.type() == internal::TokenType::IDENT || t.type() == internal::TokenType::STRING)) + return false; + + std::string part = t.strValue(); + t = lexer.nextKeyToken(); + if (t.type() == internal::TokenType::DOT) { + current = current->findChild(part); + if (!current || !current->is<Table>()) + return false; + } else if (t.type() == internal::TokenType::END_OF_FILE) { + return current->eraseChild(part); + } else { + return false; + } + } +} + +inline bool Value::eraseChild(const std::string& key) +{ + if (!is<Table>()) + failwith("type must be table to do erase(key)."); + + return table_->erase(key) > 0; +} + +inline Value& Value::operator[](const std::string& key) +{ + if (!valid()) + *this = Value((Table())); + + if (Value* v = findChild(key)) + return *v; + + return *setChild(key, Value()); +} + +template<typename T> +inline typename call_traits<T>::return_type Value::get(size_t index) const +{ + if (!is<Array>()) + failwith("type must be array to do get(index)."); + + if (array_->size() <= index) + failwith("index out of bound"); + + return (*array_)[index].as<T>(); +} + +inline const Value* Value::find(size_t index) const +{ + if (!is<Array>()) + return nullptr; + if (index < array_->size()) + return &(*array_)[index]; + return nullptr; +} + +inline Value* Value::find(size_t index) +{ + return const_cast<Value*>(const_cast<const Value*>(this)->find(index)); +} + +inline Value* Value::push(const Value& v) +{ + if (!valid()) + *this = Value((Array())); + else if (!is<Array>()) + failwith("type must be array to do push(Value)."); + + array_->push_back(v); + return &array_->back(); +} + +inline Value* Value::push(Value&& v) +{ + if (!valid()) + *this = Value((Array())); + else if (!is<Array>()) + failwith("type must be array to do push(Value)."); + + array_->push_back(std::move(v)); + return &array_->back(); +} + +inline Value* Value::ensureValue(const std::string& key) +{ + if (!valid()) + *this = Value((Table())); + if (!is<Table>()) { + failwith("encountered non table value"); + } + + std::istringstream ss(key); + internal::Lexer lexer(ss); + + Value* current = this; + while (true) { + internal::Token t = lexer.nextKeyToken(); + if (!(t.type() == internal::TokenType::IDENT || t.type() == internal::TokenType::STRING)) { + failwith("invalid key"); + } + + std::string part = t.strValue(); + t = lexer.nextKeyToken(); + if (t.type() == internal::TokenType::DOT) { + if (Value* candidate = current->findChild(part)) { + if (!candidate->is<Table>()) + failwith("encountered non table value"); + + current = candidate; + } else { + current = current->setChild(part, Table()); + } + } else if (t.type() == internal::TokenType::END_OF_FILE) { + if (Value* v = current->findChild(part)) + return v; + return current->setChild(part, Value()); + } else { + failwith("invalid key"); + } + } +} + +inline Value* Value::findChild(const std::string& key) +{ + assert(is<Table>()); + + auto it = table_->find(key); + if (it == table_->end()) + return nullptr; + + return &it->second; +} + +inline const Value* Value::findChild(const std::string& key) const +{ + assert(is<Table>()); + + auto it = table_->find(key); + if (it == table_->end()) + return nullptr; + + return &it->second; +} + +// ---------------------------------------------------------------------- + +namespace internal { + +inline void Parser::skipForKey() +{ + while (token().type() == TokenType::END_OF_LINE) + nextKey(); +} + +inline void Parser::skipForValue() +{ + while (token().type() == TokenType::END_OF_LINE) + nextValue(); +} + +inline bool Parser::consumeForKey(TokenType type) +{ + if (token().type() == type) { + nextKey(); + return true; + } + + return false; +} + +inline bool Parser::consumeForValue(TokenType type) +{ + if (token().type() == type) { + nextValue(); + return true; + } + + return false; +} + +inline bool Parser::consumeEOLorEOFForKey() +{ + if (token().type() == TokenType::END_OF_LINE || token().type() == TokenType::END_OF_FILE) { + nextKey(); + return true; + } + + return false; +} + +inline void Parser::addError(const std::string& reason) +{ + if (!errorReason_.empty()) + return; + + std::stringstream ss; + ss << "Error: line " << lexer_.lineNo() << ": " << reason; + errorReason_ = ss.str(); +} + +inline const std::string& Parser::errorReason() +{ + return errorReason_; +} + +inline Value Parser::parse() +{ + Value root((Table())); + Value* currentValue = &root; + + while (true) { + skipForKey(); + if (token().type() == TokenType::END_OF_FILE) + break; + if (token().type() == TokenType::LBRACKET) { + currentValue = parseGroupKey(&root); + if (!currentValue) { + addError("error when parsing group key"); + return Value(); + } + continue; + } + + if (!parseKeyValue(currentValue)) { + addError("error when parsing key Value"); + return Value(); + } + } + return root; +} + +inline Value* Parser::parseGroupKey(Value* root) +{ + if (!consumeForKey(TokenType::LBRACKET)) + return nullptr; + + bool isArray = false; + if (token().type() == TokenType::LBRACKET) { + nextKey(); + isArray = true; + } + + Value* currentValue = root; + while (true) { + if (token().type() != TokenType::IDENT && token().type() != TokenType::STRING) + return nullptr; + + std::string key = token().strValue(); + nextKey(); + + if (token().type() == TokenType::DOT) { + nextKey(); + if (Value* candidate = currentValue->findChild(key)) { + if (candidate->is<Array>() && candidate->size() > 0) + candidate = candidate->find(candidate->size() - 1); + if (!candidate->is<Table>()) + return nullptr; + currentValue = candidate; + } else { + currentValue = currentValue->setChild(key, Table()); + } + continue; + } + + if (token().type() == TokenType::RBRACKET) { + nextKey(); + if (Value* candidate = currentValue->findChild(key)) { + if (isArray) { + if (!candidate->is<Array>()) + return nullptr; + currentValue = candidate->push(Table()); + } else { + if (candidate->is<Array>() && candidate->size() > 0) + candidate = candidate->find(candidate->size() - 1); + if (!candidate->is<Table>()) + return nullptr; + currentValue = candidate; + } + } else { + if (isArray) { + currentValue = currentValue->setChild(key, Array()); + currentValue = currentValue->push(Table()); + } else { + currentValue = currentValue->setChild(key, Table()); + } + } + break; + } + + return nullptr; + } + + if (isArray) { + if (!consumeForKey(TokenType::RBRACKET)) + return nullptr; + } + + if (!consumeEOLorEOFForKey()) + return nullptr; + + return currentValue; +} + +inline bool Parser::parseKeyValue(Value* current) +{ + std::string key; + if (!parseKey(&key)) { + addError("parse key failed"); + return false; + } + if (!consumeForValue(TokenType::EQUAL)) { + addError("no equal?"); + return false; + } + + Value v; + if (!parseValue(&v)) + return false; + if (!consumeEOLorEOFForKey()) + return false; + + if (current->has(key)) { + addError("Multiple same key: " + key); + return false; + } + + current->setChild(key, std::move(v)); + return true; +} + +inline bool Parser::parseKey(std::string* key) +{ + key->clear(); + + if (token().type() == TokenType::IDENT || token().type() == TokenType::STRING) { + *key = token().strValue(); + nextValue(); + return true; + } + + return false; +} + +inline bool Parser::parseValue(Value* v) +{ + switch (token().type()) { + case TokenType::STRING: + case TokenType::MULTILINE_STRING: + *v = token().strValue(); + nextValue(); + return true; + case TokenType::LBRACKET: + return parseArray(v); + case TokenType::LBRACE: + return parseInlineTable(v); + case TokenType::BOOL: + *v = token().boolValue(); + nextValue(); + return true; + case TokenType::INT: + *v = token().intValue(); + nextValue(); + return true; + case TokenType::DOUBLE: + *v = token().doubleValue(); + nextValue(); + return true; + case TokenType::TIME: + *v = token().timeValue(); + nextValue(); + return true; + case TokenType::ERROR: + addError(token().strValue()); + return false; + default: + addError("unexpected token"); + return false; + } +} + +inline bool Parser::parseBool(Value* v) +{ + if (token().strValue() == "true") { + nextValue(); + *v = true; + return true; + } + + if (token().strValue() == "false") { + nextValue(); + *v = false; + return true; + } + + return false; +} + +inline bool Parser::parseArray(Value* v) +{ + if (!consumeForValue(TokenType::LBRACKET)) + return false; + + Array a; + while (true) { + skipForValue(); + + if (token().type() == TokenType::RBRACKET) + break; + + skipForValue(); + Value x; + if (!parseValue(&x)) + return false; + + if (!a.empty()) { + if (a.front().type() != x.type()) { + addError("type check failed"); + return false; + } + } + + a.push_back(std::move(x)); + skipForValue(); + if (token().type() == TokenType::RBRACKET) + break; + if (token().type() == TokenType::COMMA) + nextValue(); + } + + if (!consumeForValue(TokenType::RBRACKET)) + return false; + *v = std::move(a); + return true; +} + +inline bool Parser::parseInlineTable(Value* value) +{ + // For inline table, next is KEY, so use consumeForKey here. + if (!consumeForKey(TokenType::LBRACE)) + return false; + + Value t((Table())); + bool first = true; + while (true) { + if (token().type() == TokenType::RBRACE) { + break; + } + + if (!first) { + if (token().type() != TokenType::COMMA) { + addError("inline table didn't have ',' for delimiter?"); + return false; + } + nextKey(); + } + first = false; + + std::string key; + if (!parseKey(&key)) + return false; + if (!consumeForValue(TokenType::EQUAL)) + return false; + Value v; + if (!parseValue(&v)) + return false; + + if (t.has(key)) { + addError("inline table has multiple same keys: key=" + key); + return false; + } + + t.set(key, v); + } + + if (!consumeForValue(TokenType::RBRACE)) + return false; + *value = std::move(t); + return true; +} + +} // namespace internal +} // namespace toml + +#endif // TINYTOML_H_ diff --git a/src/browser.cpp b/src/browser.cpp index a1c097a..f709e1e 100644 --- a/src/browser.cpp +++ b/src/browser.cpp @@ -20,7 +20,6 @@ #include "browser.h" #include "mainwindow.h" -#include "settings.h" #include <QtWebEngine> #include <QMessageBox> @@ -47,12 +46,11 @@ Browser::~Browser() */ void Browser::firstRun() { - Settings settings; - if(settings.allKeys().isEmpty()) { + if(m_settings->isEmpty()) { // There are no keys in the settings QMessageBox::information(0, tr("Configuration is empty"), - tr("The configuration file <i>%1</i> is empty. Using default values").arg(settings.staticFilePath())); + tr("The configuration file <i>%1</i> is empty. Using default values").arg(m_settings->filePath())); } } @@ -61,10 +59,8 @@ void Browser::firstRun() */ bool Browser::preLaunch(QStringList urls) { - Settings settings; - - if(settings.value("browser/singleInstance", true).toBool()) { - QString serverName = settings.value("browser/localSocket", "smolbote-singlelock").toString(); + if(m_settings->value("browser.singleInstance", true).toBool()) { + QString serverName = m_settings->value("browser.localSocket", "smolbote-singlelock").toString(); // Check for other running instance QLocalSocket socket; @@ -105,6 +101,11 @@ Browser *Browser::instance() return static_cast<Browser *>(QCoreApplication::instance()); } +Settings *Browser::settings() +{ + return m_settings; +} + BookmarksDialog *Browser::bookmarks() { return m_bookmarksManager; @@ -119,10 +120,10 @@ void Browser::setConfigPath(const QString &path) { if(path.isEmpty()) { // set default config path - Settings::setFilePath(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + "/config.ini"); + m_settings = new Settings(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + "/config.toml"); } else { // set custom config path - Settings::setFilePath(path); + m_settings = new Settings(path); } } diff --git a/src/browser.h b/src/browser.h index 3d3b297..0a6702f 100644 --- a/src/browser.h +++ b/src/browser.h @@ -26,6 +26,9 @@ #include "forms/bookmarksdialog.h" #include "forms/downloaddialog.h" #include <QLocalServer> +#include "settings.h" + +#define sSettings Browser::instance()->settings() class MainWindow; class Browser : public QApplication @@ -41,6 +44,7 @@ public: static Browser *instance(); + Settings *settings(); BookmarksDialog *bookmarks(); DownloadDialog *downloads(); @@ -55,6 +59,7 @@ private slots: void handleNewConnection(); private: + Settings *m_settings; QLocalServer *m_localServer; QVector<MainWindow*> m_windows; BookmarksDialog *m_bookmarksManager; diff --git a/src/forms/blockerdialog.cpp b/src/forms/blockerdialog.cpp index fa613bd..9c6bd0a 100644 --- a/src/forms/blockerdialog.cpp +++ b/src/forms/blockerdialog.cpp @@ -21,7 +21,7 @@ #include "blockerdialog.h" #include "ui_blockerdialog.h" -#include "settings.h" +#include "browser.h" #include <QLabel> #include <QListWidget> @@ -29,11 +29,10 @@ BlockerDialog::BlockerDialog(QWidget *parent) : QDialog(parent), ui(new Ui::UrlInterceptorDialog) { - Settings settings; ui->setupUi(this); m_subscription = new BlockerSubscription(this); - QString sublocation = settings.value("blocker/subscription").toString(); + QString sublocation = sSettings->value("blocker.path").toString(); if(!sublocation.isEmpty()) { m_subscription->loadFromFile(sublocation); } diff --git a/src/forms/downloaddialog.cpp b/src/forms/downloaddialog.cpp index 950fa96..d182076 100644 --- a/src/forms/downloaddialog.cpp +++ b/src/forms/downloaddialog.cpp @@ -27,7 +27,7 @@ #include <QListWidget> #include <QLabel> #include "webengine/downloaditemform.h" -#include "settings.h" +#include "browser.h" DownloadDialog::DownloadDialog(QWidget *parent) : QDialog(parent), @@ -46,9 +46,7 @@ DownloadDialog::~DownloadDialog() void DownloadDialog::addDownload(QWebEngineDownloadItem *item) { - Settings settings; - - QString filepath = QFileDialog::getSaveFileName(this, tr("Save"), settings.value("downloads/path").toString() + "/" + QFileInfo(item->path()).fileName()); + QString filepath = QFileDialog::getSaveFileName(this, tr("Save"), sSettings->value("downloads.path").toString() + "/" + QFileInfo(item->path()).fileName()); if(filepath.isEmpty()) { // user cancelled the save dialog diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 32c282d..6870a65 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -20,7 +20,6 @@ #include "mainwindow.h" #include "ui_mainwindow.h" -#include "settings.h" #include <QMenu> #include <QMenuBar> #include <QCloseEvent> @@ -42,13 +41,11 @@ MainWindow::MainWindow(QUrl defaultUrl, QWidget *parent) : urlLineEdit(new UrlLineEdit(navigationToolBar)), progressBar(new LoadingBar(this)) { - Settings settings; - // Load profile and connect its signals - loadProfile(settings.value("profile").toString()); + loadProfile(sSettings->value("general.profile").toString()); ui->setupUi(this); - resize(settings.value("window/width", 800).toInt(), settings.value("window/height", 600).toInt()); + resize(sSettings->value("window.width", 800).toInt(), sSettings->value("window.height", 600).toInt()); // Browser menu QMenu *browserMenu = new QMenu(qApp->applicationName(), ui->menuBar); @@ -64,9 +61,9 @@ MainWindow::MainWindow(QUrl defaultUrl, QWidget *parent) : QMenu *toolsMenu = new QMenu(tr("Tools"), ui->menuBar); ui->menuBar->addMenu(toolsMenu); QAction *downloadsAction = toolsMenu->addAction(tr("Downloads"), Browser::instance()->downloads(), SLOT(show())); - downloadsAction->setShortcut(QKeySequence::fromString(settings.value("downloads/dialogShortcut").toString())); + downloadsAction->setShortcut(QKeySequence::fromString(sSettings->value("downloads.dialogShortcut").toString())); QAction *bookmarksAction = toolsMenu->addAction(tr("Bookmarks"), Browser::instance()->bookmarks(), SLOT(show())); - bookmarksAction->setShortcut(QKeySequence(settings.value("bookmarks/dialogShortcut").toString())); + bookmarksAction->setShortcut(QKeySequence(sSettings->value("bookmarks.dialogShortcut").toString())); toolsMenu->addSeparator(); toolsMenu->addAction(tr("Blocker"), blocklistManager, SLOT(show())); @@ -78,10 +75,10 @@ MainWindow::MainWindow(QUrl defaultUrl, QWidget *parent) : //profileMenu->addAction(tr("Settings")); //profileMenu->addAction(tr("Cookies")); - navigationToolBar->setMovable(settings.value("ui/navtoolbarMovable", true).toBool()); + navigationToolBar->setMovable(sSettings->value("ui.navtoolbarMovable", true).toBool()); this->addToolBar(Qt::TopToolBarArea, navigationToolBar); this->addToolBarBreak(Qt::TopToolBarArea); - tabToolBar->setMovable(settings.value("ui/tabtoolbarMovable", true).toBool()); + tabToolBar->setMovable(sSettings->value("ui.tabtoolbarMovable", true).toBool()); this->addToolBar(Qt::TopToolBarArea, tabToolBar); navigationToolBar->addWidget(urlLineEdit); @@ -95,14 +92,14 @@ MainWindow::MainWindow(QUrl defaultUrl, QWidget *parent) : // shortcuts QAction *focusAddressAction = new QAction(this); - focusAddressAction->setShortcut(QKeySequence::fromString(settings.value("shortcuts/focusAddress").toString())); + focusAddressAction->setShortcut(QKeySequence::fromString(sSettings->value("shortcuts.focusAddress").toString())); connect(focusAddressAction, SIGNAL(triggered(bool)), this, SLOT(focusAddress())); addAction(focusAddressAction); if(!defaultUrl.isEmpty()) { addNewTab(defaultUrl); } else { - addNewTab(settings.value("homepage", QUrl("about:blank")).toUrl()); + addNewTab(sSettings->value("general.homepage", QUrl("about:blank")).toUrl()); } } @@ -124,8 +121,7 @@ void MainWindow::addNewTab(const QUrl &url) if(!url.isEmpty()) { tabBar->addTab(m_profile, url); } else { - Settings settings; - tabBar->addTab(m_profile, settings.value("newtab").toUrl()); + tabBar->addTab(m_profile, sSettings->value("general.newtab").toUrl()); } } @@ -155,7 +151,7 @@ void MainWindow::about() "<p>This program comes with ABSOLUTELY NO WARRANTY. " "This is free software, and you are welcome to redistribute it under the conditions set by the GNU GPLv3.</p>" "<p>Configuration lives in: %2</p>") - .arg(qApp->applicationVersion()).arg(Settings::staticFilePath())); + .arg(qApp->applicationVersion()).arg(sSettings->filePath())); } void MainWindow::loadProfile(const QString &name) @@ -226,8 +222,7 @@ void MainWindow::handleUrlChanged() void MainWindow::handleTitleUpdated(const QString &title) { - Settings settings; - setWindowTitle(settings.value("window/title").toString().replace("$title", title).replace("$profile", m_profile->storageName())); + setWindowTitle(sSettings->value("window.title").toString().replace("$title", title).replace("$profile", m_profile->storageName())); } void MainWindow::execProfileEditor() diff --git a/src/settings.cpp b/src/settings.cpp index f030dfd..edbecc0 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -21,41 +21,85 @@ #include "settings.h" #include <QStandardPaths> -QString Settings::_path = QString(""); +#include <fstream> -Settings::Settings(QObject *parent) : - QSettings(_path, QSettings::IniFormat, parent) +Settings::Settings(const QString &configFile) { - setIniCodec("UTF-8"); + path = configFile; + std::ifstream ifs(configFile.toStdString().c_str()); + toml::ParseResult pr = toml::parse(ifs); + + if(!pr.valid()) { + qWarning("Invalid configuration: %s", pr.errorReason.c_str()); + return; + } + + v = pr.value; } -void Settings::setFilePath(const QString &path) +Settings::~Settings() { - qDebug("config=[%s]", qUtf8Printable(path)); - _path = path; } -QString Settings::staticFilePath() +QString Settings::filePath() const { - return _path; + return path; +} + +bool Settings::isEmpty() const +{ + return v.empty(); +} + +bool Settings::contains(const QString &key) +{ + const toml::Value *x = v.find(key.toStdString()); + if(x) { + return true; + } else { + return false; + } } QVariant Settings::value(const QString &key, const QVariant &defaultValue) const { - // get the actual value - QString value = QSettings::value(key, defaultValue).toString(); - - // check if there is a reference - QRegularExpressionMatch referenceMatch = referencePattern.match(value); - if(referenceMatch.hasMatch()) { - QString pattern = referenceMatch.capturedTexts().first(); - value.replace(pattern, this->value(pattern.mid(1, pattern.length()-2)).toString()); + const toml::Value *x = v.find(key.toStdString()); + + // check if value exists + if(!x) { + return defaultValue; + } + + QVariant r; + switch (x->type()) { + case toml::Value::NULL_TYPE: + r = defaultValue; + break; + + case toml::Value::BOOL_TYPE: + r = QVariant(x->as<bool>()); + break; + + case toml::Value::INT_TYPE: + r = QVariant(x->as<int>()); + break; + + case toml::Value::STRING_TYPE: + r = QVariant(QString::fromStdString(x->as<std::string>())); + break; + + default: + qWarning("Unhandled type in configuration"); + r = defaultValue; + break; } // check if key is a path, in which case replace '~' with the home location if(key.endsWith(QLatin1String("path"), Qt::CaseInsensitive)) { + QString value = r.toString(); value.replace('~', QStandardPaths::writableLocation(QStandardPaths::HomeLocation)); + r = QVariant(value); } - return QVariant(value); + return r; } diff --git a/src/settings.h b/src/settings.h index 01eff6b..57d868c 100644 --- a/src/settings.h +++ b/src/settings.h @@ -21,28 +21,23 @@ #ifndef SETTINGS_H #define SETTINGS_H -#include <QSettings> -#include <QRegularExpression> +#include <toml/toml.h> +#include <QVariant> -class Settings : public QSettings +class Settings { - Q_OBJECT public: - explicit Settings(QObject *parent = 0); - - static void setFilePath(const QString &path); - static QString staticFilePath(); + Settings(const QString &configFile); + ~Settings(); + QString filePath() const; + bool isEmpty() const; + bool contains(const QString &key); QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const; - //void setValue(const QString &key, const QVariant &value); - -signals: - -public slots: private: - static QString _path; - const QRegularExpression referencePattern = QRegularExpression("~[\\w/]+~"); + toml::Value v; + QString path; }; #endif // SETTINGS_H diff --git a/src/widgets/webviewtabbar.cpp b/src/widgets/webviewtabbar.cpp index db44bc9..745a7a5 100644 --- a/src/widgets/webviewtabbar.cpp +++ b/src/widgets/webviewtabbar.cpp @@ -19,7 +19,7 @@ ******************************************************************************/ #include "webviewtabbar.h" -#include "settings.h" +#include "browser.h" #include <QAction> WebViewTabBar::WebViewTabBar(QWidget *parent) : @@ -32,26 +32,25 @@ WebViewTabBar::WebViewTabBar(QWidget *parent) : connect(this, SIGNAL(currentChanged(int)), this, SLOT(handleCurrentChanged(int))); connect(this, SIGNAL(tabMoved(int,int)), this, SLOT(updateVectorArrangement(int,int))); - Settings settings; - if(settings.contains("shortcuts/tabClose")) { + if(sSettings->contains("shortcuts.tabClose")) { QAction *tabCloseAction = new QAction(this); - tabCloseAction->setShortcut(QKeySequence::fromString(settings.value("shortcuts/tabClose").toString())); + tabCloseAction->setShortcut(QKeySequence::fromString(sSettings->value("shortcuts.tabClose").toString())); connect(tabCloseAction, &QAction::triggered, [this]() { this->handleTabClose(currentIndex()); }); addAction(tabCloseAction); } - if(settings.contains("shortcuts/tabLeft")) { + if(sSettings->contains("shortcuts.tabLeft")) { QAction *tabLeftAction = new QAction(this); - tabLeftAction->setShortcut(QKeySequence::fromString(settings.value("shortcuts/tabLeft").toString())); + tabLeftAction->setShortcut(QKeySequence::fromString(sSettings->value("shortcuts.tabLeft").toString())); connect(tabLeftAction, &QAction::triggered, [this]() { this->setCurrentIndex(currentIndex()-1); }); addAction(tabLeftAction); } - if(settings.contains("shortcuts/tabRight")) { + if(sSettings->contains("shortcuts.tabRight")) { QAction *tabRightAction = new QAction(this); - tabRightAction->setShortcut(QKeySequence::fromString(settings.value("shortcuts/tabRight").toString())); + tabRightAction->setShortcut(QKeySequence::fromString(sSettings->value("shortcuts.tabRight").toString())); connect(tabRightAction, &QAction::triggered, [this]() { this->setCurrentIndex(currentIndex()+1); }); diff --git a/test/config.ini b/test/config.ini deleted file mode 100644 index ea5c258..0000000 --- a/test/config.ini +++ /dev/null @@ -1,34 +0,0 @@ -[browser] -singleInstance=true -localSocket=smolbote-singlelock - -[general] -homepage=https://duckduckgo.com -newtab=about:blank -profile=Default - -[blocker] -subscription= - -[bookmarks] -dialogShortcut=Ctrl+Shift+B -path=bookmarks.xbel - -[downloads] -dialogShortcut=Ctrl+Shift+D -path=~/Downloads - -[window] -height=720 -width=1280 -title=$title — smolbote [$profile] - -[shortcuts] -focusAddress=F4 -tabClose=Ctrl+X -tabLeft=Shift+Left -tabRight=Shift+Right - -[ui] -navtoolbarMovable=false -tabtoolbarMovable=false diff --git a/test/config.toml b/test/config.toml new file mode 100644 index 0000000..747282b --- /dev/null +++ b/test/config.toml @@ -0,0 +1,36 @@ +[browser] +singleInstance=true +localSocket="smolbote-singlelock" + +[general] +homepage="https://duckduckgo.com" +newtab="about:blank" +profile="Default" + +[blocker] +dialogShortcut="Ctrl+Shift+A" +path="" +subscriptions="" + +[bookmarks] +dialogShortcut="Ctrl+Shift+B" +path="bookmarks.xbel" + +[downloads] +dialogShortcut="Ctrl+Shift+D" +path="~/Downloads" + +[window] +height=720 +width=1280 +title="$title — smolbote [$profile]" + +[shortcuts] +focusAddress="F4" +tabClose="Ctrl+X" +tabLeft="Shift+Left" +tabRight="Shift+Right" + +[ui] +navtoolbarMovable=false +tabtoolbarMovable=false |