Skip to content

Commit 95af782

Browse files
authored
Add Child Inheritance Feature (pantor#198)
* add block and extend feature * constify, code cleaning * update single include * add html language to readme * clean tests * constantify, update year
1 parent b4b9d8d commit 95af782

File tree

22 files changed

+559
-171
lines changed

22 files changed

+559
-171
lines changed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2018 lbersch
3+
Copyright (c) 2018-2021 lbersch
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -254,25 +254,6 @@ render("{{ isArray(guests) }}", data); // "true"
254254
// Implemented type checks: isArray, isBoolean, isFloat, isInteger, isNumber, isObject, isString,
255255
```
256256
257-
### Whitespace Control
258-
259-
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.
260-
261-
```.cpp
262-
Environment env;
263-
env.set_trim_blocks(true);
264-
env.set_lstrip_blocks(true);
265-
```
266-
267-
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:
268-
269-
```.cpp
270-
render("Hello {{- name -}} !", data); // "Hello Inja!"
271-
render("{% if neighbour in guests -%} I was there{% endif -%} !", data); // Renders without any whitespaces
272-
```
273-
274-
Stripping behind a statement or expression also removes any newlines.
275-
276257
### Callbacks
277258
278259
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.add_void_callback("log", 1, [greet](Arguments args) {
316297
env.render("{{ log(neighbour) }}", data); // Prints nothing to result, only to cout...
317298
```
318299

300+
### Template Inheritance
301+
302+
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
303+
```.html
304+
<!DOCTYPE html>
305+
<html>
306+
<head>
307+
{% block head %}
308+
<link rel="stylesheet" href="style.css" />
309+
<title>{% block title %}{% endblock %} - My Webpage</title>
310+
{% endblock %}
311+
</head>
312+
<body>
313+
<div id="content">{% block content %}{% endblock %}</div>
314+
</body>
315+
</html>
316+
```
317+
contains three `blocks` that child templates can fill in. The child template
318+
```.html
319+
{% extends "base.html" %}
320+
{% block title %}Index{% endblock %}
321+
{% block head %}
322+
{{ super() }}
323+
<style type="text/css">
324+
.important { color: #336699; }
325+
</style>
326+
{% endblock %}
327+
{% block content %}
328+
<h1>Index</h1>
329+
<p class="important">
330+
Welcome to my blog!
331+
</p>
332+
{% endblock %}
333+
```
334+
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.
335+
336+
### Whitespace Control
337+
338+
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.
339+
340+
```.cpp
341+
Environment env;
342+
env.set_trim_blocks(true);
343+
env.set_lstrip_blocks(true);
344+
```
345+
346+
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:
347+
348+
```.cpp
349+
render("Hello {{- name -}} !", data); // "Hello Inja!"
350+
render("{% if neighbour in guests -%} I was there{% endif -%} !", data); // Renders without any whitespaces
351+
```
352+
353+
Stripping behind a statement or expression also removes any newlines.
354+
319355
### Comments
320356
321357
Comments can be written with the `{# ... #}` syntax.

include/inja/config.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2019 Pantor. All rights reserved.
1+
// Copyright (c) 2021 Pantor. All rights reserved.
22

33
#ifndef INCLUDE_INJA_CONFIG_HPP_
44
#define INCLUDE_INJA_CONFIG_HPP_

include/inja/environment.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2019 Pantor. All rights reserved.
1+
// Copyright (c) 2021 Pantor. All rights reserved.
22

33
#ifndef INCLUDE_INJA_ENVIRONMENT_HPP_
44
#define INCLUDE_INJA_ENVIRONMENT_HPP_

include/inja/exceptions.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2020 Pantor. All rights reserved.
1+
// Copyright (c) 2021 Pantor. All rights reserved.
22

33
#ifndef INCLUDE_INJA_EXCEPTIONS_HPP_
44
#define INCLUDE_INJA_EXCEPTIONS_HPP_

include/inja/function_storage.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2020 Pantor. All rights reserved.
1+
// Copyright (c) 2021 Pantor. All rights reserved.
22

33
#ifndef INCLUDE_INJA_FUNCTION_STORAGE_HPP_
44
#define INCLUDE_INJA_FUNCTION_STORAGE_HPP_
@@ -64,6 +64,7 @@ class FunctionStorage {
6464
Round,
6565
Sort,
6666
Upper,
67+
Super,
6768
Callback,
6869
ParenLeft,
6970
ParenRight,
@@ -106,6 +107,8 @@ class FunctionStorage {
106107
{std::make_pair("round", 2), FunctionData { Operation::Round }},
107108
{std::make_pair("sort", 1), FunctionData { Operation::Sort }},
108109
{std::make_pair("upper", 1), FunctionData { Operation::Upper }},
110+
{std::make_pair("super", 0), FunctionData { Operation::Super }},
111+
{std::make_pair("super", 1), FunctionData { Operation::Super }},
109112
};
110113

111114
public:

include/inja/inja.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2020 Pantor. All rights reserved.
1+
// Copyright (c) 2021 Pantor. All rights reserved.
22

33
#ifndef INCLUDE_INJA_INJA_HPP_
44
#define INCLUDE_INJA_INJA_HPP_

include/inja/lexer.hpp

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class Lexer {
5151
if (tok_start >= m_in.size()) {
5252
return make_token(Token::Kind::Eof);
5353
}
54-
char ch = m_in[tok_start];
54+
const char ch = m_in[tok_start];
5555
if (ch == ' ' || ch == '\t' || ch == '\r') {
5656
tok_start += 1;
5757
goto again;
@@ -61,15 +61,15 @@ class Lexer {
6161
if (!close_trim.empty() && inja::string_view::starts_with(m_in.substr(tok_start), close_trim)) {
6262
state = State::Text;
6363
pos = tok_start + close_trim.size();
64-
Token tok = make_token(closeKind);
64+
const Token tok = make_token(closeKind);
6565
skip_whitespaces_and_newlines();
6666
return tok;
6767
}
6868

6969
if (inja::string_view::starts_with(m_in.substr(tok_start), close)) {
7070
state = State::Text;
7171
pos = tok_start + close.size();
72-
Token tok = make_token(closeKind);
72+
const Token tok = make_token(closeKind);
7373
if (trim) {
7474
skip_whitespaces_and_first_newline();
7575
}
@@ -88,7 +88,7 @@ class Lexer {
8888
return scan_id();
8989
}
9090

91-
MinusState current_minus_state = minus_state;
91+
const MinusState current_minus_state = minus_state;
9292
if (minus_state == MinusState::Operator) {
9393
minus_state = MinusState::Number;
9494
}
@@ -183,7 +183,7 @@ class Lexer {
183183
if (pos >= m_in.size()) {
184184
break;
185185
}
186-
char ch = m_in[pos];
186+
const char ch = m_in[pos];
187187
if (!std::isalnum(ch) && ch != '.' && ch != '/' && ch != '_' && ch != '-') {
188188
break;
189189
}
@@ -197,7 +197,7 @@ class Lexer {
197197
if (pos >= m_in.size()) {
198198
break;
199199
}
200-
char ch = m_in[pos];
200+
const char ch = m_in[pos];
201201
// be very permissive in lexer (we'll catch errors when conversion happens)
202202
if (!std::isdigit(ch) && ch != '.' && ch != 'e' && ch != 'E' && ch != '+' && ch != '-') {
203203
break;
@@ -213,7 +213,7 @@ class Lexer {
213213
if (pos >= m_in.size()) {
214214
break;
215215
}
216-
char ch = m_in[pos++];
216+
const char ch = m_in[pos++];
217217
if (ch == '\\') {
218218
escape = true;
219219
} else if (!escape && ch == m_in[tok_start]) {
@@ -302,7 +302,7 @@ class Lexer {
302302
default:
303303
case State::Text: {
304304
// fast-scan to first open character
305-
size_t open_start = m_in.substr(pos).find_first_of(config.open_chars);
305+
const size_t open_start = m_in.substr(pos).find_first_of(config.open_chars);
306306
if (open_start == nonstd::string_view::npos) {
307307
// didn't find open, return remaining text as text token
308308
pos = m_in.size();

include/inja/node.hpp

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2020 Pantor. All rights reserved.
1+
// Copyright (c) 2021 Pantor. All rights reserved.
22

33
#ifndef INCLUDE_INJA_NODE_HPP_
44
#define INCLUDE_INJA_NODE_HPP_
@@ -28,6 +28,8 @@ class ForArrayStatementNode;
2828
class ForObjectStatementNode;
2929
class IfStatementNode;
3030
class IncludeStatementNode;
31+
class ExtendsStatementNode;
32+
class BlockStatementNode;
3133
class SetStatementNode;
3234

3335

@@ -48,6 +50,8 @@ class NodeVisitor {
4850
virtual void visit(const ForObjectStatementNode& node) = 0;
4951
virtual void visit(const IfStatementNode& node) = 0;
5052
virtual void visit(const IncludeStatementNode& node) = 0;
53+
virtual void visit(const ExtendsStatementNode& node) = 0;
54+
virtual void visit(const BlockStatementNode& node) = 0;
5155
virtual void visit(const SetStatementNode& node) = 0;
5256
};
5357

@@ -331,6 +335,30 @@ class IncludeStatementNode : public StatementNode {
331335
}
332336
};
333337

338+
class ExtendsStatementNode : public StatementNode {
339+
public:
340+
const std::string file;
341+
342+
explicit ExtendsStatementNode(const std::string& file, size_t pos) : StatementNode(pos), file(file) { }
343+
344+
void accept(NodeVisitor& v) const {
345+
v.visit(*this);
346+
};
347+
};
348+
349+
class BlockStatementNode : public StatementNode {
350+
public:
351+
const std::string name;
352+
BlockNode block;
353+
BlockNode *const parent;
354+
355+
explicit BlockStatementNode(BlockNode *const parent, const std::string& name, size_t pos) : StatementNode(pos), parent(parent), name(name) { }
356+
357+
void accept(NodeVisitor& v) const {
358+
v.visit(*this);
359+
};
360+
};
361+
334362
class SetStatementNode : public StatementNode {
335363
public:
336364
const std::string key;

include/inja/parser.hpp

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2020 Pantor. All rights reserved.
1+
// Copyright (c) 2021 Pantor. All rights reserved.
22

33
#ifndef INCLUDE_INJA_PARSER_HPP_
44
#define INCLUDE_INJA_PARSER_HPP_
@@ -50,6 +50,7 @@ class Parser {
5050
std::stack<std::shared_ptr<FunctionNode>> operator_stack;
5151
std::stack<IfStatementNode*> if_statement_stack;
5252
std::stack<ForStatementNode*> for_statement_stack;
53+
std::stack<BlockStatementNode*> block_statement_stack;
5354

5455
inline void throw_parser_error(const std::string &message) {
5556
INJA_THROW(ParserError(message, lexer.current_position()));
@@ -87,6 +88,22 @@ class Parser {
8788
arguments.emplace_back(function);
8889
}
8990

91+
void add_to_template_storage(nonstd::string_view path, std::string& template_name) {
92+
if (config.search_included_templates_in_files && template_storage.find(template_name) == template_storage.end()) {
93+
// Build the relative path
94+
template_name = static_cast<std::string>(path) + template_name;
95+
if (template_name.compare(0, 2, "./") == 0) {
96+
template_name.erase(0, 2);
97+
}
98+
99+
if (template_storage.find(template_name) == template_storage.end()) {
100+
auto include_template = Template(load_file(template_name));
101+
template_storage.emplace(template_name, include_template);
102+
parse_into_template(template_storage[template_name], template_name);
103+
}
104+
}
105+
}
106+
90107
bool parse_expression(Template &tmpl, Token::Kind closing) {
91108
while (tok.kind != closing && tok.kind != Token::Kind::Eof) {
92109
// Literals
@@ -387,6 +404,37 @@ class Parser {
387404
current_block = if_statement_data->parent;
388405
if_statement_stack.pop();
389406

407+
} else if (tok.text == static_cast<decltype(tok.text)>("block")) {
408+
get_next_token();
409+
410+
if (tok.kind != Token::Kind::Id) {
411+
throw_parser_error("expected block name, got '" + tok.describe() + "'");
412+
}
413+
414+
const std::string block_name = static_cast<std::string>(tok.text);
415+
416+
auto block_statement_node = std::make_shared<BlockStatementNode>(current_block, block_name, tok.text.data() - tmpl.content.c_str());
417+
current_block->nodes.emplace_back(block_statement_node);
418+
block_statement_stack.emplace(block_statement_node.get());
419+
current_block = &block_statement_node->block;
420+
auto success = tmpl.block_storage.emplace(block_name, block_statement_node);
421+
if (!success.second) {
422+
throw_parser_error("block with the name '" + block_name + "' does already exist");
423+
}
424+
425+
get_next_token();
426+
427+
} else if (tok.text == static_cast<decltype(tok.text)>("endblock")) {
428+
if (block_statement_stack.empty()) {
429+
throw_parser_error("endblock without matching block");
430+
}
431+
432+
auto &block_statement_data = block_statement_stack.top();
433+
get_next_token();
434+
435+
current_block = block_statement_data->parent;
436+
block_statement_stack.pop();
437+
390438
} else if (tok.text == static_cast<decltype(tok.text)>("for")) {
391439
get_next_token();
392440

@@ -450,21 +498,23 @@ class Parser {
450498
}
451499

452500
std::string template_name = json::parse(tok.text).get_ref<const std::string &>();
453-
if (config.search_included_templates_in_files && template_storage.find(template_name) == template_storage.end()) {
454-
// Build the relative path
455-
template_name = static_cast<std::string>(path) + template_name;
456-
if (template_name.compare(0, 2, "./") == 0) {
457-
template_name.erase(0, 2);
458-
}
501+
add_to_template_storage(path, template_name);
459502

460-
if (template_storage.find(template_name) == template_storage.end()) {
461-
auto include_template = Template(load_file(template_name));
462-
template_storage.emplace(template_name, include_template);
463-
parse_into_template(template_storage[template_name], template_name);
464-
}
503+
current_block->nodes.emplace_back(std::make_shared<IncludeStatementNode>(template_name, tok.text.data() - tmpl.content.c_str()));
504+
505+
get_next_token();
506+
507+
} else if (tok.text == static_cast<decltype(tok.text)>("extends")) {
508+
get_next_token();
509+
510+
if (tok.kind != Token::Kind::String) {
511+
throw_parser_error("expected string, got '" + tok.describe() + "'");
465512
}
466513

467-
current_block->nodes.emplace_back(std::make_shared<IncludeStatementNode>(template_name, tok.text.data() - tmpl.content.c_str()));
514+
std::string template_name = json::parse(tok.text).get_ref<const std::string &>();
515+
add_to_template_storage(path, template_name);
516+
517+
current_block->nodes.emplace_back(std::make_shared<ExtendsStatementNode>(template_name, tok.text.data() - tmpl.content.c_str()));
468518

469519
get_next_token();
470520

0 commit comments

Comments
 (0)