Add Child Inheritance Feature (#198)

* add block and extend feature

* constify, code cleaning

* update single include

* add html language to readme

* clean tests

* constantify, update year
diff --git a/LICENSE b/LICENSE
index c6a3b44..d9d0e94 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2018 lbersch
+Copyright (c) 2018-2021 lbersch
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 2dba30e..4db9707 100644
--- a/README.md
+++ b/README.md
@@ -254,25 +254,6 @@
 // Implemented type checks: isArray, isBoolean, isFloat, isInteger, isNumber, isObject, isString,
 ```
 
-### Whitespace Control
-
-In the default configuration, no whitespace is removed while rendering the file. To support a more readable template style, you can configure the environment to control whitespaces before and after a statement automatically. While enabling `set_trim_blocks` removes the first newline after a statement, `set_lstrip_blocks` strips tabs and spaces from the beginning of a line to the start of a block.
-
-```.cpp
-Environment env;
-env.set_trim_blocks(true);
-env.set_lstrip_blocks(true);
-```
-
-With both `trim_blocks` and `lstrip_blocks` enabled, you can put statements on their own lines. Furthermore, you can also strip whitespaces for both statements and expressions by hand. If you add a minus sign (`-`) to the start or end, the whitespaces before or after that block will be removed:
-
-```.cpp
-render("Hello       {{- name -}}     !", data); // "Hello Inja!"
-render("{% if neighbour in guests -%}   I was there{% endif -%}   !", data); // Renders without any whitespaces
-```
-
-Stripping behind a statement or expression also removes any newlines.
-
 ### Callbacks
 
 You can create your own and more complex functions with callbacks. These are implemented with `std::function`, so you can for example use C++ lambdas. Inja `Arguments` are a vector of json pointers.
@@ -316,6 +297,61 @@
 env.render("{{ log(neighbour) }}", data); // Prints nothing to result, only to cout...
 ```
 
+### Template Inheritance
+
+Template inheritance allows you to build a base *skeleton* template that contains all the common elements and defines blocks that child templates can override. Lets show an example: The base template
+```.html
+<!DOCTYPE html>
+<html>
+<head>
+  {% block head %}
+  <link rel="stylesheet" href="style.css" />
+  <title>{% block title %}{% endblock %} - My Webpage</title>
+  {% endblock %}
+</head>
+<body>
+  <div id="content">{% block content %}{% endblock %}</div>
+</body>
+</html>
+```
+contains three `blocks` that child templates can fill in. The child template
+```.html
+{% extends "base.html" %}
+{% block title %}Index{% endblock %}
+{% block head %}
+  {{ super() }}
+  <style type="text/css">
+    .important { color: #336699; }
+  </style>
+{% endblock %}
+{% block content %}
+  <h1>Index</h1>
+  <p class="important">
+    Welcome to my blog!
+  </p>
+{% endblock %}
+```
+calls a parent template with the `extends` keyword; it should be the first element in the template. It is possible to render the contents of the parent block by calling `super()`. In the case of multiple levels of `{% extends %}`, super references may be called with an argument (e.g. `super(2)`) to skip levels in the inheritance tree.
+
+### Whitespace Control
+
+In the default configuration, no whitespace is removed while rendering the file. To support a more readable template style, you can configure the environment to control whitespaces before and after a statement automatically. While enabling `set_trim_blocks` removes the first newline after a statement, `set_lstrip_blocks` strips tabs and spaces from the beginning of a line to the start of a block.
+
+```.cpp
+Environment env;
+env.set_trim_blocks(true);
+env.set_lstrip_blocks(true);
+```
+
+With both `trim_blocks` and `lstrip_blocks` enabled, you can put statements on their own lines. Furthermore, you can also strip whitespaces for both statements and expressions by hand. If you add a minus sign (`-`) to the start or end, the whitespaces before or after that block will be removed:
+
+```.cpp
+render("Hello       {{- name -}}     !", data); // "Hello Inja!"
+render("{% if neighbour in guests -%}   I was there{% endif -%}   !", data); // Renders without any whitespaces
+```
+
+Stripping behind a statement or expression also removes any newlines.
+
 ### Comments
 
 Comments can be written with the `{# ... #}` syntax.
diff --git a/include/inja/config.hpp b/include/inja/config.hpp
index 3f284a4..35ea2a5 100644
--- a/include/inja/config.hpp
+++ b/include/inja/config.hpp
@@ -1,4 +1,4 @@
-// Copyright (c) 2019 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_CONFIG_HPP_
 #define INCLUDE_INJA_CONFIG_HPP_
diff --git a/include/inja/environment.hpp b/include/inja/environment.hpp
index ed99537..ea21efe 100644
--- a/include/inja/environment.hpp
+++ b/include/inja/environment.hpp
@@ -1,4 +1,4 @@
-// Copyright (c) 2019 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_ENVIRONMENT_HPP_
 #define INCLUDE_INJA_ENVIRONMENT_HPP_
diff --git a/include/inja/exceptions.hpp b/include/inja/exceptions.hpp
index 2784da8..9b349a4 100644
--- a/include/inja/exceptions.hpp
+++ b/include/inja/exceptions.hpp
@@ -1,4 +1,4 @@
-// Copyright (c) 2020 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_EXCEPTIONS_HPP_
 #define INCLUDE_INJA_EXCEPTIONS_HPP_
diff --git a/include/inja/function_storage.hpp b/include/inja/function_storage.hpp
index b0091bd..1b6070b 100644
--- a/include/inja/function_storage.hpp
+++ b/include/inja/function_storage.hpp
@@ -1,4 +1,4 @@
-// Copyright (c) 2020 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_FUNCTION_STORAGE_HPP_
 #define INCLUDE_INJA_FUNCTION_STORAGE_HPP_
@@ -64,6 +64,7 @@
     Round,
     Sort,
     Upper,
+    Super,
     Callback,
     ParenLeft,
     ParenRight,
@@ -106,6 +107,8 @@
     {std::make_pair("round", 2), FunctionData { Operation::Round }},
     {std::make_pair("sort", 1), FunctionData { Operation::Sort }},
     {std::make_pair("upper", 1), FunctionData { Operation::Upper }},
+    {std::make_pair("super", 0), FunctionData { Operation::Super }},
+    {std::make_pair("super", 1), FunctionData { Operation::Super }},
   };
 
 public:
diff --git a/include/inja/inja.hpp b/include/inja/inja.hpp
index 92b6345..49170ad 100644
--- a/include/inja/inja.hpp
+++ b/include/inja/inja.hpp
@@ -1,4 +1,4 @@
-// Copyright (c) 2020 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_INJA_HPP_
 #define INCLUDE_INJA_INJA_HPP_
diff --git a/include/inja/lexer.hpp b/include/inja/lexer.hpp
index e31c3d6..3be56fa 100644
--- a/include/inja/lexer.hpp
+++ b/include/inja/lexer.hpp
@@ -51,7 +51,7 @@
     if (tok_start >= m_in.size()) {
       return make_token(Token::Kind::Eof);
     }
-    char ch = m_in[tok_start];
+    const char ch = m_in[tok_start];
     if (ch == ' ' || ch == '\t' || ch == '\r') {
       tok_start += 1;
       goto again;
@@ -61,7 +61,7 @@
     if (!close_trim.empty() && inja::string_view::starts_with(m_in.substr(tok_start), close_trim)) {
       state = State::Text;
       pos = tok_start + close_trim.size();
-      Token tok = make_token(closeKind);
+      const Token tok = make_token(closeKind);
       skip_whitespaces_and_newlines();
       return tok;
     }
@@ -69,7 +69,7 @@
     if (inja::string_view::starts_with(m_in.substr(tok_start), close)) {
       state = State::Text;
       pos = tok_start + close.size();
-      Token tok = make_token(closeKind);
+      const Token tok = make_token(closeKind);
       if (trim) {
         skip_whitespaces_and_first_newline();
       }
@@ -88,7 +88,7 @@
       return scan_id();
     }
 
-    MinusState current_minus_state = minus_state;
+    const MinusState current_minus_state = minus_state;
     if (minus_state == MinusState::Operator) {
       minus_state = MinusState::Number;
     }
@@ -183,7 +183,7 @@
       if (pos >= m_in.size()) {
         break;
       }
-      char ch = m_in[pos];
+      const char ch = m_in[pos];
       if (!std::isalnum(ch) && ch != '.' && ch != '/' && ch != '_' && ch != '-') {
         break;
       }
@@ -197,7 +197,7 @@
       if (pos >= m_in.size()) {
         break;
       }
-      char ch = m_in[pos];
+      const char ch = m_in[pos];
       // be very permissive in lexer (we'll catch errors when conversion happens)
       if (!std::isdigit(ch) && ch != '.' && ch != 'e' && ch != 'E' && ch != '+' && ch != '-') {
         break;
@@ -213,7 +213,7 @@
       if (pos >= m_in.size()) {
         break;
       }
-      char ch = m_in[pos++];
+      const char ch = m_in[pos++];
       if (ch == '\\') {
         escape = true;
       } else if (!escape && ch == m_in[tok_start]) {
@@ -302,7 +302,7 @@
     default:
     case State::Text: {
       // fast-scan to first open character
-      size_t open_start = m_in.substr(pos).find_first_of(config.open_chars);
+      const size_t open_start = m_in.substr(pos).find_first_of(config.open_chars);
       if (open_start == nonstd::string_view::npos) {
         // didn't find open, return remaining text as text token
         pos = m_in.size();
diff --git a/include/inja/node.hpp b/include/inja/node.hpp
index e85df57..0fd9a41 100644
--- a/include/inja/node.hpp
+++ b/include/inja/node.hpp
@@ -1,4 +1,4 @@
-// Copyright (c) 2020 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_NODE_HPP_
 #define INCLUDE_INJA_NODE_HPP_
@@ -28,6 +28,8 @@
 class ForObjectStatementNode;
 class IfStatementNode;
 class IncludeStatementNode;
+class ExtendsStatementNode;
+class BlockStatementNode;
 class SetStatementNode;
 
 
@@ -48,6 +50,8 @@
   virtual void visit(const ForObjectStatementNode& node) = 0;
   virtual void visit(const IfStatementNode& node) = 0;
   virtual void visit(const IncludeStatementNode& node) = 0;
+  virtual void visit(const ExtendsStatementNode& node) = 0;
+  virtual void visit(const BlockStatementNode& node) = 0;
   virtual void visit(const SetStatementNode& node) = 0;
 };
 
@@ -331,6 +335,30 @@
   }
 };
 
+class ExtendsStatementNode : public StatementNode {
+public:
+  const std::string file;
+
+  explicit ExtendsStatementNode(const std::string& file, size_t pos) : StatementNode(pos), file(file) { }
+
+  void accept(NodeVisitor& v) const {
+    v.visit(*this);
+  };
+};
+
+class BlockStatementNode : public StatementNode {
+public:
+  const std::string name;
+  BlockNode block;
+  BlockNode *const parent;
+
+  explicit BlockStatementNode(BlockNode *const parent, const std::string& name, size_t pos) : StatementNode(pos), parent(parent), name(name) { }
+
+  void accept(NodeVisitor& v) const {
+    v.visit(*this);
+  };
+};
+
 class SetStatementNode : public StatementNode {
 public:
   const std::string key;
diff --git a/include/inja/parser.hpp b/include/inja/parser.hpp
index 3a85181..a211d31 100644
--- a/include/inja/parser.hpp
+++ b/include/inja/parser.hpp
@@ -1,4 +1,4 @@
-// Copyright (c) 2020 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_PARSER_HPP_
 #define INCLUDE_INJA_PARSER_HPP_
@@ -50,6 +50,7 @@
   std::stack<std::shared_ptr<FunctionNode>> operator_stack;
   std::stack<IfStatementNode*> if_statement_stack;
   std::stack<ForStatementNode*> for_statement_stack;
+  std::stack<BlockStatementNode*> block_statement_stack;
 
   inline void throw_parser_error(const std::string &message) {
     INJA_THROW(ParserError(message, lexer.current_position()));
@@ -87,6 +88,22 @@
     arguments.emplace_back(function);
   }
 
+  void add_to_template_storage(nonstd::string_view path, std::string& template_name) {
+    if (config.search_included_templates_in_files && template_storage.find(template_name) == template_storage.end()) {
+      // Build the relative path
+      template_name = static_cast<std::string>(path) + template_name;
+      if (template_name.compare(0, 2, "./") == 0) {
+        template_name.erase(0, 2);
+      }
+
+      if (template_storage.find(template_name) == template_storage.end()) {
+        auto include_template = Template(load_file(template_name));
+        template_storage.emplace(template_name, include_template);
+        parse_into_template(template_storage[template_name], template_name);
+      }
+    }
+  }
+
   bool parse_expression(Template &tmpl, Token::Kind closing) {
     while (tok.kind != closing && tok.kind != Token::Kind::Eof) {
       // Literals
@@ -387,6 +404,37 @@
       current_block = if_statement_data->parent;
       if_statement_stack.pop();
 
+    } else if (tok.text == static_cast<decltype(tok.text)>("block")) {
+      get_next_token();
+
+      if (tok.kind != Token::Kind::Id) {
+        throw_parser_error("expected block name, got '" + tok.describe() + "'");
+      }
+
+      const std::string block_name = static_cast<std::string>(tok.text);
+
+      auto block_statement_node = std::make_shared<BlockStatementNode>(current_block, block_name, tok.text.data() - tmpl.content.c_str());
+      current_block->nodes.emplace_back(block_statement_node);
+      block_statement_stack.emplace(block_statement_node.get());
+      current_block = &block_statement_node->block;
+      auto success = tmpl.block_storage.emplace(block_name, block_statement_node);
+      if (!success.second) {
+        throw_parser_error("block with the name '" + block_name + "' does already exist");
+      }
+
+      get_next_token();
+
+    } else if (tok.text == static_cast<decltype(tok.text)>("endblock")) {
+      if (block_statement_stack.empty()) {
+        throw_parser_error("endblock without matching block");
+      }
+
+      auto &block_statement_data = block_statement_stack.top();
+      get_next_token();
+
+      current_block = block_statement_data->parent;
+      block_statement_stack.pop();
+
     } else if (tok.text == static_cast<decltype(tok.text)>("for")) {
       get_next_token();
 
@@ -450,24 +498,26 @@
       }
 
       std::string template_name = json::parse(tok.text).get_ref<const std::string &>();
-      if (config.search_included_templates_in_files && template_storage.find(template_name) == template_storage.end()) {
-        // Build the relative path
-        template_name = static_cast<std::string>(path) + template_name;
-        if (template_name.compare(0, 2, "./") == 0) {
-          template_name.erase(0, 2);
-        }
-
-        if (template_storage.find(template_name) == template_storage.end()) {
-          auto include_template = Template(load_file(template_name));
-          template_storage.emplace(template_name, include_template);
-          parse_into_template(template_storage[template_name], template_name);
-        }
-      }
+      add_to_template_storage(path, template_name);
 
       current_block->nodes.emplace_back(std::make_shared<IncludeStatementNode>(template_name, tok.text.data() - tmpl.content.c_str()));
 
       get_next_token();
 
+    } else if (tok.text == static_cast<decltype(tok.text)>("extends")) {
+      get_next_token();
+
+      if (tok.kind != Token::Kind::String) {
+        throw_parser_error("expected string, got '" + tok.describe() + "'");
+      }
+
+      std::string template_name = json::parse(tok.text).get_ref<const std::string &>();
+      add_to_template_storage(path, template_name);
+
+      current_block->nodes.emplace_back(std::make_shared<ExtendsStatementNode>(template_name, tok.text.data() - tmpl.content.c_str()));
+
+      get_next_token();
+
     } else if (tok.text == static_cast<decltype(tok.text)>("set")) {
       get_next_token();
 
diff --git a/include/inja/renderer.hpp b/include/inja/renderer.hpp
index baece36..05c3c3b 100644
--- a/include/inja/renderer.hpp
+++ b/include/inja/renderer.hpp
@@ -1,4 +1,4 @@
-// Copyright (c) 2020 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_RENDERER_HPP_
 #define INCLUDE_INJA_RENDERER_HPP_
@@ -26,10 +26,14 @@
   using Op = FunctionStorage::Operation;
 
   const RenderConfig config;
-  const Template *current_template;
   const TemplateStorage &template_storage;
   const FunctionStorage &function_storage;
 
+  const Template *current_template;
+  size_t current_level {0};
+  std::vector<const Template*> template_stack;
+  std::vector<const BlockStatementNode*> block_statement_stack;
+
   const json *json_input;
   std::ostream *output_stream;
 
@@ -40,6 +44,8 @@
   std::stack<const json*> json_eval_stack;
   std::stack<const JsonNode*> not_found_stack;
 
+  bool break_rendering {false};
+
   bool truthy(const json* data) const {
     if (data->is_boolean()) {
       return data->get<bool>();
@@ -75,7 +81,7 @@
       throw_renderer_error("malformed expression", expression_list);
     }
 
-    auto result = json_eval_stack.top();
+    const auto result = json_eval_stack.top();
     json_eval_stack.pop();
 
     if (!result) {
@@ -116,7 +122,7 @@
       json_eval_stack.pop();
 
       if (!result[N - i - 1]) {
-        auto json_node = not_found_stack.top();
+        const auto json_node = not_found_stack.top();
         not_found_stack.pop();
 
         if (throw_not_found) {
@@ -144,7 +150,7 @@
       json_eval_stack.pop();
 
       if (!result[N - i - 1]) {
-        auto json_node = not_found_stack.top();
+        const auto json_node = not_found_stack.top();
         not_found_stack.pop();
 
         if (throw_not_found) {
@@ -158,6 +164,10 @@
   void visit(const BlockNode& node) {
     for (auto& n : node.nodes) {
       n->accept(*this);
+
+      if (break_rendering) {
+        break;
+      }
     }
   }
 
@@ -180,10 +190,10 @@
     
     } else {
       // Try to evaluate as a no-argument callback
-      auto function_data = function_storage.find_function(node.name, 0);
+      const auto function_data = function_storage.find_function(node.name, 0);
       if (function_data.operation == FunctionStorage::Operation::Callback) {
         Arguments empty_args {};
-        auto value = std::make_shared<json>(function_data.callback(empty_args));
+        const auto value = std::make_shared<json>(function_data.callback(empty_args));
         json_tmp_stack.push_back(value);
         json_eval_stack.push(value.get());
 
@@ -199,7 +209,7 @@
 
     switch (node.operation) {
     case Op::Not: {
-      auto args = get_arguments<1>(node);
+      const auto args = get_arguments<1>(node);
       result_ptr = std::make_shared<json>(!truthy(args[0]));
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
@@ -215,49 +225,49 @@
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::In: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       result_ptr = std::make_shared<json>(std::find(args[1]->begin(), args[1]->end(), *args[0]) != args[1]->end());
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Equal: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       result_ptr = std::make_shared<json>(*args[0] == *args[1]);
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::NotEqual: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       result_ptr = std::make_shared<json>(*args[0] != *args[1]);
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Greater: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       result_ptr = std::make_shared<json>(*args[0] > *args[1]);
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::GreaterEqual: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       result_ptr = std::make_shared<json>(*args[0] >= *args[1]);
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Less: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       result_ptr = std::make_shared<json>(*args[0] < *args[1]);
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::LessEqual: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       result_ptr = std::make_shared<json>(*args[0] <= *args[1]);
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Add: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       if (args[0]->is_string() && args[1]->is_string()) {
         result_ptr = std::make_shared<json>(args[0]->get_ref<const std::string&>() + args[1]->get_ref<const std::string&>());
         json_tmp_stack.push_back(result_ptr);
@@ -271,7 +281,7 @@
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Subtract: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       if (args[0]->is_number_integer() && args[1]->is_number_integer()) {
         result_ptr = std::make_shared<json>(args[0]->get<int>() - args[1]->get<int>());
         json_tmp_stack.push_back(result_ptr);
@@ -282,7 +292,7 @@
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Multiplication: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       if (args[0]->is_number_integer() && args[1]->is_number_integer()) {
         result_ptr = std::make_shared<json>(args[0]->get<int>() * args[1]->get<int>());
         json_tmp_stack.push_back(result_ptr);
@@ -293,7 +303,7 @@
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Division: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       if (args[1]->get<double>() == 0) {
         throw_renderer_error("division by zero", node);
       }
@@ -302,7 +312,7 @@
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Power: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       if (args[0]->is_number_integer() && args[1]->get<int>() >= 0) {
         int result = static_cast<int>(std::pow(args[0]->get<int>(), args[1]->get<int>()));
         result_ptr = std::make_shared<json>(std::move(result));
@@ -315,33 +325,33 @@
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Modulo: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       result_ptr = std::make_shared<json>(args[0]->get<int>() % args[1]->get<int>());
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::AtId: {
-      auto container = get_arguments<1, 0, false>(node)[0];
+      const auto container = get_arguments<1, 0, false>(node)[0];
       node.arguments[1]->accept(*this);
       if (not_found_stack.empty()) {
         throw_renderer_error("could not find element with given name", node);
       }
-      auto id_node = not_found_stack.top();
+      const auto id_node = not_found_stack.top();
       not_found_stack.pop();
       json_eval_stack.pop();
       json_eval_stack.push(&container->at(id_node->name));
     } break;
     case Op::At: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       json_eval_stack.push(&args[0]->at(args[1]->get<int>()));
     } break;
     case Op::Default: {
-      auto test_arg = get_arguments<1, 0, false>(node)[0];
+      const auto test_arg = get_arguments<1, 0, false>(node)[0];
       json_eval_stack.push(test_arg ? test_arg : get_arguments<1, 1>(node)[0]);
     } break;
     case Op::DivisibleBy: {
-      auto args = get_arguments<2>(node);
-      int divisor = args[1]->get<int>();
+      const auto args = get_arguments<2>(node);
+      const int divisor = args[1]->get<int>();
       result_ptr = std::make_shared<json>((divisor != 0) && (args[0]->get<int>() % divisor == 0));
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
@@ -358,14 +368,14 @@
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::ExistsInObject: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       auto &&name = args[1]->get_ref<const std::string &>();
       result_ptr = std::make_shared<json>(args[0]->find(name) != args[0]->end());
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::First: {
-      auto result = &get_arguments<1>(node)[0]->front();
+      const auto result = &get_arguments<1>(node)[0]->front();
       json_eval_stack.push(result);
     } break;
     case Op::Float: {
@@ -379,11 +389,11 @@
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Last: {
-      auto result = &get_arguments<1>(node)[0]->back();
+      const auto result = &get_arguments<1>(node)[0]->back();
       json_eval_stack.push(result);
     } break;
     case Op::Length: {
-      auto val = get_arguments<1>(node)[0];
+      const auto val = get_arguments<1>(node)[0];
       if (val->is_string()) {
         result_ptr = std::make_shared<json>(val->get_ref<const std::string &>().length());
       } else {
@@ -400,13 +410,13 @@
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Max: {
-      auto args = get_arguments<1>(node);
-      auto result = std::max_element(args[0]->begin(), args[0]->end());
+      const auto args = get_arguments<1>(node);
+      const auto result = std::max_element(args[0]->begin(), args[0]->end());
       json_eval_stack.push(&(*result));
     } break;
     case Op::Min: {
-      auto args = get_arguments<1>(node);
-      auto result = std::min_element(args[0]->begin(), args[0]->end());
+      const auto args = get_arguments<1>(node);
+      const auto result = std::min_element(args[0]->begin(), args[0]->end());
       json_eval_stack.push(&(*result));
     } break;
     case Op::Odd: {
@@ -422,9 +432,9 @@
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Round: {
-      auto args = get_arguments<2>(node);
-      int precision = args[1]->get<int>();
-      double result = std::round(args[0]->get<double>() * std::pow(10.0, precision)) / std::pow(10.0, precision);
+      const auto args = get_arguments<2>(node);
+      const int precision = args[1]->get<int>();
+      const double result = std::round(args[0]->get<double>() * std::pow(10.0, precision)) / std::pow(10.0, precision);
       result_ptr = std::make_shared<json>(std::move(result));
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
@@ -483,6 +493,37 @@
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
+    case Op::Super: {
+      const auto args = get_argument_vector(node);
+      const size_t old_level = current_level;
+      const size_t level_diff = (args.size() == 1) ? args[0]->get<int>() : 1;
+      const size_t level = current_level + level_diff;
+
+      if (block_statement_stack.empty()) {
+        throw_renderer_error("super() call is not within a block", node);
+      }
+
+      if (level < 1 || level > template_stack.size() - 1) {
+        throw_renderer_error("level of super() call does not match parent templates (between 1 and " + std::to_string(template_stack.size() - 1) + ")", node);
+      }
+
+      const auto current_block_statement = block_statement_stack.back();
+      const Template *new_template = template_stack.at(level);
+      const Template *old_template = current_template;
+      const auto block_it = new_template->block_storage.find(current_block_statement->name);
+      if (block_it != new_template->block_storage.end()) {
+        current_template = new_template;
+        current_level = level;
+        block_it->second->block.accept(*this);
+        current_level = old_level;
+        current_template = old_template;
+      } else {
+        throw_renderer_error("could not find block with name '" + current_block_statement->name + "'", node);
+      }
+      result_ptr = std::make_shared<json>(nullptr);
+      json_tmp_stack.push_back(result_ptr);
+      json_eval_stack.push(result_ptr.get());
+    } break;
     case Op::ParenLeft:
     case Op::ParenRight:
     case Op::None:
@@ -499,7 +540,7 @@
   void visit(const ForStatementNode&) { }
 
   void visit(const ForArrayStatementNode& node) {
-    auto result = eval_expression_list(node.condition);
+    const auto result = eval_expression_list(node.condition);
     if (!result->is_array()) {
       throw_renderer_error("object must be an array", node);
     }
@@ -530,7 +571,7 @@
 
     json_additional_data[static_cast<std::string>(node.value)].clear();
     if (!(*current_loop_data)["parent"].empty()) {
-      auto tmp = (*current_loop_data)["parent"];
+      const auto tmp = (*current_loop_data)["parent"];
       *current_loop_data = std::move(tmp);
     } else {
       current_loop_data = &json_additional_data["loop"];
@@ -538,7 +579,7 @@
   }
 
   void visit(const ForObjectStatementNode& node) {
-    auto result = eval_expression_list(node.condition);
+    const auto result = eval_expression_list(node.condition);
     if (!result->is_object()) {
       throw_renderer_error("object must be an object", node);
     }
@@ -577,7 +618,7 @@
   }
 
   void visit(const IfStatementNode& node) {
-    auto result = eval_expression_list(node.condition);
+    const auto result = eval_expression_list(node.condition);
     if (truthy(result.get())) {
       node.true_statement.accept(*this);
     } else if (node.has_false_statement) {
@@ -587,8 +628,7 @@
 
   void visit(const IncludeStatementNode& node) {
     auto sub_renderer = Renderer(config, template_storage, function_storage);
-    auto included_template_it = template_storage.find(node.file);
-
+    const auto included_template_it = template_storage.find(node.file);
     if (included_template_it != template_storage.end()) {
       sub_renderer.render_to(*output_stream, included_template_it->second, *json_input, &json_additional_data);
     } else if (config.throw_at_missing_includes) {
@@ -596,6 +636,31 @@
     }
   }
 
+  void visit(const ExtendsStatementNode& node) {
+    const auto included_template_it = template_storage.find(node.file);
+    if (included_template_it != template_storage.end()) {
+      const Template *parent_template = &included_template_it->second;
+      render_to(*output_stream, *parent_template, *json_input, &json_additional_data);
+      break_rendering = true;
+    } else if (config.throw_at_missing_includes) {
+      throw_renderer_error("extends '" + node.file + "' not found", node);
+    }
+  }
+
+  void visit(const BlockStatementNode& node) {
+    const size_t old_level = current_level;
+    current_level = 0;
+    current_template = template_stack.front();
+    const auto block_it = current_template->block_storage.find(node.name);
+    if (block_it != current_template->block_storage.end()) {
+      block_statement_stack.emplace_back(&node);
+      block_it->second->block.accept(*this);
+      block_statement_stack.pop_back(); 
+    }
+    current_level = old_level;
+    current_template = template_stack.back();
+  }
+
   void visit(const SetStatementNode& node) {
     json_additional_data[node.key] = *eval_expression_list(node.expression);
   }
@@ -613,6 +678,7 @@
       current_loop_data = &json_additional_data["loop"];
     }
 
+    template_stack.emplace_back(current_template);
     current_template->root.accept(*this);
 
     json_tmp_stack.clear();
diff --git a/include/inja/statistics.hpp b/include/inja/statistics.hpp
index 045d1a5..48d129a 100644
--- a/include/inja/statistics.hpp
+++ b/include/inja/statistics.hpp
@@ -1,4 +1,4 @@
-// Copyright (c) 2019 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_STATISTICS_HPP_
 #define INCLUDE_INJA_STATISTICS_HPP_
@@ -57,6 +57,12 @@
 
   void visit(const IncludeStatementNode&) { }
 
+  void visit(const ExtendsStatementNode&) { }
+
+  void visit(const BlockStatementNode& node) {
+    node.block.accept(*this);
+  }
+
   void visit(const SetStatementNode&) { }
 
 public:
diff --git a/include/inja/template.hpp b/include/inja/template.hpp
index 9de0a96..cc32bfe 100644
--- a/include/inja/template.hpp
+++ b/include/inja/template.hpp
@@ -1,4 +1,4 @@
-// Copyright (c) 2019 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_TEMPLATE_HPP_
 #define INCLUDE_INJA_TEMPLATE_HPP_
@@ -20,6 +20,7 @@
 struct Template {
   BlockNode root;
   std::string content;
+  std::map<std::string, std::shared_ptr<BlockStatementNode>> block_storage;
 
   explicit Template() { }
   explicit Template(const std::string& content): content(content) { }
diff --git a/include/inja/token.hpp b/include/inja/token.hpp
index c000138..3fb4c23 100644
--- a/include/inja/token.hpp
+++ b/include/inja/token.hpp
@@ -1,4 +1,4 @@
-// Copyright (c) 2020 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_TOKEN_HPP_
 #define INCLUDE_INJA_TOKEN_HPP_
diff --git a/include/inja/utils.hpp b/include/inja/utils.hpp
index 836425d..8750759 100644
--- a/include/inja/utils.hpp
+++ b/include/inja/utils.hpp
@@ -1,4 +1,4 @@
-// Copyright (c) 2020 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_UTILS_HPP_
 #define INCLUDE_INJA_UTILS_HPP_
diff --git a/single_include/inja/inja.hpp b/single_include/inja/inja.hpp
index c767ef7..a91f505 100644
--- a/single_include/inja/inja.hpp
+++ b/single_include/inja/inja.hpp
@@ -1,4 +1,4 @@
-// Copyright (c) 2020 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_INJA_HPP_
 #define INCLUDE_INJA_INJA_HPP_
@@ -16,7 +16,7 @@
 #endif
 
 // #include "environment.hpp"
-// Copyright (c) 2019 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_ENVIRONMENT_HPP_
 #define INCLUDE_INJA_ENVIRONMENT_HPP_
@@ -30,7 +30,7 @@
 #include <nlohmann/json.hpp>
 
 // #include "config.hpp"
-// Copyright (c) 2019 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_CONFIG_HPP_
 #define INCLUDE_INJA_CONFIG_HPP_
@@ -1525,7 +1525,7 @@
 #endif // INCLUDE_INJA_CONFIG_HPP_
 
 // #include "function_storage.hpp"
-// Copyright (c) 2020 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_FUNCTION_STORAGE_HPP_
 #define INCLUDE_INJA_FUNCTION_STORAGE_HPP_
@@ -1592,6 +1592,7 @@
     Round,
     Sort,
     Upper,
+    Super,
     Callback,
     ParenLeft,
     ParenRight,
@@ -1634,6 +1635,8 @@
     {std::make_pair("round", 2), FunctionData { Operation::Round }},
     {std::make_pair("sort", 1), FunctionData { Operation::Sort }},
     {std::make_pair("upper", 1), FunctionData { Operation::Upper }},
+    {std::make_pair("super", 0), FunctionData { Operation::Super }},
+    {std::make_pair("super", 1), FunctionData { Operation::Super }},
   };
 
 public:
@@ -1667,7 +1670,7 @@
 #endif // INCLUDE_INJA_FUNCTION_STORAGE_HPP_
 
 // #include "parser.hpp"
-// Copyright (c) 2020 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_PARSER_HPP_
 #define INCLUDE_INJA_PARSER_HPP_
@@ -1682,7 +1685,7 @@
 // #include "config.hpp"
 
 // #include "exceptions.hpp"
-// Copyright (c) 2020 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_EXCEPTIONS_HPP_
 #define INCLUDE_INJA_EXCEPTIONS_HPP_
@@ -1747,7 +1750,7 @@
 // #include "config.hpp"
 
 // #include "token.hpp"
-// Copyright (c) 2020 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_TOKEN_HPP_
 #define INCLUDE_INJA_TOKEN_HPP_
@@ -1826,7 +1829,7 @@
 #endif // INCLUDE_INJA_TOKEN_HPP_
 
 // #include "utils.hpp"
-// Copyright (c) 2020 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_UTILS_HPP_
 #define INCLUDE_INJA_UTILS_HPP_
@@ -1945,7 +1948,7 @@
     if (tok_start >= m_in.size()) {
       return make_token(Token::Kind::Eof);
     }
-    char ch = m_in[tok_start];
+    const char ch = m_in[tok_start];
     if (ch == ' ' || ch == '\t' || ch == '\r') {
       tok_start += 1;
       goto again;
@@ -1955,7 +1958,7 @@
     if (!close_trim.empty() && inja::string_view::starts_with(m_in.substr(tok_start), close_trim)) {
       state = State::Text;
       pos = tok_start + close_trim.size();
-      Token tok = make_token(closeKind);
+      const Token tok = make_token(closeKind);
       skip_whitespaces_and_newlines();
       return tok;
     }
@@ -1963,7 +1966,7 @@
     if (inja::string_view::starts_with(m_in.substr(tok_start), close)) {
       state = State::Text;
       pos = tok_start + close.size();
-      Token tok = make_token(closeKind);
+      const Token tok = make_token(closeKind);
       if (trim) {
         skip_whitespaces_and_first_newline();
       }
@@ -1982,7 +1985,7 @@
       return scan_id();
     }
 
-    MinusState current_minus_state = minus_state;
+    const MinusState current_minus_state = minus_state;
     if (minus_state == MinusState::Operator) {
       minus_state = MinusState::Number;
     }
@@ -2077,7 +2080,7 @@
       if (pos >= m_in.size()) {
         break;
       }
-      char ch = m_in[pos];
+      const char ch = m_in[pos];
       if (!std::isalnum(ch) && ch != '.' && ch != '/' && ch != '_' && ch != '-') {
         break;
       }
@@ -2091,7 +2094,7 @@
       if (pos >= m_in.size()) {
         break;
       }
-      char ch = m_in[pos];
+      const char ch = m_in[pos];
       // be very permissive in lexer (we'll catch errors when conversion happens)
       if (!std::isdigit(ch) && ch != '.' && ch != 'e' && ch != 'E' && ch != '+' && ch != '-') {
         break;
@@ -2107,7 +2110,7 @@
       if (pos >= m_in.size()) {
         break;
       }
-      char ch = m_in[pos++];
+      const char ch = m_in[pos++];
       if (ch == '\\') {
         escape = true;
       } else if (!escape && ch == m_in[tok_start]) {
@@ -2196,7 +2199,7 @@
     default:
     case State::Text: {
       // fast-scan to first open character
-      size_t open_start = m_in.substr(pos).find_first_of(config.open_chars);
+      const size_t open_start = m_in.substr(pos).find_first_of(config.open_chars);
       if (open_start == nonstd::string_view::npos) {
         // didn't find open, return remaining text as text token
         pos = m_in.size();
@@ -2314,7 +2317,7 @@
 #endif // INCLUDE_INJA_LEXER_HPP_
 
 // #include "node.hpp"
-// Copyright (c) 2020 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_NODE_HPP_
 #define INCLUDE_INJA_NODE_HPP_
@@ -2346,6 +2349,8 @@
 class ForObjectStatementNode;
 class IfStatementNode;
 class IncludeStatementNode;
+class ExtendsStatementNode;
+class BlockStatementNode;
 class SetStatementNode;
 
 
@@ -2366,6 +2371,8 @@
   virtual void visit(const ForObjectStatementNode& node) = 0;
   virtual void visit(const IfStatementNode& node) = 0;
   virtual void visit(const IncludeStatementNode& node) = 0;
+  virtual void visit(const ExtendsStatementNode& node) = 0;
+  virtual void visit(const BlockStatementNode& node) = 0;
   virtual void visit(const SetStatementNode& node) = 0;
 };
 
@@ -2649,6 +2656,30 @@
   }
 };
 
+class ExtendsStatementNode : public StatementNode {
+public:
+  const std::string file;
+
+  explicit ExtendsStatementNode(const std::string& file, size_t pos) : StatementNode(pos), file(file) { }
+
+  void accept(NodeVisitor& v) const {
+    v.visit(*this);
+  };
+};
+
+class BlockStatementNode : public StatementNode {
+public:
+  const std::string name;
+  BlockNode block;
+  BlockNode *const parent;
+
+  explicit BlockStatementNode(BlockNode *const parent, const std::string& name, size_t pos) : StatementNode(pos), parent(parent), name(name) { }
+
+  void accept(NodeVisitor& v) const {
+    v.visit(*this);
+  };
+};
+
 class SetStatementNode : public StatementNode {
 public:
   const std::string key;
@@ -2666,7 +2697,7 @@
 #endif // INCLUDE_INJA_NODE_HPP_
 
 // #include "template.hpp"
-// Copyright (c) 2019 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_TEMPLATE_HPP_
 #define INCLUDE_INJA_TEMPLATE_HPP_
@@ -2679,7 +2710,7 @@
 // #include "node.hpp"
 
 // #include "statistics.hpp"
-// Copyright (c) 2019 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_STATISTICS_HPP_
 #define INCLUDE_INJA_STATISTICS_HPP_
@@ -2739,6 +2770,12 @@
 
   void visit(const IncludeStatementNode&) { }
 
+  void visit(const ExtendsStatementNode&) { }
+
+  void visit(const BlockStatementNode& node) {
+    node.block.accept(*this);
+  }
+
   void visit(const SetStatementNode&) { }
 
 public:
@@ -2761,6 +2798,7 @@
 struct Template {
   BlockNode root;
   std::string content;
+  std::map<std::string, std::shared_ptr<BlockStatementNode>> block_storage;
 
   explicit Template() { }
   explicit Template(const std::string& content): content(content) { }
@@ -2815,6 +2853,7 @@
   std::stack<std::shared_ptr<FunctionNode>> operator_stack;
   std::stack<IfStatementNode*> if_statement_stack;
   std::stack<ForStatementNode*> for_statement_stack;
+  std::stack<BlockStatementNode*> block_statement_stack;
 
   inline void throw_parser_error(const std::string &message) {
     INJA_THROW(ParserError(message, lexer.current_position()));
@@ -2852,6 +2891,22 @@
     arguments.emplace_back(function);
   }
 
+  void add_to_template_storage(nonstd::string_view path, std::string& template_name) {
+    if (config.search_included_templates_in_files && template_storage.find(template_name) == template_storage.end()) {
+      // Build the relative path
+      template_name = static_cast<std::string>(path) + template_name;
+      if (template_name.compare(0, 2, "./") == 0) {
+        template_name.erase(0, 2);
+      }
+
+      if (template_storage.find(template_name) == template_storage.end()) {
+        auto include_template = Template(load_file(template_name));
+        template_storage.emplace(template_name, include_template);
+        parse_into_template(template_storage[template_name], template_name);
+      }
+    }
+  }
+
   bool parse_expression(Template &tmpl, Token::Kind closing) {
     while (tok.kind != closing && tok.kind != Token::Kind::Eof) {
       // Literals
@@ -3152,6 +3207,37 @@
       current_block = if_statement_data->parent;
       if_statement_stack.pop();
 
+    } else if (tok.text == static_cast<decltype(tok.text)>("block")) {
+      get_next_token();
+
+      if (tok.kind != Token::Kind::Id) {
+        throw_parser_error("expected block name, got '" + tok.describe() + "'");
+      }
+
+      const std::string block_name = static_cast<std::string>(tok.text);
+
+      auto block_statement_node = std::make_shared<BlockStatementNode>(current_block, block_name, tok.text.data() - tmpl.content.c_str());
+      current_block->nodes.emplace_back(block_statement_node);
+      block_statement_stack.emplace(block_statement_node.get());
+      current_block = &block_statement_node->block;
+      auto success = tmpl.block_storage.emplace(block_name, block_statement_node);
+      if (!success.second) {
+        throw_parser_error("block with the name '" + block_name + "' does already exist");
+      }
+
+      get_next_token();
+
+    } else if (tok.text == static_cast<decltype(tok.text)>("endblock")) {
+      if (block_statement_stack.empty()) {
+        throw_parser_error("endblock without matching block");
+      }
+
+      auto &block_statement_data = block_statement_stack.top();
+      get_next_token();
+
+      current_block = block_statement_data->parent;
+      block_statement_stack.pop();
+
     } else if (tok.text == static_cast<decltype(tok.text)>("for")) {
       get_next_token();
 
@@ -3215,24 +3301,26 @@
       }
 
       std::string template_name = json::parse(tok.text).get_ref<const std::string &>();
-      if (config.search_included_templates_in_files && template_storage.find(template_name) == template_storage.end()) {
-        // Build the relative path
-        template_name = static_cast<std::string>(path) + template_name;
-        if (template_name.compare(0, 2, "./") == 0) {
-          template_name.erase(0, 2);
-        }
-
-        if (template_storage.find(template_name) == template_storage.end()) {
-          auto include_template = Template(load_file(template_name));
-          template_storage.emplace(template_name, include_template);
-          parse_into_template(template_storage[template_name], template_name);
-        }
-      }
+      add_to_template_storage(path, template_name);
 
       current_block->nodes.emplace_back(std::make_shared<IncludeStatementNode>(template_name, tok.text.data() - tmpl.content.c_str()));
 
       get_next_token();
 
+    } else if (tok.text == static_cast<decltype(tok.text)>("extends")) {
+      get_next_token();
+
+      if (tok.kind != Token::Kind::String) {
+        throw_parser_error("expected string, got '" + tok.describe() + "'");
+      }
+
+      std::string template_name = json::parse(tok.text).get_ref<const std::string &>();
+      add_to_template_storage(path, template_name);
+
+      current_block->nodes.emplace_back(std::make_shared<ExtendsStatementNode>(template_name, tok.text.data() - tmpl.content.c_str()));
+
+      get_next_token();
+
     } else if (tok.text == static_cast<decltype(tok.text)>("set")) {
       get_next_token();
 
@@ -3363,7 +3451,7 @@
 #endif // INCLUDE_INJA_PARSER_HPP_
 
 // #include "renderer.hpp"
-// Copyright (c) 2020 Pantor. All rights reserved.
+// Copyright (c) 2021 Pantor. All rights reserved.
 
 #ifndef INCLUDE_INJA_RENDERER_HPP_
 #define INCLUDE_INJA_RENDERER_HPP_
@@ -3396,10 +3484,14 @@
   using Op = FunctionStorage::Operation;
 
   const RenderConfig config;
-  const Template *current_template;
   const TemplateStorage &template_storage;
   const FunctionStorage &function_storage;
 
+  const Template *current_template;
+  size_t current_level {0};
+  std::vector<const Template*> template_stack;
+  std::vector<const BlockStatementNode*> block_statement_stack;
+
   const json *json_input;
   std::ostream *output_stream;
 
@@ -3410,6 +3502,8 @@
   std::stack<const json*> json_eval_stack;
   std::stack<const JsonNode*> not_found_stack;
 
+  bool break_rendering {false};
+
   bool truthy(const json* data) const {
     if (data->is_boolean()) {
       return data->get<bool>();
@@ -3445,7 +3539,7 @@
       throw_renderer_error("malformed expression", expression_list);
     }
 
-    auto result = json_eval_stack.top();
+    const auto result = json_eval_stack.top();
     json_eval_stack.pop();
 
     if (!result) {
@@ -3486,7 +3580,7 @@
       json_eval_stack.pop();
 
       if (!result[N - i - 1]) {
-        auto json_node = not_found_stack.top();
+        const auto json_node = not_found_stack.top();
         not_found_stack.pop();
 
         if (throw_not_found) {
@@ -3514,7 +3608,7 @@
       json_eval_stack.pop();
 
       if (!result[N - i - 1]) {
-        auto json_node = not_found_stack.top();
+        const auto json_node = not_found_stack.top();
         not_found_stack.pop();
 
         if (throw_not_found) {
@@ -3528,6 +3622,10 @@
   void visit(const BlockNode& node) {
     for (auto& n : node.nodes) {
       n->accept(*this);
+
+      if (break_rendering) {
+        break;
+      }
     }
   }
 
@@ -3550,10 +3648,10 @@
     
     } else {
       // Try to evaluate as a no-argument callback
-      auto function_data = function_storage.find_function(node.name, 0);
+      const auto function_data = function_storage.find_function(node.name, 0);
       if (function_data.operation == FunctionStorage::Operation::Callback) {
         Arguments empty_args {};
-        auto value = std::make_shared<json>(function_data.callback(empty_args));
+        const auto value = std::make_shared<json>(function_data.callback(empty_args));
         json_tmp_stack.push_back(value);
         json_eval_stack.push(value.get());
 
@@ -3569,7 +3667,7 @@
 
     switch (node.operation) {
     case Op::Not: {
-      auto args = get_arguments<1>(node);
+      const auto args = get_arguments<1>(node);
       result_ptr = std::make_shared<json>(!truthy(args[0]));
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
@@ -3585,49 +3683,49 @@
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::In: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       result_ptr = std::make_shared<json>(std::find(args[1]->begin(), args[1]->end(), *args[0]) != args[1]->end());
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Equal: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       result_ptr = std::make_shared<json>(*args[0] == *args[1]);
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::NotEqual: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       result_ptr = std::make_shared<json>(*args[0] != *args[1]);
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Greater: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       result_ptr = std::make_shared<json>(*args[0] > *args[1]);
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::GreaterEqual: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       result_ptr = std::make_shared<json>(*args[0] >= *args[1]);
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Less: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       result_ptr = std::make_shared<json>(*args[0] < *args[1]);
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::LessEqual: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       result_ptr = std::make_shared<json>(*args[0] <= *args[1]);
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Add: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       if (args[0]->is_string() && args[1]->is_string()) {
         result_ptr = std::make_shared<json>(args[0]->get_ref<const std::string&>() + args[1]->get_ref<const std::string&>());
         json_tmp_stack.push_back(result_ptr);
@@ -3641,7 +3739,7 @@
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Subtract: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       if (args[0]->is_number_integer() && args[1]->is_number_integer()) {
         result_ptr = std::make_shared<json>(args[0]->get<int>() - args[1]->get<int>());
         json_tmp_stack.push_back(result_ptr);
@@ -3652,7 +3750,7 @@
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Multiplication: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       if (args[0]->is_number_integer() && args[1]->is_number_integer()) {
         result_ptr = std::make_shared<json>(args[0]->get<int>() * args[1]->get<int>());
         json_tmp_stack.push_back(result_ptr);
@@ -3663,7 +3761,7 @@
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Division: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       if (args[1]->get<double>() == 0) {
         throw_renderer_error("division by zero", node);
       }
@@ -3672,7 +3770,7 @@
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Power: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       if (args[0]->is_number_integer() && args[1]->get<int>() >= 0) {
         int result = static_cast<int>(std::pow(args[0]->get<int>(), args[1]->get<int>()));
         result_ptr = std::make_shared<json>(std::move(result));
@@ -3685,33 +3783,33 @@
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Modulo: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       result_ptr = std::make_shared<json>(args[0]->get<int>() % args[1]->get<int>());
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::AtId: {
-      auto container = get_arguments<1, 0, false>(node)[0];
+      const auto container = get_arguments<1, 0, false>(node)[0];
       node.arguments[1]->accept(*this);
       if (not_found_stack.empty()) {
         throw_renderer_error("could not find element with given name", node);
       }
-      auto id_node = not_found_stack.top();
+      const auto id_node = not_found_stack.top();
       not_found_stack.pop();
       json_eval_stack.pop();
       json_eval_stack.push(&container->at(id_node->name));
     } break;
     case Op::At: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       json_eval_stack.push(&args[0]->at(args[1]->get<int>()));
     } break;
     case Op::Default: {
-      auto test_arg = get_arguments<1, 0, false>(node)[0];
+      const auto test_arg = get_arguments<1, 0, false>(node)[0];
       json_eval_stack.push(test_arg ? test_arg : get_arguments<1, 1>(node)[0]);
     } break;
     case Op::DivisibleBy: {
-      auto args = get_arguments<2>(node);
-      int divisor = args[1]->get<int>();
+      const auto args = get_arguments<2>(node);
+      const int divisor = args[1]->get<int>();
       result_ptr = std::make_shared<json>((divisor != 0) && (args[0]->get<int>() % divisor == 0));
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
@@ -3728,14 +3826,14 @@
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::ExistsInObject: {
-      auto args = get_arguments<2>(node);
+      const auto args = get_arguments<2>(node);
       auto &&name = args[1]->get_ref<const std::string &>();
       result_ptr = std::make_shared<json>(args[0]->find(name) != args[0]->end());
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::First: {
-      auto result = &get_arguments<1>(node)[0]->front();
+      const auto result = &get_arguments<1>(node)[0]->front();
       json_eval_stack.push(result);
     } break;
     case Op::Float: {
@@ -3749,11 +3847,11 @@
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Last: {
-      auto result = &get_arguments<1>(node)[0]->back();
+      const auto result = &get_arguments<1>(node)[0]->back();
       json_eval_stack.push(result);
     } break;
     case Op::Length: {
-      auto val = get_arguments<1>(node)[0];
+      const auto val = get_arguments<1>(node)[0];
       if (val->is_string()) {
         result_ptr = std::make_shared<json>(val->get_ref<const std::string &>().length());
       } else {
@@ -3770,13 +3868,13 @@
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Max: {
-      auto args = get_arguments<1>(node);
-      auto result = std::max_element(args[0]->begin(), args[0]->end());
+      const auto args = get_arguments<1>(node);
+      const auto result = std::max_element(args[0]->begin(), args[0]->end());
       json_eval_stack.push(&(*result));
     } break;
     case Op::Min: {
-      auto args = get_arguments<1>(node);
-      auto result = std::min_element(args[0]->begin(), args[0]->end());
+      const auto args = get_arguments<1>(node);
+      const auto result = std::min_element(args[0]->begin(), args[0]->end());
       json_eval_stack.push(&(*result));
     } break;
     case Op::Odd: {
@@ -3792,9 +3890,9 @@
       json_eval_stack.push(result_ptr.get());
     } break;
     case Op::Round: {
-      auto args = get_arguments<2>(node);
-      int precision = args[1]->get<int>();
-      double result = std::round(args[0]->get<double>() * std::pow(10.0, precision)) / std::pow(10.0, precision);
+      const auto args = get_arguments<2>(node);
+      const int precision = args[1]->get<int>();
+      const double result = std::round(args[0]->get<double>() * std::pow(10.0, precision)) / std::pow(10.0, precision);
       result_ptr = std::make_shared<json>(std::move(result));
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
@@ -3853,6 +3951,37 @@
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
+    case Op::Super: {
+      const auto args = get_argument_vector(node);
+      const size_t old_level = current_level;
+      const size_t level_diff = (args.size() == 1) ? args[0]->get<int>() : 1;
+      const size_t level = current_level + level_diff;
+
+      if (block_statement_stack.empty()) {
+        throw_renderer_error("super() call is not within a block", node);
+      }
+
+      if (level < 1 || level > template_stack.size() - 1) {
+        throw_renderer_error("level of super() call does not match parent templates (between 1 and " + std::to_string(template_stack.size() - 1) + ")", node);
+      }
+
+      const auto current_block_statement = block_statement_stack.back();
+      const Template *new_template = template_stack.at(level);
+      const Template *old_template = current_template;
+      const auto block_it = new_template->block_storage.find(current_block_statement->name);
+      if (block_it != new_template->block_storage.end()) {
+        current_template = new_template;
+        current_level = level;
+        block_it->second->block.accept(*this);
+        current_level = old_level;
+        current_template = old_template;
+      } else {
+        throw_renderer_error("could not find block with name '" + current_block_statement->name + "'", node);
+      }
+      result_ptr = std::make_shared<json>(nullptr);
+      json_tmp_stack.push_back(result_ptr);
+      json_eval_stack.push(result_ptr.get());
+    } break;
     case Op::ParenLeft:
     case Op::ParenRight:
     case Op::None:
@@ -3869,7 +3998,7 @@
   void visit(const ForStatementNode&) { }
 
   void visit(const ForArrayStatementNode& node) {
-    auto result = eval_expression_list(node.condition);
+    const auto result = eval_expression_list(node.condition);
     if (!result->is_array()) {
       throw_renderer_error("object must be an array", node);
     }
@@ -3900,7 +4029,7 @@
 
     json_additional_data[static_cast<std::string>(node.value)].clear();
     if (!(*current_loop_data)["parent"].empty()) {
-      auto tmp = (*current_loop_data)["parent"];
+      const auto tmp = (*current_loop_data)["parent"];
       *current_loop_data = std::move(tmp);
     } else {
       current_loop_data = &json_additional_data["loop"];
@@ -3908,7 +4037,7 @@
   }
 
   void visit(const ForObjectStatementNode& node) {
-    auto result = eval_expression_list(node.condition);
+    const auto result = eval_expression_list(node.condition);
     if (!result->is_object()) {
       throw_renderer_error("object must be an object", node);
     }
@@ -3947,7 +4076,7 @@
   }
 
   void visit(const IfStatementNode& node) {
-    auto result = eval_expression_list(node.condition);
+    const auto result = eval_expression_list(node.condition);
     if (truthy(result.get())) {
       node.true_statement.accept(*this);
     } else if (node.has_false_statement) {
@@ -3957,8 +4086,7 @@
 
   void visit(const IncludeStatementNode& node) {
     auto sub_renderer = Renderer(config, template_storage, function_storage);
-    auto included_template_it = template_storage.find(node.file);
-
+    const auto included_template_it = template_storage.find(node.file);
     if (included_template_it != template_storage.end()) {
       sub_renderer.render_to(*output_stream, included_template_it->second, *json_input, &json_additional_data);
     } else if (config.throw_at_missing_includes) {
@@ -3966,6 +4094,31 @@
     }
   }
 
+  void visit(const ExtendsStatementNode& node) {
+    const auto included_template_it = template_storage.find(node.file);
+    if (included_template_it != template_storage.end()) {
+      const Template *parent_template = &included_template_it->second;
+      render_to(*output_stream, *parent_template, *json_input, &json_additional_data);
+      break_rendering = true;
+    } else if (config.throw_at_missing_includes) {
+      throw_renderer_error("extends '" + node.file + "' not found", node);
+    }
+  }
+
+  void visit(const BlockStatementNode& node) {
+    const size_t old_level = current_level;
+    current_level = 0;
+    current_template = template_stack.front();
+    const auto block_it = current_template->block_storage.find(node.name);
+    if (block_it != current_template->block_storage.end()) {
+      block_statement_stack.emplace_back(&node);
+      block_it->second->block.accept(*this);
+      block_statement_stack.pop_back(); 
+    }
+    current_level = old_level;
+    current_template = template_stack.back();
+  }
+
   void visit(const SetStatementNode& node) {
     json_additional_data[node.key] = *eval_expression_list(node.expression);
   }
@@ -3983,6 +4136,7 @@
       current_loop_data = &json_additional_data["loop"];
     }
 
+    template_stack.emplace_back(current_template);
     current_template->root.accept(*this);
 
     json_tmp_stack.clear();
diff --git a/test/data/html-extend/base.txt b/test/data/html-extend/base.txt
new file mode 100644
index 0000000..e301c7a
--- /dev/null
+++ b/test/data/html-extend/base.txt
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    {% block head -%}
+    <title>{% block title %}Default - {% endblock %}</title>
+    {%- endblock -%}
+</head>
+<body>
+    {% block body %}ignored{% endblock %}
+</body>
+</html>
diff --git a/test/data/html-extend/data.json b/test/data/html-extend/data.json
new file mode 100644
index 0000000..c5726a5
--- /dev/null
+++ b/test/data/html-extend/data.json
@@ -0,0 +1,12 @@
+{
+    "author": "Pantor",
+    "date": "23/12/2018",
+    "tags": [
+        "test",
+        "templates"
+    ],
+    "views": 123,
+    "title": "Inja works.",
+    "content": "Inja is the best and fastest template engine for C++. Period.",
+    "footer-text": "This is the footer."
+}
diff --git a/test/data/html-extend/inter.txt b/test/data/html-extend/inter.txt
new file mode 100644
index 0000000..d20d97b
--- /dev/null
+++ b/test/data/html-extend/inter.txt
@@ -0,0 +1,3 @@
+{% extends "base.txt" %}
+{% block title %}Inter {{ author }}{% endblock %}
+{% block body %}<main>Intermediate Content</main>{% endblock %}
\ No newline at end of file
diff --git a/test/data/html-extend/result.txt b/test/data/html-extend/result.txt
new file mode 100644
index 0000000..6d99e29
--- /dev/null
+++ b/test/data/html-extend/result.txt
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <title>Default - Inter Pantor: Inja works.</title>
+
+    <style type="text/css">.important { color: #336699; }</style>
+</head>
+<body>
+    <main>Inja is the best and fastest template engine for C++. Period.</main>
+</body>
+</html>
diff --git a/test/data/html-extend/template.txt b/test/data/html-extend/template.txt
new file mode 100644
index 0000000..18eeb5d
--- /dev/null
+++ b/test/data/html-extend/template.txt
@@ -0,0 +1,7 @@
+{% extends "inter.txt" %}
+{% block head -%}
+    {{ super(2) }}
+    <style type="text/css">.important { color: #336699; }</style>
+{%- endblock %}
+{% block title %}{{ super(2) }}{{ super() }}: {{ title }}{% endblock %}
+{% block body %}<main>{{ content }}</main>{% endblock %}
diff --git a/test/test-files.cpp b/test/test-files.cpp
index 17a0055..d1f859c 100644
--- a/test/test-files.cpp
+++ b/test/test-files.cpp
@@ -27,7 +27,7 @@
 TEST_CASE("complete-files") {
   inja::Environment env {test_file_directory};
 
-  for (std::string test_name : {"simple-file", "nested", "nested-line", "html"}) {
+  for (std::string test_name : {"simple-file", "nested", "nested-line", "html", "html-extend"}) {
     SUBCASE(test_name.c_str()) {
       CHECK(env.render_file_with_json_file(test_name + "/template.txt", test_name + "/data.json") ==
             env.load_file(test_name + "/result.txt"));