Просмотр исходного кода

Another rewrite

- Cleaner separation of responsibilities in the code between
  scanning, parsing, compiling, and rendering functions.
- Much faster
tags/0.5.1
Michael Jackson 14 лет назад
Родитель
Сommit
fbc66a8140
100 измененных файлов: 532 добавлений и 726 удалений
  1. +9
    -11
      Rakefile
  2. +444
    -400
      mustache.js
  3. +0
    -3
      spec/_files/ampersand_escape.js
  4. +0
    -1
      spec/_files/apostrophe.js
  5. +0
    -1
      spec/_files/array_of_strings.js
  6. +0
    -3
      spec/_files/backslashes.js
  7. +0
    -3
      spec/_files/bug_11_eating_whitespace.js
  8. +0
    -4
      spec/_files/changing_delimiters.js
  9. +0
    -5
      spec/_files/comments.js
  10. +0
    -4
      spec/_files/disappearing_whitespace.js
  11. +0
    -3
      spec/_files/empty_list.js
  12. +0
    -1
      spec/_files/empty_sections.js
  13. +0
    -1
      spec/_files/empty_template.js
  14. +0
    -1
      spec/_files/error_not_found.js
  15. +0
    -9
      spec/_files/higher_order_sections.js
  16. +0
    -3
      spec/_files/inverted_section.js
  17. +0
    -1
      spec/_files/multiline_comment.js
  18. +0
    -3
      spec/_files/partial_array_of_partials.js
  19. +0
    -3
      spec/_files/partial_array_of_partials_implicit.js
  20. +0
    -3
      spec/_files/partial_empty.js
  21. +0
    -6
      spec/_files/partial_template.js
  22. +0
    -4
      spec/_files/string_as_context.js
  23. +0
    -1
      spec/_files/two_sections.js
  24. +0
    -5
      spec/_files/unescaped.js
  25. +0
    -218
      spec/mustache_spec.rb
  26. +3
    -0
      test/_files/ampersand_escape.js
  27. +0
    -0
      test/_files/ampersand_escape.mustache
  28. +0
    -0
      test/_files/ampersand_escape.txt
  29. +4
    -0
      test/_files/apostrophe.js
  30. +0
    -0
      test/_files/apostrophe.mustache
  31. +0
    -0
      test/_files/apostrophe.txt
  32. +3
    -0
      test/_files/array_of_strings.js
  33. +0
    -0
      test/_files/array_of_strings.mustache
  34. +0
    -0
      test/_files/array_of_strings.txt
  35. +3
    -0
      test/_files/backslashes.js
  36. +0
    -0
      test/_files/backslashes.mustache
  37. +0
    -0
      test/_files/backslashes.txt
  38. +3
    -0
      test/_files/bug_11_eating_whitespace.js
  39. +0
    -0
      test/_files/bug_11_eating_whitespace.mustache
  40. +0
    -0
      test/_files/bug_11_eating_whitespace.txt
  41. +4
    -0
      test/_files/changing_delimiters.js
  42. +0
    -0
      test/_files/changing_delimiters.mustache
  43. +0
    -0
      test/_files/changing_delimiters.txt
  44. +5
    -0
      test/_files/comments.js
  45. +0
    -0
      test/_files/comments.mustache
  46. +0
    -0
      test/_files/comments.txt
  47. +6
    -6
      test/_files/complex.js
  48. +0
    -0
      test/_files/complex.mustache
  49. +0
    -0
      test/_files/complex.txt
  50. +2
    -2
      test/_files/context_lookup.js
  51. +0
    -0
      test/_files/context_lookup.mustache
  52. +0
    -0
      test/_files/context_lookup.txt
  53. +2
    -2
      test/_files/delimiters.js
  54. +0
    -0
      test/_files/delimiters.mustache
  55. +0
    -0
      test/_files/delimiters.txt
  56. +4
    -0
      test/_files/disappearing_whitespace.js
  57. +0
    -0
      test/_files/disappearing_whitespace.mustache
  58. +0
    -0
      test/_files/disappearing_whitespace.txt
  59. +5
    -5
      test/_files/dot_notation.js
  60. +1
    -1
      test/_files/dot_notation.mustache
  61. +0
    -0
      test/_files/dot_notation.txt
  62. +2
    -2
      test/_files/double_render.js
  63. +0
    -0
      test/_files/double_render.mustache
  64. +0
    -0
      test/_files/double_render.txt
  65. +3
    -0
      test/_files/empty_list.js
  66. +0
    -0
      test/_files/empty_list.mustache
  67. +0
    -0
      test/_files/empty_list.txt
  68. +1
    -0
      test/_files/empty_sections.js
  69. +0
    -0
      test/_files/empty_sections.mustache
  70. +0
    -0
      test/_files/empty_sections.txt
  71. +2
    -2
      test/_files/empty_string.js
  72. +0
    -0
      test/_files/empty_string.mustache
  73. +0
    -0
      test/_files/empty_string.txt
  74. +1
    -0
      test/_files/empty_template.js
  75. +0
    -0
      test/_files/empty_template.mustache
  76. +0
    -0
      test/_files/empty_template.txt
  77. +3
    -0
      test/_files/error_not_found.js
  78. +0
    -0
      test/_files/error_not_found.mustache
  79. +0
    -0
      test/_files/error_not_found.txt
  80. +3
    -3
      test/_files/escaped.js
  81. +0
    -0
      test/_files/escaped.mustache
  82. +0
    -0
      test/_files/escaped.txt
  83. +9
    -0
      test/_files/higher_order_sections.js
  84. +0
    -0
      test/_files/higher_order_sections.mustache
  85. +0
    -0
      test/_files/higher_order_sections.txt
  86. +2
    -2
      test/_files/included_tag.js
  87. +0
    -0
      test/_files/included_tag.mustache
  88. +0
    -0
      test/_files/included_tag.txt
  89. +3
    -0
      test/_files/inverted_section.js
  90. +0
    -0
      test/_files/inverted_section.mustache
  91. +0
    -0
      test/_files/inverted_section.txt
  92. +2
    -2
      test/_files/keys_with_questionmarks.js
  93. +0
    -0
      test/_files/keys_with_questionmarks.mustache
  94. +0
    -0
      test/_files/keys_with_questionmarks.txt
  95. +1
    -0
      test/_files/multiline_comment.js
  96. +0
    -0
      test/_files/multiline_comment.mustache
  97. +0
    -0
      test/_files/multiline_comment.txt
  98. +2
    -2
      test/_files/nested_iterating.js
  99. +0
    -0
      test/_files/nested_iterating.mustache
  100. +0
    -0
      test/_files/nested_iterating.txt

+ 9
- 11
Rakefile Просмотреть файл

@@ -1,20 +1,18 @@
require 'rake'
require 'rake/clean'

task :default => :spec
task :default => 'test:integration'

desc "Run all specs"
task :spec do
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec) do |t|
#t.spec_opts = ['--options', "\"#{File.dirname(__FILE__)}/spec/spec.opts\""]
t.pattern = 'spec/*_spec.rb'
namespace :test do
desc "Run all integration tests"
task :integration do
require File.expand_path('../test/integration', __FILE__)
end
end

desc "Run all unit tests"
task :test do
exec "vows test/*_test.js"
desc "Run all unit tests"
task :unit do
require File.expand_path('../test/unit', __FILE__)
end
end

# Creates a task that uses the various template wrappers to make a wrapped


+ 444
- 400
mustache.js Просмотреть файл

@@ -9,12 +9,18 @@ var Mustache = (typeof module !== "undefined" && module.exports) || {};
exports.name = "mustache.js";
exports.version = "0.5.0-dev";
exports.tags = ["{{", "}}"];

exports.parse = parse;
exports.clearCache = clearCache;
exports.compile = compile;
exports.compilePartial = compilePartial;
exports.render = render;
exports.clearCache = clearCache;

// This is here for backwards compatibility with 0.4.x.
exports.Scanner = Scanner;
exports.Context = Context;
exports.Renderer = Renderer;

// // This is here for backwards compatibility with 0.4.x.
exports.to_html = function (template, view, partials, send) {
var result = render(template, view, partials);

@@ -25,63 +31,30 @@ var Mustache = (typeof module !== "undefined" && module.exports) || {};
}
};

var _toString = Object.prototype.toString;
var _isArray = Array.isArray;
var _forEach = Array.prototype.forEach;
var _trim = String.prototype.trim;

var isArray;
if (_isArray) {
isArray = _isArray;
} else {
isArray = function (obj) {
return _toString.call(obj) === "[object Array]";
};
}

var forEach;
if (_forEach) {
forEach = function (obj, callback, scope) {
return _forEach.call(obj, callback, scope);
};
} else {
forEach = function (obj, callback, scope) {
for (var i = 0, len = obj.length; i < len; ++i) {
callback.call(scope, obj[i], i, obj);
}
};
}

var spaceRe = /^\s*$/;
var whiteRe = /\s*/;
var spaceRe = /\s+/;
var nonSpaceRe = /\S/;
var eqRe = /\s*=/;
var curlyRe = /\s*\}/;
var tagRe = /#|\^|\/|>|\{|&|=|!/;

function isWhitespace(string) {
return spaceRe.test(string);
return !nonSpaceRe.test(string);
}

var trim;
if (_trim) {
trim = function (string) {
return string == null ? "" : _trim.call(string);
};
} else {
var trimLeft, trimRight;
var isArray = Array.isArray || function (obj) {
return Object.prototype.toString.call(obj) === "[object Array]";
};

if (isWhitespace("\xA0")) {
trimLeft = /^\s+/;
trimRight = /\s+$/;
} else {
// IE doesn't match non-breaking spaces with \s, thanks jQuery.
trimLeft = /^[\s\xA0]+/;
trimRight = /[\s\xA0]+$/;
}
var quote = (typeof JSON !== "undefined" && JSON.stringify) || function (string) {
return '"' + String(string).replace(/(^|[^\\])"/g, '\\"') + '"';
};

trim = function (string) {
return string == null ? "" :
String(string).replace(trimLeft, "").replace(trimRight, "");
};
function escapeRe(string) {
return string.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}

var escapeMap = {
var entityMap = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
@@ -89,448 +62,519 @@ var Mustache = (typeof module !== "undefined" && module.exports) || {};
"'": '&#39;'
};

function escapeHTML(string) {
function escapeHtml(string) {
return String(string).replace(/&(?!\w+;)|[<>"']/g, function (s) {
return escapeMap[s] || s;
return entityMap[s];
});
}

// Export these utility functions.
exports.isWhitespace = isWhitespace;
exports.isArray = isArray;
exports.quote = quote;
exports.escapeRe = escapeRe;
exports.escapeHtml = escapeHtml;

function Scanner(string) {
this.string = string;
this.tail = string;
this.pos = 0;
}

/**
* Adds the `template`, `line`, and `file` properties to the given error
* object and alters the message to provide more useful debugging information.
* Returns `true` if the tail is empty (end of string).
*/
function debug(e, template, line, file) {
file = file || "<template>";

var lines = template.split("\n"),
start = Math.max(line - 3, 0),
end = Math.min(lines.length, line + 3),
context = lines.slice(start, end);

var c;
for (var i = 0, len = context.length; i < len; ++i) {
c = i + start + 1;
context[i] = (c === line ? " >> " : " ") + context[i];
}
Scanner.prototype.eos = function () {
return this.tail === "";
};

e.template = template;
e.line = line;
e.file = file;
e.message = [file + ":" + line, context.join("\n"), "", e.message].join("\n");
/**
* Tries to match the given regular expression at the current position.
* Returns the matched text if it can match, `null` otherwise.
*/
Scanner.prototype.scan = function (re) {
var match = this.tail.match(re);

return e;
}
if (match && match.index === 0) {
this.tail = this.tail.substring(match[0].length);
this.pos += match[0].length;
return match[0];
}

return null;
};

/**
* Looks up the value of the given `name` in the given context `stack`.
* Skips all text until the given regular expression can be matched. Returns
* the skipped string, which is the entire tail of this scanner if no match
* can be made.
*/
function lookup(name, stack, defaultValue) {
if (name === ".") {
return stack[stack.length - 1];
Scanner.prototype.scanUntil = function (re) {
var match, pos = this.tail.search(re);

switch (pos) {
case -1:
match = this.tail;
this.pos += this.tail.length;
this.tail = "";
break;
case 0:
match = null;
break;
default:
match = this.tail.substring(0, pos);
this.tail = this.tail.substring(pos);
this.pos += pos;
}

var names = name.split(".");
var lastIndex = names.length - 1;
var target = names[lastIndex];
return match;
};

var value, context, i = stack.length, j, localStack;
while (i) {
localStack = stack.slice(0);
context = stack[--i];
function Context(view, parent) {
this.view = view;
this.parent = parent;
this.clearCache();
}

j = 0;
while (j < lastIndex) {
context = context[names[j++]];
Context.make = function (view) {
return (view instanceof Context) ? view : new Context(view);
};

if (context == null) {
break;
}
Context.prototype.clearCache = function () {
this._cache = {};
};

localStack.push(context);
}
Context.prototype.push = function (view) {
return new Context(view, this);
};

if (context && typeof context === "object" && target in context) {
value = context[target];
break;
Context.prototype.lookup = function (name) {
var value = this._cache[name];

if (!value) {
if (name === ".") {
value = this.view;
} else {
var context = this;

while (context) {
if (name.indexOf(".") > 0) {
var names = name.split("."), i = 0;

value = context.view;

while (value && i < names.length) {
value = value[names[i++]];
}
} else {
value = context.view[name];
}

if (value != null) {
break;
}

context = context.parent;
}
}
}

// If the value is a function, call it in the current context.
if (typeof value === "function") {
value = value.call(localStack[localStack.length - 1]);
this._cache[name] = value;
}

if (value == null) {
return defaultValue;
if (typeof value === "function") {
value = value.call(this.view);
}

return value;
};

function Renderer() {
this.clearCache();
}

function renderSection(name, stack, callback, inverted) {
var buffer = "";
var value = lookup(name, stack);
Renderer.prototype.clearCache = function () {
this._cache = {};
this._partialCache = {};
};

if (inverted) {
// From the spec: inverted sections may render text once based on the
// inverse value of the key. That is, they will be rendered if the key
// doesn't exist, is false, or is an empty list.
if (value == null || value === false || (isArray(value) && value.length === 0)) {
buffer += callback();
}
} else if (isArray(value)) {
forEach(value, function (value) {
stack.push(value);
buffer += callback();
stack.pop();
});
} else if (typeof value === "object") {
stack.push(value);
buffer += callback();
stack.pop();
} else if (typeof value === "function") {
var scope = stack[stack.length - 1];
var scopedRender = function (template) {
return render(template, scope);
};
buffer += value.call(scope, callback(), scopedRender) || "";
} else if (value) {
buffer += callback();
Renderer.prototype.compile = function (tokens, tags) {
var fn = compileTokens(tokens),
self = this;

return function (view) {
return fn(Context.make(view), self);
};
};

Renderer.prototype.compilePartial = function (name, tokens, tags) {
this._partialCache[name] = this.compile(tokens, tags);
return this._partialCache[name];
};

Renderer.prototype.render = function (template, view) {
var fn = this._cache[template];

if (!fn) {
fn = this.compile(template);
this._cache[template] = fn;
}

return buffer;
}
return fn(view);
};

/**
* Parses the given `template` and returns the source of a function that,
* with the proper arguments, will render the template. Recognized options
* include the following:
*
* - file The name of the file the template comes from (displayed in
* error messages)
* - tags An array of open and close tags the `template` uses. Defaults
* to the value of Mustache.tags
* - debug Set `true` to log the body of the generated function to the
* console
* - space Set `true` to preserve whitespace from lines that otherwise
* contain only a {{tag}}. Defaults to `false`
*/
function parse(template, options) {
options = options || {};

var tags = options.tags || exports.tags,
openTag = tags[0],
closeTag = tags[tags.length - 1];

var code = [
'var buffer = "";', // output buffer
"\nvar line = 1;", // keep track of source line number
"\ntry {",
'\nbuffer += "'
];

var spaces = [], // indices of whitespace in code on the current line
hasTag = false, // is there a {{tag}} on the current line?
nonSpace = false; // is there a non-space char on the current line?

// Strips all space characters from the code array for the current line
// if there was a {{tag}} on it and otherwise only spaces.
var stripSpace = function () {
if (hasTag && !nonSpace && !options.space) {
while (spaces.length) {
code.splice(spaces.pop(), 1);
Renderer.prototype._section = function (name, context, callback) {
var value = context.lookup(name);

switch (typeof value) {
case "object":
if (isArray(value)) {
var buffer = "";
for (var i = 0, len = value.length; i < len; ++i) {
buffer += callback(context.push(value[i]), this);
}
return buffer;
} else {
spaces = [];
return callback(context.push(value), this);
}
break;
case "function":
var sectionText = callback(context, this), self = this;
function scopedRender(template) {
return self.render(template, context);
}
return value.call(context.view, sectionText, scopedRender) || "";
break;
default:
if (value) {
return callback(context, this);
}
}

hasTag = false;
nonSpace = false;
};
return "";
};

var sectionStack = [], updateLine, nextOpenTag, nextCloseTag;
Renderer.prototype._inverted = function (name, context, callback) {
var value = context.lookup(name);

var setTags = function (source) {
tags = trim(source).split(/\s+/);
nextOpenTag = tags[0];
nextCloseTag = tags[tags.length - 1];
};
// From the spec: inverted sections may render text once based on the
// inverse value of the key. That is, they will be rendered if the key
// doesn't exist, is false, or is an empty list.
if (value == null || value === false || (isArray(value) && value.length === 0)) {
return callback(context, this);
}

var includePartial = function (source) {
code.push(
'";',
updateLine,
'\nvar partial = partials["' + trim(source) + '"];',
'\nif (partial) {',
'\n buffer += render(partial,stack[stack.length - 1],partials);',
'\n}',
'\nbuffer += "'
);
};
return "";
};

var openSection = function (source, inverted) {
var name = trim(source);
Renderer.prototype._partial = function (name, context) {
var fn = this._partialCache[name];

if (name === "") {
throw debug(new Error("Section name may not be empty"), template, line, options.file);
}
if (fn) {
return fn(context, this);
}

sectionStack.push({name: name, inverted: inverted});

code.push(
'";',
updateLine,
'\nvar name = "' + name + '";',
'\nvar callback = (function () {',
'\n return function () {',
'\n var buffer = "";',
'\nbuffer += "'
);
};
return "";
};

var openInvertedSection = function (source) {
openSection(source, true);
};
Renderer.prototype._name = function (name, context, escape) {
var value = context.lookup(name);

var closeSection = function (source) {
var name = trim(source);
var openName = sectionStack.length != 0 && sectionStack[sectionStack.length - 1].name;
if (typeof value === "function") {
value = value.call(context.view);
}

if (!openName || name != openName) {
throw debug(new Error('Section named "' + name + '" was never opened'), template, line, options.file);
}
var string = (value == null) ? "" : String(value);

var section = sectionStack.pop();
if (escape) {
return escapeHtml(string);
}

code.push(
'";',
'\n return buffer;',
'\n };',
'\n})();'
);
return string;
};

if (section.inverted) {
code.push("\nbuffer += renderSection(name,stack,callback,true);");
} else {
code.push("\nbuffer += renderSection(name,stack,callback);");
/**
* Low-level function that compiles the given `tokens` into a
* function that accepts two arguments: a Context and a
* Renderer. Returns the body of the function as a string if
* `returnBody` is true.
*/
function compileTokens(tokens, returnBody) {
if (typeof tokens === "string") {
tokens = parse(tokens);
}

var body = ['""'];
var token, method, escape;

for (var i = 0, len = tokens.length; i < len; ++i) {
token = tokens[i];

switch (token.type) {
case "#":
case "^":
method = (token.type === "#") ? "_section" : "_inverted";
body.push("r." + method + "(" + quote(token.value) + ", c, function (c, r) {\n" +
" " + compileTokens(token.tokens, true) + "\n" +
"})");
break;
case "{":
case "&":
case "name":
escape = token.type === "name" ? "true" : "false";
body.push("r._name(" + quote(token.value) + ", c, " + escape + ")");
break;
case ">":
body.push("r._partial(" + quote(token.value) + ", c)");
break;
case "text":
body.push(quote(token.value));
break;
}
}

code.push('\nbuffer += "');
};
// Convert to a string body.
body = "return " + body.join(" + ") + ";";

var sendPlain = function (source) {
code.push(
'";',
updateLine,
'\nbuffer += lookup("' + trim(source) + '",stack,"");',
'\nbuffer += "'
);
};
// Good for debugging.
// console.log(body);

var sendEscaped = function (source) {
code.push(
'";',
updateLine,
'\nbuffer += escapeHTML(lookup("' + trim(source) + '",stack,""));',
'\nbuffer += "'
);
};
if (returnBody) {
return body;
}

var line = 1, c, callback;
for (var i = 0, len = template.length; i < len; ++i) {
if (template.slice(i, i + openTag.length) === openTag) {
i += openTag.length;
c = template.substr(i, 1);
updateLine = '\nline = ' + line + ';';
nextOpenTag = openTag;
nextCloseTag = closeTag;
hasTag = true;

switch (c) {
case "!": // comment
i++;
callback = null;
break;
case "=": // change open/close tags, e.g. {{=<% %>=}}
i++;
closeTag = "=" + closeTag;
callback = setTags;
break;
case ">": // include partial
i++;
callback = includePartial;
break;
case "#": // start section
i++;
callback = openSection;
break;
case "^": // start inverted section
i++;
callback = openInvertedSection;
break;
case "/": // end section
i++;
callback = closeSection;
break;
case "{": // plain variable
closeTag = "}" + closeTag;
// fall through
case "&": // plain variable
i++;
nonSpace = true;
callback = sendPlain;
break;
default: // escaped variable
nonSpace = true;
callback = sendEscaped;
}
// For great evil!
return new Function("c, r", body);
}

var end = template.indexOf(closeTag, i);
function escapeTags(tags) {
if (tags.length === 2) {
return [
new RegExp(escapeRe(tags[0]) + "\\s*"),
new RegExp("\\s*" + escapeRe(tags[1]))
];
}

if (end === -1) {
throw debug(new Error('Tag "' + openTag + '" was not closed properly'), template, line, options.file);
throw new Error("Invalid tags: " + tags.join(" "));
}

/**
* Forms the given linear array of `tokens` into a nested tree structure
* where tokens that represent a section have a "tokens" array property
* that contains all tokens that are in that section.
*/
function nestTokens(tokens) {
var tree = [];
var collector = tree;
var sections = [];
var token, section;

for (var i = 0; i < tokens.length; ++i) {
token = tokens[i];

switch (token.type) {
case "#":
case "^":
token.tokens = [];
sections.push(token);
collector.push(token);
collector = token.tokens;
break;
case "/":
if (sections.length === 0) {
throw new Error("Unopened section: " + token.value);
}

var source = template.substring(i, end);
section = sections.pop();

if (callback) {
callback(source);
if (section.value !== token.value) {
throw new Error("Unclosed section: " + section.value);
}

// Maintain line count for \n in source.
var n = 0;
while (~(n = source.indexOf("\n", n))) {
line++;
n++;
if (sections.length > 0) {
collector = sections[sections.length - 1].tokens;
} else {
collector = tree;
}
break;
default:
collector.push(token);
}
}

// Make sure there were no open sections when we're done.
section = sections.pop();

if (section) {
throw new Error("Unclosed section: " + section.value);
}

return tree;
}

/**
* Combines the values of consecutive text tokens in the given `tokens` array
* to a single token.
*/
function squashTokens(tokens) {
var lastToken;

for (var i = 0; i < tokens.length; ++i) {
token = tokens[i];

if (lastToken && lastToken.type === "text" && token.type === "text") {
lastToken.value += token.value;
tokens.splice(i--, 1); // Remove this token from the array.
} else {
lastToken = token;
}
}
}

i = end + closeTag.length - 1;
openTag = nextOpenTag;
closeTag = nextCloseTag;
/**
* Breaks up the given `template` string into a tree of token objects. If
* `tags` is given here it must be an array with two string values: the
* opening and closing tags used in the template (e.g. ["<%", "%>"]). Of
* course, the default is to use mustaches (i.e. Mustache.tags).
*/
function parse(template, tags) {
tags = escapeTags(tags || exports.tags);

var scanner = new Scanner(template);

var tokens = [], // Buffer to hold the tokens
spaces = [], // Indices of whitespace tokens on the current line
hasTag = false, // Is there a {{tag}} on the current line?
nonSpace = 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.
function stripSpace() {
if (hasTag && !nonSpace) {
while (spaces.length) {
tokens.splice(spaces.pop(), 1);
}
} else {
c = template.substr(i, 1);

switch (c) {
case '"':
case "\\":
nonSpace = true;
code.push("\\" + c);
break;
case "\r":
// Ignore carriage returns.
break;
case "\n":
spaces.push(code.length);
code.push("\\n");
stripSpace(); // Check for whitespace on the current line.
line++;
break;
default:
if (isWhitespace(c)) {
spaces.push(code.length);
spaces = [];
}

hasTag = false;
nonSpace = false;
}

var type, value, chr;

while (!scanner.eos()) {
value = scanner.scanUntil(tags[0]);

if (value) {
for (var i = 0, len = value.length; i < len; ++i) {
chr = value[i];

if (isWhitespace(chr)) {
spaces.push(tokens.length);
} else {
nonSpace = true;
}

code.push(c);
tokens.push({type: "text", value: chr});

if (chr === "\n") {
stripSpace(); // Check for whitespace on the current line.
}
}
}
}

if (sectionStack.length != 0) {
throw debug(new Error('Section "' + sectionStack[sectionStack.length - 1].name + '" was not closed properly'), template, line, options.file);
}
// Match the opening tag.
if (!scanner.scan(tags[0])) {
break;
}

hasTag = true;
type = scanner.scan(tagRe) || "name";

// Skip any whitespace between tag and value.
scanner.scan(whiteRe);

// Extract the tag value.
if (type === "=") {
value = scanner.scanUntil(eqRe);
scanner.scan(eqRe);
scanner.scanUntil(tags[1]);
} else if (type === "{") {
value = scanner.scanUntil(curlyRe);
scanner.scan(curlyRe);
scanner.scanUntil(tags[1]);
} else {
value = scanner.scanUntil(tags[1]);
}

// Clean up any whitespace from a closing {{tag}} that was at the end
// of the template without a trailing \n.
stripSpace();
// Match the closing tag.
if (!scanner.scan(tags[1])) {
throw new Error("Unclosed tag at " + scanner.pos);
}

code.push(
'";',
"\nreturn buffer;",
"\n} catch (e) { throw {error: e, line: line}; }"
);
tokens.push({type: type, value: value});

// Ignore `buffer += "";` statements.
var body = code.join("").replace(/buffer \+= "";\n/g, "");
if (type === "name" || type === "{" || type === "&") {
nonSpace = true;
}

if (options.debug) {
if (typeof console != "undefined" && console.log) {
console.log(body);
} else if (typeof print === "function") {
print(body);
// Set the tags for the next time around.
if (type === "=") {
tags = escapeTags(value.split(spaceRe));
}
}

return body;
}
squashTokens(tokens);

/**
* Used by `compile` to generate a reusable function for the given `template`.
*/
function _compile(template, options) {
var args = "view,partials,stack,lookup,escapeHTML,renderSection,render";
var body = parse(template, options);
var fn = new Function(args, body);

// This anonymous function wraps the generated function so we can do
// argument coercion, setup some variables, and handle any errors
// encountered while executing it.
return function (view, partials) {
partials = partials || {};

var stack = [view]; // context stack

try {
return fn(view, partials, stack, lookup, escapeHTML, renderSection, render);
} catch (e) {
throw debug(e.error, template, e.line, options.file);
}
};
return nestTokens(tokens);
}

// Cache of pre-compiled templates.
var _cache = {};
// The high-level clearCache, compile, compilePartial, and render functions
// use this default renderer.
var _renderer = new Renderer;

/**
* Clear the cache of compiled templates.
* Clears all cached templates and partials.
*/
function clearCache() {
_cache = {};
_renderer.clearCache();
}

/**
* Compiles the given `template` into a reusable function using the given
* `options`. In addition to the options accepted by Mustache.parse,
* recognized options include the following:
*
* - cache Set `false` to bypass any pre-compiled version of the given
* template. Otherwise, a given `template` string will be cached
* the first time it is parsed
* High-level API for compiling the given `tokens` down to a reusable
* function. If `tokens` is a string it will be parsed using the given `tags`
* before it is compiled.
*/
function compile(template, options) {
options = options || {};

// Use a pre-compiled version from the cache if we have one.
if (options.cache !== false) {
if (!_cache[template]) {
_cache[template] = _compile(template, options);
}

return _cache[template];
}
function compile(tokens, tags) {
return _renderer.compile(tokens, tags);
}

return _compile(template, options);
/**
* High-level API for compiling the `tokens` for the partial with the given
* `name` down to a reusable function. If `tokens` is a string it will be
* parsed using the given `tags` before it is compiled.
*/
function compilePartial(name, tokens, tags) {
return _renderer.compilePartial(name, tokens, tags);
}

/**
* High-level function that renders the given `template` using the given
* `view` and `partials`. If you need to use any of the template options (see
* `compile` above), you must compile in a separate step, and then call that
* compiled function.
* High-level API for rendering the `template` using the given `view`. The
* optional `partials` object may be given here for convenience, but note that
* it will cause all partials to be re-compiled, thus hurting performance. Of
* course, this only matters if you're going to render the same template more
* than once. If so, it is best to call `compilePartial` before calling this
* function and to leave the `partials` argument blank.
*/
function render(template, view, partials) {
return compile(template)(view, partials);
if (partials) {
for (var name in partials) {
compilePartial(name, partials[name]);
}
}

return _renderer.render(template, view);
}

})(Mustache);

+ 0
- 3
spec/_files/ampersand_escape.js Просмотреть файл

@@ -1,3 +0,0 @@
var ampersand_escape = {
message: "Some <code>"
};

+ 0
- 1
spec/_files/apostrophe.js Просмотреть файл

@@ -1 +0,0 @@
var apostrophe = {'apos': "'", 'control':'X'};

+ 0
- 1
spec/_files/array_of_strings.js Просмотреть файл

@@ -1 +0,0 @@
var array_of_strings = {array_of_strings: ['hello', 'world']};

+ 0
- 3
spec/_files/backslashes.js Просмотреть файл

@@ -1,3 +0,0 @@
var backslashes = {
value: "\\abc"
};

+ 0
- 3
spec/_files/bug_11_eating_whitespace.js Просмотреть файл

@@ -1,3 +0,0 @@
var bug_11_eating_whitespace = {
tag: "yo"
};

+ 0
- 4
spec/_files/changing_delimiters.js Просмотреть файл

@@ -1,4 +0,0 @@
var changing_delimiters = {
"foo": "foooooooooooooo",
"bar":"<b>bar!</b>"
};

+ 0
- 5
spec/_files/comments.js Просмотреть файл

@@ -1,5 +0,0 @@
var comments = {
title: function() {
return "A Comedy of Errors";
}
};

+ 0
- 4
spec/_files/disappearing_whitespace.js Просмотреть файл

@@ -1,4 +0,0 @@
var disappearing_whitespace = {
bedrooms: true,
total: 1
};

+ 0
- 3
spec/_files/empty_list.js Просмотреть файл

@@ -1,3 +0,0 @@
var empty_list = {
jobs: []
};

+ 0
- 1
spec/_files/empty_sections.js Просмотреть файл

@@ -1 +0,0 @@
var empty_sections = {};

+ 0
- 1
spec/_files/empty_template.js Просмотреть файл

@@ -1 +0,0 @@
var empty_template = {};

+ 0
- 1
spec/_files/error_not_found.js Просмотреть файл

@@ -1 +0,0 @@
var error_not_found = {bar: 2};

+ 0
- 9
spec/_files/higher_order_sections.js Просмотреть файл

@@ -1,9 +0,0 @@
var higher_order_sections = {
"name": "Tater",
"helper": "To tinker?",
"bolder": function() {
return function(text, render) {
return "<b>" + render(text) + '</b> ' + this.helper;
}
}
}

+ 0
- 3
spec/_files/inverted_section.js Просмотреть файл

@@ -1,3 +0,0 @@
var inverted_section = {
"repos": []
};

+ 0
- 1
spec/_files/multiline_comment.js Просмотреть файл

@@ -1 +0,0 @@
var multiline_comment = {};

+ 0
- 3
spec/_files/partial_array_of_partials.js Просмотреть файл

@@ -1,3 +0,0 @@
var partial_array_of_partials = {
numbers: [{i: '1'}, {i: '2'}, {i: '3'}, {i: '4'}]
};

+ 0
- 3
spec/_files/partial_array_of_partials_implicit.js Просмотреть файл

@@ -1,3 +0,0 @@
var partial_array_of_partials_implicit = {
numbers: ['1', '2', '3', '4']
};

+ 0
- 3
spec/_files/partial_empty.js Просмотреть файл

@@ -1,3 +0,0 @@
var partial_empty = {
foo: 1
};

+ 0
- 6
spec/_files/partial_template.js Просмотреть файл

@@ -1,6 +0,0 @@
var partial_template = {
title: function() {
return "Welcome";
},
again: "Goodbye"
};

+ 0
- 4
spec/_files/string_as_context.js Просмотреть файл

@@ -1,4 +0,0 @@
var string_as_context = {
a_string: 'aa',
a_list: ['a','b','c']
};

+ 0
- 1
spec/_files/two_sections.js Просмотреть файл

@@ -1 +0,0 @@
var two_sections = {};

+ 0
- 5
spec/_files/unescaped.js Просмотреть файл

@@ -1,5 +0,0 @@
var unescaped = {
title: function() {
return "Bear > Shark";
}
};

+ 0
- 218
spec/mustache_spec.rb Просмотреть файл

@@ -1,218 +0,0 @@
require 'rubygems'
require 'json'

ROOT = File.expand_path('../..', __FILE__)
SPEC = File.join(ROOT, 'spec')
FILES = File.join(SPEC, '_files')

MUSTACHE = File.read(File.join(ROOT, "mustache.js"))

TESTS = Dir.glob(File.join(FILES, '*.js')).map do |name|
File.basename name, '.js'
end

NODE_PATH = `which node`.strip
JS_PATH = `which js`.strip
JSC_PATH = "/System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Resources/jsc"
RHINO_JAR = "org.mozilla.javascript.tools.shell.Main"

def load_test(name)
template = File.read(File.join(FILES, "#{name}.mustache"))
view = File.read(File.join(FILES, "#{name}.js"))
partial_file = File.join(FILES, "#{name}.partial")
partial = if File.exist?(partial_file)
File.read(partial_file)
end
expect = File.read(File.join(FILES, "#{name}.txt"))

[template, view, partial, expect]
end

def run_js(runner, js)
cmd = case runner
when :spidermonkey
JS_PATH
when :jsc
JSC_PATH
when :rhino
"java #{RHINO_JAR}"
when :v8
NODE_PATH
end

runner_file = "runner.js"
File.open(runner_file, 'w') {|file| file.write(js) }
`#{cmd} #{runner_file}`
ensure
FileUtils.rm_r(runner_file)
end

$engines_run = 0

describe "mustache" do
shared_examples_for "mustache rendering" do
before(:all) do
$engines_run += 1
end

it "should return the same result when invoked multiple times" do
js = <<-JS
#{@boilerplate}
Mustache.render("x")
print(Mustache.render("x"));
JS

run_js(@runner, js).should == "x\n"
end

it "should clear the context after each run" do
js = <<-JS
#{@boilerplate}
Mustache.render("{{#list}}{{x}}{{/list}}", {list: [{x: 1}]})
try {
print(Mustache.render("{{#list}}{{x}}{{/list}}", {list: [{}]}));
} catch(e) {
print('ERROR: ' + e.message);
}
JS

run_js(@runner, js).should == "\n"
end

TESTS.each do |test|
describe test do
it "should render the correct output" do
template, view, partial, expect = load_test(test)

js = <<-JS
try {
#{@boilerplate}
var template = #{template.to_json};
#{view}
var partials = {partial: #{partial ? partial.to_json : '""'}};
print(Mustache.render(template, #{test}, partials));
} catch(e) {
print('ERROR: ' + e.message);
}
JS

run_js(@runner, js).chomp.should == expect
end

# it "should send the correct output" do
# template, view, partial, expect = load_test(test)
#
# js = <<-JS
# try {
# #{@boilerplate}
# var template = #{template.to_json};
# #{view}
# var partials = {
# "partial": #{(partial || '').to_json}
# };
# var buffer = [];
# var send = function (chunk) {
# buffer.push(chunk);
# };
# Mustache.render(template, #{test}, partials, send);
# print(buffer.join(""));
# } catch(e) {
# print('ERROR: ' + e.message);
# }
# JS
#
# run_js(@runner, js).chomp.should == expect
# end
end
end
end

context "running in V8 (Chrome, node)" do
if File.exist?(NODE_PATH)
before(:all) do
$stdout.write "Testing in V8 "
@runner = :v8
@boilerplate = MUSTACHE.dup
@boilerplate << <<-JS
var print = console.log;
JS
end

after(:all) do
puts " Done!"
end

it_should_behave_like "mustache rendering"
else
puts "Skipping tests in V8 (node not found)"
end
end

context "running in SpiderMonkey (Mozilla, Firefox)" do
if File.exist?(JS_PATH)
before(:all) do
$stdout.write "Testing in SpiderMonkey "
@runner = :spidermonkey
@boilerplate = MUSTACHE.dup
end

after(:all) do
puts " Done!"
end

it_should_behave_like "mustache rendering"
else
puts "Skipping tests in SpiderMonkey (js not found)"
end
end

context "running in JavaScriptCore (WebKit, Safari)" do
if File.exist?(JSC_PATH)
before(:all) do
$stdout.write "Testing in JavaScriptCore "
@runner = :jsc
@boilerplate = MUSTACHE.dup
end

after(:all) do
puts " Done!"
end

it_should_behave_like "mustache rendering"
else
puts "Skipping tests in JavaScriptCore (jsc not found)"
end
end

context "running in Rhino (Mozilla, Java)" do
if `java #{RHINO_JAR} 'foo' 2>&1` !~ /ClassNotFoundException/
before(:all) do
$stdout.write "Testing in Rhino "
@runner = :rhino
@boilerplate = MUSTACHE.dup
end

after(:all) do
puts " Done!"
end

it_should_behave_like "mustache rendering"
else
puts "Skipping tests in Rhino (JAR #{RHINO_JAR} was not found)"
end
end

context "suite" do
before(:each) do
$stdout.write "Verifying that we ran the tests in at least one engine ... "
end

after(:each) do
puts @exception.nil? ? "OK" : "ERROR"
end

it "should have run at least one time" do
$engines_run.should > 0
end
end
end

+ 3
- 0
test/_files/ampersand_escape.js Просмотреть файл

@@ -0,0 +1,3 @@
({
message: "Some <code>"
})

spec/_files/ampersand_escape.mustache → test/_files/ampersand_escape.mustache Просмотреть файл


spec/_files/ampersand_escape.txt → test/_files/ampersand_escape.txt Просмотреть файл


+ 4
- 0
test/_files/apostrophe.js Просмотреть файл

@@ -0,0 +1,4 @@
({
'apos': "'",
'control': 'X'
})

spec/_files/apostrophe.mustache → test/_files/apostrophe.mustache Просмотреть файл


spec/_files/apostrophe.txt → test/_files/apostrophe.txt Просмотреть файл


+ 3
- 0
test/_files/array_of_strings.js Просмотреть файл

@@ -0,0 +1,3 @@
({
array_of_strings: ['hello', 'world']
})

spec/_files/array_of_strings.mustache → test/_files/array_of_strings.mustache Просмотреть файл


spec/_files/array_of_strings.txt → test/_files/array_of_strings.txt Просмотреть файл


+ 3
- 0
test/_files/backslashes.js Просмотреть файл

@@ -0,0 +1,3 @@
({
value: "\\abc"
})

spec/_files/backslashes.mustache → test/_files/backslashes.mustache Просмотреть файл


spec/_files/backslashes.txt → test/_files/backslashes.txt Просмотреть файл


+ 3
- 0
test/_files/bug_11_eating_whitespace.js Просмотреть файл

@@ -0,0 +1,3 @@
({
tag: "yo"
})

spec/_files/bug_11_eating_whitespace.mustache → test/_files/bug_11_eating_whitespace.mustache Просмотреть файл


spec/_files/bug_11_eating_whitespace.txt → test/_files/bug_11_eating_whitespace.txt Просмотреть файл


+ 4
- 0
test/_files/changing_delimiters.js Просмотреть файл

@@ -0,0 +1,4 @@
({
"foo": "foooooooooooooo",
"bar": "<b>bar!</b>"
})

spec/_files/changing_delimiters.mustache → test/_files/changing_delimiters.mustache Просмотреть файл


spec/_files/changing_delimiters.txt → test/_files/changing_delimiters.txt Просмотреть файл


+ 5
- 0
test/_files/comments.js Просмотреть файл

@@ -0,0 +1,5 @@
({
title: function () {
return "A Comedy of Errors";
}
})

spec/_files/comments.mustache → test/_files/comments.mustache Просмотреть файл


spec/_files/comments.txt → test/_files/comments.txt Просмотреть файл


spec/_files/complex.js → test/_files/complex.js Просмотреть файл

@@ -1,5 +1,5 @@
var complex = {
header: function() {
({
header: function () {
return "Colors";
},
item: [
@@ -7,13 +7,13 @@ var complex = {
{name: "green", current: false, url: "#Green"},
{name: "blue", current: false, url: "#Blue"}
],
link: function() {
link: function () {
return this["current"] !== true;
},
list: function() {
list: function () {
return this.item.length !== 0;
},
empty: function() {
empty: function () {
return this.item.length === 0;
}
};
})

spec/_files/complex.mustache → test/_files/complex.mustache Просмотреть файл


spec/_files/complex.txt → test/_files/complex.txt Просмотреть файл


spec/_files/context_lookup.js → test/_files/context_lookup.js Просмотреть файл

@@ -1,8 +1,8 @@
var context_lookup = {
({
"outer": {
"id": 1,
"second": {
"nothing": 2
}
}
};
})

spec/_files/context_lookup.mustache → test/_files/context_lookup.mustache Просмотреть файл


spec/_files/context_lookup.txt → test/_files/context_lookup.txt Просмотреть файл


spec/_files/delimiters.js → test/_files/delimiters.js Просмотреть файл

@@ -1,6 +1,6 @@
var delimiters = {
({
first: "It worked the first time.",
second: "And it worked the second time.",
third: "Then, surprisingly, it worked the third time.",
fourth: "Fourth time also fine!."
}
})

spec/_files/delimiters.mustache → test/_files/delimiters.mustache Просмотреть файл


spec/_files/delimiters.txt → test/_files/delimiters.txt Просмотреть файл


+ 4
- 0
test/_files/disappearing_whitespace.js Просмотреть файл

@@ -0,0 +1,4 @@
({
bedrooms: true,
total: 1
})

spec/_files/disappearing_whitespace.mustache → test/_files/disappearing_whitespace.mustache Просмотреть файл


spec/_files/disappearing_whitespace.txt → test/_files/disappearing_whitespace.txt Просмотреть файл


spec/_files/dot_notation.js → test/_files/dot_notation.js Просмотреть файл

@@ -1,9 +1,9 @@
var dot_notation = {
({
name: "A Book",
authors: ["John Power", "Jamie Walsh"],
price:{
price: {
value: 200,
vat: function() {
vat: function () {
return this.value * 0.2;
},
currency: {
@@ -11,7 +11,7 @@ var dot_notation = {
name: 'Euro'
}
},
availability:{
availability: {
status: true,
text: "In Stock"
},
@@ -20,4 +20,4 @@ var dot_notation = {
zero: 0,
notTrue: false
}
};
})

spec/_files/dot_notation.mustache → test/_files/dot_notation.mustache Просмотреть файл

@@ -2,7 +2,7 @@
<h1>{{name}}</h1>
<p>Authors: <ul>{{#authors}}<li>{{.}}</li>{{/authors}}</ul></p>
<p>Price: {{price.currency.symbol}}{{price.value}} {{#price.currency}}{{name}} <b>{{availability.text}}</b>{{/price.currency}}</p>
<p>VAT: {{price.currency.symbol}}{{price.vat}}</p>
<p>VAT: {{price.currency.symbol}}{{#price}}{{vat}}{{/price}}</p>
<!-- boring part -->
<h2>Test truthy false values:</h2>
<p>Zero: {{truthy.zero}}</p>

spec/_files/dot_notation.txt → test/_files/dot_notation.txt Просмотреть файл


spec/_files/double_render.js → test/_files/double_render.js Просмотреть файл

@@ -1,5 +1,5 @@
var double_render = {
({
foo: true,
bar: "{{win}}",
win: "FAIL"
};
})

spec/_files/double_render.mustache → test/_files/double_render.mustache Просмотреть файл


spec/_files/double_render.txt → test/_files/double_render.txt Просмотреть файл


+ 3
- 0
test/_files/empty_list.js Просмотреть файл

@@ -0,0 +1,3 @@
({
jobs: []
})

spec/_files/empty_list.mustache → test/_files/empty_list.mustache Просмотреть файл


spec/_files/empty_list.txt → test/_files/empty_list.txt Просмотреть файл


+ 1
- 0
test/_files/empty_sections.js Просмотреть файл

@@ -0,0 +1 @@
({})

spec/_files/empty_sections.mustache → test/_files/empty_sections.mustache Просмотреть файл


spec/_files/empty_sections.txt → test/_files/empty_sections.txt Просмотреть файл


spec/_files/empty_string.js → test/_files/empty_string.js Просмотреть файл

@@ -1,6 +1,6 @@
var empty_string = {
({
description: "That is all!",
child: {
description: ""
}
};
})

spec/_files/empty_string.mustache → test/_files/empty_string.mustache Просмотреть файл


spec/_files/empty_string.txt → test/_files/empty_string.txt Просмотреть файл


+ 1
- 0
test/_files/empty_template.js Просмотреть файл

@@ -0,0 +1 @@
({})

spec/_files/empty_template.mustache → test/_files/empty_template.mustache Просмотреть файл


spec/_files/empty_template.txt → test/_files/empty_template.txt Просмотреть файл


+ 3
- 0
test/_files/error_not_found.js Просмотреть файл

@@ -0,0 +1,3 @@
({
bar: 2
})

spec/_files/error_not_found.mustache → test/_files/error_not_found.mustache Просмотреть файл


spec/_files/error_not_found.txt → test/_files/error_not_found.txt Просмотреть файл


spec/_files/escaped.js → test/_files/escaped.js Просмотреть файл

@@ -1,6 +1,6 @@
var escaped = {
title: function() {
({
title: function () {
return "Bear > Shark";
},
entities: "&quot;"
};
})

spec/_files/escaped.mustache → test/_files/escaped.mustache Просмотреть файл


spec/_files/escaped.txt → test/_files/escaped.txt Просмотреть файл


+ 9
- 0
test/_files/higher_order_sections.js Просмотреть файл

@@ -0,0 +1,9 @@
({
name: "Tater",
helper: "To tinker?",
bolder: function () {
return function (text, render) {
return "<b>" + render(text) + '</b> ' + this.helper;
}
}
})

spec/_files/higher_order_sections.mustache → test/_files/higher_order_sections.mustache Просмотреть файл


spec/_files/higher_order_sections.txt → test/_files/higher_order_sections.txt Просмотреть файл


spec/_files/included_tag.js → test/_files/included_tag.js Просмотреть файл

@@ -1,3 +1,3 @@
var included_tag = {
({
html: "I like {{mustache}}"
};
})

spec/_files/included_tag.mustache → test/_files/included_tag.mustache Просмотреть файл


spec/_files/included_tag.txt → test/_files/included_tag.txt Просмотреть файл


+ 3
- 0
test/_files/inverted_section.js Просмотреть файл

@@ -0,0 +1,3 @@
({
"repos": []
})

spec/_files/inverted_section.mustache → test/_files/inverted_section.mustache Просмотреть файл


spec/_files/inverted_section.txt → test/_files/inverted_section.txt Просмотреть файл


spec/_files/keys_with_questionmarks.js → test/_files/keys_with_questionmarks.js Просмотреть файл

@@ -1,5 +1,5 @@
var keys_with_questionmarks = {
({
"person?": {
name: "Jon"
}
}
})

spec/_files/keys_with_questionmarks.mustache → test/_files/keys_with_questionmarks.mustache Просмотреть файл


spec/_files/keys_with_questionmarks.txt → test/_files/keys_with_questionmarks.txt Просмотреть файл


+ 1
- 0
test/_files/multiline_comment.js Просмотреть файл

@@ -0,0 +1 @@
({})

spec/_files/multiline_comment.mustache → test/_files/multiline_comment.mustache Просмотреть файл


spec/_files/multiline_comment.txt → test/_files/multiline_comment.txt Просмотреть файл


spec/_files/nested_iterating.js → test/_files/nested_iterating.js Просмотреть файл

@@ -1,8 +1,8 @@
var nested_iterating = {
({
inner: [{
foo: 'foo',
inner: [{
bar: 'bar'
}]
}]
};
})

spec/_files/nested_iterating.mustache → test/_files/nested_iterating.mustache Просмотреть файл


spec/_files/nested_iterating.txt → test/_files/nested_iterating.txt Просмотреть файл


Некоторые файлы не были показаны из-за большого количества измененных файлов

Загрузка…
Отмена
Сохранить