mirror of
https://github.com/hyprwm/hyprlang.git
synced 2025-05-12 21:30:37 +01:00
core: add multiline support (#58)
Adds support for multi-line commands with a backslash
This commit is contained in:
parent
0404833ea1
commit
55608efdaa
6 changed files with 128 additions and 31 deletions
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
20
tests/config/multiline-errors.conf
Normal file
20
tests/config/multiline-errors.conf
Normal 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! \
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue