/src/crow/include/crow/mustache.h
Line | Count | Source |
1 | | /** |
2 | | * \file crow/mustache.h |
3 | | * \brief This file includes the definition of the crow::mustache |
4 | | * namespace and its members. |
5 | | */ |
6 | | |
7 | | #pragma once |
8 | | #include <string> |
9 | | #include <vector> |
10 | | #include <fstream> |
11 | | #include <iterator> |
12 | | #include <functional> |
13 | | #include "crow/json.h" |
14 | | #include "crow/logging.h" |
15 | | #include "crow/returnable.h" |
16 | | #include "crow/utility.h" |
17 | | |
18 | | namespace crow // NOTE: Already documented in "crow/app.h" |
19 | | { |
20 | | /** |
21 | | * \namespace crow::mustache |
22 | | * \brief In this namespace is defined most of the functions and |
23 | | * classes related to template rendering. |
24 | | * |
25 | | * If you are here you might want to read these functions and |
26 | | * classes: |
27 | | * |
28 | | * - \ref template_t |
29 | | * - \ref load_text |
30 | | * - \ref load_text_unsafe |
31 | | * - \ref load |
32 | | * - \ref load_unsafe |
33 | | * |
34 | | * As name suggest, crow uses [mustache](https://en.wikipedia.org/wiki/Mustache_(template_system)) |
35 | | * as main template rendering system. |
36 | | * |
37 | | * You may be interested in taking a look at the [Templating guide |
38 | | * page](https://crowcpp.org/master/guides/templating/). |
39 | | */ |
40 | | namespace mustache |
41 | | { |
42 | | using context = json::wvalue; |
43 | | |
44 | | template_t load(const std::string& filename); |
45 | | |
46 | | /** |
47 | | * \class invalid_template_exception |
48 | | * \brief Represents compilation error of an template. Throwed |
49 | | * specially at mustache compile time. |
50 | | */ |
51 | | class invalid_template_exception : public std::exception |
52 | | { |
53 | | public: |
54 | | invalid_template_exception(const std::string& msg_): |
55 | 817 | msg("crow::mustache error: " + msg_) |
56 | 817 | {} |
57 | | virtual const char* what() const throw() override |
58 | 0 | { |
59 | 0 | return msg.c_str(); |
60 | 0 | } |
61 | | std::string msg; |
62 | | }; |
63 | | |
64 | | /** |
65 | | * \struct rendered_template |
66 | | * \brief Returned object after call the |
67 | | * \ref template_t::render() method. Its intended to be |
68 | | * returned during a **rule declaration**. |
69 | | * |
70 | | * \see \ref CROW_ROUTE |
71 | | * \see \ref CROW_BP_ROUTE |
72 | | */ |
73 | | struct rendered_template : returnable |
74 | | { |
75 | | rendered_template(): |
76 | 0 | returnable("text/html") {} |
77 | | |
78 | | rendered_template(std::string& body): |
79 | 0 | returnable("text/html"), body_(std::move(body)) {} |
80 | | |
81 | | std::string body_; |
82 | | |
83 | | std::string dump() const override |
84 | 0 | { |
85 | 0 | return body_; |
86 | 0 | } |
87 | | }; |
88 | | |
89 | | /** |
90 | | * \enum ActionType |
91 | | * \brief Used in \ref Action to represent different parsing |
92 | | * behaviors. |
93 | | * |
94 | | * \see \ref Action |
95 | | */ |
96 | | enum class ActionType |
97 | | { |
98 | | Ignore, |
99 | | Tag, |
100 | | UnescapeTag, |
101 | | OpenBlock, |
102 | | CloseBlock, |
103 | | ElseBlock, |
104 | | Partial, |
105 | | }; |
106 | | |
107 | | /** |
108 | | * \struct Action |
109 | | * \brief Used during mustache template compilation to |
110 | | * represent parsing actions. |
111 | | * |
112 | | * \see \ref compile |
113 | | * \see \ref template_t |
114 | | */ |
115 | | struct Action |
116 | | { |
117 | | bool has_end_match; |
118 | | char tag_char; |
119 | | int start; |
120 | | int end; |
121 | | int pos; |
122 | | ActionType t; |
123 | | |
124 | | Action(char tag_char_, ActionType t_, size_t start_, size_t end_, size_t pos_ = 0): |
125 | 9.85M | has_end_match(false), tag_char(tag_char_), start(static_cast<int>(start_)), end(static_cast<int>(end_)), pos(static_cast<int>(pos_)), t(t_) |
126 | 9.85M | { |
127 | 9.85M | } |
128 | | |
129 | 8.07M | bool missing_end_pair() const { |
130 | 8.07M | switch (t) |
131 | 8.07M | { |
132 | 3.92M | case ActionType::Ignore: |
133 | 4.14M | case ActionType::Tag: |
134 | 4.16M | case ActionType::UnescapeTag: |
135 | 4.23M | case ActionType::CloseBlock: |
136 | 7.99M | case ActionType::Partial: |
137 | 7.99M | return false; |
138 | | |
139 | | // requires a match |
140 | 10.0k | case ActionType::OpenBlock: |
141 | 75.0k | case ActionType::ElseBlock: |
142 | 75.0k | return !has_end_match; |
143 | | |
144 | 0 | default: |
145 | 0 | throw std::logic_error("invalid type"); |
146 | 8.07M | } |
147 | 8.07M | } |
148 | | }; |
149 | | |
150 | | /** |
151 | | * \class template_t |
152 | | * \brief Compiled mustache template object. |
153 | | * |
154 | | * \warning Use \ref compile instead. |
155 | | */ |
156 | | class template_t |
157 | | { |
158 | | public: |
159 | | template_t(std::string body): |
160 | 2.66M | body_(std::move(body)) |
161 | 2.66M | { |
162 | | // {{ {{# {{/ {{^ {{! {{> {{= |
163 | 2.66M | parse(); |
164 | 2.66M | } |
165 | | |
166 | | private: |
167 | | std::string tag_name(const Action& action) const |
168 | 2.92M | { |
169 | 2.92M | return body_.substr(action.start, action.end - action.start); |
170 | 2.92M | } |
171 | | auto find_context(const std::string& name, const std::vector<const context*>& stack, bool shouldUseOnlyFirstStackValue = false) const -> std::pair<bool, const context&> |
172 | 0 | { |
173 | 0 | if (name == ".") |
174 | 0 | { |
175 | 0 | return {true, *stack.back()}; |
176 | 0 | } |
177 | 0 | static json::wvalue empty_str; |
178 | 0 | empty_str = ""; |
179 | 0 |
|
180 | 0 | size_t dotPosition = name.find("."); |
181 | 0 | if (dotPosition == std::string::npos) |
182 | 0 | { |
183 | 0 | for (auto it = stack.rbegin(); it != stack.rend(); ++it) |
184 | 0 | { |
185 | 0 | if ((*it)->t() == json::type::Object) |
186 | 0 | { |
187 | 0 | if ((*it)->count(name)) |
188 | 0 | return {true, (**it)[name]}; |
189 | 0 | } |
190 | 0 | } |
191 | 0 | } |
192 | 0 | else |
193 | 0 | { |
194 | 0 | std::vector<size_t> dotPositions; |
195 | 0 | dotPositions.push_back(std::string::npos); |
196 | 0 | while (dotPosition != std::string::npos) |
197 | 0 | { |
198 | 0 | dotPositions.push_back(dotPosition); |
199 | 0 | dotPosition = name.find(".", dotPosition + 1); |
200 | 0 | } |
201 | 0 | dotPositions.push_back(name.size()); |
202 | 0 | std::vector<std::string> names; |
203 | 0 | names.reserve(dotPositions.size() - 1); |
204 | 0 | for (size_t i = 1; i < dotPositions.size(); i++) |
205 | 0 | names.emplace_back(name.substr(dotPositions[i - 1] + 1, dotPositions[i] - dotPositions[i - 1] - 1)); |
206 | 0 |
|
207 | 0 | for (auto it = stack.rbegin(); it != stack.rend(); ++it) |
208 | 0 | { |
209 | 0 | const context* view = *it; |
210 | 0 | bool found = true; |
211 | 0 | for (auto jt = names.begin(); jt != names.end(); ++jt) |
212 | 0 | { |
213 | 0 | if (view->t() == json::type::Object && |
214 | 0 | view->count(*jt)) |
215 | 0 | { |
216 | 0 | view = &(*view)[*jt]; |
217 | 0 | } |
218 | 0 | else |
219 | 0 | { |
220 | 0 | if (shouldUseOnlyFirstStackValue) |
221 | 0 | { |
222 | 0 | return {false, empty_str}; |
223 | 0 | } |
224 | 0 | found = false; |
225 | 0 | break; |
226 | 0 | } |
227 | 0 | } |
228 | 0 | if (found) |
229 | 0 | return {true, *view}; |
230 | 0 | } |
231 | 0 | } |
232 | 0 |
|
233 | 0 | return {false, empty_str}; |
234 | 0 | } |
235 | | |
236 | | void escape(const std::string& in, std::string& out) const |
237 | 0 | { |
238 | 0 | out.reserve(out.size() + in.size()); |
239 | 0 | for (auto it = in.begin(); it != in.end(); ++it) |
240 | 0 | { |
241 | 0 | switch (*it) |
242 | 0 | { |
243 | 0 | case '&': out += "&"; break; |
244 | 0 | case '<': out += "<"; break; |
245 | 0 | case '>': out += ">"; break; |
246 | 0 | case '"': out += """; break; |
247 | 0 | case '\'': out += "'"; break; |
248 | 0 | case '/': out += "/"; break; |
249 | 0 | case '`': out += "`"; break; |
250 | 0 | case '=': out += "="; break; |
251 | 0 | default: out += *it; break; |
252 | 0 | } |
253 | 0 | } |
254 | 0 | } |
255 | | |
256 | | bool isTagInsideObjectBlock(const int& current, const std::vector<const context*>& stack) const |
257 | 0 | { |
258 | 0 | int openedBlock = 0; |
259 | 0 | for (int i = current; i > 0; --i) |
260 | 0 | { |
261 | 0 | auto& action = actions_[i - 1]; |
262 | 0 |
|
263 | 0 | if (action.t == ActionType::OpenBlock) |
264 | 0 | { |
265 | 0 | if (openedBlock == 0 && (*stack.rbegin())->t() == json::type::Object) |
266 | 0 | { |
267 | 0 | return true; |
268 | 0 | } |
269 | 0 | --openedBlock; |
270 | 0 | } |
271 | 0 | else if (action.t == ActionType::CloseBlock) |
272 | 0 | { |
273 | 0 | ++openedBlock; |
274 | 0 | } |
275 | 0 | } |
276 | 0 |
|
277 | 0 | return false; |
278 | 0 | } |
279 | | |
280 | | void render_internal(int actionBegin, int actionEnd, std::vector<const context*>& stack, std::string& out, int indent) const |
281 | 2.66M | { |
282 | 2.66M | int current = actionBegin; |
283 | | |
284 | 2.66M | if (indent) |
285 | 4.09k | out.insert(out.size(), indent, ' '); |
286 | | |
287 | 6.85M | while (current < actionEnd) |
288 | 4.18M | { |
289 | 4.18M | auto& fragment = fragments_[current]; |
290 | 4.18M | auto& action = actions_[current]; |
291 | 4.18M | render_fragment(fragment, indent, out); |
292 | 4.18M | switch (action.t) |
293 | 4.18M | { |
294 | 1.22M | case ActionType::Ignore: |
295 | | // do nothing |
296 | 1.22M | break; |
297 | 2.66M | case ActionType::Partial: |
298 | 2.66M | { |
299 | 2.66M | std::string partial_name = tag_name(action); |
300 | 2.66M | auto partial_templ = load(partial_name); |
301 | 2.66M | int partial_indent = action.pos; |
302 | 2.66M | partial_templ.render_internal(0, partial_templ.fragments_.size() - 1, stack, out, partial_indent ? indent + partial_indent : 0); |
303 | 2.66M | } |
304 | 2.66M | break; |
305 | 12.5k | case ActionType::UnescapeTag: |
306 | 226k | case ActionType::Tag: |
307 | 226k | { |
308 | 226k | bool shouldUseOnlyFirstStackValue = false; |
309 | 226k | if (isTagInsideObjectBlock(current, stack)) |
310 | 75.4k | { |
311 | 75.4k | shouldUseOnlyFirstStackValue = true; |
312 | 75.4k | } |
313 | 226k | auto optional_ctx = find_context(tag_name(action), stack, shouldUseOnlyFirstStackValue); |
314 | 226k | auto& ctx = optional_ctx.second; |
315 | 226k | switch (ctx.t()) |
316 | 226k | { |
317 | 0 | case json::type::False: |
318 | 0 | case json::type::True: |
319 | 0 | case json::type::Number: |
320 | 0 | out += ctx.dump(); |
321 | 0 | break; |
322 | 226k | case json::type::String: |
323 | 226k | if (action.t == ActionType::Tag) |
324 | 213k | escape(ctx.s, out); |
325 | 12.5k | else |
326 | 12.5k | out += ctx.s; |
327 | 226k | break; |
328 | 0 | case json::type::Function: |
329 | 0 | { |
330 | 0 | std::string execute_result = ctx.execute(); |
331 | 0 | while (execute_result.find("{{") != std::string::npos) |
332 | 0 | { |
333 | 0 | template_t result_plug(execute_result); |
334 | 0 | execute_result = result_plug.render_string(*(stack[0])); |
335 | 0 | } |
336 | |
|
337 | 0 | if (action.t == ActionType::Tag) |
338 | 0 | escape(execute_result, out); |
339 | 0 | else |
340 | 0 | out += execute_result; |
341 | 0 | } |
342 | 0 | break; |
343 | 60 | default: |
344 | 60 | throw std::runtime_error("not implemented tag type" + utility::lexical_cast<std::string>(static_cast<int>(ctx.t()))); |
345 | 226k | } |
346 | 226k | } |
347 | 226k | break; |
348 | 226k | case ActionType::ElseBlock: |
349 | 33.6k | { |
350 | 33.6k | static context nullContext; |
351 | 33.6k | auto optional_ctx = find_context(tag_name(action), stack); |
352 | 33.6k | if (!optional_ctx.first) |
353 | 30.8k | { |
354 | 30.8k | stack.emplace_back(&nullContext); |
355 | 30.8k | break; |
356 | 30.8k | } |
357 | | |
358 | 2.72k | auto& ctx = optional_ctx.second; |
359 | 2.72k | switch (ctx.t()) |
360 | 2.72k | { |
361 | 0 | case json::type::List: |
362 | 0 | if (ctx.l && !ctx.l->empty()) |
363 | 0 | current = action.pos; |
364 | 0 | else |
365 | 0 | stack.emplace_back(&nullContext); |
366 | 0 | break; |
367 | 0 | case json::type::False: |
368 | 1.98k | case json::type::Null: |
369 | 1.98k | stack.emplace_back(&nullContext); |
370 | 1.98k | break; |
371 | 740 | default: |
372 | 740 | current = action.pos; |
373 | 740 | break; |
374 | 2.72k | } |
375 | 2.72k | break; |
376 | 2.72k | } |
377 | 7.80k | case ActionType::OpenBlock: |
378 | 7.80k | { |
379 | 7.80k | auto optional_ctx = find_context(tag_name(action), stack); |
380 | 7.80k | if (!optional_ctx.first) |
381 | 2.67k | { |
382 | 2.67k | current = action.pos; |
383 | 2.67k | break; |
384 | 2.67k | } |
385 | | |
386 | 5.12k | auto& ctx = optional_ctx.second; |
387 | 5.12k | switch (ctx.t()) |
388 | 5.12k | { |
389 | 0 | case json::type::List: |
390 | 0 | if (ctx.l) |
391 | 0 | for (auto it = ctx.l->begin(); it != ctx.l->end(); ++it) |
392 | 0 | { |
393 | 0 | stack.push_back(&*it); |
394 | 0 | render_internal(current + 1, action.pos, stack, out, indent); |
395 | 0 | stack.pop_back(); |
396 | 0 | } |
397 | 0 | current = action.pos; |
398 | 0 | break; |
399 | 0 | case json::type::Number: |
400 | 3.12k | case json::type::String: |
401 | 4.07k | case json::type::Object: |
402 | 4.07k | case json::type::True: |
403 | 4.07k | stack.push_back(&ctx); |
404 | 4.07k | break; |
405 | 0 | case json::type::False: |
406 | 1.04k | case json::type::Null: |
407 | 1.04k | current = action.pos; |
408 | 1.04k | break; |
409 | 0 | default: |
410 | 0 | throw std::runtime_error("{{#: not implemented context type: " + utility::lexical_cast<std::string>(static_cast<int>(ctx.t()))); |
411 | 0 | break; |
412 | 5.12k | } |
413 | 5.12k | break; |
414 | 5.12k | } |
415 | 33.6k | case ActionType::CloseBlock: |
416 | 33.6k | stack.pop_back(); |
417 | 33.6k | break; |
418 | 0 | default: |
419 | 0 | throw std::runtime_error("not implemented " + utility::lexical_cast<std::string>(static_cast<int>(action.t))); |
420 | 4.18M | } |
421 | 4.18M | current++; |
422 | 4.18M | } |
423 | 2.66M | auto& fragment = fragments_[actionEnd]; |
424 | 2.66M | render_fragment(fragment, indent, out); |
425 | 2.66M | } |
426 | | void render_fragment(const std::pair<int, int> fragment, int indent, std::string& out) const |
427 | 6.85M | { |
428 | 6.85M | if (indent) |
429 | 4.09k | { |
430 | 4.09k | for (int i = fragment.first; i < fragment.second; i++) |
431 | 0 | { |
432 | 0 | out += body_[i]; |
433 | 0 | if (body_[i] == '\n' && i + 1 != static_cast<int>(body_.size())) |
434 | 0 | out.insert(out.size(), indent, ' '); |
435 | 0 | } |
436 | 4.09k | } |
437 | 6.84M | else |
438 | 6.84M | out.insert(out.size(), body_, fragment.first, fragment.second - fragment.first); |
439 | 6.85M | } |
440 | | |
441 | | public: |
442 | | /// Output a returnable template from this mustache template |
443 | | rendered_template render() const |
444 | 0 | { |
445 | 0 | context empty_ctx; |
446 | 0 | std::vector<const context*> stack; |
447 | 0 | stack.emplace_back(&empty_ctx); |
448 | 0 |
|
449 | 0 | std::string ret; |
450 | 0 | render_internal(0, fragments_.size() - 1, stack, ret, 0); |
451 | 0 | return rendered_template(ret); |
452 | 0 | } |
453 | | |
454 | | /// Apply the values from the context provided and output a returnable template from this mustache template |
455 | | rendered_template render(const context& ctx) const |
456 | 0 | { |
457 | 0 | std::vector<const context*> stack; |
458 | 0 | stack.emplace_back(&ctx); |
459 | 0 |
|
460 | 0 | std::string ret; |
461 | 0 | render_internal(0, fragments_.size() - 1, stack, ret, 0); |
462 | 0 | return rendered_template(ret); |
463 | 0 | } |
464 | | |
465 | | /// Apply the values from the context provided and output a returnable template from this mustache template |
466 | | rendered_template render(const context&& ctx) const |
467 | 0 | { |
468 | 0 | return render(ctx); |
469 | 0 | } |
470 | | |
471 | | /// Output a returnable template from this mustache template |
472 | | std::string render_string() const |
473 | 0 | { |
474 | 0 | context empty_ctx; |
475 | 0 | std::vector<const context*> stack; |
476 | 0 | stack.emplace_back(&empty_ctx); |
477 | 0 |
|
478 | 0 | std::string ret; |
479 | 0 | render_internal(0, fragments_.size() - 1, stack, ret, 0); |
480 | 0 | return ret; |
481 | 0 | } |
482 | | |
483 | | /// Apply the values from the context provided and output a returnable template from this mustache template |
484 | | std::string render_string(const context& ctx) const |
485 | 4.28k | { |
486 | 4.28k | std::vector<const context*> stack; |
487 | 4.28k | stack.emplace_back(&ctx); |
488 | | |
489 | 4.28k | std::string ret; |
490 | 4.28k | render_internal(0, fragments_.size() - 1, stack, ret, 0); |
491 | 4.28k | return ret; |
492 | 4.28k | } |
493 | | |
494 | | private: |
495 | | void parse() |
496 | 2.66M | { |
497 | 2.66M | std::string tag_open = "{{"; |
498 | 2.66M | std::string tag_close = "}}"; |
499 | | |
500 | 2.66M | std::vector<int> blockPositions; |
501 | | |
502 | 2.66M | size_t current = 0; |
503 | 9.85M | while (1) |
504 | 9.85M | { |
505 | 9.85M | size_t idx = body_.find(tag_open, current); |
506 | 9.85M | if (idx == body_.npos) |
507 | 2.66M | { |
508 | 2.66M | fragments_.emplace_back(static_cast<int>(current), static_cast<int>(body_.size())); |
509 | 2.66M | actions_.emplace_back('!', ActionType::Ignore, 0, 0); |
510 | 2.66M | break; |
511 | 2.66M | } |
512 | 7.19M | fragments_.emplace_back(static_cast<int>(current), static_cast<int>(idx)); |
513 | | |
514 | 7.19M | idx += tag_open.size(); |
515 | 7.19M | size_t endIdx = body_.find(tag_close, idx); |
516 | 7.19M | if (endIdx == idx) |
517 | 12 | { |
518 | 12 | throw invalid_template_exception("empty tag is not allowed"); |
519 | 12 | } |
520 | 7.19M | if (endIdx == body_.npos) |
521 | 179 | { |
522 | | // error, no matching tag |
523 | 179 | throw invalid_template_exception("not matched opening tag"); |
524 | 179 | } |
525 | 7.19M | current = endIdx + tag_close.size(); |
526 | 7.19M | char tag_char = body_[idx]; |
527 | 7.19M | switch (tag_char) |
528 | 7.19M | { |
529 | 687k | case '#': |
530 | 687k | idx++; |
531 | 688k | while (body_[idx] == ' ') |
532 | 461 | idx++; |
533 | 688k | while (body_[endIdx - 1] == ' ') |
534 | 653 | endIdx--; |
535 | 687k | blockPositions.emplace_back(static_cast<int>(actions_.size())); |
536 | 687k | actions_.emplace_back(tag_char, ActionType::OpenBlock, idx, endIdx); |
537 | 687k | break; |
538 | 160k | case '/': |
539 | 160k | idx++; |
540 | 161k | while (body_[idx] == ' ') |
541 | 706 | idx++; |
542 | 161k | while (body_[endIdx - 1] == ' ') |
543 | 697 | endIdx--; |
544 | 160k | { |
545 | 160k | if (blockPositions.empty()) |
546 | 27 | { |
547 | 27 | throw invalid_template_exception( |
548 | 27 | std::string("unexpected closing tag: ") |
549 | 27 | + body_.substr(idx, endIdx - idx) |
550 | 27 | ); |
551 | 27 | } |
552 | 160k | auto& matched = actions_[blockPositions.back()]; |
553 | 160k | if (body_.compare(idx, endIdx - idx, |
554 | 160k | body_, matched.start, matched.end - matched.start) != 0) |
555 | 198 | { |
556 | 198 | throw invalid_template_exception( |
557 | 198 | std::string("not matched {{") |
558 | 198 | + matched.tag_char |
559 | 198 | + "{{/ pair: " |
560 | 198 | + body_.substr(matched.start, matched.end - matched.start) + ", " |
561 | 198 | + body_.substr(idx, endIdx - idx) |
562 | 198 | ); |
563 | 198 | } |
564 | 160k | matched.pos = static_cast<int>(actions_.size()); |
565 | 160k | matched.has_end_match = true; |
566 | 160k | } |
567 | 0 | actions_.emplace_back(tag_char, ActionType::CloseBlock, idx, endIdx, blockPositions.back()); |
568 | 160k | blockPositions.pop_back(); |
569 | 160k | break; |
570 | 163k | case '^': |
571 | 163k | idx++; |
572 | 164k | while (body_[idx] == ' ') |
573 | 722 | idx++; |
574 | 164k | while (body_[endIdx - 1] == ' ') |
575 | 818 | endIdx--; |
576 | 163k | blockPositions.emplace_back(static_cast<int>(actions_.size())); |
577 | 163k | actions_.emplace_back(tag_char, ActionType::ElseBlock, idx, endIdx); |
578 | 163k | break; |
579 | 1.52M | case '!': |
580 | | // do nothing action |
581 | 1.52M | actions_.emplace_back(tag_char, ActionType::Ignore, idx + 1, endIdx); |
582 | 1.52M | break; |
583 | 4.35M | case '>': // partial |
584 | 4.35M | idx++; |
585 | 4.52M | while (body_[idx] == ' ') |
586 | 174k | idx++; |
587 | 4.44M | while (body_[endIdx - 1] == ' ') |
588 | 95.6k | endIdx--; |
589 | 4.35M | actions_.emplace_back(tag_char, ActionType::Partial, idx, endIdx); |
590 | 4.35M | break; |
591 | 6.88k | case '{': |
592 | 6.88k | if (tag_open != "{{" || tag_close != "}}") |
593 | 77 | throw invalid_template_exception("cannot use triple mustache when delimiter changed"); |
594 | | |
595 | 6.80k | idx++; |
596 | 6.80k | if (body_[endIdx + 2] != '}') |
597 | 18 | { |
598 | 18 | throw invalid_template_exception("{{{: }}} not matched"); |
599 | 18 | } |
600 | 8.41k | while (body_[idx] == ' ') |
601 | 1.61k | idx++; |
602 | 11.3k | while (body_[endIdx - 1] == ' ') |
603 | 4.57k | endIdx--; |
604 | 6.79k | actions_.emplace_back(tag_char, ActionType::UnescapeTag, idx, endIdx); |
605 | 6.79k | current++; |
606 | 6.79k | break; |
607 | 27.2k | case '&': |
608 | 27.2k | idx++; |
609 | 33.4k | while (body_[idx] == ' ') |
610 | 6.20k | idx++; |
611 | 33.2k | while (body_[endIdx - 1] == ' ') |
612 | 5.96k | endIdx--; |
613 | 27.2k | actions_.emplace_back(tag_char, ActionType::UnescapeTag, idx, endIdx); |
614 | 27.2k | break; |
615 | 22.6k | case '=': |
616 | | // tag itself is no-op |
617 | 22.6k | idx++; |
618 | 22.6k | actions_.emplace_back(tag_char, ActionType::Ignore, idx, endIdx); |
619 | 22.6k | endIdx--; |
620 | 22.6k | if (body_[endIdx] != '=') |
621 | 31 | throw invalid_template_exception("{{=: not matching = tag: " + body_.substr(idx, endIdx - idx)); |
622 | 22.5k | endIdx--; |
623 | 1.07M | while (body_[idx] == ' ') |
624 | 1.05M | idx++; |
625 | 1.07M | while (body_[endIdx] == ' ') |
626 | 1.05M | endIdx--; |
627 | 22.5k | endIdx++; |
628 | 22.5k | { |
629 | 22.5k | bool succeeded = false; |
630 | 10.8M | for (size_t i = idx; i < endIdx; i++) |
631 | 10.8M | { |
632 | 10.8M | if (body_[i] == ' ') |
633 | 22.5k | { |
634 | 22.5k | tag_open = body_.substr(idx, i - idx); |
635 | 46.0k | while (body_[i] == ' ') |
636 | 23.5k | i++; |
637 | 22.5k | tag_close = body_.substr(i, endIdx - i); |
638 | 22.5k | if (tag_open.empty()) |
639 | 0 | throw invalid_template_exception("{{=: empty open tag"); |
640 | 22.5k | if (tag_close.empty()) |
641 | 0 | throw invalid_template_exception("{{=: empty close tag"); |
642 | | |
643 | 22.5k | if (tag_close.find(" ") != tag_close.npos) |
644 | 75 | throw invalid_template_exception("{{=: invalid open/close tag: " + tag_open + " " + tag_close); |
645 | 22.4k | succeeded = true; |
646 | 22.4k | break; |
647 | 22.5k | } |
648 | 10.8M | } |
649 | 22.5k | if (!succeeded) |
650 | 75 | throw invalid_template_exception("{{=: cannot find space between new open/close tags"); |
651 | 22.5k | } |
652 | 22.4k | break; |
653 | 251k | default: |
654 | | // normal tag case; |
655 | 437k | while (body_[idx] == ' ') |
656 | 185k | idx++; |
657 | 444k | while (body_[endIdx - 1] == ' ') |
658 | 193k | endIdx--; |
659 | 251k | actions_.emplace_back(tag_char, ActionType::Tag, idx, endIdx); |
660 | 251k | break; |
661 | 7.19M | } |
662 | 7.19M | } |
663 | | |
664 | | // ensure no unmatched tags |
665 | 10.7M | for (int i = 0; i < static_cast<int>(actions_.size()); i++) |
666 | 8.07M | { |
667 | 8.07M | if (actions_[i].missing_end_pair()) |
668 | 125 | { |
669 | 125 | throw invalid_template_exception( |
670 | 125 | std::string("open tag has no matching end tag {{") |
671 | 125 | + actions_[i].tag_char |
672 | 125 | + " {{/ pair: " |
673 | 125 | + body_.substr(actions_[i].start, actions_[i].end - actions_[i].start) |
674 | 125 | ); |
675 | 125 | } |
676 | 8.07M | } |
677 | | |
678 | | // removing standalones |
679 | 7.95M | for (int i = static_cast<int>(actions_.size()) - 2; i >= 0; i--) |
680 | 5.29M | { |
681 | 5.29M | if (actions_[i].t == ActionType::Tag || actions_[i].t == ActionType::UnescapeTag) |
682 | 238k | continue; |
683 | 5.05M | auto& fragment_before = fragments_[i]; |
684 | 5.05M | auto& fragment_after = fragments_[i + 1]; |
685 | 5.05M | bool is_last_action = i == static_cast<int>(actions_.size()) - 2; |
686 | 5.05M | bool all_space_before = true; |
687 | 5.05M | int j, k; |
688 | 6.97M | for (j = fragment_before.second - 1; j >= fragment_before.first; j--) |
689 | 2.24M | { |
690 | 2.24M | if (body_[j] != ' ') |
691 | 320k | { |
692 | 320k | all_space_before = false; |
693 | 320k | break; |
694 | 320k | } |
695 | 2.24M | } |
696 | 5.05M | if (all_space_before && i > 0) |
697 | 4.73M | continue; |
698 | 323k | if (!all_space_before && body_[j] != '\n') |
699 | 191k | continue; |
700 | 132k | bool all_space_after = true; |
701 | 143k | for (k = fragment_after.first; k < static_cast<int>(body_.size()) && k < fragment_after.second; k++) |
702 | 139k | { |
703 | 139k | if (body_[k] != ' ') |
704 | 128k | { |
705 | 128k | all_space_after = false; |
706 | 128k | break; |
707 | 128k | } |
708 | 139k | } |
709 | 132k | if (all_space_after && !is_last_action) |
710 | 3.16k | continue; |
711 | 129k | if (!all_space_after && |
712 | 128k | !( |
713 | 128k | body_[k] == '\n' || |
714 | 118k | (body_[k] == '\r' && |
715 | 99.7k | k + 1 < static_cast<int>(body_.size()) && |
716 | 99.7k | body_[k + 1] == '\n'))) |
717 | 34.0k | continue; |
718 | 94.9k | if (actions_[i].t == ActionType::Partial) |
719 | 93.1k | { |
720 | 93.1k | actions_[i].pos = fragment_before.second - j - 1; |
721 | 93.1k | } |
722 | 94.9k | fragment_before.second = j + 1; |
723 | 94.9k | if (!all_space_after) |
724 | 94.1k | { |
725 | 94.1k | if (body_[k] == '\n') |
726 | 9.48k | k++; |
727 | 84.7k | else |
728 | 84.7k | k += 2; |
729 | 94.1k | fragment_after.first = k; |
730 | 94.1k | } |
731 | 94.9k | } |
732 | 2.66M | } |
733 | | |
734 | | std::vector<std::pair<int, int>> fragments_; |
735 | | std::vector<Action> actions_; |
736 | | std::string body_; |
737 | | }; |
738 | | |
739 | | /// \brief The function that compiles a source into a mustache |
740 | | /// template. |
741 | | inline template_t compile(const std::string& body) |
742 | 2.66M | { |
743 | 2.66M | return template_t(body); |
744 | 2.66M | } |
745 | | |
746 | | namespace detail |
747 | | { |
748 | | inline std::string& get_template_base_directory_ref() |
749 | 2.66M | { |
750 | 2.66M | static std::string template_base_directory = "templates"; |
751 | 2.66M | return template_base_directory; |
752 | 2.66M | } |
753 | | |
754 | | /// A base directory not related to any blueprint |
755 | | inline std::string& get_global_template_base_directory_ref() |
756 | 0 | { |
757 | 0 | static std::string template_base_directory = "templates"; |
758 | 0 | return template_base_directory; |
759 | 0 | } |
760 | | } // namespace detail |
761 | | |
762 | | /// \brief The default way that \ref load, \ref load_unsafe, |
763 | | /// \ref load_text and \ref load_text_unsafe use to read the |
764 | | /// contents of a file. |
765 | | inline std::string default_loader(const std::string& filename) |
766 | 2.66M | { |
767 | 2.66M | std::string path = detail::get_template_base_directory_ref(); |
768 | 2.66M | std::ifstream inf(utility::join_path(path, filename)); |
769 | 2.66M | if (!inf) |
770 | 2.66M | { |
771 | 2.66M | CROW_LOG_WARNING << "Template \"" << filename << "\" not found."; |
772 | 2.66M | return {}; |
773 | 2.66M | } |
774 | 0 | return {std::istreambuf_iterator<char>(inf), std::istreambuf_iterator<char>()}; |
775 | 2.66M | } |
776 | | |
777 | | namespace detail |
778 | | { |
779 | | inline std::function<std::string(std::string)>& get_loader_ref() |
780 | 2.66M | { |
781 | 2.66M | static std::function<std::string(std::string)> loader = default_loader; |
782 | 2.66M | return loader; |
783 | 2.66M | } |
784 | | } // namespace detail |
785 | | |
786 | | /// \brief Defines the templates directory path at **route |
787 | | /// level**. By default is `templates/`. |
788 | | inline void set_base(const std::string& path) |
789 | 0 | { |
790 | 0 | auto& base = detail::get_template_base_directory_ref(); |
791 | 0 | base = path; |
792 | 0 | if (base.back() != '\\' && |
793 | 0 | base.back() != '/') |
794 | 0 | { |
795 | 0 | base += '/'; |
796 | 0 | } |
797 | 0 | } |
798 | | |
799 | | /// \brief Defines the templates directory path at **global |
800 | | /// level**. By default is `templates/`. |
801 | | inline void set_global_base(const std::string& path) |
802 | 0 | { |
803 | 0 | auto& base = detail::get_global_template_base_directory_ref(); |
804 | 0 | base = path; |
805 | 0 | if (base.back() != '\\' && |
806 | 0 | base.back() != '/') |
807 | 0 | { |
808 | 0 | base += '/'; |
809 | 0 | } |
810 | 0 | } |
811 | | |
812 | | /// \brief Change the way that \ref load, \ref load_unsafe, |
813 | | /// \ref load_text and \ref load_text_unsafe reads a file. |
814 | | /// |
815 | | /// By default, the previously mentioned functions load files |
816 | | /// using \ref default_loader, that only reads a file and |
817 | | /// returns a std::string. |
818 | | inline void set_loader(std::function<std::string(std::string)> loader) |
819 | 0 | { |
820 | 0 | detail::get_loader_ref() = std::move(loader); |
821 | 0 | } |
822 | | |
823 | | /// \brief Open, read and sanitize a file but returns a |
824 | | /// std::string without a previous rendering process. |
825 | | /// |
826 | | /// Except for the **sanitize process** this function does the |
827 | | /// almost the same thing that \ref load_text_unsafe. |
828 | | inline std::string load_text(const std::string& filename) |
829 | 0 | { |
830 | 0 | std::string filename_sanitized(filename); |
831 | 0 | utility::sanitize_filename(filename_sanitized); |
832 | 0 | return detail::get_loader_ref()(filename_sanitized); |
833 | 0 | } |
834 | | |
835 | | /// \brief Open and read a file but returns a std::string |
836 | | /// without a previous rendering process. |
837 | | /// |
838 | | /// This function is more like a helper to reduce code like |
839 | | /// this... |
840 | | /// |
841 | | /// ```cpp |
842 | | /// std::ifstream file("home.html"); |
843 | | /// return std::string({std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()}); |
844 | | /// ``` |
845 | | /// |
846 | | /// ... Into this... |
847 | | /// |
848 | | /// ```cpp |
849 | | /// return load("home.html"); |
850 | | /// ``` |
851 | | /// |
852 | | /// \warning Usually \ref load_text is more recommended to use |
853 | | /// instead because it may prevent some [XSS Attacks](https://en.wikipedia.org/wiki/Cross-site_scripting). |
854 | | /// **Never blindly trust your users!** |
855 | | inline std::string load_text_unsafe(const std::string& filename) |
856 | 0 | { |
857 | 0 | return detail::get_loader_ref()(filename); |
858 | 0 | } |
859 | | |
860 | | /// \brief Open, read and renders a file using a mustache |
861 | | /// compiler. It also sanitize the input before compilation. |
862 | | inline template_t load(const std::string& filename) |
863 | 2.66M | { |
864 | 2.66M | std::string filename_sanitized(filename); |
865 | 2.66M | utility::sanitize_filename(filename_sanitized); |
866 | 2.66M | return compile(detail::get_loader_ref()(filename_sanitized)); |
867 | 2.66M | } |
868 | | |
869 | | /// \brief Open, read and renders a file using a mustache |
870 | | /// compiler. But it **do not** sanitize the input before |
871 | | /// compilation. |
872 | | /// |
873 | | /// \warning Usually \ref load is more recommended to use |
874 | | /// instead because it may prevent some [XSS Attacks](https://en.wikipedia.org/wiki/Cross-site_scripting). |
875 | | /// **Never blindly trust your users!** |
876 | | inline template_t load_unsafe(const std::string& filename) |
877 | 0 | { |
878 | 0 | return compile(detail::get_loader_ref()(filename)); |
879 | 0 | } |
880 | | } // namespace mustache |
881 | | } // namespace crow |