#pike __REAL_VERSION__ |
#require constant(Regexp.PCRE) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import Regexp.PCRE; |
|
#ifdef MUSTACHE_DEBUG |
# define TRACE(X...)werror("%s:%d: %s",basename(__FILE__),__LINE__,sprintf(X)) |
#else |
# define TRACE(X...)0 |
#endif |
|
|
|
protected class Re |
{ |
inherit Widestring; |
|
int search(string s) |
{ |
array(int)|int r = ::exec(s); |
|
if (intp(r) && r != -1) { |
error("Regex error %d!\n", r); |
} |
|
return intp(r) ? r : r[0]; |
} |
} |
|
|
protected array(string) tags = ({ "{{", "}}" }); |
|
protected Re |
_re_escape_re = Re("[\\-\\[\\]{}()*+?.,\\\\^$|#\\s]"), |
_re_escape_html = Re("[&<>\"'`=/]"), |
_re_nonspace = Re("\\S"), |
_re_white = Re("\\s*"), |
_re_space = Re("\\s+"), |
_re_equals = Re("\\s*="), |
_re_curly = Re("\\s*\\}"), |
_re_tag = Re("#|\\^|\\/|>|\\{|&|=|!"); |
|
|
protected string escape_regexp(string s) |
{ |
return _re_escape_re->replace(s, lambda (string a) { |
return "\\" + a; |
}); |
} |
|
|
protected bool has_property(mixed obj, string prop) |
{ |
if (objectp(obj) || mappingp(obj) || multisetp(obj) || arrayp(obj)) { |
return has_index(obj, prop); |
} |
|
return false; |
} |
|
|
protected bool is_whitespace(string|int s) |
{ |
if (stringp(s)) { |
return !_re_nonspace->match(s); |
} |
|
return (< '\n', ' ', '\t', '\r' >)[s]; |
} |
|
|
protected mapping entity_map = ([ |
"&" : "&", |
"<" : "<", |
">" : ">", |
"\"" : """, |
"'" : "'", |
"/" : "/", |
"`" : "`", |
"=" : "=" |
]); |
|
|
public string escape_html(string s) |
{ |
return _re_escape_html->replace(s, lambda (string a) { |
return entity_map[a] || a; |
}); |
} |
|
|
|
protected class Scanner |
{ |
string str; |
string tail; |
int pos; |
|
protected void create(string s) |
{ |
str = s; |
tail = s; |
pos = 0; |
} |
|
|
public bool eos() |
{ |
return tail == ""; |
} |
|
|
|
public string scan(Re re) |
{ |
array(int)|int r = re->exec(tail); |
|
if (intp(r) && r != -1) { |
error("Regexp error: %d", r); |
} |
|
if ((intp(r) && r == -1) || r[0] != 0) { |
return 0; |
} |
|
[int start, int end] = r; |
int len = end-start; |
string s = tail[start..end-1]; |
|
tail = tail[len..]; |
pos += len; |
|
return s; |
} |
|
|
|
|
|
public string scan_until(Re|string what) |
{ |
int index; |
|
if (stringp(what)) { |
index = search(tail, what); |
} |
else { |
index = what->search(tail); |
} |
|
string match; |
|
switch (index) |
{ |
case -1: |
match = tail; |
tail = ""; |
break; |
|
case 0: |
match = ""; |
break; |
|
default: |
match = tail[0..index-1]; |
tail = tail[index..]; |
break; |
} |
|
pos += sizeof(match); |
return match; |
} |
|
#ifdef MUSTACHE_DEBUG |
protected void destroy() |
{ |
TRACE("Scanner destroyed!\n"); |
} |
#endif |
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected class Token |
{ |
protected string type; |
protected mixed value; |
protected int start, end; |
protected mixed extra, extra2; |
|
protected void create(string type, mixed value, int start, int end) |
{ |
this::type = type; |
this::value = value; |
this::start = start; |
this::end = end; |
} |
|
mixed `[](int idx) |
{ |
switch (idx) { |
case 0: return type; |
case 1: return value; |
case 2: return start; |
case 3: return end; |
case 4: return extra; |
case 5: return extra2; |
} |
} |
|
mixed `[]=(int idx, mixed val) |
{ |
switch (idx) |
{ |
case 0: return type = val; |
case 1: return value = val; |
case 2: return start = val; |
case 3: return end = val; |
case 4: return extra = val; |
case 5: return extra2 = val; |
} |
} |
|
mixed cast(string how) |
{ |
switch (how) |
{ |
case "array": |
return ({ type, value, start, end, extra, extra2 }); |
|
default: |
error("Unknown cast (%O) in object! ", how); |
} |
} |
|
string _sprintf(int t) |
{ |
return sprintf("({ %O, %O, %O, %O, %O, %O })", |
type, value, start, end, extra, extra2); |
} |
|
#ifdef MUSTACHE_DEBUG |
protected void destroy() |
{ |
TRACE("Token destroyed!\n"); |
} |
#endif |
} |
|
|
|
|
|
|
array(Token) parse_template(string|function template, void|array(string) _tags) |
{ |
if (functionp(template)) { |
template = (string)template(0); |
} |
|
if (!template || !sizeof(template)) { |
return ({}); |
} |
|
array(Token) sections = ({}); |
array(Token) tokens = ({}); |
array(int) spaces = ({}); |
bool has_tag = false; |
bool none_space = false; |
|
|
|
void strip_space() { |
if (has_tag && !none_space) { |
while (sizeof(spaces)) { |
int t = spaces[-1]; |
spaces = spaces[..<1]; |
tokens[t] = 0; |
tokens -= ({ 0 }); |
} |
} |
else { |
spaces = ({}); |
} |
|
has_tag = false; |
none_space = false; |
}; |
|
Re opening_tag_re, closing_tag_re, closing_curly_re; |
|
void compile_tags(string|array(string) t) { |
if (stringp(t)) { |
t = _re_space->split(t); |
} |
|
if (!arrayp(t) || sizeof(t) != 2) { |
error("Invalid tags: %O\n", t); |
} |
|
opening_tag_re = Re(escape_regexp(t[0]) + "\\s*"); |
closing_tag_re = Re("\\s*" + escape_regexp(t[1])); |
closing_curly_re = Re("\\s*" + escape_regexp("}" + t[1])); |
}; |
|
compile_tags(_tags || tags); |
|
Scanner scanner = Scanner(template); |
|
int start, chr; |
Token token, open_section; |
string value; |
|
while (!scanner->eos()) { |
start = scanner->pos; |
|
value = scanner->scan_until(opening_tag_re); |
|
if (value) { |
for (int i; i < sizeof(value); i++) { |
chr = value[i]; |
|
if (is_whitespace(chr)) { |
spaces += ({ sizeof(tokens) }); |
} |
else { |
none_space = true; |
} |
|
tokens += ({ Token("text", value[i..i], start, start + 1) }); |
|
start += 1; |
|
if (chr == '\n') { |
strip_space(); |
} |
} |
} |
|
|
if (!scanner->scan(opening_tag_re)) { |
break; |
} |
|
has_tag = true; |
|
|
string type = scanner->scan(_re_tag) || "name"; |
|
scanner->scan(_re_white); |
|
|
if (type == "=") { |
value = scanner->scan_until(_re_equals); |
scanner->scan(_re_equals); |
scanner->scan_until(closing_tag_re); |
} |
else if (type == "{") { |
value = scanner->scan_until(closing_curly_re); |
scanner->scan(_re_curly); |
scanner->scan_until(closing_tag_re); |
type = "&"; |
} |
else { |
value = scanner->scan_until(closing_tag_re); |
} |
|
|
if (!scanner->scan(closing_tag_re)) { |
error("Unclosed tag at byte %d!\n", scanner->pos); |
} |
|
token = Token(type, value, start, scanner->pos); |
tokens += ({ token }); |
|
if ((< "#", "^" >)[type]) { |
sections += ({ token }); |
} |
else if (type == "/") { |
|
open_section = sections[-1]; |
sections = sections[..<1]; |
|
if (!open_section) { |
error("Unopened section \"%s\" at byte %d!\n", value, start); |
} |
|
if (open_section[1] != value) { |
error("Unclosed section \"%s\" at byte %d!\n", open_section[1], start); |
} |
} |
else if ((< "name", "{", "&" >)[type]) { |
none_space = true; |
} |
else if (type == "=") { |
|
compile_tags(value); |
} |
} |
|
|
if (sizeof(sections)) { |
open_section = sections[-1]; |
error("Unclosed section \"%s\" at byte %d!\n", |
open_section[1], scanner->pos); |
} |
|
return nest_tokens(squash_tokens(tokens)); |
} |
|
|
|
|
protected array(Token) squash_tokens(array(Token) tokens) |
{ |
array(Token) st = ({}); |
Token token, last_token; |
int len = sizeof(tokens); |
|
for (int i; i < len; ++i) { |
token = tokens[i]; |
|
if (token) { |
if (token[0] == "text" && last_token && last_token[0] == "text") { |
last_token[1] += token[1]; |
last_token[3] = token[3]; |
} |
else { |
st += ({ token }); |
last_token = token; |
} |
} |
} |
|
return st; |
} |
|
|
class TokRef |
{ |
private array _data = ({}); |
|
array `data() |
{ |
return _data; |
} |
|
TokRef `+(Token t) |
{ |
_data += ({ t }); |
return this; |
} |
|
TokRef `[]=(int index, mixed v) |
{ |
if (has_index(_data, index)) { |
_data[index] = v; |
} |
return this; |
} |
|
Token `[](int t) |
{ |
if (sizeof(_data) >= t) { |
return _data[t]; |
} |
} |
|
Token pop() |
{ |
if (sizeof(_data)) { |
Token t = _data[-1]; |
_data = _data[..<1]; |
return t; |
} |
} |
|
int _sizeof() |
{ |
return sizeof(_data); |
} |
|
mixed cast(string how) |
{ |
if (how == "array") { |
array(Token) out = allocate(sizeof(_data)); |
|
for (int i; i < sizeof(_data); i++) { |
out[i] = _data[i]; |
|
if (objectp(out[i][4])) { |
out[i][4] = out[i][4]->cast("array"); |
} |
} |
|
return out; |
} |
} |
|
string _sprintf(int t) |
{ |
return sprintf("%O(%d)", object_program(this), sizeof(_data)); |
} |
|
#ifdef MUSTACHE_DEBUG |
protected void destroy() |
{ |
TRACE("TokRef destroyed!\n"); |
} |
#endif |
} |
|
|
|
|
|
protected array(Token) nest_tokens(array(Token) tokens) |
{ |
TokRef |
nested_tokens = TokRef(), |
collector = nested_tokens, |
sections = TokRef(); |
|
Token token, section; |
|
int len = sizeof(tokens); |
|
for (int i; i < len; ++i) { |
token = tokens[i]; |
|
switch (token[0]) { |
case "#": |
case "^": |
collector += token; |
sections += token; |
collector = token[4] = TokRef(); |
break; |
|
case "/": |
section = sections->pop(); |
section[5] = token[2]; |
collector = sizeof(sections) |
? sections[-1][4] |
: nested_tokens; |
break; |
|
default: |
collector += token; |
break; |
} |
} |
|
array(Token) my_toks = (array(object(Token))) nested_tokens; |
|
|
|
destruct(collector); |
destruct(sections); |
destruct(nested_tokens); |
|
|
|
return my_toks; |
} |
|
|
|
|
protected class Context |
{ |
mixed view; |
mapping cache; |
Context parent; |
|
|
|
protected void create(mixed view, void|Context parent_context) |
{ |
this::view = view; |
this::parent = parent_context; |
cache = ([ "." : view ]); |
} |
|
|
|
public Context push(mixed view) |
{ |
return Context(view, this); |
} |
|
|
|
public mixed lookup(string name) |
{ |
mixed value = cache[name]; |
|
if (undefinedp(value)) { |
Context ctx = this; |
array(string) names; |
int index; |
bool lookuphit = false; |
|
while (ctx) { |
if (search(name, ".") > -1) { |
value = ctx->view; |
names = name/"."; |
index = 0; |
int namelen = sizeof(names); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
while (value && index < namelen) { |
if (index == namelen - 1) { |
lookuphit = has_property(value, names[index]); |
} |
|
value = value[names[index++]]; |
} |
} |
else { |
value = ctx->view[name]; |
lookuphit = has_property(ctx->view, name); |
} |
|
if (lookuphit) { |
break; |
} |
|
ctx = ctx->parent; |
} |
|
cache[name] = value; |
} |
|
if (functionp(value)) { |
value = value(name, view); |
} |
|
return safe_string(value); |
} |
|
#ifdef MUSTACHE_DEBUG |
protected void destroy() |
{ |
TRACE("Context destroyed!\n"); |
} |
#endif |
} |
|
|
|
|
|
protected class Writer |
{ |
private mapping __cache = ([]); |
|
|
public void clear_cache() |
{ |
__cache = ([]); |
} |
|
|
|
public array(Token) parse(string template, void|array(string) tags) |
{ |
array(Token) tokens = __cache[template]; |
|
if (!tokens) { |
tokens = __cache[template] = parse_template(template, tags); |
} |
|
return tokens; |
} |
|
|
|
|
|
|
|
|
public string render(string template, mixed view, |
void|mixed partials) |
{ |
array(Token) tokens = parse(template); |
Context ctx = objectp(view) && object_program(view) == Context |
? view |
: Context(view); |
|
string res = render_tokens(tokens, ctx, partials, template); |
|
|
|
|
tokens = 0; |
return res; |
} |
|
|
|
|
|
|
|
|
|
protected string render_tokens(array(Token) tokens, Context ctx, |
mixed partials, string template) |
{ |
String.Buffer buf = String.Buffer(); |
function add = buf->add; |
|
Token token; |
string symbol; |
mixed value; |
int len = sizeof(tokens); |
|
for (int i; i < len; ++i) { |
value = UNDEFINED; |
token = tokens[i]; |
symbol = token[0]; |
|
switch (symbol) { |
case "#": |
value = render_section(token, ctx, partials, template); |
break; |
|
case "^": |
value = render_inverted(token, ctx, partials, template); |
break; |
|
case ">": |
value = render_partial(token, ctx, partials); |
break; |
|
case "&": |
value = unescaped_value(token, ctx); |
break; |
|
case "name": |
value = escaped_value(token, ctx); |
break; |
|
case "text": |
value = raw_value(token); |
break; |
} |
|
if (value != UNDEFINED) { |
add(value); |
} |
} |
|
return buf->get(); |
} |
|
|
protected string render_section(Token token, Context ctx, mixed partials, |
string template) |
{ |
String.Buffer b = String.Buffer(); |
function add = b->add; |
mixed value = ctx->lookup(token[1]); |
|
if (!value) { |
return ""; |
} |
|
|
|
string subrender(string tmpl) { |
return render(tmpl, ctx, partials); |
}; |
|
if (multisetp(value)) { |
value = (array)value; |
} |
|
if (arrayp(value)) { |
int len = sizeof(value); |
|
for (int j; j < len; ++j) { |
add(render_tokens(token[4], ctx->push(value[j]), partials, template)); |
} |
} |
else if (objectp(value) || mappingp(value)) { |
add(render_tokens(token[4], ctx->push(value), partials, template)); |
} |
else if (functionp(value)) { |
if (!stringp(template)) { |
error("Cannot use higher-order sections without the original template"); |
} |
|
value = value(ctx->view, template[token[3]..token[5]-1], subrender); |
|
if (value && sizeof(value)) { |
add(value); |
} |
} |
else { |
add(safe_string(render_tokens(token[4], ctx, partials, template))); |
} |
|
return b->get(); |
} |
|
|
string render_inverted(Token token, Context ctx, mixed partials, |
string template) |
{ |
mixed value = ctx->lookup(token[1]); |
|
if (falsy(value)) { |
return render_tokens(token[4], ctx, partials, template); |
} |
} |
|
|
string render_partial(Token token, Context ctx, mixed partials) |
{ |
if (!partials) { |
return UNDEFINED; |
} |
|
mixed value = callablep(partials) ? partials(token[1]) : partials[token[1]]; |
|
if (value) { |
return render_tokens(parse(value), ctx, partials, value); |
} |
} |
|
|
string unescaped_value(Token token, Context ctx) |
{ |
mixed value = ctx->lookup(token[1]); |
|
if (value) { |
return (string) value; |
} |
} |
|
|
string escaped_value(Token token, Context ctx) |
{ |
mixed value = ctx->lookup(token[1]); |
|
if (value != UNDEFINED) { |
return escape_html((string)value); |
} |
} |
|
|
string raw_value(Token token) |
{ |
return (string) token[1]; |
} |
|
#ifdef MUSTACHE_DEBUG |
protected void destroy() |
{ |
TRACE("Writer destroyed!\n"); |
} |
#endif |
} |
|
protected mixed safe_string(mixed i) |
{ |
if (!stringp(i)) { |
return i; |
} |
|
catch { |
i = utf8_to_string(i); |
return i; |
}; |
|
return i; |
} |
|
protected Writer default_writer = Writer(); |
|
|
|
public void clear_cache() |
{ |
default_writer->clear_cache(); |
} |
|
|
|
|
public array(Token) parse(string template, void|array(string) tags) |
{ |
return default_writer->parse(template, tags); |
} |
|
|
|
public string render(string template, mixed view, void|mixed partials) |
{ |
return default_writer->render(template, view, partials); |
} |
|
|
|
protected bool falsy(mixed v) |
{ |
if (!v) return true; |
if ((stringp(v) || arrayp(v)) && !sizeof(v)) { |
return true; |
} |
|
return false; |
} |
|
#ifdef MUSTACHE_DEBUG |
protected void destroy() |
{ |
TRACE("Mustache destroyed!\n"); |
} |
#endif |
|
|