Skip to content

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also .

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also .
...
  • 8 commits
  • 7 files changed
  • 0 commit comments
  • 1 contributor
Showing with 216 additions and 131 deletions.
  1. +5 −9 README.md
  2. +10 −14 backbone-nested.js
  3. +4 −17 index.html
  4. +1 −1 test/index.html
  5. +81 −36 test/nested-model.js
  6. +1 −0 vendor/documentup.min.js
  7. +114 −54 vendor/underscore.js
View
14 README.md
@@ -30,7 +30,7 @@ user.bind('change:name.first', function(){ ... });
## Usage
-1. Download the latest version [here](https://github.com/afeld/backbone-nested/tags), and add `backbone-nested.js` to your HTML `<head>`, **after** `backbone.js` is included ([tested](http://afeld.github.com/backbone-nested/test/) against [jQuery](http://jquery.com/) v1.7.1, [Underscore](http://documentcloud.github.com/underscore/) v1.3.1 and [Backbone](http://documentcloud.github.com/backbone/) v0.9.2).
+1. Download the latest version [here](https://github.com/afeld/backbone-nested/tags), and add `backbone-nested.js` to your HTML `<head>`, **after** `backbone.js` is included ([tested](http://afeld.github.com/backbone-nested/test/) against [jQuery](http://jquery.com/) v1.7.1, [Underscore](http://documentcloud.github.com/underscore/) v1.3.3 and [Backbone](http://documentcloud.github.com/backbone/) v0.9.2).
```html
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
@@ -157,16 +157,12 @@ user.remove('addresses[0]');
user.get('addresses').length; //=> 1
```
-## Warnings
+## Changelog
-Accessing a nested attribute will throw a warning in your console, because it's safer to use the getter syntax above. To silence these warnings, add an argument of `{silent: true}` to `get()`:
+#### HEAD ([diff](https://github.com/afeld/backbone-nested/compare/v1.1.1...master?w=1))
-```javascript
-user.get('addresses[0]'); // gives a warning in your console
-user.get('addresses[0]', {silent:true}); // (silent)
-```
-
-## Changelog
+* `changedAttributes()` should include the nested attribute paths
+* remove warnings when retrieving nested objects - more of a nuisance than a convenience
#### 1.1.1 ([diff](https://github.com/afeld/backbone-nested/compare/v1.1.0...v1.1.1?w=1))
View
24 backbone-nested.js
@@ -1,20 +1,18 @@
/**
- * Backbone-Nested 1.1.1 - An extension of Backbone.js that keeps track of nested attributes
+ * Backbone-Nested 1.1.2 - An extension of Backbone.js that keeps track of nested attributes
*
* http://afeld.github.com/backbone-nested/
*
* Copyright (c) 2011-2012 Aidan Feldman
* MIT Licensed (LICENSE)
*/
-/*global window, $, _, Backbone */
+/*global $, _, Backbone */
(function(){
'use strict';
Backbone.NestedModel = Backbone.Model.extend({
- get: function(attrStrOrPath, opts){
- opts = opts || {};
-
+ get: function(attrStrOrPath){
var attrPath = Backbone.NestedModel.attrPath(attrStrOrPath),
childAttr = attrPath[0],
result = Backbone.NestedModel.__super__.get.call(this, childAttr);
@@ -29,18 +27,12 @@
result = result[childAttr];
}
- // check if the result is an Object, Array, etc.
- if (!opts.silent && _.isObject(result) && window.console){
- window.console.warn("Backbone-Nested syntax is preferred for accesing values of attribute '" + attrStrOrPath + "'.");
- }
- // else it's a leaf
-
return result;
},
has: function(attr){
// for some reason this is not how Backbone.Model is implemented - it accesses the attributes object directly
- var result = this.get(attr, {silent: true});
+ var result = this.get(attr);
return !(result === null || _.isUndefined(result));
},
@@ -77,7 +69,7 @@
},
add: function(attrStr, value, opts){
- var current = this.get(attrStr, {silent: true});
+ var current = this.get(attrStr);
this.set(attrStr + '[' + current.length + ']', value, opts);
},
@@ -86,7 +78,7 @@
var attrPath = Backbone.NestedModel.attrPath(attrStr),
aryPath = _.initial(attrPath),
- val = this.get(aryPath, {silent: true}),
+ val = this.get(aryPath),
i = _.last(attrPath);
if (!_.isArray(val)){
@@ -140,12 +132,15 @@
});
if (isChildAry && !_.isArray(destVal)){
+ // assigning an array to a previously non-array value
destVal = dest[prop] = [];
}
if (prop in dest && _.isObject(sourceVal) && _.isObject(destVal)){
+ // both new and original are objects/arrays, and thus need to be merged
destVal = dest[prop] = this._mergeAttrs(destVal, sourceVal, opts, newStack);
} else {
+ // new value is a primitive
var oldVal = destVal;
destVal = dest[prop] = sourceVal;
@@ -165,6 +160,7 @@
if (!opts.silent && newStack.length > 1){
attrStr = Backbone.NestedModel.createAttrStr(newStack);
this.trigger('change:' + attrStr, this, destVal);
+ this.changed[attrStr] = destVal;
}
}, this);
View
21 index.html
@@ -1,28 +1,15 @@
<!DOCTYPE html>
<html>
<head>
- <meta charset="utf-8">
- <meta name="apple-mobile-web-app-capable" content="yes">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title></title>
- <script src="http://cdnjs.cloudflare.com/ajax/libs/documentup/latest.min.js"></script>
+ <script src="vendor/documentup.min.js"></script>
<script>
DocumentUp.document({
repo: 'afeld/backbone-nested',
- twitter: 'aidanfeldman'
+ issues: true,
+ twitter: 'aidanfeldman',
+ google_analytics: 'UA-19048260-5'
});
</script>
- <script type="text/javascript">
- var _gaq = _gaq || [];
- _gaq.push(['_setAccount', 'UA-19048260-5']);
- _gaq.push(['_trackPageview']);
-
- (function() {
- var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
- ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
- var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
- })();
- </script>
</head>
<body></body>
</html>
View
2 test/index.html
@@ -1,7 +1,7 @@
<!DOCTYPE HTML>
<html>
<head>
- <title>Backbone Test Suite</title>
+ <title>Backbone-Nested Test Suite</title>
<link rel="stylesheet" href="vendor/qunit.css" type="text/css" media="screen" />
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script type="text/javascript" src="vendor/qunit.js"></script>
View
117 test/nested-model.js
@@ -9,21 +9,21 @@ $(document).ready(function() {
doc = new klass({
gender: 'M',
name: {
- first: "Aidan",
+ first: 'Aidan',
middle: {
initial: 'L',
full: 'Lee'
},
- last: "Feldman"
+ last: 'Feldman'
},
addresses: [
{
- city: "Brooklyn",
- state: "NY"
+ city: 'Brooklyn',
+ state: 'NY'
},
{
- city: "Oak Park",
- state: "IL"
+ city: 'Oak Park',
+ state: 'IL'
}
]
});
@@ -85,7 +85,7 @@ $(document).ready(function() {
// ----- GET --------
test("#get() 1-1 returns attributes object", function() {
- var name = doc.get('name', {silent: true});
+ var name = doc.get('name');
deepEqual(name, {
first: 'Aidan',
@@ -118,12 +118,12 @@ $(document).ready(function() {
});
test("#get() 1-N returns attributes object", function() {
- deepEqual(doc.get('addresses[0]', {silent: true}), {
+ deepEqual(doc.get('addresses[0]'), {
city: 'Brooklyn',
state: 'NY'
});
- deepEqual(doc.get('addresses[1]', {silent: true}), {
+ deepEqual(doc.get('addresses[1]'), {
city: 'Oak Park',
state: 'IL'
});
@@ -298,9 +298,9 @@ $(document).ready(function() {
test("change event on nested attribute", function() {
var callbacksFired = [0, 0, 0];
- doc.bind('change', function(){ callbacksFired[0] += 1 });
- doc.bind('change:name', function(){ callbacksFired[1] += 1 });
- doc.bind('change:name.first', function(){ callbacksFired[2] += 1 });
+ doc.bind('change', function(){ callbacksFired[0] += 1; });
+ doc.bind('change:name', function(){ callbacksFired[1] += 1; });
+ doc.bind('change:name.first', function(){ callbacksFired[2] += 1; });
doc.bind('change:name.last', function(){ ok(false, "'change:name.last' should not fire"); });
doc.bind('change:gender', function(){ ok(false, "'change:gender' should not fire"); });
@@ -341,10 +341,10 @@ $(document).ready(function() {
test("change event on deeply nested attribute", function() {
var callbacksFired = [false, false, false, false];
- doc.bind('change', function(){ callbacksFired[0] = true });
- doc.bind('change:name', function(){ callbacksFired[1] = true });
- doc.bind('change:name.middle', function(){ callbacksFired[2] = true });
- doc.bind('change:name.middle.full', function(){ callbacksFired[3] = true });
+ doc.bind('change', function(){ callbacksFired[0] = true; });
+ doc.bind('change:name', function(){ callbacksFired[1] = true; });
+ doc.bind('change:name.middle', function(){ callbacksFired[2] = true; });
+ doc.bind('change:name.middle.full', function(){ callbacksFired[3] = true; });
doc.bind('change:name.middle.initial', function(){ ok(false, "'change:name.middle.initial' should not fire"); });
doc.bind('change:name.first', function(){ ok(false, "'change:name.first' should not fire"); });
@@ -360,11 +360,11 @@ $(document).ready(function() {
test("change event on deeply nested attribute with object", function() {
var callbacksFired = [false, false, false, false, false];
- doc.bind('change', function(){ callbacksFired[0] = true });
- doc.bind('change:name', function(){ callbacksFired[1] = true });
- doc.bind('change:name.middle', function(){ callbacksFired[2] = true });
- doc.bind('change:name.middle.initial', function(){ callbacksFired[3] = true });
- doc.bind('change:name.middle.full', function(){ callbacksFired[4] = true });
+ doc.bind('change', function(){ callbacksFired[0] = true; });
+ doc.bind('change:name', function(){ callbacksFired[1] = true; });
+ doc.bind('change:name.middle', function(){ callbacksFired[2] = true; });
+ doc.bind('change:name.middle.initial', function(){ callbacksFired[3] = true; });
+ doc.bind('change:name.middle.full', function(){ callbacksFired[4] = true; });
doc.bind('change:name.first', function(){ ok(false, "'change:name.first' should not fire"); });
@@ -383,10 +383,10 @@ $(document).ready(function() {
test("change event on nested array", function() {
var callbacksFired = [false, false, false, false];
- doc.bind('change', function(){ callbacksFired[0] = true });
- doc.bind('change:addresses', function(){ callbacksFired[1] = true });
- doc.bind('change:addresses[0]', function(){ callbacksFired[2] = true });
- doc.bind('change:addresses[0].city', function(){ callbacksFired[3] = true });
+ doc.bind('change', function(){ callbacksFired[0] = true; });
+ doc.bind('change:addresses', function(){ callbacksFired[1] = true; });
+ doc.bind('change:addresses[0]', function(){ callbacksFired[2] = true; });
+ doc.bind('change:addresses[0].city', function(){ callbacksFired[3] = true; });
doc.bind('change:addresses[0].state', function(){ ok(false, "'change:addresses[0].state' should not fire"); });
doc.bind('change:addresses[1]', function(){ ok(false, "'change:addresses[1]' should not fire"); });
@@ -402,9 +402,9 @@ $(document).ready(function() {
test("change+add when adding to array", function() {
var callbacksFired = [false, false, false];
- doc.bind('change', function(){ callbacksFired[0] = true });
- doc.bind('change:addresses', function(){ callbacksFired[1] = true });
- doc.bind('add:addresses', function(){ callbacksFired[2] = true });
+ doc.bind('change', function(){ callbacksFired[0] = true; });
+ doc.bind('change:addresses', function(){ callbacksFired[1] = true; });
+ doc.bind('add:addresses', function(){ callbacksFired[2] = true; });
doc.set({
'addresses[2]': {
@@ -421,9 +421,9 @@ $(document).ready(function() {
test("change+remove when unsetting on array", function() {
var callbacksFired = [false, false, false];
- doc.bind('change', function(){ callbacksFired[0] = true });
- doc.bind('change:addresses', function(){ callbacksFired[1] = true });
- doc.bind('remove:addresses', function(){ callbacksFired[2] = true });
+ doc.bind('change', function(){ callbacksFired[0] = true; });
+ doc.bind('change:addresses', function(){ callbacksFired[1] = true; });
+ doc.bind('remove:addresses', function(){ callbacksFired[2] = true; });
doc.unset('addresses[1]');
@@ -435,9 +435,9 @@ $(document).ready(function() {
test("change+add event on append", function() {
var callbacksFired = [false, false, false];
- doc.bind('change', function(){ callbacksFired[0] = true });
- doc.bind('change:addresses', function(){ callbacksFired[1] = true });
- doc.bind('add:addresses', function(){ callbacksFired[2] = true });
+ doc.bind('change', function(){ callbacksFired[0] = true; });
+ doc.bind('change:addresses', function(){ callbacksFired[1] = true; });
+ doc.bind('add:addresses', function(){ callbacksFired[2] = true; });
doc.set({
'addresses[]': {
@@ -452,6 +452,51 @@ $(document).ready(function() {
});
+ // ----- CHANGED_ATTRIBUTES --------
+
+ test("#changedAttributes() should return the attributes for the full path and all sub-paths", function() {
+ doc.bind('change', function(){
+ deepEqual(this.changedAttributes(), {
+ name: {
+ first: 'Aidan',
+ middle: {
+ initial: 'L',
+ full: 'Limburger'
+ },
+ last: 'Feldman'
+ },
+ 'name.middle': {
+ initial: 'L',
+ full: 'Limburger'
+ },
+ 'name.middle.full': 'Limburger'
+ });
+ });
+
+ doc.set({'name.middle.full': 'Limburger'});
+ });
+
+ test("#changedAttributes() should clear the nested attributes between change events", function() {
+ doc.set({'name.first': 'Bob'});
+
+ doc.bind('change', function(){
+ deepEqual(this.changedAttributes(), {
+ name: {
+ first: 'Bob',
+ middle: {
+ initial: 'L',
+ full: 'Lee'
+ },
+ last: 'Dylan'
+ },
+ 'name.last': 'Dylan'
+ });
+ });
+
+ doc.set({'name.last': 'Dylan'});
+ });
+
+
// ----- UNSET --------
test("#unset() nested attribute", function() {
@@ -470,7 +515,7 @@ $(document).ready(function() {
doc.add('addresses', attrs);
- deepEqual(doc.get('addresses[2]', {silent: true}), attrs);
+ deepEqual(doc.get('addresses[2]'), attrs);
});
test("#add() on nested array should trigger 'add' event", function() {
@@ -493,7 +538,7 @@ $(document).ready(function() {
test("#remove() on nested array succeeds", function() {
doc.remove('addresses[0]');
- deepEqual(doc.get('addresses[0]', {silent: true}), {
+ deepEqual(doc.get('addresses[0]'), {
city: "Oak Park",
state: "IL"
});
View
1 vendor/documentup.min.js
@@ -0,0 +1 @@
+this.DocumentUp={},DocumentUp.document=function(a){var b;"string"==typeof a?(b=a,a=null):(b=a.repo,delete a.repo),window.callback=function(b){b.status===200&&(document.open(),document.write(b.html),document.close(),a&&a.afterRender&&typeof a.afterRender=="function"&&a.afterRender())};var c=document.createElement("script");c.src="http://documentup.com/"+b,a?c.src+="?config="+encodeURIComponent(JSON.stringify(a))+"&callback=callback":c.src+="?callback=callback",document.getElementsByTagName("head")[0].appendChild(c)}
View
168 vendor/underscore.js
@@ -1,4 +1,4 @@
-// Underscore.js 1.3.1
+// Underscore.js 1.3.3
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Underscore is freely distributable under the MIT license.
// Portions of Underscore are inspired or borrowed from Prototype,
@@ -62,7 +62,7 @@
}
// Current version.
- _.VERSION = '1.3.1';
+ _.VERSION = '1.3.3';
// Collection Functions
// --------------------
@@ -180,7 +180,7 @@
each(obj, function(value, index, list) {
if (!(result = result && iterator.call(context, value, index, list))) return breaker;
});
- return result;
+ return !!result;
};
// Determine if at least one element in the object matches a truth test.
@@ -224,7 +224,7 @@
// Return the maximum element or (element-based computation).
_.max = function(obj, iterator, context) {
- if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj);
+ if (!iterator && _.isArray(obj) && obj[0] === +obj[0]) return Math.max.apply(Math, obj);
if (!iterator && _.isEmpty(obj)) return -Infinity;
var result = {computed : -Infinity};
each(obj, function(value, index, list) {
@@ -236,7 +236,7 @@
// Return the minimum element (or element-based computation).
_.min = function(obj, iterator, context) {
- if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);
+ if (!iterator && _.isArray(obj) && obj[0] === +obj[0]) return Math.min.apply(Math, obj);
if (!iterator && _.isEmpty(obj)) return Infinity;
var result = {computed : Infinity};
each(obj, function(value, index, list) {
@@ -250,26 +250,25 @@
_.shuffle = function(obj) {
var shuffled = [], rand;
each(obj, function(value, index, list) {
- if (index == 0) {
- shuffled[0] = value;
- } else {
rand = Math.floor(Math.random() * (index + 1));
shuffled[index] = shuffled[rand];
shuffled[rand] = value;
- }
});
return shuffled;
};
// Sort the object's values by a criterion produced by an iterator.
- _.sortBy = function(obj, iterator, context) {
+ _.sortBy = function(obj, val, context) {
+ var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; };
return _.pluck(_.map(obj, function(value, index, list) {
return {
value : value,
criteria : iterator.call(context, value, index, list)
};
}).sort(function(left, right) {
var a = left.criteria, b = right.criteria;
+ if (a === void 0) return 1;
+ if (b === void 0) return -1;
return a < b ? -1 : a > b ? 1 : 0;
}), 'value');
};
@@ -299,26 +298,26 @@
};
// Safely convert anything iterable into a real, live array.
- _.toArray = function(iterable) {
- if (!iterable) return [];
- if (iterable.toArray) return iterable.toArray();
- if (_.isArray(iterable)) return slice.call(iterable);
- if (_.isArguments(iterable)) return slice.call(iterable);
- return _.values(iterable);
+ _.toArray = function(obj) {
+ if (!obj) return [];
+ if (_.isArray(obj)) return slice.call(obj);
+ if (_.isArguments(obj)) return slice.call(obj);
+ if (obj.toArray && _.isFunction(obj.toArray)) return obj.toArray();
+ return _.values(obj);
};
// Return the number of elements in an object.
_.size = function(obj) {
- return _.toArray(obj).length;
+ return _.isArray(obj) ? obj.length : _.keys(obj).length;
};
// Array Functions
// ---------------
// Get the first element of an array. Passing **n** will return the first N
- // values in the array. Aliased as `head`. The **guard** check allows it to work
- // with `_.map`.
- _.first = _.head = function(array, n, guard) {
+ // values in the array. Aliased as `head` and `take`. The **guard** check
+ // allows it to work with `_.map`.
+ _.first = _.head = _.take = function(array, n, guard) {
return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
};
@@ -372,15 +371,17 @@
// Aliased as `unique`.
_.uniq = _.unique = function(array, isSorted, iterator) {
var initial = iterator ? _.map(array, iterator) : array;
- var result = [];
- _.reduce(initial, function(memo, el, i) {
- if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) {
- memo[memo.length] = el;
- result[result.length] = array[i];
+ var results = [];
+ // The `isSorted` flag is irrelevant if the array only contains two elements.
+ if (array.length < 3) isSorted = true;
+ _.reduce(initial, function (memo, value, index) {
+ if (isSorted ? _.last(memo) !== value || !memo.length : !_.include(memo, value)) {
+ memo.push(value);
+ results.push(array[index]);
}
return memo;
}, []);
- return result;
+ return results;
};
// Produce an array that contains the union: each distinct element from all of
@@ -403,7 +404,7 @@
// Take the difference between one array and a number of other arrays.
// Only the elements present in just the first array will remain.
_.difference = function(array) {
- var rest = _.flatten(slice.call(arguments, 1));
+ var rest = _.flatten(slice.call(arguments, 1), true);
return _.filter(array, function(value){ return !_.include(rest, value); });
};
@@ -514,7 +515,7 @@
// it with the arguments supplied.
_.delay = function(func, wait) {
var args = slice.call(arguments, 2);
- return setTimeout(function(){ return func.apply(func, args); }, wait);
+ return setTimeout(function(){ return func.apply(null, args); }, wait);
};
// Defers a function, scheduling it to run after the current call stack has
@@ -526,7 +527,7 @@
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time.
_.throttle = function(func, wait) {
- var context, args, timeout, throttling, more;
+ var context, args, timeout, throttling, more, result;
var whenDone = _.debounce(function(){ more = throttling = false; }, wait);
return function() {
context = this; args = arguments;
@@ -539,24 +540,27 @@
if (throttling) {
more = true;
} else {
- func.apply(context, args);
+ result = func.apply(context, args);
}
whenDone();
throttling = true;
+ return result;
};
};
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
- // N milliseconds.
- _.debounce = function(func, wait) {
+ // N milliseconds. If `immediate` is passed, trigger the function on the
+ // leading edge, instead of the trailing.
+ _.debounce = function(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
- func.apply(context, args);
+ if (!immediate) func.apply(context, args);
};
+ if (immediate && !timeout) func.apply(context, args);
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
@@ -641,6 +645,15 @@
return obj;
};
+ // Return a copy of the object only containing the whitelisted properties.
+ _.pick = function(obj) {
+ var result = {};
+ each(_.flatten(slice.call(arguments, 1)), function(key) {
+ if (key in obj) result[key] = obj[key];
+ });
+ return result;
+ };
+
// Fill in a given object with default properties.
_.defaults = function(obj) {
each(slice.call(arguments, 1), function(source) {
@@ -761,6 +774,7 @@
// Is a given array, string, or object empty?
// An "empty" object has no enumerable own-properties.
_.isEmpty = function(obj) {
+ if (obj == null) return true;
if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
for (var key in obj) if (_.has(obj, key)) return false;
return true;
@@ -807,6 +821,11 @@
return toString.call(obj) == '[object Number]';
};
+ // Is a given object a finite number?
+ _.isFinite = function(obj) {
+ return _.isNumber(obj) && isFinite(obj);
+ };
+
// Is the given value `NaN`?
_.isNaN = function(obj) {
// `NaN` is the only value for which `===` is not reflexive.
@@ -868,6 +887,14 @@
return (''+string).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;').replace(/\//g,'&#x2F;');
};
+ // If the value of the named property is a function then invoke it;
+ // otherwise, return it.
+ _.result = function(object, property) {
+ if (object == null) return null;
+ var value = object[property];
+ return _.isFunction(value) ? value.call(object) : value;
+ };
+
// Add your own custom functions to the Underscore object, ensuring that
// they're correctly added to the OOP wrapper as well.
_.mixin = function(obj) {
@@ -897,39 +924,72 @@
// guaranteed not to match.
var noMatch = /.^/;
+ // Certain characters need to be escaped so that they can be put into a
+ // string literal.
+ var escapes = {
+ '\\': '\\',
+ "'": "'",
+ 'r': '\r',
+ 'n': '\n',
+ 't': '\t',
+ 'u2028': '\u2028',
+ 'u2029': '\u2029'
+ };
+
+ for (var p in escapes) escapes[escapes[p]] = p;
+ var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
+ var unescaper = /\\(\\|'|r|n|t|u2028|u2029)/g;
+
// Within an interpolation, evaluation, or escaping, remove HTML escaping
// that had been previously added.
var unescape = function(code) {
- return code.replace(/\\\\/g, '\\').replace(/\\'/g, "'");
+ return code.replace(unescaper, function(match, escape) {
+ return escapes[escape];
+ });
};
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
- _.template = function(str, data) {
- var c = _.templateSettings;
- var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' +
- 'with(obj||{}){__p.push(\'' +
- str.replace(/\\/g, '\\\\')
- .replace(/'/g, "\\'")
- .replace(c.escape || noMatch, function(match, code) {
- return "',_.escape(" + unescape(code) + "),'";
+ _.template = function(text, data, settings) {
+ settings = _.defaults(settings || {}, _.templateSettings);
+
+ // Compile the template source, taking care to escape characters that
+ // cannot be included in a string literal and then unescape them in code
+ // blocks.
+ var source = "__p+='" + text
+ .replace(escaper, function(match) {
+ return '\\' + escapes[match];
})
- .replace(c.interpolate || noMatch, function(match, code) {
- return "'," + unescape(code) + ",'";
+ .replace(settings.escape || noMatch, function(match, code) {
+ return "'+\n_.escape(" + unescape(code) + ")+\n'";
})
- .replace(c.evaluate || noMatch, function(match, code) {
- return "');" + unescape(code).replace(/[\r\n\t]/g, ' ') + ";__p.push('";
+ .replace(settings.interpolate || noMatch, function(match, code) {
+ return "'+\n(" + unescape(code) + ")+\n'";
})
- .replace(/\r/g, '\\r')
- .replace(/\n/g, '\\n')
- .replace(/\t/g, '\\t')
- + "');}return __p.join('');";
- var func = new Function('obj', '_', tmpl);
- if (data) return func(data, _);
- return function(data) {
- return func.call(this, data, _);
+ .replace(settings.evaluate || noMatch, function(match, code) {
+ return "';\n" + unescape(code) + "\n;__p+='";
+ }) + "';\n";
+
+ // If a variable is not specified, place data values in local scope.
+ if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
+
+ source = "var __p='';" +
+ "var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n" +
+ source + "return __p;\n";
+
+ var render = new Function(settings.variable || 'obj', '_', source);
+ if (data) return render(data, _);
+ var template = function(data) {
+ return render.call(this, data, _);
};
+
+ // Provide the compiled function source as a convenience for build time
+ // precompilation.
+ template.source = 'function(' + (settings.variable || 'obj') + '){\n' +
+ source + '}';
+
+ return template;
};
// Add a "chain" function, which will delegate to the wrapper.

No commit comments for this range

Something went wrong with that request. Please try again.