Skip to content

Commit

Permalink
glz::stencil: remove unescaped and handle inverted and normal sections (
Browse files Browse the repository at this point in the history
#1493)

* remove unescaped

* inverted sections

* Update stencil.md

* normal sections tests

* nested inverted test
  • Loading branch information
stephenberry authored Dec 13, 2024
1 parent 3ace2a2 commit 084b273
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 53 deletions.
8 changes: 8 additions & 0 deletions docs/stencil.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@ struct person
};
```

## Specification

- `{{field}}`
- Represents an interpolated field, to be replaced
- `{{#boolean}} section {{/boolean}}`
- Activates the section if the boolean field is true
- `{{^boolean}} section {{/boolean}}`
- Activates the section if the boolean field is false
1 change: 1 addition & 0 deletions include/glaze/glaze.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@
#include "glaze/file/write_directory.hpp"
#include "glaze/json.hpp"
#include "glaze/record/recorder.hpp"
#include "glaze/stencil/stencil.hpp"
141 changes: 95 additions & 46 deletions include/glaze/stencil/stencil.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,33 +26,22 @@ namespace glz

if (not bool(ctx.error)) [[likely]] {
auto skip_whitespace = [&] {
while (detail::whitespace_table[uint8_t(*it)]) {
while (it < end && detail::whitespace_table[uint8_t(*it)]) {
++it;
}
};

while (it < end) {
switch (*it) {
case '{': {
if (*it == '{') {
++it;
if (it != end && *it == '{') {
++it;
bool unescaped = false;
bool is_section = false;
bool is_inverted_section = false;
bool is_comment = false;
[[maybe_unused]] bool is_partial = false;

if (it != end) {
if (*it == '{') {
++it;
unescaped = true;
}
else if (*it == '&') {
++it;
unescaped = true;
}
else if (*it == '!') {
if (*it == '!') {
++it;
is_comment = true;
}
Expand Down Expand Up @@ -83,20 +72,96 @@ namespace glz
skip_whitespace();

if (is_comment) {
while (it != end && !(*it == '}' && (it + 1 != end && *(it + 1) == '}'))) {
while (it < end && !(it + 1 < end && *it == '}' && *(it + 1) == '}')) {
++it;
}
if (it != end) {
if (it + 1 < end) {
it += 2; // Skip '}}'
}
break;
continue;
}

if (is_section || is_inverted_section) {
ctx.error = error_code::feature_not_supported;
return {ctx.error, "Sections are not yet supported", size_t(it - start)};
// Find the closing tag '{{/key}}'
std::string closing_tag = "{{/" + std::string(key) + "}}";
auto closing_pos = std::search(it, end, closing_tag.begin(), closing_tag.end());

if (closing_pos == end) {
ctx.error = error_code::unexpected_end;
return {ctx.error, "Closing tag not found for section", size_t(it - start)};
}

if (it + 1 < end) {
it += 2; // Skip '}}'
}

// Extract inner template between current position and closing tag
std::string_view inner_template(it, closing_pos);
it = closing_pos + closing_tag.size();

// Retrieve the value associated with 'key'
bool condition = false;
{
static constexpr auto N = reflect<T>::size;
static constexpr auto HashInfo = detail::hash_info<T>;

const auto index =
detail::decode_hash_with_size<STENCIL, T, HashInfo, HashInfo.type>::op(start, end, key.size());

if (index >= N) {
ctx.error = error_code::unknown_key;
return {ctx.error, ctx.custom_error_message, size_t(it - start)};
}
else {
visit<N>(
[&]<size_t I>() {
static constexpr auto TargetKey = get<I>(reflect<T>::keys);
if (TargetKey == key) [[likely]] {
if constexpr (detail::bool_t<refl_t<T, I>>) {
if constexpr (detail::reflectable<T>) {
condition = bool(get_member(value, get<I>(to_tuple(value))));
}
else if constexpr (detail::glaze_object_t<T>) {
condition = bool(get_member(value, get<I>(reflect<T>::values)));
}
}
else {
// For non-boolean types
ctx.error = error_code::syntax_error;
}
}
else {
ctx.error = error_code::unknown_key;
}
},
index);
}
}


if (bool(ctx.error)) [[unlikely]] {
return {ctx.error, ctx.custom_error_message, size_t(it - start)};
}

// If it's an inverted section, include inner content if condition is false
// Otherwise (regular section), include if condition is true
bool should_include = is_inverted_section ? !condition : condition;

if (should_include) {
// Recursively process the inner template
std::string inner_buffer;
auto inner_ec = stencil<Opts>(inner_template, value, inner_buffer);
if (inner_ec) {
return inner_ec;
}
buffer.append(inner_buffer);
}

skip_whitespace();
continue;
}

// Handle regular placeholder
static constexpr auto N = reflect<T>::size;
static constexpr auto HashInfo = detail::hash_info<T>;

Expand Down Expand Up @@ -143,46 +208,30 @@ namespace glz

skip_whitespace();

// Handle closing braces
if (unescaped) {
if (*it == '}') {
if (*it == '}') {
++it;
if (it != end && *it == '}') {
++it;
if (it != end && *it == '}') {
++it;
if (it != end && *it == '}') {
++it;
break;
}
}
continue;
}
else {
buffer.append("}");
}
ctx.error = error_code::syntax_error;
return {ctx.error, ctx.custom_error_message, size_t(it - start)};
}
else {
if (*it == '}') {
++it;
if (it != end && *it == '}') {
++it;
break;
}
else {
buffer.append("}");
}
break;
}
ctx.error = error_code::syntax_error;
return {ctx.error, ctx.custom_error_message, size_t(it - start)};
}
}
else {
buffer.append("{");
++it;
// 'it' is already incremented past the first '{'
}
break;
}
default: {
else {
buffer.push_back(*it);
++it;
}
}
}
}

Expand Down
146 changes: 139 additions & 7 deletions tests/stencil/stencil_test.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
// Glaze Library
// For the license information refer to glaze.hpp

#include "glaze/stencil/stencil.hpp"

#include "glaze/glaze.hpp"
#include "glaze/stencil/stencilcount.hpp"
#include "ut/ut.hpp"

using namespace ut;
Expand All @@ -15,6 +12,7 @@ struct person
std::string last_name{};
uint32_t age{};
bool hungry{};
bool employed{};
};

suite mustache_tests = [] {
Expand Down Expand Up @@ -50,17 +48,151 @@ suite mustache_tests = [] {
auto result = glz::stencil(layout, p).value_or("error");
expect(result == "Henry Foster") << result;
};

// **Regular Section Tests (#)**

"section_true"_test = [] {
std::string_view layout = R"({{first_name}} {{last_name}} {{#employed}}Employed{{/employed}})";

person p{"Alice", "Johnson", 28, true, true}; // employed is true
auto result = glz::stencil(layout, p).value_or("error");
expect(result == "Alice Johnson Employed") << result;
};

"section_false"_test = [] {
std::string_view layout = R"({{first_name}} {{last_name}} {{#employed}}Employed{{/employed}})";

person p{"Bob", "Smith", 45, false, false}; // employed is false
auto result = glz::stencil(layout, p).value_or("error");
expect(result == "Bob Smith ") << result; // The section should be skipped
};

"section_with_inner_placeholders"_test = [] {
std::string_view layout = R"({{first_name}} {{last_name}} {{#employed}}Status: Employed, Age: {{age}}{{/employed}})";

person p{"Carol", "Davis", 30, true, true};
auto result = glz::stencil(layout, p).value_or("error");
expect(result == "Carol Davis Status: Employed, Age: 30") << result;
};

"section_with_extra_text"_test = [] {
std::string_view layout = R"({{first_name}} {{last_name}} {{#employed}}Employed{{/employed}}. Welcome!)";

person p{"Dave", "Miller", 40, true, true};
auto result = glz::stencil(layout, p).value_or("error");
expect(result == "Dave Miller Employed. Welcome!") << result;
};

"section_with_extra_text_skipped"_test = [] {
std::string_view layout = R"({{first_name}} {{last_name}} {{#employed}}Employed{{/employed}}. Welcome!)";

person p{"Eve", "Wilson", 22, true, false}; // employed is false
auto result = glz::stencil(layout, p).value_or("error");
expect(result == "Eve Wilson . Welcome!") << result;
};

"nested_sections"_test = [] {
std::string_view layout = R"({{first_name}} {{last_name}} {{#employed}}Status: Employed {{#hungry}}and Hungry{{/hungry}}{{/employed}})";

person p1{"Frank", "Taylor", 50, true, true}; // employed is true, hungry is true
auto result1 = glz::stencil(layout, p1);
expect(result1 == "Frank Taylor Status: Employed and Hungry");

person p2{"Grace", "Anderson", 0, false, true}; // employed is true, hungry is false
auto result2 = glz::stencil(layout, p2);
expect(result2 == "Grace Anderson Status: Employed ") << result2.value();
};

"section_unknown_key"_test = [] {
std::string_view layout = R"({{first_name}} {{last_name}} {{#unknown}}Should not appear{{/unknown}})";

person p{"Henry", "Foster", 34, false, true};
auto result = glz::stencil(layout, p);
expect(not result.has_value());
expect(result.error() == glz::error_code::unknown_key);
};

"section_mismatched_closing_tag"_test = [] {
std::string_view layout = R"({{first_name}} {{last_name}} {{#employed}}Employed{{/employment}})"; // Mismatched closing tag

person p{"Ivy", "Thomas", 29, false, true};
auto result = glz::stencil(layout, p);
expect(not result.has_value());
expect(result.error() == glz::error_code::unexpected_end);
};

// **Inverted Section Tests**

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

person p{"Henry", "Foster", 34, true};
person p{"Henry", "Foster", 34, false}; // hungry is false
auto result = glz::stencil(layout, p).value_or("error");
expect(result == "Henry Foster I'm not hungry") << result;
};

"inverted_section_false"_test = [] {
std::string_view layout = R"({{first_name}} {{last_name}} {{^hungry}}I'm not hungry{{/hungry}})";

person p{"Henry", "Foster", 34, true}; // hungry is true
auto result = glz::stencil(layout, p).value_or("error");
expect(result == "Henry Foster ") << result; // The inverted section should be skipped
};

"inverted_section_with_extra_text_true"_test = [] {
std::string_view layout = R"({{first_name}} {{last_name}} {{^hungry}}I'm not hungry{{/hungry}}. Have a nice day!)";

person p{"Henry", "Foster", 34, false}; // hungry is false
auto result = glz::stencil(layout, p).value_or("error");
expect(result == "Henry Foster I'm not hungry. Have a nice day!") << result;
};

"inverted_section_with_extra_text_false"_test = [] {
std::string_view layout = R"({{first_name}} {{last_name}} {{^hungry}}I'm not hungry{{/hungry}}. Have a nice day!)";

person p{"Henry", "Foster", 34, true}; // hungry is true
auto result = glz::stencil(layout, p).value_or("error");
expect(result == "Henry Foster . Have a nice day!") << result;
};

"nested_inverted_section"_test = [] {
std::string_view layout = R"({{first_name}} {{last_name}} {{^hungry}}I'm not hungry {{^employed}}and not employed{{/employed}}{{/hungry}})";

person p1{"Henry", "Foster", 34, false, false};
auto result1 = glz::stencil(layout, p1).value_or("error");
expect(result1 == "Henry Foster I'm not hungry and not employed") << result1;

person p2{"Henry", "Foster", 34, false, true};
auto result2 = glz::stencil(layout, p2).value_or("error");
expect(result2 == "Henry Foster I'm not hungry ") << result2;

person p3{"Henry", "Foster", 34, true, false};
std::string_view layout_skip = R"({{first_name}} {{last_name}} {{^hungry}}I'm not hungry {{^employed}}and not employed{{/employed}}{{/hungry}})";
auto result3 = glz::stencil(layout_skip, p3).value_or("error");
expect(result3 == "Henry Foster ") << result3;
};

"inverted_section_unknown_key"_test = [] {
std::string_view layout = R"({{first_name}} {{last_name}} {{^unknown}}Should not appear{{/unknown}})";

person p{"Henry", "Foster", 34, false};
auto result = glz::stencil(layout, p);
expect(not result.has_value());
expect(result.error() == glz::error_code::feature_not_supported);
expect(result.error() == glz::error_code::unknown_key);
};

"inverted_section_mismatched_closing_tag"_test = [] {
std::string_view layout = R"({{first_name}} {{last_name}} {{^hungry}}I'm not hungry{{/hunger}})"; // Mismatched closing tag

person p{"Henry", "Foster", 34, false};
auto result = glz::stencil(layout, p);
expect(not result.has_value());
expect(result.error() == glz::error_code::unexpected_end);
};
};

#include "glaze/stencil/stencilcount.hpp"

suite stencilcount_tests = [] {
"basic docstencil"_test = [] {
std::string_view layout = R"(# About
Expand Down

0 comments on commit 084b273

Please sign in to comment.