From dcec9c90801a7c5e3535b6679ad9fd1a17a3f738 Mon Sep 17 00:00:00 2001 From: Ryan J Daw Date: Fri, 1 Jun 2012 14:37:09 -0700 Subject: [PATCH] Initial commit. v1.1 --- LICENSE | 21 - Makefile | 11 + README.md | 2 +- api-browser.konf | 22 - api-client.konf | 7 - api-legacy.konf | 22 - api-node.konf | 18 - api/analytics.js | 136 - api/ark.js | 73 + api/blank.js | 0 api/combo-caching.js | 364 + api/combo-client.js | 131 + api/config.js | 15 + api/cont.js | 47 +- api/data.js | 367 - api/data2.js | 62 +- api/deferrable.js | 131 - api/enhance.js | 89 +- api/externals.js | 197 +- api/extractDOM.js | 94 + api/extractHTML.js | 136 + api/logging.js | 36 + api/main.js | 312 +- api/noconflict.js | 20 +- api/orientation.js | 65 + api/persistHash.js | 19 + api/resizeImages.js | 98 + api/stack.js | 13 +- api/studioJS.js | 58 + timing.js => api/timing.js | 39 +- api/tmpl.js | 310 +- api/util.js | 212 +- init.js | 1 - konf/base.konf | 75 + lib/defaultConf.konf => konf/defaults.konf | 3 +- lib/backbone.js | 1011 -- lib/base_konf.konf | 95 +- lib/base_legacy_konf.konf | 14 - lib/debug.js | 267 - lib/detect.konf | 207 - lib/fy/bubble.html | 132 - lib/fy/bubble.js | 108 - lib/fy/bubble2.js | 126 - lib/fy/menu.html | 61 - lib/fy/menu.js | 64 - lib/fy/slide.html | 42 - lib/jquery-1.5.2.js | 8376 --------------- lib/jquery-1.6.2.js | 8981 ---------------- lib/jquery-1.6.4.js | 9046 ----------------- lib/jquery-1.6.js | 8865 ---------------- lib/jquery-base-1.5.2.js | 5144 ---------- lib/jquery-latest.js | 1 - lib/jquery/jquery.cookie.js | 99 - lib/jquery/jquery.mapattributes.js | 29 - lib/jquery/jquery.outerhtml.js | 32 - lib/jquery/jquery.tap.js | 76 - lib/json2.js | 480 - lib/livereload.js | 811 -- lib/underscore.js | 855 -- lib/zepto.js | 862 -- package.json | 15 + test/detect-test.html | 46 - test/enhance-test.html | 71 - test/externals-test.html | 167 - test/fixtures-externals/plaintext.html | 13 - test/fixtures-unmobify/basic.html | 11 - test/fixtures-unmobify/escaped.html | 28 - test/fixtures-unmobify/legacy.html | 33 - .../plaintext-leading-content.html | 22 - .../plaintext-missing-body.html | 26 - .../plaintext-missing-closing-head.html | 32 - test/fixtures-unmobify/plaintext.html | 33 - test/qunit/qunit.js | 1597 --- test/test-server.js | 17 - test/unmobify-test.html | 226 - tests/combo.html | 105 + tests/enhance.html | 47 + tests/externals.html | 233 + tests/extractDOM.html | 37 + tests/fixtures/combo/combo-1.js | 4 + tests/fixtures/combo/combo-2.js | 4 + tests/fixtures/combo/combo-3.js | 4 + tests/fixtures/combo/combo-iframe.html | 16 + tests/fixtures/unmobify-basic.html | 10 + tests/qunit/junit.js | 89 + {test => tests}/qunit/qunit.css | 14 +- tests/qunit/qunit.js | 1803 ++++ tests/resizeImages.html | 69 + tests/runner.js | 64 + tests/runner.sh | 5 + tests/server.js | 10 + {test => tests}/util-test.html | 0 tmpl/analytics.tmpl | 108 - tmpl/base_mobileHead.tmpl | 19 - tmpl/base_root.tmpl | 118 +- tmpl/base_scripts.tmpl | 18 + tmpl/combo_client.tmpl | 224 + tmpl/legacy_root.tmpl | 17 + unmobify.js | 358 - vendor/bubble.js | 1 + {lib => vendor}/dust-core.js | 0 {lib/fy => vendor}/slide.js | 25 +- vendor/zepto.ie.js | 76 + vendor/zepto.js | 1274 +++ 104 files changed, 5805 insertions(+), 50274 deletions(-) delete mode 100644 LICENSE create mode 100644 Makefile delete mode 100644 api-browser.konf delete mode 100644 api-client.konf delete mode 100644 api-legacy.konf delete mode 100644 api-node.konf delete mode 100644 api/analytics.js create mode 100644 api/ark.js delete mode 100644 api/blank.js create mode 100644 api/combo-caching.js create mode 100644 api/combo-client.js create mode 100644 api/config.js delete mode 100644 api/data.js delete mode 100644 api/deferrable.js create mode 100644 api/extractDOM.js create mode 100644 api/extractHTML.js create mode 100644 api/logging.js create mode 100644 api/orientation.js create mode 100644 api/persistHash.js create mode 100644 api/resizeImages.js create mode 100644 api/studioJS.js rename timing.js => api/timing.js (65%) delete mode 100644 init.js create mode 100644 konf/base.konf rename lib/defaultConf.konf => konf/defaults.konf (93%) delete mode 100644 lib/backbone.js delete mode 100644 lib/base_legacy_konf.konf delete mode 100644 lib/debug.js delete mode 100644 lib/detect.konf delete mode 100644 lib/fy/bubble.html delete mode 100644 lib/fy/bubble.js delete mode 100644 lib/fy/bubble2.js delete mode 100644 lib/fy/menu.html delete mode 100644 lib/fy/menu.js delete mode 100644 lib/fy/slide.html delete mode 100644 lib/jquery-1.5.2.js delete mode 100644 lib/jquery-1.6.2.js delete mode 100644 lib/jquery-1.6.4.js delete mode 100644 lib/jquery-1.6.js delete mode 100644 lib/jquery-base-1.5.2.js delete mode 120000 lib/jquery-latest.js delete mode 100644 lib/jquery/jquery.cookie.js delete mode 100644 lib/jquery/jquery.mapattributes.js delete mode 100644 lib/jquery/jquery.outerhtml.js delete mode 100644 lib/jquery/jquery.tap.js delete mode 100644 lib/json2.js delete mode 100644 lib/livereload.js delete mode 100644 lib/underscore.js delete mode 100644 lib/zepto.js create mode 100644 package.json delete mode 100644 test/detect-test.html delete mode 100644 test/enhance-test.html delete mode 100644 test/externals-test.html delete mode 100644 test/fixtures-externals/plaintext.html delete mode 100644 test/fixtures-unmobify/basic.html delete mode 100644 test/fixtures-unmobify/escaped.html delete mode 100644 test/fixtures-unmobify/legacy.html delete mode 100644 test/fixtures-unmobify/plaintext-leading-content.html delete mode 100644 test/fixtures-unmobify/plaintext-missing-body.html delete mode 100644 test/fixtures-unmobify/plaintext-missing-closing-head.html delete mode 100644 test/fixtures-unmobify/plaintext.html delete mode 100644 test/qunit/qunit.js delete mode 100644 test/test-server.js delete mode 100644 test/unmobify-test.html create mode 100644 tests/combo.html create mode 100644 tests/enhance.html create mode 100644 tests/externals.html create mode 100644 tests/extractDOM.html create mode 100644 tests/fixtures/combo/combo-1.js create mode 100644 tests/fixtures/combo/combo-2.js create mode 100644 tests/fixtures/combo/combo-3.js create mode 100644 tests/fixtures/combo/combo-iframe.html create mode 100644 tests/fixtures/unmobify-basic.html create mode 100644 tests/qunit/junit.js rename {test => tests}/qunit/qunit.css (94%) create mode 100644 tests/qunit/qunit.js create mode 100644 tests/resizeImages.html create mode 100644 tests/runner.js create mode 100755 tests/runner.sh create mode 100644 tests/server.js rename {test => tests}/util-test.html (100%) delete mode 100644 tmpl/analytics.tmpl delete mode 100644 tmpl/base_mobileHead.tmpl create mode 100644 tmpl/base_scripts.tmpl create mode 100644 tmpl/combo_client.tmpl create mode 100644 tmpl/legacy_root.tmpl delete mode 100644 unmobify.js create mode 100644 vendor/bubble.js rename {lib => vendor}/dust-core.js (100%) rename {lib/fy => vendor}/slide.js (91%) create mode 100644 vendor/zepto.ie.js create mode 100644 vendor/zepto.js diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 629ecea4..00000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -Copyright (c) Mobify R&D Inc. -http://www.mobify.com/ - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..caed9d15 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +install: + npm install + +test: + ./tests/runner.sh + +jenkins: + ./tests/runner.sh + +all: + install \ No newline at end of file diff --git a/README.md b/README.md index e1ba9cf3..cc1b4e9f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ Mobify.js ========= -For information on using Mobify.js see [http://portal.mobify.com/docs/](http://portal.mobify.com/docs/). \ No newline at end of file +For information on using Mobify.js see [http://cloud.mobify.com/](http://cloud.mobify.com/). \ No newline at end of file diff --git a/api-browser.konf b/api-browser.konf deleted file mode 100644 index aaa6d6da..00000000 --- a/api-browser.konf +++ /dev/null @@ -1,22 +0,0 @@ -{%rebase} - -{>"lib/jquery/jquery.cookie.js"/} -{>"lib/jquery/jquery.outerhtml.js"/} -{>"lib/jquery/jquery.mapattributes.js"/} -{>"lib/dust-core.js"/} - -{>"api/analytics.js"/} -{>"api/util.js"/} -{>"api/externals.js"/} -{>"unmobify.js"/} - -{>"api/data2.js"/} -{>"api/stack.js"/} -{>"api/cont.js"/} - -{>"api/tmpl.js"/} - -{>"api/enhance.js"/} -{>"api/main.js"/} - -{/rebase} \ No newline at end of file diff --git a/api-client.konf b/api-client.konf deleted file mode 100644 index bb25ea0b..00000000 --- a/api-client.konf +++ /dev/null @@ -1,7 +0,0 @@ -{%rebase} - -{>"api/analytics.js"/} -{>"api/externals.js"/} -{>"api/enhance.js"/} - -{/rebase} \ No newline at end of file diff --git a/api-legacy.konf b/api-legacy.konf deleted file mode 100644 index bd403a9b..00000000 --- a/api-legacy.konf +++ /dev/null @@ -1,22 +0,0 @@ -{%rebase} - -{>"lib/jquery/jquery.cookie.js"/} -{>"lib/jquery/jquery.outerhtml.js"/} -{>"lib/jquery/jquery.mapattributes.js"/} -{>"lib/dust-core.js"/} - -{>"api/analytics.js"/} -{>"api/util.js"/} -{>"api/externals.js"/} -{>"unmobify.js"/} - -{>"api/deferrable.js"/} -{>"api/data.js"/} - -{>"api/tmpl.js"/} - -{>"api/enhance.js"/} - -{>"api/main.js"/} - -{/rebase} diff --git a/api-node.konf b/api-node.konf deleted file mode 100644 index b80a601b..00000000 --- a/api-node.konf +++ /dev/null @@ -1,18 +0,0 @@ -{%rebase} - -{>"lib/jquery/jquery.cookie.js"/} -{>"lib/jquery/jquery.outerhtml.js"/} -{>"lib/jquery/jquery.mapattributes.js"/} -{>"lib/dust-core.js"/} - -{>"api/util.js"/} - -{>"api/data2.js"/} -{>"api/stack.js"/} -{>"api/cont.js"/} - -{>"api/tmpl.js"/} - -{>"api/main.js"/} - -{/rebase} \ No newline at end of file diff --git a/api/analytics.js b/api/analytics.js deleted file mode 100644 index 03a88006..00000000 --- a/api/analytics.js +++ /dev/null @@ -1,136 +0,0 @@ -/* DEPRECATED DEPRECATED DEPRECATED DEPRECATED DEPRECATED DEPRECATED DEPRECATED */ -/* DEPRECATED DEPRECATED DEPRECATED DEPRECATED DEPRECATED DEPRECATED DEPRECATED */ - -/* This is now only used by api-legacy js konfs. Look at analytics.tmpl in the - template directory for up to date info. */ - -/* DEPRECATED DEPRECATED DEPRECATED DEPRECATED DEPRECATED DEPRECATED DEPRECATED */ -/* DEPRECATED DEPRECATED DEPRECATED DEPRECATED DEPRECATED DEPRECATED DEPRECATED */ - - -// TODO: It would be nice if these scripts were more natural in the mobify.js flow. -// TODO: Rename these functions, they're not just about GA anymore. -(function($, _) { - window._gaq = window._gaq || []; - - var ga = Mobify.ga = { - - init: function() { - // Load time rounded to nearest 100ms. - var ed = Mobify.evaluatedData; - var start; - try { - start = Mobify.timing.points[0][0]; - } catch (err) { - start = undefined; - } - - var loadTime = Math.round((Mobify.timing.addPoint('Done') - start) / 100) * 100; - var template = ed.bodyType || ed.rootPageType || 'miss'; - //var buildDate = (ed.siteConfig.buildDate || (ed && ed.buildDate)) + ''; - var buildDate = ( - (ed.siteConfig && ed.siteConfig.buildDate) || - (ed.buildDate) - ) + ''; - - // http://code.google.com/apis/analytics/docs/tracking/gaTrackingCustomVariables.html - // _setCustomVar scope levels: - // 1 => Visitor - // 2 => Session - // 3 => Page (default) - - // TODO: If they have multiple domains we need to configure a single tracking. - - // this looks weird but it's populated down in the push: definition - var ga_args = [['_setAccount', null]]; - // If we have site or konf specified args, populate them here, otherwise assume domain 'none' - // Corollory: this means site specified arguments should ALWAYS include _setDomainName - - var ga_domain = (function(hostname, domain_dict){ - var domain = 'none'; - if (! domain_dict) return domain; - var hostname_parts = hostname.split('.'); - - while (hostname_parts.length > 0) { - domain = domain_dict[hostname_parts.join('_')]; - if (!! domain) return domain; - hostname_parts = hostname_parts.slice(1); // cut off the head and continue - } - // fallback, domain is set to 'none' - return domain; - })(window.location.hostname, (!! ed.siteConfig) ? ed.siteConfig.ga_domains : false ); - ga_args.push(['_setDomainName', ((ga_domain === 'none') ? '' : '.') + ga_domain]); - - var site_args = ed.gaOptions || ((!! (ed.siteConfig && ed.siteConfig.ga_options)) ? ed.siteConfig.ga_options : []); - - // Our custom variables, some of which aren't currently being populated properly (timing, build_dt) - var ga_final = [ - ['_setCustomVar', 1, 'loadTime', '' + loadTime], - ['_setCustomVar', 2, 't', template], - ['_setCustomVar', 3, 'build_dt', buildDate], - ['_setCustomVar', 4, 'mobi', 'y', 1], - ['_trackPageview'], - ['_trackPageLoadTime'] - ]; - - var ga_args_array = ga_args.concat(site_args, ga_final); - ga.push.apply(this, ga_args_array); - - var insertAt = document.getElementsByTagName('script')[0] || document.getElementsByTagName('head')[0]; - var isSSL = location.protocol[4] == 's'; - - // JB: Would this ever really happen? - // PM: If clients aren't using GA in their site, yes. (Many ecommerce sites don't use GA.) - // QA Tracking. Load the QA script if its not already loaded. - if (!window._gat) { - var gaScript = document.createElement('script'); - gaScript.onload = gaScript.onreadystatechange = ga.load; - gaScript.src = '//' + (isSSL ? 'ssl' : 'www') + '.google-analytics.com/ga.js'; - insertAt.parentNode.insertBefore(gaScript, insertAt); - } else { - ga.load(); - } - - // Quantcast Tracking - var _qevents = window._qevents = window._qevents || []; - var qcScript = document.createElement('script'); - qcScript.src = '//' + (isSSL ? 'secure' : 'edge') + '.quantserve.com/quant.js'; - insertAt.parentNode.insertBefore(qcScript, insertAt); - - _qevents.push({qacct:"p-eb0xvejp1OUw6"}); - }, - - load: function() { - if (ga.loaded) return; - ga.loaded = true; - ga.push.apply(null, ga.q); - // Don't queue anymore. - ga.queue = ga.push; - }, - - loaded: false, - - // Queue arguments to be pushed to _gaq on ga.load. - queue: function() { - [].push.apply(ga.q, [].slice.call(arguments)); - }, - - q: [], - - // Iterates through arguments and pushes them to _gaq. - // Replaces null values with gaId. - push: function() { - var ed = Mobify.evaluatedData; - var args = [].slice.call(arguments); - _.each(ed.gaId || ((!! (ed.siteConfig && ed.siteConfig.ga_account)) ? [ed.siteConfig.ga_account] : false) || [], function(gaId, i) { - var prefix = 'MOBIFY' + i; - _.each(args, function(arg, j) { - var data = arg.slice(0); - data[0] = prefix + '.' + data[0]; - if (data[1] === null) data[1] = gaId; - _gaq.push(data); - }); - }); - } - }; -})(Mobify.$, Mobify._); diff --git a/api/ark.js b/api/ark.js new file mode 100644 index 00000000..d8af908a --- /dev/null +++ b/api/ark.js @@ -0,0 +1,73 @@ +/* Ark saves scripts before the flood (document.open) and can restore them after. + */ +(function(Mobify) { + +var contraband = {} + + , index = 0 + + , nextId = function() { + return "_generatedID_" + index++; + } + + // `document.open` wipes objects in all browsers but WebKit. + , documentOpenWipesObjects = !navigator.userAgent.match(/webkit/i) + , _store = function(name, fn) { + var bucket = contraband[name] = contraband[name] || []; + bucket.push(fn); + } + + , ark = Mobify.ark = { + // Store a script in the ark. + // `name`: Storage key. + // `fn`: What to store. + // `passive`: Whether `fn` should be executed now or not. + store: function(name, fn, passive) { + if (typeof name == 'function') { + passive = fn; + fn = name; + name = nextId(); + } + + if (!passive && fn.call) { + if (documentOpenWipesObjects) { + _store(name, fn); + } + fn(); + } else { + _store(name, fn); + } + + } + + // Returns the HTML to restore a script from the ark. + , load: function(sNames) { + var result = []; + if (sNames) { + var aNames = sNames.split(/[ ,]/); + for (var i = 0, l = aNames.length; i < l; ++i) { + var bucket = contraband[aNames[i]]; + if (!bucket) continue; + + for (var j = 0, bl = bucket.length; j < bl; ++j) { + var fn = bucket[j]; + if (fn.call) fn = '(' + fn + ')()'; + result.push(''); + } + } + } else { + for (var key in contraband) { + result.push(Mobify.ark.load(key)); + } + } + return result.join('\n'); + } + + // Dust helper to restore scripts from the ark. + , dustSection: function(chunk, context, bodies, params) { + var output = ark.load(params && params.name); + return chunk.write(output); + } + }; + +})(Mobify); \ No newline at end of file diff --git a/api/blank.js b/api/blank.js deleted file mode 100644 index e69de29b..00000000 diff --git a/api/combo-caching.js b/api/combo-caching.js new file mode 100644 index 00000000..76f944de --- /dev/null +++ b/api/combo-caching.js @@ -0,0 +1,364 @@ +(function() { + +/** + * HTTP 1.1 Caching header helpers + */ + +// Regular expressions for cache-control directives. +// See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 +var ccPublic = /^\s*public\s*$/ + , ccPrivate = /^\s*private\s*$/ + , ccNoCache = /^\s*no-cache\s*$/ + , ccNoStore = /^\s*no-store\s*$/ + , ccNoTransform = /^\s*no-store\s*$/ + , ccMustRevalidate = /^\s*must-revalidate\s*$/ + , ccProxyRevalidate = /^\s*proxy-revalidate\s*$/ + , ccMaxAge = /^\s*max-age\s*=\s*(\d+)\s*$/ + , ccSMaxAge = /^\s*s-maxage\s*=\s*(\d+)\s*$/ + + /** + * ccParse - takes a string argument that is an HTTP 1.1 + * Cache-Control directive and returns a parsed objects with booleans set for + * specified values and integers for max-age and s-maxage + */ + , ccParse = function (directive) { + var directives = directive.split(',') + + // A default object with keys for every cache-control response property, + // all of them will be null or boolean except max-age and s-maxage which + // should be positive integers + , parsedDirective = { + 'public': null + , 'private': null + , 'no-cache': null + , 'no-store': null + , 'no-transform': null + , 'must-revalidate': null + , 'proxy-revalidate': null + , 'max-age': null + , 's-maxage': null + }; + + directives.forEach(function(d) { + var matches; + if (ccPublic.test(d)) parsedDirective['public'] = true; + else if (ccPrivate.test(d)) parsedDirective['private'] = true; + else if (ccNoCache.test(d)) parsedDirective['no-cache'] = true; + else if (ccNoStore.test(d)) parsedDirective['no-store'] = true; + else if (ccNoTransform.test(d)) parsedDirective['no-transform'] = true; + else if (ccMustRevalidate.test(d)) { + parsedDirective['must-revalidate'] = true; + } else if (ccProxyRevalidate.test(d)) { + parsedDirective['proxy-revalidate'] = true; + } else if (matches = ccMaxAge.exec(d)) { + parsedDirective['max-age'] = matches[1]; + } else if (matches = ccSMaxAge.exec(d)) { + parsedDirective['s-maxage'] = matches[1]; + } + }) + + return parsedDirective; + } + + /** + * A function to compute the age of an HTTP response, returns the age in + * milliseconds + */ + , getAge = function(response) { + var apparentAge, date, age = 0, now = Date.now(); + if (date = response.headers['Date']) { + date = Date.parse(date); + } else { + date = now; + } + return apparentAge = now - date; + } + + /** + * getFreshnessLifetime - returns the freshnessLifetime of the response in + * milliseconds + */ + , getFreshnessLifetime = function (response) { + var cacheControl, maxAge, expires, now; + now = Date.now(); + + // If there's a max-age cache-control directive, return it + if (cacheControl = response.headers['cache-control']) { + cacheControl = ccParse(cacheControl); + if ((cacheControl['max-age'] !== null) && + (cacheControl['private'] === null) && + (cacheControl['no-store'] === null) && + (cacheControl['no-cache'] === null)) { + + maxAge = parseInt(cacheControl['max-age']); + // max-age is in seconds, these functions deal in milliseconds + return maxAge * 1000; + } + } + + // Otherewise, try to compute a max-age from the Expires header + if (expires = response.headers['expires']) { + expires = Date.parse(expires); + return expires - now; + } + + // Otherwise, the freshness lifetime is 0 + return 0; + } + + /** + * maxAge - takes an http response object, returns a new max-age value, + * in seconds, for a new response containing it to be sent now. + */ + , maxAge = function (response) { + var newMaxAge = getFreshnessLifetime(response) - getAge(response); + return Math.floor(newMaxAge/1000); + } + + + /** + * A function to create a data URI from a mobify combo resource object + */ + , dataURI = function(resource) { + return 'data:' + + resource.headers['content-type'] + + (!resource['text'] ? (';base64,' + resource.body) : + (',' + encodeURIComponent(resource.body))); + } + + + , cache = Mobify.httpCaching = { + /** + * Predicate for determining whether or not a comboed resource is "stale" by + * HTTP 1.1 caching rules. + */ + isStale: function(resource) { + return getAge(resource) > maxAge(resource) + } + + , isCacheable: function(resource) { + return getFreshnessLifetime(resource) > 0 && !isStale(resource) + } + + /** + * Deletes resources from the dictionary that are considered stale by + * HTTP 1.1 caching rules. + */ + , evictStale: function() { + for (var i in resources) { + if (resources.hasOwnProperty(i) && isStale(resources[i])) { + delete resources[i] + } + } + } + + /** + * Takes an array of urls to be comboed, returns a list of those URLs that + * are not already in the resources dictionary. + */ + , notCachedUrls: function(urls) { + var notCachedUrls = []; + for (var i = 0; i < urls.length; i++) { + if (!(resources[urls[i]])) { + notCachedUrls.push(urls[i]); + } + } + + return notCachedUrls; + } + + /** + * Takes an array of urls to be comboed, returns those which are already in + * the resources dictionary. + */ + , cachedUrls: function(urls) { + var cachedUrls = []; + for (var i = 0; i < urls.length; i++) { + if (resources[urls[i]]) { + cachedUrls.push(urls[i]); + } + } + + return cachedUrls; + } + } + + , localStorageAvailable = (function() { + var k = 'MobifyTestLocalStorage', v = 'Yay!'; + try { + localStorage.setItem(k, v); + if(v === localStorage.getItem(k)) { + localStorage.removeItem(k); + return true; + } else { + return false; + } + } catch(e) { + return false; + } + })() + + // Global store of resources downloaded with the combo service. + , resources = {} + + , storeResource = function(resource) { + var url = resource.url; + + /* ensure this response was successfully fetched by the service */ + if (resource.status == 'ready') { + resources[url] = resource; + } else { + console.log("Combo service failed to retrieve: %s", url); + } + } + + + /* asynchronously recursive function that attempts to whittle down a + cache to a storeable size */ + , evictAndStore = function(resources, attempts) { + var serialzed; + if (attempts == 0) { + console.log('Mobify.combo.dehydrateCache: evict and store attempts exceeded, aborting'); + // get rid of something. + } else { + evictOne(resources); + try { + serialzed = JSON.stringify(resources) + } catch(e) { + console.log("Mobify.combo.dehydrateCache error stringifying: " + e.message); + return; + } + + try { + localStorage.setItem(combo.key, serialzed); + // If localStorage is full, try again with one less item, "co-operatively". + } catch(e) { + setTimeout(function() { + evictAndStore(resources, attempts - 1) + }, 0); + } + } + } + + /* evict one item from a set of resources */ + /* I FOUND YOU FIRST cache eviction policy, bad, but fast! REPLACE ME */ + , evictOne = function(resources) { + for (var i in resources) { + if (resources.hasOwnProperty(i)) { + delete resources[i]; + return; + } + } + } + + , combo = Mobify.combo = { + + resources: resources + + , key: 'Mobify-Combo-Cache-v1.0' + + /** + * Store a resoruce or an array of resources from the mobify combo service + * in our resource dictionary. + */ + , store: function(r) { + if (r instanceof Array) { + for (var i = 0; i < r.length; i++) { + storeResource(r[i]); + } + } else { + storeResource(r); + } + } + + /** + * Retrieve a JS resource from the combo.resources object and write out a + * script tag with it as a dataURI. + * Note, document.writing the script tag is probably the only way to + * preserve execution order: + * http://blog.getify.com/ff4-script-loaders-and-order-preservation/ + */ + , loadSync: function(url) { + var r; + + /* do a little accounting for caching purposes */ + if (r = resources[url]) { + r.lastUsed = Date.now(); + r.useCount = r.useCount++ || 1 + /* if we have the resource in our dictionary, use data uri rather + than a network uri */ + url = dataURI(r) + } + /* write out a script tag which contains either the data uri of the + resource or the original network uri if for some reason it was not in + combo's resource dictionary */ + //console.log('document.write: ') + document.write(''); + } + + + /** + * Get keys out of the localStorage cache and into our in-memory reosurce + * dictionary, + */ + , rehydrateCache: function() { + if (!localStorageAvailable) return; + + var r, i, cacheContents = localStorage.getItem(this.key); + if (cacheContents !== null) { + try { + cacheContents = JSON.parse(cacheContents); + } catch(e) { + console.log('Mobify.combo.rehydrateCache: error parsing localStorage[' + + this.key + ']: ', e.message); + return; + } + + // Extract keys which are not loaded. + for (i in cacheContents) { + if (cacheContents.hasOwnProperty(i) && !resources[i]) { + resources[i] = cacheContents[i]; + } + } + } + } + + /** + * Store keys from the local reource dictionary back into the localStorage + * cache. + */ + , dehydrateCache: function() { + if (!localStorageAvailable) return; + + var MAX_ATTEMPTS = 10 + , toBeCached = {} + , serialized; + + // start by shallow copying the global resources dictionary, since + // we're going to modify its key list, but not its values + for (var i in resources) { + if (resources.hasOwnProperty(i)) { + toBeCached[i] = resources[i]; + } + } + + /* serialize the shallow copy */ + try { + serialzed = JSON.stringify(toBeCached) + } catch(e) { + console.log("Mobify.combo.dehydrateCache error stringifying: " + e.message); + return; + } + + try { + localStorage.setItem(this.key, serialzed); + // when localStorage is full, try again with one less item, "co-operatively" + } catch(e) { + setTimeout(function() { + evictAndStore(toBeCached, MAX_ATTEMPTS) + }, 0); + } + } + }; + +})(Mobify); \ No newline at end of file diff --git a/api/combo-client.js b/api/combo-client.js new file mode 100644 index 00000000..53a5e062 --- /dev/null +++ b/api/combo-client.js @@ -0,0 +1,131 @@ +(function(Mobify) { + +var caching = Mobify.httpCaching + + , combo = Mobify.combo + + , absolutify = document.createElement('a') + + /** + * encode the given object as uri encoded JSON + */ + , JSONURIencode = Mobify.JSONURIencode = function(object) { + return encodeURIComponent(JSON.stringify(object)); + } + + /** + * Generate a URL to the jsonp endpoint of the combo service for the + * given array of URLs + */ + , getComboStoreURL = function(urls) { + urls = caching.notCachedUrls(urls); + return defaults.endpoint + defaults.storeCallback + '/' + JSONURIencode(urls); + } + + , getComboStoreandLoadAsyncURL = function(urls) { + return defaults.endpoint + defaults.storeAndLoadAsyncCallback + '/' + JSONURIencode(urls); + } + + /** + * Prepare to make combo requests by rehydrating the cache, if there is one + * and getting rid of stale items. + */ + // rehydrate, evict stale items and save back the (potentially smaller) + // cache to localStorage + // Note, after this a "live copy" of the cache still exists at + // window.Mobify.combo.resources until "the flood" + , initializeFromCache = function() { + combo.rehydrateCache(); + caching.evictStale(); + combo.dehydrateCache(); + } + + /** + * Searches the collection for scripts and modifies them to use the `combo` + * service. Returns a collection suitable for use with document.write. + */ + , comboScriptSync = $.fn.comboScriptSync = function() { + var $scripts = this.filter(defaults.selector).add(this.find(defaults.selector)).remove() + , urls = [] + , url + , bootstrap; + + $scripts.filter('[' + defaults.attribute + ']').each(function() { + absolutify.href = this.getAttribute(defaults.attribute); + url = absolutify.href; + urls.push(url); + + this.removeAttribute(defaults.attribute); + + this.className += ' x-combo'; + + this.innerHTML = defaults.loadSyncCallback + "('" + url + "');"; + }); + + bootstrap = document.createElement('script'); + bootstrap.src = getComboStoreURL(urls); + + $scripts = $(bootstrap).add($scripts); + return $scripts; + } + + , comboScriptAsync = $.fn.comboScriptAsync = function() { + var $scripts = this.filter(defaults.selector).add(this.find(defaults.selector)).remove(); + var url, urls, uncached, cached, $loaders; + + // Collect up urls + $scripts.filter('[' + defaults.attribute + ']').each( function() { + absolutify.href = this.getAttribute(defaults.attribute); + url = absolutify.href; + urls.push(url); + }); + + /* Build a script tag that gets the uncached scripts, stores them and then + loads/executes them asynchronously */ + uncached = caching.notCachedUrls(urls); + var resourceLoader = document.createElement('SCRIPT'); + resourceLoader.src = getComboStoreandLoadAsyncURL(uncached); + + /* Build a second script tag that will be inline, and cause the cached + scripts to be laoded/executed asynchronously */ + cached = caching.cachedUrls(urls); + var loadCachedAsync = ''; + for(var i = 0; i < cached.length; i++) { + cachedScriptloaderText += defaults.loadAsyncCallback + + "('" + cached[i] + "');\n"; + } + + var cachedScriptLoader = document.createElement('SCRIPT'); + cachedScriptLoader.type = 'text/javascript'; + cachedScriptLoader.innerText = loadCachedAsync; + + return $(resourceLoader).add(cachedScriptLoader); + } + + // Combo defaults. + , defaults = combo.defaults = { + selector: 'script' + , attribute: 'x-src' + //, endpoint: '//jazzcat01.mobify.com/jsonp/' + , endpoint: '//combo.mobify.com/jsonp/' + , loadSyncCallback: 'Mobify.combo.loadSync' + , storeCallback: 'Mobify.combo.store' + , storeAndLoadAsyncCallback: 'Mobify.combo.storeAndLoadAsync' + , loadAsyncCallback: 'Mobify.combo.loadAsync' + }; + +// ## +// # CSS REWRITING SERVICE CLIENT FUNCTIONS +// ## + +// Endpoint host +var cssHost = 'combo.mobify.com' + /** + * Get the CSS rewriting service URL for the request corresponding to + * the given object. + */ + , cssURL = Mobify.cssURL = function(object) { + return '//' + cssHost + '/css/' + JSONURIencode(object); + }; + +})(Mobify); \ No newline at end of file diff --git a/api/config.js b/api/config.js new file mode 100644 index 00000000..8c264ab5 --- /dev/null +++ b/api/config.js @@ -0,0 +1,15 @@ +(function() { + var config = Mobify.config = Mobify.config || {}; + + // If loaded with preview, set debug, otherwise debug is off. + var match = /mobify-path=([^&;]*)/g.exec(document.cookie); + config.isDebug = match && match[1] ? 1 : 0; + + // configFile my already exists if rendering server side, so only grab mobify.js script tag + // if configFile is undefined. + // V6 moved mobify.js to the first script. + if (!config.configFile) { + config.configFile = Mobify.$('script[src*="mobify.js"]').first().attr('src') || ''; + } + config.configDir = config.configFile.replace(/\/[^\/]*$/, '/'); +})(); \ No newline at end of file diff --git a/api/cont.js b/api/cont.js index 2c18611c..44141bc3 100644 --- a/api/cont.js +++ b/api/cont.js @@ -1,4 +1,4 @@ -(function($, _, Mobify, undefined) { +(function($, Mobify, undefined) { var decodeAssignmentRe = /^([?!]?)(.*)$/ ,Location = window.Location ,Stack = Mobify.data2.stack @@ -22,7 +22,7 @@ } }; - _.extend(Cont, { + $.extend(Cont, { importance : {'!' : 1, '?' : -1, '' : 0} ,decodeAssignment : function(selector) { parse = selector.toString().match(decodeAssignmentRe); @@ -33,7 +33,7 @@ } }); - Cont.prototype = _.extend(new Stack(), { + Cont.prototype = $.extend(new Stack(), { extend: function(head, idx, len, env) { return new Cont(head, this, idx, len, env); } @@ -48,13 +48,13 @@ : this.get('source'); } ,all: function() { - return _(this.env()).find(function(stack) { - return !stack.tail.tail; - }).head; + var env; + for (env = this.env(); env.tail.tail; env = env.tail); + return env.head; } ,blankTarget: function() { var source = this.source(); - if (_.isArray(source)) return []; + if ($.isArray(source)) return []; if ($.isPlainObject(source)) return {}; } ,_eval : function(source) { @@ -94,7 +94,7 @@ try { if (!source) return source; - return (_.isFunction(source) && !source._async) + return ($.isFunction(source) && !source._async) ? source.call(this.env().head, this) : source; } catch (e) { return e; } @@ -104,7 +104,7 @@ sourceLength = source.length, continuation = this; - _.each(source, function(sourceFragment, idx) { + $.each(source, function(idx, sourceFragment) { if (sourceFragment && (sourceFragment.jquery || sourceFragment.nodeType) && (typeof idx == "string") && idx.indexOf('$')) { var root = continuation.root; @@ -120,13 +120,13 @@ ,evalReference : function() { - var ref, value, cont = this - ,assignment = Cont.decodeAssignment(this.index); + var ref, value, cont = this + , assignment = Cont.decodeAssignment(this.index); if (assignment.importance >= this.get('laziness')) { this.ref = ref = this.env().ref(assignment.selector, true); if (!ref) { - debug.warn(assignment.selector + Mobify.console.warn(assignment.selector , " has a syntax error or points to object that does not exist"); return; } @@ -168,7 +168,7 @@ ,forgotten = root.forgotten = root.forgotten || [] ,branches = arguments ,choices = root.choices = root.choices || {} - ,chosen = _.detect(branches, function(branch, idx) { + ,chosen = ([].some.call(branches, function(branch, idx) { var attempt = new Mobify.data2.cont({source: branch, laziness: 1}); attempt.root = attempt; attempt.env(cont.env().extend({})); @@ -177,8 +177,10 @@ [].push.apply(root.forgotten, attempt.forgotten || []); - return !(attempt.warnings && _.keys(attempt.warnings).length); - }) + for (var firstWarning in attempt.warnings) break; + if (!firstWarning) chosen = branch; + return !firstWarning; + }), chosen) ,chosenCont = cont.extend({source: chosen}, cont.index, cont.of); if (chosen) { @@ -190,7 +192,7 @@ ,continuation = this ,result; - result = _.map(source, function(sourceFragment, idx) { + result = $.map(source, function(sourceFragment, idx) { var cont = continuation .extend({source: evaluatable}, idx, sourceLength, { $: sourceFragment.tagName && $(sourceFragment).anchor() @@ -211,7 +213,7 @@ cont.env(cont.env().extend({THIS: responseData})); async.finish(cont.eval(evaluatable)); } else { - var context = $(Mobify.externals.disable(responseData)); + var context = $(Mobify.html.disable(responseData)); cont.env(cont.env().extend({THIS: context, $: context.anchor()})); async.finish(cont.eval(evaluatable)); } @@ -222,13 +224,16 @@ } ,tmpl: function(template, data) { var args = arguments; + if (template instanceof Array) template = template[0]; return Async(this, function(cont, async) { + var base = dust.makeBase({ lib_import: Mobify.ark.dustSection }); + if (args.length == 1) data = cont.all(); - dust.render(template, data, function(err, out) { + dust.render(template, base.push(data), function(err, out) { if (err) { async.finish(out); - debug.die(err); + Mobify.console.die(err); } else async.finish(out); }); }); @@ -255,10 +260,10 @@ ,continuation = this ,allHandlers = this.root.handlers; - _.each(allHandlers[event] || [], function(handler) { + $.each(allHandlers[event] || [], function(i, handler) { handler.apply(continuation, args); }); } }); -})(Mobify.$, Mobify._, Mobify); \ No newline at end of file +})(Mobify.$, Mobify); \ No newline at end of file diff --git a/api/data.js b/api/data.js deleted file mode 100644 index f727a790..00000000 --- a/api/data.js +++ /dev/null @@ -1,367 +0,0 @@ -(function($, _) { - - // Processing for data that will be fed to templating engine - // Data takes form of a regular JSON object, and is evaluated by: - // * Walking to inspect values inside arrays and objects - // * Returning >value< from {BASIC : >value< } expression without further processing - // * Replacing {CSS : >value< } with result of evaluation of selector through jQuery selector engine - -/* Feature use example -play: { - "$root": _$('#main-nav'), - a: _$('a'), - b: M._data('a').parent(), - extra: M._map( - _$("> div", M._data('*sliderPanel')), { - cat: "orange", - product: M._data('*@this').attr('id'), - text: M._data('*@idx') - } - ) -} */ - -var jQuery = $; - - var isCollection = function(leaf) { - if (!leaf) { - return false; - } - return _.isArray(leaf) - || ($.isPlainObject(leaf) - && !leaf.deferrable) - && !(window.Location && (leaf instanceof window.Location)); - }, - - evaluateLeaf = function(state, src) { - var ctx = state.ctx; - var env = state.env; - var result = src; - - // console.log(ctx, env, result); - - if (!result) { - return result; - } - if (_.isFunction(result)) { - try { - result = result.call(this, state); - } catch (e) { - debug.error(e); - result = undefined; - } - } - - if (result && result.deferrable) { - result = result._eval(state); - } - - return result; - }, - - dataSelectorRe = /(\.|\*|\?)/i, - - evalBasicDataSelector = function(state, selector, address) { - var env = state.env, - result = env.stack, - getAddress, - token, - mandatory = true, - predicate = '.', - - selector = selector.split(dataSelectorRe); - - for (var i = 0; i < selector.length; i +=1 ) { - token = selector[i]; - getAddress = address && (i === selector.length - 1); - switch (token) { - case "": - continue; - break; - case '?': - mandatory = false; - break; - case '.': case '*': - predicate = token; - break; - default: - if (predicate === ".") { - if ("head" in result) { - result = result.head ? result.head : env.global; - } - - if (getAddress) { - return [result, token, mandatory]; - } else { - result = result[token]; - } - } else if (predicate === "*") { - while (true) { - if (!result || !("head" in result)) { - result = env.global; - break; - } - if (result.head && (token in result.head)) { - result = result.head; - break; - } - result = result.tail; - } - - if (getAddress) { - return [result, token, mandatory]; - } else { - result = result[token]; - } - } - break; - } - } - if (!getAddress) return result; - }, - - dataSelectorIncludeRe = /([\[\]\{\}])/i, - - evalDataSelector = function(state, selector, address) { - var stack = [[]]; - var token, result; - - if (_.isNumber(selector)) { - selector = selector.toString(); - } - if (_.isString(selector)) { - selector = selector.split(dataSelectorIncludeRe); - } - for (var i = 0; i < selector.length; i +=1 ) { - token = selector[i]; - switch (token) { - case '': - continue; - break; - case '{' : case '[' : - stack.push([]); - break; - case '}' : case ']' : - result = evalBasicDataSelector(state, stack.pop().join('')); - if (_.isUndefined(result)) { - return; - } - stack[stack.length - 1].push(result); - break; - default: - stack[stack.length - 1].push(token); - } - } - return evalBasicDataSelector(state, stack.pop().join(''), address) - }, - - evaluateNode = function(state, src, key, len) { - var ctx = state.ctx; - var env = state.env; - var srcIsCollection = isCollection(src); - var computedKey = evalDataSelector(state, key, true); - var parentDest = computedKey[0]; - var destKey = computedKey[1]; - var mandatory = computedKey[2]; - var path = ctx.get('path').concat(key); - var innerEnv, innerCtx, dest; - - if (srcIsCollection && _.isUndefined(parentDest[destKey])) { - dest = parentDest[destKey] = _.isArray(src) ? [] : {}; - } - - innerEnv = env.push(dest, destKey, len), - innerCtx = ctx.push({ - src: src, - dest: dest, - path: path - }, destKey, len); - - if (srcIsCollection) { - evaluateCollection({ctx: innerCtx, env: innerEnv}); - return dest; - } else { - var result = evaluateLeaf({ctx: innerCtx, env: env}, src); - parentDest[destKey] = result; - // if (mandatory && isFalsey(result) - if (mandatory && ((result === null) || (result === undefined) || (result === '') - || ($.isPlainObject(result) && _.isEmpty(result)) - || ((typeof result.length != 'undefined') && !result.length))) { - state.ctx.get('warnings').push([path.join('.'), result]); - } - } - }, - - evaluateCollection = function(state) { - _.each(state.ctx.get('src'), function(src, key, parentSrc) { - evaluateNode(state, src, key, parentSrc.length); - }); - }, - - M = { - $ : Mobify.$, - _ : Mobify._, - _$: Mobify.deferrable.jQuery, - map: function(state, value, src) { - if (value && value.deferrable) { - value = value._eval(state); - } - - if (value.jquery) value = value.toArray(); - var newDest = [], - newState = { - ctx: state.ctx.push({src: src, dest: newDest}) - }; - - _.each(value, function(v, key) { - if (v.nodeType) v = $(v); - newState.env = state.env.push({"@this" : v, "$root": v, "@idx" : key}).push(newDest); - evaluateNode(newState, src, key, value.length); - }); - return newDest; - }, - _map: function(value, src) { - return function(state) { - return M.map(state, value, src); - } - }, - data: function(state, selector, value) { - var set = !_.isUndefined(value); - var address, result; - if (set) { - address = evalBasicDataSelector(state, selector, true); - if (value && value.deferrable) { - value = value._eval(state); - } - address[0][address[1]] = value; - } else { - value = evalBasicDataSelector(state, selector, false); - if (value && value.deferrable) { - value = value._eval(state); - } - } - return value; - }, - _data: function(selector, value) { - return Mobify.deferrable.constructor(Mobify.deferrable.actuals, "data", arguments); - }, - - verbatim: _.identity, - - choose: function() { - var map = arguments, - str = Mobify.config.location.pathname.slice(1); - - return function(state) { - var result = _(map).detect(function(block) { - var value, - key = block[1], - detector = block[0]; - - if (_.isString(detector)) { - return $(detector, state.env.get('$root')).length; - } else if (detector.deferrable) { - value = detector._eval(state); - return (value.jQuery || _.isArray(value)) - ? value.length - : value; - } else if (detector instanceof RegExp) { - return detector.test(str); - } else { - return detector(state); - } - }); - if (!result || !("1" in result)) { - debug.group('All extracted data'); - debug.log(state.ctx.get('destRoot')); - debug.groupEnd(); - - debug.die("error: " + state.ctx.get('path').pop() + " did not match any of choose function selectors"); - } - - result = evaluateLeaf(state, result[1]); - - state.ctx.get('choices').push([state.ctx.get('path').join('.') + ' chose ' + result]); - - return result; - } - }, - cond: function(picker) { - var map = _.toArray(arguments).slice(1); - - return function(state) { - var result, - target = evaluateLeaf(state, picker), - match = _(map).detect(function(block) { - return (target === block[0]) - }); - - if (!match || !("1" in match)) { - debug.group('All extracted data'); - debug.log(state.ctx.get('destRoot')); - debug.groupEnd(); - - debug.die("error: cond function in " + state.ctx.get('path').pop() + " was given ", target, ", which did not match any of conditions"); - } - - match = match[1]; - - result = evaluateNode(state, match, state.ctx.stack.index); - state.ctx.get('branches').push([state.ctx.get('path').join('.') + ' conditional was ' + target + ', producing ', result]); - - return result; - } - } - }, data = { - M : M, - evaluate: function(state) { - var result = {}, - pending = [], - destRoot = {}, - ctx = dust.makeBase({ - src: state.data, - destRoot: destRoot, - dest: destRoot, - path: [], - warnings: [], - branches: [], - choices: [], - getPath: function() { return this.path; } - }), - env = dust.makeBase({'$root': state.data.$html}).push(destRoot); - - - evaluateCollection({ctx: ctx, env: env}); - - debug.group('Choices'); - _.map(ctx.get('choices'), function(log) { - debug.log.apply(debug, log); - }) - debug.groupEnd(); - - debug.group('Branches'); - _.map(ctx.get('branches'), function(log) { - debug.log.apply(debug, log); - }) - debug.groupEnd(); - - if (ctx.get('warnings').length) { - debug.group('Unfilled values'); - _.map(ctx.get('warnings'), function(warning) { - debug.warn.apply(debug, warning); - }) - debug.groupEnd(); - } - - debug.group('All extracted data'); - debug.log(destRoot); - debug.groupEnd(); - - return destRoot; - } - - - }; - - Mobify.data = data; - -})(Mobify.$, Mobify._); \ No newline at end of file diff --git a/api/data2.js b/api/data2.js index 5744dc29..3b20fad7 100644 --- a/api/data2.js +++ b/api/data2.js @@ -1,12 +1,12 @@ -(function($, _) { - +(function($) { + + var console = Mobify.console; var gatherEmpties = function(assignment, ref, value) { var root = this.root ,warnings = root.warnings = root.warnings || {} ,overwrites = root.overwrites = root.overwrites || {}; var isEmpty = (value === null) || (value === undefined) || (value === '') - || ($.isPlainObject(value) && _.isEmpty(value)) || ((typeof value.length != 'undefined') && !value.length) || (value instanceof Error) || (!value && (this.get('laziness') > 0)); @@ -25,14 +25,14 @@ ,logResult = function(value) { if (!this.tail) { - debug.logGroup('warn', 'Unfilled values', this.warnings); - debug.logGroup('warn', 'Missing -> Wrappers', this.forgotten); - debug.logGroup('log', 'Overwrites', this.overwrites); - debug.logGroup('log', 'Choices', this.choices); - - debug.group('All extracted data'); - debug.log(value); - debug.groupEnd(); + console.logGroup('warn', 'Unfilled values', this.warnings); + console.logGroup('warn', 'Missing -> Wrappers', this.forgotten); + console.logGroup('log', 'Overwrites', this.overwrites); + console.logGroup('log', 'Choices', this.choices); + + console.group('All extracted data'); + console.log(value); + console.groupEnd(); } } ,Async = function(cont, start) { @@ -64,7 +64,7 @@ async.finish = function(value) { result = value; done = true; - _.each(listeners, function(f) { + $.each(listeners, function(i, f) { f.call(async, value) }); } @@ -75,26 +75,33 @@ return done ? result : async; } - // TODO: If we want to Mobify this subb'd jQuery when / how should we do it? ,anchor = function($root) { - var anchored = $.sub(); + var rootedQuery = function(selector, context, rootQuery) { + $root = $root || (Mobify.conf.data && Mobify.conf.data.$html); + + return ($.fn.init || $.zepto.init).call(this, selector, context || anchored.context(), rootQuery); + }; + + var anchored = $.sub(rootedQuery); anchored.context = function() { return $root || (Mobify.conf.data ? Mobify.conf.data.$html : '
'); } - anchored.fn.init = function(selector, context, rootQuery) { - $root = $root || (Mobify.conf.data && Mobify.conf.data.$html); - - //Zepto won't have $.fn.init - return ($.fn.init || $).call(this, selector, context || anchored.context(), rootQuery); - }; + if (!anchored.zepto) { + anchored.fn.init = rootedQuery; + anchored.fn.init.prototype = $.fn; + } - anchored.fn.init.prototype = $.fn; - return anchored; } + $.sub = $.sub || function(rootedQuery) { + $.extend(rootedQuery, $); + rootedQuery.zepto = $.extend({}, $.zepto); + return rootedQuery; + }; + $.fn.anchor = function() { return anchor(this); }; @@ -102,7 +109,7 @@ Mobify.data2 = { gatherEmpties: gatherEmpties ,makeCont: function(opts) { - var cont = new Mobify.data2.cont(_.defaults(opts, {laziness: -1 })); + var cont = new Mobify.data2.cont($.extend({}, {laziness: -1 }, opts)); if (Mobify.config.isDebug) { cont .on('assignReference', gatherEmpties) @@ -111,10 +118,7 @@ return cont; } ,Async: Async - ,M: { - $ : anchor() - ,_ : _ - ,async : Async - } + ,M: {$ : anchor(), async: Async} }; -})(Mobify.$, Mobify._); \ No newline at end of file + +})(Mobify.$); \ No newline at end of file diff --git a/api/deferrable.js b/api/deferrable.js deleted file mode 100644 index 59e44432..00000000 --- a/api/deferrable.js +++ /dev/null @@ -1,131 +0,0 @@ -(function($, _) { - var _toString = function() { - return this._toString - ? this._toString() - : '"' + this.toString() + '"'; - }; - - var deferrable = Mobify.deferrable = { - actuals: { - _$: function(state, selector, context, rootQuery) { - return $(selector, context || state.env.get('$root'), rootQuery); - }, - data: function() { - var M = Mobify.data.M; - return M.data.apply(M, arguments); - }, - _toString: function() { return ''; } - }, - constructor: function(target, name, args) { - return new this.fn._init(target, name, args); - }, - jQuery: function() { - return Mobify.deferrable.constructor(Mobify.deferrable.actuals, "_$", arguments); - }, - chain: function(name) { - return function() { - return Mobify.deferrable.constructor(this, name, arguments); - } - }, - _throw: {} - } - - - - deferrable.fn = { - deferrable: "deferrable", - - // Make sure that _.isArray does not consider deferrables to be arrays - callee : true, - _toString : function() { - var sTarget = this.target && _toString.apply(this.target); - sTarget = sTarget ? sTarget + '.' : ""; - - var aArgs = _.map(this.args, function(arg) { - return _toString.apply(arg); - }); - var sArgs = this.args.length ? aArgs.join(', ') : ""; - return sTarget + this.name + '(' + sArgs + ')'; - }, - _init: function(target, name, args) { - this.target = target; - this.name = name; - this.args = _.toArray(args); - }, - - _eval: function(state) { - var result = this.__eval(state); - Mobify.timing.addPoint(' Selector ' + this._toString()); - if (result == deferrable._throw) { - return; - } - return result; - }, - - __eval: function(state) { - var result, - target = this.target, - name = this.name, - args = _(this.args).map(function(arg) { - if (arg && arg.deferrable) { - var evaluatedArg = arg.__eval(state); - if (evaluatedArg == deferrable._throw) { - result = deferrable._throw; - return deferrable._throw; - } - return evaluatedArg; - } else if (_.isFunction(arg)) { - return function() { - return arg.apply(this, [state].concat(_.toArray(arguments))) - } - } else return arg; - }); - - if (target.deferrable) { - target = target.__eval(state); - if (target == deferrable._throw) { - return target; - } - } - - if (!result) { - try { - if (target === deferrable.actuals) { - result = target[name].apply(target, [state].concat(args)); - } else if (name == "_get") { - result = target[args[0]]; - } else { - result = target[name].apply(target, args); - } - } catch(err) { - result = deferrable._throw; - } - } - - return result; - } - }; - - var arrayMethods = "pop|push|reverse|shift|sort|splice|unshift|concat|join|slice|toSource|toString|indexOf|lastIndexOf|filter|forEach|every|map|some|reduce|reduceRight".split("|"), - stringMethods = "charAt|charCodeAt|concat|indexOf|lastIndexOf|localeCompare|match|quote|replace|search|slice|split|substr|substring|toLocaleLowerCase|toLocaleUpperCase|toLowerCase|toSource|toString|toUpperCase|trim|trimLeft|trimRight|valueOf".split("|"), - elementMethods = "insertAdjacentElement|insertAdjacentHTML|insertAdjacentText|getElementsByTagName|getAttribute|querySelectorAll|webkitMatchesSelector|getElementsByClassName|contains|getBoundingClientRect|querySelector|hasAttribute|getAttributeNode|getAttributeNS|getElementsByTagNameNS|removeAttributeNS|getClientRects|scrollByPages|setAttributeNode|setAttributeNS|hasAttributeNS|blur|scrollIntoViewIfNeeded|setAttribute|scrollByLines|removeAttribute|setAttributeNodeNS|removeAttributeNode|getAttributeNodeNS|focus|scrollIntoView|addEventListener|appendChild|cloneNode|removeEventListener|compareDocumentPosition|insertBefore|removeChild|hasAttributes|isSupported|isEqualNode|dispatchEvent|isDefaultNamespace|hasChildNodes|normalize|replaceChild|isSameNode|lookupPrefix|lookupNamespaceURI".split('|'), - replacedMethods = []; - - var x = $(); - for (var i in x) { - if (_.isFunction(x[i])) { - replacedMethods.push(i); - } - } - - var all = replacedMethods.concat(['_get'], arrayMethods, stringMethods, elementMethods); - _.each(all, function(x) { - deferrable.fn[x] = deferrable.chain(x); - }); - - deferrable.fn._init.prototype = deferrable.fn; - -})(Mobify.$, Mobify._); - - - \ No newline at end of file diff --git a/api/enhance.js b/api/enhance.js index b5d370fe..51fe4720 100644 --- a/api/enhance.js +++ b/api/enhance.js @@ -9,70 +9,6 @@ // http://windowsteamblog.com/windows_phone/b/wpdev/archive/2011/03/22/targeting-mobile-optimized-css-at-windows-phone-7.aspx (function(window, document, $) { -// ### -// # Orientation -// ### - -// Android `orientation` support is broken. -$.support.orientation = 'orientation' in window && 'onorientationchange' in window - && !/android/i.test(navigator.userAgent); - -var prevWidth - , prevOrientation - // Returns 'landscape' or 'portrait' based on the current orientation. - getOrientation = function() { - var docEl = document.documentElement; - return ($.support.orientation - // 0 in portrait, 1 in landscape - ? orientation % 180 - // false in portrait, true in landscape - : docEl.clientWidth > docEl.clientHeight) - ? 'landscape' - : 'portrait'; - } - - // Some Android browsers (HTC Sensation) don't update widths immediately, - // so wait to trigger the event. - , orientationHandler = function() { - function triggerEvent() { - var width = document.documentElement.clientWidth - , orientation; - - if (width == prevWidth) { - return setTimeout(triggerEvent, 250); - } - - prevWidth = width; - - orientation = getOrientation(); - if (orientation != prevOrientation) { - prevOrientation = orientation; - $(window).trigger('orientationchange'); - } - } - - triggerEvent(); - } - -// Polyfill `orientationchange` event. -$.event.special.orientationchange = { - setup: function() { - if ($.support.orientation) return false; - $(window).bind('resize', orientationHandler); - }, - teardown: function() { - if ($.support.orientation) return false; - $(window).unbind('resize', orientationHandler); - }, - add: function(handleObj) { - var handler = handleObj.handler; - handleObj.handler = function(e) { - e.orientation = getOrientation(); - return handler.apply(this, arguments); - }; - } -} - // ### // # Device Properties // ### @@ -87,13 +23,16 @@ var $test = $('
', {id: 'mc-test'}) , osMatch = /(ip(od|ad|hone)|android|nokia|blackberry|webos)/gi.exec(navigator.userAgent) , os = (osMatch && (osMatch[2] ? 'ios' : osMatch[1].toLowerCase())) || 'desktop' + , tablet = /ipad|android(?!.*mobile)/i.test(navigator.userAgent) + + , smartphone = ((os != 'desktop') && !tablet) + // Device Pixel Ratio: 1, 1.5, 2.0 , dpr = 1 , q = [ 'screen and (-webkit-min-device-pixel-ratio:1.5)', - 'screen and (-webkit-min-device-pixel-ratio:2)', + 'screen and (-webkit-min-device-pixel-ratio:2)' ]; - // Use `devicePixelRatio` if available, falling back to querying using // `matchMedia` or manual media queries. if ('devicePixelRatio' in window) { @@ -121,7 +60,6 @@ if ('devicePixelRatio' in window) { } } - // ### // # Mobify.config // ### @@ -129,10 +67,12 @@ if ('devicePixelRatio' in window) { // Expose Touch, OS, HD and Orientation properties on `Mobify.config` for // use in templating. -var config = Mobify.config; +var config = Mobify.config || {}; config.os = os; +config.tablet = tablet; +config.smartphone = smartphone; config.touch = touch; -config.orientation = getOrientation(); +config.orientation = Mobify.orientation(); if (dpr > 1) { config.HD = '@2x'; @@ -156,8 +96,7 @@ if (dpr > 1) { // .dpr1 .dpr15 .dpr2 Mobify.enhance = function() { - var prevOrientation = getOrientation() - , classes = [os, (!touch ? 'no' : '') + 'touch', prevOrientation]; + var classes = [os, (!touch ? 'no' : '') + 'touch', Mobify.orientation()]; if (dpr > 1) { classes.push('hd' + (dpr + '').replace(/[^\w]/, ''), 'hd'); @@ -167,11 +106,9 @@ Mobify.enhance = function() { $('html').addClass('x-' + classes.join(' x-')); - $(window).bind('orientationchange', function() { - var orientation = getOrientation(); + Mobify.orientation(function(orientation, prevOrientation) { $('html').removeClass('x-' + prevOrientation).addClass('x-' + orientation); - prevOrientation = orientation; - }) + }); }; -})(this, document, Mobify.$); \ No newline at end of file +})(this, document, Mobify.$); diff --git a/api/externals.js b/api/externals.js index e9bc4fdb..1f7966ef 100644 --- a/api/externals.js +++ b/api/externals.js @@ -1,138 +1,85 @@ -(function($, _, Mobify, document) { - var attributesToEnable = { - link: ['href'], - img: ['src'], - iframe: ['src'], - style: ['media'], - script: ['src', 'type'] - } - // JB: Why is `src` in attributes to kill? - , attributesToKill = {img: ['src', 'width', 'height']} - , attributesToDisable = _({}).extend(attributesToEnable, attributesToKill) - // Cache for expressions generated by `getAttrRe`. - , attrExps = {} - , tagRe = new RegExp( - '<(' - + _.keys(attributesToEnable).join('|') - + ')([\\s\\S]*?)>', 'gi') - , openingScriptRe = new RegExp('()', 'gi') - , timing = Mobify.timing; - - function getAttrRe(srcAttr) { - // ='...'|"..." - return new RegExp( - '\\s+((?:' - + srcAttr - + ")\\s*=\\s*(?:'([\\s\\S])+?'|\"([\\s\\S])+?\"))", 'gi'); - } +(function($, Mobify) { - function deactivate(whole, tag, tail) { - tag = tag.toLowerCase(); +var keys = function(obj) { return $.map(obj, function(val, key) { return key }) } + , values = function(obj) { return $.map(obj, function(val, key) { return val }) } - var srcAttr = attributesToDisable[tag].join('|') - , attrRe = attrExps[srcAttr] = attrExps[srcAttr] || getAttrRe(srcAttr) - , killer = ''; - - // Give an unknown type. - if (tag == 'script') { - killer = ' type="text/mobify-script"'; - } else if (tag == 'style') { - killer = ' media="mobify-media"'; - } + , openingScriptRe = new RegExp('()', 'gi') - var result = '<' + tag + killer + tail.replace(attrRe, ' x-$1') + '>'; - return result; + // Inline styles are scripts are disabled using a unkonwn type. + , tagDisablers = { + style: ' media="mobify-media"' + , script: ' type="text/mobify-script"' } + , tagEnablingRe = new RegExp(values(tagDisablers).join('|'), 'g') + , disablingMap = { + img: ['src'] + , iframe: ['src'] + , script: ['src', 'type'] + , link: ['href'] + , style: ['media'] + } + , affectedTagRe = new RegExp('<(' + keys(disablingMap).join('|') + ')([\\s\\S]*?)>', 'gi') + , attributeDisablingRes = {} + , attributesToEnable = {} + , attributeEnablingRe + , HTML = Mobify.html = Mobify.html || {}; - Mobify.externals = { - // Disables external attributes in HTML eg. - // Doesn't disable attributes inside '); - } - - if (config.isDebug > 2) { - Mobify.weinre(hostname + ':8081'); - } - } - } + // In Webkit, `document.write` immediately executes inline scripts + // not preceded by an external resource. + document.write(markup); + document.close(); + }); + }, - Mobify.weinre = function(host) { - if (window.WeinreServerURL) return; - - var weinreScript = document.createElement('script'); - weinreScript.src = 'http://' + host + '/target/target-script-min.js#anonymous'; - document.body.appendChild(weinreScript); - } + // Kickstart processing. Guard against beginning before the document is ready. + run: function(conf) { + var prepareConf = function() { + Mobify.transform.prepareConf(conf); + }; - // Kickstart processing, and submit analytics upon its success or failure - Mobify.init = function(mode) { if (!/complete|loaded/.test(document.readyState)) { - window.setTimeout(Mobify.init, 100); - return; - } - - if (mode == "livereload") { - window.setTimeout(Mobify.load, 0); - return; + return setTimeout(prepareConf, 25); } - config.started = true; - - // Protection against Mobify.snippet calling `document.write` after - // `DOMContentLoaded`. - Mobify.snippet = $.noop; - - Mobify.load(); - }; - - if (Mobify.config.configFile.match(/\?livereload=\d+$/)) { - setTimeout(Mobify.load, 0); + prepareConf(); } +}); -})(this, document, Mobify); +})(document, Mobify.$, Mobify); -Mobify.timing.addPoint('Walked Mobify.js'); +Mobify.timing.addPoint('Walked Mobify.js'); \ No newline at end of file diff --git a/api/noconflict.js b/api/noconflict.js index 2c0b731e..78beffb4 100644 --- a/api/noconflict.js +++ b/api/noconflict.js @@ -1,12 +1,8 @@ -$.extend(Mobify, { - $: $.noConflict(true), - _: _.noConflict() -}); -window.debug = window.console; -// detect.js schedules a desktopAnalytics() call for tracking on normal -// untransformed desktop pages. This data can be used to determine what pages -// should be transformed first. -// However, if mobify.js was actually loaded, we are able to perform much -// more detailed analytics information gathering, and ought to cancel that -// desktopAnalytics call. The simplest way to do so is by nooping it. -Mobify.desktopAnalytics = function(){}; \ No newline at end of file +if ($.noConflict) { + Mobify.$ = $.noConflict(true) +} else { + Mobify.$ = window.Zepto; + Mobify.$.support = Mobify.$.support || {}; + if (Zepto === $) delete window.$; + delete window.Zepto; +} \ No newline at end of file diff --git a/api/orientation.js b/api/orientation.js new file mode 100644 index 00000000..32cddc11 --- /dev/null +++ b/api/orientation.js @@ -0,0 +1,65 @@ +(function(window, $) { + +// Android `orientation` support is broken. +var supportsOrientation = $.support.orientation + = 'orientation' in window && 'onorientationchange' in window + && !/android/i.test(navigator.userAgent) + + // Returns 'landscape' or 'portrait' based on the current orientation. + , getOrientation = function() { + var docEl = document.documentElement; + return !!(supportsOrientation + // 0 in portrait, 1 in landscape + ? orientation % 180 + // false in portrait, true in landscape + : docEl.clientWidth > docEl.clientHeight) + ? 'landscape' + : 'portrait'; + } + + // Some Android browsers (HTC Sensation) don't update widths immediately, + // so wait to trigger the event. + , prevWidth + , timeout + , ersatzOrientation = function() { + clearTimeout(timeout); + var width = document.documentElement.clientWidth; + if (width == prevWidth) { + return timeout = setTimeout(ersatzOrientation, 250); + } + prevWidth = width; + $(window).trigger('orientationchange'); + dispatchListeners(); + } + + , lastOrientation = getOrientation() + , listeners = [] + , dispatchListeners = function() { + var orientation = getOrientation(), + prev = lastOrientation; + + if (orientation != lastOrientation) { + lastOrientation = orientation; + + // We have this strange order and an extra variable + // to ensure that exception in a listener would not leave + // lastOrientation not updated + for (var i = 0, l = listeners.length; i < l; ++i) { + listeners[i](orientation, prev); + } + } + } + + , evName = supportsOrientation ? "orientationchange" : "resize" + , handler = supportsOrientation ? dispatchListeners : ersatzOrientation + , ensureOrientationHandler = function() { + $(window).unbind(evName, handler).bind(evName, handler); + } + +Mobify.orientation = function(fn) { + if (!fn) return getOrientation(); + ensureOrientationHandler(); + listeners.push(fn); +} + +})(this, Mobify.$); \ No newline at end of file diff --git a/api/persistHash.js b/api/persistHash.js new file mode 100644 index 00000000..71e67931 --- /dev/null +++ b/api/persistHash.js @@ -0,0 +1,19 @@ +(function() { + // V6 tags don't set cookies/storage themselves, so we set them here. + // https://github.com/mobify/portal_app/issues/186 + // + // mobify-path= + // mobify-all + + var hash = location.hash; + var match = /mobify-path=([^&;]+)/g.exec(hash); + if (match) { + var path = match[1]; + if (/mobify-all/.test(hash)) { + document.cookie = 'mobify-path=' + path + '; path=/'; + } else { + document.cookie = 'mobify-path=1; path=/'; + sessionStorage["mobify-path"] = path; + } + } +})(); \ No newline at end of file diff --git a/api/resizeImages.js b/api/resizeImages.js new file mode 100644 index 00000000..dc667e41 --- /dev/null +++ b/api/resizeImages.js @@ -0,0 +1,98 @@ +// 1) Set a device-width viewport +// 2) Set a border or outline on the body +// 3) get document.body.clientWidth +// 4) Give me a goddamn prize +(function(window, $) { + +var absolutify = document.createElement('a') + + , hosts = [ + '//ir0.mobify.com' + , '//ir1.mobify.com' + , '//ir2.mobify.com' + , '//ir3.mobify.com' + ] + + /** + * Hash `url` into a well distributed int. + */ + , URLHash = Mobify.URLHash = function(url) { + var hc, len = url.length; + + // Let's hash on 8 different character codes, chosen + // progresively back from the end of the URL, and xor 'em + hc = url.charCodeAt(len - 2 % len) ^ url.charCodeAt(len - 3 % len) + ^ url.charCodeAt(len - 5 % len) ^ url.charCodeAt(len - 7 % len) + ^ url.charCodeAt(len - 11 % len) ^ url.charCodeAt(len - 13 % len) + ^ url.charCodeAt(len - 17 % len) ^ url.charCodeAt(len - 19 % len) + + // A little linear congruential generator action to shuffle + // things up, inspired by libc's random number generator + hc = (((hc * 1103515245) % 4294967296 + 12345) % 4294967296); + hc = (hc < 0) ? hc + 4294967296: hc; + return hc; + } + + /** + * Returns a URL suitable for use with the 'ir' service. + * :host/:format:quality/:width/:height/:url + */ + , getImageURL = Mobify.getImageURL = function(url, options) { + options = options || {} + + var host = hosts[URLHash(url) % hosts.length] + , bits = [host]; + + if (options.format) { + bits.push(options.format + (options.quality || '')); + } + + if (options.maxWidth) { + bits.push(options.maxWidth) + + if (options.maxHeight) { + bits.push(options.maxHeight); + } + } + + bits.push(url); + return bits.join('/'); + } + + /** + * Searches the collection for imgs and modifies them to use the `ir` service. + * Pass `options` to modify how the images are serviced. + */ + , resizeImages = $.fn.resizeImages = function(options) { + var opts = $.extend(defaults, typeof options == 'object' && options) + , dpr = window.devicePixelRatio + , $imgs = this.filter(opts.selector).add(this.find(opts.selector)) + , attr; + + if (typeof options == 'number') { + opts.maxWidth = options; + } + + if (dpr) { + if (opts.maxWidth) { + opts.maxWidth = Math.ceil(opts.maxWidth * dpr); + } + + if (opts.maxHeight) { + opts.maxHeight = Math.ceil(opts.maxHeight * dpr); + } + } + + return $imgs.each(function() { + if (attr = this.getAttribute(opts.attribute)) { + absolutify.href = attr; + this.setAttribute('x-src', getImageURL(absolutify.href, opts)) + } + }); + } + , defaults = resizeImages.defaults = { + selector: 'img[x-src]' + , attribute: 'x-src' + } + +})(this, Mobify.$); diff --git a/api/stack.js b/api/stack.js index 416f6272..1c9f0276 100644 --- a/api/stack.js +++ b/api/stack.js @@ -1,4 +1,4 @@ -(function($, _) { +(function($, Mobify) { var Stack = Mobify.data2.stack = function(head, parent, idx, len) { this.tail = parent; this.head = head; @@ -13,9 +13,10 @@ return new Stack(head, this, idx, len); } ,crumbs: function() { - var crumbs = _(this).chain().pluck('index') - .reject(function(idx) { return idx == undefined}) - .reverse().value(); + var crumbs = []; + for (var walk = this; walk.tail; walk = walk.tail) { + if (walk.index !== undefined) crumbs.unshift(walk.index); + } crumbs.toString = function() { return this.join('.') }; return crumbs; @@ -43,7 +44,7 @@ // If token is numeric and stack.head is an array, we should // disable ascending logic and force local assignment. Otherwise, // we risk ending up with unwanted overwrites in cases of nested arrays - && (isNaN(token) || !_.isArray(stack.head))) { + && (isNaN(token) || !$.isArray(stack.head))) { while (!(token in stack.head) && stack.tail) stack = stack.tail; } @@ -75,4 +76,4 @@ } } }; -})(Mobify.$, Mobify._); \ No newline at end of file +})(Mobify.$, Mobify); \ No newline at end of file diff --git a/api/studioJS.js b/api/studioJS.js new file mode 100644 index 00000000..d6a81319 --- /dev/null +++ b/api/studioJS.js @@ -0,0 +1,58 @@ +(function($, Mobify) { + +if (!window.enableStudioJS) return; + +var _tagIndex = 0; +var indexTags = function(html) { + var tagIndex = /|[^<]+||()|(<\w+)((?:[^>'"]*|'[^']*?'|"[^"]*?")*\/?>)/gi + , result = html.replace(tagIndex, function(all, scriptName, scriptTail, name, tail) { + name = name || scriptName; + tail = tail || scriptTail; + if (!name) return all; + return name + ' mobifyjsindex="src' + tagIndex++ + '"' + tail; + }); + return result; +}; + +var get = function(key, callback) { + var handler = function(ev) { + if ((ev.data.command === 'html') + && (ev.data.dest === 'page') + && (ev.data.key === key) + && (ev.source === window)) { + window.removeEventListener("message", handler, false); + callback(ev.data.value, ev.data.key); + } + } + window.addEventListener("message", handler, false); +}; + +var set = function(key, value) { + window.postMessage({ + dest : 'extension', + command : 'html', + key: key, + value: value + }, '*'); +}; + +var oldEmitMarkup = Mobify.emitMarkup; +Mobify.emitMarkup = function(markup) { + Mobify.studioJS.get('renderHTML', function(markup) { + oldEmitMarkup(markup); + }); + Mobify.studioJS.set('resultHTML', markup); +}; + +var oldExtractHTML = Mobify.html.extractHTML; +Mobify.html.extractHTML = function() { + var captured = oldExtractHTML(markup); + $.each(captured, function(key, value) { + captured[key] = indexTags(value); + }); + + Mobify.studioJS.set('sourceHTML', captured.all()); + return result; +}; + +})(Mobify.$, Mobify); \ No newline at end of file diff --git a/timing.js b/api/timing.js similarity index 65% rename from timing.js rename to api/timing.js index 98a7db8f..a5c782ec 100644 --- a/timing.js +++ b/api/timing.js @@ -5,28 +5,8 @@ * Copyright (c) Mobify R&D Inc. * Full license available at http://portal.mobify.com/license/ */ -(function(Mobify) { - Mobify.restoreMethods && Mobify.restoreMethods(); - - // Comptability. - - // V6 needs this to think things have loaded. - Mobify.api = true; - - // V6 comptability. Copy properties to `Mobify.config`. - var config = Mobify.config = Mobify.config || {}; - if (Mobify.tagVersion) { - config.tagVersion = Mobify.tagVersion.join('.'); - } - - if (typeof config.tagVersion === 'string') { - config.tagVersion = parseInt(config.tagVersion) - } - - // V3 tags didn't have `Mobify.points`. - if (!Mobify.points) { - Mobify.points = [+new Date, +new Date] - } +(function ($, Mobify) { + var console = Mobify.console; function formatMillis(ms) { return (' ' + (+ms) + 'ms ').slice(-10); @@ -42,7 +22,7 @@ } // TODO: Break start out into it's own parameters - bandwidth etc. is unreleated to our load time. - Mobify.timing = { + var timing = Mobify.timing = { points: [], selectors: [], @@ -58,7 +38,7 @@ }, logGroup: function(group, name) { - var processed = Mobify._.map(group, formatEntry); + var processed = group.map(formatEntry); console.groupCollapsed ? console.groupCollapsed(name) @@ -67,7 +47,7 @@ if (console.dir) { console.dir(processed); } else { - Mobify._.each(processed, function(x) { + $.each(processed, function(i, x) { console.log(x); }); } @@ -77,13 +57,16 @@ logPoints: function() { this.logGroup(this.points, 'Global timing'); this.logGroup(this.selectors, 'Data evaluation timing'); + }, + // Allow plugins to reset timing for their own use. + reset: function() { this.points = []; this.selectors = []; } }; - Mobify.timing.addPoint('Finished Document', Mobify.points[1]); - Mobify.timing.addPoint('Loaded Mobify.js'); + timing.addPoint('Finished Document', Mobify.points[1]); + timing.addPoint('Loaded Mobify.js'); -})(Mobify); \ No newline at end of file +})(Mobify.$, Mobify); \ No newline at end of file diff --git a/api/tmpl.js b/api/tmpl.js index f17d1da6..52ae6e7f 100644 --- a/api/tmpl.js +++ b/api/tmpl.js @@ -1,174 +1,150 @@ -(function($, _) { - var Async = Mobify.data2 && Mobify.data2.Async; - - var Context = dust.makeBase({}).constructor, - Chunk = dust.stream('', {}).head.constructor, - oldExists = Chunk.prototype.exists, - oldNotExists = Chunk.prototype.notexists, - oldBlock = Chunk.prototype.block; - - Chunk.prototype.exists = function(elem, context, bodies) { - if (typeof elem === "function") { - elem = elem(this, context, bodies, 'exists'); - if (elem instanceof Chunk) { - return elem; - } - } - return oldExists.call(this, elem, context, bodies); - }; - - Chunk.prototype.notexists = function(elem, context, bodies) { - if (typeof elem === "function") { - elem = elem(this, context, bodies, 'notexists'); - if (elem instanceof Chunk) { - return elem; - } - } - return oldNotExists.call(this, elem, context, bodies); - }; - - Chunk.prototype.block = function(elem, context, bodies) { - var topElem = elem ? elem.shift() : undefined; - if (topElem) { - context = new context.constructor( - context.stack - ,_.extend( - context.global || {}, - { '_SUPER_' : function(_elem, context, _bodies) { - return _elem.block(elem, context, bodies); - }}) - ,context.blocks); - } - - return oldBlock.call(this, topElem, context, bodies); - }; - - - var descend = function(ctx, down, i) { +(function($, Mobify) { + +var Async = Mobify.data2 && Mobify.data2.Async + , Context = dust.makeBase({}).constructor + , Chunk = dust.stream('', {}).head.constructor + , oldExists = Chunk.prototype.exists + , oldNotExists = Chunk.prototype.notexists + , oldBlock = Chunk.prototype.block; + +Chunk.prototype.exists = function(elem, context, bodies) { + if (typeof elem === "function") { + elem = elem(this, context, bodies, 'exists'); + if (elem instanceof Chunk) { + return elem; + } + } + return oldExists.call(this, elem, context, bodies); +}; + +Chunk.prototype.notexists = function(elem, context, bodies) { + if (typeof elem === "function") { + elem = elem(this, context, bodies, 'notexists'); + if (elem instanceof Chunk) { + return elem; + } + } + return oldNotExists.call(this, elem, context, bodies); +}; + +Chunk.prototype.block = function(elem, context, bodies) { + var topElem = elem ? elem.shift() : undefined; + if (topElem) { + context = new context.constructor( + context.stack, + $.extend(context.global || {}, { + '_SUPER_': function(_elem, context, _bodies) { + return _elem.block(elem, context, bodies); + }}) + , context.blocks); + } + + return oldBlock.call(this, topElem, context, bodies); +}; + +var descend = function(ctx, down, i) { while (ctx && i < down.length) { - if (ctx._async) { - var unwrap = Async($.noop); - ctx.onresult.push(function(result) { - unwrap.result(descend(result, down, i)); - }); - return unwrap; - } + if (ctx._async) { + var unwrap = Async($.noop); + ctx.onresult.push(function(result) { + unwrap.result(descend(result, down, i)); + }); + return unwrap; + } ctx = ctx[down[i]]; i++; - } - + } return ctx; - } - - Context.prototype.getAscendablePath = function(cur, down) { - var ctx = this.stack; - - if (cur) return this.getPath(cur, down); - if (!ctx.isObject) return undefined; - - ctx = this.get(down[0]); - - return descend(ctx, down, 1); - }; - Context.prototype.getBlock = function(key) { - var blocks = this.blocks; + } - if (!blocks) return []; +Context.prototype.getAscendablePath = function(cur, down) { + var ctx = this.stack; + + if (cur) return this.getPath(cur, down); + if (!ctx.isObject) return undefined; + + ctx = this.get(down[0]); + + return descend(ctx, down, 1); +}; + +Context.prototype.getBlock = function(key) { + var blocks = this.blocks; + + if (!blocks) return []; + + blocks = $.map(blocks, function(block) { + return block[key]; + }); + return blocks; +} + +var likeArray = function(candidate) { + return (typeof candidate != 'string') + && (typeof candidate.length == 'number') + && (!candidate.tagName); + }; + +// Additional dust filters +// html returns node outerHTML +// innerHTML returns node innerHTML +// openTag and closeTag return first opening and last closing tags from a string +$.extend(dust.filters, { + h: function(node) { + if (!node) return ''; + if (likeArray(node)) { + return $.map(node, dust.filters.h).join(''); + } - blocks = _.compact(_.pluck(blocks, key)); - return blocks; + return (typeof node.outerHTML !== 'undefined') + ? node.outerHTML + : dust.escapeHtml(node); + } + + , innerHTML: function(node) { + if (!node) return ''; + if (likeArray(node)) { + return $.map(node, function(el) { + return el.innerHTML || el.nodeValue; + }).join('') + } else { + return $(node).html(); + } + } + , openTag: Mobify.html.openTag + + , closeTag: Mobify.html.closeTag +}); + +var conditionalHelper = function(chunk, context, bodies, accept) { + if (accept) { + return bodies.block(chunk, context); + } else if (bodies['else']) { + return bodies['else'](chunk, context); + } else { + return chunk; + } +} + +$.extend(dust.helpers, { + first: function(chunk, context, bodies) { + var accept = context.stack.index === 0; + return conditionalHelper(chunk, context, bodies, accept); + }, + last: function(chunk, context, bodies) { + var accept = context.stack.index === context.stack.of - 1; + return conditionalHelper(chunk, context, bodies, accept); } - - // Additional dust filters - // html returns node outerHTML - // innerHTML returns node innerHTML - // openTag and closeTag return first opening and last closing tags from a string - $.extend(dust.filters, { - h: function(value) { - if (_.isArray(value)) { - return _.map(value, dust.filters.h).join(''); - } - - if (value && value.nodeType) { - value = $(value); - } - - return (value.outerHTML && value.outerHTML.apply) - ? value.outerHTML() - : dust.escapeHtml(value); - }, - - html: function(nodes) { - var wrapper = $(document.createElement('div')).append( - $(nodes).filter(function(i, el) { - return el && el.nodeType; - }) - ); - return wrapper.html(); - }, - - innerHTML: function(node) { - if (node && node.jquery) { - node = node.toArray(); - } - if (_.isArray(node)) { - var res = []; - for (var i = 0; i < node.length; i+= 1) { - res.push($(node).html()); - } - return res.join(''); - } else { - return $(node).html(); - } - }, - - openTag: function(node) { - if (!node) return ''; - if (node.length) { - node = node[0]; - } - var attrs = $(node).mapAttributes(); - //alert(JSON.stringify(attrs)); - var attrStr = "", val; - for (var key in attrs) { - attrStr += ' ' + key + '="' + attrs[key] + '"'; - } - - var res = '<' + node.nodeName.toLowerCase() + attrStr + '>'; - return res; - }, - - closeTag: function(node) { - var firstNode = node.length ? node[0] : node; - return firstNode ? '' : ""; - } - }); - - $.extend(dust.helpers, { - first: function(chunk, context, bodies) { - if (context.stack.index === 0) { - return bodies.block(chunk, context); - } - if (bodies['else']) return bodies['else'](chunk, context); - return chunk; - }, - last: function(chunk, context, bodies) { - if (context.stack.index === context.stack.of - 1) { - return bodies.block(chunk, context); - } - if (bodies['else']) return bodies['else'](chunk, context); - return chunk; - } - }) - - var oldIsArray = dust.isArray; - dust.isArray = function(arr) { - return (arr && arr.jquery) || oldIsArray(arr); - } - - var oldLoad = dust.load; - dust.load = function(name, chunk, context) { - return name ? oldLoad.apply(this, arguments) : chunk; - } - -})(Mobify.$, Mobify._); +}) + +var oldIsArray = dust.isArray; +dust.isArray = function(arr) { + return (arr && arr.appendTo) || oldIsArray(arr); +} + +var oldLoad = dust.load; +dust.load = function(name, chunk, context) { + return name ? oldLoad.apply(this, arguments) : chunk; +} + +})(Mobify.$, Mobify); \ No newline at end of file diff --git a/api/util.js b/api/util.js index 76d2afa0..c09c2515 100644 --- a/api/util.js +++ b/api/util.js @@ -1,181 +1,31 @@ -(function(window) { - - var Mobify = window.Mobify - , $ = Mobify.$ - , _ = Mobify._ - , config = Mobify.config - , math = Math - , undefined; - - // ### - // # Logging - // ### - - debug.die = function() { - var args = _.toArray(arguments); - debug.group('(T_T) Fatal error (T_T)') - debug.error.apply(debug, args); - debug.groupEnd(); - - // unmobify() sets `Mobify.bail` when run. If set, we died in unmobify, - // and running it again won't help. - if (!config.isDebug && !Mobify.bail) { - Mobify.unmobify(); - } - - throw args; - }; - - debug.logGroup = function(fn, title, obj) { - var noneWritten = true; - _.each(obj, function(value, key) { - noneWritten && debug.group(title); - if (typeof key == "number") { - debug[fn].apply(window, value); - } else { - debug[fn](key, value); - } - - noneWritten = false; - }); - - noneWritten || debug.groupEnd(); - }; - - // ### - // # Utils - // ### - - // Set optout cookie and reload to goto desktop. - // V3.0: mobify=0 - // V3.X: mobify-js=-1 - // V6.X: mobify-path= - // - // `url`: Optional url to redirect to after opting out. - Mobify.desktop = function(url) { - var tagVersion = config.tagVersion - , val = tagVersion > 5 ? '-path=' : (tagVersion ? '-js=-1' : '=0') - - document.cookie = 'mobify' + val + '; path=/;'; - - if (url) { - location = url; - } else { - location.reload(); - } - }; - - // i18n function converts in a list of language types and data and returns - // a function that allows you to grab translation keys from that data - Mobify.i18n = function(list, data) { - list.push("DEFAULT"); - - var i18nlookup = function(key) { - for(var i = 0; i < list.length; i++) { - var value = data[list[i]][key]; - if (value) return value; - } - } - return i18nlookup; - }; - - // ### - // # SERVICES - // ### - - // 1) Set a device-width viewport - // 2) Set a border or outline on the body - // 3) get document.body.clientWidth - // 4) Give me a goddamn prize - (function() { - var absolutify = document.createElement('a') - , hosts = [ - '//ir0.mobify.com' - , '//ir1.mobify.com' - , '//ir2.mobify.com' - , '//ir3.mobify.com' - ] - - // Hash `url` into a well distributed int. - , URLHash = Mobify.URLHash = function(url) { - var hc, len = url.length; - - // Let's hash on 8 different character codes, chosen - // progresively back from the end of the URL, and xor 'em - hc = url.charCodeAt(len - 2 % len) ^ url.charCodeAt(len - 3 % len) - ^ url.charCodeAt(len - 5 % len) ^ url.charCodeAt(len - 7 % len) - ^ url.charCodeAt(len - 11 % len) ^ url.charCodeAt(len - 13 % len) - ^ url.charCodeAt(len - 17 % len) ^ url.charCodeAt(len - 19 % len) - - // A little linear congruential generator action to shuffle - // things up, inspired by libc's random number generator - hc = (((hc * 1103515245) % 4294967296 + 12345) % 4294967296); - hc = (hc < 0) ? hc + 4294967296: hc; - return hc; - } - - // Returns a URL suitable for use with irX.mobify.com. - // :host/:format:quality/:width/:height/:url - , getImageURL = Mobify.getImageURL = function(url, opts) { - opts = opts || {} - - var host = hosts[URLHash(url) % hosts.length] - , bits = [host]; - - if (opts.format) { - bits.push(opts.format + (opts.quality || '')); - } - - if (opts.maxWidth) { - bits.push(opts.maxWidth) - - if (opts.maxHeight) { - bits.push(opts.maxHeight); - } - } - - bits.push(url); - return bits.join('/'); - } - - // Alter the `src` of child images to pass through - // irX.mobify.com. Return the set of altered elements. - , resizeImages = $.fn.resizeImages = function(options) { - var opts = $.extend(resizeImages.defaults, typeof options == 'object' && options) - , dpr = window.devicePixelRatio; - - if (typeof options == 'number') { - opts.maxWidth = options; - } - - // https://github.com/Modernizr/Modernizr/pull/443 - if (dpr) { - if (opts.maxWidth) { - opts.maxWidth = math.ceil(opts.maxWidth * dpr); - } - - if (opts.maxHeight) { - opts.maxHeight = math.ceil(opts.maxHeight * dpr); - } - } - - var $imgs = this.filter(opts.selector).add(this.find(opts.selector)); - return $imgs.each(function() { - var attr = this.getAttribute(opts.attribute); - if (attr) { - absolutify.href = attr; - // This is slow, but its nice because it preloads the asset. - //this.src = getImageURL(absolutify.href, opts); - this.setAttribute('x-src', getImageURL(absolutify.href, opts)) - // this.removeAttribute(opts.attribute); - } - }); - }; - - resizeImages.defaults = { - selector: 'img[x-src]', - attribute: 'x-src' - }; - - })(); -})(this); +(function($, Mobify) { + +// Set optout cookie and reload to goto desktop. +// V6.X: mobify-path= +// +// `url`: Optional url to redirect to after opting out. +Mobify.desktop = function(url) { + document.cookie = 'mobify-path; path=/;'; + + if (url) { + location = url; + } else { + location.reload(); + } +}; + +// i18n function converts in a list of language types and data and returns +// a function that allows you to grab translation keys from that data +Mobify.i18n = function(list, data) { + list.push("DEFAULT"); + + var i18nlookup = function(key) { + for(var i = 0; i < list.length; i++) { + var value = data[list[i]][key]; + if (value) return value; + } + } + return i18nlookup; +}; + +})(Mobify.$, Mobify); diff --git a/init.js b/init.js deleted file mode 100644 index e5b3ca34..00000000 --- a/init.js +++ /dev/null @@ -1 +0,0 @@ -Mobify.init(); \ No newline at end of file diff --git a/konf/base.konf b/konf/base.konf new file mode 100644 index 00000000..631f0b9e --- /dev/null +++ b/konf/base.konf @@ -0,0 +1,75 @@ +{>"/base/api/ark.js"/} + +{#lib_export name="jquery"} + window.Mobify = window.Mobify || {}; + {+jquery} + {>"/base/vendor/zepto/src/polyfill.js"/} + {>"/base/vendor/zepto/src/zepto.js"/} + {>"/base/vendor/zepto/src/detect.js"/} + {>"/base/vendor/zepto/src/event.js"/} + {>"/base/vendor/zepto/src/fx.js"/} + {>"/base/vendor/zepto/src/ajax.js"/} + {>"/base/vendor/zepto/src/form.js"/} + {>"/base/vendor/zepto/src/selector.js"/} + {>"/base/vendor/zepto/src/stack.js"/} + + {>"/base/vendor/zepto.ie.js"/} + {/jquery} + {>"/base/api/noconflict.js"/} +{/lib_export} + +{#lib_export name="lib" passive="true"}{+lib} + {>"/base/vendor/slide.js"/} +{/lib}{/lib_export} + +{>"/base/api/persistHash.js"/} +{>"/base/api/logging.js"/} +{>"/base/api/util.js"/} +{>"/base/api/timing.js"/} +{>"/base/api/externals.js"/} +{>"/base/api/extractHTML.js"/} +{>"/base/api/extractDOM.js"/} + +{+dust} + {>"/base/vendor/dust-core.js"/} + {>"/base/api/data2.js"/} + {>"/base/api/stack.js"/} + {>"/base/api/cont.js"/} + {>"/base/api/tmpl.js"/} + {>"/base/api/studioJS.js"/} +{/dust} + +{>"/base/api/config.js"/} + +{#lib_export name="enhance"}{+enhance} + {>"/base/api/orientation.js"/} + {>"/base/api/enhance.js"/} +{/enhance}{/lib_export} + +{>"/base/api/main.js"/} +{>"/base/api/resizeImages.js"/} + +{+tmpl} + {>"/base/tmpl/*.tmpl"/} + {>"tmpl/*.tmpl"/} +{/tmpl} + +{+main} +Mobify.transform.run({+rawconf}function() { + var $ = this.$, M = this; + return { data: $.extend( + {>"/base/konf/defaults.konf"/}, + {+konf} + {! Use konf for plain JS configs, and data for legacy kaffeine ones. !} + {#kaffeine} + {+data}{/data} + {/kaffeine} + {/konf} + )}{/rawconf} +}); +{/main} + +// Version control tag, use an array with version numbers. Examples: +// V 1.1: [1,1]. V1.2.3: [1,2,3] + +Mobify.api = [1,1]; diff --git a/lib/defaultConf.konf b/konf/defaults.konf similarity index 93% rename from lib/defaultConf.konf rename to konf/defaults.konf index 6548647e..7424e9b2 100644 --- a/lib/defaultConf.konf +++ b/konf/defaults.konf @@ -32,6 +32,5 @@ cssName: function(cont) { return cont.data('configName').split('/').pop().split('.')[0] || 'stylesheet'; - }, - mobifylib: document.getElementById('x-mobifylib'), + } } diff --git a/lib/backbone.js b/lib/backbone.js deleted file mode 100644 index 9f441cbf..00000000 --- a/lib/backbone.js +++ /dev/null @@ -1,1011 +0,0 @@ -// Backbone.js 0.3.3 -// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. -// Backbone may be freely distributed under the MIT license. -// For all details and documentation: -// http://documentcloud.github.com/backbone - -(function(){ - - // Initial Setup - // ------------- - - // The top-level namespace. All public Backbone classes and modules will - // be attached to this. Exported for both CommonJS and the browser. - var Backbone; - if (typeof exports !== 'undefined') { - Backbone = exports; - } else { - Backbone = this.Backbone = {}; - } - - // Current version of the library. Keep in sync with `package.json`. - Backbone.VERSION = '0.3.3'; - - // Require Underscore, if we're on the server, and it's not already present. - var _ = this._; - if (!_ && (typeof require !== 'undefined')) _ = require("underscore")._; - - // For Backbone's purposes, either jQuery or Zepto owns the `$` variable. - var $ = this.jQuery || this.Zepto; - - // Turn on `emulateHTTP` to use support legacy HTTP servers. Setting this option will - // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a - // `X-Http-Method-Override` header. - Backbone.emulateHTTP = false; - - // Turn on `emulateJSON` to support legacy servers that can't deal with direct - // `application/json` requests ... will encode the body as - // `application/x-www-form-urlencoded` instead and will send the model in a - // form param named `model`. - Backbone.emulateJSON = false; - - // Backbone.Events - // ----------------- - - // A module that can be mixed in to *any object* in order to provide it with - // custom events. You may `bind` or `unbind` a callback function to an event; - // `trigger`-ing an event fires all callbacks in succession. - // - // var object = {}; - // _.extend(object, Backbone.Events); - // object.bind('expand', function(){ alert('expanded'); }); - // object.trigger('expand'); - // - Backbone.Events = { - - // Bind an event, specified by a string name, `ev`, to a `callback` function. - // Passing `"all"` will bind the callback to all events fired. - bind : function(ev, callback) { - var calls = this._callbacks || (this._callbacks = {}); - var list = this._callbacks[ev] || (this._callbacks[ev] = []); - list.push(callback); - return this; - }, - - // Remove one or many callbacks. If `callback` is null, removes all - // callbacks for the event. If `ev` is null, removes all bound callbacks - // for all events. - unbind : function(ev, callback) { - var calls; - if (!ev) { - this._callbacks = {}; - } else if (calls = this._callbacks) { - if (!callback) { - calls[ev] = []; - } else { - var list = calls[ev]; - if (!list) return this; - for (var i = 0, l = list.length; i < l; i++) { - if (callback === list[i]) { - list.splice(i, 1); - break; - } - } - } - } - return this; - }, - - // Trigger an event, firing all bound callbacks. Callbacks are passed the - // same arguments as `trigger` is, apart from the event name. - // Listening for `"all"` passes the true event name as the first argument. - trigger : function(ev) { - var list, calls, i, l; - if (!(calls = this._callbacks)) return this; - if (list = calls[ev]) { - for (i = 0, l = list.length; i < l; i++) { - list[i].apply(this, Array.prototype.slice.call(arguments, 1)); - } - } - if (list = calls['all']) { - for (i = 0, l = list.length; i < l; i++) { - list[i].apply(this, arguments); - } - } - return this; - } - - }; - - // Backbone.Model - // -------------- - - // Create a new model, with defined attributes. A client id (`cid`) - // is automatically generated and assigned for you. - Backbone.Model = function(attributes, options) { - attributes || (attributes = {}); - if (this.defaults) attributes = _.extend({}, this.defaults, attributes); - this.attributes = {}; - this._escapedAttributes = {}; - this.cid = _.uniqueId('c'); - this.set(attributes, {silent : true}); - this._previousAttributes = _.clone(this.attributes); - if (options && options.collection) this.collection = options.collection; - this.initialize(attributes, options); - }; - - // Attach all inheritable methods to the Model prototype. - _.extend(Backbone.Model.prototype, Backbone.Events, { - - // A snapshot of the model's previous attributes, taken immediately - // after the last `"change"` event was fired. - _previousAttributes : null, - - // Has the item been changed since the last `"change"` event? - _changed : false, - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize : function(){}, - - // Return a copy of the model's `attributes` object. - toJSON : function() { - return _.clone(this.attributes); - }, - - // Get the value of an attribute. - get : function(attr) { - return this.attributes[attr]; - }, - - // Get the HTML-escaped value of an attribute. - escape : function(attr) { - var html; - if (html = this._escapedAttributes[attr]) return html; - var val = this.attributes[attr]; - return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : val); - }, - - // Set a hash of model attributes on the object, firing `"change"` unless you - // choose to silence it. - set : function(attrs, options) { - - // Extract attributes and options. - options || (options = {}); - if (!attrs) return this; - if (attrs.attributes) attrs = attrs.attributes; - var now = this.attributes, escaped = this._escapedAttributes; - - // Run validation. - if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false; - - // Check for changes of `id`. - if ('id' in attrs) this.id = attrs.id; - - // Update attributes. - for (var attr in attrs) { - var val = attrs[attr]; - if (!_.isEqual(now[attr], val)) { - now[attr] = val; - delete escaped[attr]; - if (!options.silent) { - this._changed = true; - this.trigger('change:' + attr, this, val, options); - } - } - } - - // Fire the `"change"` event, if the model has been changed. - if (!options.silent && this._changed) this.change(options); - return this; - }, - - // Remove an attribute from the model, firing `"change"` unless you choose - // to silence it. - unset : function(attr, options) { - options || (options = {}); - var value = this.attributes[attr]; - - // Run validation. - var validObj = {}; - validObj[attr] = void 0; - if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; - - // Remove the attribute. - delete this.attributes[attr]; - delete this._escapedAttributes[attr]; - if (!options.silent) { - this._changed = true; - this.trigger('change:' + attr, this, void 0, options); - this.change(options); - } - return this; - }, - - // Clear all attributes on the model, firing `"change"` unless you choose - // to silence it. - clear : function(options) { - options || (options = {}); - var old = this.attributes; - - // Run validation. - var validObj = {}; - for (attr in old) validObj[attr] = void 0; - if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; - - this.attributes = {}; - this._escapedAttributes = {}; - if (!options.silent) { - this._changed = true; - for (attr in old) { - this.trigger('change:' + attr, this, void 0, options); - } - this.change(options); - } - return this; - }, - - // Fetch the model from the server. If the server's representation of the - // model differs from its current attributes, they will be overriden, - // triggering a `"change"` event. - fetch : function(options) { - options || (options = {}); - var model = this; - var success = function(resp) { - if (!model.set(model.parse(resp), options)) return false; - if (options.success) options.success(model, resp); - }; - var error = wrapError(options.error, model, options); - (this.sync || Backbone.sync)('read', this, success, error); - return this; - }, - - // Set a hash of model attributes, and sync the model to the server. - // If the server returns an attributes hash that differs, the model's - // state will be `set` again. - save : function(attrs, options) { - options || (options = {}); - if (attrs && !this.set(attrs, options)) return false; - var model = this; - var success = function(resp) { - if (!model.set(model.parse(resp), options)) return false; - if (options.success) options.success(model, resp); - }; - var error = wrapError(options.error, model, options); - var method = this.isNew() ? 'create' : 'update'; - (this.sync || Backbone.sync)(method, this, success, error); - return this; - }, - - // Destroy this model on the server. Upon success, the model is removed - // from its collection, if it has one. - destroy : function(options) { - options || (options = {}); - var model = this; - var success = function(resp) { - if (model.collection) model.collection.remove(model); - if (options.success) options.success(model, resp); - }; - var error = wrapError(options.error, model, options); - (this.sync || Backbone.sync)('delete', this, success, error); - return this; - }, - - // Default URL for the model's representation on the server -- if you're - // using Backbone's restful methods, override this to change the endpoint - // that will be called. - url : function() { - var base = getUrl(this.collection); - if (this.isNew()) return base; - return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + this.id; - }, - - // **parse** converts a response into the hash of attributes to be `set` on - // the model. The default implementation is just to pass the response along. - parse : function(resp) { - return resp; - }, - - // Create a new model with identical attributes to this one. - clone : function() { - return new this.constructor(this); - }, - - // A model is new if it has never been saved to the server, and has a negative - // ID. - isNew : function() { - return !this.id; - }, - - // Call this method to manually fire a `change` event for this model. - // Calling this will cause all objects observing the model to update. - change : function(options) { - this.trigger('change', this, options); - this._previousAttributes = _.clone(this.attributes); - this._changed = false; - }, - - // Determine if the model has changed since the last `"change"` event. - // If you specify an attribute name, determine if that attribute has changed. - hasChanged : function(attr) { - if (attr) return this._previousAttributes[attr] != this.attributes[attr]; - return this._changed; - }, - - // Return an object containing all the attributes that have changed, or false - // if there are no changed attributes. Useful for determining what parts of a - // view need to be updated and/or what attributes need to be persisted to - // the server. - changedAttributes : function(now) { - now || (now = this.attributes); - var old = this._previousAttributes; - var changed = false; - for (var attr in now) { - if (!_.isEqual(old[attr], now[attr])) { - changed = changed || {}; - changed[attr] = now[attr]; - } - } - return changed; - }, - - // Get the previous value of an attribute, recorded at the time the last - // `"change"` event was fired. - previous : function(attr) { - if (!attr || !this._previousAttributes) return null; - return this._previousAttributes[attr]; - }, - - // Get all of the attributes of the model at the time of the previous - // `"change"` event. - previousAttributes : function() { - return _.clone(this._previousAttributes); - }, - - // Run validation against a set of incoming attributes, returning `true` - // if all is well. If a specific `error` callback has been passed, - // call that instead of firing the general `"error"` event. - _performValidation : function(attrs, options) { - var error = this.validate(attrs); - if (error) { - if (options.error) { - options.error(this, error); - } else { - this.trigger('error', this, error, options); - } - return false; - } - return true; - } - - }); - - // Backbone.Collection - // ------------------- - - // Provides a standard collection class for our sets of models, ordered - // or unordered. If a `comparator` is specified, the Collection will maintain - // its models in sort order, as they're added and removed. - Backbone.Collection = function(models, options) { - options || (options = {}); - if (options.comparator) { - this.comparator = options.comparator; - delete options.comparator; - } - this._boundOnModelEvent = _.bind(this._onModelEvent, this); - this._reset(); - if (models) this.refresh(models, {silent: true}); - this.initialize(models, options); - }; - - // Define the Collection's inheritable methods. - _.extend(Backbone.Collection.prototype, Backbone.Events, { - - // The default model for a collection is just a **Backbone.Model**. - // This should be overridden in most cases. - model : Backbone.Model, - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize : function(){}, - - // The JSON representation of a Collection is an array of the - // models' attributes. - toJSON : function() { - return this.map(function(model){ return model.toJSON(); }); - }, - - // Add a model, or list of models to the set. Pass **silent** to avoid - // firing the `added` event for every new model. - add : function(models, options) { - if (_.isArray(models)) { - for (var i = 0, l = models.length; i < l; i++) { - this._add(models[i], options); - } - } else { - this._add(models, options); - } - return this; - }, - - // Remove a model, or a list of models from the set. Pass silent to avoid - // firing the `removed` event for every model removed. - remove : function(models, options) { - if (_.isArray(models)) { - for (var i = 0, l = models.length; i < l; i++) { - this._remove(models[i], options); - } - } else { - this._remove(models, options); - } - return this; - }, - - // Get a model from the set by id. - get : function(id) { - if (id == null) return null; - return this._byId[id.id != null ? id.id : id]; - }, - - // Get a model from the set by client id. - getByCid : function(cid) { - return cid && this._byCid[cid.cid || cid]; - }, - - // Get the model at the given index. - at: function(index) { - return this.models[index]; - }, - - // Force the collection to re-sort itself. You don't need to call this under normal - // circumstances, as the set will maintain sort order as each item is added. - sort : function(options) { - options || (options = {}); - if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); - this.models = this.sortBy(this.comparator); - if (!options.silent) this.trigger('refresh', this, options); - return this; - }, - - // Pluck an attribute from each model in the collection. - pluck : function(attr) { - return _.map(this.models, function(model){ return model.get(attr); }); - }, - - // When you have more items than you want to add or remove individually, - // you can refresh the entire set with a new list of models, without firing - // any `added` or `removed` events. Fires `refresh` when finished. - refresh : function(models, options) { - models || (models = []); - options || (options = {}); - this._reset(); - this.add(models, {silent: true}); - if (!options.silent) this.trigger('refresh', this, options); - return this; - }, - - // Fetch the default set of models for this collection, refreshing the - // collection when they arrive. - fetch : function(options) { - options || (options = {}); - var collection = this; - var success = function(resp) { - collection.refresh(collection.parse(resp)); - if (options.success) options.success(collection, resp); - }; - var error = wrapError(options.error, collection, options); - (this.sync || Backbone.sync)('read', this, success, error); - return this; - }, - - // Create a new instance of a model in this collection. After the model - // has been created on the server, it will be added to the collection. - create : function(model, options) { - var coll = this; - options || (options = {}); - if (!(model instanceof Backbone.Model)) { - model = new this.model(model, {collection: coll}); - } else { - model.collection = coll; - } - var success = function(nextModel, resp) { - coll.add(nextModel); - if (options.success) options.success(nextModel, resp); - }; - return model.save(null, {success : success, error : options.error}); - }, - - // **parse** converts a response into a list of models to be added to the - // collection. The default implementation is just to pass it through. - parse : function(resp) { - return resp; - }, - - // Proxy to _'s chain. Can't be proxied the same way the rest of the - // underscore methods are proxied because it relies on the underscore - // constructor. - chain: function () { - return _(this.models).chain(); - }, - - // Reset all internal state. Called when the collection is refreshed. - _reset : function(options) { - this.length = 0; - this.models = []; - this._byId = {}; - this._byCid = {}; - }, - - // Internal implementation of adding a single model to the set, updating - // hash indexes for `id` and `cid` lookups. - _add : function(model, options) { - options || (options = {}); - if (!(model instanceof Backbone.Model)) { - model = new this.model(model, {collection: this}); - } - var already = this.getByCid(model); - if (already) throw new Error(["Can't add the same model to a set twice", already.id]); - this._byId[model.id] = model; - this._byCid[model.cid] = model; - model.collection = this; - var index = this.comparator ? this.sortedIndex(model, this.comparator) : this.length; - this.models.splice(index, 0, model); - model.bind('all', this._boundOnModelEvent); - this.length++; - if (!options.silent) model.trigger('add', model, this, options); - return model; - }, - - // Internal implementation of removing a single model from the set, updating - // hash indexes for `id` and `cid` lookups. - _remove : function(model, options) { - options || (options = {}); - model = this.getByCid(model) || this.get(model); - if (!model) return null; - delete this._byId[model.id]; - delete this._byCid[model.cid]; - delete model.collection; - this.models.splice(this.indexOf(model), 1); - this.length--; - if (!options.silent) model.trigger('remove', model, this, options); - model.unbind('all', this._boundOnModelEvent); - return model; - }, - - // Internal method called every time a model in the set fires an event. - // Sets need to update their indexes when models change ids. All other - // events simply proxy through. - _onModelEvent : function(ev, model) { - if (ev === 'change:id') { - delete this._byId[model.previous('id')]; - this._byId[model.id] = model; - } - this.trigger.apply(this, arguments); - } - - }); - - // Underscore methods that we want to implement on the Collection. - var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect', - 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', - 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', - 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty']; - - // Mix in each Underscore method as a proxy to `Collection#models`. - _.each(methods, function(method) { - Backbone.Collection.prototype[method] = function() { - return _[method].apply(_, [this.models].concat(_.toArray(arguments))); - }; - }); - - // Backbone.Controller - // ------------------- - - // Controllers map faux-URLs to actions, and fire events when routes are - // matched. Creating a new one sets its `routes` hash, if not set statically. - Backbone.Controller = function(options) { - options || (options = {}); - if (options.routes) this.routes = options.routes; - this._bindRoutes(); - this.initialize(options); - }; - - // Cached regular expressions for matching named param parts and splatted - // parts of route strings. - var namedParam = /:([\w\d]+)/g; - var splatParam = /\*([\w\d]+)/g; - - // Set up all inheritable **Backbone.Controller** properties and methods. - _.extend(Backbone.Controller.prototype, Backbone.Events, { - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize : function(){}, - - // Manually bind a single named route to a callback. For example: - // - // this.route('search/:query/p:num', 'search', function(query, num) { - // ... - // }); - // - route : function(route, name, callback) { - Backbone.history || (Backbone.history = new Backbone.History); - if (!_.isRegExp(route)) route = this._routeToRegExp(route); - Backbone.history.route(route, _.bind(function(fragment) { - var args = this._extractParameters(route, fragment); - callback.apply(this, args); - this.trigger.apply(this, ['route:' + name].concat(args)); - }, this)); - }, - - // Simple proxy to `Backbone.history` to save a fragment into the history, - // without triggering routes. - saveLocation : function(fragment) { - Backbone.history.saveLocation(fragment); - }, - - // Bind all defined routes to `Backbone.history`. - _bindRoutes : function() { - if (!this.routes) return; - for (var route in this.routes) { - var name = this.routes[route]; - this.route(route, name, this[name]); - } - }, - - // Convert a route string into a regular expression, suitable for matching - // against the current location fragment. - _routeToRegExp : function(route) { - route = route.replace(namedParam, "([^\/]*)").replace(splatParam, "(.*?)"); - return new RegExp('^' + route + '$'); - }, - - // Given a route, and a URL fragment that it matches, return the array of - // extracted parameters. - _extractParameters : function(route, fragment) { - return route.exec(fragment).slice(1); - } - - }); - - // Backbone.History - // ---------------- - - // Handles cross-browser history management, based on URL hashes. If the - // browser does not support `onhashchange`, falls back to polling. - Backbone.History = function() { - this.handlers = []; - this.fragment = this.getFragment(); - _.bindAll(this, 'checkUrl'); - }; - - // Cached regex for cleaning hashes. - var hashStrip = /^#*/; - - // Set up all inheritable **Backbone.History** properties and methods. - _.extend(Backbone.History.prototype, { - - // The default interval to poll for hash changes, if necessary, is - // twenty times a second. - interval: 50, - - // Get the cross-browser normalized URL fragment. - getFragment : function(loc) { - return (loc || window.location).hash.replace(hashStrip, ''); - }, - - // Start the hash change handling, returning `true` if the current URL matches - // an existing route, and `false` otherwise. - start : function() { - var docMode = document.documentMode; - var oldIE = ($.browser.msie && (!docMode || docMode <= 7)); - if (oldIE) { - this.iframe = $(' - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/test/fixtures-externals/plaintext.html b/test/fixtures-externals/plaintext.html deleted file mode 100644 index 13837767..00000000 --- a/test/fixtures-externals/plaintext.html +++ /dev/null @@ -1,13 +0,0 @@ - - - -