1196 lines
40 KiB
C++
1196 lines
40 KiB
C++
/*
|
|
* Boost Software License - Version 1.0
|
|
*
|
|
* Mustache v4.1
|
|
* Copyright 2015-2020 Kevin Wojniak
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person or organization
|
|
* obtaining a copy of the software and accompanying documentation covered by
|
|
* this license (the "Software") to use, reproduce, display, distribute,
|
|
* execute, and transmit the Software, and to prepare derivative works of the
|
|
* Software, and to permit third-parties to whom the Software is furnished to
|
|
* do so, all subject to the following:
|
|
*
|
|
* The copyright notices in the Software and this entire statement, including
|
|
* the above license grant, this restriction and the following disclaimer,
|
|
* must be included in all copies of the Software, in whole or in part, and
|
|
* all derivative works of the Software, unless such copies or derivative
|
|
* works are solely in the form of machine-executable object code generated by
|
|
* a source language processor.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
|
* SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
|
* FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
|
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
* DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
#ifndef KAINJOW_MUSTACHE_HPP
|
|
#define KAINJOW_MUSTACHE_HPP
|
|
|
|
#include <cassert>
|
|
#include <cctype>
|
|
#include <functional>
|
|
#include <iostream>
|
|
#include <memory>
|
|
#include <sstream>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
namespace kainjow {
|
|
namespace mustache {
|
|
|
|
template<typename string_type>
|
|
string_type
|
|
trim(const string_type &s)
|
|
{
|
|
auto it = s.begin();
|
|
while (it != s.end() && std::isspace(*it)) { it++; }
|
|
auto rit = s.rbegin();
|
|
while (rit.base() != it && std::isspace(*rit)) { rit++; }
|
|
return {it, rit.base()};
|
|
}
|
|
|
|
template<typename string_type>
|
|
string_type
|
|
html_escape(const string_type &s)
|
|
{
|
|
string_type ret;
|
|
ret.reserve(s.size() * 2);
|
|
for (const auto ch : s) {
|
|
switch (ch) {
|
|
case '&':
|
|
ret.append({'&', 'a', 'm', 'p', ';'});
|
|
break;
|
|
case '<':
|
|
ret.append({'&', 'l', 't', ';'});
|
|
break;
|
|
case '>':
|
|
ret.append({'&', 'g', 't', ';'});
|
|
break;
|
|
case '\"':
|
|
ret.append({'&', 'q', 'u', 'o', 't', ';'});
|
|
break;
|
|
case '\'':
|
|
ret.append({'&', 'a', 'p', 'o', 's', ';'});
|
|
break;
|
|
default:
|
|
ret.append(1, ch);
|
|
break;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
template<typename string_type>
|
|
std::vector<string_type>
|
|
split(const string_type &s, typename string_type::value_type delim)
|
|
{
|
|
std::vector<string_type> elems;
|
|
std::basic_stringstream<typename string_type::value_type> ss(s);
|
|
string_type item;
|
|
while (std::getline(ss, item, delim)) { elems.push_back(item); }
|
|
return elems;
|
|
}
|
|
|
|
template<typename string_type>
|
|
class basic_renderer {
|
|
public:
|
|
using type1 = std::function<string_type(const string_type &)>;
|
|
using type2 = std::function<string_type(const string_type &, bool escaped)>;
|
|
|
|
string_type operator()(const string_type &text) const { return type1_(text); }
|
|
|
|
string_type operator()(const string_type &text, bool escaped) const
|
|
{
|
|
return type2_(text, escaped);
|
|
}
|
|
|
|
private:
|
|
basic_renderer(const type1 &t1, const type2 &t2) : type1_(t1), type2_(t2) {}
|
|
|
|
const type1 &type1_;
|
|
const type2 &type2_;
|
|
|
|
template<typename StringType>
|
|
friend class basic_mustache;
|
|
};
|
|
|
|
template<typename string_type>
|
|
class basic_lambda_t {
|
|
public:
|
|
using type1 = std::function<string_type(const string_type &)>;
|
|
using type2 = std::function<string_type(const string_type &,
|
|
const basic_renderer<string_type> &render)>;
|
|
|
|
basic_lambda_t(const type1 &t) : type1_(new type1(t)) {}
|
|
|
|
basic_lambda_t(const type2 &t) : type2_(new type2(t)) {}
|
|
|
|
bool is_type1() const { return static_cast<bool>(type1_); }
|
|
|
|
bool is_type2() const { return static_cast<bool>(type2_); }
|
|
|
|
const type1 &type1_value() const { return *type1_; }
|
|
|
|
const type2 &type2_value() const { return *type2_; }
|
|
|
|
// Copying
|
|
basic_lambda_t(const basic_lambda_t &l)
|
|
{
|
|
if (l.type1_) {
|
|
type1_.reset(new type1(*l.type1_));
|
|
} else if (l.type2_) {
|
|
type2_.reset(new type2(*l.type2_));
|
|
}
|
|
}
|
|
|
|
string_type operator()(const string_type &text) const { return (*type1_)(text); }
|
|
|
|
string_type operator()(const string_type &text, const basic_renderer<string_type> &render) const
|
|
{
|
|
return (*type2_)(text, render);
|
|
}
|
|
|
|
private:
|
|
std::unique_ptr<type1> type1_;
|
|
std::unique_ptr<type2> type2_;
|
|
};
|
|
|
|
template<typename string_type>
|
|
class basic_data;
|
|
template<typename string_type>
|
|
using basic_object = std::unordered_map<string_type, basic_data<string_type>>;
|
|
template<typename string_type>
|
|
using basic_list = std::vector<basic_data<string_type>>;
|
|
template<typename string_type>
|
|
using basic_partial = std::function<string_type()>;
|
|
template<typename string_type>
|
|
using basic_lambda = typename basic_lambda_t<string_type>::type1;
|
|
template<typename string_type>
|
|
using basic_lambda2 = typename basic_lambda_t<string_type>::type2;
|
|
|
|
template<typename string_type>
|
|
class basic_data {
|
|
public:
|
|
enum class type {
|
|
object,
|
|
string,
|
|
list,
|
|
bool_true,
|
|
bool_false,
|
|
partial,
|
|
lambda,
|
|
lambda2,
|
|
invalid,
|
|
};
|
|
|
|
// Construction
|
|
basic_data() : basic_data(type::object) {}
|
|
|
|
basic_data(const string_type &string) : type_{type::string}
|
|
{
|
|
str_.reset(new string_type(string));
|
|
}
|
|
|
|
basic_data(const typename string_type::value_type *string) : type_{type::string}
|
|
{
|
|
str_.reset(new string_type(string));
|
|
}
|
|
|
|
basic_data(const basic_object<string_type> &obj) : type_{type::object}
|
|
{
|
|
obj_.reset(new basic_object<string_type>(obj));
|
|
}
|
|
|
|
basic_data(const basic_list<string_type> &l) : type_{type::list}
|
|
{
|
|
list_.reset(new basic_list<string_type>(l));
|
|
}
|
|
|
|
basic_data(type t) : type_{t}
|
|
{
|
|
switch (type_) {
|
|
case type::object:
|
|
obj_.reset(new basic_object<string_type>);
|
|
break;
|
|
case type::string:
|
|
str_.reset(new string_type);
|
|
break;
|
|
case type::list:
|
|
list_.reset(new basic_list<string_type>);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
basic_data(const string_type &name, const basic_data &var) : basic_data{} { set(name, var); }
|
|
|
|
basic_data(const basic_partial<string_type> &p) : type_{type::partial}
|
|
{
|
|
partial_.reset(new basic_partial<string_type>(p));
|
|
}
|
|
|
|
basic_data(const basic_lambda<string_type> &l) : type_{type::lambda}
|
|
{
|
|
lambda_.reset(new basic_lambda_t<string_type>(l));
|
|
}
|
|
|
|
basic_data(const basic_lambda2<string_type> &l) : type_{type::lambda2}
|
|
{
|
|
lambda_.reset(new basic_lambda_t<string_type>(l));
|
|
}
|
|
|
|
basic_data(const basic_lambda_t<string_type> &l)
|
|
{
|
|
if (l.is_type1()) {
|
|
type_ = type::lambda;
|
|
} else if (l.is_type2()) {
|
|
type_ = type::lambda2;
|
|
}
|
|
lambda_.reset(new basic_lambda_t<string_type>(l));
|
|
}
|
|
|
|
basic_data(bool b) : type_{b ? type::bool_true : type::bool_false} {}
|
|
|
|
// Copying
|
|
basic_data(const basic_data &dat) : type_(dat.type_)
|
|
{
|
|
if (dat.obj_) {
|
|
obj_.reset(new basic_object<string_type>(*dat.obj_));
|
|
} else if (dat.str_) {
|
|
str_.reset(new string_type(*dat.str_));
|
|
} else if (dat.list_) {
|
|
list_.reset(new basic_list<string_type>(*dat.list_));
|
|
} else if (dat.partial_) {
|
|
partial_.reset(new basic_partial<string_type>(*dat.partial_));
|
|
} else if (dat.lambda_) {
|
|
lambda_.reset(new basic_lambda_t<string_type>(*dat.lambda_));
|
|
}
|
|
}
|
|
|
|
// Move
|
|
basic_data(basic_data &&dat) : type_{dat.type_}
|
|
{
|
|
if (dat.obj_) {
|
|
obj_ = std::move(dat.obj_);
|
|
} else if (dat.str_) {
|
|
str_ = std::move(dat.str_);
|
|
} else if (dat.list_) {
|
|
list_ = std::move(dat.list_);
|
|
} else if (dat.partial_) {
|
|
partial_ = std::move(dat.partial_);
|
|
} else if (dat.lambda_) {
|
|
lambda_ = std::move(dat.lambda_);
|
|
}
|
|
dat.type_ = type::invalid;
|
|
}
|
|
|
|
basic_data &operator=(basic_data &&dat)
|
|
{
|
|
if (this != &dat) {
|
|
obj_.reset();
|
|
str_.reset();
|
|
list_.reset();
|
|
partial_.reset();
|
|
lambda_.reset();
|
|
if (dat.obj_) {
|
|
obj_ = std::move(dat.obj_);
|
|
} else if (dat.str_) {
|
|
str_ = std::move(dat.str_);
|
|
} else if (dat.list_) {
|
|
list_ = std::move(dat.list_);
|
|
} else if (dat.partial_) {
|
|
partial_ = std::move(dat.partial_);
|
|
} else if (dat.lambda_) {
|
|
lambda_ = std::move(dat.lambda_);
|
|
}
|
|
type_ = dat.type_;
|
|
dat.type_ = type::invalid;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
// Type info
|
|
bool is_object() const { return type_ == type::object; }
|
|
|
|
bool is_string() const { return type_ == type::string; }
|
|
|
|
bool is_list() const { return type_ == type::list; }
|
|
|
|
bool is_bool() const { return is_true() || is_false(); }
|
|
|
|
bool is_true() const { return type_ == type::bool_true; }
|
|
|
|
bool is_false() const { return type_ == type::bool_false; }
|
|
|
|
bool is_partial() const { return type_ == type::partial; }
|
|
|
|
bool is_lambda() const { return type_ == type::lambda; }
|
|
|
|
bool is_lambda2() const { return type_ == type::lambda2; }
|
|
|
|
bool is_invalid() const { return type_ == type::invalid; }
|
|
|
|
// Object data
|
|
bool is_empty_object() const { return is_object() && obj_->empty(); }
|
|
|
|
bool is_non_empty_object() const { return is_object() && !obj_->empty(); }
|
|
|
|
void set(const string_type &name, const basic_data &var)
|
|
{
|
|
if (is_object()) {
|
|
auto it = obj_->find(name);
|
|
if (it != obj_->end()) { obj_->erase(it); }
|
|
obj_->insert(std::pair<string_type, basic_data>{name, var});
|
|
}
|
|
}
|
|
|
|
const basic_data *get(const string_type &name) const
|
|
{
|
|
if (!is_object()) { return nullptr; }
|
|
const auto &it = obj_->find(name);
|
|
if (it == obj_->end()) { return nullptr; }
|
|
return &it->second;
|
|
}
|
|
|
|
// List data
|
|
void push_back(const basic_data &var)
|
|
{
|
|
if (is_list()) { list_->push_back(var); }
|
|
}
|
|
|
|
const basic_list<string_type> &list_value() const { return *list_; }
|
|
|
|
bool is_empty_list() const { return is_list() && list_->empty(); }
|
|
|
|
bool is_non_empty_list() const { return is_list() && !list_->empty(); }
|
|
|
|
basic_data &operator<<(const basic_data &data)
|
|
{
|
|
push_back(data);
|
|
return *this;
|
|
}
|
|
|
|
// String data
|
|
const string_type &string_value() const { return *str_; }
|
|
|
|
basic_data &operator[](const string_type &key) { return (*obj_)[key]; }
|
|
|
|
const basic_partial<string_type> &partial_value() const { return (*partial_); }
|
|
|
|
const basic_lambda<string_type> &lambda_value() const { return lambda_->type1_value(); }
|
|
|
|
const basic_lambda2<string_type> &lambda2_value() const { return lambda_->type2_value(); }
|
|
|
|
private:
|
|
type type_;
|
|
std::unique_ptr<basic_object<string_type>> obj_;
|
|
std::unique_ptr<string_type> str_;
|
|
std::unique_ptr<basic_list<string_type>> list_;
|
|
std::unique_ptr<basic_partial<string_type>> partial_;
|
|
std::unique_ptr<basic_lambda_t<string_type>> lambda_;
|
|
};
|
|
|
|
template<typename string_type>
|
|
class delimiter_set {
|
|
public:
|
|
string_type begin;
|
|
string_type end;
|
|
|
|
delimiter_set() : begin(default_begin), end(default_end) {}
|
|
|
|
bool is_default() const { return begin == default_begin && end == default_end; }
|
|
|
|
static const string_type default_begin;
|
|
static const string_type default_end;
|
|
};
|
|
|
|
template<typename string_type>
|
|
const string_type delimiter_set<string_type>::default_begin(2, '{');
|
|
template<typename string_type>
|
|
const string_type delimiter_set<string_type>::default_end(2, '}');
|
|
|
|
template<typename string_type>
|
|
class basic_context {
|
|
public:
|
|
virtual ~basic_context() = default;
|
|
virtual void push(const basic_data<string_type> *data) = 0;
|
|
virtual void pop() = 0;
|
|
|
|
virtual const basic_data<string_type> *get(const string_type &name) const = 0;
|
|
virtual const basic_data<string_type> *get_partial(const string_type &name) const = 0;
|
|
};
|
|
|
|
template<typename string_type>
|
|
class context : public basic_context<string_type> {
|
|
public:
|
|
context(const basic_data<string_type> *data) { push(data); }
|
|
|
|
context() {}
|
|
|
|
virtual void push(const basic_data<string_type> *data) override
|
|
{
|
|
items_.insert(items_.begin(), data);
|
|
}
|
|
|
|
virtual void pop() override { items_.erase(items_.begin()); }
|
|
|
|
virtual const basic_data<string_type> *get(const string_type &name) const override
|
|
{
|
|
// process {{.}} name
|
|
if (name.size() == 1 && name.at(0) == '.') { return items_.front(); }
|
|
if (name.find('.') == string_type::npos) {
|
|
// process normal name without having to split which is slower
|
|
for (const auto &item : items_) {
|
|
const auto var = item->get(name);
|
|
if (var) { return var; }
|
|
}
|
|
return nullptr;
|
|
}
|
|
// process x.y-like name
|
|
const auto names = split(name, '.');
|
|
for (const auto &item : items_) {
|
|
auto var = item;
|
|
for (const auto &n : names) {
|
|
var = var->get(n);
|
|
if (!var) { break; }
|
|
}
|
|
if (var) { return var; }
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
virtual const basic_data<string_type> *get_partial(const string_type &name) const override
|
|
{
|
|
for (const auto &item : items_) {
|
|
const auto var = item->get(name);
|
|
if (var) { return var; }
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
context(const context &) = delete;
|
|
context &operator=(const context &) = delete;
|
|
|
|
private:
|
|
std::vector<const basic_data<string_type> *> items_;
|
|
};
|
|
|
|
template<typename string_type>
|
|
class line_buffer_state {
|
|
public:
|
|
string_type data;
|
|
bool contained_section_tag = false;
|
|
|
|
bool is_empty_or_contains_only_whitespace() const
|
|
{
|
|
for (const auto ch : data) {
|
|
// don't look at newlines
|
|
if (ch != ' ' && ch != '\t') { return false; }
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void clear()
|
|
{
|
|
data.clear();
|
|
contained_section_tag = false;
|
|
}
|
|
};
|
|
|
|
template<typename string_type>
|
|
class context_internal {
|
|
public:
|
|
basic_context<string_type> &ctx;
|
|
delimiter_set<string_type> delim_set;
|
|
line_buffer_state<string_type> line_buffer;
|
|
|
|
context_internal(basic_context<string_type> &a_ctx) : ctx(a_ctx) {}
|
|
};
|
|
|
|
enum class tag_type {
|
|
text,
|
|
variable,
|
|
unescaped_variable,
|
|
section_begin,
|
|
section_end,
|
|
section_begin_inverted,
|
|
comment,
|
|
partial,
|
|
set_delimiter,
|
|
};
|
|
|
|
template<typename string_type>
|
|
class mstch_tag /* gcc doesn't allow "tag tag;" so rename the class :( */ {
|
|
public:
|
|
string_type name;
|
|
tag_type type = tag_type::text;
|
|
std::shared_ptr<string_type> section_text;
|
|
std::shared_ptr<delimiter_set<string_type>> delim_set;
|
|
|
|
bool is_section_begin() const
|
|
{
|
|
return type == tag_type::section_begin || type == tag_type::section_begin_inverted;
|
|
}
|
|
|
|
bool is_section_end() const { return type == tag_type::section_end; }
|
|
};
|
|
|
|
template<typename string_type>
|
|
class context_pusher {
|
|
public:
|
|
context_pusher(context_internal<string_type> &ctx, const basic_data<string_type> *data)
|
|
: ctx_(ctx)
|
|
{
|
|
ctx.ctx.push(data);
|
|
}
|
|
|
|
~context_pusher() { ctx_.ctx.pop(); }
|
|
|
|
context_pusher(const context_pusher &) = delete;
|
|
context_pusher &operator=(const context_pusher &) = delete;
|
|
|
|
private:
|
|
context_internal<string_type> &ctx_;
|
|
};
|
|
|
|
template<typename string_type>
|
|
class component {
|
|
private:
|
|
using string_size_type = typename string_type::size_type;
|
|
|
|
public:
|
|
string_type text;
|
|
mstch_tag<string_type> tag;
|
|
std::vector<component> children;
|
|
string_size_type position = string_type::npos;
|
|
|
|
enum class walk_control {
|
|
walk,// "continue" is reserved :/
|
|
stop,
|
|
skip,
|
|
};
|
|
using walk_callback = std::function<walk_control(component &)>;
|
|
|
|
component() {}
|
|
|
|
component(const string_type &t, string_size_type p) : text(t), position(p) {}
|
|
|
|
bool is_text() const { return tag.type == tag_type::text; }
|
|
|
|
bool is_newline() const
|
|
{
|
|
return is_text()
|
|
&& ((text.size() == 2 && text[0] == '\r' && text[1] == '\n')
|
|
|| (text.size() == 1 && (text[0] == '\n' || text[0] == '\r')));
|
|
}
|
|
|
|
bool is_non_newline_whitespace() const
|
|
{
|
|
return is_text() && !is_newline() && text.size() == 1
|
|
&& (text[0] == ' ' || text[0] == '\t');
|
|
}
|
|
|
|
void walk_children(const walk_callback &callback)
|
|
{
|
|
for (auto &child : children) {
|
|
if (child.walk(callback) != walk_control::walk) { break; }
|
|
}
|
|
}
|
|
|
|
private:
|
|
walk_control walk(const walk_callback &callback)
|
|
{
|
|
walk_control control{callback(*this)};
|
|
if (control == walk_control::stop) {
|
|
return control;
|
|
} else if (control == walk_control::skip) {
|
|
return walk_control::walk;
|
|
}
|
|
for (auto &child : children) {
|
|
control = child.walk(callback);
|
|
if (control == walk_control::stop) { return control; }
|
|
}
|
|
return control;
|
|
}
|
|
};
|
|
|
|
template<typename string_type>
|
|
class parser {
|
|
public:
|
|
parser(const string_type &input,
|
|
context_internal<string_type> &ctx,
|
|
component<string_type> &root_component,
|
|
string_type &error_message)
|
|
{
|
|
parse(input, ctx, root_component, error_message);
|
|
}
|
|
|
|
private:
|
|
void parse(const string_type &input,
|
|
context_internal<string_type> &ctx,
|
|
component<string_type> &root_component,
|
|
string_type &error_message) const
|
|
{
|
|
using string_size_type = typename string_type::size_type;
|
|
using streamstring = std::basic_ostringstream<typename string_type::value_type>;
|
|
|
|
const string_type brace_delimiter_end_unescaped(3, '}');
|
|
const string_size_type input_size{input.size()};
|
|
|
|
bool current_delimiter_is_brace{ctx.delim_set.is_default()};
|
|
|
|
std::vector<component<string_type> *> sections{&root_component};
|
|
std::vector<string_size_type> section_starts;
|
|
string_type current_text;
|
|
string_size_type current_text_position = -1;
|
|
|
|
current_text.reserve(input_size);
|
|
|
|
const auto process_current_text = [¤t_text, ¤t_text_position, §ions]() {
|
|
if (!current_text.empty()) {
|
|
const component<string_type> comp{current_text, current_text_position};
|
|
sections.back()->children.push_back(comp);
|
|
current_text.clear();
|
|
current_text_position = -1;
|
|
}
|
|
};
|
|
|
|
const std::vector<string_type> whitespace{
|
|
string_type(1, '\r') + string_type(1, '\n'),
|
|
string_type(1, '\n'),
|
|
string_type(1, '\r'),
|
|
string_type(1, ' '),
|
|
string_type(1, '\t'),
|
|
};
|
|
|
|
for (string_size_type input_position = 0; input_position != input_size;) {
|
|
bool parse_tag = false;
|
|
|
|
if (input.compare(input_position, ctx.delim_set.begin.size(), ctx.delim_set.begin)
|
|
== 0) {
|
|
process_current_text();
|
|
|
|
// Tag start delimiter
|
|
parse_tag = true;
|
|
} else {
|
|
bool parsed_whitespace = false;
|
|
for (const auto &whitespace_text : whitespace) {
|
|
if (input.compare(input_position, whitespace_text.size(), whitespace_text)
|
|
== 0) {
|
|
process_current_text();
|
|
|
|
const component<string_type> comp{whitespace_text, input_position};
|
|
sections.back()->children.push_back(comp);
|
|
input_position += whitespace_text.size();
|
|
|
|
parsed_whitespace = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!parsed_whitespace) {
|
|
if (current_text.empty()) { current_text_position = input_position; }
|
|
current_text.append(1, input[input_position]);
|
|
input_position++;
|
|
}
|
|
}
|
|
|
|
if (!parse_tag) { continue; }
|
|
|
|
// Find the next tag start delimiter
|
|
const string_size_type tag_location_start = input_position;
|
|
|
|
// Find the next tag end delimiter
|
|
string_size_type tag_contents_location{tag_location_start + ctx.delim_set.begin.size()};
|
|
const bool tag_is_unescaped_var{
|
|
current_delimiter_is_brace && tag_location_start != (input_size - 2)
|
|
&& input.at(tag_contents_location) == ctx.delim_set.begin.at(0)};
|
|
const string_type ¤t_tag_delimiter_end{
|
|
tag_is_unescaped_var ? brace_delimiter_end_unescaped : ctx.delim_set.end};
|
|
const auto current_tag_delimiter_end_size = current_tag_delimiter_end.size();
|
|
if (tag_is_unescaped_var) { ++tag_contents_location; }
|
|
const string_size_type tag_location_end{
|
|
input.find(current_tag_delimiter_end, tag_contents_location)};
|
|
if (tag_location_end == string_type::npos) {
|
|
streamstring ss;
|
|
ss << "Unclosed tag at " << tag_location_start;
|
|
error_message.assign(ss.str());
|
|
return;
|
|
}
|
|
|
|
// Parse tag
|
|
const string_type tag_contents{
|
|
trim(string_type{input,
|
|
tag_contents_location,
|
|
tag_location_end - tag_contents_location})};
|
|
component<string_type> comp;
|
|
if (!tag_contents.empty() && tag_contents[0] == '=') {
|
|
if (!parse_set_delimiter_tag(tag_contents, ctx.delim_set)) {
|
|
streamstring ss;
|
|
ss << "Invalid set delimiter tag at " << tag_location_start;
|
|
error_message.assign(ss.str());
|
|
return;
|
|
}
|
|
current_delimiter_is_brace = ctx.delim_set.is_default();
|
|
comp.tag.type = tag_type::set_delimiter;
|
|
comp.tag.delim_set.reset(new delimiter_set<string_type>(ctx.delim_set));
|
|
}
|
|
if (comp.tag.type != tag_type::set_delimiter) {
|
|
parse_tag_contents(tag_is_unescaped_var, tag_contents, comp.tag);
|
|
}
|
|
comp.position = tag_location_start;
|
|
sections.back()->children.push_back(comp);
|
|
|
|
// Start next search after this tag
|
|
input_position = tag_location_end + current_tag_delimiter_end_size;
|
|
|
|
// Push or pop sections
|
|
if (comp.tag.is_section_begin()) {
|
|
sections.push_back(§ions.back()->children.back());
|
|
section_starts.push_back(input_position);
|
|
} else if (comp.tag.is_section_end()) {
|
|
if (sections.size() == 1) {
|
|
streamstring ss;
|
|
ss << "Unopened section \"" << comp.tag.name << "\" at " << comp.position;
|
|
error_message.assign(ss.str());
|
|
return;
|
|
}
|
|
sections.back()->tag.section_text.reset(
|
|
new string_type(input.substr(section_starts.back(),
|
|
tag_location_start - section_starts.back())));
|
|
sections.pop_back();
|
|
section_starts.pop_back();
|
|
}
|
|
}
|
|
|
|
process_current_text();
|
|
|
|
// Check for sections without an ending tag
|
|
root_component.walk_children(
|
|
[&error_message](component<string_type> &comp) ->
|
|
typename component<string_type>::walk_control {
|
|
if (!comp.tag.is_section_begin()) {
|
|
return component<string_type>::walk_control::walk;
|
|
}
|
|
if (comp.children.empty() || !comp.children.back().tag.is_section_end()
|
|
|| comp.children.back().tag.name != comp.tag.name) {
|
|
streamstring ss;
|
|
ss << "Unclosed section \"" << comp.tag.name << "\" at " << comp.position;
|
|
error_message.assign(ss.str());
|
|
return component<string_type>::walk_control::stop;
|
|
}
|
|
comp.children.pop_back();// remove now useless end section component
|
|
return component<string_type>::walk_control::walk;
|
|
});
|
|
if (!error_message.empty()) { return; }
|
|
}
|
|
|
|
bool is_set_delimiter_valid(const string_type &delimiter) const
|
|
{
|
|
// "Custom delimiters may not contain whitespace or the equals sign."
|
|
for (const auto ch : delimiter) {
|
|
if (ch == '=' || std::isspace(ch)) { return false; }
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool parse_set_delimiter_tag(const string_type &contents,
|
|
delimiter_set<string_type> &delimiter_set) const
|
|
{
|
|
// Smallest legal tag is "=X X="
|
|
if (contents.size() < 5) { return false; }
|
|
if (contents.back() != '=') { return false; }
|
|
const auto contents_substr = trim(contents.substr(1, contents.size() - 2));
|
|
const auto spacepos = contents_substr.find(' ');
|
|
if (spacepos == string_type::npos) { return false; }
|
|
const auto nonspace = contents_substr.find_first_not_of(' ', spacepos + 1);
|
|
assert(nonspace != string_type::npos);
|
|
const string_type begin = contents_substr.substr(0, spacepos);
|
|
const string_type end = contents_substr.substr(nonspace, contents_substr.size() - nonspace);
|
|
if (!is_set_delimiter_valid(begin) || !is_set_delimiter_valid(end)) { return false; }
|
|
delimiter_set.begin = begin;
|
|
delimiter_set.end = end;
|
|
return true;
|
|
}
|
|
|
|
void parse_tag_contents(bool is_unescaped_var,
|
|
const string_type &contents,
|
|
mstch_tag<string_type> &tag) const
|
|
{
|
|
if (is_unescaped_var) {
|
|
tag.type = tag_type::unescaped_variable;
|
|
tag.name = contents;
|
|
} else if (contents.empty()) {
|
|
tag.type = tag_type::variable;
|
|
tag.name.clear();
|
|
} else {
|
|
switch (contents.at(0)) {
|
|
case '#':
|
|
tag.type = tag_type::section_begin;
|
|
break;
|
|
case '^':
|
|
tag.type = tag_type::section_begin_inverted;
|
|
break;
|
|
case '/':
|
|
tag.type = tag_type::section_end;
|
|
break;
|
|
case '>':
|
|
tag.type = tag_type::partial;
|
|
break;
|
|
case '&':
|
|
tag.type = tag_type::unescaped_variable;
|
|
break;
|
|
case '!':
|
|
tag.type = tag_type::comment;
|
|
break;
|
|
default:
|
|
tag.type = tag_type::variable;
|
|
break;
|
|
}
|
|
if (tag.type == tag_type::variable) {
|
|
tag.name = contents;
|
|
} else {
|
|
string_type name{contents};
|
|
name.erase(name.begin());
|
|
tag.name = trim(name);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
template<typename StringType>
|
|
class basic_mustache {
|
|
public:
|
|
using string_type = StringType;
|
|
|
|
basic_mustache(const string_type &input) : basic_mustache()
|
|
{
|
|
context<string_type> ctx;
|
|
context_internal<string_type> context{ctx};
|
|
parser<string_type> parser{input, context, root_component_, error_message_};
|
|
}
|
|
|
|
bool is_valid() const { return error_message_.empty(); }
|
|
|
|
const string_type &error_message() const { return error_message_; }
|
|
|
|
using escape_handler = std::function<string_type(const string_type &)>;
|
|
|
|
void set_custom_escape(const escape_handler &escape_fn) { escape_ = escape_fn; }
|
|
|
|
template<typename stream_type>
|
|
stream_type &render(const basic_data<string_type> &data, stream_type &stream)
|
|
{
|
|
render(data, [&stream](const string_type &str) { stream << str; });
|
|
return stream;
|
|
}
|
|
|
|
string_type render(const basic_data<string_type> &data)
|
|
{
|
|
std::basic_ostringstream<typename string_type::value_type> ss;
|
|
return render(data, ss).str();
|
|
}
|
|
|
|
template<typename stream_type>
|
|
stream_type &render(basic_context<string_type> &ctx, stream_type &stream)
|
|
{
|
|
context_internal<string_type> context{ctx};
|
|
render([&stream](const string_type &str) { stream << str; }, context);
|
|
return stream;
|
|
}
|
|
|
|
string_type render(basic_context<string_type> &ctx)
|
|
{
|
|
std::basic_ostringstream<typename string_type::value_type> ss;
|
|
return render(ctx, ss).str();
|
|
}
|
|
|
|
using render_handler = std::function<void(const string_type &)>;
|
|
|
|
void render(const basic_data<string_type> &data, const render_handler &handler)
|
|
{
|
|
if (!is_valid()) { return; }
|
|
context<string_type> ctx{&data};
|
|
context_internal<string_type> context{ctx};
|
|
render(handler, context);
|
|
}
|
|
|
|
basic_mustache() : escape_(html_escape<string_type>) {}
|
|
|
|
private:
|
|
using string_size_type = typename string_type::size_type;
|
|
|
|
basic_mustache(const string_type &input, context_internal<string_type> &ctx) : basic_mustache()
|
|
{
|
|
parser<string_type> parser{input, ctx, root_component_, error_message_};
|
|
}
|
|
|
|
string_type render(context_internal<string_type> &ctx)
|
|
{
|
|
std::basic_ostringstream<typename string_type::value_type> ss;
|
|
render([&ss](const string_type &str) { ss << str; }, ctx);
|
|
return ss.str();
|
|
}
|
|
|
|
void render(const render_handler &handler,
|
|
context_internal<string_type> &ctx,
|
|
bool root_renderer = true)
|
|
{
|
|
root_component_.walk_children([&handler, &ctx, this](component<string_type> &comp) ->
|
|
typename component<string_type>::walk_control {
|
|
return render_component(handler, ctx, comp);
|
|
});
|
|
// process the last line, but only for the top-level renderer
|
|
if (root_renderer) { render_current_line(handler, ctx, nullptr); }
|
|
}
|
|
|
|
void render_current_line(const render_handler &handler,
|
|
context_internal<string_type> &ctx,
|
|
const component<string_type> *comp) const
|
|
{
|
|
// We're at the end of a line, so check the line buffer state to see
|
|
// if the line had tags in it, and also if the line is now empty or
|
|
// contains whitespace only. if this situation is true, skip the line.
|
|
bool output = true;
|
|
if (ctx.line_buffer.contained_section_tag
|
|
&& ctx.line_buffer.is_empty_or_contains_only_whitespace()) {
|
|
output = false;
|
|
}
|
|
if (output) {
|
|
handler(ctx.line_buffer.data);
|
|
if (comp) { handler(comp->text); }
|
|
}
|
|
ctx.line_buffer.clear();
|
|
}
|
|
|
|
void render_result(context_internal<string_type> &ctx, const string_type &text) const
|
|
{
|
|
ctx.line_buffer.data.append(text);
|
|
}
|
|
|
|
typename component<string_type>::walk_control
|
|
render_component(const render_handler &handler,
|
|
context_internal<string_type> &ctx,
|
|
component<string_type> &comp)
|
|
{
|
|
if (comp.is_text()) {
|
|
if (comp.is_newline()) {
|
|
render_current_line(handler, ctx, &comp);
|
|
} else {
|
|
render_result(ctx, comp.text);
|
|
}
|
|
return component<string_type>::walk_control::walk;
|
|
}
|
|
|
|
const mstch_tag<string_type> &tag{comp.tag};
|
|
const basic_data<string_type> *var = nullptr;
|
|
switch (tag.type) {
|
|
case tag_type::variable:
|
|
case tag_type::unescaped_variable:
|
|
if ((var = ctx.ctx.get(tag.name)) != nullptr) {
|
|
if (!render_variable(handler, var, ctx, tag.type == tag_type::variable)) {
|
|
return component<string_type>::walk_control::stop;
|
|
}
|
|
}
|
|
break;
|
|
case tag_type::section_begin:
|
|
if ((var = ctx.ctx.get(tag.name)) != nullptr) {
|
|
if (var->is_lambda() || var->is_lambda2()) {
|
|
if (!render_lambda(handler,
|
|
var,
|
|
ctx,
|
|
render_lambda_escape::optional,
|
|
*comp.tag.section_text,
|
|
true)) {
|
|
return component<string_type>::walk_control::stop;
|
|
}
|
|
} else if (!var->is_false() && !var->is_empty_list()) {
|
|
render_section(handler, ctx, comp, var);
|
|
}
|
|
}
|
|
return component<string_type>::walk_control::skip;
|
|
case tag_type::section_begin_inverted:
|
|
if ((var = ctx.ctx.get(tag.name)) == nullptr || var->is_false()
|
|
|| var->is_empty_list()) {
|
|
render_section(handler, ctx, comp, var);
|
|
}
|
|
return component<string_type>::walk_control::skip;
|
|
case tag_type::partial:
|
|
if ((var = ctx.ctx.get_partial(tag.name)) != nullptr
|
|
&& (var->is_partial() || var->is_string())) {
|
|
const auto &partial_result
|
|
= var->is_partial() ? var->partial_value()() : var->string_value();
|
|
basic_mustache tmpl{partial_result};
|
|
tmpl.set_custom_escape(escape_);
|
|
if (!tmpl.is_valid()) {
|
|
error_message_ = tmpl.error_message();
|
|
} else {
|
|
tmpl.render(handler, ctx, false);
|
|
if (!tmpl.is_valid()) { error_message_ = tmpl.error_message(); }
|
|
}
|
|
if (!tmpl.is_valid()) { return component<string_type>::walk_control::stop; }
|
|
}
|
|
break;
|
|
case tag_type::set_delimiter:
|
|
ctx.delim_set = *comp.tag.delim_set;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return component<string_type>::walk_control::walk;
|
|
}
|
|
|
|
enum class render_lambda_escape {
|
|
escape,
|
|
unescape,
|
|
optional,
|
|
};
|
|
|
|
bool render_lambda(const render_handler &handler,
|
|
const basic_data<string_type> *var,
|
|
context_internal<string_type> &ctx,
|
|
render_lambda_escape escape,
|
|
const string_type &text,
|
|
bool parse_with_same_context)
|
|
{
|
|
const typename basic_renderer<string_type>::type2 render2
|
|
= [this, &ctx, parse_with_same_context, escape](const string_type &text, bool escaped) {
|
|
const auto process_template
|
|
= [this, &ctx, escape, escaped](basic_mustache &tmpl) -> string_type {
|
|
if (!tmpl.is_valid()) {
|
|
error_message_ = tmpl.error_message();
|
|
return {};
|
|
}
|
|
context_internal<string_type> render_ctx{ctx.ctx};// start a new line_buffer
|
|
const auto str = tmpl.render(render_ctx);
|
|
if (!tmpl.is_valid()) {
|
|
error_message_ = tmpl.error_message();
|
|
return {};
|
|
}
|
|
bool do_escape = false;
|
|
switch (escape) {
|
|
case render_lambda_escape::escape:
|
|
do_escape = true;
|
|
break;
|
|
case render_lambda_escape::unescape:
|
|
do_escape = false;
|
|
break;
|
|
case render_lambda_escape::optional:
|
|
do_escape = escaped;
|
|
break;
|
|
}
|
|
return do_escape ? escape_(str) : str;
|
|
};
|
|
if (parse_with_same_context) {
|
|
basic_mustache tmpl{text, ctx};
|
|
tmpl.set_custom_escape(escape_);
|
|
return process_template(tmpl);
|
|
}
|
|
basic_mustache tmpl{text};
|
|
tmpl.set_custom_escape(escape_);
|
|
return process_template(tmpl);
|
|
};
|
|
const typename basic_renderer<string_type>::type1 render
|
|
= [&render2](const string_type &text) { return render2(text, false); };
|
|
if (var->is_lambda2()) {
|
|
const basic_renderer<string_type> renderer{render, render2};
|
|
render_result(ctx, var->lambda2_value()(text, renderer));
|
|
} else {
|
|
render_current_line(handler, ctx, nullptr);
|
|
render_result(ctx, render(var->lambda_value()(text)));
|
|
}
|
|
return error_message_.empty();
|
|
}
|
|
|
|
bool render_variable(const render_handler &handler,
|
|
const basic_data<string_type> *var,
|
|
context_internal<string_type> &ctx,
|
|
bool escaped)
|
|
{
|
|
if (var->is_string()) {
|
|
const auto &varstr = var->string_value();
|
|
render_result(ctx, escaped ? escape_(varstr) : varstr);
|
|
} else if (var->is_lambda()) {
|
|
const render_lambda_escape escape_opt
|
|
= escaped ? render_lambda_escape::escape : render_lambda_escape::unescape;
|
|
return render_lambda(handler, var, ctx, escape_opt, {}, false);
|
|
} else if (var->is_lambda2()) {
|
|
using streamstring = std::basic_ostringstream<typename string_type::value_type>;
|
|
streamstring ss;
|
|
ss << "Lambda with render argument is not allowed for regular variables";
|
|
error_message_ = ss.str();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void render_section(const render_handler &handler,
|
|
context_internal<string_type> &ctx,
|
|
component<string_type> &incomp,
|
|
const basic_data<string_type> *var)
|
|
{
|
|
const auto callback = [&handler, &ctx, this](component<string_type> &comp) ->
|
|
typename component<string_type>::walk_control {
|
|
return render_component(handler, ctx, comp);
|
|
};
|
|
if (var && var->is_non_empty_list()) {
|
|
for (const auto &item : var->list_value()) {
|
|
// account for the section begin tag
|
|
ctx.line_buffer.contained_section_tag = true;
|
|
|
|
const context_pusher<string_type> ctxpusher{ctx, &item};
|
|
incomp.walk_children(callback);
|
|
|
|
// ctx may have been cleared. account for the section end tag
|
|
ctx.line_buffer.contained_section_tag = true;
|
|
}
|
|
} else if (var) {
|
|
// account for the section begin tag
|
|
ctx.line_buffer.contained_section_tag = true;
|
|
|
|
const context_pusher<string_type> ctxpusher{ctx, var};
|
|
incomp.walk_children(callback);
|
|
|
|
// ctx may have been cleared. account for the section end tag
|
|
ctx.line_buffer.contained_section_tag = true;
|
|
} else {
|
|
// account for the section begin tag
|
|
ctx.line_buffer.contained_section_tag = true;
|
|
|
|
incomp.walk_children(callback);
|
|
|
|
// ctx may have been cleared. account for the section end tag
|
|
ctx.line_buffer.contained_section_tag = true;
|
|
}
|
|
}
|
|
|
|
private:
|
|
string_type error_message_;
|
|
component<string_type> root_component_;
|
|
escape_handler escape_;
|
|
};
|
|
|
|
using mustache = basic_mustache<std::string>;
|
|
using data = basic_data<mustache::string_type>;
|
|
using object = basic_object<mustache::string_type>;
|
|
using list = basic_list<mustache::string_type>;
|
|
using partial = basic_partial<mustache::string_type>;
|
|
using renderer = basic_renderer<mustache::string_type>;
|
|
using lambda = basic_lambda<mustache::string_type>;
|
|
using lambda2 = basic_lambda2<mustache::string_type>;
|
|
using lambda_t = basic_lambda_t<mustache::string_type>;
|
|
|
|
using mustachew = basic_mustache<std::wstring>;
|
|
using dataw = basic_data<mustachew::string_type>;
|
|
|
|
}// namespace mustache
|
|
}// namespace kainjow
|
|
|
|
#endif// KAINJOW_MUSTACHE_HPP
|