Inja 3.5.0
A Template Engine for Modern C++
Loading...
Searching...
No Matches
parser.hpp
1#ifndef INCLUDE_INJA_PARSER_HPP_
2#define INCLUDE_INJA_PARSER_HPP_
3
4#include <cstddef>
5#include <filesystem>
6#include <fstream>
7#include <iterator>
8#include <memory>
9#include <stack>
10#include <string>
11#include <string_view>
12#include <utility>
13#include <vector>
14
15#include "config.hpp"
16#include "exceptions.hpp"
17#include "function_storage.hpp"
18#include "lexer.hpp"
19#include "node.hpp"
20#include "template.hpp"
21#include "throw.hpp"
22#include "token.hpp"
23
24namespace inja {
25
29class Parser {
30 using Arguments = std::vector<std::shared_ptr<ExpressionNode>>;
31 using OperatorStack = std::stack<std::shared_ptr<FunctionNode>>;
32
33 const ParserConfig& config;
34
35 Lexer lexer;
36 TemplateStorage& template_storage;
37 const FunctionStorage& function_storage;
38
39 Token tok, peek_tok;
40 bool have_peek_tok {false};
41
42 std::string_view literal_start;
43
44 BlockNode* current_block {nullptr};
45 ExpressionListNode* current_expression_list {nullptr};
46
47 std::stack<IfStatementNode*> if_statement_stack;
48 std::stack<ForStatementNode*> for_statement_stack;
49 std::stack<BlockStatementNode*> block_statement_stack;
50
51 void throw_parser_error(const std::string& message) const {
52 INJA_THROW(ParserError(message, lexer.current_position()));
53 }
54
55 void get_next_token() {
56 if (have_peek_tok) {
57 tok = peek_tok;
58 have_peek_tok = false;
59 } else {
60 tok = lexer.scan();
61 }
62 }
63
64 void get_peek_token() {
65 if (!have_peek_tok) {
66 peek_tok = lexer.scan();
67 have_peek_tok = true;
68 }
69 }
70
71 void add_literal(Arguments &arguments, const char* content_ptr) {
72 const std::string_view data_text(literal_start.data(), tok.text.data() - literal_start.data() + tok.text.size());
73 arguments.emplace_back(std::make_shared<LiteralNode>(data_text, data_text.data() - content_ptr));
74 }
75
76 void add_operator(Arguments &arguments, OperatorStack &operator_stack) {
77 auto function = operator_stack.top();
78 operator_stack.pop();
79
80 if (static_cast<int>(arguments.size()) < function->number_args) {
81 throw_parser_error("too few arguments");
82 }
83
84 for (int i = 0; i < function->number_args; ++i) {
85 function->arguments.insert(function->arguments.begin(), arguments.back());
86 arguments.pop_back();
87 }
88 arguments.emplace_back(function);
89 }
90
91 void add_to_template_storage(const std::filesystem::path& path, std::string& template_name) {
92 if (template_storage.find(template_name) != template_storage.end()) {
93 return;
94 }
95
96 const std::string original_name = template_name;
97
98 if (config.search_included_templates_in_files) {
99 // Build the relative path
100 template_name = (path / original_name).string();
101 if (template_name.compare(0, 2, "./") == 0) {
102 template_name.erase(0, 2);
103 }
104
105 if (template_storage.find(template_name) == template_storage.end()) {
106 // Load file
107 std::ifstream file;
108 file.open(template_name);
109 if (!file.fail()) {
110 const std::string text((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
111
112 auto include_template = Template(text);
113 template_storage.emplace(template_name, include_template);
114 parse_into_template(template_storage[template_name], template_name);
115 return;
116 } else if (!config.include_callback) {
117 INJA_THROW(FileError("failed accessing file at '" + template_name + "'"));
118 }
119 }
120 }
121
122 // Try include callback
123 if (config.include_callback) {
124 auto include_template = config.include_callback(path, original_name);
125 template_storage.emplace(template_name, include_template);
126 }
127 }
128
129 std::string parse_filename() const {
130 if (tok.kind != Token::Kind::String) {
131 throw_parser_error("expected string, got '" + tok.describe() + "'");
132 }
133
134 if (tok.text.length() < 2) {
135 throw_parser_error("expected filename, got '" + static_cast<std::string>(tok.text) + "'");
136 }
137
138 // Remove first and last character ""
139 return std::string {tok.text.substr(1, tok.text.length() - 2)};
140 }
141
142 bool parse_expression(Template& tmpl, Token::Kind closing) {
143 current_expression_list->root = parse_expression(tmpl);
144 return tok.kind == closing;
145 }
146
147 std::shared_ptr<ExpressionNode> parse_expression(Template& tmpl) {
148 size_t current_bracket_level {0};
149 size_t current_brace_level {0};
150 Arguments arguments;
151 OperatorStack operator_stack;
152
153 while (tok.kind != Token::Kind::Eof) {
154 // Literals
155 switch (tok.kind) {
156 case Token::Kind::String: {
157 if (current_brace_level == 0 && current_bracket_level == 0) {
158 literal_start = tok.text;
159 add_literal(arguments, tmpl.content.c_str());
160 }
161 } break;
162 case Token::Kind::Number: {
163 if (current_brace_level == 0 && current_bracket_level == 0) {
164 literal_start = tok.text;
165 add_literal(arguments, tmpl.content.c_str());
166 }
167 } break;
168 case Token::Kind::LeftBracket: {
169 if (current_brace_level == 0 && current_bracket_level == 0) {
170 literal_start = tok.text;
171 }
172 current_bracket_level += 1;
173 } break;
174 case Token::Kind::LeftBrace: {
175 if (current_brace_level == 0 && current_bracket_level == 0) {
176 literal_start = tok.text;
177 }
178 current_brace_level += 1;
179 } break;
180 case Token::Kind::RightBracket: {
181 if (current_bracket_level == 0) {
182 throw_parser_error("unexpected ']'");
183 }
184
185 current_bracket_level -= 1;
186 if (current_brace_level == 0 && current_bracket_level == 0) {
187 add_literal(arguments, tmpl.content.c_str());
188 }
189 } break;
190 case Token::Kind::RightBrace: {
191 if (current_brace_level == 0) {
192 throw_parser_error("unexpected '}'");
193 }
194
195 current_brace_level -= 1;
196 if (current_brace_level == 0 && current_bracket_level == 0) {
197 add_literal(arguments, tmpl.content.c_str());
198 }
199 } break;
200 case Token::Kind::Id: {
201 get_peek_token();
202
203 // Data Literal
204 if (tok.text == static_cast<decltype(tok.text)>("true") || tok.text == static_cast<decltype(tok.text)>("false") ||
205 tok.text == static_cast<decltype(tok.text)>("null")) {
206 if (current_brace_level == 0 && current_bracket_level == 0) {
207 literal_start = tok.text;
208 add_literal(arguments, tmpl.content.c_str());
209 }
210
211 // Operator
212 } else if (tok.text == "and" || tok.text == "or" || tok.text == "in" || tok.text == "not") {
213 goto parse_operator;
214
215 // Functions
216 } else if (peek_tok.kind == Token::Kind::LeftParen) {
217 auto func = std::make_shared<FunctionNode>(tok.text, tok.text.data() - tmpl.content.c_str());
218 get_next_token();
219 do {
220 get_next_token();
221 auto expr = parse_expression(tmpl);
222 if (!expr) {
223 break;
224 }
225 func->number_args += 1;
226 func->arguments.emplace_back(expr);
227 } while (tok.kind == Token::Kind::Comma);
228 if (tok.kind != Token::Kind::RightParen) {
229 throw_parser_error("expected right parenthesis, got '" + tok.describe() + "'");
230 }
231
232 auto function_data = function_storage.find_function(func->name, func->number_args);
233 if (function_data.operation == FunctionStorage::Operation::None) {
234 throw_parser_error("unknown function " + func->name);
235 }
236 func->operation = function_data.operation;
237 if (function_data.operation == FunctionStorage::Operation::Callback) {
238 func->callback = function_data.callback;
239 }
240 arguments.emplace_back(func);
241
242 // Variables
243 } else {
244 arguments.emplace_back(std::make_shared<DataNode>(static_cast<std::string>(tok.text), tok.text.data() - tmpl.content.c_str()));
245 }
246
247 // Operators
248 } break;
249 case Token::Kind::Equal:
250 case Token::Kind::NotEqual:
251 case Token::Kind::GreaterThan:
252 case Token::Kind::GreaterEqual:
253 case Token::Kind::LessThan:
254 case Token::Kind::LessEqual:
255 case Token::Kind::Plus:
256 case Token::Kind::Minus:
257 case Token::Kind::Times:
258 case Token::Kind::Slash:
259 case Token::Kind::Power:
260 case Token::Kind::Percent:
261 case Token::Kind::Dot: {
262
263 parse_operator:
264 FunctionStorage::Operation operation;
265 switch (tok.kind) {
266 case Token::Kind::Id: {
267 if (tok.text == "and") {
268 operation = FunctionStorage::Operation::And;
269 } else if (tok.text == "or") {
270 operation = FunctionStorage::Operation::Or;
271 } else if (tok.text == "in") {
272 operation = FunctionStorage::Operation::In;
273 } else if (tok.text == "not") {
274 operation = FunctionStorage::Operation::Not;
275 } else {
276 throw_parser_error("unknown operator in parser.");
277 }
278 } break;
279 case Token::Kind::Equal: {
280 operation = FunctionStorage::Operation::Equal;
281 } break;
282 case Token::Kind::NotEqual: {
283 operation = FunctionStorage::Operation::NotEqual;
284 } break;
285 case Token::Kind::GreaterThan: {
286 operation = FunctionStorage::Operation::Greater;
287 } break;
288 case Token::Kind::GreaterEqual: {
289 operation = FunctionStorage::Operation::GreaterEqual;
290 } break;
291 case Token::Kind::LessThan: {
292 operation = FunctionStorage::Operation::Less;
293 } break;
294 case Token::Kind::LessEqual: {
295 operation = FunctionStorage::Operation::LessEqual;
296 } break;
297 case Token::Kind::Plus: {
298 operation = FunctionStorage::Operation::Add;
299 } break;
300 case Token::Kind::Minus: {
301 operation = FunctionStorage::Operation::Subtract;
302 } break;
303 case Token::Kind::Times: {
304 operation = FunctionStorage::Operation::Multiplication;
305 } break;
306 case Token::Kind::Slash: {
307 operation = FunctionStorage::Operation::Division;
308 } break;
309 case Token::Kind::Power: {
310 operation = FunctionStorage::Operation::Power;
311 } break;
312 case Token::Kind::Percent: {
313 operation = FunctionStorage::Operation::Modulo;
314 } break;
315 case Token::Kind::Dot: {
316 operation = FunctionStorage::Operation::AtId;
317 } break;
318 default: {
319 throw_parser_error("unknown operator in parser.");
320 }
321 }
322 auto function_node = std::make_shared<FunctionNode>(operation, tok.text.data() - tmpl.content.c_str());
323
324 while (!operator_stack.empty() &&
325 ((operator_stack.top()->precedence > function_node->precedence) ||
326 (operator_stack.top()->precedence == function_node->precedence && function_node->associativity == FunctionNode::Associativity::Left))) {
327 add_operator(arguments, operator_stack);
328 }
329
330 operator_stack.emplace(function_node);
331 } break;
332 case Token::Kind::Comma: {
333 if (current_brace_level == 0 && current_bracket_level == 0) {
334 goto break_loop;
335 }
336 } break;
337 case Token::Kind::Colon: {
338 if (current_brace_level == 0 && current_bracket_level == 0) {
339 throw_parser_error("unexpected ':'");
340 }
341 } break;
342 case Token::Kind::LeftParen: {
343 get_next_token();
344 auto expr = parse_expression(tmpl);
345 if (tok.kind != Token::Kind::RightParen) {
346 throw_parser_error("expected right parenthesis, got '" + tok.describe() + "'");
347 }
348 if (!expr) {
349 throw_parser_error("empty expression in parentheses");
350 }
351 arguments.emplace_back(expr);
352 } break;
353
354 // parse function call pipe syntax
355 case Token::Kind::Pipe: {
356 // get function name
357 get_next_token();
358 if (tok.kind != Token::Kind::Id) {
359 throw_parser_error("expected function name, got '" + tok.describe() + "'");
360 }
361 auto func = std::make_shared<FunctionNode>(tok.text, tok.text.data() - tmpl.content.c_str());
362 // add first parameter as last value from arguments
363 func->number_args += 1;
364 func->arguments.emplace_back(arguments.back());
365 arguments.pop_back();
366 get_peek_token();
367 if (peek_tok.kind == Token::Kind::LeftParen) {
368 get_next_token();
369 // parse additional parameters
370 do {
371 get_next_token();
372 auto expr = parse_expression(tmpl);
373 if (!expr) {
374 break;
375 }
376 func->number_args += 1;
377 func->arguments.emplace_back(expr);
378 } while (tok.kind == Token::Kind::Comma);
379 if (tok.kind != Token::Kind::RightParen) {
380 throw_parser_error("expected right parenthesis, got '" + tok.describe() + "'");
381 }
382 }
383 // search store for defined function with such name and number of args
384 auto function_data = function_storage.find_function(func->name, func->number_args);
385 if (function_data.operation == FunctionStorage::Operation::None) {
386 throw_parser_error("unknown function " + func->name);
387 }
388 func->operation = function_data.operation;
389 if (function_data.operation == FunctionStorage::Operation::Callback) {
390 func->callback = function_data.callback;
391 }
392 arguments.emplace_back(func);
393 } break;
394 default:
395 goto break_loop;
396 }
397
398 get_next_token();
399 }
400
401 break_loop:
402 while (!operator_stack.empty()) {
403 add_operator(arguments, operator_stack);
404 }
405
406 std::shared_ptr<ExpressionNode> expr;
407 if (arguments.size() == 1) {
408 expr = arguments[0];
409 arguments = {};
410 } else if (arguments.size() > 1) {
411 throw_parser_error("malformed expression");
412 }
413 return expr;
414 }
415
416 bool parse_statement(Template& tmpl, Token::Kind closing, const std::filesystem::path& path) {
417 if (tok.kind != Token::Kind::Id) {
418 return false;
419 }
420
421 if (tok.text == static_cast<decltype(tok.text)>("if")) {
422 get_next_token();
423
424 auto if_statement_node = std::make_shared<IfStatementNode>(current_block, tok.text.data() - tmpl.content.c_str());
425 current_block->nodes.emplace_back(if_statement_node);
426 if_statement_stack.emplace(if_statement_node.get());
427 current_block = &if_statement_node->true_statement;
428 current_expression_list = &if_statement_node->condition;
429
430 if (!parse_expression(tmpl, closing)) {
431 return false;
432 }
433 } else if (tok.text == static_cast<decltype(tok.text)>("else")) {
434 if (if_statement_stack.empty()) {
435 throw_parser_error("else without matching if");
436 }
437 auto& if_statement_data = if_statement_stack.top();
438 get_next_token();
439
440 if_statement_data->has_false_statement = true;
441 current_block = &if_statement_data->false_statement;
442
443 // Chained else if
444 if (tok.kind == Token::Kind::Id && tok.text == static_cast<decltype(tok.text)>("if")) {
445 get_next_token();
446
447 auto if_statement_node = std::make_shared<IfStatementNode>(true, current_block, tok.text.data() - tmpl.content.c_str());
448 current_block->nodes.emplace_back(if_statement_node);
449 if_statement_stack.emplace(if_statement_node.get());
450 current_block = &if_statement_node->true_statement;
451 current_expression_list = &if_statement_node->condition;
452
453 if (!parse_expression(tmpl, closing)) {
454 return false;
455 }
456 }
457 } else if (tok.text == static_cast<decltype(tok.text)>("endif")) {
458 if (if_statement_stack.empty()) {
459 throw_parser_error("endif without matching if");
460 }
461
462 // Nested if statements
463 while (if_statement_stack.top()->is_nested) {
464 if_statement_stack.pop();
465 }
466
467 auto& if_statement_data = if_statement_stack.top();
468 get_next_token();
469
470 current_block = if_statement_data->parent;
471 if_statement_stack.pop();
472 } else if (tok.text == static_cast<decltype(tok.text)>("block")) {
473 get_next_token();
474
475 if (tok.kind != Token::Kind::Id) {
476 throw_parser_error("expected block name, got '" + tok.describe() + "'");
477 }
478
479 const std::string block_name = static_cast<std::string>(tok.text);
480
481 auto block_statement_node = std::make_shared<BlockStatementNode>(current_block, block_name, tok.text.data() - tmpl.content.c_str());
482 current_block->nodes.emplace_back(block_statement_node);
483 block_statement_stack.emplace(block_statement_node.get());
484 current_block = &block_statement_node->block;
485 auto success = tmpl.block_storage.emplace(block_name, block_statement_node);
486 if (!success.second) {
487 throw_parser_error("block with the name '" + block_name + "' does already exist");
488 }
489
490 get_next_token();
491 } else if (tok.text == static_cast<decltype(tok.text)>("endblock")) {
492 if (block_statement_stack.empty()) {
493 throw_parser_error("endblock without matching block");
494 }
495
496 auto& block_statement_data = block_statement_stack.top();
497 get_next_token();
498
499 current_block = block_statement_data->parent;
500 block_statement_stack.pop();
501 } else if (tok.text == static_cast<decltype(tok.text)>("for")) {
502 get_next_token();
503
504 // options: for a in arr; for a, b in obj
505 if (tok.kind != Token::Kind::Id) {
506 throw_parser_error("expected id, got '" + tok.describe() + "'");
507 }
508
509 Token value_token = tok;
510 get_next_token();
511
512 // Object type
513 std::shared_ptr<ForStatementNode> for_statement_node;
514 if (tok.kind == Token::Kind::Comma) {
515 get_next_token();
516 if (tok.kind != Token::Kind::Id) {
517 throw_parser_error("expected id, got '" + tok.describe() + "'");
518 }
519
520 const Token key_token = value_token;
521 value_token = tok;
522 get_next_token();
523
524 for_statement_node = std::make_shared<ForObjectStatementNode>(static_cast<std::string>(key_token.text), static_cast<std::string>(value_token.text),
525 current_block, tok.text.data() - tmpl.content.c_str());
526
527 // Array type
528 } else {
529 for_statement_node =
530 std::make_shared<ForArrayStatementNode>(static_cast<std::string>(value_token.text), current_block, tok.text.data() - tmpl.content.c_str());
531 }
532
533 current_block->nodes.emplace_back(for_statement_node);
534 for_statement_stack.emplace(for_statement_node.get());
535 current_block = &for_statement_node->body;
536 current_expression_list = &for_statement_node->condition;
537
538 if (tok.kind != Token::Kind::Id || tok.text != static_cast<decltype(tok.text)>("in")) {
539 throw_parser_error("expected 'in', got '" + tok.describe() + "'");
540 }
541 get_next_token();
542
543 if (!parse_expression(tmpl, closing)) {
544 return false;
545 }
546 } else if (tok.text == static_cast<decltype(tok.text)>("endfor")) {
547 if (for_statement_stack.empty()) {
548 throw_parser_error("endfor without matching for");
549 }
550
551 auto& for_statement_data = for_statement_stack.top();
552 get_next_token();
553
554 current_block = for_statement_data->parent;
555 for_statement_stack.pop();
556 } else if (tok.text == static_cast<decltype(tok.text)>("include")) {
557 get_next_token();
558
559 std::string template_name = parse_filename();
560 add_to_template_storage(path, template_name);
561
562 current_block->nodes.emplace_back(std::make_shared<IncludeStatementNode>(template_name, tok.text.data() - tmpl.content.c_str()));
563
564 get_next_token();
565 } else if (tok.text == static_cast<decltype(tok.text)>("extends")) {
566 get_next_token();
567
568 std::string template_name = parse_filename();
569 add_to_template_storage(path, template_name);
570
571 current_block->nodes.emplace_back(std::make_shared<ExtendsStatementNode>(template_name, tok.text.data() - tmpl.content.c_str()));
572
573 get_next_token();
574 } else if (tok.text == static_cast<decltype(tok.text)>("set")) {
575 get_next_token();
576
577 if (tok.kind != Token::Kind::Id) {
578 throw_parser_error("expected variable name, got '" + tok.describe() + "'");
579 }
580
581 const std::string key = static_cast<std::string>(tok.text);
582 get_next_token();
583
584 auto set_statement_node = std::make_shared<SetStatementNode>(key, tok.text.data() - tmpl.content.c_str());
585 current_block->nodes.emplace_back(set_statement_node);
586 current_expression_list = &set_statement_node->expression;
587
588 if (tok.text != static_cast<decltype(tok.text)>("=")) {
589 throw_parser_error("expected '=', got '" + tok.describe() + "'");
590 }
591 get_next_token();
592
593 if (!parse_expression(tmpl, closing)) {
594 return false;
595 }
596 } else {
597 return false;
598 }
599 return true;
600 }
601
602 void parse_into(Template& tmpl, const std::filesystem::path& path) {
603 lexer.start(tmpl.content);
604 current_block = &tmpl.root;
605
606 for (;;) {
607 get_next_token();
608 switch (tok.kind) {
609 case Token::Kind::Eof: {
610 if (!if_statement_stack.empty()) {
611 throw_parser_error("unmatched if");
612 }
613 if (!for_statement_stack.empty()) {
614 throw_parser_error("unmatched for");
615 }
616 }
617 current_block = nullptr;
618 return;
619 case Token::Kind::Text: {
620 current_block->nodes.emplace_back(std::make_shared<TextNode>(tok.text.data() - tmpl.content.c_str(), tok.text.size()));
621 } break;
622 case Token::Kind::StatementOpen: {
623 get_next_token();
624 if (!parse_statement(tmpl, Token::Kind::StatementClose, path)) {
625 throw_parser_error("expected statement, got '" + tok.describe() + "'");
626 }
627 if (tok.kind != Token::Kind::StatementClose) {
628 throw_parser_error("expected statement close, got '" + tok.describe() + "'");
629 }
630 } break;
631 case Token::Kind::LineStatementOpen: {
632 get_next_token();
633 if (!parse_statement(tmpl, Token::Kind::LineStatementClose, path)) {
634 throw_parser_error("expected statement, got '" + tok.describe() + "'");
635 }
636 if (tok.kind != Token::Kind::LineStatementClose && tok.kind != Token::Kind::Eof) {
637 throw_parser_error("expected line statement close, got '" + tok.describe() + "'");
638 }
639 } break;
640 case Token::Kind::ExpressionOpen: {
641 get_next_token();
642
643 auto expression_list_node = std::make_shared<ExpressionListNode>(tok.text.data() - tmpl.content.c_str());
644 current_block->nodes.emplace_back(expression_list_node);
645 current_expression_list = expression_list_node.get();
646
647 if (!parse_expression(tmpl, Token::Kind::ExpressionClose)) {
648 throw_parser_error("expected expression close, got '" + tok.describe() + "'");
649 }
650 } break;
651 case Token::Kind::CommentOpen: {
652 get_next_token();
653 if (tok.kind != Token::Kind::CommentClose) {
654 throw_parser_error("expected comment close, got '" + tok.describe() + "'");
655 }
656 } break;
657 default: {
658 throw_parser_error("unexpected token '" + tok.describe() + "'");
659 } break;
660 }
661 }
662 current_block = nullptr;
663 }
664
665public:
666 explicit Parser(const ParserConfig& parser_config, const LexerConfig& lexer_config, TemplateStorage& template_storage,
667 const FunctionStorage& function_storage)
668 : config(parser_config), lexer(lexer_config), template_storage(template_storage), function_storage(function_storage) {}
669
670 Template parse(std::string_view input, const std::filesystem::path& path) {
671 auto result = Template(std::string(input));
672 parse_into(result, path);
673 return result;
674 }
675
676 void parse_into_template(Template& tmpl, const std::filesystem::path& filename) {
677 auto sub_parser = Parser(config, lexer.get_config(), template_storage, function_storage);
678 sub_parser.parse_into(tmpl, filename.parent_path());
679 }
680
681 static std::string load_file(const std::filesystem::path& filename) {
682 std::ifstream file;
683 file.open(filename);
684 if (file.fail()) {
685 INJA_THROW(FileError("failed accessing file at '" + filename.string() + "'"));
686 }
687 std::string text((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
688 return text;
689 }
690};
691
692} // namespace inja
693
694#endif // INCLUDE_INJA_PARSER_HPP_
Definition node.hpp:70
Definition node.hpp:255
Class for builtin functions and user-defined callbacks.
Definition function_storage.hpp:22
Class for lexing an inja Template.
Definition lexer.hpp:18
Class for parsing an inja Template.
Definition parser.hpp:29
Definition exceptions.hpp:37
Class for lexer configuration.
Definition config.hpp:15
Class for parser configuration.
Definition config.hpp:67
Definition exceptions.hpp:29
The main inja Template.
Definition template.hpp:16
Helper-class for the inja Lexer.
Definition token.hpp:12