core: add multiline support (#58)

Adds support for multi-line commands with a backslash
This commit is contained in:
Joshua Baker 2024-12-28 07:36:59 -06:00 committed by GitHub
parent 0404833ea1
commit 55608efdaa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 128 additions and 31 deletions

View file

@ -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
#endif

View file

@ -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<std::string, eGetNextLineFailure> 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();

View file

@ -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;
};
};

View file

@ -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

View file

@ -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! \

View file

@ -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<int64_t*>(std::any_cast<void*>(config.getConfigValue("customType"))), (Hyprlang::INT)1);
// test multiline config
EXPECT(std::any_cast<const char*>(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;