Skip to content

Commit f858c28

Browse files
committed
feat(template): add filters support (V2)
- add FilterNode to AST and extend VariableNode - extend parser to support filter pipelines (| syntax) - update renderer to apply filters with Builtins registry - allow filters on missing variables (enables default behavior) - add unit tests for filters (parser + renderer) - add vix.json package definition
1 parent f598755 commit f858c28

8 files changed

Lines changed: 329 additions & 13 deletions

File tree

include/vix/template/AST.hpp

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,50 @@ namespace vix::template_
5252
*/
5353
using NodeList = std::vector<NodePtr>;
5454

55+
/**
56+
* @brief Filter node attached to a variable interpolation.
57+
*
58+
* Example:
59+
* {{ name | upper | length }}
60+
*
61+
* In this example:
62+
* - upper is one FilterNode
63+
* - length is another FilterNode
64+
*
65+
* V2 keeps filters simple:
66+
* - filter name only
67+
* - no arguments yet
68+
*/
69+
class FilterNode
70+
{
71+
public:
72+
/**
73+
* @brief Construct a filter node.
74+
*
75+
* @param name Filter name.
76+
*/
77+
explicit FilterNode(std::string name)
78+
: name_(std::move(name))
79+
{
80+
}
81+
82+
/**
83+
* @brief Get the filter name.
84+
*
85+
* @return Filter name.
86+
*/
87+
[[nodiscard]] const std::string &name() const noexcept
88+
{
89+
return name_;
90+
}
91+
92+
private:
93+
/**
94+
* @brief Filter name.
95+
*/
96+
std::string name_;
97+
};
98+
5599
/**
56100
* @brief Base class for all AST nodes.
57101
*/
@@ -190,14 +234,16 @@ namespace vix::template_
190234
/**
191235
* @brief Variable interpolation node.
192236
*
193-
* Example:
237+
* Examples:
194238
* {{ user }}
239+
* {{ user | upper }}
240+
* {{ items | length }}
195241
*/
196242
class VariableNode final : public Node
197243
{
198244
public:
199245
/**
200-
* @brief Construct a variable node.
246+
* @brief Construct a variable node without filters.
201247
*
202248
* @param name Variable name.
203249
*/
@@ -206,6 +252,18 @@ namespace vix::template_
206252
{
207253
}
208254

255+
/**
256+
* @brief Construct a variable node with filters.
257+
*
258+
* @param name Variable name.
259+
* @param filters Filter pipeline.
260+
*/
261+
VariableNode(std::string name, std::vector<FilterNode> filters)
262+
: name_(std::move(name)),
263+
filters_(std::move(filters))
264+
{
265+
}
266+
209267
/**
210268
* @brief Get the node type.
211269
*
@@ -226,11 +284,36 @@ namespace vix::template_
226284
return name_;
227285
}
228286

287+
/**
288+
* @brief Get the filter pipeline.
289+
*
290+
* @return Ordered list of filters to apply.
291+
*/
292+
[[nodiscard]] const std::vector<FilterNode> &filters() const noexcept
293+
{
294+
return filters_;
295+
}
296+
297+
/**
298+
* @brief Check whether the variable has filters.
299+
*
300+
* @return True if one or more filters are attached.
301+
*/
302+
[[nodiscard]] bool has_filters() const noexcept
303+
{
304+
return !filters_.empty();
305+
}
306+
229307
private:
230308
/**
231309
* @brief Variable name to resolve from the rendering context.
232310
*/
233311
std::string name_;
312+
313+
/**
314+
* @brief Ordered filter pipeline.
315+
*/
316+
std::vector<FilterNode> filters_;
234317
};
235318

236319
/**

include/vix/template/Parser.hpp

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ namespace vix::template_
3030
* Parser consumes the token stream produced by Lexer and builds
3131
* a structured AST representation of the template.
3232
*
33-
* Supported V1 constructs:
33+
* Supported V2 constructs:
3434
* - plain text
3535
* - variable interpolation: {{ name }}
36+
* - filtered variable interpolation: {{ name | upper }}
37+
* - chained filters: {{ name | upper | length }}
3638
* - if blocks: {% if cond %} ... {% endif %}
3739
* - for blocks: {% for item in items %} ... {% endfor %}
3840
*/
@@ -146,13 +148,25 @@ namespace vix::template_
146148
/**
147149
* @brief Parse a variable interpolation node.
148150
*
149-
* Expected form:
151+
* Expected forms:
150152
* {{ identifier }}
153+
* {{ identifier | filter }}
154+
* {{ identifier | filter1 | filter2 }}
151155
*
152156
* @return Parsed variable node.
153157
*/
154158
[[nodiscard]] NodePtr parse_variable();
155159

160+
/**
161+
* @brief Parse a filter pipeline attached to a variable expression.
162+
*
163+
* Expected form:
164+
* | filter_name | other_filter
165+
*
166+
* @return Ordered filter list.
167+
*/
168+
[[nodiscard]] std::vector<FilterNode> parse_filters();
169+
156170
/**
157171
* @brief Parse a block expression.
158172
*

include/vix/template/Renderer.hpp

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
#define VIX_TEMPLATE_RENDERER_HPP
1818

1919
#include <string>
20+
#include <unordered_map>
21+
#include <vector>
2022

2123
#include <vix/template/AST.hpp>
24+
#include <vix/template/Builtins.hpp>
2225
#include <vix/template/Context.hpp>
2326
#include <vix/template/RenderResult.hpp>
2427

@@ -30,12 +33,16 @@ namespace vix::template_
3033
* Renderer walks the parsed AST and produces the final textual output
3134
* using the provided runtime context.
3235
*
33-
* Supported V1 nodes:
36+
* Supported V2 nodes:
3437
* - RootNode
3538
* - TextNode
3639
* - VariableNode
3740
* - IfNode
3841
* - ForNode
42+
*
43+
* Variable nodes may include a filter pipeline such as:
44+
* - {{ name | upper }}
45+
* - {{ items | length }}
3946
*/
4047
class Renderer
4148
{
@@ -96,6 +103,9 @@ namespace vix::template_
96103
/**
97104
* @brief Render a variable node.
98105
*
106+
* Variable rendering resolves the source variable, applies filters
107+
* in declaration order, then converts the final value to string.
108+
*
99109
* @param node Variable node.
100110
* @param context Runtime rendering context.
101111
* @param output Output string buffer.
@@ -140,11 +150,37 @@ namespace vix::template_
140150
const std::string &name,
141151
const Context &context) const noexcept;
142152

153+
/**
154+
* @brief Apply a filter pipeline to a value.
155+
*
156+
* Filters are applied from left to right.
157+
*
158+
* Example:
159+
* {{ name | upper | length }}
160+
*
161+
* This means:
162+
* 1. resolve name
163+
* 2. apply upper
164+
* 3. apply length
165+
*
166+
* @param input Initial value.
167+
* @param filters Ordered filter pipeline.
168+
* @return Transformed value.
169+
*/
170+
[[nodiscard]] Value apply_filters(
171+
const Value &input,
172+
const std::vector<FilterNode> &filters) const;
173+
143174
private:
144175
/**
145176
* @brief Whether HTML escaping is enabled for variable output.
146177
*/
147178
bool auto_escape_html_{true};
179+
180+
/**
181+
* @brief Built-in filter registry.
182+
*/
183+
std::unordered_map<std::string, Filter> filters_;
148184
};
149185

150186
} // namespace vix::template_

src/Parser.cpp

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
#include <memory>
2020
#include <utility>
21+
#include <vector>
2122

2223
namespace vix::template_
2324
{
@@ -59,6 +60,7 @@ namespace vix::template_
5960
{
6061
++pos_;
6162
}
63+
6264
return token;
6365
}
6466

@@ -140,9 +142,27 @@ namespace vix::template_
140142
const Token &name =
141143
expect(TokenType::Identifier, "expected identifier inside variable expression");
142144

145+
std::vector<FilterNode> filters = parse_filters();
146+
143147
expect(TokenType::VariableClose, "expected '}}'");
144148

145-
return std::make_unique<VariableNode>(name.value);
149+
return std::make_unique<VariableNode>(name.value, std::move(filters));
150+
}
151+
152+
std::vector<FilterNode> Parser::parse_filters()
153+
{
154+
std::vector<FilterNode> filters;
155+
156+
while (check(TokenType::Operator, "|"))
157+
{
158+
advance();
159+
160+
const Token &filter_name =
161+
expect(TokenType::Identifier, "expected filter name after '|'");
162+
filters.emplace_back(filter_name.value);
163+
}
164+
165+
return filters;
146166
}
147167

148168
NodePtr Parser::parse_block()

src/Renderer.cpp

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
namespace vix::template_
2121
{
2222
Renderer::Renderer(bool auto_escape_html)
23-
: auto_escape_html_(auto_escape_html)
23+
: auto_escape_html_(auto_escape_html),
24+
filters_(Builtins::filters())
2425
{
2526
}
2627

@@ -98,12 +99,11 @@ namespace vix::template_
9899
std::string &output) const
99100
{
100101
const Value *value = resolve_variable(node.name(), context);
101-
if (value == nullptr)
102-
{
103-
return;
104-
}
105102

106-
std::string rendered = value->to_string();
103+
const Value initial = (value != nullptr) ? *value : Value{};
104+
Value transformed = apply_filters(initial, node.filters());
105+
106+
std::string rendered = transformed.to_string();
107107
if (auto_escape_html_)
108108
{
109109
rendered = Escape::html(rendered);
@@ -174,4 +174,24 @@ namespace vix::template_
174174
return context.get(name);
175175
}
176176

177+
Value Renderer::apply_filters(
178+
const Value &input,
179+
const std::vector<FilterNode> &filters) const
180+
{
181+
Value current = input;
182+
183+
for (const auto &filter : filters)
184+
{
185+
const auto it = filters_.find(filter.name());
186+
if (it == filters_.end())
187+
{
188+
throw RendererError("unknown filter: " + filter.name());
189+
}
190+
191+
current = it->second(current);
192+
}
193+
194+
return current;
195+
}
196+
177197
} // namespace vix::template_

tests/parser_test.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,38 @@ static void test_parse_variable()
5959
static_cast<const VariableNode *>(root.children()[1].get());
6060

6161
assert(var->name() == "name");
62+
assert(var->filters().empty());
63+
}
64+
65+
static void test_parse_variable_with_one_filter()
66+
{
67+
RootNode root = parse_template("{{ name | upper }}");
68+
69+
assert(root.children().size() == 1);
70+
assert(root.children()[0]->type() == NodeType::Variable);
71+
72+
const auto *var =
73+
static_cast<const VariableNode *>(root.children()[0].get());
74+
75+
assert(var->name() == "name");
76+
assert(var->filters().size() == 1);
77+
assert(var->filters()[0].name() == "upper");
78+
}
79+
80+
static void test_parse_variable_with_multiple_filters()
81+
{
82+
RootNode root = parse_template("{{ name | upper | length }}");
83+
84+
assert(root.children().size() == 1);
85+
assert(root.children()[0]->type() == NodeType::Variable);
86+
87+
const auto *var =
88+
static_cast<const VariableNode *>(root.children()[0].get());
89+
90+
assert(var->name() == "name");
91+
assert(var->filters().size() == 2);
92+
assert(var->filters()[0].name() == "upper");
93+
assert(var->filters()[1].name() == "length");
6294
}
6395

6496
static void test_parse_if()
@@ -100,6 +132,7 @@ static void test_parse_for()
100132
static_cast<const VariableNode *>(for_node->body()[0].get());
101133

102134
assert(var->name() == "item");
135+
assert(var->filters().empty());
103136
}
104137

105138
static void test_parse_nested_if_in_for()
@@ -128,6 +161,8 @@ int main()
128161
{
129162
test_parse_text();
130163
test_parse_variable();
164+
test_parse_variable_with_one_filter();
165+
test_parse_variable_with_multiple_filters();
131166
test_parse_if();
132167
test_parse_for();
133168
test_parse_nested_if_in_for();

0 commit comments

Comments
 (0)