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 <lars.berscheid@online.de>
This commit is contained in:
bert hubert
2024-10-05 09:08:46 +02:00
committed by GitHub
parent c360b19855
commit 807620c80c
6 changed files with 77 additions and 9 deletions

View File

@@ -56,18 +56,18 @@ jobs:
os: windows-2022 os: windows-2022
compiler: msvc compiler: msvc
- name: macOS-11-gcc - name: macOS-12-gcc
os: macOS-11 os: macOS-12
compiler: gcc compiler: gcc
- name: macOS-11-clang
os: macOS-11
compiler: clang
- name: macOS-12-clang - name: macOS-12-clang
os: macOS-12 os: macOS-12
compiler: clang compiler: clang
# - name: macOS-14-clang
# os: macOS-14
# compiler: clang
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

View File

@@ -110,6 +110,7 @@ env.set_expression("{{", "}}"); // Expressions
env.set_comment("{#", "#}"); // Comments env.set_comment("{#", "#}"); // Comments
env.set_statement("{%", "%}"); // Statements {% %} for many things, see below env.set_statement("{%", "%}"); // Statements {% %} for many things, see below
env.set_line_statement("##"); // Line statements ## (just an opener) env.set_line_statement("##"); // Line statements ## (just an opener)
env.set_html_autoescape(true); // Perform HTML escaping on all strings
``` ```
### Variables ### Variables
@@ -364,6 +365,13 @@ render("{% if neighbour in guests -%} I was there{% endif -%} !", data); //
Stripping behind a statement or expression also removes any newlines. Stripping behind a statement or expression also removes any newlines.
### HTML escaping
Templates are frequently used to creat HTML pages. Source data that contains
characters that have meaning within HTML (like <. >, &) needs to be escaped.
It is often inconvenient to perform such escaping within the JSON data. With `Environment::set_html_autoescape(true)`, Inja can be configured to
HTML escape each and every string created.
### Comments ### Comments
Comments can be written with the `{# ... #}` syntax. Comments can be written with the `{# ... #}` syntax.

View File

@@ -74,6 +74,7 @@ struct ParserConfig {
*/ */
struct RenderConfig { struct RenderConfig {
bool throw_at_missing_includes {true}; bool throw_at_missing_includes {true};
bool html_autoescape {false};
}; };
} // namespace inja } // namespace inja

View File

@@ -93,6 +93,11 @@ public:
render_config.throw_at_missing_includes = will_throw; render_config.throw_at_missing_includes = will_throw;
} }
/// Sets whether we'll automatically perform HTML escape
void set_html_autoescape(bool will_escape) {
render_config.html_autoescape = will_escape;
}
Template parse(std::string_view input) { Template parse(std::string_view input) {
Parser parser(parser_config, lexer_config, template_storage, function_storage); Parser parser(parser_config, lexer_config, template_storage, function_storage);
return parser.parse(input, input_path); return parser.parse(input, input_path);
@@ -155,6 +160,10 @@ public:
return os; return os;
} }
std::ostream& render_to(std::ostream& os, const std::string_view input, const json& data) {
return render_to(os, parse(input), data);
}
std::string load_file(const std::string& filename) { std::string load_file(const std::string& filename) {
Parser parser(parser_config, lexer_config, template_storage, function_storage); Parser parser(parser_config, lexer_config, template_storage, function_storage);
return Parser::load_file(input_path + filename); return Parser::load_file(input_path + filename);

View File

@@ -53,9 +53,29 @@ class Renderer : public NodeVisitor {
return !data->empty(); return !data->empty();
} }
static std::string htmlescape(const std::string& data) {
std::string buffer;
buffer.reserve(1.1 * data.size());
for (size_t pos = 0; pos != data.size(); ++pos) {
switch (data[pos]) {
case '&': buffer.append("&amp;"); break;
case '\"': buffer.append("&quot;"); break;
case '\'': buffer.append("&apos;"); break;
case '<': buffer.append("&lt;"); break;
case '>': buffer.append("&gt;"); break;
default: buffer.append(&data[pos], 1); break;
}
}
return buffer;
}
void print_data(const std::shared_ptr<json> value) { void print_data(const std::shared_ptr<json> value) {
if (value->is_string()) { if (value->is_string()) {
*output_stream << value->get_ref<const json::string_t&>(); if (config.html_autoescape) {
*output_stream << htmlescape(value->get_ref<const json::string_t&>());
} else {
*output_stream << value->get_ref<const json::string_t&>();
}
} else if (value->is_number_unsigned()) { } else if (value->is_number_unsigned()) {
*output_stream << value->get<const json::number_unsigned_t>(); *output_stream << value->get<const json::number_unsigned_t>();
} else if (value->is_number_integer()) { } else if (value->is_number_integer()) {

View File

@@ -884,6 +884,7 @@ struct ParserConfig {
*/ */
struct RenderConfig { struct RenderConfig {
bool throw_at_missing_includes {true}; bool throw_at_missing_includes {true};
bool html_autoescape {false};
}; };
} // namespace inja } // namespace inja
@@ -2124,9 +2125,29 @@ class Renderer : public NodeVisitor {
return !data->empty(); return !data->empty();
} }
static std::string htmlescape(const std::string& data) {
std::string buffer;
buffer.reserve(1.1 * data.size());
for (size_t pos = 0; pos != data.size(); ++pos) {
switch (data[pos]) {
case '&': buffer.append("&amp;"); break;
case '\"': buffer.append("&quot;"); break;
case '\'': buffer.append("&apos;"); break;
case '<': buffer.append("&lt;"); break;
case '>': buffer.append("&gt;"); break;
default: buffer.append(&data[pos], 1); break;
}
}
return buffer;
}
void print_data(const std::shared_ptr<json> value) { void print_data(const std::shared_ptr<json> value) {
if (value->is_string()) { if (value->is_string()) {
*output_stream << value->get_ref<const json::string_t&>(); if (config.html_autoescape) {
*output_stream << htmlescape(value->get_ref<const json::string_t&>());
} else {
*output_stream << value->get_ref<const json::string_t&>();
}
} else if (value->is_number_unsigned()) { } else if (value->is_number_unsigned()) {
*output_stream << value->get<const json::number_unsigned_t>(); *output_stream << value->get<const json::number_unsigned_t>();
} else if (value->is_number_integer()) { } else if (value->is_number_integer()) {
@@ -2382,7 +2403,7 @@ class Renderer : public NodeVisitor {
} break; } break;
case Op::Capitalize: { case Op::Capitalize: {
auto result = get_arguments<1>(node)[0]->get<json::string_t>(); auto result = get_arguments<1>(node)[0]->get<json::string_t>();
result[0] = std::toupper(result[0]); result[0] = static_cast<char>(::toupper(result[0]));
std::transform(result.begin() + 1, result.end(), result.begin() + 1, [](char c) { return static_cast<char>(::tolower(c)); }); std::transform(result.begin() + 1, result.end(), result.begin() + 1, [](char c) { return static_cast<char>(::tolower(c)); });
make_result(std::move(result)); make_result(std::move(result));
} break; } break;
@@ -2792,6 +2813,11 @@ public:
render_config.throw_at_missing_includes = will_throw; render_config.throw_at_missing_includes = will_throw;
} }
/// Sets whether we'll automatically perform HTML escape
void set_html_autoescape(bool will_escape) {
render_config.html_autoescape = will_escape;
}
Template parse(std::string_view input) { Template parse(std::string_view input) {
Parser parser(parser_config, lexer_config, template_storage, function_storage); Parser parser(parser_config, lexer_config, template_storage, function_storage);
return parser.parse(input, input_path); return parser.parse(input, input_path);
@@ -2854,6 +2880,10 @@ public:
return os; return os;
} }
std::ostream& render_to(std::ostream& os, const std::string_view input, const json& data) {
return render_to(os, parse(input), data);
}
std::string load_file(const std::string& filename) { std::string load_file(const std::string& filename) {
Parser parser(parser_config, lexer_config, template_storage, function_storage); Parser parser(parser_config, lexer_config, template_storage, function_storage);
return Parser::load_file(input_path + filename); return Parser::load_file(input_path + filename);