Changes
diff --git a/include/macro_engine.h b/include/macro_engine.h
index d84e9f5..6bc554b 100644
--- a/include/macro_engine.h
+++ b/include/macro_engine.h
@@ -52,6 +52,12 @@ public:
std::string evaluateMacro(const Macro& macro);
private:
+ using ArgScope = std::map<std::string, std::string>;
+
+ std::string evaluateInScope(const Node& node, const ArgScope& scope);
+ std::string evaluateMacroInScope(const Macro& macro,
+ const ArgScope& scope);
+
std::map<std::string, MacroDefinition> macros_;
};
diff --git a/src/macro_engine.cpp b/src/macro_engine.cpp
index 3add8de..185ef6c 100644
--- a/src/macro_engine.cpp
+++ b/src/macro_engine.cpp
@@ -33,19 +33,6 @@ std::vector<std::string> split(const std::string& s, char delimiter)
return tokens;
}
-// Helper to replace all occurrences of a substring
-std::string replace_all(std::string str, const std::string& from,
- const std::string& to)
-{
- size_t start_pos = 0;
- while((start_pos = str.find(from, start_pos)) != std::string::npos)
- {
- str.replace(start_pos, from.length(), to);
- start_pos += to.length();
- }
- return str;
-}
-
} // namespace
Evaluator::Evaluator()
@@ -84,9 +71,21 @@ void Evaluator::defineIntrinsic(const std::string& name, MacroCallback callback)
}
std::string Evaluator::evaluate(const Node& node)
+{
+ ArgScope empty_scope;
+ return evaluateInScope(node, empty_scope);
+}
+
+std::string Evaluator::evaluateMacro(const Macro& macro)
+{
+ ArgScope empty_scope;
+ return evaluateMacroInScope(macro, empty_scope);
+}
+
+std::string Evaluator::evaluateInScope(const Node& node, const ArgScope& scope)
{
return std::visit(
- [this](auto&& arg) -> std::string
+ [this, &scope](auto&& arg) -> std::string
{
using T = std::decay_t<decltype(arg)>;
if constexpr(std::is_same_v<T, Text>)
@@ -95,14 +94,26 @@ std::string Evaluator::evaluate(const Node& node)
}
else if constexpr(std::is_same_v<T, Macro>)
{
- return this->evaluateMacro(arg);
+ // A macro reference with no arguments may be an argument
+ // placeholder (e.g. `%url` inside a user-defined body). In
+ // that case substitute the already-evaluated argument value
+ // verbatim, without re-parsing it as MacroDown source.
+ if(arg.arguments.empty())
+ {
+ auto it = scope.find(arg.name);
+ if(it != scope.end())
+ {
+ return it->second;
+ }
+ }
+ return this->evaluateMacroInScope(arg, scope);
}
else if constexpr(std::is_same_v<T, Group>)
{
std::string result;
for(const auto& child : arg.children)
{
- result += this->evaluate(*child);
+ result += this->evaluateInScope(*child, scope);
}
return result;
}
@@ -111,7 +122,8 @@ std::string Evaluator::evaluate(const Node& node)
node.data);
}
-std::string Evaluator::evaluateMacro(const Macro& macro)
+std::string Evaluator::evaluateMacroInScope(const Macro& macro,
+ const ArgScope& outer_scope)
{
auto it = macros_.find(macro.name);
if(it == macros_.end())
@@ -119,7 +131,7 @@ std::string Evaluator::evaluateMacro(const Macro& macro)
std::string result = "%" + macro.name;
for(const auto& arg : macro.arguments)
{
- result += "{" + evaluate(*arg) + "}";
+ result += "{" + evaluateInScope(*arg, outer_scope) + "}";
}
return result;
}
@@ -129,7 +141,7 @@ std::string Evaluator::evaluateMacro(const Macro& macro)
std::vector<std::string> evaluated_args;
for(const auto& arg : macro.arguments)
{
- evaluated_args.push_back(evaluate(*arg));
+ evaluated_args.push_back(evaluateInScope(*arg, outer_scope));
}
if(def.is_intrinsic)
@@ -138,22 +150,24 @@ std::string Evaluator::evaluateMacro(const Macro& macro)
}
else
{
- std::string body = def.body;
-
+ // Parse the body template once. Argument values are NOT substituted
+ // textually into the body — they're bound in a new scope and looked
+ // up when evaluation reaches a matching `%argname` reference. This
+ // keeps argument values opaque so characters like `_` inside a URL
+ // aren't re-interpreted as markdown.
+ auto body_nodes = Parser::parse(def.body);
+
+ ArgScope new_scope;
for(size_t i = 0; i < def.arg_names.size(); ++i)
{
- std::string placeholder = "%" + def.arg_names[i];
- std::string value =
+ new_scope[def.arg_names[i]] =
(i < evaluated_args.size()) ? evaluated_args[i] : "";
- body = replace_all(body, placeholder, value);
}
- auto nodes = Parser::parse(body);
-
std::string result;
- for(const auto& n : nodes)
+ for(const auto& n : body_nodes)
{
- result += evaluate(*n);
+ result += evaluateInScope(*n, new_scope);
}
return result;
}
diff --git a/tests/test_macrodown.cpp b/tests/test_macrodown.cpp
index 61de739..3acad7a 100644
--- a/tests/test_macrodown.cpp
+++ b/tests/test_macrodown.cpp
@@ -169,3 +169,12 @@ ccc
"<pre><code>* aaa\n\nbbb\n</code></pre>\n<p>ccc</p>\n";
EXPECT_EQ(md.render(*md.parse(input)), expected);
}
+
+TEST(MacroDownTest, URLsAreVerbatim)
+{
+ MacroDown md;
+ std::string input = R"([aaa](http://its_a_test.com/))";
+ std::string expected =
+ "<p><a href=\"http://its_a_test.com/\">aaa</a></p>\n";
+ EXPECT_EQ(md.render(*md.parse(input)), expected);
+}