Crow/include/mustache.h

569 lines
24 KiB
C++

#pragma once
#include <string>
#include <vector>
#include <fstream>
#include <iterator>
#include <functional>
#include "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 += "&amp;"; break;
case '<': out += "&lt;"; break;
case '>': out += "&gt;"; break;
case '"': out += "&quot;"; break;
case '\'': out += "&#39;"; break;
case '/': out += "&#x2F;"; 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));
}
}
}