Skip to content

Commit 084b273

Browse files
authored
glz::stencil: remove unescaped and handle inverted and normal sections (#1493)
* remove unescaped * inverted sections * Update stencil.md * normal sections tests * nested inverted test
1 parent 3ace2a2 commit 084b273

File tree

4 files changed

+243
-53
lines changed

4 files changed

+243
-53
lines changed

docs/stencil.md

+8
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,11 @@ struct person
2626
};
2727
```
2828

29+
## Specification
30+
31+
- `{{field}}`
32+
- Represents an interpolated field, to be replaced
33+
- `{{#boolean}} section {{/boolean}}`
34+
- Activates the section if the boolean field is true
35+
- `{{^boolean}} section {{/boolean}}`
36+
- Activates the section if the boolean field is false

include/glaze/glaze.hpp

+1
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@
4040
#include "glaze/file/write_directory.hpp"
4141
#include "glaze/json.hpp"
4242
#include "glaze/record/recorder.hpp"
43+
#include "glaze/stencil/stencil.hpp"

include/glaze/stencil/stencil.hpp

+95-46
Original file line numberDiff line numberDiff line change
@@ -26,33 +26,22 @@ namespace glz
2626

2727
if (not bool(ctx.error)) [[likely]] {
2828
auto skip_whitespace = [&] {
29-
while (detail::whitespace_table[uint8_t(*it)]) {
29+
while (it < end && detail::whitespace_table[uint8_t(*it)]) {
3030
++it;
3131
}
3232
};
3333

3434
while (it < end) {
35-
switch (*it) {
36-
case '{': {
35+
if (*it == '{') {
3736
++it;
3837
if (it != end && *it == '{') {
3938
++it;
40-
bool unescaped = false;
4139
bool is_section = false;
4240
bool is_inverted_section = false;
4341
bool is_comment = false;
44-
[[maybe_unused]] bool is_partial = false;
4542

4643
if (it != end) {
47-
if (*it == '{') {
48-
++it;
49-
unescaped = true;
50-
}
51-
else if (*it == '&') {
52-
++it;
53-
unescaped = true;
54-
}
55-
else if (*it == '!') {
44+
if (*it == '!') {
5645
++it;
5746
is_comment = true;
5847
}
@@ -83,20 +72,96 @@ namespace glz
8372
skip_whitespace();
8473

8574
if (is_comment) {
86-
while (it != end && !(*it == '}' && (it + 1 != end && *(it + 1) == '}'))) {
75+
while (it < end && !(it + 1 < end && *it == '}' && *(it + 1) == '}')) {
8776
++it;
8877
}
89-
if (it != end) {
78+
if (it + 1 < end) {
9079
it += 2; // Skip '}}'
9180
}
92-
break;
81+
continue;
9382
}
9483

9584
if (is_section || is_inverted_section) {
96-
ctx.error = error_code::feature_not_supported;
97-
return {ctx.error, "Sections are not yet supported", size_t(it - start)};
85+
// Find the closing tag '{{/key}}'
86+
std::string closing_tag = "{{/" + std::string(key) + "}}";
87+
auto closing_pos = std::search(it, end, closing_tag.begin(), closing_tag.end());
88+
89+
if (closing_pos == end) {
90+
ctx.error = error_code::unexpected_end;
91+
return {ctx.error, "Closing tag not found for section", size_t(it - start)};
92+
}
93+
94+
if (it + 1 < end) {
95+
it += 2; // Skip '}}'
96+
}
97+
98+
// Extract inner template between current position and closing tag
99+
std::string_view inner_template(it, closing_pos);
100+
it = closing_pos + closing_tag.size();
101+
102+
// Retrieve the value associated with 'key'
103+
bool condition = false;
104+
{
105+
static constexpr auto N = reflect<T>::size;
106+
static constexpr auto HashInfo = detail::hash_info<T>;
107+
108+
const auto index =
109+
detail::decode_hash_with_size<STENCIL, T, HashInfo, HashInfo.type>::op(start, end, key.size());
110+
111+
if (index >= N) {
112+
ctx.error = error_code::unknown_key;
113+
return {ctx.error, ctx.custom_error_message, size_t(it - start)};
114+
}
115+
else {
116+
visit<N>(
117+
[&]<size_t I>() {
118+
static constexpr auto TargetKey = get<I>(reflect<T>::keys);
119+
if (TargetKey == key) [[likely]] {
120+
if constexpr (detail::bool_t<refl_t<T, I>>) {
121+
if constexpr (detail::reflectable<T>) {
122+
condition = bool(get_member(value, get<I>(to_tuple(value))));
123+
}
124+
else if constexpr (detail::glaze_object_t<T>) {
125+
condition = bool(get_member(value, get<I>(reflect<T>::values)));
126+
}
127+
}
128+
else {
129+
// For non-boolean types
130+
ctx.error = error_code::syntax_error;
131+
}
132+
}
133+
else {
134+
ctx.error = error_code::unknown_key;
135+
}
136+
},
137+
index);
138+
}
139+
}
140+
141+
142+
if (bool(ctx.error)) [[unlikely]] {
143+
return {ctx.error, ctx.custom_error_message, size_t(it - start)};
144+
}
145+
146+
// If it's an inverted section, include inner content if condition is false
147+
// Otherwise (regular section), include if condition is true
148+
bool should_include = is_inverted_section ? !condition : condition;
149+
150+
if (should_include) {
151+
// Recursively process the inner template
152+
std::string inner_buffer;
153+
auto inner_ec = stencil<Opts>(inner_template, value, inner_buffer);
154+
if (inner_ec) {
155+
return inner_ec;
156+
}
157+
buffer.append(inner_buffer);
158+
}
159+
160+
skip_whitespace();
161+
continue;
98162
}
99163

164+
// Handle regular placeholder
100165
static constexpr auto N = reflect<T>::size;
101166
static constexpr auto HashInfo = detail::hash_info<T>;
102167

@@ -143,46 +208,30 @@ namespace glz
143208

144209
skip_whitespace();
145210

146-
// Handle closing braces
147-
if (unescaped) {
148-
if (*it == '}') {
211+
if (*it == '}') {
212+
++it;
213+
if (it != end && *it == '}') {
149214
++it;
150-
if (it != end && *it == '}') {
151-
++it;
152-
if (it != end && *it == '}') {
153-
++it;
154-
break;
155-
}
156-
}
215+
continue;
216+
}
217+
else {
218+
buffer.append("}");
157219
}
158-
ctx.error = error_code::syntax_error;
159-
return {ctx.error, ctx.custom_error_message, size_t(it - start)};
160220
}
161221
else {
162-
if (*it == '}') {
163-
++it;
164-
if (it != end && *it == '}') {
165-
++it;
166-
break;
167-
}
168-
else {
169-
buffer.append("}");
170-
}
171-
break;
172-
}
222+
ctx.error = error_code::syntax_error;
223+
return {ctx.error, ctx.custom_error_message, size_t(it - start)};
173224
}
174225
}
175226
else {
176227
buffer.append("{");
177-
++it;
228+
// 'it' is already incremented past the first '{'
178229
}
179-
break;
180230
}
181-
default: {
231+
else {
182232
buffer.push_back(*it);
183233
++it;
184234
}
185-
}
186235
}
187236
}
188237

tests/stencil/stencil_test.cpp

+139-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
// Glaze Library
22
// For the license information refer to glaze.hpp
33

4-
#include "glaze/stencil/stencil.hpp"
5-
64
#include "glaze/glaze.hpp"
7-
#include "glaze/stencil/stencilcount.hpp"
85
#include "ut/ut.hpp"
96

107
using namespace ut;
@@ -15,6 +12,7 @@ struct person
1512
std::string last_name{};
1613
uint32_t age{};
1714
bool hungry{};
15+
bool employed{};
1816
};
1917

2018
suite mustache_tests = [] {
@@ -50,17 +48,151 @@ suite mustache_tests = [] {
5048
auto result = glz::stencil(layout, p).value_or("error");
5149
expect(result == "Henry Foster") << result;
5250
};
51+
52+
// **Regular Section Tests (#)**
53+
54+
"section_true"_test = [] {
55+
std::string_view layout = R"({{first_name}} {{last_name}} {{#employed}}Employed{{/employed}})";
56+
57+
person p{"Alice", "Johnson", 28, true, true}; // employed is true
58+
auto result = glz::stencil(layout, p).value_or("error");
59+
expect(result == "Alice Johnson Employed") << result;
60+
};
61+
62+
"section_false"_test = [] {
63+
std::string_view layout = R"({{first_name}} {{last_name}} {{#employed}}Employed{{/employed}})";
64+
65+
person p{"Bob", "Smith", 45, false, false}; // employed is false
66+
auto result = glz::stencil(layout, p).value_or("error");
67+
expect(result == "Bob Smith ") << result; // The section should be skipped
68+
};
69+
70+
"section_with_inner_placeholders"_test = [] {
71+
std::string_view layout = R"({{first_name}} {{last_name}} {{#employed}}Status: Employed, Age: {{age}}{{/employed}})";
72+
73+
person p{"Carol", "Davis", 30, true, true};
74+
auto result = glz::stencil(layout, p).value_or("error");
75+
expect(result == "Carol Davis Status: Employed, Age: 30") << result;
76+
};
77+
78+
"section_with_extra_text"_test = [] {
79+
std::string_view layout = R"({{first_name}} {{last_name}} {{#employed}}Employed{{/employed}}. Welcome!)";
80+
81+
person p{"Dave", "Miller", 40, true, true};
82+
auto result = glz::stencil(layout, p).value_or("error");
83+
expect(result == "Dave Miller Employed. Welcome!") << result;
84+
};
85+
86+
"section_with_extra_text_skipped"_test = [] {
87+
std::string_view layout = R"({{first_name}} {{last_name}} {{#employed}}Employed{{/employed}}. Welcome!)";
88+
89+
person p{"Eve", "Wilson", 22, true, false}; // employed is false
90+
auto result = glz::stencil(layout, p).value_or("error");
91+
expect(result == "Eve Wilson . Welcome!") << result;
92+
};
93+
94+
"nested_sections"_test = [] {
95+
std::string_view layout = R"({{first_name}} {{last_name}} {{#employed}}Status: Employed {{#hungry}}and Hungry{{/hungry}}{{/employed}})";
96+
97+
person p1{"Frank", "Taylor", 50, true, true}; // employed is true, hungry is true
98+
auto result1 = glz::stencil(layout, p1);
99+
expect(result1 == "Frank Taylor Status: Employed and Hungry");
100+
101+
person p2{"Grace", "Anderson", 0, false, true}; // employed is true, hungry is false
102+
auto result2 = glz::stencil(layout, p2);
103+
expect(result2 == "Grace Anderson Status: Employed ") << result2.value();
104+
};
105+
106+
"section_unknown_key"_test = [] {
107+
std::string_view layout = R"({{first_name}} {{last_name}} {{#unknown}}Should not appear{{/unknown}})";
108+
109+
person p{"Henry", "Foster", 34, false, true};
110+
auto result = glz::stencil(layout, p);
111+
expect(not result.has_value());
112+
expect(result.error() == glz::error_code::unknown_key);
113+
};
114+
115+
"section_mismatched_closing_tag"_test = [] {
116+
std::string_view layout = R"({{first_name}} {{last_name}} {{#employed}}Employed{{/employment}})"; // Mismatched closing tag
117+
118+
person p{"Ivy", "Thomas", 29, false, true};
119+
auto result = glz::stencil(layout, p);
120+
expect(not result.has_value());
121+
expect(result.error() == glz::error_code::unexpected_end);
122+
};
123+
124+
// **Inverted Section Tests**
53125

54-
"unsupported section"_test = [] {
55-
std::string_view layout = R"({{#hungry}}I am hungry{{/hungry}})";
126+
"inverted_section_true"_test = [] {
127+
std::string_view layout = R"({{first_name}} {{last_name}} {{^hungry}}I'm not hungry{{/hungry}})";
56128

57-
person p{"Henry", "Foster", 34, true};
129+
person p{"Henry", "Foster", 34, false}; // hungry is false
130+
auto result = glz::stencil(layout, p).value_or("error");
131+
expect(result == "Henry Foster I'm not hungry") << result;
132+
};
133+
134+
"inverted_section_false"_test = [] {
135+
std::string_view layout = R"({{first_name}} {{last_name}} {{^hungry}}I'm not hungry{{/hungry}})";
136+
137+
person p{"Henry", "Foster", 34, true}; // hungry is true
138+
auto result = glz::stencil(layout, p).value_or("error");
139+
expect(result == "Henry Foster ") << result; // The inverted section should be skipped
140+
};
141+
142+
"inverted_section_with_extra_text_true"_test = [] {
143+
std::string_view layout = R"({{first_name}} {{last_name}} {{^hungry}}I'm not hungry{{/hungry}}. Have a nice day!)";
144+
145+
person p{"Henry", "Foster", 34, false}; // hungry is false
146+
auto result = glz::stencil(layout, p).value_or("error");
147+
expect(result == "Henry Foster I'm not hungry. Have a nice day!") << result;
148+
};
149+
150+
"inverted_section_with_extra_text_false"_test = [] {
151+
std::string_view layout = R"({{first_name}} {{last_name}} {{^hungry}}I'm not hungry{{/hungry}}. Have a nice day!)";
152+
153+
person p{"Henry", "Foster", 34, true}; // hungry is true
154+
auto result = glz::stencil(layout, p).value_or("error");
155+
expect(result == "Henry Foster . Have a nice day!") << result;
156+
};
157+
158+
"nested_inverted_section"_test = [] {
159+
std::string_view layout = R"({{first_name}} {{last_name}} {{^hungry}}I'm not hungry {{^employed}}and not employed{{/employed}}{{/hungry}})";
160+
161+
person p1{"Henry", "Foster", 34, false, false};
162+
auto result1 = glz::stencil(layout, p1).value_or("error");
163+
expect(result1 == "Henry Foster I'm not hungry and not employed") << result1;
164+
165+
person p2{"Henry", "Foster", 34, false, true};
166+
auto result2 = glz::stencil(layout, p2).value_or("error");
167+
expect(result2 == "Henry Foster I'm not hungry ") << result2;
168+
169+
person p3{"Henry", "Foster", 34, true, false};
170+
std::string_view layout_skip = R"({{first_name}} {{last_name}} {{^hungry}}I'm not hungry {{^employed}}and not employed{{/employed}}{{/hungry}})";
171+
auto result3 = glz::stencil(layout_skip, p3).value_or("error");
172+
expect(result3 == "Henry Foster ") << result3;
173+
};
174+
175+
"inverted_section_unknown_key"_test = [] {
176+
std::string_view layout = R"({{first_name}} {{last_name}} {{^unknown}}Should not appear{{/unknown}})";
177+
178+
person p{"Henry", "Foster", 34, false};
58179
auto result = glz::stencil(layout, p);
59180
expect(not result.has_value());
60-
expect(result.error() == glz::error_code::feature_not_supported);
181+
expect(result.error() == glz::error_code::unknown_key);
182+
};
183+
184+
"inverted_section_mismatched_closing_tag"_test = [] {
185+
std::string_view layout = R"({{first_name}} {{last_name}} {{^hungry}}I'm not hungry{{/hunger}})"; // Mismatched closing tag
186+
187+
person p{"Henry", "Foster", 34, false};
188+
auto result = glz::stencil(layout, p);
189+
expect(not result.has_value());
190+
expect(result.error() == glz::error_code::unexpected_end);
61191
};
62192
};
63193

194+
#include "glaze/stencil/stencilcount.hpp"
195+
64196
suite stencilcount_tests = [] {
65197
"basic docstencil"_test = [] {
66198
std::string_view layout = R"(# About

0 commit comments

Comments
 (0)