Skip to content

Commit

Permalink
Added string interpolation
Browse files Browse the repository at this point in the history
  • Loading branch information
houmain committed Aug 19, 2024
1 parent 6559e8d commit 210cae1
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 50 deletions.
102 changes: 63 additions & 39 deletions src/config/ParseConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,23 @@ const char* current_system = "MacOS";
namespace {
using namespace std::placeholders;

template<typename It>
std::string_view make_string_view(It begin, It end) {
return std::string_view(&*begin, std::distance(begin, end));
}

bool is_alpha(char c) {
return std::isalpha(static_cast<unsigned char>(c));
}

char to_lower(char c) {
return static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}

bool starts_with_lower_case(std::string_view str) {
return (!str.empty() && to_lower(str.front()) == str.front());
return (!str.empty() &&
is_alpha(str.front()) &&
to_lower(str.front()) == str.front());
}

bool equal_case_insensitive(std::string_view a, std::string_view b) {
Expand Down Expand Up @@ -97,8 +108,7 @@ namespace {
skip_arglist(&it, end);
continue;
}
auto argument = std::string_view(
&*begin, std::distance(begin, it) - 1);
auto argument = make_string_view(begin, std::prev(it));
arguments.emplace_back(trim(argument));
}
begin = it;
Expand Down Expand Up @@ -139,6 +149,18 @@ namespace {
return std::nullopt;
return result;
}

bool contains_variable(std::string_view string) {
auto it = string.begin();
const auto end = string.end();
while (skip_until_not_in_string(&it, end, "$")) {
auto begin = it;
if (skip_ident(&it, end) &&
parse_int(make_string_view(begin, it)))
return true;
}
return false;
}
} // namespace

Config ParseConfig::operator()(std::istream& is,
Expand Down Expand Up @@ -380,25 +402,10 @@ void ParseConfig::parse_directive(It it, const It end) {

std::string ParseConfig::read_filter_string(It* it, const It end) {
const auto begin = *it;
if (skip(it, end, "/")) {
// a regular expression
for (;;) {
if (!skip_until(it, end, "/"))
error("Unterminated regular expression");
// check for irregular number of preceding backslashes
auto prev = std::prev(*it, 2);
while (prev != begin && *prev == '\\')
prev = std::prev(prev);
if (std::distance(prev, *it) % 2 == 0)
break;
}
skip(it, end, "i");
if (skip_regular_expression(it, end)) {
return std::string(begin, *it);
}
else if (skip(it, end, "'") || skip(it, end, "\"")) {
// a string
if (!skip_until(it, end, *std::prev(*it)))
error("Unterminated string");
else if (skip_string(it, end)) {
return std::string(begin + 1, *it - 1);
}
else {
Expand Down Expand Up @@ -682,13 +689,39 @@ std::string ParseConfig::apply_builtin_macro(const std::string& ident,
error("Unknown macro '" + ident + "'");
}

std::string ParseConfig::substitute_variables(std::string string) const {
auto it = string.begin();
const auto end = string.end();
auto result = std::string();
for (;;) {
const auto begin = it;
if (!skip_until(&it, end, '$')) {
result.append(begin, end);
break;
}

const auto var_begin = std::prev(it);
const auto skipped_brace = skip(&it, end, '{');
auto ident = read_ident(&it, end);
if ((skipped_brace && !skip(&it, end, '}')) ||
m_macros.find(ident) == m_macros.end()) {
result.append(begin, it);
continue;
}
result.append(begin, var_begin);
result.append(unquote(preprocess(std::move(ident))));
}
return result;
}

std::string ParseConfig::preprocess(std::string expression) const {
if (++m_preprocess_level > 30)
error("Recursive macro instantiation");
struct Guard {

const struct Guard {
int& level;
~Guard() { --level; }
} guard{ m_preprocess_level };
} decrement_level{ m_preprocess_level };

// simply substitute when expression is a single identifier
auto it = expression.begin();
Expand All @@ -697,7 +730,7 @@ std::string ParseConfig::preprocess(std::string expression) const {
it == end) {
const auto macro = m_macros.find(expression);
if (macro != cend(m_macros))
return macro->second;
return preprocess(macro->second);
return expression;
}
return preprocess(expression.begin(), expression.end());
Expand Down Expand Up @@ -728,8 +761,7 @@ std::string ParseConfig::preprocess(It it, const It end,
}
else {
// apply macro arguments
auto arguments = get_argument_list(std::string_view(
&*begin, std::distance(begin, it)));
auto arguments = get_argument_list(make_string_view(begin, it));
for (auto& argument : arguments)
argument = preprocess(std::move(argument));

Expand All @@ -742,28 +774,20 @@ std::string ParseConfig::preprocess(It it, const It end,
ident = substitute_arguments(macro->second, arguments);

// preprocess result again only if it does not contain new variables
if (ident.find('$') == std::string::npos) {
if (!contains_variable(ident)) {
begin = it;
skip_arglists(&it, end);
ident = preprocess(ident + std::string(begin, it));
}
result.append(std::move(ident));
}
}
else if (*it == '\'' || *it == '\"') {
// a string
if (!skip_until(&it, end, *it++))
error("Unterminated string");

result.append(begin, it);
}
else if (*it == '/') {
// a regular expression
++it;
skip_until(&it, end, "/");
result.append(begin, it);
else if (skip_string(&it, end) ||
skip_terminal_command(&it, end) ||
skip_regular_expression(&it, end)) {
result.append(substitute_variables(std::string(begin, it)));
}
else if (*it == '#') {
else if (*it == '#' || *it == ';') {
break;
}
else {
Expand Down
1 change: 1 addition & 0 deletions src/config/ParseConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class ParseConfig {
It out_begin, It out_end);
KeySequence parse_input(It begin, It end);
KeySequence parse_output(It begin, It end);
std::string substitute_variables(std::string string) const;
std::string preprocess(It begin, It end, bool apply_arguments = true) const;
std::string preprocess(std::string expression) const;
std::string apply_builtin_macro(const std::string& ident,
Expand Down
45 changes: 45 additions & 0 deletions src/config/string_iteration.h
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,48 @@ bool skip_arglists(ForwardIt* it, ForwardIt end) {
if (!skip_arglist(it, end))
return skipped;
}

template<typename ForwardIt>
bool skip_string(ForwardIt* it, ForwardIt end) {
auto begin = *it;
if (*it != end) {
const auto c = **it;
if (c == '\'' || c == '\"') {
++*it;
if (!skip_until(it, end, c))
throw std::runtime_error("Unterminated string");
}
}
return (begin != *it);
}

template<typename ForwardIt>
bool skip_regular_expression(ForwardIt* it, ForwardIt end) {
const auto begin = *it;
if (skip(it, end, '/')) {
for (;;) {
if (!skip_until(it, end, "/"))
throw std::runtime_error("Unterminated regular expression");
// check for irregular number of preceding backslashes
auto prev = std::prev(*it, 2);
while (prev != begin && *prev == '\\')
prev = std::prev(prev);
if (std::distance(prev, *it) % 2 == 0)
break;
}
skip(it, end, "i");
return true;
}
return false;
}

template<typename ForwardIt>
bool skip_terminal_command(ForwardIt* it, ForwardIt end) {
if (skip(it, end, "$(")) {
if (!skip_until_not_in_string(it, end, ")"))
throw std::runtime_error("Unterminated terminal command");
return true;
}
return false;
}

83 changes: 72 additions & 11 deletions src/test/test1_ParseConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,17 @@ namespace {
TEST_CASE("Valid config", "[ParseConfig]") {
auto string = R"(
# comment
MyMacro = A B C# comment
MyMacro = A B C# comment "
Shift{A} >> B
C >> CommandA ; comment
C >> CommandA ; comment $(
CommandA >> X
E >> CommandB
; comment
[ system = "Windows" class='test'title=test ] # comment
CommandA >> Y #; comment
CommandB >> MyMacro ;# comment
CommandA >> Y #; comment /
CommandB >> MyMacro ;# comment '
[system='Linux', title=/firefox[123]*x{1,3}/i ] # comment
CommandA >> Shift{Y} ## comment
Expand Down Expand Up @@ -286,6 +286,9 @@ TEST_CASE("Config directives", "[ParseConfig]") {
@enforce-lowercase-commands
A >> command
command >> B
# ensure numbers are not treated as lowercase
31 >> 64
)"));

CHECK_THROWS(parse_config(R"(
Expand Down Expand Up @@ -801,6 +804,23 @@ TEST_CASE("Macros with arguments - Problems", "[ParseConfig]") {

//--------------------------------------------------------------------

TEST_CASE("Macros with arguments #2", "[ParseConfig]") {
auto string = R"(
LOG_FILE = "logfile.txt"
base00 = "#657b83"
logColor = $(pastel --force-color paint "$1" "[$0]" >> "${LOG_FILE}")
stage1 = 50ms logColor["$0", blue] ^ logColor["$0", base00]
F1 >> stage1["EditMode"]
)";
auto config = parse_config(string);
REQUIRE(config.contexts.size() == 1);
REQUIRE(config.actions.size() == 2);
CHECK(config.actions[0].terminal_command == R"(pastel --force-color paint "blue" "[EditMode]" >> "logfile.txt")");
CHECK(config.actions[1].terminal_command == R"(pastel --force-color paint "#657b83" "[EditMode]" >> "logfile.txt")");
}

//--------------------------------------------------------------------

TEST_CASE("Macros with Alias arguments", "[ParseConfig]") {
auto string = R"(
twice = $0 $0
Expand All @@ -827,22 +847,21 @@ TEST_CASE("Macros with Terminal Commands", "[ParseConfig]") {
echo = $(echo $0$1)
F2 >> echo[echo]
F3 >> echo[" echo "]
F3 >> echo["echo$echo"]
F4 >> echo["echo, echo"]
F5 >> echo["echo", " echo "]
F6 >> $(echo "echo")
F6 >> $(echo ${echo})
)";
auto config = parse_config(string);
REQUIRE(config.actions.size() == 6);

CHECK(config.actions[0].terminal_command == R"(notify-send -t 2000 -a "keymapper" "F7")");
// in strings only $0... are substituted
// in strings and terminal commands only $0... are substituted
CHECK(config.actions[1].terminal_command == R"(echo $(echo $0$1))");
CHECK(config.actions[2].terminal_command == R"(echo echo )");
CHECK(config.actions[2].terminal_command == R"(echo echo$(echo $0$1))");
CHECK(config.actions[3].terminal_command == R"(echo echo, echo)");
CHECK(config.actions[4].terminal_command == R"(echo echo echo )");
// in terminal commands macros are also substituted
CHECK(config.actions[5].terminal_command == R"($(echo $0$1) "echo")");
CHECK(config.actions[5].terminal_command == R"(echo $(echo $0$1))");
}

//--------------------------------------------------------------------
Expand Down Expand Up @@ -901,6 +920,27 @@ TEST_CASE("Builtin Macros", "[ParseConfig]") {

//--------------------------------------------------------------------

TEST_CASE("Builtin Macros #2", "[ParseConfig]") {
auto string = R"(
type = $(keymapperctl --type $0)
# Replace trigger string with the second argument
trigger = ? "$0" >> repeat[Backspace, sub[length["$0"], 1]] $1
# Format date
format_date = $(date +"$0")
trigger[":time", type[format_date["%H:%M:%S"]]]
)";
auto config = parse_config(string);
REQUIRE(config.contexts.size() == 1);
REQUIRE(config.actions.size() == 1);
CHECK(config.actions[0].terminal_command ==
R"(keymapperctl --type $(date +"%H:%M:%S"))");
}

//--------------------------------------------------------------------

TEST_CASE("Top-level Macro", "[ParseConfig]") {
auto string = R"(
macro = A >> B ; comment
Expand Down Expand Up @@ -939,7 +979,7 @@ TEST_CASE("Top-level Macro", "[ParseConfig]") {
CHECK(format_sequence(config.contexts[0].outputs[0]) ==
"+A -A +B -B +C -C");

CHECK_NOTHROW(parse_config(R"(
CHECK_THROWS(parse_config(R"(
default = [default]
default
)"));
Expand Down Expand Up @@ -1174,3 +1214,24 @@ TEST_CASE("Line break", "[ParseConfig]") {
CHECK(format_sequence(config.contexts[0].inputs[0].input) == "+A ~A");
CHECK(format_sequence(config.contexts[0].outputs[0]) == "+B -B +C -C");
}

//--------------------------------------------------------------------

TEST_CASE("String interpolation", "[ParseConfig]") {
auto string = R"(
TEST = "bc"
A >> "${TEST}TEST$TEST";
[title = /${TEST}TEST$TEST/]
B >> $(${TEST}TEST$TEST)
D >> $(${TEST1 TEST$TEST1)
)";
auto config = parse_config(string);
REQUIRE(config.contexts.size() == 2);
REQUIRE(config.contexts[0].outputs.size() == 1);
REQUIRE(config.actions.size() == 2);
CHECK(format_sequence(config.contexts[0].outputs[0]) ==
"!Any +B -B +C -C +ShiftLeft +T -T +E -E +S -S +T -T -ShiftLeft +B -B +C -C");
CHECK(config.contexts[1].window_title_filter.string == "/bcTESTbc/");
CHECK(config.actions[0].terminal_command == R"(bcTESTbc)");
CHECK(config.actions[1].terminal_command == R"(${TEST1 TEST$TEST1)");
}

0 comments on commit 210cae1

Please sign in to comment.