From 35bddaa17ac6629dbf4f1b9ef369cdad42b7c7a5 Mon Sep 17 00:00:00 2001 From: soulgalore Date: Wed, 5 Feb 2014 09:29:48 +0100 Subject: [PATCH] upgraded to YSlow 3.1.8 --- bin/sitespeed.io | 5 +- dependencies/yslow-3.1.5-sitespeed.js | 1 - dependencies/yslow-3.1.8-sitespeed.js | 11662 ++++++++++++++++++++++++ 3 files changed, 11665 insertions(+), 3 deletions(-) delete mode 100644 dependencies/yslow-3.1.5-sitespeed.js create mode 100644 dependencies/yslow-3.1.8-sitespeed.js diff --git a/bin/sitespeed.io b/bin/sitespeed.io index ea787c057..43a49c447 100755 --- a/bin/sitespeed.io +++ b/bin/sitespeed.io @@ -81,7 +81,7 @@ PAGES_COLUMNS= ## The default user agent USER_AGENT="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.71 Safari/537.36" ## The YSlow file to use -YSLOW_FILE="$SITESPEED_HOME"/dependencies/yslow-3.1.5-sitespeed.js +YSLOW_FILE="$SITESPEED_HOME"/dependencies/yslow-3.1.8-sitespeed.js ## The desktop ruleset RULESET=sitespeed.io-desktop RULESET_MOBILE=sitespeed.io-mobile @@ -823,7 +823,8 @@ function analyze() { local pagefilename=$(get_filename $1 $2) echo "Analyzing $url" - phantomjs --ignore-ssl-errors=yes $PROXY_PHANTOMJS $YSLOW_FILE -d -r $RULESET -f xml --ua "$USER_AGENT_YSLOW" $VIEWPORT_YSLOW -n "$REPORT_DATA_HAR_DIR/$pagefilename.har" "$url" >"$REPORT_DATA_PAGES_DIR/$pagefilename.xml" 2>> $REPORT_DATA_DIR/phantomjs.error.log || echo "PhantomJS could not handle $url , check the error log: $REPORT_DATA_DIR/phantomjs.error.log" + ## Removing HAR functionality from phantomjs, will be included in browsertime -n "$REPORT_DATA_HAR_DIR/$pagefilename.har" + phantomjs --ignore-ssl-errors=yes $PROXY_PHANTOMJS $YSLOW_FILE -d -r $RULESET -f xml --ua "$USER_AGENT_YSLOW" $VIEWPORT_YSLOW "$url" >"$REPORT_DATA_PAGES_DIR/$pagefilename.xml" 2>> $REPORT_DATA_DIR/phantomjs.error.log || echo "PhantomJS could not handle $url , check the error log: $REPORT_DATA_DIR/phantomjs.error.log" local s=$(du -k "$REPORT_DATA_PAGES_DIR/$pagefilename.xml" | cut -f1) # Check that the size is bigger than 0 diff --git a/dependencies/yslow-3.1.5-sitespeed.js b/dependencies/yslow-3.1.5-sitespeed.js deleted file mode 100644 index 90206fa98..000000000 --- a/dependencies/yslow-3.1.5-sitespeed.js +++ /dev/null @@ -1 +0,0 @@ -if(!Date.prototype.toISOString){Date.prototype.toISOString=function(){function b(c){return c<10?"0"+c:c}function a(c){return c<10?"00"+c:c<100?"0"+c:c}return this.getFullYear()+"-"+b(this.getMonth()+1)+"-"+b(this.getDate())+"T"+b(this.getHours())+":"+b(this.getMinutes())+":"+b(this.getSeconds())+"."+a(this.getMilliseconds())+"Z"}}function createHAR(c,f,d,e,b){var a=[];e.forEach(function(j){var h=j.request,g=j.startReply,k=j.endReply;if(!h||!g||!k){return}if(h.url.match(/(^data:image\/.*)/i)){return}a.push({startedDateTime:h.time.toISOString(),time:k.time-h.time,request:{method:h.method,url:h.url,httpVersion:"HTTP/1.1",cookies:[],headers:h.headers,queryString:[],headersSize:-1,bodySize:-1},response:{status:k.status,statusText:k.statusText,httpVersion:"HTTP/1.1",cookies:[],headers:k.headers,redirectURL:"",headersSize:-1,bodySize:g.bodySize,content:{size:g.bodySize,mimeType:k.contentType}},cache:{},timings:{blocked:0,dns:-1,connect:-1,send:0,wait:g.time-h.time,receive:k.time-g.time,ssl:-1},pageref:c})});return{log:{version:"1.2",creator:{name:"PhantomJS",version:phantom.version.major+"."+phantom.version.minor+"."+phantom.version.patch},pages:[{startedDateTime:d.toISOString(),id:c,title:f,pageTimings:{onLoad:b}}],entries:a}}}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,harfilename:""},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",n:"harfilename"};for(i=0;i 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]"," -n, --harfilename the name of the HAR file, if no name is supplied, no HAR is created",""," 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()}yslowArgs.dict=unaryArgs.dict;yslowArgs.verbose=unaryArgs.verbose;urls.forEach(function(a){var c=webpage.create();c.resources={};c.harresources=[];c.address=a;c.redirects=[];c.settings.webSecurityEnabled=false;c.onLoadStarted=function(){c.startTime=new Date()};c.onResourceRequested=function(d){c.resources[d.url]={request:d};d.starttime=d.time.getTime();c.harresources[d.id]={request:d,startReply:null,endReply:null}};c.onResourceReceived=function(e){if(e.stage==="start"){c.harresources[e.id].startReply=e}if(e.stage==="end"){c.harresources[e.id].endReply=e;if(e.status===301||e.status===302){var h;for(var d=0;d0){if(!n.exists(yslowArgs.harfilename)){n.write(yslowArgs.harfilename,JSON.stringify(har,undefined,4),"w")}}for(d in g){if(g.hasOwnProperty(d)){h=g[d].response;if(h){h.time=new Date(h.time)-e}}}f=function(){if(typeof YSLOW==="undefined"){YSLOW={}}YSLOW.DEBUG=true;YSLOW.registerRule=function(s){YSLOW.controller.addRule(s)};YSLOW.registerRuleset=function(s){YSLOW.controller.addRuleset(s)};YSLOW.registerRenderer=function(s){YSLOW.controller.addRenderer(s)};YSLOW.registerTool=function(s){YSLOW.Tools.addCustomTool(s)};YSLOW.addEventListener=function(t,u,s){YSLOW.util.event.addListener(t,u,s)};YSLOW.removeEventListener=function(s,t){return YSLOW.util.event.removeListener(s,t)};YSLOW.Error=function(s,t){this.name=s;this.message=t};YSLOW.Error.prototype={toString:function(){return this.name+"\n"+this.message}};YSLOW.version="3.1.8";YSLOW.ComponentSet=function(s,t){this.root_node=s;this.components=[];this.outstanding_net_request=0;this.component_info=[];this.onloadTimestamp=t;this.nextID=1;this.notified_fetch_done=false};YSLOW.ComponentSet.prototype={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.")}},addComponent:function(v,w,t,y){var s,x,u;if(!v){if(!this.empty_url){this.empty_url=[]}this.empty_url[w]=(this.empty_url[w]||0)+1}if(v&&w){if(!YSLOW.ComponentSet.isValidProtocol(v)||!YSLOW.ComponentSet.isValidURL(v)){return s}v=YSLOW.util.makeAbsoluteUrl(v,t);v=YSLOW.util.escapeHtml(v);x=typeof this.component_info[v]!=="undefined";u=w==="doc";if(!x||u){this.component_info[v]={state:"NONE",count:x?this.component_info[v].count:0};s=new YSLOW.Component(v,w,this,y);if(s){s.id=this.nextID+=1;this.components[this.components.length]=s;if(!this.doc_comp&&u){this.doc_comp=s}if(this.component_info[v].state==="NONE"){this.component_info[v].state="REQUESTED";this.outstanding_net_request+=1}}else{this.component_info[v].state="ERROR";YSLOW.util.event.fire("componentFetchError")}}this.component_info[v].count+=1}return s},addComponentNoDuplicate:function(t,u,s){if(t&&u){t=YSLOW.util.escapeHtml(t);t=YSLOW.util.makeAbsoluteUrl(t,s);if(this.component_info[t]===undefined){return this.addComponent(t,u,s)}}},getComponentsByType:function(F,C,w){var y,x,D,v,G,A,u,z=this.components,E=this.component_info,s=[],B={};if(typeof C==="undefined"){C=!(YSLOW.util.Preference.getPref("excludeAfterOnload",true))}if(typeof w==="undefined"){w=!(YSLOW.util.Preference.getPref("excludeBeaconsFromLint",true))}if(typeof F==="string"){B[F]=1}else{for(y=0,D=F.length;y0){y=w.substr(0,u);for(v=0;v0){y.expires=new Date(y.headers.expires)}}if(y.type==="image"&&!u){if(typeof Image!==D){w=new Image()}else{w=document.createElement("img")}if(y.body.length){A="data:"+y.headers["content-type"]+";base64,"+YSLOW.util.base64Encode(y.body);B=1}else{A=y.url}w.onerror=function(){w.onerror=v;if(B){w.src=y.url}};w.onload=function(){w.onload=v;if(w&&w.width&&w.height){if(y.object_prop){y.object_prop.actual_width=w.width;y.object_prop.actual_height=w.height}else{y.object_prop={width:w.width,height:w.height,actual_width:w.width,actual_height:w.height}}if(w.width<2&&w.height<2){y.is_beacon=true}}};w.src=A}};YSLOW.Component.prototype.hasOldModifiedDate=function(){var s=Number(new Date()),t=this.headers["last-modified"];if(typeof t!=="undefined"){return((s-Number(new Date(t)))>(24*60*60*1000))}return false};YSLOW.Component.prototype.hasFarFutureExpiresOrMaxAge=function(){var v,t=Number(new Date()),u=YSLOW.util.Preference.getPref("minFutureExpiresSeconds",2*24*60*60),s=u*1000;if(typeof this.expires==="object"){v=Number(this.expires);if((v-t)>s){return true}}return false};YSLOW.Component.prototype.getEtag=function(){return this.headers.etag||""};YSLOW.Component.prototype.getMaxAge=function(){var t,u,s,v=this.headers["cache-control"];if(v){t=v.indexOf("max-age");if(t>-1){u=parseInt(v.substring(t+8),10);if(u>0){s=YSLOW.util.maxAgeToDate(u)}}}return s};YSLOW.Component.prototype.getSetCookieSize=function(){var u,s,t=0;if(this.headers&&this.headers["set-cookie"]){u=this.headers["set-cookie"].split("\n");if(u.length>0){for(s=0;s0){u=this.cookie.split("\n");if(u.length>0){for(s=0;ss.parent.onloadTimestamp;s.populateProperties(false,true);s.get_info_state="DONE";s.parent.onComponentGetInfoStateChange({comp:s,state:"DONE"})};if(u.request&&u.response){t(u.request,u.response)}};YSLOW.controller={rules:{},rulesets:{},onloadTimestamp:null,renderers:{},default_ruleset_id:"ydefault",run_pending:0,init:function(){var t,s,v,u;YSLOW.util.event.addListener("onload",function(w){this.onloadTimestamp=w.time;YSLOW.util.setTimer(function(){YSLOW.controller.run_pending_event()})},this);YSLOW.util.event.addListener("onUnload",function(w){this.run_pending=0;this.onloadTimestamp=null},this);t=YSLOW.util.Preference.getPrefList("customRuleset.",undefined);if(t&&t.length>0){for(s=0;s0){v=JSON.parse(u,null);v.custom=true;this.addRuleset(v)}}}this.default_ruleset_id=YSLOW.util.Preference.getPref("defaultRuleset","ydefault");this.loadRulePreference()},run:function(v,w,t){var x,s,u=v.document;if(!u||!u.location||u.location.href.indexOf("about:")===0||"undefined"===typeof u.location.hostname){if(!t){s="Please enter a valid website address before running YSlow.";YSLOW.ysview.openDialog(YSLOW.ysview.panel_doc,389,150,s,"","Ok")}return}if(!w.PAGE.loaded){this.run_pending={win:v,yscontext:w};return}YSLOW.util.event.fire("peelStart",undefined);x=YSLOW.peeler.peel(u,this.onloadTimestamp);w.component_set=x;YSLOW.util.event.fire("peelComplete",{component_set:x});x.notifyPeelDone()},run_pending_event:function(){if(this.run_pending){this.run(this.run_pending.win,this.run_pending.yscontext,false);this.run_pending=0}},lint:function(J,u,s){var v,z,H,y,A,B,I,G=[],E=[],F=0,D=0,w=this,x=w.rulesets,C=w.default_ruleset_id;if(s){G=x[s]}else{if(C&&x[C]){G=x[C]}else{for(H in x){if(x.hasOwnProperty(H)&&x[H]){G=x[H];break}}}}z=G.rules;for(H in z){if(z.hasOwnProperty(H)&&z[H]&&this.rules.hasOwnProperty(H)){try{v=this.rules[H];y=YSLOW.util.merge(v.config,z[H]);A=v.lint(J,u.component_set,y);B=(G.weights?G.weights[H]:undefined);if(B!==undefined){B=parseInt(B,10)}if(B===undefined||B<0||B>100){if(x.ydefault.weights[H]){B=x.ydefault.weights[H]}else{B=5}}A.weight=B;if(A.score!==undefined){if(typeof A.score!=="number"){I=parseInt(A.score,10);if(!isNaN(I)){A.score=I}}if(typeof A.score==="number"){D+=A.weight;if(!YSLOW.util.Preference.getPref("allowNegativeScore",false)){if(A.score<0){A.score=0}if(typeof A.score!=="number"){A.score=-1}}if(A.score!==0){F+=A.score*(typeof A.weight!=="undefined"?A.weight:1)}}}A.name=v.name;A.category=v.category;A.rule_id=H;E[E.length]=A}catch(t){YSLOW.util.dump("YSLOW.controller.lint: "+H,t);YSLOW.util.event.fire("lintError",{rule:H,message:t})}}}u.PAGE.overallScore=F/(D>0?D:1);u.result_set=new YSLOW.ResultSet(E,u.PAGE.overallScore,G);u.result_set.url=u.component_set.doc_comp.url;YSLOW.util.event.fire("lintResultReady",{yslowContext:u});return u.result_set},runTool:function(F,C,u){var I,y,E,x,A,t,D,v,H,G,z,B=YSLOW.Tools.getTool(F);try{if(typeof B==="object"){I=B.run(C.document,C.component_set,u);if(B.print_output){y="";if(typeof I==="object"){y=I.html}else{if(typeof I==="string"){y=I}}E=YSLOW.util.getNewDoc();z=E.body||E.documentElement;z.innerHTML=y;x=E.getElementsByTagName("head")[0];if(typeof I.css==="undefined"){t="chrome://yslow/content/yslow/tool.css";D=new XMLHttpRequest();D.open("GET",t,false);D.send(null);A=D.responseText}else{A=I.css}if(typeof A==="string"){v=E.createElement("style");v.setAttribute("type","text/css");v.appendChild(E.createTextNode(A));x.appendChild(v)}if(typeof I.js!=="undefined"){H=E.createElement("script");H.setAttribute("type","text/javascript");H.appendChild(E.createTextNode(I.js));x.appendChild(H)}if(typeof I.plot_component!=="undefined"&&I.plot_component===true){YSLOW.renderer.plotComponents(E,C)}}}else{G=F+" is not a tool.";YSLOW.util.dump(G);YSLOW.util.event.fire("toolError",{tool_id:F,message:G})}}catch(w){YSLOW.util.dump("YSLOW.controller.runTool: "+F,w);YSLOW.util.event.fire("toolError",{tool_id:F,message:w})}},render:function(w,s,v){var u=this.renderers[w],t="";if(u.supports[s]!==undefined&&u.supports[s]===1){switch(s){case"components":t=u.componentsView(v.comps,v.total_size);break;case"reportcard":t=u.reportcardView(v.result_set);break;case"stats":t=u.statsView(v.stats);break;case"tools":t=u.toolsView(v.tools);break;case"rulesetEdit":t=u.rulesetEditView(v.rulesets);break}}return t},getRenderer:function(s){return this.renderers[s]},addRule:function(u){var s,t,v=["id","name","config","info","lint"];if(YSLOW.doc.rules&&YSLOW.doc.rules[u.id]){t=YSLOW.doc.rules[u.id];if(t.name){u.name=t.name}if(t.info){u.info=t.info}}for(s=0;s0&&t){t.config.howfar=s}}};YSLOW.util={merge:function(t,s){var u,v={};for(u in t){if(t.hasOwnProperty(u)){v[u]=t[u]}}for(u in s){if(s.hasOwnProperty(u)){v[u]=s[u]}}return v},dump:function(){var s;if(!YSLOW.DEBUG){return}s=Array.prototype.slice.apply(arguments);s=s&&s.length===1?s[0]:s;try{if(typeof Firebug!=="undefined"&&Firebug.Console&&Firebug.Console.log){Firebug.Console.log(s)}else{if(typeof Components!=="undefined"&&Components.classes&&Components.interfaces){Components.classes["@mozilla.org/consoleservice;1"].getService(Components.interfaces.nsIConsoleService).logStringMessage(JSON.stringify(s,null,2))}}}catch(u){try{console.log(s)}catch(t){}}},filter:function(v,w,t){var u,s=t?[]:{};for(u in v){if(v.hasOwnProperty(u)&&w(u,v[u])){s[t?s.length:u]=v[u]}}return s},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},prettyExpiresDate:function(s){var t;if(Object.prototype.toString.call(s)==="[object Date]"&&s.toString()!=="Invalid Date"&&!isNaN(s)){t=s.getMonth()+1;return s.getFullYear()+"/"+t+"/"+s.getDate()}else{if(!s){return"no expires"}}return"invalid date object"},maxAgeToDate:function(t){var s=new Date();s=s.getTime()+parseInt(t,10)*1000;return new Date(s)},plural:function(u,v){var t,s=u,w={are:["are","is"],s:["s",""],"do":["do","does"],num:[v,v]};for(t in w){if(w.hasOwnProperty(t)){s=s.replace(new RegExp("%"+t+"%","gm"),(v===1)?w[t][1]:w[t][0])}}return s},countExpressions:function(u){var s=0,t;t=u.indexOf("expression(");while(t!==-1){s+=1;t=u.indexOf("expression(",t+1)}return s},countAlphaImageLoaderFilter:function(x){var v,w,u,y,t=0,z=0,s={};v=x.indexOf("filter:");while(v!==-1){u=false;if(v>0&&x.charAt(v-1)==="_"){u=true}w=x.indexOf(";",v+7);if(w!==-1){y=x.substring(v+7,w);if(y.indexOf("AlphaImageLoader")!==-1){if(u){z+=1}else{t+=1}}}v=x.indexOf("filter:",v+1)}if(z>0){s.hackFilter=z}if(t>0){s.filter=t}return s},getHostname:function(t){var s=t.split("/")[2];return(s&&s.split(":")[0])||""},getUniqueDomains:function(y,w){var v,t,x,s={},u=[];for(v=0,t=y.length;v0.2){return false}return true},isETagGood:function(s){var u=/^[0-9a-f]+:([1-9a-f]|[0-9a-f]{2,})$/,t=/^[0-9a-f]+\-[0-9a-f]+\-[0-9a-f]+$/;if(!s){return true}s=s.replace(/^["']|["'][\s\S]*$/g,"");return !(t.test(s)||u.test(s))},getComponentType:function(s){var t="unknown";if(s&&typeof s==="string"){if(s==="text/html"||s==="text/plain"){t="doc"}else{if(s==="text/css"){t="css"}else{if(/javascript/.test(s)){t="js"}else{if(/flash/.test(s)){t="flash"}else{if(/image/.test(s)){t="image"}else{if(/font/.test(s)){t="font"}}}}}}}return t},base64Encode:function(x){var w,v,u,z,t="",y=0,s=["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(w=0;w>2];t+=s[((v&3)<<4)|((u&240)>>4)];if(y>0){t+="="}else{t+=s[((u&15)<<2)|((z&192)>>6)]}if(y>1){t+="="}else{t+=s[(z&63)]}}return t},getXHR:function(){var s=0,v=null,t=["MSXML2.XMLHTTP.3.0","MSXML2.XMLHTTP","Microsoft.XMLHTTP"];if(typeof XMLHttpRequest==="function"){return new XMLHttpRequest()}for(s=0;s/g,">")},escapeQuotes:function(t,s){if(s==="single"){return t.replace(/\'/g,"\\'")}if(s==="double"){return t.replace(/\"/g,'\\"')}return t.replace(/\'/g,"\\'").replace(/\"/g,'\\"')},formatHeaderName:(function(){var s={"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(u){var t=u.toLowerCase();if(s.hasOwnProperty(t)){return s[t]}else{return t.replace(/(^|-)([a-z])/g,function(v,w,x){return w+x.toUpperCase()})}}}()),mod:function(s,t){return Math.round(s-(Math.floor(s/t)*t))},briefUrl:function(u,s){var t,v,w,x;s=s||100;if(u===undefined){return""}t=u.indexOf("//");if(-1!==t){v=u.indexOf("?");if(-1!==v){u=u.substring(0,v)+"?..."}if(u.length>s){w=u.indexOf("/",t+2);x=u.lastIndexOf("/");if(-1!==w&&-1!==x&&w!==x){u=u.substring(0,w+1)+"..."+u.substring(x)}else{u=u.substring(0,s+1)+"..."}}}return u},prettyAnchor:function(A,t,v,u,y,s,B){var w,z="",x="",C=0;if(typeof t==="undefined"){t=A}if(typeof v==="undefined"){v=""}else{v=' class="'+v+'"'}if(typeof y==="undefined"){y=100}if(typeof s==="undefined"){s=1}B=(B)?' rel="'+B+'"':"";t=YSLOW.util.escapeHtml(t);A=YSLOW.util.escapeHtml(A);w=YSLOW.util.escapeQuotes(t,"double");if(u){A=YSLOW.util.briefUrl(A,y);z=' title="'+w+'"'}while(0"+A.substring(0,y);A=A.substring(y);C+=1;if(C>=s){if(0";break}else{x+=" "}}return x},kbSize:function(s){var t=s%(s>100?100:10);s-=t;return parseFloat(s/1000)+(0===(s%1000)?".0":"")+"K"},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"},prettyType:function(s){return YSLOW.util.prettyTypes[s]},prettyScore:function(t){var s="F";if(!parseInt(t,10)&&t!==0){return t}if(t===-1){return"N/A"}if(t>=90){s="A"}else{if(t>=80){s="B"}else{if(t>=70){s="C"}else{if(t>=60){s="D"}else{if(t>=50){s="E"}}}}}return s},getResults:function(y,U){var Q,N,L,w,v,J,V,I,H,z,x,t,Y,S,K,u,G,F,A,C,P,M,D,B=/ "):"")}}return{score:L,message:B,components:t}}});YSLOW.registerRule({id:"yexpires",url:"http://developer.yahoo.com/performance/rules.html#expires",category:["server"],config:{points:11,howfar:172800,types:["css","js","image","cssimage","flash","favicon"]},lint:function(A,C,t){var z,v,B,u,y,w=parseInt(t.howfar,10)*1000,x=[],s=C.getComponentsByType(t.types);for(v=0,y=s.length;vz+w){continue}}x.push(s[v])}u=100-x.length*parseInt(t.points,10);return{score:u,message:(x.length>0)?YSLOW.util.plural("There %are% %num% static component%s%",x.length)+" without a far-future expiration date.":"",components:x}}});YSLOW.registerRule({id:"ycompress",url:"http://developer.yahoo.com/performance/rules.html#gzip",category:["server"],config:{min_filesize:500,types:["doc","iframe","xhr","js","css"],points:11},lint:function(z,A,t){var v,y,u,w,x=[],s=A.getComponentsByType(t.types);for(v=0,y=s.length;v0)?YSLOW.util.plural("There %are% %num% plain text component%s%",x.length)+" that should be sent compressed":"",components:x}}});YSLOW.registerRule({id:"ycsstop",url:"http://developer.yahoo.com/performance/rules.html#css_top",category:["css"],config:{points:10},lint:function(z,A,t){var v,y,u,w,s=A.getComponentsByType("css"),x=[];for(v=0,y=s.length;v0){u-=1+x.length*parseInt(t.points,10)}return{score:u,message:(x.length>0)?YSLOW.util.plural("There %are% %num% stylesheet%s%",x.length)+" found in the body of the document":"",components:x}}});YSLOW.registerRule({id:"yjsbottom",url:"http://developer.yahoo.com/performance/rules.html#js_bottom",category:["javascript"],config:{points:5},lint:function(z,A,t){var v,y,w,u,x=[],s=A.getComponentsByType("js");for(v=0,y=s.length;v0)?YSLOW.util.plural("There %are% %num% JavaScript script%s%",x.length)+" found in the head of the document":"",components:x}}});YSLOW.registerRule({id:"yexpressions",url:"http://developer.yahoo.com/performance/rules.html#css_expressions",category:["css"],config:{points:2},lint:function(A,D,t){var v,y,C,w,B=(D.inline&&D.inline.styles)||[],s=D.getComponentsByType("css"),x=[],u=100,z=0;for(v=0,y=s.length;v0){w.yexpressions=YSLOW.util.plural("%num% expression%s%",C);z+=C;x.push(w)}}for(v=0,y=B.length;v0){x.push("inline <style> tag #"+(v+1)+" ("+YSLOW.util.plural("%num% expression%s%",C)+")");z+=C}}if(z>0){u=90-z*t.points}return{score:u,message:z>0?"There is a total of "+YSLOW.util.plural("%num% expression%s%",z):"",components:x}}});YSLOW.registerRule({id:"yexternal",url:"http://developer.yahoo.com/performance/rules.html#external",category:["javascript","css"],config:{},lint:function(x,z,t){var v,w=z.inline,u=(w&&w.styles)||[],s=(w&&w.scripts)||[],y=[];if(u.length){v=YSLOW.util.plural("There is a total of %num% inline css",u.length);y.push(v)}if(s.length){v=YSLOW.util.plural("There is a total of %num% inline script%s%",s.length);y.push(v)}return{score:"n/a",message:"Only consider this if your property is a common user home page.",components:y}}});YSLOW.registerRule({id:"ydns",url:"http://developer.yahoo.com/performance/rules.html#dns_lookups",category:["content"],config:{max_domains:4,points:5},lint:function(A,C,s){var v,x,u,w=YSLOW.util,y=w.kbSize,z=w.plural,t=100,B=w.summaryByDomain(C.components,["size","size_compressed"],true);if(B.length>s.max_domains){t-=(B.length-s.max_domains)*s.points}if(B.length){for(v=0,x=B.length;v0?" ("+y(u.sum_size_compressed)+" GZip)":"")}}return{score:t,message:(B.length>s.max_domains)?z("The components are split over more than %num% domain%s%",s.max_domains):"",components:B}}});YSLOW.registerRule({id:"yminify",url:"http://developer.yahoo.com/performance/rules.html#minify",category:["javascript","css"],config:{points:10,types:["js","css"]},lint:function(C,E,t){var w,z,u,B,x,A=E.inline,D=(A&&A.styles)||[],v=(A&&A.scripts)||[],s=E.getComponentsByType(t.types),y=[];for(w=0,z=s.length;w0)?YSLOW.util.plural("There %are% %num% component%s% that can be minified",y.length):"",components:y}}});YSLOW.registerRule({id:"yredirects",url:"http://developer.yahoo.com/performance/rules.html#redirects",category:["content"],config:{points:10},lint:function(A,B,t){var w,z,x,u,y=[],v=YSLOW.util.briefUrl,s=B.getComponentsByType("redirect");for(w=0,z=s.length;w0)?YSLOW.util.plural("There %are% %num% redirect%s%",s.length):"",components:y}}});YSLOW.registerRule({id:"ydupes",url:"http://developer.yahoo.com/performance/rules.html#js_dupes",category:["javascript","css"],config:{points:5,types:["js","css"]},lint:function(A,B,u){var x,s,v,z,w={},y=[],t=B.getComponentsByType(u.types);for(x=0,z=t.length;x1){y.push(t[w[x].compindex])}}v=100-y.length*parseInt(u.points,10);return{score:v,message:(y.length>0)?YSLOW.util.plural("There %are% %num% duplicate component%s%",y.length):"",components:y}}});YSLOW.registerRule({id:"yetags",url:"http://developer.yahoo.com/performance/rules.html#etags",category:["server"],config:{points:11,types:["flash","js","css","cssimage","image","favicon"]},lint:function(A,B,t){var v,y,u,w,z,x=[],s=B.getComponentsByType(t.types);for(v=0,y=s.length;v0)?YSLOW.util.plural("There %are% %num% component%s% with misconfigured ETags",x.length):"",components:x}}});YSLOW.registerRule({id:"yxhr",url:"http://developer.yahoo.com/performance/rules.html#cacheajax",category:["content"],config:{points:5,min_cache_time:3600},lint:function(A,C,t){var w,B,z,u,y,v=parseInt(t.min_cache_time,10)*1000,x=[],s=C.getComponentsByType("xhr");for(w=0;wz+v){continue}}x.push(s[w])}u=100-x.length*parseInt(t.points,10);return{score:u,message:(x.length>0)?YSLOW.util.plural("There %are% %num% XHR component%s% that %are% not cacheable",x.length):"",components:x}}});YSLOW.registerRule({id:"yxhrmethod",url:"http://developer.yahoo.com/performance/rules.html#ajax_get",category:["server"],config:{points:5},lint:function(u,y,s){var t,v,x=[],w=y.getComponentsByType("xhr");for(t=0;t0)?YSLOW.util.plural("There %are% %num% XHR component%s% that %do% not use GET HTTP method",x.length):"",components:x}}});YSLOW.registerRule({id:"ymindom",url:"http://developer.yahoo.com/performance/rules.html#min_dom",category:["content"],config:{range:250,points:10,maxdom:900},lint:function(u,w,t){var s=w.domElementsCount,v=100;if(s>t.maxdom){v=99-Math.ceil((s-parseInt(t.maxdom,10))/parseInt(t.range,10))*parseInt(t.points,10)}return{score:v,message:(s>t.maxdom)?YSLOW.util.plural("There %are% %num% DOM element%s% on the page",s):"",components:[]}}});YSLOW.registerRule({id:"yno404",url:"http://developer.yahoo.com/performance/rules.html#no404",category:["content"],config:{points:5,types:["css","js","image","cssimage","flash","xhr","favicon"]},lint:function(z,A,t){var v,y,w,u,x=[],s=A.getComponentsByType(t.types);for(v=0,y=s.length;v0)?YSLOW.util.plural("There %are% %num% request%s% that %are% 404 Not Found",x.length):"",components:x}}});YSLOW.registerRule({id:"ymincookie",url:"http://developer.yahoo.com/performance/rules.html#cookie_size",category:["cookie"],config:{points:10,max_cookie_size:1000},lint:function(w,z,t){var y,v=z.cookies,s=(v&&v.length)||0,u="",x=100;if(s>t.max_cookie_size){y=Math.floor(s/t.max_cookie_size);x-=1+y*parseInt(t.points,10);u=YSLOW.util.plural("There %are% %num% byte%s% of cookies on this page",s)}return{score:x,message:u,components:[]}}});YSLOW.registerRule({id:"ycookiefree",url:"http://developer.yahoo.com/performance/rules.html#cookie_free",category:["cookie"],config:{points:5,types:["js","css","image","cssimage","flash","favicon"]},lint:function(C,D,v){var x,B,w,z,s,A=[],u=YSLOW.util.getHostname,y=u(D.doc_comp.url),t=D.getComponentsByType(v.types);for(x=0,B=t.length;x0)?YSLOW.util.plural("There %are% %num% component%s% that %are% not cookie-free",A.length):"",components:A}}});YSLOW.registerRule({id:"ynofilter",url:"http://developer.yahoo.com/performance/rules.html#no_filters",category:["css"],config:{points:5,halfpoints:2},lint:function(D,G,u){var w,A,v,x,C,z,F,E=(G.inline&&G.inline.styles)||[],t=G.getComponentsByType("css"),y=[],s=0,B=0;for(w=0,A=t.length;w0){t[w].yfilters=YSLOW.util.plural("%num% filter%s%",z);y.push(t[w])}}for(w=0,A=E.length;w0){y.push("inline <style> tag #"+(w+1)+" ("+YSLOW.util.plural("%num% filter%s%",z)+")")}}v=100-(s*u.points+B*u.halfpoints);return{score:v,message:(s+B)>0?"There is a total of "+YSLOW.util.plural("%num% filter%s%",s+B):"",components:y}}});YSLOW.registerRule({id:"yimgnoscale",url:"http://developer.yahoo.com/performance/rules.html#no_scale",category:["images"],config:{points:5},lint:function(u,z,s){var t,y,v,x=[],w=z.getComponentsByType("image");for(t=0;t0)?YSLOW.util.plural("There %are% %num% image%s% that %are% scaled down",x.length):"",components:x}}});YSLOW.registerRule({id:"yfavicon",url:"http://developer.yahoo.com/performance/rules.html#favicon",category:["images"],config:{points:5,size:2000,min_cache_time:3600},lint:function(A,C,t){var z,B,y,v,u,x=[],w=parseInt(t.min_cache_time,10)*1000,s=C.getComponentsByType("favicon");if(s.length){y=s[0];if(parseInt(y.status,10)===404){x.push("Favicon was not found")}if(y.size>t.size){x.push(YSLOW.util.plural("Favicon is more than %num% bytes",t.size))}B=y.expires;if(typeof B==="object"&&typeof B.getTime==="function"){z=new Date().getTime();u=B.getTime()>=z+w}if(!u){x.push("Favicon is not cacheable")}}v=100-x.length*parseInt(t.points,10);return{score:v,message:(x.length>0)?x.join("\n"):"",components:[]}}});YSLOW.registerRule({id:"yemptysrc",url:"http://developer.yahoo.com/performance/rules.html#emptysrc",category:["server"],config:{points:100},lint:function(z,C,t){var y,u,x,A=C.empty_url,w=[],v=[],s="",B=parseInt(t.points,10);u=100;if(A){for(y in A){if(A.hasOwnProperty(y)){x=A[y];u-=x*B;v.push(x+" "+y)}}s=v.join(", ")+YSLOW.util.plural(" component%s% with empty link were found.",v.length)}return{score:u,message:s,components:w}}});YSLOW.registerRuleset({id:"ydefault",name:"YSlow(V2)",rules:{ynumreq:{},ycdn:{},yemptysrc:{},yexpires:{},ycompress:{},ycsstop:{},yjsbottom:{},yexpressions:{},yexternal:{},ydns:{},yminify:{},yredirects:{},ydupes:{},yetags:{},yxhr:{},yxhrmethod:{},ymindom:{},yno404:{},ymincookie:{},ycookiefree:{},ynofilter:{},yimgnoscale:{},yfavicon:{}},weights:{ynumreq:8,ycdn:6,yemptysrc:30,yexpires:10,ycompress:8,ycsstop:4,yjsbottom:4,yexpressions:3,yexternal:4,ydns:3,yminify:4,yredirects:4,ydupes:4,yetags:2,yxhr:4,yxhrmethod:3,ymindom:3,yno404:4,ymincookie:3,ycookiefree:3,ynofilter:4,yimgnoscale:3,yfavicon:2}});YSLOW.registerRuleset({id:"yslow1",name:"Classic(V1)",rules:{ynumreq:{},ycdn:{},yexpires:{},ycompress:{},ycsstop:{},yjsbottom:{},yexpressions:{},yexternal:{},ydns:{},yminify:{types:["js"],check_inline:false},yredirects:{},ydupes:{types:["js"]},yetags:{}},weights:{ynumreq:8,ycdn:6,yexpires:10,ycompress:8,ycsstop:4,yjsbottom:4,yexpressions:3,yexternal:4,ydns:3,yminify:4,yredirects:4,ydupes:4,yetags:2}});YSLOW.registerRuleset({id:"yblog",name:"Small Site or Blog",rules:{ynumreq:{},yemptysrc:{},ycompress:{},ycsstop:{},yjsbottom:{},yexpressions:{},ydns:{},yminify:{},yredirects:{},ydupes:{},ymindom:{},yno404:{},ynofilter:{},yimgnoscale:{},yfavicon:{}},weights:{ynumreq:8,yemptysrc:30,ycompress:8,ycsstop:4,yjsbottom:4,yexpressions:3,ydns:3,yminify:4,yredirects:4,ydupes:4,ymindom:3,yno404:4,ynofilter:4,yimgnoscale:3,yfavicon:2}});var p={};p.getTLD=function(u){var t=u;var s=/\.(gov|ac|mil|net|org|co)\.\w\w$/i;if(u.match(s)){var v=/[\w]+\.[\w]+\.[\w]+$/i;t=u.match(v).toString()}else{var w=/[\w]+\.[\w]+$/i;t=u.match(w).toString()}return t};p.getTextLength=function(u){var v=("script style").split(" ");var t=0;function s(x){if(x.childNodes&&x.childNodes.length>0){for(var w=0;w=(t||0)}p.versionCompare=function(u,w){if(u===undefined){return true}u=u.split(".");var s=r(u[0],w[0]),t=r(u[1],w[1]),v=r(u[2],w[2]);return(!s||s&&!t||s&&t&&!v)};p.isSameDomainTLD=function(t,u,s){if((/^http/).test(s)){if(t===p.getTLD(YSLOW.util.getHostname(s))){return true}else{return false}}else{if(t===p.getTLD(YSLOW.util.getHostname(u))){return true}else{return false}}return false};YSLOW.registerRule({id:"cssprint",name:"Do not load print stylesheets, use @media type print instead",info:"Loading a specific stylesheet for printing slows down the page, even though it is not used",category:["css"],config:{points:20},url:"http://sitespeed.io/rules/#cssprint",lint:function(A,C,v){var y,u,w,s,z=[],x={},t=C.getComponentsByType("css"),B=A.getElementsByTagName("link");for(y=0,len=B.length;y0)?YSLOW.util.plural("There %are% %num% print css files included on the page, that should be @media query instead",z.length):"",components:z}}});YSLOW.registerRule({id:"ttfb",name:"Time to first byte",info:"It is important to have low time to the first byte to be able to render the page fast",category:["server"],config:{points:10,limitInMs:300,hurtEveryMs:100},url:"http://sitespeed.io/rules/#ttfb",lint:function(y,A,u){var x,w=parseInt(u.limitInMs,10),z=parseInt(u.hurtEveryMs,10),v,t,s=A.getComponentsByType("doc");for(x=0,len=s.length;xw){v=100-(Math.ceil((t-w)/z)*parseInt(u.points,10))}}if(v<0){v=0}return{score:v,message:(v<100)?"The TTFB is too slow:"+t+" ms. The limit is "+w+" ms and for every "+z+" ms points are removed":"",components:[""+t]}}});YSLOW.registerRule({id:"cssinheaddomain",name:"Load CSS in head from document domain",info:"Make sure css in head is loaded from same domain as document, in order to have a better user experience and minimize dns lookups",category:["css"],config:{points:10},url:"http://sitespeed.io/rules/#cssinheaddomain",lint:function(D,F,u){var A=D.getElementsByTagName("link"),t=F.getComponentsByType("css"),B,z,s,C={},w=[],x=[],v=100;z=YSLOW.util.getHostname(F.doc_comp.url);for(y=0,len=A.length;yy){continue}}w.push(s[v])}u=100-w.length*parseInt(t.points,10);B=(w.length>0)?YSLOW.util.plural("There %are% %num% static component%s%",w.length)+" without a future expiration date.":"";return{score:u,message:B,components:w}}});YSLOW.registerRule({id:"longexpirehead",name:"Have expires headers equals or longer than one year",info:"All static components of a page should have at least one year expire header. However, analythics scripts will not give you bad points.",url:"http://sitespeed.io/rules/#longexpires",category:["server"],config:{points:5,types:["css","js","image","cssimage","flash","favicon"],skip:["https://secure.gaug.es/track.js","https://ssl.google-analytics.com/ga.js","http://www.google-analytics.com/ga.js"]},lint:function(B,E,t){var A,v,C,u,y,D,x=[],z=[],w=31535000*1000,s=E.getComponentsByType(t.types);for(v=0,y=s.length;vA+w){continue}else{if(t.skip.indexOf(s[v].url)>1){z.push(s[v].url);continue}}}x.push(s[v])}u=100-x.length*parseInt(t.points,10);D=(x.length>0)?YSLOW.util.plural("There %are% %num% static component%s%",x.length)+" without a expire header equal or longer than one year.":"";D+=(z.length>0)?YSLOW.util.plural(" There %are% %num% static component%s% that are skipped from the score calculation",z.length)+":"+z:"";return{score:u,message:D,components:x}}});YSLOW.registerRule({id:"inlinecsswhenfewrequest",name:"Do not load css files when the page has few request",info:"When a page has few requests, it is better to inline the css, to make the page to start render as early as possible",category:["css"],config:{points:20,limit:15,types:["css","js","image","cssimage","flash","favicon"]},url:"http://sitespeed.io/rules/#inlinecsswhenfewrequest",lint:function(v,z,s){var y=z.getComponentsByType(s.types),t=z.getComponentsByType("css"),u="",w=100,x=[];if(y.length0){for(i=0,len=t.length;i1){for(w=0,len=s.length;ws.max_css){x-=(u.length-s.max_css)*s.points_css;v="This page has "+YSLOW.util.plural("%num% external stylesheet%s%",u.length)+". Try combining them into fewer requests.";for(var t=0;ts.max_cssimages){x-=(u.length-s.max_cssimages)*s.points_cssimages;v="This page has "+YSLOW.util.plural("%num% external css image%s%",u.length)+". Try combining them into fewer request.";for(var t=0;tt.max_js){B="There "+YSLOW.util.plural("%are% %num% script%s%",x.length)+" loaded synchronously that could be combined into fewer requests.";u-=(x.length-t.max_js)*parseInt(t.points_js,10)}return{score:u,message:B,components:x}}});YSLOW.registerRule({id:"noduplicates",name:"Remove duplicate JS and CSS",info:"It is bad practice include the same js or css twice",category:["js","css"],config:{},url:"http://developer.yahoo.com/performance/rules.html#js_dupes",lint:function(D,E,u){var y,s,v,C,A,x={},B=[],t=E.getComponentsByType(["js","css"]),w=D.getElementsByTagName("script"),z=D.getElementsByTagName("link");for(y=0,C=w.length;y1){B.push(t[y])}}v=100-B.length*11;return{score:v,message:(B.length>0)?YSLOW.util.plural("There %are% %num% js/css file%s% included more than once on the page",B.length):"",components:B}}});YSLOW.registerRule({id:"mindom",name:"Reduce the number of DOM elements",info:"The number of dom elements are in correlation to if the page is fast or not",url:"http://developer.yahoo.com/performance/rules.html#min_dom",category:["content"],config:{range:250,points:10,maxdom:900},lint:function(u,w,t){var s=w.domElementsCount,v=100;if(s>t.maxdom){v=99-Math.ceil((s-parseInt(t.maxdom,10))/parseInt(t.range,10))*parseInt(t.points,10)}return{score:v,message:(s>t.maxdom)?YSLOW.util.plural("There %are% %num% DOM element%s% on the page",s):"",components:[""+s]}}});YSLOW.registerRule({id:"thirdpartyversions",name:"Always use latest versions of third party javascripts",info:"Unisng the latest versions, will make sure you have the fastest and hopefully leanest javascripts.",url:"http://sitespeed.io/rules/#thirdpartyversions",category:["js"],config:{points:10},lint:function(u,x,s){var t="",v,w=0;if(typeof jQuery=="function"){if(p.versionCompare(jQuery.fn.jquery,[2,0,0])){t="You are using an old version of JQuery: "+jQuery.fn.jquery+" Newer version is faster & better. Upgrade to the newest version from http://jquery.com/";w+=1}}v=100-w*parseInt(s.points,10);return{score:v,message:t,components:[]}}});YSLOW.registerRule({id:"avoidscalingimages",name:"Never scale images in HTML",info:"Always use the correct size for images to avoid that a browser download an image that is larger than necessary.",url:"http://sitespeed.io/rules/#avoidscalingimages",category:["images"],config:{reallyBadLimit:100},lint:function(B,C,t){var D="",u,y=[],x={},A=0,s=C.getComponentsByType("image"),z=B.getElementsByTagName("img");for(var w=0;w0){D=D+" "+v.src+" [browserWidth:"+v.clientWidth+" realImageWidth: "+v.naturalWidth+"]";x[v.src]=1;if((v.clientWidth+t.reallyBadLimit)0)?YSLOW.util.plural("You have %num% image%s% that %are% scaled in the HTML:"+D,y.length):"",components:y}}});YSLOW.registerRule({id:"redirects",name:"Never do redirects",info:"Avoid doing redirects, it will kill you web page on mobile.",url:"http://sitespeed.io/rules/#redirects",category:["content"],config:{points:10},lint:function(t,v,s){var u;u=100-v.redirects.length*parseInt(s.points,10);return{score:u,message:(v.redirects.length>0)?YSLOW.util.plural("There %are% %num% redirect%s%.",v.redirects.length)+" "+v.redirects:"",components:[]}}});YSLOW.registerRuleset({id:"sitespeed.io-desktop",name:"Sitespeed.io desktop rules",rules:{criticalpath:{},spof:{fontFaceInCssSpof:false,inlineFontFaceSpof:false},cssnumreq:{},cssimagesnumreq:{},jsnumreq:{},yemptysrc:{},ycompress:{},ycsstop:{},yjsbottom:{},yexpressions:{},ydns:{},yminify:{},redirects:{},noduplicates:{},yetags:{},yxhr:{},yxhrmethod:{},mindom:{},yno404:{},ymincookie:{},ycookiefree:{},ynofilter:{},avoidscalingimages:{},yfavicon:{},thirdpartyasyncjs:{},cssprint:{},cssinheaddomain:{},syncjsinhead:{},avoidfont:{},totalrequests:{},expiresmod:{},longexpirehead:{},nodnslookupswhenfewrequests:{},inlinecsswhenfewrequest:{},textcontent:{},thirdpartyversions:{}},weights:{criticalpath:15,spof:5,cssnumreq:8,cssimagesnumreq:8,jsnumreq:8,yemptysrc:30,ycompress:8,ycsstop:4,yjsbottom:4,yexpressions:3,ydns:3,yminify:4,redirects:4,noduplicates:4,yetags:2,yxhr:4,yxhrmethod:3,mindom:3,yno404:4,ymincookie:3,ycookiefree:3,ynofilter:4,avoidscalingimages:5,yfavicon:2,thirdpartyasyncjs:10,cssprint:3,cssinheaddomain:8,syncjsinhead:20,avoidfont:1,totalrequests:10,expiresmod:10,longexpirehead:5,nodnslookupswhenfewrequests:8,inlinecsswhenfewrequest:7,textcontent:1,thirdpartyversions:5}});YSLOW.registerRuleset({id:"sitespeed.io-mobile",name:"Sitespeed.io mobile rules",rules:{criticalpath:{},spof:{fontFaceInCssSpof:false,inlineFontFaceSpof:false},cssnumreq:{},cssimagesnumreq:{},jsnumreq:{},yemptysrc:{},ycompress:{},ycsstop:{},yjsbottom:{},yexpressions:{},ydns:{},yminify:{},redirects:{},noduplicates:{},yetags:{},yxhr:{},yxhrmethod:{},mindom:{},yno404:{},ymincookie:{},ycookiefree:{},ynofilter:{},avoidscalingimages:{},yfavicon:{},thirdpartyasyncjs:{},cssprint:{},cssinheaddomain:{},syncjsinhead:{},avoidfont:{},totalrequests:{},expiresmod:{},longexpirehead:{},nodnslookupswhenfewrequests:{},inlinecsswhenfewrequest:{},textcontent:{},thirdpartyversions:{}},weights:{criticalpath:20,spof:5,cssnumreq:8,cssimagesnumreq:8,jsnumreq:8,yemptysrc:30,ycompress:8,ycsstop:4,yjsbottom:4,yexpressions:3,ydns:3,yminify:4,redirects:15,noduplicates:4,yetags:2,yxhr:4,yxhrmethod:3,mindom:3,yno404:4,ymincookie:3,ycookiefree:3,ynofilter:4,avoidscalingimages:10,yfavicon:2,thirdpartyasyncjs:10,cssprint:3,cssinheaddomain:8,syncjsinhead:20,avoidfont:5,totalrequests:14,expiresmod:10,longexpirehead:5,nodnslookupswhenfewrequests:15,inlinecsswhenfewrequest:10,textcontent:1,thirdpartyversions:5}});YSLOW.registerRuleset({id:"sitespeed.io-desktop-http2.0",name:"Sitespeed.io desktop rules for HTTP 2.0",rules:{criticalpath:{},spof:{fontFaceInCssSpof:false,inlineFontFaceSpof:false},yemptysrc:{},ycompress:{},ycsstop:{},yjsbottom:{},yexpressions:{},ydns:{},yminify:{},redirects:{},noduplicates:{},yetags:{},yxhr:{},yxhrmethod:{},mindom:{},yno404:{},ymincookie:{},ycookiefree:{},ynofilter:{},avoidscalingimages:{},yfavicon:{},thirdpartyasyncjs:{},cssprint:{},cssinheaddomain:{},syncjsinhead:{},avoidfont:{},expiresmod:{},longexpirehead:{},textcontent:{},thirdpartyversions:{}},weights:{criticalpath:15,spof:5,yemptysrc:30,ycompress:8,ycsstop:4,yjsbottom:4,yexpressions:3,ydns:3,yminify:4,redirects:4,noduplicates:4,yetags:2,yxhr:4,yxhrmethod:3,mindom:3,yno404:4,ymincookie:3,ycookiefree:3,ynofilter:4,avoidscalingimages:5,yfavicon:2,thirdpartyasyncjs:10,cssprint:3,cssinheaddomain:8,syncjsinhead:20,avoidfont:1,expiresmod:10,longexpirehead:5,textcontent:1,thirdpartyversions:5}});YSLOW.ResultSet=function(u,t,s){this.ruleset_applied=s;this.overall_score=t;this.results=u};YSLOW.ResultSet.prototype={getResults:function(){return this.results},getRulesetApplied:function(){return this.ruleset_applied},getOverallScore:function(){return this.overall_score}};YSLOW.view=function(t,y){var w,v,x,s,u;this.panel_doc=t.document;this.buttonViews={};this.curButtonId="";this.panelNode=t.panelNode;this.loadCSS(this.panel_doc);w=this.panel_doc.createElement("div");w.id="toolbarDiv";w.innerHTML=this.getToolbarSource();w.style.display="block";v=this.panel_doc.createElement("div");v.style.display="block";x='

text

';s=this.panel_doc.createElement("div");s.id="dialogDiv";s.innerHTML=x;s.style.display="none";this.modaldlg=s;this.tooltip=new YSLOW.view.Tooltip(this.panel_doc,t.panelNode);u=this.panel_doc.createElement("div");u.id="copyrightDiv";u.innerHTML=YSLOW.doc.copyright;this.copyright=u;if(t.panelNode){t.panelNode.id="yslowDiv";t.panelNode.appendChild(s);t.panelNode.appendChild(w);t.panelNode.appendChild(v);t.panelNode.appendChild(u)}this.viewNode=v;this.viewNode.id="viewDiv";this.viewNode.className="yui-skin-sam";this.yscontext=y;YSLOW.util.addEventListener(this.panelNode,"click",function(F){var A,B,z,G,C;var E=FBL.getContentView(t.document);var D=E.ysview.getElementByTagNameAndId(t.panelNode,"div","toolbarDiv");if(D){B=E.ysview.getElementByTagNameAndId(D,"li","helpLink");if(B){z=B.offsetLeft;G=B.offsetTop;C=B.offsetParent;while(C){z+=C.offsetLeft;G+=C.offsetTop;C=C.offsetParent}if(F.clientX>=z&&F.clientY>=G&&F.clientX"+A.name+"

"+A.info,B.target)}}}});YSLOW.util.addEventListener(this.panelNode,"mouseout",function(A){var z=FBL.getContentView(t.document);z.ysview.tooltip.hide()});YSLOW.util.addEventListener(this.panel_doc.defaultView||this.panel_doc.parentWindow,"resize",function(B){var A=FBL.getContentView(t.document);var z=A.ysview.modaldlg;if(z&&z.style.display==="block"){z.style.display="none"}})};YSLOW.view.prototype={setDocument:function(s){this.panel_doc=s},loadCSS:function(){},addButtonView:function(s,u){var t=this.getButtonView(s);if(!t){t=this.panel_doc.createElement("div");t.style.display="none";this.viewNode.appendChild(t);this.buttonViews[s]=t}t.innerHTML=u;this.showButtonView(s)},clearAllButtonView:function(){var t=this.buttonViews,u=this.viewNode,s=function(w){if(t.hasOwnProperty(w)){u.removeChild(t[w]);delete t[w]}};s("ysPerfButton");s("ysCompsButton");s("ysStatsButton")},showButtonView:function(t){var s,u=this.getButtonView(t);if(!u){YSLOW.util.dump("ERROR: YSLOW.view.showButtonView: Couldn't find ButtonView '"+t+"'.");return}for(s in this.buttonViews){if(this.buttonViews.hasOwnProperty(s)&&s!==t){this.buttonViews[s].style.display="none"}}if(t==="ysPerfButton"){if(this.copyright){this.copyright.style.display="none"}}else{if(this.curButtonId==="ysPerfButton"){if(this.copyright){this.copyright.style.display="block"}}}u.style.display="block";this.curButtonId=t},getButtonView:function(s){return(this.buttonViews.hasOwnProperty(s)?this.buttonViews[s]:undefined)},setButtonView:function(s,u){var t=this.getButtonView(s);if(!t){YSLOW.util.dump("ERROR: YSLOW.view.setButtonView: Couldn't find ButtonView '"+s+"'.");return}t.innerHTML=u;this.showButtonView(s)},setSplashView:function(t,u,v){var y,x="Grade your web pages with YSlow",z="YSlow gives you:",w="
  • Grade based on the performance (you can define your own rules)
  • Summary of the Components in the page
  • Chart with statistics
  • Tools including Smush.It and JSLint
  • ",s="Learn more about YSlow and YDN";if(YSLOW.doc.splash){if(YSLOW.doc.splash.title){x=YSLOW.doc.splash.title}if(YSLOW.doc.splash.content){if(YSLOW.doc.splash.content.header){z=YSLOW.doc.splash.content.header}if(YSLOW.doc.splash.content.text){w=YSLOW.doc.splash.content.text}}if(YSLOW.doc.splash.more_info){s=YSLOW.doc.splash.more_info}}if(typeof v!=="undefined"){YSLOW.hideToolsInfo=v}else{v=YSLOW.hideToolsInfo}if(v){w=w.replace(/
  • Tools[^<]+<\/li>/,"")}y='

    '+x+'

    '+z+'

      '+w+"
    ";if(typeof t!=="undefined"){YSLOW.hideAutoRun=t}else{t=YSLOW.hideAutoRun}if(!t){y+=''}y+='
    ";this.addButtonView("panel_about",y)},genProgressView:function(){var s='

    Finding components in the page:

    Getting component information:

    start...
    ';this.setButtonView("panel_about",s)},updateProgressView:function(s,x){var w,z,A,y,B,t,u,v,C="";if(this.curButtonId==="panel_about"){B=this.getButtonView(this.curButtonId);if(s==="peel"){w=this.getElementByTagNameAndId(B,"div","peelprogress");z=this.getElementByTagNameAndId(B,"div","progbar");A=this.getElementByTagNameAndId(B,"div","progtext");C=x.message;y=(x.current_step*100)/x.total_step}else{if(s==="fetch"){w=this.getElementByTagNameAndId(B,"div","fetchprogress");z=this.getElementByTagNameAndId(B,"div","progbar2");A=this.getElementByTagNameAndId(B,"div","progtext2");C=x.last_component_url;y=(x.current*100)/x.total}else{if(s==="message"){A=this.getElementByTagNameAndId(B,"div","progtext2");if(A){A.innerHTML=x}return}else{return}}}}if(w&&z&&A){t=w.clientWidth;if(y<0){y=0}if(y>100){y=100}y=100-y;u=(t*y)/100;if(u>t){u=t}v=t-parseInt(u,10);z.style.width=parseInt(u,10)+"px";z.style.left=parseInt(v,10)+"px";A.innerHTML=C}},updateStatusBar:function(z){var B,u,C,v,s,t=YSLOW,w=t.util,y=t.view,A=w.Preference,x=this.yscontext;if(!x.PAGE.statusbar){x.PAGE.statusbar=true;if(!x.PAGE.overallScore){t.controller.lint(z,x)}if(!x.PAGE.totalSize){x.collectStats()}B=w.kbSize(x.PAGE.totalSize);u=w.prettyScore(x.PAGE.overallScore);y.setStatusBar(u,"yslow_status_grade");y.setStatusBar(B,"yslow_status_size");if(A.getPref("optinBeacon",false)){v=A.getPref("beaconInfo","basic"),s=A.getPref("beaconUrl","http://rtblab.pclick.yahoo.com/images/ysb.gif");C=w.getResults(x,v);w.sendBeacon(C,v,s)}}},getRulesetListSource:function(s){var w,u,v="",t=YSLOW.controller.getDefaultRulesetId();for(w in s){if(s[w]){v+='"}}return v},updateRulesetList:function(){var w,y,u,v=this.panel_doc.getElementsByTagName("select"),s=YSLOW.controller.getRegisteredRuleset(),t=this.getRulesetListSource(s),x=function(z){var A=FBL.getContentView(this.ownerDocument);A.ysview.onChangeRuleset(z)};for(w=0;wRulesets ";v+='';v+='";v+='";v+="";return v},show:function(t){var u="html",s="";t=t||this.yscontext.defaultview;if(this.yscontext.component_set===null){YSLOW.controller.run(window.top.content,this.yscontext,false);this.yscontext.defaultview=t}else{if(this.getButtonView(t)){this.showButtonView(t)}else{if("ysCompsButton"===t){s+=this.yscontext.genComponents(u);this.addButtonView("ysCompsButton",s)}else{if("ysStatsButton"===t){s+=this.yscontext.genStats(u);this.addButtonView("ysStatsButton",s);YSLOW.renderer.plotComponents(this.getButtonView("ysStatsButton"),this.yscontext)}else{if("ysToolButton"===t){s+=this.yscontext.genToolsView(u);this.addButtonView("ysToolButton",s)}else{s+=this.yscontext.genPerformance(u);this.addButtonView("ysPerfButton",s)}}}}this.panelNode.scrollTop=0;this.panelNode.scrollLeft=0;this.updateStatusBar(this.yscontext.document);this.updateToolbarSelection()}},updateToolbarSelection:function(){var s,t,u;switch(this.curButtonId){case"ysCompsButton":case"ysPerfButton":case"ysStatsButton":case"ysToolButton":s=this.getElementByTagNameAndId(this.panelNode,"li",this.curButtonId);if(s){if(s.className.indexOf("selected")!==-1){return}else{s.className+=" selected";if(s.previousSibling){s.previousSibling.className+=" off"}}}break;default:break}t=this.getElementByTagNameAndId(this.panelNode,"ul","toolbarLinks");u=t.firstChild;while(u){if(u.id!==this.curButtonId&&u.className.indexOf("selected")!==-1){this.unselect(u);if(u.previousSibling){YSLOW.view.removeClassName(u.previousSibling,"off")}}u=u.nextSibling}},showPerformance:function(){this.show("ysPerfButton")},showStats:function(){this.show("ysStatsButton")},showComponents:function(){this.show("ysCompsButton")},showTools:function(){this.show("ysToolButton")},showRuleSettings:function(){var s=this.yscontext.genRulesetEditView("html");this.addButtonView("ysRuleEditButton",s);this.panelNode.scrollTop=0;this.panelNode.scrollLeft=0;this.updateToolbarSelection()},runTest:function(){YSLOW.controller.run(window.top.content,this.yscontext,false)},setAutorun:function(s){YSLOW.util.Preference.setPref("extensions.yslow.autorun",s)},setAntiIframe:function(s){YSLOW.antiIframe=s},addCDN:function(B){var v,t,x=this,z=document,C=x.yscontext,D=YSLOW.util.Preference,y=D.getPref("cdnHostnames",""),s=x.panel_doc,u=s.getElementById("tab-label-list"),A=u.getElementsByTagName("li"),w=A.length;if(y){y=y.replace(/\s+/g,"").split(",");y.push(B);y=y.join()}else{y=B}D.setPref("extensions.yslow.cdnHostnames",y);for(v=0;v-1){t=u.id;break}}YSLOW.controller.lint(C.document,C);x.addButtonView("ysPerfButton",C.genPerformance("html"));YSLOW.view.restoreStatusBar(C);x.updateToolbarSelection();u=s.getElementById(t);x.onclickTabLabel({currentTarget:u},true)},onChangeRuleset:function(v){var w,t,x,y,s=YSLOW.util.getCurrentTarget(v),u=s.options[s.selectedIndex];YSLOW.controller.setDefaultRuleset(u.value);w=s.ownerDocument;t="Do you want to run the selected ruleset now?";x="Run Test";y=function(A){var z;w.ysview.closeDialog(w);if(w.yslowContext.component_set===null){YSLOW.controller.run(w.yslowContext.document.defaultView||w.yslowContext.document.parentWindow,w.yslowContext,false)}else{YSLOW.controller.lint(w.yslowContext.document,w.yslowContext)}z=w.yslowContext.genPerformance("html");w.ysview.addButtonView("ysPerfButton",z);w.ysview.panelNode.scrollTop=0;w.ysview.panelNode.scrollLeft=0;YSLOW.view.restoreStatusBar(w.yslowContext);w.ysview.updateToolbarSelection()};this.openDialog(w,389,150,t,undefined,x,y)},onclickTabLabel:function(s,y){var u,t,v,A,C,B,x,D=YSLOW.util.getCurrentTarget(s),z=D.parentNode,w=z.nextSibling;if(D.className.indexOf("selected")!==-1||D.id.indexOf("label")===-1){return false}if(z){u=z.firstChild;while(u){if(this.unselect(u)){t=u.id.substring(5);break}u=u.nextSibling}D.className+=" selected";v=D.id.substring(5);u=w.firstChild;while(u){x=u.id.substring(3);if(!A&&t&&x===t){if(u.className.indexOf("yui-hidden")===-1){u.className+=" yui-hidden"}A=true}if(!C&&v&&x===v){YSLOW.view.removeClassName(u,"yui-hidden");C=true;B=u}if((A||!t)&&(C||!v)){break}u=u.nextSibling}if(y===true&&C===true&&B){this.positionResultTab(B,w,D)}}return false},positionResultTab:function(t,s,z){var w,B,C,x=5,A=this.panel_doc,v=A.defaultView||A.parentWindow,u=v.offsetHeight?v.offsetHeight:v.innerHeight,D=z.offsetTop+t.offsetHeight;s.style.height=D+"px";t.style.position="absolute";t.style.left=z.offsetLeft+z.offsetWidth+"px";t.style.top=z.offsetTop+"px";w=t.offsetTop;B=t.offsetParent;while(B!==null){w+=B.offsetTop;B=B.offsetParent}if(wthis.panelNode.scrollTop+u){if(ww-this.panelNode.scrollTop){C=w-this.panelNode.scrollTop}this.panelNode.scrollTop+=C}}},onclickResult:function(s){YSLOW.util.preventDefault(s);return this.onclickTabLabel(s,true)},unselect:function(s){return YSLOW.view.removeClassName(s,"selected")},filterResult:function(B,s){var x,w,t,y,u,z,v,A=this.getButtonView("ysPerfButton");if(s==="all"){w=true}if(A){x=this.getElementByTagNameAndId(A,"ul","tab-label-list")}if(x){t=x.firstChild;v=x.nextSibling;u=v.firstChild;while(t){YSLOW.view.removeClassName(t,"first");if(w||t.className.indexOf(s)!==-1){t.style.display="block";if(y===undefined){y=u;z=t;YSLOW.view.removeClassName(u,"yui-hidden");t.className+=" first";if(t.className.indexOf("selected")===-1){t.className+=" selected"}t=t.nextSibling;u=u.nextSibling;continue}}else{t.style.display="none"}if(u.className.indexOf("yui-hidden")===-1){u.className+=" yui-hidden"}this.unselect(t);t=t.nextSibling;u=u.nextSibling}if(y){this.positionResultTab(y,v,z)}}},updateFilterSelection:function(u){var s,t=YSLOW.util.getCurrentTarget(u);YSLOW.util.preventDefault(u);if(t.className.indexOf("selected")!==-1){return}t.className+=" selected";s=t.parentNode.firstChild;while(s){if(s!==t&&this.unselect(s)){break}s=s.nextSibling}this.filterResult(t.ownerDocument,t.id)},onclickToolbarMenu:function(s){var w,u=YSLOW.util.getCurrentTarget(s),v=u.parentNode,t=v.parentNode;if(v.className.indexOf("selected")!==-1){return}v.className+=" selected";if(v.previousSibling){v.previousSibling.className+=" off"}if(t){w=t.firstChild;while(w){if(w!==v&&this.unselect(w)){if(w.previousSibling){YSLOW.view.removeClassName(w.previousSibling,"off")}break}w=w.nextSibling}}},expandCollapseComponentType:function(w,t){var u,v=YSLOW.controller.getRenderer("html"),s=this.getButtonView("ysCompsButton");if(s){u=this.getElementByTagNameAndId(s,"table","components-table");v.expandCollapseComponentType(w,u,t)}},expandAll:function(v){var t,u=YSLOW.controller.getRenderer("html"),s=this.getButtonView("ysCompsButton");if(s){t=this.getElementByTagNameAndId(s,"table","components-table");u.expandAllComponentType(v,t)}},regenComponentsTable:function(x,w,t){var u,v=YSLOW.controller.getRenderer("html"),s=this.getButtonView("ysCompsButton");if(s){u=this.getElementByTagNameAndId(s,"table","components-table");v.regenComponentsTable(x,u,w,t,this.yscontext.component_set)}},showComponentHeaders:function(u){var t,v,s=this.getButtonView("ysCompsButton");if(s){t=this.getElementByTagNameAndId(s,"tr",u);if(t){v=t.firstChild;if(t.style.display==="none"){t.style.display="table-row"}else{t.style.display="none"}}}},openLink:function(s){YSLOW.util.openLink(s)},openPopup:function(u,t,w,s,v){window.open(u,t||"_blank","width="+(w||626)+",height="+(s||436)+","+(v||"toolbar=0,status=1,location=1,resizable=1"))},runTool:function(s,t){YSLOW.controller.runTool(s,this.yscontext,t)},onclickRuleset:function(x){var u,t,s,w,y=YSLOW.util.getCurrentTarget(x),v=y.className.indexOf("ruleset-");YSLOW.util.preventDefault(x);if(v!==-1){t=y.className.indexOf(" ",v+8);if(t!==-1){u=y.className.substring(v+8,t)}else{u=y.className.substring(v+8)}s=this.getButtonView("ysRuleEditButton");if(s){w=this.getElementByTagNameAndId(s,"form","edit-form");YSLOW.renderer.initRulesetEditForm(y.ownerDocument,w,YSLOW.controller.getRuleset(u))}}return this.onclickTabLabel(x,false)},openSaveAsDialog:function(u,t){var s='',v="Save",w=function(D){var A,y,x,B,z,C=YSLOW.util.getCurrentTarget(D).ownerDocument;if(C.ysview.modaldlg){A=C.ysview.getElementByTagNameAndId(C.ysview.modaldlg,"input","saveas-name")}if(A){if(YSLOW.controller.checkRulesetName(A.value)===true){y=s+'
    '+A.value+" ruleset already exists.
    ";C.ysview.closeDialog(C);C.ysview.openDialog(C,389,150,y,"",v,w)}else{x=C.ysview.getButtonView("ysRuleEditButton");if(x){B=C.ysview.getElementByTagNameAndId(x,"form",t);z=C.createElement("input");z.type="hidden";z.name="saveas-name";z.value=A.value;B.appendChild(z);B.submit()}C.ysview.closeDialog(C)}}};this.openDialog(u,389,150,s,undefined,v,w)},openPrintableDialog:function(v){var u="Please run YSlow first before using Printable View.",t="Check which information you want to view or print
    ",s='
    ',w="Ok",x=function(A){var y,z=YSLOW.util.getCurrentTarget(A).ownerDocument,z=FBL.getContentView(z);aInputs=z.getElementsByName("print-type"),print_type={};for(y=0;y0){for(s=0;s0){if(L[G].className==="dialog-box"){I=L[G]}else{if(L[G].className==="dialog-text"){B=L[G]}else{if(L[G].className==="dialog-more-text"){A=L[G]}}}}}if(J&&I&&B&&A){B.innerHTML=(z?z:"");A.innerHTML=(y?y:"");t=J.getElementsByTagName("input");for(F=0;F0)?u:225)+"px";I.style.top=((C&&C>0)?C:80)+"px";J.style.left=this.panelNode.scrollLeft+"px";J.style.top=this.panelNode.scrollTop+"px";J.style.display="block";if(t.length>0){t[0].focus()}}},closeDialog:function(t){var s=this.modaldlg;s.style.display="none"},saveRuleset:function(w,t){var u,v=YSLOW.controller.getRenderer("html"),s=this.getButtonView("ysRuleEditButton");if(s){u=this.getElementByTagNameAndId(s,"form",t);v.saveRuleset(w,u)}},deleteRuleset:function(w,t){var u,v=YSLOW.controller.getRenderer("html"),s=this.getButtonView("ysRuleEditButton");if(s){u=this.getElementByTagNameAndId(s,"form",t);v.deleteRuleset(w,u)}},shareRuleset:function(x,v){var s,z,w,A,y,t=YSLOW.controller.getRenderer("html"),u=this.getButtonView("ysRuleEditButton");if(u){s=this.getElementByTagNameAndId(u,"form",v);z=t.getEditFormRulesetId(s);w=YSLOW.controller.getRuleset(z);if(w){A=YSLOW.Exporter.exportRuleset(w);if(A){y="";this.openDialog(x,389,150,y,"","Ok")}}}},createRuleset:function(u,t){var s,v,x=u.parentNode,w=x.parentNode,y=w.firstChild;while(y){this.unselect(y);y=y.nextSibling}s=this.getButtonView("ysRuleEditButton");if(s){v=this.getElementByTagNameAndId(s,"form",t);YSLOW.renderer.initRulesetEditForm(this.panel_doc,v)}},showHideHelp:function(){var s,t=this.getElementByTagNameAndId(this.panelNode,"div","toolbarDiv");if(t){s=this.getElementByTagNameAndId(t,"div","helpDiv")}if(s){if(s.style.visibility==="visible"){s.style.visibility="hidden"}else{s.style.visibility="visible"}}},smushIt:function(t,s){YSLOW.util.smushIt(s,function(y){var w,v,x,z,u="";if(y.error){u+="
    "+y.error+"
    "}else{x=YSLOW.util.getSmushUrl();z=YSLOW.util.makeAbsoluteUrl(y.dest,x);u+="
    Original size: "+y.src_size+" bytes
    Result size: "+y.dest_size+" bytes
    % Savings: "+y.percent+"%
    "}w='
    Image: '+YSLOW.util.briefUrl(s,250)+"
    ";v=u;t.ysview.openDialog(t,389,150,w,v,"Ok")})},checkAllRules:function(x,u,t){var v,s,w,y;if(typeof t!=="boolean"){return}s=this.getButtonView("ysRuleEditButton");if(s){w=this.getElementByTagNameAndId(s,"form",u);y=w.elements;for(v=0;vF||J>s){this.tooltip.style.display="none";return}H=A.offsetParent;while(H!==null){D+=H.offsetLeft;C+=H.offsetTop;H=H.offsetParent}D+=A.offsetLeft;C+=A.offsetTop;if(DG.ysview.panelNode.scrollTop+s)){this.tooltip.style.display="none";return}v=D+A.offsetWidth/2;u=C+A.offsetHeight/2;if(D+A.offsetWidth+E+w=G.ysview.panelNode.scrollTop)&&(C-E+J+E<=G.ysview.panelNode.scrollTop+s)){C=C-E;t="right top"}else{C+=A.offsetHeight-J;t="right bottom"}}else{if(C-J-E>=G.ysview.panelNode.scrollTop){C-=J+E;t="top"}else{C+=A.offsetHeight+E;t="bottom"}B=Math.floor(v-w/2);if((B>=G.ysview.panelNode.scrollLeft)&&(B+w<=G.ysview.panelNode.scrollLeft+F)){D=B}else{if(B0&&s&&s.length>0){v=u.className.split(" ");for(t=0;t0){A[s]=y[s]}E+=y[s]}}return{total_size:E,num_requests:t,count_obj:x,size_obj:y,canvas_data:A}},collectStats:function(){var s=this.computeStats();if(s!==undefined){this.PAGE.totalSize=s.total_size;this.PAGE.totalRequests=s.num_requests;this.PAGE.totalObjCount=s.count_obj;this.PAGE.totalObjSize=s.size_obj;this.PAGE.canvas_data.empty=s.canvas_data}s=this.computeStats(true);if(s){this.PAGE.totalSizePrimed=s.total_size;this.PAGE.totalRequestsPrimed=s.num_requests;this.PAGE.totalObjCountPrimed=s.count_obj;this.PAGE.totalObjSizePrimed=s.size_obj;this.PAGE.canvas_data.primed=s.canvas_data}},genPerformance:function(t,s){if(this.result_set===null){if(!s){s=this.document}YSLOW.controller.lint(s,this)}return YSLOW.controller.render(t,"reportcard",{result_set:this.result_set})},genStats:function(t){var s={};if(!this.PAGE.totalSize){this.collectStats()}s.PAGE=this.PAGE;return YSLOW.controller.render(t,"stats",{stats:s})},genComponents:function(s){if(!this.PAGE.totalSize){this.collectStats()}return YSLOW.controller.render(s,"components",{comps:this.component_set.components,total_size:this.PAGE.totalSize})},genToolsView:function(t){var s=YSLOW.Tools.getAllTools();return YSLOW.controller.render(t,"tools",{tools:s})},genRulesetEditView:function(s){return YSLOW.controller.render(s,"rulesetEdit",{rulesets:YSLOW.controller.getRegisteredRuleset()})}};YSLOW.renderer={sortBy:"type",sortDesc:false,bPrintable:false,colors:{doc:"#8963df",redirect:"#FC8C8C",iframe:"#FFDFDF",xhr:"#89631f",flash:"#8D4F5B",js:"#9fd0e8",css:"#aba5eb",cssimage:"#677ab8",image:"#d375cd",favicon:"#a26c00",unknown:"#888888"},reset:function(){this.sortBy="type";this.sortDesc=false},genStats:function(A,B){var x,y,u,t,D,z,s,w,v="",C=0;if(!A.PAGE){return""}if(B){x=A.PAGE.totalObjCountPrimed;y=A.PAGE.totalObjSizePrimed;u=A.PAGE.totalRequestsPrimed;C=A.PAGE.totalSizePrimed}else{x=A.PAGE.totalObjCount;y=A.PAGE.totalObjSize;u=A.PAGE.totalRequests;C=A.PAGE.totalSize}t=YSLOW.peeler.types;D=(B)?"primed":"empty";for(z=0;z
     
    '+x[s]+''+YSLOW.util.prettyType(s)+''+YSLOW.util.kbSize(y[s])+""}}w='
    HTTP Requests - '+u+'
    Total Weight - '+YSLOW.util.kbSize(C)+'
    '+v+"
    ";return w},plotComponents:function(s,t){if(typeof s!=="object"){return}this.plotOne(s,t.PAGE.canvas_data.empty,t.PAGE.totalSize,"comp-canvas-empty");this.plotOne(s,t.PAGE.canvas_data.primed,t.PAGE.totalSizePrimed,"comp-canvas-primed")},plotOne:function(C,v,B,A){var u,w,E,t,z,s,y,F,x,D=C.getElementsByTagName("canvas");for(w=0;w'+YSLOW.util.escapeHtml(s.headers[u])+""}}if(s.req_headers){t+='Request Headers';for(u in s.req_headers){if(s.req_headers.hasOwnProperty(u)&&s.req_headers[u]){t+=''+YSLOW.util.escapeHtml(YSLOW.util.formatHeaderName(u))+'

    '+YSLOW.util.escapeHtml(s.req_headers[u])+"

    "}}}t+="";return t},genComponentRow:function(y,x,t,w){var u,C,v,s,B,A,z;if(typeof t!=="string"){t=""}if(x.status>=400&&x.status<500){t+=" compError"}if(x.after_onload===true){t+=" afteronload"}u="compHeaders"+x.id;C='";for(v in y){if(y.hasOwnProperty(v)){s=v;B="";if(v==="type"){B+=x[v];if(x.is_beacon){B+=" †"}if(x.after_onload){B+=" *"}}else{if(v==="size"){B+=YSLOW.util.kbSize(x.size)}else{if(v==="url"){if(x.status>=400&&x.status<500){C+=''+x[v]+" (status: "+x.status+")";continue}else{B+=YSLOW.util.prettyAnchor(x[v],x[v],undefined,!YSLOW.renderer.bPrintable,100,1,x.type)}}else{if(v==="gzip"&&(x.compressed==="gzip"||x.compressed==="deflate")){B+=(x.size_compressed!==undefined?YSLOW.util.kbSize(x.size_compressed):"uncertain")}else{if(v==="set-cookie"){A=x.getSetCookieSize();B+=A>0?A:""}else{if(v==="cookie"){z=x.getReceivedCookieSize();B+=z>0?z:""}else{if(v==="etag"){B+=x.getEtag()}else{if(v==="expires"){B+=YSLOW.util.prettyExpiresDate(x.expires)}else{if(v==="headers"){if(YSLOW.renderer.bPrintable){continue}if(x.raw_headers&&x.raw_headers.length>0){B+="'}}else{if(v==="action"){if(YSLOW.renderer.bPrintable){continue}if(x.type==="cssimage"||x.type==="image"){if(x.response_type===undefined||x.response_type==="image"){B+="smush.it"}}}else{if(x[v]!==undefined){B+=x[v]}}}}}}}}}}}C+=''+B+""}}C+="";if(x.raw_headers&&x.raw_headers.length>0){C+=''+this.getComponentHeadersTable(x)+""}return C},componentSortCallback:function(s,A){var t,v,w,y="",x="",z=YSLOW.renderer.sortBy,u=YSLOW.renderer.sortDesc;switch(z){case"type":y=s.type;x=A.type;break;case"size":y=s.size?Number(s.size):0;x=A.size?Number(A.size):0;break;case"gzip":y=s.size_compressed?Number(s.size_compressed):0;x=A.size_compressed?Number(A.size_compressed):0;break;case"set-cookie":y=s.getSetCookieSize();x=A.getSetCookieSize();break;case"cookie":y=s.getReceivedCookieSize();x=A.getReceivedCookieSize();break;case"headers":break;case"url":y=s.url;x=A.url;break;case"respTime":y=s.respTime?Number(s.respTime):0;x=A.respTime?Number(A.respTime):0;break;case"etag":y=s.getEtag();x=A.getEtag();break;case"action":if(s.type==="cssimage"||s.type==="image"){y="smush.it"}if(A.type==="cssimage"||A.type==="image"){x="smush.it"}break;case"expires":y=s.expires||0;x=A.expires||0;break}if(y===x){if(s.id>A.id){return(u)?-1:1}if(s.idx){return(u)?-1:1}if(y',x='
    ',v='
    ';for(s in C){if(C.hasOwnProperty(s)&&C[s]){A=C[s];u='
    ";if(B.rules[s]!==undefined){D+=1}if(B.weights!==undefined&&B.weights[s]!==undefined){y+=''}t=(w%3);switch(t){case 0:z+=u;break;case 1:x+=u;break;case 2:v+=u;break}w+=1}}z+="
    ";x+="
    ";v+="";return'

    '+B.name+' Ruleset (includes '+parseInt(D,10)+" of "+parseInt(w,10)+' rules)

    '+z+""+x+""+v+'
    '+y+"
    "},genRulesetEditForm:function(s){var t="";t+='
    '+YSLOW.renderer.genRulesCheckbox(s)+'
    ';return t},initRulesetEditForm:function(K,t,F){var E,H,G,D,I,M,y,L,x,A,s,w,C,u=t.elements,v="",B=[],z=0,J=0;for(H=0;H'}z+=1}}s.innerHTML="(includes "+parseInt(z,10)+" of "+parseInt(J,10)+" rules)";M.innerHTML='';y.innerHTML='';L.innerHTML=F.name}else{M.innerHTML="";y.innerHTML="";L.innerHTML="New";s.innerHTML=""}x.innerHTML=v}};YSLOW.registerRenderer({id:"html",supports:{components:1,reportcard:1,stats:1,tools:1,rulesetEdit:1},genComponentsTable:function(t,E,x){var C,A,D,B,w={type:"TYPE",size:"SIZE
    (KB)",gzip:"GZIP
    (KB)","set-cookie":"COOKIE RECEIVED
    (bytes)",cookie:"COOKIE SENT
    (bytes)",headers:"HEADERS",url:"URL",expires:"EXPIRES
    (Y/M/D)",respTime:"RESPONSE
    TIME (ms)",etag:"ETAG",action:"ACTION"},z=false,v="",y="",s=0,u=0;if(E!==undefined&&w[E]===undefined){return""}if(YSLOW.renderer.bPrintable){E=YSLOW.renderer.sortBy;x=YSLOW.renderer.sortDesc}else{if(E===undefined||E==="type"){E="type";z=true}}t=YSLOW.renderer.sortComponents(t,E,x);v+='';for(C in w){if(w.hasOwnProperty(C)&&w[C]){if(YSLOW.renderer.bPrintable&&(C==="action"||C==="components"||C==="headers")){continue}v+="'+(E===C?(x?"↓":"↑"):"")+" "+w[C]+""}}}v+="";for(A=0;A";v+=y;y="";s=0;u=0;D=B.type}}y+=YSLOW.renderer.genComponentRow(w,B,(s%2===0?"even":"odd"),z);s+=1;u+=B.size}else{v+=YSLOW.renderer.genComponentRow(w,B,(A%2===0?"even":"odd"),false)}}if(y.length>0){v+='";v+=y}v+="
    '+D+" ("+s+')'+YSLOW.util.kbSize(u)+"
    '+D+" ("+s+')'+YSLOW.util.kbSize(u)+"
    ";return v},componentsView:function(y,t){var v,u=this.genComponentsTable(y,YSLOW.renderer.sortBy,false),w="in type column indicates the component is loaded after window onload event.",s="denotes 1x1 pixels image that may be image beacon",x="Components";if(YSLOW.doc){if(YSLOW.doc.components_legend){if(YSLOW.doc.components_legend.beacon){w=YSLOW.doc.components_legend.beacon}if(YSLOW.doc.components_legend.after_onload){s=YSLOW.doc.components_legend.after_onload}}if(YSLOW.doc.view_names&&YSLOW.doc.view_names.components){x=YSLOW.doc.view_names.components}}v='
    '+x+'The page has a total of '+y.length+' components and a total weight of '+YSLOW.util.kbSize(t)+' bytes
    '+u+'
    * '+w+"
    † "+s+"
    ";return v},reportcardPrintableView:function(v,t,y){var w,u,A,s,z,x='
    ";for(w=0;w"}}x+="
    Overall Grade: '+t+" (Ruleset applied: "+y.name+")
    '+s+'

    '+A.name+'

    '+A.message+"
    ";if(A.components&&A.components.length>0){x+='
      ';for(u=0;u"+A.components[u]+""}else{if(A.components[u].url!==undefined){x+="
    • "+YSLOW.util.briefUrl(A.components[u].url,60)+"
    • "}}}x+="

    "}x+="

    ";return x},getFilterCode:function(z,w,u,s){var y,t,x,A,D,E,v,C=w.length,B=[];for(t in z){if(z.hasOwnProperty(t)&&z[t]){B.push(t)}}B.sort();y='
    • ALL ('+C+')
    • FILTER BY:
    • ';for(x=0,A=B.length;x'+B[x].toUpperCase()+" ("+z[B[x]]+")"}D="http://yslow.org/scoremeter/?url="+encodeURIComponent(s)+"&grade="+u;for(x=0;x=0&&v<100){D+="&"+E.rule_id.toLowerCase()+"="+v}}D=encodeURIComponent(encodeURIComponent(D));s=encodeURIComponent(encodeURIComponent(s.slice(0,60)+(s.length>60?"...":"")));y+='";y+='";y+="
    ";return y},reportcardView:function(u){var y,J,H,G,z,t,x,N,D,K,M,L,s,w,A='
    ',I=u.getRulesetApplied(),C=u.getResults(),v=u.url,O="Grade",F="",E="",B={};if(YSLOW.doc){if(YSLOW.doc.view_names&&YSLOW.doc.view_names.grade){O=YSLOW.doc.view_names.grade}}y=YSLOW.util.prettyScore(u.getOverallScore());if(YSLOW.renderer.bPrintable){return this.reportcardPrintableView(C,y,I)}A+='
    '+O+'
    '+y+'
    Overall performance score '+Math.round(u.getOverallScore())+'Ruleset applied: '+I.name+'URL: '+YSLOW.util.briefUrl(v,100)+"
    ";for(J=0;J0){N+=" "}N+=z.category[G];if(B[z.category[G]]===undefined){B[z.category[G]]=0}B[z.category[G]]+=1}}if(N.length>0){F+=' class="'+N+'"'}F+=' onclick="javascript:document.ysview.onclickResult(event)">
    '+t+''+z.name+"
  • ";E+='
    ")}E+='">

    Grade '+t+" on "+z.name+"

    "+z.message+"
    ";if(z.components&&z.components.length>0){E+='

      ';for(H=0;H"+L+""}else{if(L.url!==undefined){E+="
    • ";s=z.rule_id.toLowerCase();if(z.rule_id.match("expires")){E+="("+YSLOW.util.prettyExpiresDate(L.expires)+") "}E+=YSLOW.util.prettyAnchor(L.url,L.url,undefined,true,120,undefined,L.type)+"
    • "}}}E+="

    "}E+="

    ";w=YSLOW.controller.getRule(z.rule_id);if(w){E+='

    '+(w.info||"** To be added **")+"

    ";if(w.url!==undefined){E+='

    »Read More

    "}}E+="
    "}}A+='
    '+this.getFilterCode(B,C,y,v)+'
      '+F+'
    '+E+'
    '+YSLOW.doc.copyright+"
    ";return A},statsView:function(t){var s="",u="Stats";if(YSLOW.doc){if(YSLOW.doc.view_names&&YSLOW.doc.view_names.stats){u=YSLOW.doc.view_names.stats}}s+='
    '+u+'The page has a total of '+t.PAGE.totalRequests+' HTTP requests and a total weight of '+YSLOW.util.kbSize(t.PAGE.totalSize)+" bytes with empty cache
    ";s+='
    WEIGHT GRAPHS
    ';s+='
    Empty Cache
    '+YSLOW.renderer.genStats(t,false)+"
    ";s+='
    Primed Cache
    '+YSLOW.renderer.genStats(t,true)+"
    ";s+="
    ";return s},toolsView:function(w){var v,u,t,s="",y="Tools",x="Click the Launch Tool link next to the tool you want to run to start the tool.";if(YSLOW.doc){if(YSLOW.doc.tools_desc){x=YSLOW.doc.tools_desc}if(YSLOW.doc.view_names&&YSLOW.doc.view_names.tools){y=YSLOW.doc.view_names.tools}}for(v=0;v"}s+="
    "+t.name+"-"+(t.short_desc||"Short text here explaining what are the main benefits of running this App")+"
    ";u='
    '+y+""+x+'
    '+s+"
    ";return u},rulesetEditView:function(F){var u,D,w,v,y='
    ',E,B,A=0,t=false,s,x,C="Rule Settings",z="Choose which ruleset better fit your specific needs. You can Save As an existing rule, based on an existing ruleset.";if(YSLOW.doc){if(YSLOW.doc.rulesettings_desc){z=YSLOW.doc.rulesettings_desc}if(YSLOW.doc.view_names&&YSLOW.doc.view_names.rulesetedit){C=YSLOW.doc.view_names.rulesetedit}}x=YSLOW.controller.getDefaultRulesetId();E='
    • STANDARD SETS
    • ';for(u in F){if(F.hasOwnProperty(u)&&F[u]){D=F[u];w="tab"+A;if(!t&&D.custom===true){E+='
    • CUSTOM SETS
    • ';t=true}E+='
    • '+D.name+"
    • ";A+=1}}E+='
    ';B='
    '+YSLOW.renderer.genRulesetEditForm(s)+"
    ";y+=E+B;v='
    '+C+""+z+"
    "+y+"
    ";return v},rulesetEditUpdateTab:function(K,s,E,t,C){var u,D,H,w,B,I,F,z,v,A,J,G,x,y=s.parentNode.parentNode.parentNode;if(y&&y.id==="settingsDiv"&&E.custom===true){u=y.firstChild;D=u.nextSibling;if(t<1){H=u.firstChild;while(H){w=H.className.indexOf("ruleset-");if(w!==-1){B=H.className.substring(w+8);w=B.indexOf(" ");if(w!==-1){B=B.substring(0,w)}if(E.id===B){w=H.id.indexOf("label");if(w!==-1){I=H.id.substring(w+5);if(H.className.indexOf("selected")!==-1){F={};F.currentTarget=J;K.ysview.onclickRuleset(F)}if(H.previousSibling&&H.previousSibling.id==="custom-set-title"&&H.nextSibling&&H.nextSibling.id==="create-ruleset"){z=H.previousSibling}u.removeChild(H);if(z){u.removeChild(z)}}break}else{J=H}}H=H.nextSibling}}else{H=u.lastChild;while(H){A=H.id.indexOf("label");if(A!==-1){v=H.id.substring(A+5);break}H=H.previousSibling}v=Number(v)+1;H=K.createElement("li");H.className="ruleset-"+E.id;H.id="label"+v;H.onclick=function(L){K.ysview.onclickRuleset(L)};H.innerHTML=''+E.name+"";u.insertBefore(H,u.lastChild);G=u.firstChild;while(G){if(G.id&&G.id==="custom-set-title"){z=G;break}G=G.nextSibling}if(!z){z=K.createElement("li");z.className="new-section header";z.id="custom-set-title";z.innerHTML="CUSTOM SETS";u.insertBefore(z,H)}if(C){x={};x.currentTarget=H;K.ysview.onclickRuleset(x)}}}},hasClassName:function(v,s){var t,u=v.split(" ");if(u){for(t=0;t";u+=""+w[v].type+"";u+=""+w[v].size+"";if(w[v].compressed===false){u+=""}else{u+=""+(w[v].size_compressed!==undefined?parseInt(w[v].size_compressed,10):"uncertain")+""}s=w[v].getSetCookieSize();if(s>0){u+=""+parseInt(s,10)+""}s=w[v].getReceivedCookieSize();if(s>0){u+=""+parseInt(s,10)+""}u+=""+encodeURI(w[v].url)+"";u+=""+w[v].expires+"";u+=""+w[v].respTime+"";u+=""+w[v].getEtag()+"";u+=""}u+="";return u},reportcardView:function(y){var v,t,A,w=y.getOverallScore(),u=YSLOW.util.prettyScore(w),z=y.getRulesetApplied(),x=y.getResults(),s='';s+='';for(v=0;v';s+=""+A.message+"";if(x.components&&x.components.length>0){s+="";for(t=0;t"+A.components[t]+""}else{if(A.components[t].url!==undefined){s+=""+A.components[t].url+""}}}s+=""}s+=""}s+="";return s},statsView:function(v){var u,y,t,x='',w='',s=YSLOW.peeler.types;for(u=0;u'}if((v.PAGE.totalObjCount[y])!==undefined){w+=''}}x+="";w+="";t=''+x+w+"";return t}});YSLOW.peeler={types:["doc","js","css","iframe","flash","cssimage","image","favicon","xhr","redirect","font"],NODETYPE:{ELEMENT:1,DOCUMENT:9},CSSRULE:{IMPORT_RULE:3,FONT_FACE_RULE:5},peel:function(s,t){},findDocuments:function(t){var A,D,w,B,x,y,s,E,C,u={};YSLOW.util.event.fire("peelProgress",{total_step:7,current_step:1,message:"Finding documents"});if(!t){return}if(!YSLOW.util.Preference.getPref("extensions.yslow.getFramesComponents",true)){u[t.URL]={document:t,type:"doc"};return u}B="doc";if(t.nodeType===this.NODETYPE.DOCUMENT){D=t;w=t.URL}else{if(t.nodeType===this.NODETYPE.ELEMENT&&t.nodeName.toLowerCase()==="frame"){D=t.contentDocument;w=t.src}else{if(t.nodeType===this.NODETYPE.ELEMENT&&t.nodeName.toLowerCase()==="iframe"){D=t.contentDocument;w=t.src;B="iframe";try{C=t.contentWindow;C=C&&C.parent;C=C&&C.document;C=C||t.ownerDocument;if(C&&C.URL===w){w=!t.getAttribute("src")?"":"about:blank"}}catch(v){YSLOW.util.dump(v)}}else{return u}}}u[w]={document:D,type:B};try{A=D.getElementsByTagName("iframe");for(x=0,y=A.length;x0){for(u=0;u0){for(u=0;u 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]', + ' --cdns "" specify comma separated list of additional CDNs', + '', + ' 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; + + // this is a hack for sitespeed.io 2.0 for solving the + // redirect issue in YSLow. + page.redirects = []; + + // request + page.onResourceRequested = function (req) { + page.resources[req.url] = { + request: req + }; + }; + + // response + page.onResourceReceived = function (res) { + var info, + resp = page.resources[res.url].response; + + // hack for taking care of redirects + if (res.stage === 'end' ) + if (res.status === 301 || res.status === 302) { + var locationValue; + for (var i = 0; i < res.headers.length; i++) { + if (res.headers[i].name==='Location') + locationValue = res.headers[i].value; + } + page.redirects.push('From ' + res.url + ' to ' + locationValue + '.'); + } + + 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); + }; + page.onError = function (msg) { + console.error(msg); + }; + } else { + page.onConsoleMessage = function (msg, line, source) { + console.log(JSON.stringify({ + message: msg, + lineNumber: line, + source: source + }, null, 4)); + }; + page.onError = function (msg, trace) { + console.error(JSON.stringify({ + message: msg, + stacktrace: trace + })); + }; + } + } else { + page.onError = function () { + // catch uncaught error from the page + }; + } + + // 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, output, + exitStatus = 0, + 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.8'; +/** + * 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 valid protocols in component sets. + */ +YSLOW.ComponentSet.validProtocols = ['http', 'https', 'ftp']; + +/** + * @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, + validProtocols = this.validProtocols, + len = validProtocols.length; + + s = s.toLowerCase(); + index = s.indexOf(':'); + if (index > 0) { + protocol = s.substr(0, index); + for (i = 0; i < len; i += 1) { + if (protocol === validProtocols[i]) { + return true; + } + } + } + + return false; +}; + + +/** + * @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 && that.headers.location !== that.url) { + // 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 + // always use max-age if exists following 1.1 spec + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3 + if (that.getMaxAge() !== undefined) { + that.expires = that.getMaxAge(); + } + else if (that.headers.expires && that.headers.expires.length > 0) { + that.expires = new Date(that.headers.expires); + } + + // 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; + + comp.ttfb = response.starttime - request.starttime; + // 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]+:([1-9a-f]|[0-9a-f]{2,})$/, + 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 = / ') : ''); + } + } + + return { + score: score, + message: message, + components: offenders + }; + } +}); + +YSLOW.registerRule({ + id: 'yexpires', + //name: 'Add an Expires header', + url: 'http://developer.yahoo.com/performance/rules.html#expires', + category: ['server'], + + config: { + // how many points to take for each component without Expires header + points: 11, + // 2 days = 2 * 24 * 60 * 60 seconds, how far is far enough + howfar: 172800, + // component types to be inspected for expires headers + types: ['css', 'js', 'image', 'cssimage', 'flash', 'favicon'] + }, + + lint: function (doc, cset, config) { + var ts, i, expiration, score, len, + // far-ness in milliseconds + far = parseInt(config.howfar, 10) * 1000, + offenders = [], + comps = cset.getComponentsByType(config.types); + + for (i = 0, len = comps.length; i < len; i += 1) { + expiration = comps[i].expires; + if (typeof expiration === 'object' && + typeof expiration.getTime === 'function') { + // looks like a Date object + ts = new Date().getTime(); + if (expiration.getTime() > ts + far) { + continue; + } + } + offenders.push(comps[i]); + } + + score = 100 - offenders.length * parseInt(config.points, 10); + + return { + score: score, + message: (offenders.length > 0) ? YSLOW.util.plural( + 'There %are% %num% static component%s%', + offenders.length + ) + ' without a far-future expiration date.' : '', + components: offenders + }; + } +}); + +YSLOW.registerRule({ + id: 'ycompress', + //name: 'Compress components', + url: 'http://developer.yahoo.com/performance/rules.html#gzip', + category: ['server'], + + config: { + // files below this size are exceptions of the gzip rule + min_filesize: 500, + // file types to inspect + types: ['doc', 'iframe', 'xhr', 'js', 'css'], + // points to take out for each non-compressed component + points: 11 + }, + + lint: function (doc, cset, config) { + var i, len, score, comp, + offenders = [], + comps = cset.getComponentsByType(config.types); + + for (i = 0, len = comps.length; i < len; i += 1) { + comp = comps[i]; + if (comp.compressed || comp.size < 500) { + continue; + } + offenders.push(comp); + } + + score = 100 - offenders.length * parseInt(config.points, 10); + + return { + score: score, + message: (offenders.length > 0) ? YSLOW.util.plural( + 'There %are% %num% plain text component%s%', + offenders.length + ) + ' that should be sent compressed' : '', + components: offenders + }; + } +}); + +YSLOW.registerRule({ + id: 'ycsstop', + //name: 'Put CSS at the top', + url: 'http://developer.yahoo.com/performance/rules.html#css_top', + category: ['css'], + + config: { + points: 10 + }, + + lint: function (doc, cset, config) { + var i, len, score, comp, + comps = cset.getComponentsByType('css'), + offenders = []; + + // expose all offenders + for (i = 0, len = comps.length; i < len; i += 1) { + comp = comps[i]; + if (comp.containerNode === 'body') { + offenders.push(comp); + } + } + + score = 100; + if (offenders.length > 0) { + // start at 99 so each ding drops us a grade + score -= 1 + offenders.length * parseInt(config.points, 10); + } + + return { + score: score, + message: (offenders.length > 0) ? YSLOW.util.plural( + 'There %are% %num% stylesheet%s%', + offenders.length + ) + ' found in the body of the document' : '', + components: offenders + }; + } +}); + +YSLOW.registerRule({ + id: 'yjsbottom', + //name: 'Put Javascript at the bottom', + url: 'http://developer.yahoo.com/performance/rules.html#js_bottom', + category: ['javascript'], + config: { + points: 5 // how many points for each script in the + }, + + lint: function (doc, cset, config) { + var i, len, comp, score, + offenders = [], + comps = cset.getComponentsByType('js'); + + // offenders are components not injected (tag found on document payload) + // except if they have either defer or async attributes + for (i = 0, len = comps.length; i < len; i += 1) { + comp = comps[i]; + if (comp.containerNode === 'head' && + !comp.injected && (!comp.defer || !comp.async)) { + offenders.push(comp); + } + } + + score = 100 - offenders.length * parseInt(config.points, 10); + + return { + score: score, + message: (offenders.length > 0) ? + YSLOW.util.plural( + 'There %are% %num% JavaScript script%s%', + offenders.length + ) + ' found in the head of the document' : '', + components: offenders + }; + } +}); + +YSLOW.registerRule({ + id: 'yexpressions', + //name: 'Avoid CSS expressions', + url: 'http://developer.yahoo.com/performance/rules.html#css_expressions', + category: ['css'], + + config: { + points: 2 // how many points for each expression + }, + + lint: function (doc, cset, config) { + var i, len, expr_count, comp, + instyles = (cset.inline && cset.inline.styles) || [], + comps = cset.getComponentsByType('css'), + offenders = [], + score = 100, + total = 0; + + for (i = 0, len = comps.length; i < len; i += 1) { + comp = comps[i]; + if (typeof comp.expr_count === 'undefined') { + expr_count = YSLOW.util.countExpressions(comp.body); + comp.expr_count = expr_count; + } else { + expr_count = comp.expr_count; + } + + // offence + if (expr_count > 0) { + comp.yexpressions = YSLOW.util.plural( + '%num% expression%s%', + expr_count + ); + total += expr_count; + offenders.push(comp); + } + } + + for (i = 0, len = instyles.length; i < len; i += 1) { + expr_count = YSLOW.util.countExpressions(instyles[i].body); + if (expr_count > 0) { + offenders.push('inline <style> tag #' + (i + 1) + ' (' + + YSLOW.util.plural( + '%num% expression%s%', + expr_count + ) + ')' + ); + total += expr_count; + } + } + + if (total > 0) { + score = 90 - total * config.points; + } + + return { + score: score, + message: total > 0 ? 'There is a total of ' + + YSLOW.util.plural('%num% expression%s%', total) : '', + components: offenders + }; + } +}); + +YSLOW.registerRule({ + id: 'yexternal', + //name: 'Make JS and CSS external', + url: 'http://developer.yahoo.com/performance/rules.html#external', + category: ['javascript', 'css'], + config: {}, + + lint: function (doc, cset, config) { + var message, + inline = cset.inline, + styles = (inline && inline.styles) || [], + scripts = (inline && inline.scripts) || [], + offenders = []; + + if (styles.length) { + message = YSLOW.util.plural( + 'There is a total of %num% inline css', + styles.length + ); + offenders.push(message); + } + + if (scripts.length) { + message = YSLOW.util.plural( + 'There is a total of %num% inline script%s%', + scripts.length + ); + offenders.push(message); + } + + return { + score: 'n/a', + message: 'Only consider this if your property is a common user home page.', + components: offenders + }; + } +}); + +YSLOW.registerRule({ + id: 'ydns', + //name: 'Reduce DNS lookups', + url: 'http://developer.yahoo.com/performance/rules.html#dns_lookups', + category: ['content'], + + config: { + // maximum allowed domains, excluding ports and IP addresses + max_domains: 4, + // the cost of each additional domain over the maximum + points: 5 + }, + + lint: function (doc, cset, config) { + var i, len, domain, + util = YSLOW.util, + kbSize = util.kbSize, + plural = util.plural, + score = 100, + domains = util.summaryByDomain(cset.components, + ['size', 'size_compressed'], true); + + if (domains.length > config.max_domains) { + score -= (domains.length - config.max_domains) * config.points; + } + + // list unique domains only to avoid long list of offenders + if (domains.length) { + for (i = 0, len = domains.length; i < len; i += 1) { + domain = domains[i]; + domains[i] = domain.domain + ': ' + + plural('%num% component%s%, ', domain.count) + + kbSize(domain.sum_size) + ( + domain.sum_size_compressed > 0 ? ' (' + + kbSize(domain.sum_size_compressed) + ' GZip)' : '' + ); + } + } + + return { + score: score, + message: (domains.length > config.max_domains) ? plural( + 'The components are split over more than %num% domain%s%', + config.max_domains + ) : '', + components: domains + }; + } +}); + +YSLOW.registerRule({ + id: 'yminify', + //name: 'Minify JS and CSS', + url: 'http://developer.yahoo.com/performance/rules.html#minify', + category: ['javascript', 'css'], + + config: { + // penalty for each unminified component + points: 10, + // types of components to inspect for minification + types: ['js', 'css'] + }, + + lint: function (doc, cset, config) { + var i, len, score, minified, comp, + inline = cset.inline, + styles = (inline && inline.styles) || [], + scripts = (inline && inline.scripts) || [], + comps = cset.getComponentsByType(config.types), + offenders = []; + + // check all peeled components + for (i = 0, len = comps.length; i < len; i += 1) { + comp = comps[i]; + // set/get minified flag + if (typeof comp.minified === 'undefined') { + minified = YSLOW.util.isMinified(comp.body); + comp.minified = minified; + } else { + minified = comp.minified; + } + + if (!minified) { + offenders.push(comp); + } + } + + // check inline scripts/styles/whatever + for (i = 0, len = styles.length; i < len; i += 1) { + if (!YSLOW.util.isMinified(styles[i].body)) { + offenders.push('inline <style> tag #' + (i + 1)); + } + } + for (i = 0, len = scripts.length; i < len; i += 1) { + if (!YSLOW.util.isMinified(scripts[i].body)) { + offenders.push('inline <script> tag #' + (i + 1)); + } + } + + score = 100 - offenders.length * config.points; + + return { + score: score, + message: (offenders.length > 0) ? YSLOW.util.plural('There %are% %num% component%s% that can be minified', offenders.length) : '', + components: offenders + }; + } +}); + +YSLOW.registerRule({ + id: 'yredirects', + //name: 'Avoid redirects', + url: 'http://developer.yahoo.com/performance/rules.html#redirects', + category: ['content'], + + config: { + points: 10 // the penalty for each redirect + }, + + lint: function (doc, cset, config) { + var i, len, comp, score, + offenders = [], + briefUrl = YSLOW.util.briefUrl, + comps = cset.getComponentsByType('redirect'); + + for (i = 0, len = comps.length; i < len; i += 1) { + comp = comps[i]; + offenders.push(briefUrl(comp.url, 80) + ' redirects to ' + + briefUrl(comp.headers.location, 60)); + } + score = 100 - comps.length * parseInt(config.points, 10); + + return { + score: score, + message: (comps.length > 0) ? YSLOW.util.plural( + 'There %are% %num% redirect%s%', + comps.length + ) : '', + components: offenders + }; + } +}); + +YSLOW.registerRule({ + id: 'ydupes', + //name: 'Remove duplicate JS and CSS', + url: 'http://developer.yahoo.com/performance/rules.html#js_dupes', + category: ['javascript', 'css'], + + config: { + // penalty for each duplicate + points: 5, + // component types to check for duplicates + types: ['js', 'css'] + }, + + lint: function (doc, cset, config) { + var i, url, score, len, + hash = {}, + offenders = [], + comps = cset.getComponentsByType(config.types); + + for (i = 0, len = comps.length; i < len; i += 1) { + url = comps[i].url; + if (typeof hash[url] === 'undefined') { + hash[url] = { + count: 1, + compindex: i + }; + } else { + hash[url].count += 1; + } + } + + for (i in hash) { + if (hash.hasOwnProperty(i) && hash[i].count > 1) { + offenders.push(comps[hash[i].compindex]); + } + } + + score = 100 - offenders.length * parseInt(config.points, 10); + + return { + score: score, + message: (offenders.length > 0) ? YSLOW.util.plural( + 'There %are% %num% duplicate component%s%', + offenders.length + ) : '', + components: offenders + }; + } +}); + +YSLOW.registerRule({ + id: 'yetags', + //name: 'Configure ETags', + url: 'http://developer.yahoo.com/performance/rules.html#etags', + category: ['server'], + + config: { + // points to take out for each misconfigured etag + points: 11, + // types to inspect for etags + types: ['flash', 'js', 'css', 'cssimage', 'image', 'favicon'] + }, + + lint: function (doc, cset, config) { + + var i, len, score, comp, etag, + offenders = [], + comps = cset.getComponentsByType(config.types); + + for (i = 0, len = comps.length; i < len; i += 1) { + comp = comps[i]; + etag = comp.headers && comp.headers.etag; + if (etag && !YSLOW.util.isETagGood(etag)) { + offenders.push(comp); + } + } + + score = 100 - offenders.length * parseInt(config.points, 10); + + return { + score: score, + message: (offenders.length > 0) ? YSLOW.util.plural( + 'There %are% %num% component%s% with misconfigured ETags', + offenders.length + ) : '', + components: offenders + }; + } +}); + +YSLOW.registerRule({ + id: 'yxhr', + //name: 'Make Ajax cacheable', + url: 'http://developer.yahoo.com/performance/rules.html#cacheajax', + category: ['content'], + + config: { + // points to take out for each non-cached XHR + points: 5, + // at least an hour in cache. + min_cache_time: 3600 + }, + + lint: function (doc, cset, config) { + var i, expiration, ts, score, cache_control, + // far-ness in milliseconds + min = parseInt(config.min_cache_time, 10) * 1000, + offenders = [], + comps = cset.getComponentsByType('xhr'); + + for (i = 0; i < comps.length; i += 1) { + // check for cache-control: no-cache and cache-control: no-store + cache_control = comps[i].headers['cache-control']; + if (cache_control) { + if (cache_control.indexOf('no-cache') !== -1 || + cache_control.indexOf('no-store') !== -1) { + continue; + } + } + + expiration = comps[i].expires; + if (typeof expiration === 'object' && + typeof expiration.getTime === 'function') { + // looks like a Date object + ts = new Date().getTime(); + if (expiration.getTime() > ts + min) { + continue; + } + // expires less than min_cache_time => BAD. + } + offenders.push(comps[i]); + } + + score = 100 - offenders.length * parseInt(config.points, 10); + + return { + score: score, + message: (offenders.length > 0) ? YSLOW.util.plural( + 'There %are% %num% XHR component%s% that %are% not cacheable', + offenders.length + ) : '', + components: offenders + }; + } +}); + +YSLOW.registerRule({ + id: 'yxhrmethod', + //name: 'Use GET for AJAX Requests', + url: 'http://developer.yahoo.com/performance/rules.html#ajax_get', + category: ['server'], + + config: { + // points to take out for each ajax request + // that uses http method other than GET. + points: 5 + }, + + lint: function (doc, cset, config) { + var i, score, + offenders = [], + comps = cset.getComponentsByType('xhr'); + + for (i = 0; i < comps.length; i += 1) { + if (typeof comps[i].method === 'string') { + if (comps[i].method !== 'GET' && comps[i].method !== 'unknown') { + offenders.push(comps[i]); + } + } + } + score = 100 - offenders.length * parseInt(config.points, 10); + + return { + score: score, + message: (offenders.length > 0) ? YSLOW.util.plural( + 'There %are% %num% XHR component%s% that %do% not use GET HTTP method', + offenders.length + ) : '', + components: offenders + }; + } +}); + +YSLOW.registerRule({ + id: 'ymindom', + //name: 'Reduce the Number of DOM Elements', + url: 'http://developer.yahoo.com/performance/rules.html#min_dom', + category: ['content'], + + config: { + // the range + range: 250, + // points to take out for each range of DOM that's more than max. + points: 10, + // number of DOM elements are considered too many if exceeds maxdom. + maxdom: 900 + }, + + lint: function (doc, cset, config) { + var numdom = cset.domElementsCount, + score = 100; + + if (numdom > config.maxdom) { + score = 99 - Math.ceil((numdom - parseInt(config.maxdom, 10)) / + parseInt(config.range, 10)) * parseInt(config.points, 10); + } + + return { + score: score, + message: (numdom > config.maxdom) ? YSLOW.util.plural( + 'There %are% %num% DOM element%s% on the page', + numdom + ) : '', + components: [] + }; + } +}); + +YSLOW.registerRule({ + id: 'yno404', + //name: 'No 404s', + url: 'http://developer.yahoo.com/performance/rules.html#no404', + category: ['content'], + + config: { + // points to take out for each 404 response. + points: 5, + // component types to be inspected for expires headers + types: ['css', 'js', 'image', 'cssimage', 'flash', 'xhr', 'favicon'] + }, + + lint: function (doc, cset, config) { + var i, len, comp, score, + offenders = [], + comps = cset.getComponentsByType(config.types); + + for (i = 0, len = comps.length; i < len; i += 1) { + comp = comps[i]; + if (parseInt(comp.status, 10) === 404) { + offenders.push(comp); + } + } + score = 100 - offenders.length * parseInt(config.points, 10); + return { + score: score, + message: (offenders.length > 0) ? YSLOW.util.plural( + 'There %are% %num% request%s% that %are% 404 Not Found', + offenders.length + ) : '', + components: offenders + }; + } +}); + +YSLOW.registerRule({ + id: 'ymincookie', + //name: 'Reduce Cookie Size', + url: 'http://developer.yahoo.com/performance/rules.html#cookie_size', + category: ['cookie'], + + config: { + // points to take out if cookie size is more than config.max_cookie_size + points: 10, + // 1000 bytes. + max_cookie_size: 1000 + }, + + lint: function (doc, cset, config) { + var n, + cookies = cset.cookies, + cookieSize = (cookies && cookies.length) || 0, + message = '', + score = 100; + + if (cookieSize > config.max_cookie_size) { + n = Math.floor(cookieSize / config.max_cookie_size); + score -= 1 + n * parseInt(config.points, 10); + message = YSLOW.util.plural( + 'There %are% %num% byte%s% of cookies on this page', + cookieSize + ); + } + + return { + score: score, + message: message, + components: [] + }; + } +}); + +YSLOW.registerRule({ + id: 'ycookiefree', + //name: 'Use Cookie-free Domains', + url: 'http://developer.yahoo.com/performance/rules.html#cookie_free', + category: ['cookie'], + + config: { + // points to take out for each component that send cookie. + points: 5, + // which component types should be cookie-free + types: ['js', 'css', 'image', 'cssimage', 'flash', 'favicon'] + }, + + lint: function (doc, cset, config) { + var i, len, score, comp, cookie, + offenders = [], + getHostname = YSLOW.util.getHostname, + docDomain = getHostname(cset.doc_comp.url), + comps = cset.getComponentsByType(config.types); + + for (i = 0, len = comps.length; i < len; i += 1) { + comp = comps[i]; + + // ignore /favicon.ico + if (comp.type === 'favicon' && + getHostname(comp.url) === docDomain) { + continue; + } + + cookie = comp.cookie; + if (typeof cookie === 'string' && cookie.length) { + offenders.push(comps[i]); + } + } + score = 100 - offenders.length * parseInt(config.points, 10); + + return { + score: score, + message: (offenders.length > 0) ? YSLOW.util.plural( + 'There %are% %num% component%s% that %are% not cookie-free', + offenders.length + ) : '', + components: offenders + }; + } +}); + +YSLOW.registerRule({ + id: 'ynofilter', + //name: 'Avoid Filters', + url: 'http://developer.yahoo.com/performance/rules.html#no_filters', + category: ['css'], + + config: { + // points to take out for each AlphaImageLoader filter not using _filter hack. + points: 5, + // points to take out for each AlphaImageLoader filter using _filter hack. + halfpoints: 2 + }, + + lint: function (doc, cset, config) { + var i, len, score, comp, type, count, filter_count, + instyles = (cset.inline && cset.inline.styles) || [], + comps = cset.getComponentsByType('css'), + offenders = [], + filter_total = 0, + hack_filter_total = 0; + + for (i = 0, len = comps.length; i < len; i += 1) { + comp = comps[i]; + if (typeof comp.filter_count === 'undefined') { + filter_count = YSLOW.util.countAlphaImageLoaderFilter(comp.body); + comp.filter_count = filter_count; + } else { + filter_count = comp.filter_count; + } + + // offence + count = 0; + for (type in filter_count) { + if (filter_count.hasOwnProperty(type)) { + if (type === 'hackFilter') { + hack_filter_total += filter_count[type]; + count += filter_count[type]; + } else { + filter_total += filter_count[type]; + count += filter_count[type]; + } + } + } + if (count > 0) { + comps[i].yfilters = YSLOW.util.plural('%num% filter%s%', count); + offenders.push(comps[i]); + } + } + + for (i = 0, len = instyles.length; i < len; i += 1) { + filter_count = YSLOW.util.countAlphaImageLoaderFilter(instyles[i].body); + count = 0; + for (type in filter_count) { + if (filter_count.hasOwnProperty(type)) { + if (type === 'hackFilter') { + hack_filter_total += filter_count[type]; + count += filter_count[type]; + } else { + filter_total += filter_count[type]; + count += filter_count[type]; + } + } + } + if (count > 0) { + offenders.push('inline <style> tag #' + (i + 1) + ' (' + + YSLOW.util.plural('%num% filter%s%', count) + ')'); + } + } + + score = 100 - (filter_total * config.points + hack_filter_total * + config.halfpoints); + + return { + score: score, + message: (filter_total + hack_filter_total) > 0 ? + 'There is a total of ' + YSLOW.util.plural( + '%num% filter%s%', + filter_total + hack_filter_total + ) : '', + components: offenders + }; + } +}); + +YSLOW.registerRule({ + id: 'yimgnoscale', + //name: 'Don\'t Scale Images in HTML', + url: 'http://developer.yahoo.com/performance/rules.html#no_scale', + category: ['images'], + + config: { + points: 5 // points to take out for each image that scaled. + }, + + lint: function (doc, cset, config) { + var i, prop, score, + offenders = [], + comps = cset.getComponentsByType('image'); + + for (i = 0; i < comps.length; i += 1) { + prop = comps[i].object_prop; + if (prop && typeof prop.width !== 'undefined' && + typeof prop.height !== 'undefined' && + typeof prop.actual_width !== 'undefined' && + typeof prop.actual_height !== 'undefined') { + if (prop.width < prop.actual_width || + prop.height < prop.actual_height) { + // allow scale up + offenders.push(comps[i]); + } + } + } + score = 100 - offenders.length * parseInt(config.points, 10); + + return { + score: score, + message: (offenders.length > 0) ? YSLOW.util.plural( + 'There %are% %num% image%s% that %are% scaled down', + offenders.length + ) : '', + components: offenders + }; + } +}); + +YSLOW.registerRule({ + id: 'yfavicon', + //name: 'Make favicon Small and Cacheable', + url: 'http://developer.yahoo.com/performance/rules.html#favicon', + category: ['images'], + + config: { + // points to take out for each offend. + points: 5, + // deduct point if size of favicon is more than this number. + size: 2000, + // at least this amount of seconds in cache to consider cacheable. + min_cache_time: 3600 + }, + + lint: function (doc, cset, config) { + var ts, expiration, comp, score, cacheable, + messages = [], + min = parseInt(config.min_cache_time, 10) * 1000, + comps = cset.getComponentsByType('favicon'); + + if (comps.length) { + comp = comps[0]; + + // check if favicon was found + if (parseInt(comp.status, 10) === 404) { + messages.push('Favicon was not found'); + } + + // check size + if (comp.size > config.size) { + messages.push(YSLOW.util.plural( + 'Favicon is more than %num% bytes', + config.size + )); + } + // check cacheability + expiration = comp.expires; + + if (typeof expiration === 'object' && + typeof expiration.getTime === 'function') { + // looks like a Date object + ts = new Date().getTime(); + cacheable = expiration.getTime() >= ts + min; + } + if (!cacheable) { + messages.push('Favicon is not cacheable'); + } + } + score = 100 - messages.length * parseInt(config.points, 10); + + return { + score: score, + message: (messages.length > 0) ? messages.join('\n') : '', + components: [] + }; + } + +}); + +YSLOW.registerRule({ + id: 'yemptysrc', + // name: 'Avoid empty src or href', + url: 'http://developer.yahoo.com/performance/rules.html#emptysrc', + category: ['server'], + config: { + points: 100 + }, + lint: function (doc, cset, config) { + var type, score, count, + emptyUrl = cset.empty_url, + offenders = [], + messages = [], + msg = '', + points = parseInt(config.points, 10); + + score = 100; + if (emptyUrl) { + for (type in emptyUrl) { + if (emptyUrl.hasOwnProperty(type)) { + count = emptyUrl[type]; + score -= count * points; + messages.push(count + ' ' + type); + } + } + msg = messages.join(', ') + YSLOW.util.plural( + ' component%s% with empty link were found.', + messages.length + ); + } + + return { + score: score, + message: msg, + components: offenders + }; + } +}); + +/** + * YSLOW.registerRuleset({ + * + * id: 'myalgo', + * name: 'The best algo', + * rules: { + * myrule: { + * ever: 2, + * } + * } + * + * }); + */ + +YSLOW.registerRuleset({ // yahoo default with default configuration + id: 'ydefault', + name: 'YSlow(V2)', + rules: { + ynumreq: {}, + // 1 + ycdn: {}, + // 2 + yemptysrc: {}, + yexpires: {}, + // 3 + ycompress: {}, + // 4 + ycsstop: {}, + // 5 + yjsbottom: {}, + // 6 + yexpressions: {}, + // 7 + yexternal: {}, + // 8 + ydns: {}, + // 9 + yminify: {}, + // 10 + yredirects: {}, + // 11 + ydupes: {}, + // 12 + yetags: {}, + // 13 + yxhr: {}, + // 14 + yxhrmethod: {}, + // 16 + ymindom: {}, + // 19 + yno404: {}, + // 22 + ymincookie: {}, + // 23 + ycookiefree: {}, + // 24 + ynofilter: {}, + // 28 + yimgnoscale: {}, + // 31 + yfavicon: {} // 32 + }, + weights: { + ynumreq: 8, + ycdn: 6, + yemptysrc: 30, + yexpires: 10, + ycompress: 8, + ycsstop: 4, + yjsbottom: 4, + yexpressions: 3, + yexternal: 4, + ydns: 3, + yminify: 4, + yredirects: 4, + ydupes: 4, + yetags: 2, + yxhr: 4, + yxhrmethod: 3, + ymindom: 3, + yno404: 4, + ymincookie: 3, + ycookiefree: 3, + ynofilter: 4, + yimgnoscale: 3, + yfavicon: 2 + } + +}); + +YSLOW.registerRuleset({ + + id: 'yslow1', + name: 'Classic(V1)', + rules: { + ynumreq: {}, + // 1 + ycdn: {}, + // 2 + yexpires: {}, + // 3 + ycompress: {}, + // 4 + ycsstop: {}, + // 5 + yjsbottom: {}, + // 6 + yexpressions: {}, + // 7 + yexternal: {}, + // 8 + ydns: {}, + // 9 + yminify: { // 10 + types: ['js'], + check_inline: false + }, + yredirects: {}, + // 11 + ydupes: { // 12 + types: ['js'] + }, + yetags: {} // 13 + }, + weights: { + ynumreq: 8, + ycdn: 6, + yexpires: 10, + ycompress: 8, + ycsstop: 4, + yjsbottom: 4, + yexpressions: 3, + yexternal: 4, + ydns: 3, + yminify: 4, + yredirects: 4, + ydupes: 4, + yetags: 2 + } + +}); + + +YSLOW.registerRuleset({ + id: 'yblog', + name: 'Small Site or Blog', + rules: { + ynumreq: {}, + // 1 + yemptysrc: {}, + ycompress: {}, + // 4 + ycsstop: {}, + // 5 + yjsbottom: {}, + // 6 + yexpressions: {}, + // 7 + ydns: {}, + // 9 + yminify: {}, + // 10 + yredirects: {}, + // 11 + ydupes: {}, + // 12 + ymindom: {}, + // 19 + yno404: {}, + // 22 + ynofilter: {}, + // 28 + yimgnoscale: {}, + // 31 + yfavicon: {} // 32 + }, + weights: { + ynumreq: 8, + yemptysrc: 30, + ycompress: 8, + ycsstop: 4, + yjsbottom: 4, + yexpressions: 3, + ydns: 3, + yminify: 4, + yredirects: 4, + ydupes: 4, + ymindom: 3, + yno404: 4, + ynofilter: 4, + yimgnoscale: 3, + yfavicon: 2 + } +}); +// Rule file for sitespeed.io + +var SITESPEEDHELP = {}; + +// Borrowed from https://github.com/pmeenan/spof-o-matic/blob/master/src/background.js +// Determining the top-level-domain for a given host is way too complex to do right +// (you need a full list of them basically) +// We are going to simplify it and assume anything that is .co.xx will have 3 parts +// and everything else will have 2 + +SITESPEEDHELP.getTLD = function (host){ + var tld = host; + var noSecondaries = /\.(gov|ac|mil|net|org|co)\.\w\w$/i; + if (host.match(noSecondaries)) { + var threePart = /[\w]+\.[\w]+\.[\w]+$/i; + tld = host.match(threePart).toString(); + } else { + var twoPart = /[\w]+\.[\w]+$/i; + tld = host.match(twoPart).toString(); + } + return tld; +}; + +// end of borrow :) + +// Inspired by dom monster http://mir.aculo.us/dom-monster/ +SITESPEEDHELP.getTextLength = function (element) { + +var avoidTextInScriptAndStyle = ("script style").split(' '); +var textLength = 0; + +function getLength(element){ + if(element.childNodes && element.childNodes.length>0) + for(var i=0;i= (edge || 0); + } + +SITESPEEDHELP.versionCompare = function(userVersion, edgeVersion) { + if(userVersion === undefined) return true; + + userVersion = userVersion.split('.'); + + var major = digitCompare(userVersion[0], edgeVersion[0]), + minor = digitCompare(userVersion[1], edgeVersion[1]), + build = digitCompare(userVersion[2], edgeVersion[2]); + + return (!major || major && !minor || major && minor && !build); + }; + + + +SITESPEEDHELP.isSameDomainTLD = function (docDomainTLD, cssUrl, fontFaceUrl) { + +// first check the font-face url, is it absolute or relative +if ((/^http/).test(fontFaceUrl)) { + // it is absolute ... + if (docDomainTLD === SITESPEEDHELP.getTLD(YSLOW.util.getHostname(fontFaceUrl))) { + return true; + } + else return false; +} + +// it is relative, check if the css is for the same domain as doc + else if (docDomainTLD === SITESPEEDHELP.getTLD(YSLOW.util.getHostname(cssUrl))) { + return true; + } + +else return false; + +return false; +}; + + +/* End */ + +YSLOW.registerRule({ + id: 'cssprint', + name: 'Do not load print stylesheets, use @media type print instead', + info: 'Loading a specific stylesheet for printing slows down the page, ' + + 'even though it is not used', + category: ['css'], + config: {points: 20}, + url: 'http://sitespeed.io/rules/#cssprint', + + lint: function (doc, cset, config) { + var i, media, score,url, + offenders = [], + hash = {}, + comps = cset.getComponentsByType('css'), + links = doc.getElementsByTagName('link'); + + for (i = 0, len = links.length; i < len; i += 1) { + + if (links[i].media === 'print') { + url = links[i].href; + hash[url] = 1; + } + } + +for (var i = 0; i < comps.length; i++) { + if (hash[comps[i].url]) { + offenders.push(comps[i]); + } + } + score = 100 - offenders.length * parseInt(config.points, 10); + + return { + score: score, + message: (offenders.length > 0) ? YSLOW.util.plural( + 'There %are% %num% print css files included on the page, that should be @media query instead', + offenders.length + ) : '', + components: offenders + }; + + + } +}); + +YSLOW.registerRule({ + id: 'ttfb', + name: 'Time to first byte', + info: 'It is important to have low time to the first byte to be able to render the page fast', + category: ['server'], + config: {points: 10, limitInMs: 300, hurtEveryMs: 100}, + url: 'http://sitespeed.io/rules/#ttfb', + + lint: function (doc, cset, config) { + var i, limit = parseInt(config.limitInMs, 10), hurtEveryMs = parseInt(config.hurtEveryMs, 10), score, ttfb, comps = cset.getComponentsByType('doc'); + + for (i = 0, len = comps.length; i < len; i++) { + ttfb = comps[i].ttfb; + if (ttfb > limit) { + // The limit is limit, for every hurtEveryMs, remove X points + score = 100 - (Math.ceil((ttfb - limit)/hurtEveryMs) + * parseInt(config.points, 10)); + } + } + + if (score<0) + score=0; + + return { + score: score, + message: (score < 100) ? 'The TTFB is too slow:' + ttfb + ' ms. The limit is ' + limit + ' ms and for every ' + hurtEveryMs + ' ms points are removed' + : '', + components: [''+ttfb] + }; + + } +}); + + +YSLOW.registerRule({ + id: 'cssinheaddomain', + name: 'Load CSS in head from document domain', + info: 'Make sure css in head is loaded from same domain as document, in order to have a better user experience and minimize dns lookups', + category: ['css'], + config: {points: 10}, + url: 'http://sitespeed.io/rules/#cssinheaddomain', + + lint: function (doc, cset, config) { + + var css = doc.getElementsByTagName('link'), + comps = cset.getComponentsByType('css'), + comp, docdomain, src, offenders = {}, + offendercomponents = [], uniquedns = [], + score = 100; + + docdomain = YSLOW.util.getHostname(cset.doc_comp.url); + + for (i = 0, len = css.length; i < len; i++) { + comp = css[i]; + src = comp.href || comp.getAttribute('href'); + if (src && (comp.rel === 'stylesheet' || comp.type === 'text/css')) { + if (comp.parentNode.tagName === 'HEAD') { + offenders[src] = 1; + } + + } + } + + for (var i = 0; i < comps.length; i++) { + if (offenders[comps[i].url]) { + if (docdomain !== YSLOW.util.getHostname(comps[i].url)) { + offendercomponents.push(comps[i]); + } + } + } + + uniquedns = YSLOW.util.getUniqueDomains(offendercomponents, true); + + var message = offendercomponents.length === 0 ? '' : + 'The following ' + YSLOW.util.plural('%num% css', offendercomponents.length) + + ' are loaded from a different domain inside head, causing DNS lookups before page is rendered. Unique DNS in head that decreases the score:' + uniquedns.length + "."; + // only punish dns lookups + score -= uniquedns.length * parseInt(config.points, 10); + + return { + score: score, + message: message, + components: offendercomponents + }; + } +}); + + +/** Alternative to yjsbottom rule that doesn't seems to work right now +with phantomjs +*/ +YSLOW.registerRule({ + id: 'syncjsinhead', + name: 'Never load JS synchronously in head', + info: 'Use the JavaScript snippets that load the JS files asynchronously in head ' + + 'in order to speed up the user experience.', + category: ['js'], + config: {points: 10}, + url: 'http://sitespeed.io/rules/#syncjsinhead', + + lint: function (doc, cset, config) { + var scripts = doc.getElementsByTagName('script'), + comps = cset.getComponentsByType('js'), + comp, offenders = {}, + offender_comps = [], + score = 100; + + for (i = 0, len = scripts.length; i < len; i++) { + comp = scripts[i]; + if (comp.parentNode.tagName === 'HEAD') { + if (comp.src) { + if (!comp.async && !comp.defer) { + offenders[comp.src] = 1; + } + } + } + } + + for (var i = 0; i < comps.length; i++) { + if (offenders[comps[i].url]) { + offender_comps.push(comps[i]); + } + } + + var message = offender_comps.length === 0 ? '' : + 'There are ' + YSLOW.util.plural('%num% script%s%', offender_comps.length) + + ' that are not loaded asynchronously in head, that will block the rendering.'; + score -= offender_comps.length * parseInt(config.points, 10); + + return { + score: score, + message: message, + components: offender_comps + }; + } +}); + +YSLOW.registerRule({ + id: 'avoidfont', + name: 'Try to avoid fonts', + info: 'Fonts slow down the page load, try to avoid them', + category: ['css'], + config: {points: 10}, + url: 'http://sitespeed.io/rules/#avoidfonts', + + lint: function (doc, cset, config) { + + var comps = cset.getComponentsByType('font'), + score; + + score = 100 - comps.length * parseInt(config.points, 10); + + var message = comps.length === 0 ? '' : + 'There are ' + YSLOW.util.plural('%num% font%s%', comps.length) + + ' that will add extra overhead.'; + + return { + score: score, + message: message, + components: comps + }; + } +}); + + +YSLOW.registerRule({ + id: 'criticalpath', + name: 'Avoid slowing down the rendering critical path', + info: 'Every file loaded inside of head, will postpone the rendering of the page, try to avoid loading javascript synchronously, load files from the same domain as the main document, and inline css for really fast critical path.', + category: ['content'], + config: {synchronouslyJSPoints: 10, deferJSPoints: 3, dnsLookupsPoints: 5, cssPoints: 5}, + url: 'http://sitespeed.io/rules/#criticalpath', + + lint: function (doc, cset, config) { + + var scripts = doc.getElementsByTagName('script'), + jsComponents = cset.getComponentsByType('js'), + cssComponents = cset.getComponentsByType('css'), + links = doc.getElementsByTagName('link'), + score = 100, docDomain, js, offenders = [], componentOffenders = [], comp, + jsFail = 0, cssFail = 0, notDocumentDomains = 0, domains; + + // the domain where the document is fetched from + // use this domain to avoid DNS lookups + docDomain = YSLOW.util.getHostname(cset.doc_comp.url); + + // calculate the score for javascripts + // synchronous will hurt us most + // defer CAN hurt us and async will not + // all inside of head of course + for (i = 0, len = scripts.length; i < len; i++) { + js = scripts[i]; + if (js.parentNode.tagName === 'HEAD') { + if (js.src) { + if (js.async) + continue; + else if (js.defer) { + offenders[js.src] = 1; + score -= config.deferJSPoints; + jsFail++; + } + else { + offenders[js.src] = 1; + score -= config.synchronouslyJSPoints; + jsFail++; + } + } + } + } + + // then for CSS + for (i = 0, len = links.length; i < len; i++) { + comp = links[i]; + src = comp.href || comp.getAttribute('href'); + + // Skip print stylesheets for now, since they "only" will make the onload sloooow + // maybe it's better to check for screen & all in the future? + if (comp.media === 'print') + continue; + + else if (src && (comp.rel === 'stylesheet' || comp.type === 'text/css')) { + if (comp.parentNode.tagName === 'HEAD') { + offenders[src] = 1; + score -= config.cssPoints; + cssFail++; + } + + } + } + + // match them + for (var i = 0; i < jsComponents.length; i++) { + if (offenders[jsComponents[i].url]) { + componentOffenders.push(jsComponents[i]); + } + } + + for (var i = 0; i < cssComponents.length; i++) { + if (offenders[cssComponents[i].url]) { + componentOffenders.push(cssComponents[i]); + } + } + + // hurt the ones that loads from another domain + domains = YSLOW.util.getUniqueDomains(componentOffenders, true); + for (var i = 0; i < domains.length; i++) { + if (domains[i] !== docDomain) + notDocumentDomains++; + } + score -= config.dnsLookupsPoints * notDocumentDomains; + + message = score === 100 ? '' : 'You have ' + jsFail + ' javascripts in the critical path and ' + cssFail + ' stylesheets' + ' using ' + notDocumentDomains + ' extra domains'; + + return { + score: score, + message: message, + components: componentOffenders + }; + } +}); + +YSLOW.registerRule({ + id: 'totalrequests', + name: 'Low number of total requests is good', + info: 'The more number of requests, the slower the page', + category: ['content'], + config: {points: 5}, + url: 'http://sitespeed.io/rules/#totalrequests', + + lint: function (doc, cset, config) { + + + var types = ['js', 'css', 'image', 'cssimage', 'font', 'flash', 'favicon', 'doc','iframe']; + var comps = cset.getComponentsByType(types); + var score = 100; + + if (comps.length < 26) { + score = 100; + } + else { + score = score + 26 - comps.length; + } + + if (score<0) score = 0; + + var message = score === 100 ? '' : + 'The page uses ' + comps.length + + ' requests, that is too many to make the page load fast.'; + var offenders = score === 100 ? '' : comps; + + return { + score: score, + message: message, + components: offenders + }; + } +}); + +YSLOW.registerRule({ + id: 'spof', + name: 'Frontend single point of failure', + info: 'A page should not have a single point of failure a.k.a load content from a provider that can get the page to stop working.', + category: ['misc'], + config: { jsPoints: 10, + cssPoints: 8, + fontFaceInCssPoints: 8, + inlineFontFacePoints: 1, + // SPOF types to check + cssSpof: true, + jsSpof: true, + fontFaceInCssSpof: true, + inlineFontFaceSpof: true }, + url: 'http://sitespeed.io/rules/#spof', + + lint: function (doc, cset, config) { + + var css = doc.getElementsByTagName('link'), + scripts = doc.getElementsByTagName('script'), + csscomps = cset.getComponentsByType('css'), + jscomps = cset.getComponentsByType('js'), + docDomainTLD, src, url, matches, offenders = [], insideHeadOffenders = [], + nrOfInlineFontFace = 0, nrOfFontFaceCssFiles = 0, nrOfJs = 0, nrOfCss = 0, + // RegEx pattern for retrieving all the font-face styles, borrowed from https://github.com/senthilp/spofcheck/blob/master/lib/rules.js + pattern = /@font-face[\s\n]*{([^{}]*)}/gim, + urlPattern = /url\s*\(\s*['"]?([^'"]*)['"]?\s*\)/gim, + fontFaceInfo = '', + score = 100; + + docDomainTLD = SITESPEEDHELP.getTLD(YSLOW.util.getHostname(cset.doc_comp.url)); + + // Check for css loaded in head, from another domain (not subdomain) + if (config.cssSpof) { + for (i = 0, len = css.length; i < len; i++) { + csscomp = css[i]; + src = csscomp.href || csscomp.getAttribute('href'); + + // Skip print stylesheets for now, since they "only" will make the onload sloooow + // maybe it's better to check for screen & all in the future? + if (csscomp.media === 'print') + continue; + + if (src && (csscomp.rel === 'stylesheet' || csscomp.type === 'text/css')) { + if (csscomp.parentNode.tagName === 'HEAD') { + insideHeadOffenders[src] = 1; + } + } + } + + for (var i = 0; i < csscomps.length; i++) { + if (insideHeadOffenders[csscomps[i].url]) { + if (docDomainTLD !== SITESPEEDHELP.getTLD(YSLOW.util.getHostname(csscomps[i].url))) { + offenders.push(csscomps[i]); + nrOfCss++; + } + } + } + } + + + // Check for font-face in the external css files + if (config.fontFaceInCssSpof) { + for (var i = 0; i < csscomps.length; i++) { + matches = csscomps[i].body.match(pattern); + if(matches) { + matches.forEach(function(match) { + while(url = urlPattern.exec(match)) { + if (!SITESPEEDHELP.isSameDomainTLD(docDomainTLD, csscomps[i].url, url[1])) { + // we have a match, a fontface user :) + offenders.push(url[1]); + nrOfFontFaceCssFiles++; + fontFaceInfo += ' The font file:' + url[1] + ' is loaded from ' + csscomps[i].url; + } + } + }); + } + } + } + + + // Check for inline font-face + if (config.inlineFontFaceSpof) { + matches = doc.documentElement.innerHTML.match(pattern); + + if (matches) { + matches.forEach(function(match) { + while(url = urlPattern.exec(match)) { + if (!SITESPEEDHELP.isSameDomainTLD(docDomainTLD, cset.doc_comp.url, url[1])) { + offenders.push(url[1]); + nrOfInlineFontFace++; + fontFaceInfo += ' The font file:' + url[1] + ' is loaded inline.'; + + } + } + }); + } + } + + + // now the js + if (config.jsSpof) { + for (i = 0, len = scripts.length; i < len; i++) { + jscomp = scripts[i]; + if (jscomp.parentNode.tagName === 'HEAD') { + if (jscomp.src) { + if (!jscomp.async && !jscomp.defer) { + insideHeadOffenders[jscomp.src] = 1; + } + } + } + } + + for (var i = 0; i < jscomps.length; i++) { + if (insideHeadOffenders[jscomps[i].url]) { + if (docDomainTLD !== SITESPEEDHELP.getTLD(YSLOW.util.getHostname(jscomps[i].url))) { + offenders.push(jscomps[i]); + nrOfJs++; + } + } + } + } + + + var message = offenders.length === 0 ? '' : + 'There are possible of ' + YSLOW.util.plural('%num% assets', offenders.length) + + ' that can cause a frontend single point of failure. '; + + message += nrOfJs === 0 ? '' : 'There are ' + YSLOW.util.plural('%num% javascript',nrOfJs) + ' loaded from another domain that can cause SPOF. '; + message += nrOfCss === 0 ? '' : 'There are ' + YSLOW.util.plural('%num% css ',nrOfCss) + ' loaded from another domain that can cause SPOF. '; + message += nrOfFontFaceCssFiles === 0 ? '' : 'There are ' + YSLOW.util.plural('%num%',nrOfFontFaceCssFiles) + ' font face in css files that can cause SPOF. ' + fontFaceInfo; + message += nrOfInlineFontFace === 0 ? '' : 'There are ' + YSLOW.util.plural('%num% ',nrOfInlineFontFace) + ' inline font face that can cause minor SPOF. ' + fontFaceInfo; + score -= nrOfJs * config.jsPoints + nrOfCss * config.cssPoints + nrOfInlineFontFace * config.inlineFontFacePoints + nrOfFontFaceCssFiles * config.fontFaceInCssPoints; + + return { + score: score, + message: message, + components: offenders + }; + } +}); + + +/** +Modified version of yexpires, skip standard analythics scripts that you couldn't fix yourself (not 100% but ...) and will +only give bad score for assets with 0 expires. +*/ + + +YSLOW.registerRule({ + id: 'expiresmod', + name: 'Check for expires headers', + info: 'All static components of a page should have a future expire headers.', + url: 'http://sitespeed.io/rules/#expires', + category: ['server'], + + config: { + // how many points to take for each component without Expires header + points: 11, + // component types to be inspected for expires headers + types: ['css', 'js', 'image', 'cssimage', 'flash','favicon'], + }, + + lint: function (doc, cset, config) { + var ts, i, expiration, score, len, message, + offenders = [], + comps = cset.getComponentsByType(config.types); + + for (i = 0, len = comps.length; i < len; i += 1) { + expiration = comps[i].expires; + if (typeof expiration === 'object' && + typeof expiration.getTime === 'function') { + + // check if the server has set the date, if so + // use that http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18 + if (typeof comps[i].headers.date === 'undefined') { + ts = new Date().getTime(); + } + else + ts = new Date(comps[i].headers.date).getTime(); + + if (expiration.getTime() > ts) { + continue; + } + } + + offenders.push(comps[i]); + } + + score = 100 - offenders.length * parseInt(config.points, 10); + + message = (offenders.length > 0) ? YSLOW.util.plural( + 'There %are% %num% static component%s%', + offenders.length + ) + ' without a future expiration date.' : ''; + + return { + score: score, + message: message, + components: offenders + }; + } +}); + +// skip standard analythics scripts that you couldn't fix yourself (not 100% but ...) +YSLOW.registerRule({ + id: 'longexpirehead', + name: 'Have expires headers equals or longer than one year', + info: 'All static components of a page should have at least one year expire header. However, analythics scripts will not give you bad points.', + url: 'http://sitespeed.io/rules/#longexpires', + category: ['server'], + + config: { + // how many points to take for each component without Expires header + points: 5, + types: ['css', 'js', 'image', 'cssimage', 'flash', 'favicon'], + skip: ['https://secure.gaug.es/track.js','https://ssl.google-analytics.com/ga.js','http://www.google-analytics.com/ga.js'] + }, + + lint: function (doc, cset, config) { + var ts, i, expiration, score, len, message, + offenders = [], + skipped = [], + far = 31535000 * 1000, + comps = cset.getComponentsByType(config.types); + + for (i = 0, len = comps.length; i < len; i += 1) { + expiration = comps[i].expires; + if (typeof expiration === 'object' && + typeof expiration.getTime === 'function') { + + // check if the server has set the date, if so + // use that http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18 + if (typeof comps[i].headers.date === 'undefined') { + ts = new Date().getTime(); + } + else + ts = new Date(comps[i].headers.date).getTime(); + + if (expiration.getTime() > ts + far) { + continue; + } + + // if in the ok list, just skip it + else if (config.skip.indexOf(comps[i].url) > 1 ) { + skipped.push(comps[i].url); + continue; + } + + } + + offenders.push(comps[i]); + } + + score = 100 - offenders.length * parseInt(config.points, 10); + + message = (offenders.length > 0) ? YSLOW.util.plural( + 'There %are% %num% static component%s%', + offenders.length + ) + ' without a expire header equal or longer than one year.' : ''; + + message += (skipped.length > 0) ? YSLOW.util.plural(' There %are% %num% static component%s% that are skipped from the score calculation', skipped.length) + ":" + skipped : ''; + + return { + score: score, + message: message, + components: offenders + }; + } +}); + + + + +YSLOW.registerRule({ + id: 'inlinecsswhenfewrequest', + name: 'Do not load css files when the page has few request', + info: 'When a page has few requests, it is better to inline the css, to make the page to start render as early as possible', + category: ['css'], + config: {points: 20, limit: 15, types: ['css', 'js', 'image', 'cssimage', 'flash', 'favicon']}, + url: 'http://sitespeed.io/rules/#inlinecsswhenfewrequest', + + lint: function (doc, cset, config) { + + + var comps = cset.getComponentsByType(config.types), + css = cset.getComponentsByType('css'), message = '', score = 100, offenders = []; + + // If we have more requests than the set limit & we have css files, decrease the score + if (comps.length < config.limit && css.length > 0) { + + for (i = 0, len = css.length; i < len; i++) { + offenders.push(css[i].url); + } + + message = 'The page have ' + comps.length + ' requests and uses ' + css.length + ' css files. It is better to keep the css inline, when you have so few requests.'; + score -= offenders.length * parseInt(config.points, 10); + + } + + return { + score: score, + message: message, + components: offenders + }; + } +}); + +YSLOW.registerRule({ + id: 'textcontent', + name: 'Have a reasonable percentage of textual content compared to the rest of the page', + info: 'Make sure you dont have too much styling etc that hides the text you want to deliver', + category: ['content'], + config: {decimals: 2}, + url: 'http://sitespeed.io/rules/#textcontent', + + lint: function (doc, cset, config) { + var textLength = 0, score = 100, offenders = [], message, contentPercent; + textLength = SITESPEEDHELP.getTextLength(doc); + contentPercent = textLength/doc.body.innerHTML.length*100; + if (contentPercent.toFixed(0)<50) { + score = contentPercent.toFixed(0)*2; + } + + message = 'The amount of content percentage: ' + contentPercent.toFixed(config.decimals) + '%'; + offenders.push(contentPercent.toFixed(config.decimals)); + + return { + score: score, + message: message, + components: offenders + }; + } +}); + + + +YSLOW.registerRule({ + id: 'nodnslookupswhenfewrequests', + name: 'Avoid DNS lookups when the page has few request', + info: 'If you have few prequest on a page, they should all be to the same domain to avoid DNS lookups, because the lookup will take extra time', + category: ['content'], + config: {points: 20, limit: 10, types: ['css', 'image', 'cssimage', 'flash', 'favicon']}, + url: 'http://sitespeed.io/rules/#nodnslookupswhenfewrequests', + + lint: function (doc, cset, config) { + + var domains, comp, comps = cset.getComponentsByType(config.types), jsComps = cset.getComponentsByType('js'), + score = 100, message = '', offenders = [], jsSync = [], scripts = doc.getElementsByTagName('script'); + + // fetch all js that aren't async + for (i = 0, len = scripts.length; i < len; i++) { + comp = scripts[i]; + if (comp.src) { + if (!comp.async && !comp.defer) { + jsSync[comp.src] = 1; + } + } + } + + // and add the components + for (var i = 0; i < jsComps.length; i++) { + if (jsSync[jsComps[i].url]) { + comps.push(jsComps[i]); + } + } + + domains = YSLOW.util.getUniqueDomains(comps); + + // Only activate if the number of components are less than the limit + // and we have more than one domain + if (comps.length < config.limit && domains.length > 1) { + for (i = 0, len = comps.length; i < len; i++) { + offenders.push(comps[i].url); + } + message = 'Too many domains (' + domains.length + ') used for a page with only ' + comps.length + ' requests (async javascripts not included)'; + score -= offenders.length * parseInt(config.points, 10); + } + + return { + score: score, + message: message, + components: offenders + }; + } +}); + +/* +Rule borrowed from Stoyan Stefanov +https://github.com/stoyan/yslow +*/ + +var YSLOW3PO = {}; +YSLOW3PO.is3p = function (url) { + + var patterns = [ + 'ajax.googleapis.com', + 'apis.google.com', + '.google-analytics.com', + 'connect.facebook.net', + 'platform.twitter.com', + 'code.jquery.com', + 'platform.linkedin.com', + '.disqus.com', + 'assets.pinterest.com', + 'widgets.digg.com', + '.addthis.com', + 'code.jquery.com', + 'ad.doubleclick.net', + '.lognormal.com', + 'embed.spotify.com' + ]; + var hostname = YSLOW.util.getHostname(url); + var re; + for (var i = 0; i < patterns.length; i++) { + re = new RegExp(patterns[i]); + if (re.test(hostname)) { + return true; + } + } + return false; +} + + +YSLOW.registerRule({ + id: 'thirdpartyasyncjs', + name: 'Load 3rd party JS asynchronously', + info: "Use the JavaScript snippets that load the JS files asynchronously " + + "in order to speed up the user experience.", + category: ['js'], + config: {}, + url: 'http://www.phpied.com/3PO#async', + + lint: function (doc, cset, config) { + var scripts = doc.getElementsByTagName('script'), + comps = cset.getComponentsByType('js'), + comp, offenders = {}, + offender_comps = [], + score = 100; + + // find offenders + for (i = 0, len = scripts.length; i < len; i++) { + comp = scripts[i]; + if (comp.src && YSLOW3PO.is3p(comp.src)) { + if (!comp.async && !comp.defer) { + offenders[comp.src] = 1; + } + } + } + + // match offenders to YSLOW components + for (var i = 0; i < comps.length; i++) { + if (offenders[comps[i].url]) { + offender_comps.push(comps[i]); + } + } + + // final sweep + var message = offender_comps.length === 0 ? '' : + 'The following ' + YSLOW.util.plural('%num% 3rd party script%s%', offender_comps.length) + + ' not loaded asynchronously:'; + score -= offender_comps.length * 21; + + return { + score: score, + message: message, + components: offender_comps + }; + } +}); + + + + + +YSLOW.registerRule({ + id: 'cssnumreq', + name: 'Make fewer HTTP requests for CSS files', + info: 'The more number of CSS requests, the slower the page will be. Combine your css files into one.', + category: ['css'], + config: {max_css: 1, points_css: 4}, + url: 'http://sitespeed.io/rules/#cssnumreq', + + lint: function (doc, cset, config) { + var css = cset.getComponentsByType('css'), + score = 100, offenders = [], + message = ''; + + if (css.length > config.max_css) { + score -= (css.length - config.max_css) * config.points_css; + message = 'This page has ' + YSLOW.util.plural('%num% external stylesheet%s%', css.length) + '. Try combining them into fewer requests.'; + + for (var i = 0; i < css.length; i++) { + offenders.push(css[i].url); + } + } + + return { + score: score, + message: message, + components: offenders + }; + } +}); + + +YSLOW.registerRule({ + id: 'cssimagesnumreq', + name: 'Make fewer HTTP requests for CSS image files', + info: 'The more number of CSS image requests, the slower the page. Combine your images into one CSS sprite.', + url: 'http://sitespeed.io/rules/#cssimagsenumreq', + category: ['css'], + config: {max_cssimages: 1, points_cssimages: 3}, + + lint: function (doc, cset, config) { + var cssimages = cset.getComponentsByType('cssimage'), + score = 100, offenders = [], + message = ''; + + if (cssimages.length > config.max_cssimages) { + score -= (cssimages.length -config.max_cssimages) * config.points_cssimages; + message = 'This page has ' + YSLOW.util.plural('%num% external css image%s%', cssimages.length) + '. Try combining them into fewer request.'; + + for (var i = 0; i < cssimages.length; i++) { + offenders.push(cssimages[i].url); + } + } + return { + score: score, + message: message, + components: offenders + }; + } +}); + + +YSLOW.registerRule({ + id: 'jsnumreq', + name: 'Make fewer synchronously HTTP requests for Javascript files', + info: 'Combine the Javascrips into one.', + category: ['js'], + config: { max_js: 1, points_js: 4}, + url: 'http://sitespeed.io/rules/#jsnumreq', + + lint: function (doc, cset, config) { + var scripts = doc.getElementsByTagName('script'), + comps = cset.getComponentsByType('js'), + comp, offenders = {}, + offender_comps = [], message = '', + score = 100; + + // fetch all js that aren't async + for (i = 0, len = scripts.length; i < len; i++) { + comp = scripts[i]; + if (comp.src) { + if (!comp.async && !comp.defer) { + offenders[comp.src] = 1; + } + } + } + + for (var i = 0; i < comps.length; i++) { + if (offenders[comps[i].url]) { + offender_comps.push(comps[i]); + } + } + + + if (offender_comps.length > config.max_js) { + message = 'There ' + YSLOW.util.plural('%are% %num% script%s%', offender_comps.length) + + ' loaded synchronously that could be combined into fewer requests.'; + score -= (offender_comps.length - config.max_js) * parseInt(config.points_js, 10); + } + + return { + score: score, + message: message, + components: offender_comps + }; + } +}); + + + +// Rewrite of the Yslow rule that don't work for PhantomJS at least +YSLOW.registerRule({ + id: 'noduplicates', + name: 'Remove duplicate JS and CSS', + info: 'It is bad practice include the same js or css twice', + category: ['js','css'], + config: {}, + url: 'http://developer.yahoo.com/performance/rules.html#js_dupes', + + + lint: function (doc, cset, config) { + var i, url, score, len, comp, + hash = {}, + offenders = [], + comps = cset.getComponentsByType(['js','css']), + scripts = doc.getElementsByTagName('script'), + css = doc.getElementsByTagName('link'); + + // first the js + for (i = 0, len = scripts.length; i < len; i += 1) { + url = scripts[i].src; + if (typeof hash[url] === 'undefined') { + hash[url] = 1; + } else { + hash[url] += 1; + } + } + + // then the css + for (i = 0, len = css.length; i < len; i += 1) { + comp = css[i]; + url = comp.href || comp.getAttribute('href'); + if (url && (comp.rel === 'stylesheet' || comp.type === 'text/css')) { + if (typeof hash[url] === 'undefined') { + hash[url] = 1; + } else { + hash[url] += 1; + } + } + } + + + // match offenders to YSLOW components + var offenders = []; + for (var i = 0; i < comps.length; i++) { + if (hash[comps[i].url] && hash[comps[i].url] > 1) { + offenders.push(comps[i]); + } + } + + score = 100 - offenders.length * 11; + + return { + score: score, + message: (offenders.length > 0) ? YSLOW.util.plural( + 'There %are% %num% js/css file%s% included more than once on the page', + offenders.length + ) : '', + components: offenders + }; + } +}); + +// the same rule as ymindom except that it reports the nr of doms +YSLOW.registerRule({ + id: 'mindom', + name: 'Reduce the number of DOM elements', + info: 'The number of dom elements are in correlation to if the page is fast or not', + url: 'http://developer.yahoo.com/performance/rules.html#min_dom', + category: ['content'], + + config: { + // the range + range: 250, + // points to take out for each range of DOM that's more than max. + points: 10, + // number of DOM elements are considered too many if exceeds maxdom. + maxdom: 900 + }, + + lint: function (doc, cset, config) { + var numdom = cset.domElementsCount, + score = 100; + + if (numdom > config.maxdom) { + score = 99 - Math.ceil((numdom - parseInt(config.maxdom, 10)) / + parseInt(config.range, 10)) * parseInt(config.points, 10); + } + + return { + score: score, + message: (numdom > config.maxdom) ? YSLOW.util.plural( + 'There %are% %num% DOM element%s% on the page', + numdom + ) : '', + components: [''+numdom] + }; + } +}); + +YSLOW.registerRule({ + id: 'thirdpartyversions', + name: 'Always use latest versions of third party javascripts', + info: 'Unisng the latest versions, will make sure you have the fastest and hopefully leanest javascripts.', + url: 'http://sitespeed.io/rules/#thirdpartyversions', + category: ['js'], + config: { + // points to take out for each js that is old + points: 10 + }, + + lint: function (doc, cset, config) { + var message = "", + score, offenders = 0; + + if(typeof jQuery == 'function'){ + if(SITESPEEDHELP.versionCompare(jQuery.fn.jquery, [2, 0, 0])) { + message = "You are using an old version of JQuery: "+ jQuery.fn.jquery + " Newer version is faster & better. Upgrade to the newest version from http://jquery.com/" ; + offenders += 1; + } + } + + score = 100 - offenders * parseInt(config.points, 10); + + return { + score: score, + message: message, + components: [] + }; + } +}); + + +YSLOW.registerRule({ + id: 'avoidscalingimages', + name: 'Never scale images in HTML', + info: 'Always use the correct size for images to avoid that a browser download an image that is larger than necessary.', + url: 'http://sitespeed.io/rules/#avoidscalingimages', + category: ['images'], + config: { + // if an image is more than X px in width, punish the page harder + reallyBadLimit: 100 + }, + + lint: function (doc, cset, config) { + var message = '', + score, offenders =[], + hash = {}, punish = 0, + comps = cset.getComponentsByType('image'), + images = doc.getElementsByTagName('img'); + + // go through all images + for(var i = 0; i < images.length; i++){ + var img = images[i]; + // skip images that are 0 (carousell etc) + if (img.clientWidth < img.naturalWidth && img.clientWidth > 0) { + message = message + ' ' + img.src + ' [browserWidth:' + img.clientWidth + ' realImageWidth: ' + img.naturalWidth + ']'; + hash[img.src] = 1; + + // punish hard if the reallyBadLimitExceeds + if ((img.clientWidth + config.reallyBadLimit) < img.naturalWidth) + punish++; + } + } + + for (var i = 0; i < comps.length; i++) { + if (hash[comps[i].url]) { + offenders.push(comps[i]); + } + } + + score = 100 - ((offenders.length-punish) * 2) - (punish*10); + return { + score: score, + message: (offenders.length > 0) ? YSLOW.util.plural('You have %num% image%s% that %are% scaled in the HTML:' + message,offenders.length ) : '', + components: offenders + }; + } +}); + + +/** +** This is a hack for sitespeed.io 2.0. The original YSLow rule doesn't work for PhantomJS +** see why https://github.com/soulgalore/sitespeed.io/issues/243 +*/ +YSLOW.registerRule({ + id: 'redirects', + name: 'Never do redirects', + info: 'Avoid doing redirects, it will kill you web page on mobile.', + url: 'http://sitespeed.io/rules/#redirects', + category: ['content'], + + config: { + points: 10 // the penalty for each redirect + }, + + lint: function (doc, cset, config) { + var score; + + score = 100 - cset.redirects.length * parseInt(config.points, 10); + + return { + score: score, + message: (cset.redirects.length > 0) ? YSLOW.util.plural( + 'There %are% %num% redirect%s%.', + cset.redirects.length + ) + " " + cset.redirects: '', + components: [] + }; + } +}); + + +/* End */ + + +YSLOW.registerRuleset({ + id: 'sitespeed.io-desktop', + name: 'Sitespeed.io desktop rules', + rules: { + criticalpath: {}, + // ttfb: {}, + spof: { fontFaceInCssSpof: false, + inlineFontFaceSpof: false}, + cssnumreq: {}, + cssimagesnumreq: {}, + jsnumreq: {}, + yemptysrc: {}, + ycompress: {}, + ycsstop: {}, + yjsbottom: {}, + yexpressions: {}, + // yexternal: {}, + ydns: {}, + yminify: {}, + redirects: {}, + noduplicates: {}, + yetags: {}, + yxhr: {}, + yxhrmethod: {}, + mindom: {}, + yno404: {}, + ymincookie: {}, + ycookiefree: {}, + ynofilter: {}, + avoidscalingimages: {}, + yfavicon: {}, + thirdpartyasyncjs: {}, + cssprint: {}, + cssinheaddomain: {}, + syncjsinhead: {}, + avoidfont: {}, + totalrequests: {}, + expiresmod: {}, + longexpirehead: {}, + nodnslookupswhenfewrequests:{}, + inlinecsswhenfewrequest:{}, + textcontent: {}, + thirdpartyversions: {} + }, + weights: { + criticalpath: 15, + // ttfb: 10, + // Low since we fetch all different domains, not only 3rd parties + spof: 5, + cssnumreq: 8, + cssimagesnumreq: 8, + jsnumreq: 8, + yemptysrc: 30, + ycompress: 8, + ycsstop: 4, + yjsbottom: 4, + yexpressions: 3, + // yexternal: 4, + ydns: 3, + yminify: 4, + redirects: 4, + noduplicates: 4, + yetags: 2, + yxhr: 4, + yxhrmethod: 3, + mindom: 3, + yno404: 4, + ymincookie: 3, + ycookiefree: 3, + ynofilter: 4, + avoidscalingimages: 5, + yfavicon: 2, + thirdpartyasyncjs: 10, + cssprint: 3, + cssinheaddomain: 8, + syncjsinhead: 20, + avoidfont: 1, + totalrequests: 10, + expiresmod: 10, + longexpirehead: 5, + nodnslookupswhenfewrequests: 8, + inlinecsswhenfewrequest: 7, + textcontent: 1, + thirdpartyversions:5 + } + +}); + +YSLOW.registerRuleset({ + id: 'sitespeed.io-mobile', + name: 'Sitespeed.io mobile rules', + rules: { + criticalpath: {}, + // ttfb: {}, + spof: { fontFaceInCssSpof: false, + inlineFontFaceSpof: false}, + cssnumreq: {}, + cssimagesnumreq: {}, + jsnumreq: {}, + yemptysrc: {}, + ycompress: {}, + ycsstop: {}, + yjsbottom: {}, + yexpressions: {}, + // yexternal: {}, + ydns: {}, + yminify: {}, + redirects: {}, + noduplicates: {}, + yetags: {}, + yxhr: {}, + yxhrmethod: {}, + mindom: {}, + yno404: {}, + ymincookie: {}, + ycookiefree: {}, + ynofilter: {}, + avoidscalingimages: {}, + yfavicon: {}, + thirdpartyasyncjs: {}, + cssprint: {}, + cssinheaddomain: {}, + syncjsinhead: {}, + avoidfont: {}, + totalrequests: {}, + expiresmod: {}, + longexpirehead: {}, + nodnslookupswhenfewrequests:{}, + inlinecsswhenfewrequest:{}, + textcontent: {}, + thirdpartyversions: {} + }, + weights: { + criticalpath: 20, + // ttfb: 10, + // Low since we fetch all different domains, not only 3rd parties + spof: 5, + cssnumreq: 8, + cssimagesnumreq: 8, + jsnumreq: 8, + yemptysrc: 30, + ycompress: 8, + ycsstop: 4, + yjsbottom: 4, + yexpressions: 3, + //yexternal: 4, + ydns: 3, + yminify: 4, + redirects: 15, + noduplicates: 4, + yetags: 2, + yxhr: 4, + yxhrmethod: 3, + mindom: 3, + yno404: 4, + ymincookie: 3, + ycookiefree: 3, + ynofilter: 4, + avoidscalingimages: 10, + yfavicon: 2, + thirdpartyasyncjs: 10, + cssprint: 3, + cssinheaddomain: 8, + syncjsinhead: 20, + avoidfont: 5, + totalrequests: 14, + expiresmod: 10, + longexpirehead: 5, + nodnslookupswhenfewrequests: 15, + inlinecsswhenfewrequest: 10, + textcontent: 1, + thirdpartyversions:5 + } + +}); + + +YSLOW.registerRuleset({ + id: 'sitespeed.io-desktop-http2.0', + name: 'Sitespeed.io desktop rules for HTTP 2.0', + rules: { + criticalpath: {}, + // ttfb: {}, + spof: { fontFaceInCssSpof: false, + inlineFontFaceSpof: false}, + yemptysrc: {}, + ycompress: {}, + ycsstop: {}, + yjsbottom: {}, + yexpressions: {}, + // yexternal: {}, + ydns: {}, + yminify: {}, + redirects: {}, + noduplicates: {}, + yetags: {}, + yxhr: {}, + yxhrmethod: {}, + mindom: {}, + yno404: {}, + ymincookie: {}, + ycookiefree: {}, + ynofilter: {}, + avoidscalingimages: {}, + yfavicon: {}, + thirdpartyasyncjs: {}, + cssprint: {}, + cssinheaddomain: {}, + syncjsinhead: {}, + avoidfont: {}, + expiresmod: {}, + longexpirehead: {}, + textcontent: {}, + thirdpartyversions: {} + }, + weights: { + criticalpath: 15, + // ttfb: 10, + // Low since we fetch all different domains, not only 3rd parties + spof: 5, + yemptysrc: 30, + ycompress: 8, + ycsstop: 4, + yjsbottom: 4, + yexpressions: 3, + // yexternal: 4, + ydns: 3, + yminify: 4, + redirects: 4, + noduplicates: 4, + yetags: 2, + yxhr: 4, + yxhrmethod: 3, + mindom: 3, + yno404: 4, + ymincookie: 3, + ycookiefree: 3, + ynofilter: 4, + avoidscalingimages: 5, + yfavicon: 2, + thirdpartyasyncjs: 10, + cssprint: 3, + cssinheaddomain: 8, + syncjsinhead: 20, + avoidfont: 1, + expiresmod: 10, + longexpirehead: 5, + textcontent: 1, + thirdpartyversions:5 + } + +}); +/** + * Custom ruleset must be placed in this directory as rulseset_name.js + * + * Here is a very simplified snippet for registering a new rules and ruleset: + * + * YSLOW.registerRule({ + * id: 'foo-rule1', + * name: 'Sample Test #1', + * info: 'How simple is that?', + * + * lint: function (doc, cset, config) { + * return { + * score: 90, + * message: 'close but no cigar', + * components: ['element1'] + * }; + * } + * }); + * + * YSLOW.registerRuleset({ + * id: 'foo', + * name: 'Foobar Ruleset', + * rules: { + * 'foo-rule1': {} + * }, + * weights: { + * 'foo-rule1': 3 + * } + * }); + * + */ +/** + * 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, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true */ + +/** + * ResultSet class + * @constructor + * @param {Array} results array of lint result + * @param {Number} overall_score overall score + * @param {YSLOW.Ruleset} ruleset_applied Ruleset used to generate the result. + */ +YSLOW.ResultSet = function (results, overall_score, ruleset_applied) { + this.ruleset_applied = ruleset_applied; + this.overall_score = overall_score; + this.results = results; +}; + +YSLOW.ResultSet.prototype = { + + /** + * Get results array from ResultSet. + * @return results array + * @type Array + */ + getResults: function () { + return this.results; + }, + + /** + * Get ruleset applied from ResultSet + * @return ruleset applied + * @type YSLOW.Ruleset + */ + getRulesetApplied: function () { + return this.ruleset_applied; + }, + + /** + * Get overall score from ResultSet + * @return overall score + * @type Number + */ + getOverallScore: function () { + return this.overall_score; + } + +}; +/** + * Copyright (c) 2012, Yahoo! Inc. All rights reserved. + * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. + */ + +/*global YSLOW, window*/ +/*jslint white: true, browser: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true */ + +/** + * YSLOW.view manages the YSlow UI. + * @class + * @constructor + * @param {Object} panel This panel object can be YSLOW.firefox.Panel or FirebugPanel. + * @param {YSLOW.context} yscontext YSlow context to associated with this view. + */ +YSLOW.view = function (panel, yscontext) { + var toolbar, elem, dialogHtml, modaldlg, copyright; + + this.panel_doc = panel.document; + this.buttonViews = {}; + this.curButtonId = ""; + this.panelNode = panel.panelNode; + + this.loadCSS(this.panel_doc); + + toolbar = this.panel_doc.createElement("div"); + toolbar.id = "toolbarDiv"; + toolbar.innerHTML = this.getToolbarSource(); + toolbar.style.display = "block"; + + elem = this.panel_doc.createElement("div"); + elem.style.display = "block"; + + // create modal dialog. + dialogHtml = '

    text

    '; + + modaldlg = this.panel_doc.createElement('div'); + modaldlg.id = "dialogDiv"; + modaldlg.innerHTML = dialogHtml; + modaldlg.style.display = "none"; + // save modaldlg in view, make look up easier. + this.modaldlg = modaldlg; + + this.tooltip = new YSLOW.view.Tooltip(this.panel_doc, panel.panelNode); + + copyright = this.panel_doc.createElement('div'); + copyright.id = "copyrightDiv"; + copyright.innerHTML = YSLOW.doc.copyright; + this.copyright = copyright; + + if (panel.panelNode) { + panel.panelNode.id = "yslowDiv"; + panel.panelNode.appendChild(modaldlg); + panel.panelNode.appendChild(toolbar); + panel.panelNode.appendChild(elem); + panel.panelNode.appendChild(copyright); + } + this.viewNode = elem; + this.viewNode.id = "viewDiv"; + this.viewNode.className = "yui-skin-sam"; + + this.yscontext = yscontext; + + YSLOW.util.addEventListener(this.panelNode, 'click', function (e) { + var help, helplink, x, y, parent; + var doc = FBL.getContentView(panel.document); + var toolbar = doc.ysview.getElementByTagNameAndId(panel.panelNode, "div", "toolbarDiv"); + + // In order to support YSlow running on mutli-tab, + // we need to look up helpDiv using panelNode. + // panel_doc.getElementById('helpDiv') will always find + // helpDiv of YSlow running on the first browser tab. + if (toolbar) { + helplink = doc.ysview.getElementByTagNameAndId(toolbar, "li", "helpLink"); + if (helplink) { + x = helplink.offsetLeft; + y = helplink.offsetTop; + parent = helplink.offsetParent; + while (parent) { + x += parent.offsetLeft; + y += parent.offsetTop; + parent = parent.offsetParent; + } + if (e.clientX >= x && e.clientY >= y && e.clientX < x + helplink.offsetWidth && e.clientY < y + helplink.offsetHeight) { /* clicking on help link, do nothing */ + return; + } + } + help = doc.ysview.getElementByTagNameAndId(toolbar, "div", "helpDiv"); + } + if (help && help.style.visibility === "visible") { + help.style.visibility = "hidden"; + } + }); + + YSLOW.util.addEventListener(this.panelNode, 'scroll', function (e) { + var doc = FBL.getContentView(panel.document); + var overlay = doc.ysview.modaldlg; + + if (overlay && overlay.style.display === "block") { + overlay.style.top = panel.panelNode.scrollTop + 'px'; + overlay.style.left = panel.panelNode.scrollLeft + 'px'; + } + }); + + YSLOW.util.addEventListener(this.panelNode, 'mouseover', function (e) { + var rule; + + if (e.target && typeof e.target === "object") { + if (e.target.nodeName === "LABEL" && e.target.className === "rules") { + if (e.target.firstChild && e.target.firstChild.nodeName === "INPUT" && e.target.firstChild.type === "checkbox") { + rule = YSLOW.controller.getRule(e.target.firstChild.value); + var doc = FBL.getContentView(panel.document); + doc.ysview.tooltip.show('' + rule.name + '

    ' + rule.info, e.target); + } + } + } + }); + + YSLOW.util.addEventListener(this.panelNode, 'mouseout', function (e) { + var doc = FBL.getContentView(panel.document); + doc.ysview.tooltip.hide(); + }); + + YSLOW.util.addEventListener(this.panel_doc.defaultView || + this.panel_doc.parentWindow, 'resize', function (e) { + var doc = FBL.getContentView(panel.document); + var overlay = doc.ysview.modaldlg; + + if (overlay && overlay.style.display === "block") { + overlay.style.display = "none"; + } + }); + +}; + +YSLOW.view.prototype = { + + /** + * Update the document object store in View object. + * @param {Document} doc New Document object to be store in View. + */ + setDocument: function (doc) { + this.panel_doc = doc; + }, + + /** + * Platform independent implementation (optional) + */ + loadCSS: function () {}, + + /** + * @private + */ + addButtonView: function (sButtonId, sHtml) { + var btnView = this.getButtonView(sButtonId); + + if (!btnView) { + btnView = this.panel_doc.createElement("div"); + btnView.style.display = "none"; + this.viewNode.appendChild(btnView); + this.buttonViews[sButtonId] = btnView; + } + + btnView.innerHTML = sHtml; + this.showButtonView(sButtonId); + }, + + /** + * Clear all (changeable) views + */ + clearAllButtonView: function () { + var views = this.buttonViews, + node = this.viewNode, + + remove = function (v) { + if (views.hasOwnProperty(v)) { + node.removeChild(views[v]); + delete views[v]; + } + }; + + remove('ysPerfButton'); + remove('ysCompsButton'); + remove('ysStatsButton'); + }, + + /** + * @private + */ + showButtonView: function (sButtonId) { + var sId, + btnView = this.getButtonView(sButtonId); + + if (!btnView) { + YSLOW.util.dump("ERROR: YSLOW.view.showButtonView: Couldn't find ButtonView '" + sButtonId + "'."); + return; + } + + // Hide all the other button views. + for (sId in this.buttonViews) { + if (this.buttonViews.hasOwnProperty(sId) && sId !== sButtonId) { + this.buttonViews[sId].style.display = "none"; + } + } + + // special handling for copyright text in grade view + if (sButtonId === "ysPerfButton") { + // hide the main copyright text + if (this.copyright) { + this.copyright.style.display = "none"; + } + } else if (this.curButtonId === "ysPerfButton") { + // show the main copyright text + if (this.copyright) { + this.copyright.style.display = "block"; + } + } + + btnView.style.display = "block"; + this.curButtonId = sButtonId; + }, + + /** + * @private + */ + getButtonView: function (sButtonId) { + return (this.buttonViews.hasOwnProperty(sButtonId) ? this.buttonViews[sButtonId] : undefined); + }, + + /** + * @private + */ + setButtonView: function (sButtonId, sHtml) { + var btnView = this.getButtonView(sButtonId); + + if (!btnView) { + YSLOW.util.dump("ERROR: YSLOW.view.setButtonView: Couldn't find ButtonView '" + sButtonId + "'."); + return; + } + + btnView.innerHTML = sHtml; + this.showButtonView(sButtonId); + }, + + /** + * Show landing page. + */ + setSplashView: function (hideAutoRun, showAntiIframe, hideToolsInfo /*TODO: remove once tools are working*/) { + var sHtml, + title = 'Grade your web pages with YSlow', + header = 'YSlow gives you:', + text = '
  • Grade based on the performance (you can define your own rules)
  • Summary of the Components in the page
  • Chart with statistics
  • Tools including Smush.It and JSLint
  • ', + more_info_text = 'Learn more about YSlow and YDN'; + + if (YSLOW.doc.splash) { + if (YSLOW.doc.splash.title) { + title = YSLOW.doc.splash.title; + } + if (YSLOW.doc.splash.content) { + if (YSLOW.doc.splash.content.header) { + header = YSLOW.doc.splash.content.header; + } + if (YSLOW.doc.splash.content.text) { + text = YSLOW.doc.splash.content.text; + } + } + if (YSLOW.doc.splash.more_info) { + more_info_text = YSLOW.doc.splash.more_info; + } + } + + /* TODO: remove once tools are working */ + if (typeof hideToolsInfo !== 'undefined') { + YSLOW.hideToolsInfo = hideToolsInfo; + } else { + hideToolsInfo = YSLOW.hideToolsInfo; + } + if (hideToolsInfo) { + // nasty :-P + text = text.replace(/
  • Tools[^<]+<\/li>/, ''); + } + + sHtml = '
    ' + '
    ' + '' + '

    ' + title + '

    ' + '

    ' + header + '

      ' + text + '
    '; + + if (typeof hideAutoRun !== 'undefined') { + YSLOW.hideAutoRun = hideAutoRun; + } else { + hideAutoRun = YSLOW.hideAutoRun; + } + if (!hideAutoRun) { + sHtml += '
  • '; + + this.addButtonView('panel_about', sHtml); + }, + + /** + * Show progress bar. + */ + genProgressView: function () { + var sBody = '

    Finding components in the page:

    ' + '
    ' + '

    Getting component information:

    ' + '
    start...
    '; + + this.setButtonView('panel_about', sBody); + }, + + /** + * Update progress bar with passed info. + * @param {String} progress_type Type of progress info: either 'peel' or 'fetch'. + * @param {Object} progress_info + *
      For peel: + *
    • current_step - {Number} current phase of peeling
    • + *
    • total_step - {Number} total number peeling phases
    • + *
    • message - {String} Progress message
    • + *
    + *
      For fetch: + *
    • current - {Number} Number of components already downloaded
    • + *
    • total - {Number} Total number of componetns to be downloaded
    • + *
    • last_component_url - {String} URL of the last downloaded component.
    • + *
    + */ + updateProgressView: function (progress_type, progress_info) { + var outerbar, progbar, progtext, percent, view, maxwidth, width, left, + message = ''; + + if (this.curButtonId === 'panel_about') { + view = this.getButtonView(this.curButtonId); + + if (progress_type === 'peel') { + outerbar = this.getElementByTagNameAndId(view, 'div', 'peelprogress'); + progbar = this.getElementByTagNameAndId(view, 'div', 'progbar'); + progtext = this.getElementByTagNameAndId(view, 'div', 'progtext'); + message = progress_info.message; + percent = (progress_info.current_step * 100) / progress_info.total_step; + } else if (progress_type === 'fetch') { + outerbar = this.getElementByTagNameAndId(view, 'div', 'fetchprogress'); + progbar = this.getElementByTagNameAndId(view, 'div', 'progbar2'); + progtext = this.getElementByTagNameAndId(view, 'div', 'progtext2'); + message = progress_info.last_component_url; + percent = (progress_info.current * 100) / progress_info.total; + } else if (progress_type === 'message') { + progtext = this.getElementByTagNameAndId(view, 'div', 'progtext2'); + if (progtext) { + progtext.innerHTML = progress_info; + } + + return; + } else { + return; + } + } + + if (outerbar && progbar && progtext) { + maxwidth = outerbar.clientWidth; + + if (percent < 0) { + percent = 0; + } + if (percent > 100) { + percent = 100; + } + + percent = 100 - percent; + width = (maxwidth * percent) / 100; + if (width > maxwidth) { + width = maxwidth; + } + left = maxwidth - parseInt(width, 10); + progbar.style.width = parseInt(width, 10) + "px"; + progbar.style.left = parseInt(left, 10) + "px"; + + progtext.innerHTML = message; + } + }, + + /** + * @private + */ + updateStatusBar: function (doc) { + var size, grade, result, info, url, + yslow = YSLOW, + util = yslow.util, + view = yslow.view, + pref = util.Preference, + yscontext = this.yscontext; + + if (!yscontext.PAGE.statusbar) { + // only set the bar once + yscontext.PAGE.statusbar = true; + + // If some of the info isn't available, we have to run some code. + if (!yscontext.PAGE.overallScore) { + // run lint + yslow.controller.lint(doc, yscontext); + } + if (!yscontext.PAGE.totalSize) { + // collect stats + yscontext.collectStats(); + } + + size = util.kbSize(yscontext.PAGE.totalSize); + grade = util.prettyScore(yscontext.PAGE.overallScore); + + view.setStatusBar(grade, 'yslow_status_grade'); + view.setStatusBar(size, 'yslow_status_size'); + + // Send a beacon. + if (pref.getPref('optinBeacon', false)) { + info = pref.getPref('beaconInfo', 'basic'), + url = pref.getPref('beaconUrl', + 'http://rtblab.pclick.yahoo.com/images/ysb.gif'); + result = util.getResults(yscontext, info); + util.sendBeacon(result, info, url); + } + } + }, + + /** + * @private + */ + getRulesetListSource: function (rulesets) { + var id, custom, + sHtml = '', + defaultRulesetId = YSLOW.controller.getDefaultRulesetId(); + + for (id in rulesets) { + if (rulesets[id]) { + sHtml += '