diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5acf89ad..dd6bd88a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,18 +56,18 @@ jobs: os: windows-2022 compiler: msvc - - name: macOS-11-gcc - os: macOS-11 + - name: macOS-12-gcc + os: macOS-12 compiler: gcc - - name: macOS-11-clang - os: macOS-11 - compiler: clang - - name: macOS-12-clang os: macOS-12 compiler: clang + # - name: macOS-14-clang + # os: macOS-14 + # compiler: clang + steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index a45cc563..c03f8419 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ env.set_expression("{{", "}}"); // Expressions env.set_comment("{#", "#}"); // Comments env.set_statement("{%", "%}"); // Statements {% %} for many things, see below env.set_line_statement("##"); // Line statements ## (just an opener) +env.set_html_autoescape(true); // Perform HTML escaping on all strings ``` ### 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. +### 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 can be written with the `{# ... #}` syntax. diff --git a/include/inja/config.hpp b/include/inja/config.hpp index 0a8f9b7f..f81487b1 100644 --- a/include/inja/config.hpp +++ b/include/inja/config.hpp @@ -74,6 +74,7 @@ struct ParserConfig { */ struct RenderConfig { bool throw_at_missing_includes {true}; + bool html_autoescape {false}; }; } // namespace inja diff --git a/include/inja/environment.hpp b/include/inja/environment.hpp index fdb29622..e36b6a44 100644 --- a/include/inja/environment.hpp +++ b/include/inja/environment.hpp @@ -93,6 +93,11 @@ class Environment { 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) { Parser parser(parser_config, lexer_config, template_storage, function_storage); return parser.parse(input, input_path); @@ -155,6 +160,10 @@ class Environment { 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) { Parser parser(parser_config, lexer_config, template_storage, function_storage); return Parser::load_file(input_path + filename); diff --git a/include/inja/renderer.hpp b/include/inja/renderer.hpp index efd936ed..6b8b05ec 100644 --- a/include/inja/renderer.hpp +++ b/include/inja/renderer.hpp @@ -53,9 +53,29 @@ class Renderer : public NodeVisitor { 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("&"); break; + case '\"': buffer.append("""); break; + case '\'': buffer.append("'"); break; + case '<': buffer.append("<"); break; + case '>': buffer.append(">"); break; + default: buffer.append(&data[pos], 1); break; + } + } + return buffer; + } + void print_data(const std::shared_ptr value) { if (value->is_string()) { - *output_stream << value->get_ref(); + if (config.html_autoescape) { + *output_stream << htmlescape(value->get_ref()); + } else { + *output_stream << value->get_ref(); + } } else if (value->is_number_unsigned()) { *output_stream << value->get(); } else if (value->is_number_integer()) { diff --git a/single_include/inja/inja.hpp b/single_include/inja/inja.hpp index 6c62e8da..1d8d711f 100644 --- a/single_include/inja/inja.hpp +++ b/single_include/inja/inja.hpp @@ -884,6 +884,7 @@ struct ParserConfig { */ struct RenderConfig { bool throw_at_missing_includes {true}; + bool html_autoescape {false}; }; } // namespace inja @@ -2124,9 +2125,29 @@ class Renderer : public NodeVisitor { 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("&"); break; + case '\"': buffer.append("""); break; + case '\'': buffer.append("'"); break; + case '<': buffer.append("<"); break; + case '>': buffer.append(">"); break; + default: buffer.append(&data[pos], 1); break; + } + } + return buffer; + } + void print_data(const std::shared_ptr value) { if (value->is_string()) { - *output_stream << value->get_ref(); + if (config.html_autoescape) { + *output_stream << htmlescape(value->get_ref()); + } else { + *output_stream << value->get_ref(); + } } else if (value->is_number_unsigned()) { *output_stream << value->get(); } else if (value->is_number_integer()) { @@ -2382,7 +2403,7 @@ class Renderer : public NodeVisitor { } break; case Op::Capitalize: { auto result = get_arguments<1>(node)[0]->get(); - result[0] = std::toupper(result[0]); + result[0] = static_cast(::toupper(result[0])); std::transform(result.begin() + 1, result.end(), result.begin() + 1, [](char c) { return static_cast(::tolower(c)); }); make_result(std::move(result)); } break; @@ -2792,6 +2813,11 @@ class Environment { 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) { Parser parser(parser_config, lexer_config, template_storage, function_storage); return parser.parse(input, input_path); @@ -2854,6 +2880,10 @@ class Environment { 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) { Parser parser(parser_config, lexer_config, template_storage, function_storage); return Parser::load_file(input_path + filename);