Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Child Inheritance Feature #198

Merged
merged 6 commits into from
May 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -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
Expand Down
74 changes: 55 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,25 +254,6 @@ render("{{ isArray(guests) }}", data); // "true"
// 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.
Expand Down Expand Up @@ -316,6 +297,61 @@ env.add_void_callback("log", 1, [greet](Arguments args) {
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.
Expand Down
2 changes: 1 addition & 1 deletion include/inja/config.hpp
Original file line number Diff line number Diff line change
@@ -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_
Expand Down
2 changes: 1 addition & 1 deletion include/inja/environment.hpp
Original file line number Diff line number Diff line change
@@ -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_
Expand Down
2 changes: 1 addition & 1 deletion include/inja/exceptions.hpp
Original file line number Diff line number Diff line change
@@ -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_
Expand Down
5 changes: 4 additions & 1 deletion include/inja/function_storage.hpp
Original file line number Diff line number Diff line change
@@ -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_
Expand Down Expand Up @@ -64,6 +64,7 @@ class FunctionStorage {
Round,
Sort,
Upper,
Super,
Callback,
ParenLeft,
ParenRight,
Expand Down Expand Up @@ -106,6 +107,8 @@ class FunctionStorage {
{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:
Expand Down
2 changes: 1 addition & 1 deletion include/inja/inja.hpp
Original file line number Diff line number Diff line change
@@ -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_
Expand Down
16 changes: 8 additions & 8 deletions include/inja/lexer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class Lexer {
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;
Expand All @@ -61,15 +61,15 @@ class Lexer {
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;
}

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();
}
Expand All @@ -88,7 +88,7 @@ class Lexer {
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;
}
Expand Down Expand Up @@ -183,7 +183,7 @@ class Lexer {
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;
}
Expand All @@ -197,7 +197,7 @@ class Lexer {
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;
Expand All @@ -213,7 +213,7 @@ class Lexer {
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]) {
Expand Down Expand Up @@ -302,7 +302,7 @@ class Lexer {
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();
Expand Down
30 changes: 29 additions & 1 deletion include/inja/node.hpp
Original file line number Diff line number Diff line change
@@ -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_
Expand Down Expand Up @@ -28,6 +28,8 @@ class ForArrayStatementNode;
class ForObjectStatementNode;
class IfStatementNode;
class IncludeStatementNode;
class ExtendsStatementNode;
class BlockStatementNode;
class SetStatementNode;


Expand All @@ -46,6 +48,8 @@ class NodeVisitor {
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;
};

Expand Down Expand Up @@ -329,6 +333,30 @@ class IncludeStatementNode : public StatementNode {
};
};

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;
Expand Down
76 changes: 63 additions & 13 deletions include/inja/parser.hpp
Original file line number Diff line number Diff line change
@@ -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_
Expand Down Expand Up @@ -50,6 +50,7 @@ class Parser {
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()));
Expand Down Expand Up @@ -87,6 +88,22 @@ class Parser {
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
Expand Down Expand Up @@ -387,6 +404,37 @@ class Parser {
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();

Expand Down Expand Up @@ -450,21 +498,23 @@ class Parser {
}

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);
}
add_to_template_storage(path, template_name);

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);
}
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() + "'");
}

current_block->nodes.emplace_back(std::make_shared<IncludeStatementNode>(template_name, tok.text.data() - tmpl.content.c_str()));
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();

Expand Down
Loading