diff --git a/CHANGELOG b/CHANGELOG
new file mode 100644
index 000000000..7cead33f2
--- /dev/null
+++ b/CHANGELOG
@@ -0,0 +1,18 @@
+Sitespeed.io 0.8
+------------------------
+
+
+Sitespeed.io 0.7
+------------------------
+
+Enhancements:
+* Upgraded to jquery 1.8
+* Upgraded Twitter Bootstrap to 2.1
+* Better title tag on result pages
+
+Defects:
+* Fixed so that long url:s don't break
+* Sometimes output xml was broken
+* Only fetch content of type html
+
+
diff --git a/README.md b/README.md
index 70951cafd..2429992e0 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,6 @@
Sitespeed.io - how speedy is your site? [](http://travis-ci.org/soulgalore/sitespeed.io)
=============
-
Sitespeed.io is a tool that analyzes web sites and give you information of why they are slow and how you can optimize the web prformance. Today yslow rules are used in combination with other best practices.
What do sitespeed.io do?
diff --git a/dependencies/yslow-3.1.4-sitespeed.js b/dependencies/yslow-3.1.4-sitespeed.js
new file mode 100644
index 000000000..4fdeeaa9e
--- /dev/null
+++ b/dependencies/yslow-3.1.4-sitespeed.js
@@ -0,0 +1,10347 @@
+/**
+ * Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+ * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
+ */
+
+/*global phantom, YSLOW*/
+/*jslint browser: true, evil: true, sloppy: true, regexp: true*/
+
+/**
+ * JSLint is tolerating evil because there's a Function constructor needed to
+ * inject the content coming from phantom arguments and page resources which is
+ * later evaluated into the page in order to run YSlow.
+ */
+
+// For using yslow in phantomjs, see instructions @ https://github.com/marcelduran/yslow/wiki/PhantomJS
+
+// parse args
+var i, arg, page, urlCount, viewport,
+ webpage = require('webpage'),
+ args = phantom.args,
+ len = args.length,
+ urls = [],
+ yslowArgs = {
+ info: 'all',
+ format: 'json',
+ ruleset: 'ydefault',
+ beacon: false,
+ ua: false,
+ viewport: false,
+ headers: false,
+ console: 0,
+ threshold: 80
+ },
+ unaryArgs = {
+ help: false,
+ version: false,
+ dict: false,
+ verbose: false
+ },
+ argsAlias = {
+ i: 'info',
+ f: 'format',
+ r: 'ruleset',
+ h: 'help',
+ V: 'version',
+ d: 'dict',
+ u: 'ua',
+ vp: 'viewport',
+ c: 'console',
+ b: 'beacon',
+ v: 'verbose',
+ t: 'threshold',
+ ch: 'headers'
+ };
+
+// loop args
+for (i = 0; i < len; i += 1) {
+ arg = args[i];
+ if (arg[0] !== '-') {
+ // url, normalize if needed
+ if (arg.indexOf('http') !== 0) {
+ arg = 'http://' + arg;
+ }
+ urls.push(arg);
+ }
+ arg = arg.replace(/^\-\-?/, '');
+ if (yslowArgs.hasOwnProperty(arg)) {
+ // yslow argument
+ i += 1;
+ yslowArgs[arg] = args[i];
+ } else if (yslowArgs.hasOwnProperty(argsAlias[arg])) {
+ // yslow argument alias
+ i += 1;
+ yslowArgs[argsAlias[arg]] = args[i];
+ } else if (unaryArgs.hasOwnProperty(arg)) {
+ // unary argument
+ unaryArgs[arg] = true;
+ } else if (unaryArgs.hasOwnProperty(argsAlias[arg])) {
+ // unary argument alias
+ unaryArgs[argsAlias[arg]] = true;
+ }
+}
+urlCount = urls.length;
+
+// check for version
+if (unaryArgs.version) {
+ console.log('3.1.4');
+ phantom.exit();
+}
+
+// print usage
+if (len === 0 || urlCount === 0 || unaryArgs.help) {
+ console.log([
+ '',
+ ' Usage: phantomjs [phantomjs options] ' + phantom.scriptName + ' [yslow options] [url ...]',
+ '',
+ ' PhantomJS Options:',
+ '',
+ ' http://y.ahoo.it/phantomjs/options',
+ '',
+ ' YSlow Options:',
+ '',
+ ' -h, --help output usage information',
+ ' -V, --version output the version number',
+ ' -i, --info specify the information to display/log (basic|grade|stats|comps|all) [all]',
+ ' -f, --format specify the output results format (json|xml|plain|tap|junit) [json]',
+ ' -r, --ruleset specify the YSlow performance ruleset to be used (ydefault|yslow1|yblog) [ydefault]',
+ ' -b, --beacon specify an URL to log the results',
+ ' -d, --dict include dictionary of results fields',
+ ' -v, --verbose output beacon response information',
+ ' -t, --threshold for test formats, the threshold to test scores ([0-100]|[A-F]|{JSON}) [80]',
+ ' e.g.: -t B or -t 75 or -t \'{"overall": "B", "ycdn": "F", "yexpires": 85}\'',
+ ' -u, --ua "" specify the user agent string sent to server when the page requests resources',
+ ' -vp, --viewport specify page viewport size WxY, where W = width and H = height [400x300]',
+ ' -ch, --headers specify custom request headers, e.g.: -ch \'{"Cookie": "foo=bar"}\'',
+ ' -c, --console output page console messages (0: none, 1: message, 2: message + line + source) [0]',
+ '',
+ ' Examples:',
+ '',
+ ' phantomjs ' + phantom.scriptName + ' http://yslow.org',
+ ' phantomjs ' + phantom.scriptName + ' -i grade -f xml www.yahoo.com www.cnn.com www.nytimes.com',
+ ' phantomjs ' + phantom.scriptName + ' -info all --format plain --ua "MSIE 9.0" http://yslow.org',
+ ' phantomjs ' + phantom.scriptName + ' -i basic --rulseset yslow1 -d http://yslow.org',
+ ' phantomjs ' + phantom.scriptName + ' -i grade -b http://www.showslow.com/beacon/yslow/ -v yslow.org',
+ ' phantomjs --load-plugins=yes ' + phantom.scriptName + ' -vp 800x600 http://www.yahoo.com',
+ ' phantomjs ' + phantom.scriptName + ' -i grade -f tap -t 85 http://yslow.org',
+ ''
+ ].join('\n'));
+ phantom.exit();
+}
+
+// set yslow unary args
+yslowArgs.dict = unaryArgs.dict;
+yslowArgs.verbose = unaryArgs.verbose;
+
+// loop through urls
+urls.forEach(function (url) {
+ var page = webpage.create();
+
+ page.resources = {};
+
+ // allow x-domain requests, used to retrieve components content
+ page.settings.webSecurityEnabled = false;
+
+ // request
+ page.onResourceRequested = function (req) {
+ page.resources[req.url] = {
+ request: req
+ };
+ };
+
+ // response
+ page.onResourceReceived = function (res) {
+ var info,
+ resp = page.resources[res.url].response;
+
+ if (!resp) {
+ page.resources[res.url].response = res;
+ } else {
+ for (info in res) {
+ if (res.hasOwnProperty(info)) {
+ resp[info] = res[info];
+ }
+ }
+ }
+ };
+
+ // enable console output, useful for debugging
+ yslowArgs.console = parseInt(yslowArgs.console, 10) || 0;
+ if (yslowArgs.console) {
+ if (yslowArgs.console === 1) {
+ page.onConsoleMessage = function (msg) {
+ console.log(msg);
+ };
+ } else {
+ page.onConsoleMessage = function (msg, line, source) {
+ console.log(JSON.stringify({
+ message: msg,
+ lineNumber: line,
+ source: source
+ }, null, 4));
+ };
+ }
+ }
+
+ // set user agent string
+ if (yslowArgs.ua) {
+ page.settings.userAgent = yslowArgs.ua;
+ }
+
+ // set page viewport
+ if (yslowArgs.viewport) {
+ viewport = yslowArgs.viewport.toLowerCase();
+ page.viewportSize = {
+ width: parseInt(viewport.slice(0, viewport.indexOf('x')), 10) ||
+ page.viewportSize.width,
+ height: parseInt(viewport.slice(viewport.indexOf('x') + 1), 10) ||
+ page.viewportSize.height
+ };
+ }
+
+ // set custom headers
+ if (yslowArgs.headers) {
+ try {
+ page.customHeaders = JSON.parse(yslowArgs.headers);
+ } catch (err) {
+ console.log('Invalid custom headers: ' + err);
+ }
+ }
+
+ // open page
+ page.startTime = new Date();
+ page.open(url, function (status) {
+ var yslow, ysphantomjs, controller, evalFunc, loadTime, url, resp,
+ startTime = page.startTime,
+ resources = page.resources;
+
+ if (status !== 'success') {
+ console.log('FAIL to load ' + url);
+ } else {
+ // page load time
+ loadTime = new Date() - startTime;
+
+ // set resources response time
+ for (url in resources) {
+ if (resources.hasOwnProperty(url)) {
+ resp = resources[url].response;
+ if (resp) {
+ resp.time = new Date(resp.time) - startTime;
+ }
+ }
+ }
+
+ // yslow wrapper to be evaluated by page
+ yslow = function () {
+/**
+ * Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+ * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
+ */
+
+/*global YSLOW:true*/
+/*jslint white: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true */
+
+/**
+ * @module YSLOW
+ * @class YSLOW
+ * @static
+ */
+if (typeof YSLOW === 'undefined') {
+ YSLOW = {};
+}
+
+/**
+ * Enable/disable debbuging messages
+ */
+YSLOW.DEBUG = true;
+
+/**
+ *
+ * Adds a new rule to the pool of rules.
+ *
+ * Rule objects must implement the rule interface or an error will be thrown. The interface
+ * of a rule object is as follows:
+ *
+ *
id, e.g. "numreq"
+ *
name, e.g. "Minimize HTTP requests"
+ *
url, more info about the rule
+ *
config, configuration object with defaults
+ *
lint() a method that accepts a document, array of components and a config object and returns a reuslt object
+ *
+ *
+ * @param {YSLOW.Rule} rule A new rule object to add
+ */
+YSLOW.registerRule = function (rule) {
+ YSLOW.controller.addRule(rule);
+};
+
+/**
+ *
+ * Adds a new ruleset (new grading algorithm).
+ *
+ * Ruleset objects must implement the ruleset interface or an error will be thrown. The interface
+ * of a ruleset object is as follows:
+ *
+ *
id, e.g. "ydefault"
+ *
name, e.g. "Yahoo! Default"
+ *
rules a hash of ruleID => ruleconfig
+ *
weights a hash of ruleID => ruleweight
+ *
+ *
+ * @param {YSLOW.Ruleset} ruleset The new ruleset object to be registered
+ */
+YSLOW.registerRuleset = function (ruleset) {
+ YSLOW.controller.addRuleset(ruleset);
+};
+
+/**
+ * Register a renderer.
+ *
+ * Renderer objects must implement the renderer interface.
+ * The interface is as follows:
+ *
+ *
id
+ *
supports a hash of view_name => 1 or 0 to indicate what views are supported
+ *
and the methods
+ *
+ *
+ * For instance if you define a JSON renderer that only render grade. Your renderer object will look like this:
+ * { id: 'json',
+ * supports: { reportcard: 1, components: 0, stats: 0, cookies: 0},
+ * reportcardView: function(resultset) { ... }
+ * }
+ *
+ * Refer to YSLOW.HTMLRenderer for the function prototype.
+ *
+ *
+ * @param {YSLOW.renderer} renderer The new renderer object to be registered.
+ */
+YSLOW.registerRenderer = function (renderer) {
+ YSLOW.controller.addRenderer(renderer);
+};
+
+/**
+ * Adds a new tool.
+ *
+ * Tool objects must implement the tool interface or an error will be thrown.
+ * The interface of a tool object is as follows:
+ *
+ *
id, e.g. 'mytool'
+ *
name, eg. 'Custom tool #3'
+ *
print_output, whether this tool will produce output.
+ *
run, function that takes doc and componentset object, return content to be output
+ *
+ *
+ * @param {YSLOW.Tool} tool The new tool object to be registered
+ */
+YSLOW.registerTool = function (tool) {
+ YSLOW.Tools.addCustomTool(tool);
+};
+
+
+/**
+ * Register an event listener
+ *
+ * @param {String} event_name Name of the event
+ * @param {Function} callback A function to be called when the event fires
+ * @param {Object} that Object to be assigned to the "this" value of the callback function
+ */
+YSLOW.addEventListener = function (event_name, callback, that) {
+ YSLOW.util.event.addListener(event_name, callback, that);
+};
+
+/**
+ * Unregister an event listener.
+ *
+ * @param {String} event_name Name of the event
+ * @param {Function} callback The callback function that was added as a listener
+ * @return {Boolean} TRUE is the listener was removed successfully, FALSE otherwise (for example in cases when the listener doesn't exist)
+ */
+YSLOW.removeEventListener = function (event_name, callback) {
+ return YSLOW.util.event.removeListener(event_name, callback);
+};
+
+/**
+ * @namespace YSLOW
+ * @constructor
+ * @param {String} name Error type
+ * @param {String} message Error description
+ */
+YSLOW.Error = function (name, message) {
+ /**
+ * Type of error, e.g. "Interface error"
+ * @type String
+ */
+ this.name = name;
+ /**
+ * Error description
+ * @type String
+ */
+ this.message = message;
+};
+
+YSLOW.Error.prototype = {
+ toString: function () {
+ return this.name + "\n" + this.message;
+ }
+};
+/**
+ * Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+ * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
+ */
+
+YSLOW.version = '3.1.4';
+/**
+ * Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+ * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
+ */
+
+/*global YSLOW,MutationEvent*/
+/*jslint browser: true, continue: true, sloppy: true, maxerr: 50, indent: 4 */
+
+/**
+ * ComponentSet holds an array of all the components and get the response info from net module for each component.
+ *
+ * @constructor
+ * @param {DOMElement} node DOM Element
+ * @param {Number} onloadTimestamp onload timestamp
+ */
+YSLOW.ComponentSet = function (node, onloadTimestamp) {
+
+ //
+ // properties
+ //
+ this.root_node = node;
+ this.components = [];
+ this.outstanding_net_request = 0;
+ this.component_info = [];
+ this.onloadTimestamp = onloadTimestamp;
+ this.nextID = 1;
+ this.notified_fetch_done = false;
+
+};
+
+YSLOW.ComponentSet.prototype = {
+
+ /**
+ * Call this function when you don't use the component set any more.
+ * A chance to do proper clean up, e.g. remove event listener.
+ */
+ clear: function () {
+ this.components = [];
+ this.component_info = [];
+ this.cleared = true;
+ if (this.outstanding_net_request > 0) {
+ YSLOW.util.dump("YSLOW.ComponentSet.Clearing component set before all net requests finish.");
+ }
+ },
+
+ /**
+ * Add a new component to the set.
+ * @param {String} url URL of component
+ * @param {String} type type of component
+ * @param {String} base_href base href of document that the component belongs.
+ * @param {Object} obj DOMElement (for image type only)
+ * @return Component object that was added to ComponentSet
+ * @type ComponentObject
+ */
+ addComponent: function (url, type, base_href, o) {
+ var comp, found, isDoc;
+
+ if (!url) {
+ if (!this.empty_url) {
+ this.empty_url = [];
+ }
+ this.empty_url[type] = (this.empty_url[type] || 0) + 1;
+ }
+ if (url && type) {
+ // check if url is valid.
+ if (!YSLOW.ComponentSet.isValidProtocol(url) ||
+ !YSLOW.ComponentSet.isValidURL(url)) {
+ return comp;
+ }
+
+ // Make sure url is absolute url.
+ url = YSLOW.util.makeAbsoluteUrl(url, base_href);
+ // For security purpose
+ url = YSLOW.util.escapeHtml(url);
+
+ found = typeof this.component_info[url] !== 'undefined';
+ isDoc = type === 'doc';
+
+ // make sure this component is not already in this component set,
+ // but also check if a doc is coming after a redirect using same url
+ if (!found || isDoc) {
+ this.component_info[url] = {
+ 'state': 'NONE',
+ 'count': found ? this.component_info[url].count : 0
+ };
+
+ comp = new YSLOW.Component(url, type, this, o);
+ if (comp) {
+ comp.id = this.nextID += 1;
+ this.components[this.components.length] = comp;
+
+ // shortcup for document component
+ if (!this.doc_comp && isDoc) {
+ this.doc_comp = comp;
+ }
+
+ if (this.component_info[url].state === 'NONE') {
+ // net.js has probably made an async request.
+ this.component_info[url].state = 'REQUESTED';
+ this.outstanding_net_request += 1;
+ }
+ } else {
+ this.component_info[url].state = 'ERROR';
+ YSLOW.util.event.fire("componentFetchError");
+ }
+ }
+ this.component_info[url].count += 1;
+ }
+
+ return comp;
+ },
+
+ /**
+ * Add a new component to the set, ignore duplicate.
+ * @param {String} url url of component
+ * @param {String} type type of component
+ * @param {String} base_href base href of document that the component belongs.
+ */
+ addComponentNoDuplicate: function (url, type, base_href) {
+
+ if (url && type) {
+ // For security purpose
+ url = YSLOW.util.escapeHtml(url);
+ url = YSLOW.util.makeAbsoluteUrl(url, base_href);
+ if (this.component_info[url] === undefined) {
+ return this.addComponent(url, type, base_href);
+ }
+ }
+
+ },
+
+ /**
+ * Get components by type.
+ *
+ * @param {String|Array} type The type of component to get, e.g. "js" or
+ * ['js', 'css']
+ * @param {Boolean} include_after_onload If component loaded after onload
+ * should be included in the returned results, default is FALSE,
+ * don't include
+ * @param {Boolean} include_beacons If image beacons (1x1 images) should
+ * be included in the returned results, default is FALSE, don't
+ * include
+ * @return An array of matching components
+ * @type Array
+ */
+ getComponentsByType: function (type, includeAfterOnload, includeBeacons) {
+ var i, j, len, lenJ, t, comp, info,
+ components = this.components,
+ compInfo = this.component_info,
+ comps = [],
+ types = {};
+
+ if (typeof includeAfterOnload === 'undefined') {
+ includeAfterOnload = !(YSLOW.util.Preference.getPref(
+ 'excludeAfterOnload',
+ true
+ ));
+ }
+ if (typeof includeBeacons === 'undefined') {
+ includeBeacons = !(YSLOW.util.Preference.getPref(
+ 'excludeBeaconsFromLint',
+ true
+ ));
+ }
+
+ if (typeof type === 'string') {
+ types[type] = 1;
+ } else {
+ for (i = 0, len = type.length; i < len; i += 1) {
+ t = type[i];
+ if (t) {
+ types[t] = 1;
+ }
+ }
+ }
+
+ for (i = 0, len = components.length; i < len; i += 1) {
+ comp = components[i];
+ if (!comp || (comp && !types[comp.type]) ||
+ (comp.is_beacon && !includeBeacons) ||
+ (comp.after_onload && !includeAfterOnload)) {
+ continue;
+ }
+ comps[comps.length] = comp;
+ info = compInfo[i];
+ if (!info || (info && info.count <= 1)) {
+ continue;
+ }
+ for (j = 1, lenJ = info.count; j < lenJ; j += 1) {
+ comps[comps.length] = comp;
+ }
+ }
+
+ return comps;
+ },
+
+ /**
+ * @private
+ * Get fetching progress.
+ * @return { 'total' => total number of component, 'received' => number of components fetched }
+ */
+ getProgress: function () {
+ var i,
+ total = 0,
+ received = 0;
+
+ for (i in this.component_info) {
+ if (this.component_info.hasOwnProperty(i) &&
+ this.component_info[i]) {
+ if (this.component_info[i].state === 'RECEIVED') {
+ received += 1;
+ }
+ total += 1;
+ }
+ }
+
+ return {
+ 'total': total,
+ 'received': received
+ };
+ },
+
+ /**
+ * Event callback when component's GetInfoState changes.
+ * @param {Object} event object
+ */
+ onComponentGetInfoStateChange: function (event_object) {
+ var comp, state, progress;
+
+ if (event_object) {
+ if (typeof event_object.comp !== 'undefined') {
+ comp = event_object.comp;
+ }
+ if (typeof event_object.state !== 'undefined') {
+ state = event_object.state;
+ }
+ }
+ if (typeof this.component_info[comp.url] === 'undefined') {
+ // this should not happen.
+ YSLOW.util.dump("YSLOW.ComponentSet.onComponentGetInfoStateChange(): Unexpected component: " + comp.url);
+ return;
+ }
+
+ if (this.component_info[comp.url].state === "NONE" && state === 'DONE') {
+ this.component_info[comp.url].state = "RECEIVED";
+ } else if (this.component_info[comp.url].state === "REQUESTED" && state === 'DONE') {
+ this.component_info[comp.url].state = "RECEIVED";
+ this.outstanding_net_request -= 1;
+ // Got all component detail info.
+ if (this.outstanding_net_request === 0) {
+ this.notified_fetch_done = true;
+ YSLOW.util.event.fire("componentFetchDone", {
+ 'component_set': this
+ });
+ }
+ } else {
+ // how does this happen?
+ YSLOW.util.dump("Unexpected component info state: [" + comp.type + "]" + comp.url + "state: " + state + " comp_info_state: " + this.component_info[comp.url].state);
+ }
+
+ // fire event.
+ progress = this.getProgress();
+ YSLOW.util.event.fire("componentFetchProgress", {
+ 'total': progress.total,
+ 'current': progress.received,
+ 'last_component_url': comp.url
+ });
+ },
+
+ /**
+ * This is called when peeler is done.
+ * If ComponentSet has all the component info, fire componentFetchDone event.
+ */
+ notifyPeelDone: function () {
+ if (this.outstanding_net_request === 0 && !this.notified_fetch_done) {
+ this.notified_fetch_done = true;
+ YSLOW.util.event.fire("componentFetchDone", {
+ 'component_set': this
+ });
+ }
+ },
+
+ /**
+ * After onload guess (simple version)
+ * Checkes for elements with src or href attributes within
+ * the original document html source
+ */
+ setSimpleAfterOnload: function (callback, obj) {
+ var i, j, comp, doc_el, doc_comps, src,
+ indoc, url, el, type, len, lenJ,
+ docBody, doc, components, that;
+
+ if (obj) {
+ docBody = obj.docBody;
+ doc = obj.doc;
+ components = obj.components;
+ that = obj.components;
+ } else {
+ docBody = this.doc_comp && this.doc_comp.body;
+ doc = this.root_node;
+ components = this.components;
+ that = this;
+ }
+
+ // skip testing when doc not found
+ if (!docBody) {
+ YSLOW.util.dump('doc body is empty');
+ return callback(that);
+ }
+
+ doc_el = doc.createElement('div');
+ doc_el.innerHTML = docBody;
+ doc_comps = doc_el.getElementsByTagName('*');
+
+ for (i = 0, len = components.length; i < len; i += 1) {
+ comp = components[i];
+ type = comp.type;
+ if (type === 'cssimage' || type === 'doc') {
+ // docs are ignored
+ // css images are likely to be loaded before onload
+ continue;
+ }
+ indoc = false;
+ url = comp.url;
+ for (j = 0, lenJ = doc_comps.length; !indoc && j < lenJ; j += 1) {
+ el = doc_comps[j];
+ src = el.src || el.href || el.getAttribute('src') ||
+ el.getAttribute('href') ||
+ (el.nodeName === 'PARAM' && el.value);
+ indoc = (src === url);
+ }
+ // if component wasn't found on original html doc
+ // assume it was loaded after onload
+ comp.after_onload = !indoc;
+ }
+
+ callback(that);
+ },
+
+ /**
+ * After onload guess
+ * Checkes for inserted elements with src or href attributes after the
+ * page onload event triggers using an iframe with original doc html
+ */
+ setAfterOnload: function (callback, obj) {
+ var ifrm, idoc, iwin, timer, done, noOnloadTimer,
+ that, docBody, doc, components, ret, enough, triggered,
+ util = YSLOW.util,
+ addEventListener = util.addEventListener,
+ removeEventListener = util.removeEventListener,
+ setTimer = setTimeout,
+ clearTimer = clearTimeout,
+ comps = [],
+ compsHT = {},
+
+ // get changed component and push to comps array
+ // reset timer for 1s after the last dom change
+ getTarget = function (e) {
+ var type, attr, target, src, oldSrc;
+
+ clearTimer(timer);
+
+ type = e.type;
+ attr = e.attrName;
+ target = e.target;
+ src = target.src || target.href || (target.getAttribute && (
+ target.getAttribute('src') || target.getAttribute('href')
+ ));
+ oldSrc = target.dataOldSrc;
+
+ if (src &&
+ (type === 'DOMNodeInserted' ||
+ (type === 'DOMSubtreeModified' && src !== oldSrc) ||
+ (type === 'DOMAttrModified' &&
+ (attr === 'src' || attr === 'href'))) &&
+ !compsHT[src]) {
+ compsHT[src] = 1;
+ comps.push(target);
+ }
+
+ timer = setTimer(done, 1000);
+ },
+
+ // temp iframe onload listener
+ // - cancel noOnload timer since onload was fired
+ // - wait 3s before calling done if no dom mutation happens
+ // - set enough timer, limit is 10 seconds for mutations, this is
+ // for edge cases when page inserts/removes nodes within a loop
+ iframeOnload = function () {
+ var i, len, all, el, src;
+
+ clearTimer(noOnloadTimer);
+ all = idoc.getElementsByTagName('*');
+ for (i = 0, len = all.length; i < len; i += 1) {
+ el = all[i];
+ src = el.src || el.href;
+ if (src) {
+ el.dataOldSrc = src;
+ }
+ }
+ addEventListener(iwin, 'DOMSubtreeModified', getTarget);
+ addEventListener(iwin, 'DOMNodeInserted', getTarget);
+ addEventListener(iwin, 'DOMAttrModified', getTarget);
+ timer = setTimer(done, 3000);
+ enough = setTimer(done, 10000);
+ };
+
+ if (obj) {
+ that = YSLOW.ComponentSet.prototype;
+ docBody = obj.docBody;
+ doc = obj.doc;
+ components = obj.components;
+ ret = components;
+ } else {
+ that = this;
+ docBody = that.doc_comp && that.doc_comp.body;
+ doc = that.root_node;
+ components = that.components;
+ ret = that;
+ }
+
+ // check for mutation event support or anti-iframe option
+ if (typeof MutationEvent === 'undefined' || YSLOW.antiIframe) {
+ return that.setSimpleAfterOnload(callback, obj);
+ }
+
+ // skip testing when doc not found
+ if (!docBody) {
+ util.dump('doc body is empty');
+
+ return callback(ret);
+ }
+
+ // set afteronload properties for all components loaded after window onlod
+ done = function () {
+ var i, j, len, lenJ, comp, src, cmp;
+
+ // to avoid executing this function twice
+ // due to ifrm iwin double listeners
+ if (triggered) {
+ return;
+ }
+
+ // cancel timers
+ clearTimer(enough);
+ clearTimer(timer);
+
+ // remove listeners
+ removeEventListener(iwin, 'DOMSubtreeModified', getTarget);
+ removeEventListener(iwin, 'DOMNodeInserted', getTarget);
+ removeEventListener(iwin, 'DOMAttrModified', getTarget);
+ removeEventListener(ifrm, 'load', iframeOnload);
+ removeEventListener(iwin, 'load', iframeOnload);
+
+ // changed components loop
+ for (i = 0, len = comps.length; i < len; i += 1) {
+ comp = comps[i];
+ src = comp.src || comp.href || (comp.getAttribute &&
+ (comp.getAttribute('src') || comp.getAttribute('href')));
+ if (!src) {
+ continue;
+ }
+ for (j = 0, lenJ = components.length; j < lenJ; j += 1) {
+ cmp = components[j];
+ if (cmp.url === src) {
+ cmp.after_onload = true;
+ }
+ }
+ }
+
+ // remove temp iframe and invoke callback passing cset
+ ifrm.parentNode.removeChild(ifrm);
+ triggered = 1;
+ callback(ret);
+ };
+
+ // create temp iframe with doc html
+ ifrm = doc.createElement('iframe');
+ ifrm.style.cssText = 'position:absolute;top:-999em;';
+ doc.body.appendChild(ifrm);
+ iwin = ifrm.contentWindow;
+
+ // set a fallback when onload is not triggered
+ noOnloadTimer = setTimer(done, 3000);
+
+ // set onload and ifram content
+ if (iwin) {
+ idoc = iwin.document;
+ } else {
+ iwin = idoc = ifrm.contentDocument;
+ }
+ addEventListener(iwin, 'load', iframeOnload);
+ addEventListener(ifrm, 'load', iframeOnload);
+ idoc.open().write(docBody);
+ idoc.close();
+ addEventListener(iwin, 'load', iframeOnload);
+ }
+};
+
+/*
+ * List of protocols to ignore in component set.
+ */
+YSLOW.ComponentSet.ignoreProtocols = ['data', 'chrome', 'javascript', 'about',
+ 'resource', 'jar', 'chrome-extension', 'file'];
+
+/**
+ * @private
+ * Check if url has an allowed protocol (no chrome://, about:)
+ * @param url
+ * @return false if url does not contain hostname.
+ */
+YSLOW.ComponentSet.isValidProtocol = function (s) {
+ var i, index, protocol,
+ ignoreProtocols = this.ignoreProtocols,
+ len = ignoreProtocols.length;
+
+ s = s.toLowerCase();
+ index = s.indexOf(':');
+ if (index > 0) {
+ protocol = s.substr(0, index);
+ for (i = 0; i < len; i += 1) {
+ if (protocol === ignoreProtocols[i]) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+};
+
+
+/**
+ * @private
+ * Check if passed url has hostname specified.
+ * @param url
+ * @return false if url does not contain hostname.
+ */
+YSLOW.ComponentSet.isValidURL = function (url) {
+ var arr, host;
+
+ url = url.toLowerCase();
+
+ // all url is in the format of :
+ arr = url.split(":");
+
+ // for http protocol, we want to make sure there is a host in the url.
+ if (arr[0] === "http" || arr[0] === "https") {
+ if (arr[1].substr(0, 2) !== "//") {
+ return false;
+ }
+ host = arr[1].substr(2);
+ if (host.length === 0 || host.indexOf("/") === 0) {
+ // no host specified.
+ return false;
+ }
+ }
+
+ return true;
+};
+/**
+ * Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+ * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
+ */
+
+/*global YSLOW*/
+/*jslint white: true, onevar: true, undef: true, newcap: true, nomen: true, plusplus: true, bitwise: true, browser: true, maxerr: 50, indent: 4 */
+
+/**
+ * @namespace YSLOW
+ * @class Component
+ * @constructor
+ */
+YSLOW.Component = function (url, type, parent_set, o) {
+ var obj = o && o.obj,
+ comp = (o && o.comp) || {};
+
+ /**
+ * URL of the component
+ * @type String
+ */
+ this.url = url;
+
+ /**
+ * Component type, one of the following:
+ *
+ *
doc
+ *
js
+ *
css
+ *
...
+ *
+ * @type String
+ */
+ this.type = type;
+
+ /**
+ * Parent component set.
+ */
+ this.parent = parent_set;
+
+ this.headers = {};
+ this.raw_headers = '';
+ this.req_headers = null;
+ this.body = '';
+ this.compressed = false;
+ this.expires = undefined; // to be replaced by a Date object
+ this.size = 0;
+ this.status = 0;
+ this.is_beacon = false;
+ this.method = 'unknown';
+ this.cookie = '';
+ this.respTime = null;
+ this.after_onload = false;
+
+ // component object properties
+ // e.g. for image, image element width, image element height, actual width, actual height
+ this.object_prop = undefined;
+
+ // construction part
+ if (type === undefined) {
+ this.type = 'unknown';
+ }
+
+ this.get_info_state = 'NONE';
+
+ if (obj && type === 'image' && obj.width && obj.height) {
+ this.object_prop = {
+ 'width': obj.width,
+ 'height': obj.height
+ };
+ }
+
+ if (comp.containerNode) {
+ this.containerNode = comp.containerNode;
+ }
+
+ this.setComponentDetails(o);
+};
+
+/**
+ * Return the state of getting detail info from the net.
+ */
+YSLOW.Component.prototype.getInfoState = function () {
+ return this.get_info_state;
+};
+
+YSLOW.Component.prototype.populateProperties = function (resolveRedirect, ignoreImgReq) {
+ var comp, encoding, expires, content_length, img_src, obj, dataUri,
+ that = this,
+ NULL = null,
+ UNDEF = 'undefined';
+
+ // check location
+ // bookmarklet and har already handle redirects
+ if (that.headers.location && resolveRedirect) {
+ // Add a new component.
+ comp = that.parent.addComponentNoDuplicate(that.headers.location,
+ (that.type !== 'redirect' ? that.type : 'unknown'), that.url);
+ if (comp && that.after_onload) {
+ comp.after_onload = true;
+ }
+ that.type = 'redirect';
+ }
+
+ content_length = that.headers['content-length'];
+
+ // gzip, deflate
+ encoding = YSLOW.util.trim(that.headers['content-encoding']);
+ if (encoding === 'gzip' || encoding === 'deflate') {
+ that.compressed = encoding;
+ that.size = (that.body.length) ? that.body.length : NULL;
+ if (content_length) {
+ that.size_compressed = parseInt(content_length, 10) ||
+ content_length;
+ } else if (typeof that.nsize !== UNDEF) {
+ that.size_compressed = that.nsize;
+ } else {
+ // a hack
+ that.size_compressed = Math.round(that.size / 3);
+ }
+ } else {
+ that.compressed = false;
+ that.size_compressed = NULL;
+ if (content_length) {
+ that.size = parseInt(content_length, 10);
+ } else if (typeof that.nsize !== UNDEF) {
+ that.size = parseInt(that.nsize, 10);
+ } else {
+ that.size = that.body.length;
+ }
+ }
+
+ // size check/correction, @todo be more precise here
+ if (!that.size) {
+ if (typeof that.nsize !== UNDEF) {
+ that.size = that.nsize;
+ } else {
+ that.size = that.body.length;
+ }
+ }
+ that.uncompressed_size = that.body.length;
+
+ // expiration based on either Expires or Cache-Control headers
+ expires = that.headers.expires;
+ if (expires && expires.length > 0) {
+ // set expires as a JS object
+ that.expires = new Date(expires);
+ if (that.expires.toString() === 'Invalid Date') {
+ that.expires = that.getMaxAge();
+ }
+ } else {
+ that.expires = that.getMaxAge();
+ }
+
+ // compare image original dimensions with actual dimensions, data uri is
+ // first attempted to get the orginal dimension, if it fails (btoa) then
+ // another request to the orginal image is made
+ if (that.type === 'image' && !ignoreImgReq) {
+ if (typeof Image !== UNDEF) {
+ obj = new Image();
+ } else {
+ obj = document.createElement('img');
+ }
+ if (that.body.length) {
+ img_src = 'data:' + that.headers['content-type'] + ';base64,' +
+ YSLOW.util.base64Encode(that.body);
+ dataUri = 1;
+ } else {
+ img_src = that.url;
+ }
+ obj.onerror = function () {
+ obj.onerror = NULL;
+ if (dataUri) {
+ obj.src = that.url;
+ }
+ };
+ obj.onload = function () {
+ obj.onload = NULL;
+ if (obj && obj.width && obj.height) {
+ if (that.object_prop) {
+ that.object_prop.actual_width = obj.width;
+ that.object_prop.actual_height = obj.height;
+ } else {
+ that.object_prop = {
+ 'width': obj.width,
+ 'height': obj.height,
+ 'actual_width': obj.width,
+ 'actual_height': obj.height
+ };
+ }
+ if (obj.width < 2 && obj.height < 2) {
+ that.is_beacon = true;
+ }
+ }
+ };
+ obj.src = img_src;
+ }
+};
+
+/**
+ * Return true if this object has a last-modified date significantly in the past.
+ */
+YSLOW.Component.prototype.hasOldModifiedDate = function () {
+ var now = Number(new Date()),
+ modified_date = this.headers['last-modified'];
+
+ if (typeof modified_date !== 'undefined') {
+ // at least 1 day in the past
+ return ((now - Number(new Date(modified_date))) > (24 * 60 * 60 * 1000));
+ }
+
+ return false;
+};
+
+/**
+ * Return true if this object has a far future Expires.
+ * @todo: make the "far" interval configurable
+ * @param expires Date object
+ * @return true if this object has a far future Expires.
+ */
+YSLOW.Component.prototype.hasFarFutureExpiresOrMaxAge = function () {
+ var expires_in_seconds,
+ now = Number(new Date()),
+ minSeconds = YSLOW.util.Preference.getPref('minFutureExpiresSeconds', 2 * 24 * 60 * 60),
+ minMilliSeconds = minSeconds * 1000;
+
+ if (typeof this.expires === 'object') {
+ expires_in_seconds = Number(this.expires);
+ if ((expires_in_seconds - now) > minMilliSeconds) {
+ return true;
+ }
+ }
+
+ return false;
+};
+
+YSLOW.Component.prototype.getEtag = function () {
+ return this.headers.etag || '';
+};
+
+YSLOW.Component.prototype.getMaxAge = function () {
+ var index, maxage, expires,
+ cache_control = this.headers['cache-control'];
+
+ if (cache_control) {
+ index = cache_control.indexOf('max-age');
+ if (index > -1) {
+ maxage = parseInt(cache_control.substring(index + 8), 10);
+ if (maxage > 0) {
+ expires = YSLOW.util.maxAgeToDate(maxage);
+ }
+ }
+ }
+
+ return expires;
+};
+
+/**
+ * Return total size of Set-Cookie headers of this component.
+ * @return total size of Set-Cookie headers of this component.
+ * @type Number
+ */
+YSLOW.Component.prototype.getSetCookieSize = function () {
+ // only return total size of cookie received.
+ var aCookies, k,
+ size = 0;
+
+ if (this.headers && this.headers['set-cookie']) {
+ aCookies = this.headers['set-cookie'].split('\n');
+ if (aCookies.length > 0) {
+ for (k = 0; k < aCookies.length; k += 1) {
+ size += aCookies[k].length;
+ }
+ }
+ }
+
+ return size;
+};
+
+/**
+ * Return total size of Cookie HTTP Request headers of this component.
+ * @return total size of Cookie headers Request of this component.
+ * @type Number
+ */
+YSLOW.Component.prototype.getReceivedCookieSize = function () {
+ // only return total size of cookie sent.
+ var aCookies, k,
+ size = 0;
+
+ if (this.cookie && this.cookie.length > 0) {
+ aCookies = this.cookie.split('\n');
+ if (aCookies.length > 0) {
+ for (k = 0; k < aCookies.length; k += 1) {
+ size += aCookies[k].length;
+ }
+ }
+ }
+
+ return size;
+};
+
+/**
+ * Platform implementation of
+ * YSLOW.Component.prototype.setComponentDetails = function (o) {}
+ * goes here
+/*
+/**
+ * Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+ * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
+ */
+
+/*global YSLOW*/
+/*jslint browser: true, sloppy: true*/
+
+/**
+ * Parse details (HTTP headers, content, etc) from a
+ * given source and set component properties.
+ * @param o The object containing component details.
+ */
+YSLOW.Component.prototype.setComponentDetails = function (o) {
+ var comp = this,
+
+ parse = function (request, response) {
+ var xhr;
+
+ // copy from the response object
+ comp.status = response.status;
+ comp.headers = {};
+ comp.raw_headers = '';
+ response.headers.forEach(function (header) {
+ comp.headers[header.name.toLowerCase()] = header.value;
+ comp.raw_headers += header.name + ': ' + header.value + '\n';
+ });
+ comp.req_headers = {};
+ request.headers.forEach(function (header) {
+ comp.req_headers[header.name.toLowerCase()] = header.value;
+ });
+ comp.method = request.method;
+ if (response.contentText) {
+ comp.body = response.contentText;
+ } else {
+ // try to fetch component again using sync xhr while
+ // content is not available through phantomjs.
+ // see: http://code.google.com/p/phantomjs/issues/detail?id=158
+ // and http://code.google.com/p/phantomjs/issues/detail?id=156
+ try {
+ xhr = new XMLHttpRequest();
+ xhr.open('GET', comp.url, false);
+ xhr.send();
+ comp.body = xhr.responseText;
+ } catch (err) {
+ comp.body = {
+ toString: function () {
+ return '';
+ },
+ length: response.bodySize || 0
+ };
+ }
+ }
+ // for security checking
+ comp.response_type = comp.type;
+ comp.cookie = (comp.headers['set-cookie'] || '') +
+ (comp.req_headers.cookie || '');
+ comp.nsize = parseInt(comp.headers['content-length'], 10) ||
+ response.bodySize;
+ comp.respTime = response.time;
+ comp.after_onload = (new Date(request.time)
+ .getTime()) > comp.parent.onloadTimestamp;
+
+ // populate properties ignoring redirect
+ // resolution and image request
+ comp.populateProperties(false, true);
+
+ comp.get_info_state = 'DONE';
+
+ // notify parent ComponentSet that this component has gotten net response.
+ comp.parent.onComponentGetInfoStateChange({
+ 'comp': comp,
+ 'state': 'DONE'
+ });
+ };
+
+ if (o.request && o.response) {
+ parse(o.request, o.response);
+ }
+};
+/**
+ * Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+ * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
+ */
+
+/*global YSLOW*/
+/*jslint white: true, browser: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true */
+
+/**
+ * @namespace YSLOW
+ * @class controller
+ * @static
+ */
+
+YSLOW.controller = {
+
+ rules: {},
+
+ rulesets: {},
+
+ onloadTimestamp: null,
+
+ renderers: {},
+
+ default_ruleset_id: 'ydefault',
+
+ run_pending: 0,
+
+ /**
+ * Init code. Add event listeners.
+ */
+ init: function () {
+ var arr_rulesets, i, obj, value;
+
+ // listen to onload event.
+ YSLOW.util.event.addListener("onload", function (e) {
+ this.onloadTimestamp = e.time;
+ YSLOW.util.setTimer(function () {
+ YSLOW.controller.run_pending_event();
+ });
+ }, this);
+
+ // listen to onunload event.
+ YSLOW.util.event.addListener("onUnload", function (e) {
+ this.run_pending = 0;
+ this.onloadTimestamp = null;
+ }, this);
+
+ // load custom ruleset
+ arr_rulesets = YSLOW.util.Preference.getPrefList("customRuleset.", undefined);
+ if (arr_rulesets && arr_rulesets.length > 0) {
+ for (i = 0; i < arr_rulesets.length; i += 1) {
+ value = arr_rulesets[i].value;
+ if (typeof value === "string" && value.length > 0) {
+ obj = JSON.parse(value, null);
+ obj.custom = true;
+ this.addRuleset(obj);
+ }
+ }
+ }
+
+ this.default_ruleset_id = YSLOW.util.Preference.getPref("defaultRuleset", 'ydefault');
+
+ // load rule config preference
+ this.loadRulePreference();
+ },
+
+ /**
+ * Run controller to start peeler. Don't start if the page is not done loading.
+ * Delay the running until onload event.
+ *
+ * @param {Window} win window object
+ * @param {YSLOW.context} yscontext YSlow context to use.
+ * @param {Boolean} autorun value to indicate if triggered by autorun
+ */
+ run: function (win, yscontext, autorun) {
+ var cset, line,
+ doc = win.document;
+
+ if (!doc || !doc.location || doc.location.href.indexOf("about:") === 0 || "undefined" === typeof doc.location.hostname) {
+ if (!autorun) {
+ line = 'Please enter a valid website address before running YSlow.';
+ YSLOW.ysview.openDialog(YSLOW.ysview.panel_doc, 389, 150, line, '', 'Ok');
+ }
+ return;
+ }
+
+ // Since firebug 1.4, onload event is not passed to YSlow if firebug
+ // panel is not opened. Recommendation from firebug dev team is to
+ // refresh the page before running yslow, which is unnecessary from
+ // yslow point of view. For now, just don't enforce running YSlow
+ // on a page has finished loading.
+ if (!yscontext.PAGE.loaded) {
+ this.run_pending = {
+ 'win': win,
+ 'yscontext': yscontext
+ };
+ // @todo: put up spining logo to indicate waiting for page finish loading.
+ return;
+ }
+
+ YSLOW.util.event.fire("peelStart", undefined);
+ cset = YSLOW.peeler.peel(doc, this.onloadTimestamp);
+ // need to set yscontext_component_set before firing peelComplete,
+ // otherwise, may run into infinite loop.
+ yscontext.component_set = cset;
+ YSLOW.util.event.fire("peelComplete", {
+ 'component_set': cset
+ });
+
+ // notify ComponentSet peeling is done.
+ cset.notifyPeelDone();
+ },
+
+ /**
+ * Start pending run function.
+ */
+ run_pending_event: function () {
+ if (this.run_pending) {
+ this.run(this.run_pending.win, this.run_pending.yscontext, false);
+ this.run_pending = 0;
+ }
+ },
+
+ /**
+ * Run lint function of the ruleset matches the passed rulset_id.
+ * If ruleset_id is undefined, use Controller's default ruleset.
+ * @param {Document} doc Document object of the page to run lint.
+ * @param {YSLOW.context} yscontext YSlow context to use.
+ * @param {String} ruleset_id ID of the ruleset to run.
+ * @return Lint result
+ * @type YSLOW.ResultSet
+ */
+ lint: function (doc, yscontext, ruleset_id) {
+ var rule, rules, i, conf, result, weight, score,
+ ruleset = [],
+ results = [],
+ total_score = 0,
+ total_weight = 0,
+ that = this,
+ rs = that.rulesets,
+ defaultRuleSetId = that.default_ruleset_id;
+
+ if (ruleset_id) {
+ ruleset = rs[ruleset_id];
+ } else if (defaultRuleSetId && rs[defaultRuleSetId]) {
+ ruleset = rs[defaultRuleSetId];
+ } else {
+ // if no ruleset, take the first one available
+ for (i in rs) {
+ if (rs.hasOwnProperty(i) && rs[i]) {
+ ruleset = rs[i];
+ break;
+ }
+ }
+ }
+
+ rules = ruleset.rules;
+ for (i in rules) {
+ if (rules.hasOwnProperty(i) && rules[i] &&
+ this.rules.hasOwnProperty(i)) {
+ try {
+ rule = this.rules[i];
+ conf = YSLOW.util.merge(rule.config, rules[i]);
+
+ result = rule.lint(doc, yscontext.component_set, conf);
+
+ // apply rule weight to result.
+ weight = (ruleset.weights ? ruleset.weights[i] : undefined);
+ if (weight !== undefined) {
+ weight = parseInt(weight, 10);
+ }
+ if (weight === undefined || weight < 0 || weight > 100) {
+ if (rs.ydefault.weights[i]) {
+ weight = rs.ydefault.weights[i];
+ } else {
+ weight = 5;
+ }
+ }
+ result.weight = weight;
+
+ if (result.score !== undefined) {
+ if (typeof result.score !== "number") {
+ score = parseInt(result.score, 10);
+ if (!isNaN(score)) {
+ result.score = score;
+ }
+ }
+
+ if (typeof result.score === 'number') {
+ total_weight += result.weight;
+
+ if (!YSLOW.util.Preference.getPref('allowNegativeScore', false)) {
+ if (result.score < 0) {
+ result.score = 0;
+ }
+ if (typeof result.score !== 'number') {
+ // for backward compatibilty of n/a
+ result.score = -1;
+ }
+ }
+
+ if (result.score !== 0) {
+ total_score += result.score * (typeof result.weight !== 'undefined' ? result.weight : 1);
+ }
+ }
+ }
+
+ result.name = rule.name;
+ result.category = rule.category;
+ result.rule_id = i;
+
+ results[results.length] = result;
+ } catch (err) {
+ YSLOW.util.dump("YSLOW.controller.lint: " + i, err);
+ YSLOW.util.event.fire("lintError", {
+ 'rule': i,
+ 'message': err
+ });
+ }
+ }
+ }
+
+ yscontext.PAGE.overallScore = total_score / (total_weight > 0 ? total_weight : 1);
+ yscontext.result_set = new YSLOW.ResultSet(results, yscontext.PAGE.overallScore, ruleset);
+ yscontext.result_set.url = yscontext.component_set.doc_comp.url;
+ YSLOW.util.event.fire("lintResultReady", {
+ 'yslowContext': yscontext
+ });
+
+ return yscontext.result_set;
+ },
+
+ /**
+ * Run tool that matches the passed tool_id
+ * @param {String} tool_id ID of the tool to be run.
+ * @param {YSLOW.context} yscontext YSlow context
+ * @param {Object} param parameters to be passed to run method of tool.
+ */
+ runTool: function (tool_id, yscontext, param) {
+ var result, html, doc, h, css, uri, req2, l, s, message, body,
+ tool = YSLOW.Tools.getTool(tool_id);
+
+ try {
+ if (typeof tool === "object") {
+ result = tool.run(yscontext.document, yscontext.component_set, param);
+ if (tool.print_output) {
+ html = '';
+ if (typeof result === "object") {
+ html = result.html;
+ } else if (typeof result === "string") {
+ html = result;
+ }
+ doc = YSLOW.util.getNewDoc();
+ body = doc.body || doc.documentElement;
+ body.innerHTML = html;
+ h = doc.getElementsByTagName('head')[0];
+ if (typeof result.css === "undefined") {
+ // use default.
+ uri = 'chrome://yslow/content/yslow/tool.css';
+ req2 = new XMLHttpRequest();
+ req2.open('GET', uri, false);
+ req2.send(null);
+ css = req2.responseText;
+ } else {
+ css = result.css;
+ }
+ if (typeof css === "string") {
+ l = doc.createElement("style");
+ l.setAttribute("type", "text/css");
+ l.appendChild(doc.createTextNode(css));
+ h.appendChild(l);
+ }
+
+ if (typeof result.js !== "undefined") {
+ s = doc.createElement("script");
+ s.setAttribute("type", "text/javascript");
+ s.appendChild(doc.createTextNode(result.js));
+ h.appendChild(s);
+ }
+ if (typeof result.plot_component !== "undefined" && result.plot_component === true) {
+ // plot components
+ YSLOW.renderer.plotComponents(doc, yscontext);
+ }
+ }
+ } else {
+ message = tool_id + " is not a tool.";
+ YSLOW.util.dump(message);
+ YSLOW.util.event.fire("toolError", {
+ 'tool_id': tool_id,
+ 'message': message
+ });
+ }
+ } catch (err) {
+ YSLOW.util.dump("YSLOW.controller.runTool: " + tool_id, err);
+ YSLOW.util.event.fire("toolError", {
+ 'tool_id': tool_id,
+ 'message': err
+ });
+ }
+ },
+
+ /**
+ * Find a registered renderer with the passed id to render the passed view.
+ * @param {String} id ID of renderer to be used. eg. 'html'
+ * @param {String} view id of view, e.g. 'reportcard', 'stats' and 'components'
+ * @param {Object} params parameter object to pass to XXXview method of renderer.
+ * @return content the renderer generated.
+ */
+ render: function (id, view, params) {
+ var renderer = this.renderers[id],
+ content = '';
+
+ if (renderer.supports[view] !== undefined && renderer.supports[view] === 1) {
+ switch (view) {
+ case 'components':
+ content = renderer.componentsView(params.comps, params.total_size);
+ break;
+ case 'reportcard':
+ content = renderer.reportcardView(params.result_set);
+ break;
+ case 'stats':
+ content = renderer.statsView(params.stats);
+ break;
+ case 'tools':
+ content = renderer.toolsView(params.tools);
+ break;
+ case 'rulesetEdit':
+ content = renderer.rulesetEditView(params.rulesets);
+ break;
+ }
+ }
+ return content;
+
+ },
+
+ /**
+ * Get registered renderer with the passed id.
+ * @param {String} id ID of the renderer
+ */
+ getRenderer: function (id) {
+ return this.renderers[id];
+ },
+
+ /**
+ * @see YSLOW.registerRule
+ */
+ addRule: function (rule) {
+ var i, doc_obj,
+ required = ['id', 'name', 'config', 'info', 'lint'];
+
+ // check YSLOW.doc class for text
+ if (YSLOW.doc.rules && YSLOW.doc.rules[rule.id]) {
+ doc_obj = YSLOW.doc.rules[rule.id];
+ if (doc_obj.name) {
+ rule.name = doc_obj.name;
+ }
+ if (doc_obj.info) {
+ rule.info = doc_obj.info;
+ }
+ }
+
+ for (i = 0; i < required.length; i += 1) {
+ if (typeof rule[required[i]] === 'undefined') {
+ throw new YSLOW.Error('Interface error', 'Improperly implemented rule interface');
+ }
+ }
+ if (this.rules[rule.id] !== undefined) {
+ throw new YSLOW.Error('Rule register error', rule.id + " is already defined.");
+ }
+ this.rules[rule.id] = rule;
+ },
+
+ /**
+ * @see YSLOW.registerRuleset
+ */
+ addRuleset: function (ruleset, update) {
+ var i, required = ['id', 'name', 'rules'];
+
+ for (i = 0; i < required.length; i += 1) {
+ if (typeof ruleset[required[i]] === 'undefined') {
+ throw new YSLOW.Error('Interface error', 'Improperly implemented ruleset interface');
+ }
+ if (this.checkRulesetName(ruleset.id) && update !== true) {
+ throw new YSLOW.Error('Ruleset register error', ruleset.id + " is already defined.");
+ }
+ }
+ this.rulesets[ruleset.id] = ruleset;
+ },
+
+ /**
+ * Remove ruleset from controller.
+ * @param {String} ruleset_id ID of the ruleset to be deleted.
+ */
+ removeRuleset: function (ruleset_id) {
+ var ruleset = this.rulesets[ruleset_id];
+
+ if (ruleset && ruleset.custom === true) {
+ delete this.rulesets[ruleset_id];
+
+ // if we are deleting the default ruleset, change default to 'ydefault'.
+ if (this.default_ruleset_id === ruleset_id) {
+ this.default_ruleset_id = 'ydefault';
+ YSLOW.util.Preference.setPref("defaultRuleset", this.default_ruleset_id);
+ }
+ return ruleset;
+ }
+
+ return null;
+ },
+
+ /**
+ * Save ruleset to preference.
+ * @param {YSLOW.Ruleset} ruleset ruleset to be saved.
+ */
+ saveRulesetToPref: function (ruleset) {
+ if (ruleset.custom === true) {
+ YSLOW.util.Preference.setPref("customRuleset." + ruleset.id, JSON.stringify(ruleset, null, 2));
+ }
+ },
+
+ /**
+ * Remove ruleset from preference.
+ * @param {YSLOW.Ruleset} ruleset ruleset to be deleted.
+ */
+ deleteRulesetFromPref: function (ruleset) {
+ if (ruleset.custom === true) {
+ YSLOW.util.Preference.deletePref("customRuleset." + ruleset.id);
+ }
+ },
+
+ /**
+ * Get ruleset with the passed id.
+ * @param {String} ruleset_id ID of ruleset to be retrieved.
+ */
+ getRuleset: function (ruleset_id) {
+ return this.rulesets[ruleset_id];
+ },
+
+ /**
+ * @see YSLOW.registerRenderer
+ */
+ addRenderer: function (renderer) {
+ this.renderers[renderer.id] = renderer;
+ },
+
+ /**
+ * Return a hash of registered ruleset objects.
+ * @return a hash of rulesets with ruleset_id => ruleset
+ */
+ getRegisteredRuleset: function () {
+ return this.rulesets;
+ },
+
+ /**
+ * Return a hash of registered rule objects.
+ * @return all the registered rule objects in a hash. rule_id => rule object
+ */
+ getRegisteredRules: function () {
+ return this.rules;
+ },
+
+ /**
+ * Return the rule object identified by rule_id
+ * @param {String} rule_id ID of rule object to be retrieved.
+ * @return rule object.
+ */
+ getRule: function (rule_id) {
+ return this.rules[rule_id];
+ },
+
+ /**
+ * Check if name parameter is conflict with any existing ruleset name.
+ * @param {String} name Name to check.
+ * @return true if name conflicts, false otherwise.
+ * @type Boolean
+ */
+ checkRulesetName: function (name) {
+ var id, ruleset,
+ rulesets = this.rulesets;
+
+ name = name.toLowerCase();
+ for (id in rulesets) {
+ if (rulesets.hasOwnProperty(id)) {
+ ruleset = rulesets[id];
+ if (ruleset.id.toLowerCase() === name ||
+ ruleset.name.toLowerCase() === name) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * Set default ruleset.
+ * @param {String} id ID of the ruleset to be used as default.
+ */
+ setDefaultRuleset: function (id) {
+ if (this.rulesets[id] !== undefined) {
+ this.default_ruleset_id = id;
+ // save to pref
+ YSLOW.util.Preference.setPref("defaultRuleset", id);
+ }
+ },
+
+ /**
+ * Get default ruleset.
+ * @return default ruleset
+ * @type YSLOW.Ruleset
+ */
+ getDefaultRuleset: function () {
+ if (this.rulesets[this.default_ruleset_id] === undefined) {
+ this.setDefaultRuleset('ydefault');
+ }
+ return this.rulesets[this.default_ruleset_id];
+ },
+
+ /**
+ * Get default ruleset id
+ * @return ID of the default ruleset
+ * @type String
+ */
+ getDefaultRulesetId: function () {
+ return this.default_ruleset_id;
+ },
+
+ /**
+ * Load user preference for some rules. This is needed before enabling user writing ruleset yslow plugin.
+ */
+ loadRulePreference: function () {
+ var rule = this.getRule('yexpires'),
+ minSeconds = YSLOW.util.Preference.getPref("minFutureExpiresSeconds", 2 * 24 * 60 * 60);
+
+ if (minSeconds > 0 && rule) {
+ rule.config.howfar = minSeconds;
+ }
+ }
+};
+/**
+ * Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+ * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
+ */
+
+/*global YSLOW, Firebug, Components, ActiveXObject, gBrowser, window, getBrowser*/
+/*jslint sloppy: true, bitwise: true, browser: true, regexp: true*/
+
+/**
+ * @namespace YSLOW
+ * @class util
+ * @static
+ */
+YSLOW.util = {
+
+ /**
+ * merges two objects together, the properties of the second
+ * overwrite the properties of the first
+ *
+ * @param {Object} a Object a
+ * @param {Object} b Object b
+ * @return {Object} A new object, result of the merge
+ */
+ merge: function (a, b) {
+ var i, o = {};
+
+ for (i in a) {
+ if (a.hasOwnProperty(i)) {
+ o[i] = a[i];
+ }
+ }
+ for (i in b) {
+ if (b.hasOwnProperty(i)) {
+ o[i] = b[i];
+ }
+ }
+ return o;
+
+ },
+
+
+ /**
+ * Dumps debug information in FB console, Error console or alert
+ *
+ * @param {Object} what Object to dump
+ */
+ dump: function () {
+ var args;
+
+ // skip when debbuging is disabled
+ if (!YSLOW.DEBUG) {
+ return;
+ }
+
+ // get arguments and normalize single parameter
+ args = Array.prototype.slice.apply(arguments);
+ args = args && args.length === 1 ? args[0] : args;
+
+ try {
+ if (typeof Firebug !== 'undefined' && Firebug.Console
+ && Firebug.Console.log) { // Firebug
+ Firebug.Console.log(args);
+ } else if (typeof Components !== 'undefined' && Components.classes
+ && Components.interfaces) { // Firefox
+ Components.classes['@mozilla.org/consoleservice;1']
+ .getService(Components.interfaces.nsIConsoleService)
+ .logStringMessage(JSON.stringify(args, null, 2));
+ }
+ } catch (e1) {
+ try {
+ console.log(args);
+ } catch (e2) {
+ // alert shouldn't be used due to its annoying modal behavior
+ }
+ }
+ },
+
+ /**
+ * Filters an object/hash using a callback
+ *
+ * The callback function will be passed two params - a key and a value of each element
+ * It should return TRUE is the element is to be kept, FALSE otherwise
+ *
+ * @param {Object} hash Object to be filtered
+ * @param {Function} callback A callback function
+ * @param {Boolean} rekey TRUE to return a new array, FALSE to return an object and keep the keys/properties
+ */
+ filter: function (hash, callback, rekey) {
+ var i,
+ result = rekey ? [] : {};
+
+ for (i in hash) {
+ if (hash.hasOwnProperty(i) && callback(i, hash[i])) {
+ result[rekey ? result.length : i] = hash[i];
+ }
+ }
+
+ return result;
+ },
+
+ expires_month: {
+ Jan: 1,
+ Feb: 2,
+ Mar: 3,
+ Apr: 4,
+ May: 5,
+ Jun: 6,
+ Jul: 7,
+ Aug: 8,
+ Sep: 9,
+ Oct: 10,
+ Nov: 11,
+ Dec: 12
+ },
+
+
+ /**
+ * Make a pretty string out of an Expires object.
+ *
+ * @todo Remove or replace by a general-purpose date formatting method
+ *
+ * @param {String} s_expires Datetime string
+ * @return {String} Prity date
+ */
+ prettyExpiresDate: function (expires) {
+ var month;
+
+ if (Object.prototype.toString.call(expires) === '[object Date]' && expires.toString() !== 'Invalid Date' && !isNaN(expires)) {
+ month = expires.getMonth() + 1;
+ return expires.getFullYear() + "/" + month + "/" + expires.getDate();
+ } else if (!expires) {
+ return 'no expires';
+ }
+ return 'invalid date object';
+ },
+
+ /**
+ * Converts cache-control: max-age=? into a JavaScript date
+ *
+ * @param {Integer} seconds Number of seconds in the cache-control header
+ * @return {Date} A date object coresponding to the expiry date
+ */
+ maxAgeToDate: function (seconds) {
+ var d = new Date();
+
+ d = d.getTime() + parseInt(seconds, 10) * 1000;
+ return new Date(d);
+ },
+
+ /**
+ * Produces nicer sentences accounting for single/plural occurences.
+ *
+ * For example: "There are 3 scripts" vs "There is 1 script".
+ * Currently supported tags to be replaced are:
+ * %are%, %s% and %num%
+ *
+ *
+ * @param {String} template A template with tags, like "There %are% %num% script%s%"
+ * @param {Integer} num An integer value that replaces %num% and also deternmines how the other tags will be replaced
+ * @return {String} The text after substitution
+ */
+ plural: function (template, number) {
+ var i,
+ res = template,
+ repl = {
+ are: ['are', 'is'],
+ s: ['s', ''],
+ 'do': ['do', 'does'],
+ num: [number, number]
+ };
+
+
+ for (i in repl) {
+ if (repl.hasOwnProperty(i)) {
+ res = res.replace(new RegExp('%' + i + '%', 'gm'), (number === 1) ? repl[i][1] : repl[i][0]);
+ }
+ }
+
+ return res;
+ },
+
+ /**
+ * Counts the number of expression in a given piece of stylesheet.
+ *
+ * Expressions are identified by the presence of the literal string "expression(".
+ * There could be false positives in commented out styles.
+ *
+ * @param {String} content Text to inspect for the presence of expressions
+ * @return {Integer} The number of expressions in the text
+ */
+ countExpressions: function (content) {
+ var num_expr = 0,
+ index;
+
+ index = content.indexOf("expression(");
+ while (index !== -1) {
+ num_expr += 1;
+ index = content.indexOf("expression(", index + 1);
+ }
+
+ return num_expr;
+ },
+
+ /**
+ * Counts the number of AlphaImageLoader filter in a given piece of stylesheet.
+ *
+ * AlphaImageLoader filters are identified by the presence of the literal string "filter:" and
+ * "AlphaImageLoader" .
+ * There could be false positives in commented out styles.
+ *
+ * @param {String} content Text to inspect for the presence of filters
+ * @return {Hash} 'filter type' => count. For Example, {'_filter' : count }
+ */
+ countAlphaImageLoaderFilter: function (content) {
+ var index, colon, filter_hack, value,
+ num_filter = 0,
+ num_hack_filter = 0,
+ result = {};
+
+ index = content.indexOf("filter:");
+ while (index !== -1) {
+ filter_hack = false;
+ if (index > 0 && content.charAt(index - 1) === '_') {
+ // check underscore.
+ filter_hack = true;
+ }
+ // check literal string "AlphaImageLoader"
+ colon = content.indexOf(";", index + 7);
+ if (colon !== -1) {
+ value = content.substring(index + 7, colon);
+ if (value.indexOf("AlphaImageLoader") !== -1) {
+ if (filter_hack) {
+ num_hack_filter += 1;
+ } else {
+ num_filter += 1;
+ }
+ }
+ }
+ index = content.indexOf("filter:", index + 1);
+ }
+
+ if (num_hack_filter > 0) {
+ result.hackFilter = num_hack_filter;
+ }
+ if (num_filter > 0) {
+ result.filter = num_filter;
+ }
+
+ return result;
+ },
+
+ /**
+ * Returns the hostname (domain) for a given URL
+ *
+ * @param {String} url The absolute URL to get hostname from
+ * @return {String} The hostname
+ */
+ getHostname: function (url) {
+ var hostname = url.split('/')[2];
+
+ return (hostname && hostname.split(':')[0]) || '';
+ },
+
+ /**
+ * Returns an array of unique domain names, based on a given array of components
+ *
+ * @param {Array} comps An array of components (not a @see ComponentSet)
+ * @param {Boolean} exclude_ips Whether to exclude IP addresses from the list of domains (for DNS check purposes)
+ * @return {Array} An array of unique domian names
+ */
+ getUniqueDomains: function (comps, exclude_ips) {
+ var i, len, parts,
+ domains = {},
+ retval = [];
+
+ for (i = 0, len = comps.length; i < len; i += 1) {
+ parts = comps[i].url.split('/');
+ if (parts[2]) {
+ // add to hash, but remove port number first
+ domains[parts[2].split(':')[0]] = 1;
+ }
+ }
+
+ for (i in domains) {
+ if (domains.hasOwnProperty(i)) {
+ if (!exclude_ips) {
+ retval.push(i);
+ } else {
+ // exclude ips, identify them by the pattern "what.e.v.e.r.[number]"
+ parts = i.split('.');
+ if (isNaN(parseInt(parts[parts.length - 1], 10))) {
+ // the last part is "com" or something that is NaN
+ retval.push(i);
+ }
+ }
+ }
+ }
+
+ return retval;
+ },
+
+ summaryByDomain: function (comps, sumFields, excludeIPs) {
+ var i, j, len, parts, hostname, domain, comp, sumLen, field, sum,
+ domains = {},
+ retval = [];
+
+ // normalize sumField to array (makes things easier)
+ sumFields = [].concat(sumFields);
+ sumLen = sumFields.length;
+
+ // loop components, count and summarize fields
+ for (i = 0, len = comps.length; i < len; i += 1) {
+ comp = comps[i];
+ parts = comp.url.split('/');
+ if (parts[2]) {
+ // add to hash, but remove port number first
+ hostname = parts[2].split(':')[0];
+ domain = domains[hostname];
+ if (!domain) {
+ domain = {
+ domain: hostname,
+ count: 0
+ };
+ domains[hostname] = domain;
+ }
+ domain.count += 1;
+ // fields summary
+ for (j = 0; j < sumLen; j += 1) {
+ field = sumFields[j];
+ sum = domain['sum_' + field] || 0;
+ sum += parseInt(comp[field], 10) || 0;
+ domain['sum_' + field] = sum;
+ }
+ }
+ }
+
+ // loop hash of unique domains
+ for (domain in domains) {
+ if (domains.hasOwnProperty(domain)) {
+ if (!excludeIPs) {
+ retval.push(domains[domain]);
+ } else {
+ // exclude ips, identify them by the pattern "what.e.v.e.r.[number]"
+ parts = domain.split('.');
+ if (isNaN(parseInt(parts[parts.length - 1], 10))) {
+ // the last part is "com" or something that is NaN
+ retval.push(domains[domain]);
+ }
+ }
+ }
+ }
+
+ return retval;
+ },
+
+ /**
+ * Checks if a given piece of text (sctipt, stylesheet) is minified.
+ *
+ * The logic is: we strip consecutive spaces, tabs and new lines and
+ * if this improves the size by more that 20%, this means there's room for improvement.
+ *
+ * @param {String} contents The text to be checked for minification
+ * @return {Boolean} TRUE if minified, FALSE otherwise
+ */
+ isMinified: function (contents) {
+ var len = contents.length,
+ striplen;
+
+ if (len === 0) { // blank is as minified as can be
+ return true;
+ }
+
+ // TODO: enhance minifier logic by adding comment checking: \/\/[\w\d \t]*|\/\*[\s\S]*?\*\/
+ // even better: add jsmin/cssmin
+ striplen = contents.replace(/\n| {2}|\t|\r/g, '').length; // poor man's minifier
+ if (((len - striplen) / len) > 0.2) { // we saved 20%, so this component can get some mifinication done
+ return false;
+ }
+
+ return true;
+ },
+
+
+ /**
+ * Inspects the ETag.
+ *
+ * Returns FALSE (bad ETag) only if the server is Apache or IIS and the ETag format
+ * matches the default ETag format for the server. Anything else, including blank etag
+ * returns TRUE (good ETag).
+ * Default IIS: Filetimestamp:ChangeNumber
+ * Default Apache: inode-size-timestamp
+ *
+ * @param {String} etag ETag response header
+ * @return {Boolean} TRUE if ETag is good, FALSE otherwise
+ */
+ isETagGood: function (etag) {
+ var reIIS = /^[0-9a-f]+:[0-9a-f]+$/,
+ reApache = /^[0-9a-f]+\-[0-9a-f]+\-[0-9a-f]+$/;
+
+ if (!etag) {
+ return true; // no etag is ok etag
+ }
+
+ etag = etag.replace(/^["']|["'][\s\S]*$/g, ''); // strip " and '
+ return !(reApache.test(etag) || reIIS.test(etag));
+ },
+
+ /**
+ * Get internal component type from passed mime type.
+ * @param {String} content_type mime type of the content.
+ * @return yslow internal component type
+ * @type String
+ */
+ getComponentType: function (content_type) {
+ var c_type = 'unknown';
+
+ if (content_type && typeof content_type === "string") {
+ if (content_type === "text/html" || content_type === "text/plain") {
+ c_type = 'doc';
+ } else if (content_type === "text/css") {
+ c_type = 'css';
+ } else if (/javascript/.test(content_type)) {
+ c_type = 'js';
+ } else if (/flash/.test(content_type)) {
+ c_type = 'flash';
+ } else if (/image/.test(content_type)) {
+ c_type = 'image';
+ } else if (/font/.test(content_type)) {
+ c_type = 'font';
+ }
+ }
+
+ return c_type;
+ },
+
+ /**
+ * base64 encode the data. This works with data that fails win.atob.
+ * @param {bytes} data data to be encoded.
+ * @return bytes array of data base64 encoded.
+ */
+ base64Encode: function (data) {
+ var i, a, b, c, new_data = '',
+ padding = 0,
+ arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'];
+
+ for (i = 0; i < data.length; i += 3) {
+ a = data.charCodeAt(i);
+ if ((i + 1) < data.length) {
+ b = data.charCodeAt(i + 1);
+ } else {
+ b = 0;
+ padding += 1;
+ }
+ if ((i + 2) < data.length) {
+ c = data.charCodeAt(i + 2);
+ } else {
+ c = 0;
+ padding += 1;
+ }
+
+ new_data += arr[(a & 0xfc) >> 2];
+ new_data += arr[((a & 0x03) << 4) | ((b & 0xf0) >> 4)];
+ if (padding > 0) {
+ new_data += "=";
+ } else {
+ new_data += arr[((b & 0x0f) << 2) | ((c & 0xc0) >> 6)];
+ }
+ if (padding > 1) {
+ new_data += "=";
+ } else {
+ new_data += arr[(c & 0x3f)];
+ }
+ }
+
+ return new_data;
+ },
+
+ /**
+ * Creates x-browser XHR objects
+ *
+ * @return {XMLHTTPRequest} A new XHR object
+ */
+ getXHR: function () {
+ var i = 0,
+ xhr = null,
+ ids = ['MSXML2.XMLHTTP.3.0', 'MSXML2.XMLHTTP', 'Microsoft.XMLHTTP'];
+
+
+ if (typeof XMLHttpRequest === 'function') {
+ return new XMLHttpRequest();
+ }
+
+ for (i = 0; i < ids.length; i += 1) {
+ try {
+ xhr = new ActiveXObject(ids[i]);
+ break;
+ } catch (e) {}
+
+ }
+
+ return xhr;
+ },
+
+ /**
+ * Returns the computed style
+ *
+ * @param {HTMLElement} el A node
+ * @param {String} st Style identifier, e.g. "backgroundImage"
+ * @param {Boolean} get_url Whether to return a url
+ * @return {String|Boolean} The value of the computed style, FALSE if get_url is TRUE and the style is not a URL
+ */
+ getComputedStyle: function (el, st, get_url) {
+ var style, urlMatch,
+ res = '';
+
+ if (el.currentStyle) {
+ res = el.currentStyle[st];
+ }
+
+ if (el.ownerDocument && el.ownerDocument.defaultView && document.defaultView.getComputedStyle) {
+ style = el.ownerDocument.defaultView.getComputedStyle(el, '');
+ if (style) {
+ res = style[st];
+ }
+ }
+
+ if (!get_url) {
+ return res;
+ }
+
+ if (typeof res !== 'string') {
+ return false;
+ }
+
+ urlMatch = res.match(/\burl\((\'|\"|)([^\'\"]+?)\1\)/);
+ if (urlMatch) {
+ return urlMatch[2];
+ } else {
+ return false;
+ }
+ },
+
+ /**
+ * escape '<' and '>' in the passed html code.
+ * @param {String} html code to be escaped.
+ * @return escaped html code
+ * @type String
+ */
+ escapeHtml: function (html) {
+ return (html || '').toString()
+ .replace(//g, ">");
+ },
+
+ /**
+ * escape quotes in the passed html code.
+ * @param {String} str string to be escaped.
+ * @param {String} which type of quote to be escaped. 'single' or 'double'
+ * @return escaped string code
+ * @type String
+ */
+ escapeQuotes: function (str, which) {
+ if (which === 'single') {
+ return str.replace(/\'/g, '\\\''); // '
+ }
+ if (which === 'double') {
+ return str.replace(/\"/g, '\\\"'); // "
+ }
+ return str.replace(/\'/g, '\\\'').replace(/\"/g, '\\\"'); // ' and "
+ },
+
+ /**
+ * Convert a HTTP header name to its canonical form,
+ * e.g. "content-length" => "Content-Length".
+ * @param headerName the header name (case insensitive)
+ * @return {String} the formatted header name
+ */
+ formatHeaderName: (function () {
+ var specialCases = {
+ 'content-md5': 'Content-MD5',
+ dnt: 'DNT',
+ etag: 'ETag',
+ p3p: 'P3P',
+ te: 'TE',
+ 'www-authenticate': 'WWW-Authenticate',
+ 'x-att-deviceid': 'X-ATT-DeviceId',
+ 'x-cdn': 'X-CDN',
+ 'x-ua-compatible': 'X-UA-Compatible',
+ 'x-xss-protection': 'X-XSS-Protection'
+ };
+ return function (headerName) {
+ var lowerCasedHeaderName = headerName.toLowerCase();
+ if (specialCases.hasOwnProperty(lowerCasedHeaderName)) {
+ return specialCases[lowerCasedHeaderName];
+ } else {
+ // Make sure that the first char and all chars following a dash are upper-case:
+ return lowerCasedHeaderName.replace(/(^|-)([a-z])/g, function ($0, optionalLeadingDash, ch) {
+ return optionalLeadingDash + ch.toUpperCase();
+ });
+ }
+ };
+ }()),
+
+ /**
+ * Math mod method.
+ * @param {Number} divisee
+ * @param {Number} base
+ * @return mod result
+ * @type Number
+ */
+ mod: function (divisee, base) {
+ return Math.round(divisee - (Math.floor(divisee / base) * base));
+ },
+
+ /**
+ * Abbreviate the passed url to not exceed maxchars.
+ * (Just display the hostname and first few chars after the last slash.
+ * @param {String} url originial url
+ * @param {Number} maxchars max. number of characters in the result string.
+ * @return abbreviated url
+ * @type String
+ */
+ briefUrl: function (url, maxchars) {
+ var iDoubleSlash, iQMark, iFirstSlash, iLastSlash;
+
+ maxchars = maxchars || 100; // default 100 characters
+ if (url === undefined) {
+ return '';
+ }
+
+ // We assume it's a full URL.
+ iDoubleSlash = url.indexOf("//");
+ if (-1 !== iDoubleSlash) {
+
+ // remove query string
+ iQMark = url.indexOf("?");
+ if (-1 !== iQMark) {
+ url = url.substring(0, iQMark) + "?...";
+ }
+
+ if (url.length > maxchars) {
+ iFirstSlash = url.indexOf("/", iDoubleSlash + 2);
+ iLastSlash = url.lastIndexOf("/");
+ if (-1 !== iFirstSlash && -1 !== iLastSlash && iFirstSlash !== iLastSlash) {
+ url = url.substring(0, iFirstSlash + 1) + "..." + url.substring(iLastSlash);
+ } else {
+ url = url.substring(0, maxchars + 1) + "...";
+ }
+ }
+ }
+
+ return url;
+ },
+
+ /**
+ * Return a string with an anchor around a long piece of text.
+ * (It's confusing, but often the "long piece of text" is the URL itself.)
+ * Snip the long text if necessary.
+ * Optionally, break the long text across multiple lines.
+ * @param {String} text
+ * @param {String} url
+ * @param {String} sClass class name for the new anchor
+ * @param {Boolean} bBriefUrl whether the url should be abbreviated.
+ * @param {Number} maxChars max. number of chars allowed for each line.
+ * @param {Number} numLines max. number of lines allowed
+ * @param {String} rel rel attribute of anchor.
+ * @return html code for the anchor.
+ * @type String
+ */
+ prettyAnchor: function (text, url, sClass, bBriefUrl, maxChars, numLines, rel) {
+ var escaped_dq_url,
+ sTitle = '',
+ sResults = '',
+ iLines = 0;
+
+ if (typeof url === 'undefined') {
+ url = text;
+ }
+ if (typeof sClass === 'undefined') {
+ sClass = '';
+ } else {
+ sClass = ' class="' + sClass + '"';
+ }
+ if (typeof maxChars === 'undefined') {
+ maxChars = 100;
+ }
+ if (typeof numLines === 'undefined') {
+ numLines = 1;
+ }
+ rel = (rel) ? ' rel="' + rel + '"' : '';
+
+ url = YSLOW.util.escapeHtml(url);
+ text = YSLOW.util.escapeHtml(text);
+
+ escaped_dq_url = YSLOW.util.escapeQuotes(url, 'double');
+
+ if (bBriefUrl) {
+ text = YSLOW.util.briefUrl(text, maxChars);
+ sTitle = ' title="' + escaped_dq_url + '"';
+ }
+
+ while (0 < text.length) {
+ sResults += '' + text.substring(0, maxChars);
+ text = text.substring(maxChars);
+ iLines += 1;
+ if (iLines >= numLines) {
+ // We've reached the maximum number of lines.
+ if (0 < text.length) {
+ // If there's still text leftover, snip it.
+ sResults += "[snip]";
+ }
+ sResults += "";
+ break;
+ } else {
+ // My (weak) attempt to break long URLs.
+ sResults += " ";
+ }
+ }
+
+ return sResults;
+ },
+
+ /**
+ * Convert a number of bytes into a readable KB size string.
+ * @param {Number} size
+ * @return readable KB size string
+ * @type String
+ */
+ kbSize: function (size) {
+ var remainder = size % (size > 100 ? 100 : 10);
+ size -= remainder;
+ return parseFloat(size / 1000) + (0 === (size % 1000) ? ".0" : "") + "K";
+ },
+
+ /**
+ * @final
+ */
+ prettyTypes: {
+ "image": "Image",
+ "doc": "HTML/Text",
+ "cssimage": "CSS Image",
+ "css": "Stylesheet File",
+ "js": "JavaScript File",
+ "flash": "Flash Object",
+ "iframe": "IFrame",
+ "xhr": "XMLHttpRequest",
+ "redirect": "Redirect",
+ "favicon": "Favicon",
+ "unknown": "Unknown"
+ },
+
+/*
+ * Convert a type (eg, "cssimage") to a prettier name (eg, "CSS Images").
+ * @param {String} sType component type
+ * @return display name of component type
+ * @type String
+ */
+ prettyType: function (sType) {
+ return YSLOW.util.prettyTypes[sType];
+ },
+
+ /**
+ * Return a letter grade for a score.
+ * @param {String or Number} iScore
+ * @return letter grade for a score
+ * @type String
+ */
+ prettyScore: function (score) {
+ var letter = 'F';
+
+ if (!parseInt(score, 10) && score !== 0) {
+ return score;
+ }
+ if (score === -1) {
+ return 'N/A';
+ }
+
+ if (score >= 90) {
+ letter = 'A';
+ } else if (score >= 80) {
+ letter = 'B';
+ } else if (score >= 70) {
+ letter = 'C';
+ } else if (score >= 60) {
+ letter = 'D';
+ } else if (score >= 50) {
+ letter = 'E';
+ }
+
+ return letter;
+ },
+
+ /**
+ * Returns YSlow results as an Object.
+ * @param {YSLOW.context} yscontext yslow context.
+ * @param {String|Array} info Information to be shown
+ * (basic|grade|stats|comps|all) [basic].
+ * @return {Object} the YSlow results object.
+ */
+ getResults: function (yscontext, info) {
+ var i, l, results, url, type, comps, comp, encoded_url, obj, cr,
+ cs, etag, name, len, include_grade, include_comps, include_stats,
+ result, len2, spaceid, header, sourceHeaders, targetHeaders,
+ reButton = /