Skip to content

Commit 807620c

Browse files
berthubertpantor
andauthored
Add HTML autoescape (#292)
* add and document set_html_autoescape * add render_to to Environment that accepts a string (and turns it into a Template) * code style, update single include * update ci * revert macos-14 test --------- Co-authored-by: pantor <[email protected]>
1 parent c360b19 commit 807620c

File tree

6 files changed

+77
-9
lines changed

6 files changed

+77
-9
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,18 +56,18 @@ jobs:
5656
os: windows-2022
5757
compiler: msvc
5858

59-
- name: macOS-11-gcc
60-
os: macOS-11
59+
- name: macOS-12-gcc
60+
os: macOS-12
6161
compiler: gcc
6262

63-
- name: macOS-11-clang
64-
os: macOS-11
65-
compiler: clang
66-
6763
- name: macOS-12-clang
6864
os: macOS-12
6965
compiler: clang
7066

67+
# - name: macOS-14-clang
68+
# os: macOS-14
69+
# compiler: clang
70+
7171
steps:
7272
- uses: actions/checkout@v3
7373

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ env.set_expression("{{", "}}"); // Expressions
110110
env.set_comment("{#", "#}"); // Comments
111111
env.set_statement("{%", "%}"); // Statements {% %} for many things, see below
112112
env.set_line_statement("##"); // Line statements ## (just an opener)
113+
env.set_html_autoescape(true); // Perform HTML escaping on all strings
113114
```
114115
115116
### Variables
@@ -364,6 +365,13 @@ render("{% if neighbour in guests -%} I was there{% endif -%} !", data); //
364365
365366
Stripping behind a statement or expression also removes any newlines.
366367
368+
### HTML escaping
369+
370+
Templates are frequently used to creat HTML pages. Source data that contains
371+
characters that have meaning within HTML (like <. >, &) needs to be escaped.
372+
It is often inconvenient to perform such escaping within the JSON data. With `Environment::set_html_autoescape(true)`, Inja can be configured to
373+
HTML escape each and every string created.
374+
367375
### Comments
368376
369377
Comments can be written with the `{# ... #}` syntax.

include/inja/config.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ struct ParserConfig {
7474
*/
7575
struct RenderConfig {
7676
bool throw_at_missing_includes {true};
77+
bool html_autoescape {false};
7778
};
7879

7980
} // namespace inja

include/inja/environment.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ class Environment {
9393
render_config.throw_at_missing_includes = will_throw;
9494
}
9595

96+
/// Sets whether we'll automatically perform HTML escape
97+
void set_html_autoescape(bool will_escape) {
98+
render_config.html_autoescape = will_escape;
99+
}
100+
96101
Template parse(std::string_view input) {
97102
Parser parser(parser_config, lexer_config, template_storage, function_storage);
98103
return parser.parse(input, input_path);
@@ -155,6 +160,10 @@ class Environment {
155160
return os;
156161
}
157162

163+
std::ostream& render_to(std::ostream& os, const std::string_view input, const json& data) {
164+
return render_to(os, parse(input), data);
165+
}
166+
158167
std::string load_file(const std::string& filename) {
159168
Parser parser(parser_config, lexer_config, template_storage, function_storage);
160169
return Parser::load_file(input_path + filename);

include/inja/renderer.hpp

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,29 @@ class Renderer : public NodeVisitor {
5353
return !data->empty();
5454
}
5555

56+
static std::string htmlescape(const std::string& data) {
57+
std::string buffer;
58+
buffer.reserve(1.1 * data.size());
59+
for (size_t pos = 0; pos != data.size(); ++pos) {
60+
switch (data[pos]) {
61+
case '&': buffer.append("&amp;"); break;
62+
case '\"': buffer.append("&quot;"); break;
63+
case '\'': buffer.append("&apos;"); break;
64+
case '<': buffer.append("&lt;"); break;
65+
case '>': buffer.append("&gt;"); break;
66+
default: buffer.append(&data[pos], 1); break;
67+
}
68+
}
69+
return buffer;
70+
}
71+
5672
void print_data(const std::shared_ptr<json> value) {
5773
if (value->is_string()) {
58-
*output_stream << value->get_ref<const json::string_t&>();
74+
if (config.html_autoescape) {
75+
*output_stream << htmlescape(value->get_ref<const json::string_t&>());
76+
} else {
77+
*output_stream << value->get_ref<const json::string_t&>();
78+
}
5979
} else if (value->is_number_unsigned()) {
6080
*output_stream << value->get<const json::number_unsigned_t>();
6181
} else if (value->is_number_integer()) {

single_include/inja/inja.hpp

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,7 @@ struct ParserConfig {
884884
*/
885885
struct RenderConfig {
886886
bool throw_at_missing_includes {true};
887+
bool html_autoescape {false};
887888
};
888889

889890
} // namespace inja
@@ -2124,9 +2125,29 @@ class Renderer : public NodeVisitor {
21242125
return !data->empty();
21252126
}
21262127

2128+
static std::string htmlescape(const std::string& data) {
2129+
std::string buffer;
2130+
buffer.reserve(1.1 * data.size());
2131+
for (size_t pos = 0; pos != data.size(); ++pos) {
2132+
switch (data[pos]) {
2133+
case '&': buffer.append("&amp;"); break;
2134+
case '\"': buffer.append("&quot;"); break;
2135+
case '\'': buffer.append("&apos;"); break;
2136+
case '<': buffer.append("&lt;"); break;
2137+
case '>': buffer.append("&gt;"); break;
2138+
default: buffer.append(&data[pos], 1); break;
2139+
}
2140+
}
2141+
return buffer;
2142+
}
2143+
21272144
void print_data(const std::shared_ptr<json> value) {
21282145
if (value->is_string()) {
2129-
*output_stream << value->get_ref<const json::string_t&>();
2146+
if (config.html_autoescape) {
2147+
*output_stream << htmlescape(value->get_ref<const json::string_t&>());
2148+
} else {
2149+
*output_stream << value->get_ref<const json::string_t&>();
2150+
}
21302151
} else if (value->is_number_unsigned()) {
21312152
*output_stream << value->get<const json::number_unsigned_t>();
21322153
} else if (value->is_number_integer()) {
@@ -2382,7 +2403,7 @@ class Renderer : public NodeVisitor {
23822403
} break;
23832404
case Op::Capitalize: {
23842405
auto result = get_arguments<1>(node)[0]->get<json::string_t>();
2385-
result[0] = std::toupper(result[0]);
2406+
result[0] = static_cast<char>(::toupper(result[0]));
23862407
std::transform(result.begin() + 1, result.end(), result.begin() + 1, [](char c) { return static_cast<char>(::tolower(c)); });
23872408
make_result(std::move(result));
23882409
} break;
@@ -2792,6 +2813,11 @@ class Environment {
27922813
render_config.throw_at_missing_includes = will_throw;
27932814
}
27942815

2816+
/// Sets whether we'll automatically perform HTML escape
2817+
void set_html_autoescape(bool will_escape) {
2818+
render_config.html_autoescape = will_escape;
2819+
}
2820+
27952821
Template parse(std::string_view input) {
27962822
Parser parser(parser_config, lexer_config, template_storage, function_storage);
27972823
return parser.parse(input, input_path);
@@ -2854,6 +2880,10 @@ class Environment {
28542880
return os;
28552881
}
28562882

2883+
std::ostream& render_to(std::ostream& os, const std::string_view input, const json& data) {
2884+
return render_to(os, parse(input), data);
2885+
}
2886+
28572887
std::string load_file(const std::string& filename) {
28582888
Parser parser(parser_config, lexer_config, template_storage, function_storage);
28592889
return Parser::load_file(input_path + filename);

0 commit comments

Comments
 (0)