Roxen.git / server / etc / modules / Mustache.pike

version» Context lines:

Roxen.git/server/etc/modules/Mustache.pike:1: + #pike __REAL_VERSION__ + #require constant(Regexp.PCRE)    -  + //! This is a port of the Logic-less {{mustache}} templates with JavaScript + //! @url{http://mustache.github.com/@}. + //! + //! Cred goes to Chris Wanstrath (Ruby), Jan Lehnardt (JavaScript) and the + //! mustache.js community. + //! + //! @example + //! @code + //! string tmpl = #" + //! <h1>{{header}}</h1> + //! + //! {{#preamble}} + //! <p>{{preamble}}</p> + //! {{/preamble}} + //! {{^preamble}} + //! <div class="notify">Preamble is missing</div> + //! {{/preamble}} + //! + //! <ul> + //! {{#names}} + //! <li>{{>name_row}}</li> + //! {{/names}} + //! </ul>"; + //! + //! Public.Templates.Mustasche stash = Public.Templates.Mustasche(); + //! + //! // Not strictly necessary, but this pre-parses and caches the template + //! stash->parse(tmpl); + //! + //! mappping data = ([ + //! "header" : "This is a header", + //! "preamble" : "This is the preamble text", + //! "names" : ({ + //! ([ "name" : "Lisa", "age" : 29 ]), + //! ([ "name" : "Mark", "age" : 43 ]), + //! ([ "name" : "Anna", "age" : 61 ]) + //! }) + //! ]); + //! + //! string html = stash.render(tmpl, data, + //! ([ "name_row" : + //! "<li>{{name}} is {{age}} years old</li>"])); + //! @endcode + //! + //! The output of the above would be something like + //! + //! @code + //! <h1>This is a header</h1> + //! <p>This is the preamble text</p> + //! <ul> + //! <li>Lisa is 29 years old</li> + //! <li>Mark is 43 years old</li> + //! <li>Anna is 61 years old</li> + //! </ul> + //! @endcode +  + import Regexp.PCRE; +  + #ifdef MUSTACHE_DEBUG + # define TRACE(X...)werror("%s:%d: %s",basename(__FILE__),__LINE__,sprintf(X)) + #else + # define TRACE(X...)0 + #endif +  + //! @ignore + //! Internal helper class + 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]; +  } + } + //! @endignore +  + 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("#|\\^|\\/|>|\\{|&|=|!"); +  + //! Regexp escape the string @[s] + protected string escape_regexp(string s) + { +  return _re_escape_re->replace(s, lambda (string a) { +  return "\\" + a; +  }); + } +  + //! Check if @[obj] has the index @[prop]. + protected bool has_property(mixed obj, string prop) + { +  if (objectp(obj) || mappingp(obj) || multisetp(obj) || arrayp(obj)) { +  return has_index(obj, prop); +  } +  +  return false; + } +  + //! Check if @[s] is a whitespace character or not + protected bool is_whitespace(string|int s) + { +  if (stringp(s)) { +  return !_re_nonspace->match(s); +  } +  +  return (< '\n', ' ', '\t', '\r' >)[s]; + } +  + //! Entities to HTML entities mapping + protected mapping entity_map = ([ +  "&" : "&amp;", +  "<" : "&lt;", +  ">" : "&gt;", +  "\"" : "&quot;", +  "'" : "&#39;", +  "/" : "&#x2F;", +  "`" : "&#x60;", +  "=" : "&#x3D;" + ]); +  + //! HTML escape the string @[s] + public string escape_html(string s) + { +  return _re_escape_html->replace(s, lambda (string a) { +  return entity_map[a] || a; +  }); + } +  + //! A simple string scanner that is used by the template parser to find + //! tokens in template strings. + protected class Scanner + { +  string str; +  string tail; +  int pos; +  +  protected void create(string s) +  { +  str = s; +  tail = s; +  pos = 0; +  } +  +  //! Returns @tt{true@} if the tail is empty (end of string). +  public bool eos() +  { +  return tail == ""; +  } +  +  //! Tries to match the given regular expression at the current position. +  //! Returns the matched text if it can match, @tt{0@} otherwise. +  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; +  } +  +  +  //! Skips all text until the given regular expression or string can be +  //! matched. Returns the skipped string, which is the entire tail if no +  //! match can be made. +  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 + } +  +  + //! A @[Token] is an array like object with at least 4 elements. The first + //! element is the mustache symbol that was used inside the tag, e.g. "#" or + //! "&". If the tag did not contain a symbol (i.e. @code{{{myValue}}@}) this + //! element is "name". For all text that appears outside a symbol this element + //! is "text". + //! + //! The second element of a token is its "value". For mustache tags this is + //! whatever else was inside the tag besides the opening symbol. For text tokens + //! this is the text itself. + //! + //! The third and fourth elements of the token are the start and end indices, + //! respectively, of the token in the original template. + //! + //! Tokens that are the root node of a subtree contain two more elements: 1) an + //! array of tokens in the subtree and 2) the index in the original template at + //! which the closing tag for that section begins. + 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 + } +  + //! Breaks up the given @[template] string into a tree of tokens. If the + //! @[_tags] argument is given here it must be an array with two string values: + //! the opening and closing tags used in the template (e.g. + //! @code{[ "<%", "%>" ]@}). Of course, the default is to use mustaches + //! (i.e. mustache.tags). + 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 = ({}); // Stack to hold section tokens +  array(Token) tokens = ({}); // Buffer to hold the tokens +  array(int) spaces = ({}); // Indices of whitespace tokens on the current line +  bool has_tag = false; // Is there a {{tag}} on the current line? +  bool none_space = false; // Is there a non-space char on the current line? +  +  // Strips all whitespace tokens array for the current line +  // if there was a {{#tag}} on it and otherwise only space. +  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; +  // Match any text between tags. +  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(); +  } +  } +  } +  +  // Match the opening tag. +  if (!scanner->scan(opening_tag_re)) { +  break; +  } +  +  has_tag = true; +  +  // Get the tag type. +  string type = scanner->scan(_re_tag) || "name"; +  +  scanner->scan(_re_white); +  +  // Get the tag value. +  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); +  } +  +  // Match the closing tag. +  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 == "/") { +  // Check section nesting. +  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 == "=") { +  // Set the tags for the next time around. +  compile_tags(value); +  } +  } +  +  // Make sure there are no open sections when we're done. +  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)); + } +  +  + //! Combines the values of consecutive text tokens in the given @[tokens] array + //! to a single token. + 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 holding an array of @[Token] objects + 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 + } +  + //! Forms the given array of @[tokens] into a nested tree structure where + //! tokens that represent a section have two additional items: 1) an array of + //! all tokens that appear in that section and 2) the index in the original + //! template that represents the end of that section. + 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; +  +  // TRACE(">>> nest_tokens leave\n"); +  +  destruct(collector); +  destruct(sections); +  destruct(nested_tokens); +  +  // TRACE("my_toks: %O\n", my_toks); +  +  return my_toks; + } +  +  + //! Represents a rendering context by wrapping a view object and + //! maintaining a reference to the parent context. + protected class Context + { +  mixed view; +  mapping cache; +  Context parent; +  +  //! @param view +  //! The data structure for this context +  protected void create(mixed view, void|Context parent_context) +  { +  this::view = view; +  this::parent = parent_context; +  cache = ([ "." : view ]); +  } +  +  //! Creates a new context using the given view with this context +  //! as the parent. +  public Context push(mixed view) +  { +  return Context(view, this); +  } +  +  //! Returns the value of the given name in this context, traversing +  //! up the context hierarchy if the value is absent in this context's view. +  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); +  +  // TRACE("names: %O\n", names); +  // TRACE("value: %O\n", value); +  +  /** +  * Using the dot notion path in `name`, we descend through the +  * nested objects. +  * +  * To be certain that the lookup has been successful, we have to +  * check if the last object in the path actually has the property +  * we are looking for. We store the result in `lookupHit`. +  * +  * This is specially necessary for when the value has been set to +  * `undefined` and we want to avoid looking up parent contexts. +  **/ +  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 + } +  +  + //! A Writer knows how to take a stream of tokens and render them to a + //! string, given a context. It also maintains a cache of templates to + //! avoid the need to parse the same template twice. + protected class Writer + { +  private mapping __cache = ([]); +  +  //! Clears all cached templates in this writer. +  public void clear_cache() +  { +  __cache = ([]); +  } +  +  //! Parses and caches the given @[template] and returns the array of tokens +  //! that is generated from the parse. +  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; +  } +  +  //! High-level method that is used to render the given @[template] with +  //! the given @[view]. +  //! +  //! The optional @[partials] argument may be an object/mapping that contains +  //! the names and templates of partials that are used in the template. It may +  //! also be a function that is used to load partial templates on the fly +  //! that takes a single argument: the name of the partial. +  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); +  // map(tokens, lambda (Token t) { +  // TRACE("Destroy: %O\n", t); +  // destruct(t); +  // }); +  tokens = 0; +  return res; +  } +  +  +  //! Low-level method that renders the given array of @[tokens] using +  //! the given @[context] and @[partials]. +  //! +  //! Note: The @[template] is only ever used to extract the portion +  //! of the original template that was contained in a higher-order section. +  //! If the template doesn't use higher-order sections, this argument may +  //! be omitted. +  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 ""; +  } +  +  // This function is used to render an arbitrary template +  // in the current context by higher-order sections. +  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(); +  +  + //! Clears all cached templates in the default writer. + public void clear_cache() + { +  default_writer->clear_cache(); + } +  + //! Parses and caches the given template in the default writer and returns the + //! array of tokens it contains. Doing this ahead of time avoids the need to + //! parse templates on the fly as they are rendered. + public array(Token) parse(string template, void|array(string) tags) + { +  return default_writer->parse(template, tags); + } +  + //! Renders the @[template] with the given @[view] and @[partials] using the + //! default writer. + public string render(string template, mixed view, void|mixed partials) + { +  return default_writer->render(template, view, partials); + } +  + //! Is the value @[v] a @tt{falsy@} value or not. It's faly if it's + //! @tt{0, UNDEFINED, "" or ({})@} + 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   Newline at end of file added.