diff --git a/backbone.js b/backbone.js
index d16d2a5f..9a8f2571 100644
--- a/backbone.js
+++ b/backbone.js
@@ -954,9 +954,10 @@
// Handles cross-browser history management, based on URL fragments. If the
// browser does not support `onhashchange`, falls back to polling.
- var History = Backbone.History = function() {
+ var History = Backbone.History = function(options) {
this.handlers = [];
_.bindAll(this, 'checkUrl');
+ this.location = options && options.location || root.location;
};
// Cached regex for cleaning leading hashes and slashes .
@@ -977,9 +978,8 @@
// Gets the true hash value. Cannot use location.hash directly due to bug
// in Firefox where location.hash will always be decoded.
- getHash: function(windowOverride) {
- var loc = windowOverride ? windowOverride.location : window.location;
- var match = loc.href.match(/#(.*)$/);
+ getHash: function(window) {
+ var match = (window || this).location.href.match(/#(.*)$/);
return match ? match[1] : '';
},
@@ -988,7 +988,7 @@
getFragment: function(fragment, forcePushState) {
if (fragment == null) {
if (this._hasPushState || !this._wantsHashChange || forcePushState) {
- fragment = window.location.pathname;
+ fragment = this.location.pathname;
} else {
fragment = this.getHash();
}
@@ -1031,14 +1031,14 @@
// Determine if we need to change the base url, for a pushState link
// opened by a non-pushState browser.
this.fragment = fragment;
- var loc = window.location;
+ var loc = this.location;
var atRoot = (loc.pathname == this.options.root) && !loc.search;
// If we've started off with a route from a `pushState`-enabled browser,
// but we're currently in a browser that doesn't support it...
if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
this.fragment = this.getFragment(null, true);
- window.location.replace(this.options.root + window.location.search + '#' + this.fragment);
+ this.location.replace(this.options.root + this.location.search + '#' + this.fragment);
// Return immediately as browser will do redirect to new url
return true;
@@ -1072,7 +1072,9 @@
// calls `loadUrl`, normalizing across the hidden iframe.
checkUrl: function(e) {
var current = this.getFragment();
- if (current == this.fragment && this.iframe) current = this.getFragment(this.getHash(this.iframe));
+ if (current == this.fragment && this.iframe) {
+ current = this.getFragment(this.getHash(this.iframe));
+ }
if (current == this.fragment) return false;
if (this.iframe) this.navigate(current);
this.loadUrl() || this.loadUrl(this.getHash());
@@ -1115,7 +1117,7 @@
// fragment to store history.
} else if (this._wantsHashChange) {
this.fragment = frag;
- this._updateHash(window.location, frag, options.replace);
+ this._updateHash(this.location, frag, options.replace);
if (this.iframe && (frag != this.getFragment(this.getHash(this.iframe)))) {
// Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change.
// When replace is true, we don't want this.
@@ -1126,7 +1128,7 @@
// If you've told us that you explicitly don't want fallback hashchange-
// based history, then `navigate` becomes a page refresh.
} else {
- return window.location.assign(fullFrag);
+ return this.location.assign(fullFrag);
}
if (options.trigger) this.loadUrl(fragment);
},
@@ -1135,7 +1137,7 @@
// a new one to the browser history.
_updateHash: function(location, fragment, replace) {
if (replace) {
- location.replace(location.toString().replace(/(javascript:|#).*$/, '') + '#' + fragment);
+ location.replace(location.href.replace(/(javascript:|#).*$/, '') + '#' + fragment);
} else {
location.hash = fragment;
}
diff --git a/index.html b/index.html
index ed67826b..aa59f7f9 100644
--- a/index.html
+++ b/index.html
@@ -3805,7 +3805,7 @@ ActiveRecord::Base.include_root_in_json = false
-
+
diff --git a/test/router.js b/test/router.js
index 68106b4b..d1d0a5a4 100644
--- a/test/router.js
+++ b/test/router.js
@@ -1,6 +1,7 @@
$(document).ready(function() {
var router = null;
+ var location = null;
var lastRoute = null;
var lastArgs = [];
@@ -9,10 +10,33 @@ $(document).ready(function() {
lastArgs = args;
}
+ var Location = function(href) {
+ this.replace(href);
+ };
+
+ _.extend(Location.prototype, {
+
+ replace: function(href) {
+ _.extend(this, _.pick($('', {href: href})[0],
+ 'href',
+ 'hash',
+ 'search',
+ 'fragment',
+ 'pathname'
+ ));
+ },
+
+ toString: function() {
+ return this.href;
+ }
+
+ });
+
module("Backbone.Router", {
setup: function() {
- Backbone.history = null;
+ location = new Location('http://example.com');
+ Backbone.history = new Backbone.History({location: location});
router = new Router({testing: 101});
Backbone.history.interval = 9;
Backbone.history.start({pushState: false});
@@ -101,24 +125,20 @@ $(document).ready(function() {
equal(router.testing, 101);
});
- asyncTest("Router: routes (simple)", 4, function() {
- window.location.hash = 'search/news';
- setTimeout(function() {
- equal(router.query, 'news');
- equal(router.page, undefined);
- equal(lastRoute, 'search');
- equal(lastArgs[0], 'news');
- start();
- }, 10);
+ test("Router: routes (simple)", 4, function() {
+ location.replace('http://example.com#search/news');
+ Backbone.history.checkUrl();
+ equal(router.query, 'news');
+ equal(router.page, undefined);
+ equal(lastRoute, 'search');
+ equal(lastArgs[0], 'news');
});
- asyncTest("Router: routes (two part)", 2, function() {
- window.location.hash = 'search/nyc/p10';
- setTimeout(function() {
- equal(router.query, 'nyc');
- equal(router.page, '10');
- start();
- }, 10);
+ test("Router: routes (two part)", 2, function() {
+ location.replace('http://example.com#search/nyc/p10');
+ Backbone.history.checkUrl();
+ equal(router.query, 'nyc');
+ equal(router.page, '10');
});
test("Router: routes via navigate", 2, function() {
@@ -145,22 +165,12 @@ $(document).ready(function() {
});
});
- test("Router: doesn't fire routes to the same place twice", 6, function() {
- equal(router.count, 0);
- router.navigate('counter', {trigger: true});
- equal(router.count, 1);
- router.navigate('/counter', {trigger: true});
- router.navigate('/counter', {trigger: true});
- equal(router.count, 1);
- router.navigate('search/counter', {trigger: true});
- router.navigate('counter', {trigger: true});
- equal(router.count, 2);
- Backbone.history.stop();
- router.navigate('search/counter', {trigger: true});
- router.navigate('counter', {trigger: true});
- equal(router.count, 2);
- Backbone.history.start();
- equal(router.count, 3);
+ test("loadUrl is not called for identical routes.", 0, function() {
+ Backbone.history.loadUrl = function(){ ok(false); };
+ location.replace('http://example.com#route');
+ Backbone.history.navigate('route');
+ Backbone.history.navigate('/route');
+ Backbone.history.navigate('/route');
});
test("Router: use implicit callback if none provided", 1, function() {
@@ -169,73 +179,49 @@ $(document).ready(function() {
equal(router.count, 1);
});
- asyncTest("Router: routes via navigate with {replace: true}", 2, function() {
- var historyLength = window.history.length;
- router.navigate('search/manhattan/start_here');
- router.navigate('search/manhattan/then_here');
- router.navigate('search/manhattan/finally_here', {replace: true});
-
- equal(window.location.hash, "#search/manhattan/finally_here");
- window.history.go(-1);
- setTimeout(function() {
- equal(window.location.hash, "#search/manhattan/start_here");
- start();
- }, 500);
+ test("Router: routes via navigate with {replace: true}", 1, function() {
+ location.replace('http://example.com#start_here');
+ Backbone.history.checkUrl();
+ location.replace = function(href) {
+ strictEqual(href, new Location('http://example.com#end_here').href);
+ };
+ Backbone.history.navigate('end_here', {replace: true});
});
- asyncTest("Router: routes (splats)", 1, function() {
- window.location.hash = 'splat/long-list/of/splatted_99args/end';
- setTimeout(function() {
- equal(router.args, 'long-list/of/splatted_99args');
- start();
- }, 10);
+ test("Router: routes (splats)", 1, function() {
+ location.replace('http://example.com#splat/long-list/of/splatted_99args/end');
+ Backbone.history.checkUrl();
+ equal(router.args, 'long-list/of/splatted_99args');
});
- asyncTest("Router: routes (complex)", 3, function() {
- window.location.hash = 'one/two/three/complex-part/four/five/six/seven';
- setTimeout(function() {
- equal(router.first, 'one/two/three');
- equal(router.part, 'part');
- equal(router.rest, 'four/five/six/seven');
- start();
- }, 10);
+ test("Router: routes (complex)", 3, function() {
+ location.replace('http://example.com#one/two/three/complex-part/four/five/six/seven');
+ Backbone.history.checkUrl();
+ equal(router.first, 'one/two/three');
+ equal(router.part, 'part');
+ equal(router.rest, 'four/five/six/seven');
});
- asyncTest("Router: routes (query)", 5, function() {
- window.location.hash = 'mandel?a=b&c=d';
- setTimeout(function() {
- equal(router.entity, 'mandel');
- equal(router.queryArgs, 'a=b&c=d');
- equal(lastRoute, 'query');
- equal(lastArgs[0], 'mandel');
- equal(lastArgs[1], 'a=b&c=d');
- start();
- }, 10);
+ test("Router: routes (query)", 5, function() {
+ location.replace('http://example.com#mandel?a=b&c=d');
+ Backbone.history.checkUrl();
+ equal(router.entity, 'mandel');
+ equal(router.queryArgs, 'a=b&c=d');
+ equal(lastRoute, 'query');
+ equal(lastArgs[0], 'mandel');
+ equal(lastArgs[1], 'a=b&c=d');
});
- asyncTest("Router: routes (anything)", 1, function() {
- window.location.hash = 'doesnt-match-a-route';
- setTimeout(function() {
- equal(router.anything, 'doesnt-match-a-route');
- start();
- window.location.hash = '';
- }, 10);
+ test("Router: routes (anything)", 1, function() {
+ location.replace('http://example.com#doesnt-match-a-route');
+ Backbone.history.checkUrl();
+ equal(router.anything, 'doesnt-match-a-route');
});
- asyncTest("Router: fires event when router doesn't have callback on it", 1, function() {
- try{
- var callbackFired = false;
- var myCallback = function(){ callbackFired = true; };
- router.on("route:noCallback", myCallback);
- window.location.hash = "noCallback";
- setTimeout(function(){
- equal(callbackFired, true);
- start();
- window.location.hash = '';
- }, 10);
- } catch (err) {
- ok(false, "an exception was thrown trying to fire the router event with no router handler callback");
- }
+ test("Router: fires event when router doesn't have callback on it", 1, function() {
+ router.on("route:noCallback", function(){ ok(true); });
+ location.replace('http://example.com#noCallback');
+ Backbone.history.checkUrl();
});
test("#933, #908 - leading slash", 2, function() {
@@ -265,17 +251,14 @@ $(document).ready(function() {
equal(router.rest, 'has%20space');
});
- asyncTest("Router: correctly handles URLs with % (#868)", 3, function() {
- window.location.hash = 'search/fat%3A1.5%25';
- setTimeout(function() {
- window.location.hash = 'search/fat';
- setTimeout(function() {
- equal(router.query, 'fat');
- equal(router.page, undefined);
- equal(lastRoute, 'search');
- start();
- }, 50);
- }, 50);
+ test("Router: correctly handles URLs with % (#868)", 3, function() {
+ location.replace('http://example.com#search/fat%3A1.5%25');
+ Backbone.history.checkUrl();
+ location.replace('http://example.com#search/fat');
+ Backbone.history.checkUrl();
+ equal(router.query, 'fat');
+ equal(router.page, undefined);
+ equal(lastRoute, 'search');
});
});
diff --git a/test/test-ender.html b/test/test-ender.html
index 10f179cc..3a10b186 100644
--- a/test/test-ender.html
+++ b/test/test-ender.html
@@ -7,7 +7,7 @@
-
+
diff --git a/test/test-zepto.html b/test/test-zepto.html
index 8a86b044..31c0fc06 100644
--- a/test/test-zepto.html
+++ b/test/test-zepto.html
@@ -7,7 +7,7 @@
-
+
diff --git a/test/test.html b/test/test.html
index 6f70c74b..2ce88db3 100644
--- a/test/test.html
+++ b/test/test.html
@@ -8,7 +8,7 @@
-
+
diff --git a/test/vendor/underscore-1.3.1.js b/test/vendor/underscore.js
similarity index 87%
rename from test/vendor/underscore-1.3.1.js
rename to test/vendor/underscore.js
index 208d4cd8..f6f7e2f2 100644
--- a/test/vendor/underscore-1.3.1.js
+++ b/test/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,19 +250,16 @@
_.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;
- }
+ 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,
@@ -270,6 +267,8 @@
};
}).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, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/');
};
+ // 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) + "),'";
- })
- .replace(c.interpolate || noMatch, function(match, code) {
- return "'," + unescape(code) + ",'";
- })
- .replace(c.evaluate || noMatch, function(match, code) {
- return "');" + unescape(code).replace(/[\r\n\t]/g, ' ') + ";__p.push('";
- })
- .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, _);
+ _.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(settings.escape || noMatch, function(match, code) {
+ return "'+\n_.escape(" + unescape(code) + ")+\n'";
+ })
+ .replace(settings.interpolate || noMatch, function(match, code) {
+ return "'+\n(" + unescape(code) + ")+\n'";
+ })
+ .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.