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 : '
Install this web app on your phone: tap + and then Add to Home Screen.
- -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed est nunc, vestibulum id sagittis ut, bibendum eu tortor. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Quisque volutpat sodales nunc, sit amet egestas augue fringilla quis. Morbi congue adipiscing eros, quis semper odio tincidunt venenatis. Curabitur nisi nibh, ultrices non rhoncus in, facilisis vitae turpis. Cras et molestie nibh. Vivamus mollis molestie erat. Sed in magna nisi. Maecenas ac urna at felis varius consequat. Phasellus quis urna erat, ut adipiscing justo. Sed dapibus varius lectus eu consectetur.
-Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Integer non diam et elit dignissim viverra dapibus ac turpis. Donec eleifend molestie luctus. Donec ut nisi libero, id pharetra magna. Vivamus orci mi, iaculis in venenatis ac, pulvinar eu tortor. Pellentesque pulvinar, est sed ultrices consectetur, quam velit sagittis lorem, eget pulvinar ipsum magna vel eros. Curabitur sit amet diam nisl. Nunc aliquam tempus porttitor. Fusce fringilla nisl metus. Nullam tempus dapibus ante, eget egestas leo fermentum eu. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aenean luctus pretium lectus sed lobortis. Vivamus consectetur, tortor at ultricies rhoncus, massa enim vulputate nisi, ac hendrerit lectus velit vel orci.
-Mauris eget mauris nec nulla consectetur iaculis. Nulla et malesuada erat. Nullam porta lacus a sapien malesuada aliquam. Suspendisse consectetur iaculis leo eu dapibus. Pellentesque ullamcorper elementum diam sit amet elementum. Suspendisse potenti. Cras eget justo in augue consequat venenatis. Nunc id dapibus nulla. Etiam dapibus felis id eros accumsan id eleifend justo gravida. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec eu euismod sapien. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque adipiscing sagittis ligula ac porta. Proin varius facilisis nulla, vel aliquet metus eleifend eget.
-Curabitur tincidunt tincidunt dictum. Nam fermentum volutpat turpis. Vestibulum ullamcorper dapibus feugiat. Donec sodales, neque at rhoncus porttitor, nibh ligula malesuada mi, nec sodales ipsum orci ac eros. Morbi et orci ac nulla aliquam imperdiet. Phasellus faucibus ultrices nunc, ut pulvinar purus pellentesque eu. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec ac turpis lacus. Pellentesque vel lacus eu tellus congue lobortis. Aliquam vitae lectus leo. Pellentesque eget nibh tortor. Donec id ipsum id eros scelerisque adipiscing eu mattis lacus. Morbi ligula orci, vestibulum eu fermentum vitae, vehicula vitae nunc. Phasellus sit amet libero mauris. Proin lobortis sem eu nunc ultrices eget commodo leo interdum. Nullam lorem nulla, cursus in tincidunt nec, ullamcorper aliquet magna. Aliquam erat volutpat. Maecenas sed elit vitae erat gravida sagittis. Sed condimentum, velit dapibus facilisis gravida, nunc nulla dictum tortor, ac mattis risus tellus sit amet arcu. Integer suscipit, arcu a dignissim viverra, nulla sem pulvinar ligula, fermentum tempor leo ligula vel nunc.
-Cras eget augue erat. Cras porta consequat dui, ac pretium justo cursus et. Cras in elit sed libero adipiscing consectetur vel eu risus. Aliquam quis augue non est mollis gravida sit amet laoreet nulla. Etiam est velit, venenatis eget rutrum pellentesque, aliquet et lacus. Sed suscipit dignissim pulvinar. Suspendisse posuere arcu et nunc tincidunt vel bibendum nunc euismod. Duis et sapien libero, sit amet varius purus. Integer vitae orci velit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nulla mollis placerat erat, ut lacinia tellus iaculis et. Praesent orci quam, volutpat nec placerat ac, pharetra at metus. Nullam suscipit pharetra ipsum id tempor. Nam ultrices lorem a orci dignissim euismod.