diff options
Diffstat (limited to 'include/crow/mustache.h')
-rw-r--r-- | include/crow/mustache.h | 568 |
1 files changed, 568 insertions, 0 deletions
diff --git a/include/crow/mustache.h b/include/crow/mustache.h new file mode 100644 index 0000000..bfb9f14 --- /dev/null +++ b/include/crow/mustache.h @@ -0,0 +1,568 @@ +#pragma once +#include <string> +#include <vector> +#include <fstream> +#include <iterator> +#include <functional> +#include "crow/json.h" +namespace crow +{ + namespace mustache + { + using context = json::wvalue; + + template_t load(const std::string& filename); + + class invalid_template_exception : public std::exception + { + public: + invalid_template_exception(const std::string& msg) + : msg("crow::mustache error: " + msg) + { + } + virtual const char* what() const throw() + { + return msg.c_str(); + } + std::string msg; + }; + + enum class ActionType + { + Ignore, + Tag, + UnescapeTag, + OpenBlock, + CloseBlock, + ElseBlock, + Partial, + }; + + struct Action + { + int start; + int end; + int pos; + ActionType t; + Action(ActionType t, int start, int end, int pos = 0) + : start(start), end(end), pos(pos), t(t) + {} + }; + + class template_t + { + public: + template_t(std::string body) + : body_(std::move(body)) + { + // {{ {{# {{/ {{^ {{! {{> {{= + parse(); + } + + private: + std::string tag_name(const Action& action) + { + return body_.substr(action.start, action.end - action.start); + } + auto find_context(const std::string& name, const std::vector<context*>& stack)->std::pair<bool, context&> + { + if (name == ".") + { + return {true, *stack.back()}; + } + int dotPosition = name.find("."); + if (dotPosition == (int)name.npos) + { + for(auto it = stack.rbegin(); it != stack.rend(); ++it) + { + if ((*it)->t() == json::type::Object) + { + if ((*it)->count(name)) + return {true, (**it)[name]}; + } + } + } + else + { + std::vector<int> dotPositions; + dotPositions.push_back(-1); + while(dotPosition != (int)name.npos) + { + dotPositions.push_back(dotPosition); + dotPosition = name.find(".", dotPosition+1); + } + dotPositions.push_back(name.size()); + std::vector<std::string> names; + names.reserve(dotPositions.size()-1); + for(int i = 1; i < (int)dotPositions.size(); i ++) + names.emplace_back(name.substr(dotPositions[i-1]+1, dotPositions[i]-dotPositions[i-1]-1)); + + for(auto it = stack.rbegin(); it != stack.rend(); ++it) + { + context* view = *it; + bool found = true; + for(auto jt = names.begin(); jt != names.end(); ++jt) + { + if (view->t() == json::type::Object && + view->count(*jt)) + { + view = &(*view)[*jt]; + } + else + { + found = false; + break; + } + } + if (found) + return {true, *view}; + } + + } + + static json::wvalue empty_str; + empty_str = ""; + return {false, empty_str}; + } + + void escape(const std::string& in, std::string& out) + { + out.reserve(out.size() + in.size()); + for(auto it = in.begin(); it != in.end(); ++it) + { + switch(*it) + { + case '&': out += "&"; break; + case '<': out += "<"; break; + case '>': out += ">"; break; + case '"': out += """; break; + case '\'': out += "'"; break; + case '/': out += "/"; break; + default: out += *it; break; + } + } + } + + void render_internal(int actionBegin, int actionEnd, std::vector<context*>& stack, std::string& out, int indent) + { + int current = actionBegin; + + if (indent) + out.insert(out.size(), indent, ' '); + + while(current < actionEnd) + { + auto& fragment = fragments_[current]; + auto& action = actions_[current]; + render_fragment(fragment, indent, out); + switch(action.t) + { + case ActionType::Ignore: + // do nothing + break; + case ActionType::Partial: + { + std::string partial_name = tag_name(action); + auto partial_templ = load(partial_name); + int partial_indent = action.pos; + partial_templ.render_internal(0, partial_templ.fragments_.size()-1, stack, out, partial_indent?indent+partial_indent:0); + } + break; + case ActionType::UnescapeTag: + case ActionType::Tag: + { + auto optional_ctx = find_context(tag_name(action), stack); + auto& ctx = optional_ctx.second; + switch(ctx.t()) + { + case json::type::Number: + out += json::dump(ctx); + break; + case json::type::String: + if (action.t == ActionType::Tag) + escape(ctx.s, out); + else + out += ctx.s; + break; + default: + throw std::runtime_error("not implemented tag type" + boost::lexical_cast<std::string>((int)ctx.t())); + } + } + break; + case ActionType::ElseBlock: + { + static context nullContext; + auto optional_ctx = find_context(tag_name(action), stack); + if (!optional_ctx.first) + { + stack.emplace_back(&nullContext); + break; + } + + auto& ctx = optional_ctx.second; + switch(ctx.t()) + { + case json::type::List: + if (ctx.l && !ctx.l->empty()) + current = action.pos; + else + stack.emplace_back(&nullContext); + break; + case json::type::False: + case json::type::Null: + stack.emplace_back(&nullContext); + break; + default: + current = action.pos; + break; + } + break; + } + case ActionType::OpenBlock: + { + auto optional_ctx = find_context(tag_name(action), stack); + if (!optional_ctx.first) + { + current = action.pos; + break; + } + + auto& ctx = optional_ctx.second; + switch(ctx.t()) + { + case json::type::List: + if (ctx.l) + for(auto it = ctx.l->begin(); it != ctx.l->end(); ++it) + { + stack.push_back(&*it); + render_internal(current+1, action.pos, stack, out, indent); + stack.pop_back(); + } + current = action.pos; + break; + case json::type::Number: + case json::type::String: + case json::type::Object: + case json::type::True: + stack.push_back(&ctx); + break; + case json::type::False: + case json::type::Null: + current = action.pos; + break; + default: + throw std::runtime_error("{{#: not implemented context type: " + boost::lexical_cast<std::string>((int)ctx.t())); + break; + } + break; + } + case ActionType::CloseBlock: + stack.pop_back(); + break; + default: + throw std::runtime_error("not implemented " + boost::lexical_cast<std::string>((int)action.t)); + } + current++; + } + auto& fragment = fragments_[actionEnd]; + render_fragment(fragment, indent, out); + } + void render_fragment(const std::pair<int, int> fragment, int indent, std::string& out) + { + if (indent) + { + for(int i = fragment.first; i < fragment.second; i ++) + { + out += body_[i]; + if (body_[i] == '\n' && i+1 != (int)body_.size()) + out.insert(out.size(), indent, ' '); + } + } + else + out.insert(out.size(), body_, fragment.first, fragment.second-fragment.first); + } + public: + std::string render() + { + context empty_ctx; + std::vector<context*> stack; + stack.emplace_back(&empty_ctx); + + std::string ret; + render_internal(0, fragments_.size()-1, stack, ret, 0); + return ret; + } + std::string render(context& ctx) + { + std::vector<context*> stack; + stack.emplace_back(&ctx); + + std::string ret; + render_internal(0, fragments_.size()-1, stack, ret, 0); + return ret; + } + + private: + + void parse() + { + std::string tag_open = "{{"; + std::string tag_close = "}}"; + + std::vector<int> blockPositions; + + size_t current = 0; + while(1) + { + size_t idx = body_.find(tag_open, current); + if (idx == body_.npos) + { + fragments_.emplace_back(current, body_.size()); + actions_.emplace_back(ActionType::Ignore, 0, 0); + break; + } + fragments_.emplace_back(current, idx); + + idx += tag_open.size(); + size_t endIdx = body_.find(tag_close, idx); + if (endIdx == idx) + { + throw invalid_template_exception("empty tag is not allowed"); + } + if (endIdx == body_.npos) + { + // error, no matching tag + throw invalid_template_exception("not matched opening tag"); + } + current = endIdx + tag_close.size(); + switch(body_[idx]) + { + case '#': + idx++; + while(body_[idx] == ' ') idx++; + while(body_[endIdx-1] == ' ') endIdx--; + blockPositions.emplace_back(actions_.size()); + actions_.emplace_back(ActionType::OpenBlock, idx, endIdx); + break; + case '/': + idx++; + while(body_[idx] == ' ') idx++; + while(body_[endIdx-1] == ' ') endIdx--; + { + auto& matched = actions_[blockPositions.back()]; + if (body_.compare(idx, endIdx-idx, + body_, matched.start, matched.end - matched.start) != 0) + { + throw invalid_template_exception("not matched {{# {{/ pair: " + + body_.substr(matched.start, matched.end - matched.start) + ", " + + body_.substr(idx, endIdx-idx)); + } + matched.pos = actions_.size(); + } + actions_.emplace_back(ActionType::CloseBlock, idx, endIdx, blockPositions.back()); + blockPositions.pop_back(); + break; + case '^': + idx++; + while(body_[idx] == ' ') idx++; + while(body_[endIdx-1] == ' ') endIdx--; + blockPositions.emplace_back(actions_.size()); + actions_.emplace_back(ActionType::ElseBlock, idx, endIdx); + break; + case '!': + // do nothing action + actions_.emplace_back(ActionType::Ignore, idx+1, endIdx); + break; + case '>': // partial + idx++; + while(body_[idx] == ' ') idx++; + while(body_[endIdx-1] == ' ') endIdx--; + actions_.emplace_back(ActionType::Partial, idx, endIdx); + break; + case '{': + if (tag_open != "{{" || tag_close != "}}") + throw invalid_template_exception("cannot use triple mustache when delimiter changed"); + + idx ++; + if (body_[endIdx+2] != '}') + { + throw invalid_template_exception("{{{: }}} not matched"); + } + while(body_[idx] == ' ') idx++; + while(body_[endIdx-1] == ' ') endIdx--; + actions_.emplace_back(ActionType::UnescapeTag, idx, endIdx); + current++; + break; + case '&': + idx ++; + while(body_[idx] == ' ') idx++; + while(body_[endIdx-1] == ' ') endIdx--; + actions_.emplace_back(ActionType::UnescapeTag, idx, endIdx); + break; + case '=': + // tag itself is no-op + idx ++; + actions_.emplace_back(ActionType::Ignore, idx, endIdx); + endIdx --; + if (body_[endIdx] != '=') + throw invalid_template_exception("{{=: not matching = tag: "+body_.substr(idx, endIdx-idx)); + endIdx --; + while(body_[idx] == ' ') idx++; + while(body_[endIdx] == ' ') endIdx--; + endIdx++; + { + bool succeeded = false; + for(size_t i = idx; i < endIdx; i++) + { + if (body_[i] == ' ') + { + tag_open = body_.substr(idx, i-idx); + while(body_[i] == ' ') i++; + tag_close = body_.substr(i, endIdx-i); + if (tag_open.empty()) + throw invalid_template_exception("{{=: empty open tag"); + if (tag_close.empty()) + throw invalid_template_exception("{{=: empty close tag"); + + if (tag_close.find(" ") != tag_close.npos) + throw invalid_template_exception("{{=: invalid open/close tag: "+tag_open+" " + tag_close); + succeeded = true; + break; + } + } + if (!succeeded) + throw invalid_template_exception("{{=: cannot find space between new open/close tags"); + } + break; + default: + // normal tag case; + while(body_[idx] == ' ') idx++; + while(body_[endIdx-1] == ' ') endIdx--; + actions_.emplace_back(ActionType::Tag, idx, endIdx); + break; + } + } + + // removing standalones + for(int i = actions_.size()-2; i >= 0; i --) + { + if (actions_[i].t == ActionType::Tag || actions_[i].t == ActionType::UnescapeTag) + continue; + auto& fragment_before = fragments_[i]; + auto& fragment_after = fragments_[i+1]; + bool is_last_action = i == (int)actions_.size()-2; + bool all_space_before = true; + int j, k; + for(j = fragment_before.second-1;j >= fragment_before.first;j--) + { + if (body_[j] != ' ') + { + all_space_before = false; + break; + } + } + if (all_space_before && i > 0) + continue; + if (!all_space_before && body_[j] != '\n') + continue; + bool all_space_after = true; + for(k = fragment_after.first; k < (int)body_.size() && k < fragment_after.second; k ++) + { + if (body_[k] != ' ') + { + all_space_after = false; + break; + } + } + if (all_space_after && !is_last_action) + continue; + if (!all_space_after && + !( + body_[k] == '\n' + || + (body_[k] == '\r' && + k + 1 < (int)body_.size() && + body_[k+1] == '\n'))) + continue; + if (actions_[i].t == ActionType::Partial) + { + actions_[i].pos = fragment_before.second - j - 1; + } + fragment_before.second = j+1; + if (!all_space_after) + { + if (body_[k] == '\n') + k++; + else + k += 2; + fragment_after.first = k; + } + } + } + + std::vector<std::pair<int,int>> fragments_; + std::vector<Action> actions_; + std::string body_; + }; + + inline template_t compile(const std::string& body) + { + return template_t(body); + } + namespace detail + { + inline std::string& get_template_base_directory_ref() + { + static std::string template_base_directory = "templates"; + return template_base_directory; + } + } + + inline std::string default_loader(const std::string& filename) + { + std::string path = detail::get_template_base_directory_ref(); + if (!(path.back() == '/' || path.back() == '\\')) + path += '/'; + path += filename; + std::ifstream inf(path); + if (!inf) + return {}; + return {std::istreambuf_iterator<char>(inf), std::istreambuf_iterator<char>()}; + } + + namespace detail + { + inline std::function<std::string (std::string)>& get_loader_ref() + { + static std::function<std::string (std::string)> loader = default_loader; + return loader; + } + } + + inline void set_base(const std::string& path) + { + auto& base = detail::get_template_base_directory_ref(); + base = path; + if (base.back() != '\\' && + base.back() != '/') + { + base += '/'; + } + } + + inline void set_loader(std::function<std::string(std::string)> loader) + { + detail::get_loader_ref() = std::move(loader); + } + + inline std::string load_text(const std::string& filename) + { + return detail::get_loader_ref()(filename); + } + + inline template_t load(const std::string& filename) + { + return compile(detail::get_loader_ref()(filename)); + } + } +} |