From 55608efdaa387af7bfdc0eddb404c409958efa43 Mon Sep 17 00:00:00 2001 From: Joshua Baker Date: Sat, 28 Dec 2024 07:36:59 -0600 Subject: [PATCH] core: add multiline support (#58) Adds support for multi-line commands with a backslash --- include/hyprlang.hpp | 30 +++++------ src/config.cpp | 80 ++++++++++++++++++++++++------ src/config.hpp | 7 ++- tests/config/config.conf | 6 ++- tests/config/multiline-errors.conf | 20 ++++++++ tests/parse/main.cpp | 16 ++++++ 6 files changed, 128 insertions(+), 31 deletions(-) create mode 100644 tests/config/multiline-errors.conf diff --git a/include/hyprlang.hpp b/include/hyprlang.hpp index 863b277..457feca 100644 --- a/include/hyprlang.hpp +++ b/include/hyprlang.hpp @@ -50,7 +50,7 @@ namespace Hyprlang { typedef CConfigCustomValueType CUSTOMTYPE; /*! - A very simple vector type + A very simple vector type */ struct SVector2D { float x = 0, y = 0; @@ -95,12 +95,12 @@ namespace Hyprlang { Generic struct for options for the config parser */ struct SConfigOptions { - /*! + /*! Don't throw errors on missing values. */ int verifyOnly = false; - /*! + /*! Return all errors instead of just the first */ int throwAllErrors = false; @@ -175,11 +175,11 @@ namespace Hyprlang { typedef void (*PCONFIGCUSTOMVALUEDESTRUCTOR)(void** data); /*! - Container for a custom config value type + Container for a custom config value type When creating, pass your handler. Handler will receive a void** that points to a void* that you can set to your own thing. Pass a dtor to free whatever you allocated when the custom value type is being released. - data may always be pointing to a nullptr. + data may always be pointing to a nullptr. */ class CConfigCustomValueType { public: @@ -271,7 +271,7 @@ namespace Hyprlang { /*! \since 0.3.0 - + a flag to notify whether this value has been set explicitly by the user, or not. */ @@ -305,7 +305,7 @@ namespace Hyprlang { ~CConfig(); /*! - Add a config value, for example myCategory:myValue. + Add a config value, for example myCategory:myValue. This has to be done before commence() Value provided becomes default. */ @@ -319,8 +319,8 @@ namespace Hyprlang { /*! \since 0.3.0 - - Unregister a handler. + + Unregister a handler. */ void unregisterHandler(const char* name); @@ -362,14 +362,14 @@ namespace Hyprlang { CParseResult parse(); /*! - Same as parse(), but parse a specific file, without any refreshing. + Same as parse(), but parse a specific file, without any refreshing. recommended to use for stuff like source = path.conf */ CParseResult parseFile(const char* file); /*! - Parse a single "line", dynamically. - Values set by this are temporary and will be overwritten + Parse a single "line", dynamically. + Values set by this are temporary and will be overwritten by default / config on the next parse() */ CParseResult parseDynamic(const char* line); @@ -377,14 +377,14 @@ namespace Hyprlang { /*! Get a config's value ptr. These are static. - nullptr on fail + nullptr on fail */ CConfigValue* getConfigValuePtr(const char* name); /*! Get a special category's config value ptr. These are only static for static (key-less) categories. - key can be nullptr for static categories. Cannot be nullptr for id-based categories. + key can be nullptr for static categories. Cannot be nullptr for id-based categories. nullptr on fail. */ CConfigValue* getSpecialConfigValuePtr(const char* category, const char* name, const char* key = nullptr); @@ -541,4 +541,4 @@ namespace Hyprlang { #undef HYPRLANG_END_MAGIC #endif -#endif \ No newline at end of file +#endif diff --git a/src/config.cpp b/src/config.cpp index fdb71db..d879c34 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -25,6 +25,7 @@ extern "C" char** environ; // defines inline constexpr const char* ANONYMOUS_KEY = "__hyprlang_internal_anonymous_key"; +inline constexpr const char* MULTILINE_SPACE_CHARSET = " \t"; // static size_t seekABIStructSize(const void* begin, size_t startOffset, size_t maxSize) { @@ -36,6 +37,30 @@ static size_t seekABIStructSize(const void* begin, size_t startOffset, size_t ma return 0; } +static std::expected getNextLine(std::istream& str, int &rawLineNum, int &lineNum) { + std::string line = ""; + std::string nextLine = ""; + + if (!std::getline(str, line)) + return std::unexpected(GETNEXTLINEFAILURE_EOF); + + lineNum = ++rawLineNum; + + while (line.length() > 0 && line.at(line.length() - 1) == '\\') { + const auto lastNonSpace = line.length() < 2 ? -1 : line.find_last_not_of(MULTILINE_SPACE_CHARSET, line.length() - 2); + line = line.substr(0, lastNonSpace + 1); + + if (!std::getline(str, nextLine)) + return std::unexpected(GETNEXTLINEFAILURE_BACKSLASH); + + ++rawLineNum; + line += nextLine; + } + + return line; +} + + CConfig::CConfig(const char* path, const Hyprlang::SConfigOptions& options_) { SConfigOptions options; std::memcpy(&options, &options_, seekABIStructSize(&options_, 16, sizeof(SConfigOptions))); @@ -695,22 +720,36 @@ CParseResult CConfig::parse() { CParseResult CConfig::parseRawStream(const std::string& stream) { CParseResult result; - std::string line = ""; - int linenum = 1; + int rawLineNum = 0; + int lineNum = 0; std::stringstream str(stream); - while (std::getline(str, line)) { - const auto RET = parseLine(line); + while (true) { + const auto line = getNextLine(str, rawLineNum, lineNum); + + if (!line) { + switch (line.error()) { + case GETNEXTLINEFAILURE_EOF: + break; + case GETNEXTLINEFAILURE_BACKSLASH: + if (!impl->parseError.empty()) + impl->parseError += "\n"; + impl->parseError += std::format("Config error: Last line ends with backslash"); + result.setError(impl->parseError); + break; + } + break; + } + + const auto RET = parseLine(line.value()); if (RET.error && (impl->parseError.empty() || impl->configOptions.throwAllErrors)) { if (!impl->parseError.empty()) impl->parseError += "\n"; - impl->parseError += std::format("Config error at line {}: {}", linenum, RET.errorStdString); + impl->parseError += std::format("Config error at line {}: {}", lineNum, RET.errorStdString); result.setError(impl->parseError); } - - ++linenum; } if (!impl->categories.empty()) { @@ -736,21 +775,34 @@ CParseResult CConfig::parseFile(const char* file) { return result; } - std::string line = ""; - int linenum = 1; + int rawLineNum = 0; + int lineNum = 0; - while (std::getline(iffile, line)) { + while (true) { + const auto line = getNextLine(iffile, rawLineNum, lineNum); - const auto RET = parseLine(line); + if (!line) { + switch (line.error()) { + case GETNEXTLINEFAILURE_EOF: + break; + case GETNEXTLINEFAILURE_BACKSLASH: + if (!impl->parseError.empty()) + impl->parseError += "\n"; + impl->parseError += std::format("Config error in file {}: Last line ends with backslash", file); + result.setError(impl->parseError); + break; + } + break; + } + + const auto RET = parseLine(line.value()); if (!impl->currentFlags.noError && RET.error && (impl->parseError.empty() || impl->configOptions.throwAllErrors)) { if (!impl->parseError.empty()) impl->parseError += "\n"; - impl->parseError += std::format("Config error in file {} at line {}: {}", file, linenum, RET.errorStdString); + impl->parseError += std::format("Config error in file {} at line {}: {}", file, lineNum, RET.errorStdString); result.setError(impl->parseError); } - - ++linenum; } iffile.close(); diff --git a/src/config.hpp b/src/config.hpp index 46efaf5..764b348 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -65,6 +65,11 @@ struct SSpecialCategory { size_t anonymousID = 0; }; +enum eGetNextLineFailure : uint8_t { + GETNEXTLINEFAILURE_EOF = 0, + GETNEXTLINEFAILURE_BACKSLASH, +}; + class CConfigImpl { public: std::string path = ""; @@ -94,4 +99,4 @@ class CConfigImpl { struct { bool noError = false; } currentFlags; -}; \ No newline at end of file +}; diff --git a/tests/config/config.conf b/tests/config/config.conf index c8ea739..b7f7b6e 100644 --- a/tests/config/config.conf +++ b/tests/config/config.conf @@ -90,6 +90,11 @@ flagsStuff { value = 2 } +multiline = \ + very \ + long \ + command + testCategory:testValueHex = 0xFFfFaAbB $RECURSIVE1 = a @@ -103,4 +108,3 @@ doABarrelRoll = woohoo, some, params # Funny! flagsabc = test #doSomethingFunny = 1, 2, 3, 4 # Funnier! #testSpaces = abc , def # many spaces, should be trimmed - diff --git a/tests/config/multiline-errors.conf b/tests/config/multiline-errors.conf new file mode 100644 index 0000000..9ea2c28 --- /dev/null +++ b/tests/config/multiline-errors.conf @@ -0,0 +1,20 @@ +# Careful when modifying this file. Line numbers are part of the test. + +multiline = \ + one \ + two \ + three + +# Line numbers reported in errors should match the actual line numbers of the source file +# even after multi-line configs. Any errors reported should use the line number of the +# first line of any multi-line config. + +this \ + should \ + cause \ + error \ + on \ + line \ + 12 + +# A config file cannot end with a bashslash because we are expecting another line! Even in a comment! \ diff --git a/tests/parse/main.cpp b/tests/parse/main.cpp index 56b40d8..52d3154 100755 --- a/tests/parse/main.cpp +++ b/tests/parse/main.cpp @@ -145,6 +145,8 @@ int main(int argc, char** argv, char** envp) { config.addSpecialCategory("specialAnonymous", {nullptr, false, true}); config.addSpecialConfigValue("specialAnonymous", "value", (Hyprlang::INT)0); + config.addConfigValue("multiline", ""); + config.commence(); config.addSpecialCategory("specialGeneric:one", {nullptr, true}); @@ -279,6 +281,9 @@ int main(int argc, char** argv, char** envp) { std::cout << " → Testing custom types\n"; EXPECT(*reinterpret_cast(std::any_cast(config.getConfigValue("customType"))), (Hyprlang::INT)1); + // test multiline config + EXPECT(std::any_cast(config.getConfigValue("multiline")), std::string{"very long command"}); + std::cout << " → Testing error.conf\n"; Hyprlang::CConfig errorConfig("./config/error.conf", {.verifyOnly = true, .throwAllErrors = true}); @@ -307,6 +312,17 @@ int main(int argc, char** argv, char** envp) { EXPECT(ERRORS2.error, true); const auto ERRORSTR2 = std::string{ERRORS2.getError()}; EXPECT(std::count(ERRORSTR2.begin(), ERRORSTR2.end(), '\n'), 9 - 1); + + Hyprlang::CConfig multilineErrorConfig("./config/multiline-errors.conf", {.verifyOnly = true, .throwAllErrors = true}); + multilineErrorConfig.commence(); + const auto ERRORS3 = multilineErrorConfig.parse(); + EXPECT(ERRORS3.error, true); + const auto ERRORSTR3 = std::string{ERRORS3.getError()}; + + // Error on line 12 + EXPECT(ERRORSTR3.contains("12"), true); + // Backslash at end of file + EXPECT(ERRORSTR3.contains("backslash"), true); } catch (const char* e) { std::cout << Colors::RED << "Error: " << Colors::RESET << e << "\n"; return 1;