瀏覽代碼

Major reorganization and cleanup

tags/0.8.0
Michael Jackson 12 年之前
父節點
當前提交
b114120741
共有 5 個檔案被更改,包括 378 行新增315 行删除
  1. +12
    -0
      CHANGES
  2. +345
    -280
      mustache.js
  3. +0
    -7
      test/context-test.js
  4. +1
    -1
      test/parse-test.js
  5. +20
    -27
      test/writer-test.js

+ 12
- 0
CHANGES 查看文件

@@ -1,3 +1,15 @@
= HEAD

* Remove compile* writer functions and replace them with cache* equivalents. This
reflects more clearly what the functions are actually doing, and also provides
a better basis for understanding what writer.clearCache() does.
* Do not cache dynamically loaded partials.
* Throw an error when rendering a template that contains higher-order sections and
the original template is not provided.
* Remove partials argument from low-level writer.render.
* Remove low-level Context.make function.
* Better code readability and inline documentation.

= 0.7.3 / 5 Nov 2013

* Don't require the original template to be passed to the rendering function


+ 345
- 280
mustache.js 查看文件

@@ -65,6 +65,216 @@
});
}

function escapeTags(tags) {
if (!isArray(tags) || tags.length !== 2) {
throw new Error('Invalid tags: ' + tags);
}

return [
new RegExp(escapeRegExp(tags[0]) + "\\s*"),
new RegExp("\\s*" + escapeRegExp(tags[1]))
];
}

/**
* 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. [ "<%", "%>" ]). Of
* course, the default is to use mustaches (i.e. mustache.tags).
*
* A token is an array 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. {{myValue}}) this element is "name". For
* all template 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
* in the original template of the token, respectively.
*
* Tokens that are the root node of a subtree contain two more elements: an
* array of tokens in the subtree and the index in the original template at which
* the closing tag for that section begins.
*/
function parseTemplate(template, tags) {
tags = tags || mustache.tags;
template = template || '';

if (typeof tags === 'string') {
tags = tags.split(spaceRe);
}

var tagRes = escapeTags(tags);
var scanner = new Scanner(template);

var sections = []; // Stack to hold section tokens
var tokens = []; // Buffer to hold the tokens
var spaces = []; // Indices of whitespace tokens on the current line
var hasTag = false; // Is there a {{tag}} on the current line?
var 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) {
delete tokens[spaces.pop()];
}
} else {
spaces = [];
}

hasTag = false;
nonSpace = false;
}

var start, type, value, chr, token, openSection;
while (!scanner.eos()) {
start = scanner.pos;

// Match any text between tags.
value = scanner.scanUntil(tagRes[0]);
if (value) {
for (var i = 0, len = value.length; i < len; ++i) {
chr = value.charAt(i);

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

tokens.push(['text', chr, start, start + 1]);
start += 1;

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

// Match the opening tag.
if (!scanner.scan(tagRes[0])) break;
hasTag = true;

// Get the tag type.
type = scanner.scan(tagRe) || 'name';
scanner.scan(whiteRe);

// Get the tag value.
if (type === '=') {
value = scanner.scanUntil(eqRe);
scanner.scan(eqRe);
scanner.scanUntil(tagRes[1]);
} else if (type === '{') {
value = scanner.scanUntil(new RegExp('\\s*' + escapeRegExp('}' + tags[1])));
scanner.scan(curlyRe);
scanner.scanUntil(tagRes[1]);
type = '&';
} else {
value = scanner.scanUntil(tagRes[1]);
}

// Match the closing tag.
if (!scanner.scan(tagRes[1])) throw new Error('Unclosed tag at ' + scanner.pos);

token = [type, value, start, scanner.pos];
tokens.push(token);

if (type === '#' || type === '^') {
sections.push(token);
} else if (type === '/') {
// Check section nesting.
openSection = sections.pop();

if (!openSection) {
throw new Error('Unopened section "' + value + '" at ' + start);
}
if (openSection[1] !== value) {
throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
}
} else if (type === 'name' || type === '{' || type === '&') {
nonSpace = true;
} else if (type === '=') {
// Set the tags for the next time around.
tagRes = escapeTags(tags = value.split(spaceRe));
}
}

// Make sure there are no open sections when we're done.
openSection = sections.pop();
if (openSection) {
throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
}

return nestTokens(squashTokens(tokens));
}

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

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

if (token) {
if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
lastToken[1] += token[1];
lastToken[3] = token[3];
} else {
lastToken = token;
squashedTokens.push(token);
}
}
}

return squashedTokens;
}

/**
* 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.
*/
function nestTokens(tokens) {
var nestedTokens = [];
var collector = nestedTokens;
var sections = [];

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

switch (token[0]) {
case '#':
case '^':
sections.push(token);
collector.push(token);
collector = token[4] = [];
break;
case '/':
var section = sections.pop();
section[5] = token[2];
collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
break;
default:
collector.push(token);
}
}

return nestedTokens;
}

/**
* A simple string scanner that is used by the mustache.parse to find
* tokens in template strings.
*/
function Scanner(string) {
this.string = string;
this.tail = string;
@@ -120,20 +330,28 @@
return match;
};

/**
* Represents a rendering context by wrapping a view object and
* maintaining a reference to the parent context.
*/
function Context(view, parent) {
this.view = view == null ? {} : view;
this.parent = parent;
this._cache = { '.': this.view };
}

Context.make = function (view) {
return (view instanceof Context) ? view : new Context(view);
};
/**
* Creates a new context using the given view with this context
* as the parent.
*/
Context.prototype.push = function (view) {
return new 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.
*/
Context.prototype.lookup = function (name) {
var value;
if (name in this._cache) {
@@ -168,334 +386,169 @@
return value;
};

function Writer() {
/**
* 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 and
* partials to avoid the need to parse the same template twice.
*
* The `partialLoader` argument may be a function that is used to load
* partials on the fly as they are needed if they are not found in cache.
* Partials loaded in this manner are not cached.
*/
function Writer(partialLoader) {
this.clearCache();

if (isFunction(partialLoader)) {
this._loadPartial = partialLoader;
}
}

/**
* Clears all cached templates and partials in this writer.
*/
Writer.prototype.clearCache = function () {
this._cache = {};
this._partialCache = {};
};

Writer.prototype.compile = function (template, tags) {
var fn = this._cache[template];

if (!fn) {
var tokens = mustache.parse(template, tags);
fn = this._cache[template] = this.compileTokens(tokens, template);
}

return fn;
};

Writer.prototype.compilePartial = function (name, template, tags) {
var fn = this.compile(template, tags);
this._partialCache[name] = fn;
return fn;
};

/**
* Gets an array of tokens for the partial with the given `name`, either
* from cache or from this writer's partial loading function.
*/
Writer.prototype.getPartial = function (name) {
if (!(name in this._partialCache) && this._loadPartial) {
this.compilePartial(name, this._loadPartial(name));
var template = this._loadPartial(name);
if (template) {
// Intentionally do not cache dynamically-loaded templates.
return mustache.parse(template);
}
}

return this._partialCache[name];
};

Writer.prototype.compileTokens = function (tokens, template) {
var self = this;
return function (view, partials) {
if (partials) {
if (isFunction(partials)) {
self._loadPartial = partials;
} else {
for (var name in partials) {
self.compilePartial(name, partials[name]);
}
}
}
/**
* Parses and caches the given `template` with the given `name` as a
* partial. This writer may then render other templates that reference
* that partial using the ">" tag and that partial's name. Returns the
* array of tokens that is generated from the parse.
*/
Writer.prototype.cachePartial = function (name, template, tags) {
return (this._partialCache[name] = this.cache(template, tags));
};

/**
* Parses and caches the given `template` and returns the array of tokens
* that is generated from the parse.
*/
Writer.prototype.cache = function (template, tags) {
if (!(template in this._cache)) {
this._cache[template] = mustache.parse(template, tags);
}

return renderTokens(tokens, self, Context.make(view), template);
};
return this._cache[template];
};

Writer.prototype.render = function (template, view, partials) {
return this.compile(template)(view, partials);
/**
* High-level method that is used to render the given `template` with
* the given `view`.
*/
Writer.prototype.render = function (template, view) {
var tokens = this.cache(template);
var context = (view instanceof Context) ? view : new Context(view);
return this.renderTokens(tokens, context, template);
};

/**
* Low-level function that renders the given `tokens` using the given `writer`
* and `context`. The `template` string is only needed for templates that use
* higher-order sections to extract the portion of the original template that
* was contained in that section.
* Low-level method that renders the given array of `tokens` using
* the given `context`.
*
* Note: The `template` string is only needed for templates that use
* higher-order sections to extract the portion of the original template
* that was contained in that section.
*/
function renderTokens(tokens, writer, context, template) {
Writer.prototype.renderTokens = function (tokens, context, template) {
var buffer = '';

// This function is used to render an artbitrary template
// This function is used to render an arbitrary template
// in the current context by higher-order functions.
var self = this;
function subRender(template) {
return writer.render(template, context);
return self.render(template, context);
}

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

switch (token[0]) {
case '#':
value = context.lookup(tokenValue);

if (typeof value === 'object' || typeof value === 'string') {
if (isArray(value)) {
for (var j = 0, jlen = value.length; j < jlen; ++j) {
buffer += renderTokens(token[4], writer, context.push(value[j]), template);
}
} else if (value) {
buffer += renderTokens(token[4], writer, context.push(value), template);
value = context.lookup(token[1]);
if (!value) continue;

if (isArray(value)) {
for (var j = 0, jlen = value.length; j < jlen; ++j) {
buffer += this.renderTokens(token[4], context.push(value[j]), template);
}
} else if (typeof value === 'object' || typeof value === 'string') {
buffer += this.renderTokens(token[4], context.push(value), template);
} else if (isFunction(value)) {
var text = template == null ? null : template.slice(token[3], token[5]);
value = value.call(context.view, text, subRender);
if (typeof template !== 'string') {
throw new Error('Cannot use higher-order sections without the original template');
}

// Extract the portion of the original template that the section contains.
value = value.call(context.view, template.slice(token[3], token[5]), subRender);

if (value != null) buffer += value;
} else if (value) {
buffer += renderTokens(token[4], writer, context, template);
} else {
buffer += this.renderTokens(token[4], context, template);
}

break;
case '^':
value = context.lookup(tokenValue);
value = context.lookup(token[1]);

// Use JavaScript's definition of falsy. Include empty arrays.
// See https://github.com/janl/mustache.js/issues/186
if (!value || (isArray(value) && value.length === 0)) {
buffer += renderTokens(token[4], writer, context, template);
buffer += this.renderTokens(token[4], context, template);
}

break;
case '>':
value = writer.getPartial(tokenValue);
if (isFunction(value)) buffer += value(context);
value = this.getPartial(token[1]);
if (value != null) buffer += this.renderTokens(value, context, template);
break;
case '&':
value = context.lookup(tokenValue);
value = context.lookup(token[1]);
if (value != null) buffer += value;
break;
case 'name':
value = context.lookup(tokenValue);
value = context.lookup(token[1]);
if (value != null) buffer += mustache.escape(value);
break;
case 'text':
buffer += tokenValue;
buffer += token[1];
break;
}
}

return buffer;
}

/**
* 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.
*/
function nestTokens(tokens) {
var tree = [];
var collector = tree;
var sections = [];

var token;
for (var i = 0, len = tokens.length; i < len; ++i) {
token = tokens[i];
switch (token[0]) {
case '#':
case '^':
sections.push(token);
collector.push(token);
collector = token[4] = [];
break;
case '/':
var section = sections.pop();
section[5] = token[2];
collector = sections.length > 0 ? sections[sections.length - 1][4] : tree;
break;
default:
collector.push(token);
}
}

return tree;
}

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

var token, lastToken;
for (var i = 0, len = tokens.length; i < len; ++i) {
token = tokens[i];
if (token) {
if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
lastToken[1] += token[1];
lastToken[3] = token[3];
} else {
lastToken = token;
squashedTokens.push(token);
}
}
}

return squashedTokens;
}

function escapeTags(tags) {
return [
new RegExp(escapeRegExp(tags[0]) + "\\s*"),
new RegExp("\\s*" + escapeRegExp(tags[1]))
];
}

/**
* 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 parseTemplate(template, tags) {
template = template || '';
tags = tags || mustache.tags;

if (typeof tags === 'string') tags = tags.split(spaceRe);
if (tags.length !== 2) throw new Error('Invalid tags: ' + tags.join(', '));

var tagRes = escapeTags(tags);
var scanner = new Scanner(template);

var sections = []; // Stack to hold section tokens
var tokens = []; // Buffer to hold the tokens
var spaces = []; // Indices of whitespace tokens on the current line
var hasTag = false; // Is there a {{tag}} on the current line?
var 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) {
delete tokens[spaces.pop()];
}
} else {
spaces = [];
}

hasTag = false;
nonSpace = false;
}

var start, type, value, chr, token, openSection;
while (!scanner.eos()) {
start = scanner.pos;

// Match any text between tags.
value = scanner.scanUntil(tagRes[0]);
if (value) {
for (var i = 0, len = value.length; i < len; ++i) {
chr = value.charAt(i);

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

tokens.push(['text', chr, start, start + 1]);
start += 1;

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

// Match the opening tag.
if (!scanner.scan(tagRes[0])) break;
hasTag = true;

// Get the tag type.
type = scanner.scan(tagRe) || 'name';
scanner.scan(whiteRe);

// Get the tag value.
if (type === '=') {
value = scanner.scanUntil(eqRe);
scanner.scan(eqRe);
scanner.scanUntil(tagRes[1]);
} else if (type === '{') {
value = scanner.scanUntil(new RegExp('\\s*' + escapeRegExp('}' + tags[1])));
scanner.scan(curlyRe);
scanner.scanUntil(tagRes[1]);
type = '&';
} else {
value = scanner.scanUntil(tagRes[1]);
}

// Match the closing tag.
if (!scanner.scan(tagRes[1])) throw new Error('Unclosed tag at ' + scanner.pos);

token = [type, value, start, scanner.pos];
tokens.push(token);

if (type === '#' || type === '^') {
sections.push(token);
} else if (type === '/') {
// Check section nesting.
openSection = sections.pop();
if (!openSection) {
throw new Error('Unopened section "' + value + '" at ' + start);
}
if (openSection[1] !== value) {
throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
}
} else if (type === 'name' || type === '{' || type === '&') {
nonSpace = true;
} else if (type === '=') {
// Set the tags for the next time around.
tags = value.split(spaceRe);
if (tags.length !== 2) {
throw new Error('Invalid tags at ' + start + ': ' + tags.join(', '));
}
tagRes = escapeTags(tags);
}
}

// Make sure there are no open sections when we're done.
openSection = sections.pop();
if (openSection) {
throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
}

return nestTokens(squashTokens(tokens));
}
};

mustache.name = "mustache.js";
mustache.version = "0.7.3";
mustache.tags = ["{{", "}}"];

mustache.Scanner = Scanner;
mustache.Context = Context;
mustache.Writer = Writer;
mustache.tags = [ "{{", "}}" ];

// Export the parsing function.
mustache.parse = parseTemplate;

// Export the escaping function so that the user may override it.
// See https://github.com/janl/mustache.js/issues/244
mustache.escape = escapeHtml;

// All Mustache.* functions use this writer.
// All high-level mustache.* functions use this writer.
var defaultWriter = new Writer();

/**
@@ -506,35 +559,42 @@
};

/**
* Compiles the given `template` to a reusable function using the default
* writer.
* Caches the given partial in the default writer and returns the
* array of tokens it contains.
*/
mustache.compile = function (template, tags) {
return defaultWriter.compile(template, tags);
mustache.cachePartial = function (name, template, tags) {
return defaultWriter.cachePartial(name, template, tags);
};

/**
* Compiles the partial with the given `name` and `template` to a reusable
* function using the default writer.
* Caches the given template in the default writer and returns the
* array of tokens it contains.
*/
mustache.compilePartial = function (name, template, tags) {
return defaultWriter.compilePartial(name, template, tags);
mustache.cache = function (template, tags) {
return defaultWriter.cache(template, tags);
};

/**
* Compiles the given array of tokens (the output of a parse) to a reusable
* function using the default writer.
*/
mustache.compileTokens = function (tokens, template) {
return defaultWriter.compileTokens(tokens, template);
};

/**
* Renders the `template` with the given `view` and `partials` using the
* default writer.
* Renders the `template` with the given `view` using the default writer.
*
* The optionals `partials` argument may either be a function that is used to load
* partials on the fly or an object containing names and templates of partials. If
* it is an object, the partials will be cached in the default writer.
*/
mustache.render = function (template, view, partials) {
return defaultWriter.render(template, view, partials);
if (partials) {
if (isFunction(partials)) {
defaultWriter._loadPartial = partials;
} else {
for (var name in partials) {
if (partials.hasOwnProperty(name)) {
defaultWriter.cachePartial(name, partials[name]);
}
}
}
}

return defaultWriter.render(template, view);
};

// This is here for backwards compatibility with 0.4.x.
@@ -548,4 +608,9 @@
}
};

// Export these mainly for testing, but also for advanced usage.
mustache.Scanner = Scanner;
mustache.Context = Context;
mustache.Writer = Writer;

}));

+ 0
- 7
test/context-test.js 查看文件

@@ -59,10 +59,3 @@ describe('A Mustache.Context', function () {
});
});
});

describe('Mustache.Context.make', function () {
it('returns the same object when given a Context', function () {
var context = new Context;
assert.strictEqual(Context.make(context), context);
});
});

+ 1
- 1
test/parse-test.js 查看文件

@@ -99,7 +99,7 @@ describe('Mustache.parse', function () {
it('throws an error', function () {
assert.throws(function () {
Mustache.parse('A template {{=<%=}}');
}, /invalid tags at 11/i);
}, /invalid tags/i);
});
});



+ 20
- 27
test/writer-test.js 查看文件

@@ -3,41 +3,34 @@ var Writer = Mustache.Writer;

describe('A new Mustache.Writer', function () {
var writer;
beforeEach(function () {
writer = new Writer;
});

it('loads partials correctly', function () {
var partial = 'The content of the partial.';
var result = writer.render('{{>partial}}', {}, function (name) {
assert.equal(name, 'partial');
return partial;
describe('with a cached partial', function () {
beforeEach(function () {
writer = new Writer;
});

assert.equal(result, partial);
});
it('caches partials by content, not name', function () {
writer.cachePartial('partial', 'partial one');
assert.equal(writer.render('{{>partial}}'), 'partial one');

it('caches partials by content, not name', function () {
var result = writer.render('{{>partial}}', {}, {
partial: 'partial one'
writer.cachePartial('partial', 'partial two');
assert.equal(writer.render('{{>partial}}'), 'partial two');
});
});

assert.equal(result, 'partial one');

result = writer.render('{{>partial}}', {}, {
partial: 'partial two'
describe('with a partial loader', function () {
var partial;
beforeEach(function () {
partial = 'The content of the partial.';
writer = new Writer(function (name) {
assert.equal(name, 'partial');
return partial;
});
});

assert.equal(result, 'partial two');
it('loads partials correctly', function () {
assert.equal(writer.render('{{>partial}}'), partial);
});
});

it('can compile an array of tokens', function () {
var template = 'Hello {{name}}!';
var tokens = Mustache.parse(template);
var render = writer.compileTokens(tokens, template);

var result = render({ name: 'Michael' });

assert.equal(result, 'Hello Michael!');
});
});

Loading…
取消
儲存