diff --git a/assets/css/bootstrap-overrides.css b/assets/css/bootstrap-overrides.css
new file mode 100644
index 000000000..0f30cf2b1
--- /dev/null
+++ b/assets/css/bootstrap-overrides.css
@@ -0,0 +1,194 @@
+/* Align the footer to the center*/
+ footer p {
+ text-align: center;
+ }
+
+ /* Font used in the number in summary*/
+ .large {
+ font-family: Tahoma,Impact,Verdana,sans-serif;
+ font-size: 32px;
+ line-height: 38px;
+ font-weight:bold;
+ }
+
+ /* Popovers on summary page*/
+ .popover-content {
+ color: black;
+ font-size: 16px;
+ line-height: 18px;
+ font-weight:normal;
+ }
+
+ /* Popovers on summary page*/
+ .popover-title {
+ color: black;
+ font-weight:bold;
+ font-size: 16px;
+ line-height: 18px;
+ }
+
+ a.clean:hover{
+ color: #000000;
+ text-decoration: none;
+ }
+
+ a.clean {
+ color: #000000;
+ text-decoration: none;
+ }
+
+ /* Font used in the number in summary*/
+ body {
+ font-family: Tahoma,Verdana,sans-serif;
+ }
+
+ h1, h2, h3, h4, h5, h6 {
+ font-family: Tahoma,Verdana,sans-serif;
+ }
+
+ .navbar {
+ border: 0;
+ border-radius: 0;
+ }
+
+ .navbar-collapse {
+ border: 0;
+ box-shadow: none;
+ }
+
+ /* fix for the logo*/
+ .navbar-brand {
+ padding: 0px 0px;
+ max-width: 250px;
+ }
+
+ .navbar-default {
+ color: white;
+ background-color: #0095d2;
+ }
+
+ .navbar-default .navbar-nav>.active>a, .navbar-default .navbar-nav>.active>a:hover, .navbar-default .navbar-nav>.active>a:focus {
+ color: white;
+ background-color: #0073b0;
+ }
+
+ .navbar-default .navbar-nav>li>a:hover, .navbar-default .navbar-nav>li>a:focus {
+ color: white;
+ }
+
+ .navbar-default .navbar-nav>li>a {
+ color: white;
+ }
+
+ /* When displaying urls, making sure they don't break pages*/
+ .url {
+ word-wrap: break-word;
+ white-space: normal;
+ }
+
+ .navbar-default .navbar-toggle {
+ border: 1px solid #fff;
+ }
+
+ .navbar-default .navbar-toggle .icon-bar {
+ background-color: #fff;
+ }
+
+ .navbar-default .navbar-toggle:hover, .navbar-toggle:focus {
+ background-color: #0073b0;
+ }
+
+ /* Taking care of urls not breaking the table in pages*/
+ .nobreak-pages {
+ word-wrap: break-word;
+ max-width: 290px;
+ white-space: normal;
+ }
+
+ /* Taking care of urls not breaking the table in pages*/
+ .nobreak-page-url {
+ word-wrap: break-word;
+ max-width: 500px;
+ white-space: normal;
+ }
+
+ /* Taking care of assets not breaking */
+ .nobreak-page {
+ word-wrap: break-word;
+ max-width: 450px;
+ white-space: normal;
+ }
+
+ /* Taking care of urls not breaking the table in assets*/
+ .nobreak-asset-url {
+ word-wrap: break-word;
+ max-width: 500px;
+ white-space: normal;
+ }
+
+ /* Take care of response headers modal*/
+ .headers {
+ word-wrap: break-word;
+ max-width: 400px;
+ white-space: normal;
+ }
+
+ /* Fixes for the summary boxes*/
+
+ a.alert-success:hover {
+ text-decoration: none;
+ color: #468847;
+ cursor:pointer;
+ }
+
+ a.alert-danger:hover {
+ text-decoration: none;
+ color: #B94A48;
+ cursor:pointer;
+ }
+
+ a.alert-warning:hover {
+ text-decoration: none;
+ color: #C09853;
+ cursor:pointer;
+ }
+
+ a.alert-warning {
+ text-decoration: none;
+ color: #C09853;
+ cursor:pointer;
+ }
+
+ a.alert-info {
+ text-decoration: none;
+ color: #3A87AD;
+ cursor:pointer;
+ }
+
+ .alert-info {
+ background-color:#D9EDF7;
+ border-color:#BCE8F1;
+ color:#3A87AD;
+ }
+
+ th[data-sort]{
+ cursor:pointer;
+ color: #0095d2;
+ font-weight:bold;
+ }
+
+ th[data-sort]:hover{
+ text-decoration:underline;
+ }
+
+ /* Small phones*/
+ @media (max-width:480px) {
+
+ .nobreak-asset-url {
+ max-width: 70px;
+ }
+
+ .nobreak-page {
+ max-width: 70px;
+ }
+}
diff --git a/assets/css/bootstrap.min.css b/assets/css/bootstrap.min.css
new file mode 100644
index 000000000..a553c4f5e
--- /dev/null
+++ b/assets/css/bootstrap.min.css
@@ -0,0 +1,9 @@
+/*!
+ * Bootstrap v3.0.0
+ *
+ * Copyright 2013 Twitter, Inc
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Designed and built with all the love in the world by @mdo and @fat.
+ *//*! normalize.css v2.1.0 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden]{display:none}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{margin:.67em 0;font-size:2em}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}hr{height:0;-moz-box-sizing:content-box;box-sizing:content-box}mark{color:#000;background:#ff0}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}pre{white-space:pre-wrap}q{quotes:"\201C" "\201D" "\2018" "\2019"}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:0}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid #c0c0c0}legend{padding:0;border:0}button,input,select,textarea{margin:0;font-family:inherit;font-size:100%}button,input{line-height:normal}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}button[disabled],html input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{padding:0;box-sizing:border-box}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:2cm .5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}*,*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.428571429;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}button,input,select[multiple],textarea{background-image:none}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}img{vertical-align:middle}.img-responsive{display:block;height:auto;max-width:100%}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;height:auto;max-width:100%;padding:4px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);border:0}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16.099999999999998px;font-weight:200;line-height:1.4}@media(min-width:768px){.lead{font-size:21px}}small{font-size:85%}cite{font-style:normal}.text-muted{color:#999}.text-primary{color:#428bca}.text-warning{color:#c09853}.text-danger{color:#b94a48}.text-success{color:#468847}.text-info{color:#3a87ad}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:500;line-height:1.1}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{margin-top:20px;margin-bottom:10px}h4,h5,h6{margin-top:10px;margin-bottom:10px}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}h1 small,.h1 small{font-size:24px}h2 small,.h2 small{font-size:18px}h3 small,.h3 small,h4 small,.h4 small{font-size:14px}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-bottom:20px}dt,dd{line-height:1.428571429}dt{font-weight:bold}dd{margin-left:0}@media(min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{font-size:17.5px;font-weight:300;line-height:1.25}blockquote p:last-child{margin-bottom:0}blockquote small{display:block;line-height:1.428571429;color:#999}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small{text-align:right}blockquote.pull-right small:before{content:''}blockquote.pull-right small:after{content:'\00A0 \2014'}q:before,q:after,blockquote:before,blockquote:after{content:""}address{display:block;margin-bottom:20px;font-style:normal;line-height:1.428571429}code,pre{font-family:Monaco,Menlo,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;white-space:nowrap;background-color:#f9f2f4;border-radius:4px}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.428571429;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre.prettyprint{margin-bottom:20px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.row{margin-right:-15px;margin-left:-15px}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12,.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12,.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12,.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11{float:left}.col-xs-1{width:8.333333333333332%}.col-xs-2{width:16.666666666666664%}.col-xs-3{width:25%}.col-xs-4{width:33.33333333333333%}.col-xs-5{width:41.66666666666667%}.col-xs-6{width:50%}.col-xs-7{width:58.333333333333336%}.col-xs-8{width:66.66666666666666%}.col-xs-9{width:75%}.col-xs-10{width:83.33333333333334%}.col-xs-11{width:91.66666666666666%}.col-xs-12{width:100%}@media(min-width:768px){.container{max-width:750px}.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11{float:left}.col-sm-1{width:8.333333333333332%}.col-sm-2{width:16.666666666666664%}.col-sm-3{width:25%}.col-sm-4{width:33.33333333333333%}.col-sm-5{width:41.66666666666667%}.col-sm-6{width:50%}.col-sm-7{width:58.333333333333336%}.col-sm-8{width:66.66666666666666%}.col-sm-9{width:75%}.col-sm-10{width:83.33333333333334%}.col-sm-11{width:91.66666666666666%}.col-sm-12{width:100%}.col-sm-push-1{left:8.333333333333332%}.col-sm-push-2{left:16.666666666666664%}.col-sm-push-3{left:25%}.col-sm-push-4{left:33.33333333333333%}.col-sm-push-5{left:41.66666666666667%}.col-sm-push-6{left:50%}.col-sm-push-7{left:58.333333333333336%}.col-sm-push-8{left:66.66666666666666%}.col-sm-push-9{left:75%}.col-sm-push-10{left:83.33333333333334%}.col-sm-push-11{left:91.66666666666666%}.col-sm-pull-1{right:8.333333333333332%}.col-sm-pull-2{right:16.666666666666664%}.col-sm-pull-3{right:25%}.col-sm-pull-4{right:33.33333333333333%}.col-sm-pull-5{right:41.66666666666667%}.col-sm-pull-6{right:50%}.col-sm-pull-7{right:58.333333333333336%}.col-sm-pull-8{right:66.66666666666666%}.col-sm-pull-9{right:75%}.col-sm-pull-10{right:83.33333333333334%}.col-sm-pull-11{right:91.66666666666666%}.col-sm-offset-1{margin-left:8.333333333333332%}.col-sm-offset-2{margin-left:16.666666666666664%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-4{margin-left:33.33333333333333%}.col-sm-offset-5{margin-left:41.66666666666667%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-7{margin-left:58.333333333333336%}.col-sm-offset-8{margin-left:66.66666666666666%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-10{margin-left:83.33333333333334%}.col-sm-offset-11{margin-left:91.66666666666666%}}@media(min-width:992px){.container{max-width:970px}.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11{float:left}.col-md-1{width:8.333333333333332%}.col-md-2{width:16.666666666666664%}.col-md-3{width:25%}.col-md-4{width:33.33333333333333%}.col-md-5{width:41.66666666666667%}.col-md-6{width:50%}.col-md-7{width:58.333333333333336%}.col-md-8{width:66.66666666666666%}.col-md-9{width:75%}.col-md-10{width:83.33333333333334%}.col-md-11{width:91.66666666666666%}.col-md-12{width:100%}.col-md-push-0{left:auto}.col-md-push-1{left:8.333333333333332%}.col-md-push-2{left:16.666666666666664%}.col-md-push-3{left:25%}.col-md-push-4{left:33.33333333333333%}.col-md-push-5{left:41.66666666666667%}.col-md-push-6{left:50%}.col-md-push-7{left:58.333333333333336%}.col-md-push-8{left:66.66666666666666%}.col-md-push-9{left:75%}.col-md-push-10{left:83.33333333333334%}.col-md-push-11{left:91.66666666666666%}.col-md-pull-0{right:auto}.col-md-pull-1{right:8.333333333333332%}.col-md-pull-2{right:16.666666666666664%}.col-md-pull-3{right:25%}.col-md-pull-4{right:33.33333333333333%}.col-md-pull-5{right:41.66666666666667%}.col-md-pull-6{right:50%}.col-md-pull-7{right:58.333333333333336%}.col-md-pull-8{right:66.66666666666666%}.col-md-pull-9{right:75%}.col-md-pull-10{right:83.33333333333334%}.col-md-pull-11{right:91.66666666666666%}.col-md-offset-0{margin-left:0}.col-md-offset-1{margin-left:8.333333333333332%}.col-md-offset-2{margin-left:16.666666666666664%}.col-md-offset-3{margin-left:25%}.col-md-offset-4{margin-left:33.33333333333333%}.col-md-offset-5{margin-left:41.66666666666667%}.col-md-offset-6{margin-left:50%}.col-md-offset-7{margin-left:58.333333333333336%}.col-md-offset-8{margin-left:66.66666666666666%}.col-md-offset-9{margin-left:75%}.col-md-offset-10{margin-left:83.33333333333334%}.col-md-offset-11{margin-left:91.66666666666666%}}@media(min-width:1200px){.container{max-width:1170px}.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11{float:left}.col-lg-1{width:8.333333333333332%}.col-lg-2{width:16.666666666666664%}.col-lg-3{width:25%}.col-lg-4{width:33.33333333333333%}.col-lg-5{width:41.66666666666667%}.col-lg-6{width:50%}.col-lg-7{width:58.333333333333336%}.col-lg-8{width:66.66666666666666%}.col-lg-9{width:75%}.col-lg-10{width:83.33333333333334%}.col-lg-11{width:91.66666666666666%}.col-lg-12{width:100%}.col-lg-push-0{left:auto}.col-lg-push-1{left:8.333333333333332%}.col-lg-push-2{left:16.666666666666664%}.col-lg-push-3{left:25%}.col-lg-push-4{left:33.33333333333333%}.col-lg-push-5{left:41.66666666666667%}.col-lg-push-6{left:50%}.col-lg-push-7{left:58.333333333333336%}.col-lg-push-8{left:66.66666666666666%}.col-lg-push-9{left:75%}.col-lg-push-10{left:83.33333333333334%}.col-lg-push-11{left:91.66666666666666%}.col-lg-pull-0{right:auto}.col-lg-pull-1{right:8.333333333333332%}.col-lg-pull-2{right:16.666666666666664%}.col-lg-pull-3{right:25%}.col-lg-pull-4{right:33.33333333333333%}.col-lg-pull-5{right:41.66666666666667%}.col-lg-pull-6{right:50%}.col-lg-pull-7{right:58.333333333333336%}.col-lg-pull-8{right:66.66666666666666%}.col-lg-pull-9{right:75%}.col-lg-pull-10{right:83.33333333333334%}.col-lg-pull-11{right:91.66666666666666%}.col-lg-offset-0{margin-left:0}.col-lg-offset-1{margin-left:8.333333333333332%}.col-lg-offset-2{margin-left:16.666666666666664%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-4{margin-left:33.33333333333333%}.col-lg-offset-5{margin-left:41.66666666666667%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-7{margin-left:58.333333333333336%}.col-lg-offset-8{margin-left:66.66666666666666%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-10{margin-left:83.33333333333334%}.col-lg-offset-11{margin-left:91.66666666666666%}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table thead>tr>th,.table tbody>tr>th,.table tfoot>tr>th,.table thead>tr>td,.table tbody>tr>td,.table tfoot>tr>td{padding:8px;line-height:1.428571429;vertical-align:top;border-top:1px solid #ddd}.table thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table caption+thead tr:first-child th,.table colgroup+thead tr:first-child th,.table thead:first-child tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child td{border-top:0}.table tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed thead>tr>th,.table-condensed tbody>tr>th,.table-condensed tfoot>tr>th,.table-condensed thead>tr>td,.table-condensed tbody>tr>td,.table-condensed tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*="col-"]{display:table-column;float:none}table td[class*="col-"],table th[class*="col-"]{display:table-cell;float:none}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8;border-color:#d6e9c6}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td{background-color:#d0e9c6;border-color:#c9e2b3}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede;border-color:#eed3d7}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td{background-color:#ebcccc;border-color:#e6c1c7}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3;border-color:#fbeed5}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td{background-color:#faf2cc;border-color:#f8e5be}@media(max-width:768px){.table-responsive{width:100%;margin-bottom:15px;overflow-x:scroll;overflow-y:hidden;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0;background-color:#fff}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>thead>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>thead>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}select[multiple],select[size]{height:auto}select optgroup{font-family:inherit;font-size:inherit;font-style:inherit}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}input[type="number"]::-webkit-outer-spin-button,input[type="number"]::-webkit-inner-spin-button{height:auto}.form-control:-moz-placeholder{color:#999}.form-control::-moz-placeholder{color:#999}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.428571429;color:#555;vertical-align:middle;background-color:#fff;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6)}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee}textarea.form-control{height:auto}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;padding-left:20px;margin-top:10px;margin-bottom:10px;vertical-align:middle}.radio label,.checkbox label{display:inline;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:normal;vertical-align:middle;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm{height:auto}.input-lg{height:45px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:45px;line-height:45px}textarea.input-lg{height:auto}.has-warning .help-block,.has-warning .control-label{color:#c09853}.has-warning .form-control{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e}.has-warning .input-group-addon{color:#c09853;background-color:#fcf8e3;border-color:#c09853}.has-error .help-block,.has-error .control-label{color:#b94a48}.has-error .form-control{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.has-error .input-group-addon{color:#b94a48;background-color:#f2dede;border-color:#b94a48}.has-success .help-block,.has-success .control-label{color:#468847}.has-success .form-control{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b}.has-success .input-group-addon{color:#468847;background-color:#dff0d8;border-color:#468847}.form-control-static{padding-top:7px;margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media(min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block}.form-inline .radio,.form-inline .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:none;margin-left:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}@media(min-width:768px){.form-horizontal .control-label{text-align:right}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:normal;line-height:1.428571429;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;border:1px solid transparent;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-link{font-weight:normal;color:#428bca;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm,.btn-xs{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs{padding:1px 5px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}@font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons-halflings-regular.eot');src:url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';-webkit-font-smoothing:antialiased;font-style:normal;font-weight:normal;line-height:1}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-print:before{content:"\e045"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-briefcase:before{content:"\1f4bc"}.glyphicon-calendar:before{content:"\1f4c5"}.glyphicon-pushpin:before{content:"\1f4cc"}.glyphicon-paperclip:before{content:"\1f4ce"}.glyphicon-camera:before{content:"\1f4f7"}.glyphicon-lock:before{content:"\1f512"}.glyphicon-bell:before{content:"\1f514"}.glyphicon-bookmark:before{content:"\1f516"}.glyphicon-fire:before{content:"\1f525"}.glyphicon-wrench:before{content:"\1f527"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid #000;border-right:4px solid transparent;border-bottom:0 dotted;border-left:4px solid transparent;content:""}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.428571429;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#fff;text-decoration:none;background-color:#428bca}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#428bca;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.428571429;color:#999}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0 dotted;border-bottom:4px solid #000;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media(min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}}.btn-default .caret{border-top-color:#333}.btn-primary .caret,.btn-success .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret{border-top-color:#fff}.dropup .btn-default .caret{border-bottom-color:#333}.dropup .btn-primary .caret,.dropup .btn-success .caret,.dropup .btn-warning .caret,.dropup .btn-danger .caret,.dropup .btn-info .caret{border-bottom-color:#fff}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar .btn-group{float:left}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group,.btn-toolbar>.btn-group+.btn-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group-xs>.btn{padding:5px 10px;padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-bottom-left-radius:4px;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child>.btn:last-child,.btn-group-vertical>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;border-collapse:separate;table-layout:fixed}.btn-group-justified .btn{display:table-cell;float:none;width:1%}[data-toggle="buttons"]>.btn>input[type="radio"],[data-toggle="buttons"]>.btn>input[type="checkbox"]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group.col{float:none;padding-right:0;padding-left:0}.input-group .form-control{width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:45px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:45px;line-height:45px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:normal;line-height:1;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-4px}.input-group-btn>.btn:hover,.input-group-btn>.btn:active{z-index:2}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.428571429;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center}@media(min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}}.nav-tabs.nav-justified>li>a{margin-right:0;border-bottom:1px solid #ddd}.nav-tabs.nav-justified>.active>a{border-bottom-color:#fff}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:5px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{text-align:center}@media(min-width:768px){.nav-justified>li{display:table-cell;width:1%}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-bottom:1px solid #ddd}.nav-tabs-justified>.active>a{border-bottom-color:#fff}.tabbable:before,.tabbable:after{display:table;content:" "}.tabbable:after{clear:both}.tabbable:before,.tabbable:after{display:table;content:" "}.tabbable:after{clear:both}.tab-content>.tab-pane,.pill-content>.pill-pane{display:none}.tab-content>.active,.pill-content>.active{display:block}.nav .caret{border-top-color:#428bca;border-bottom-color:#428bca}.nav a:hover .caret{border-top-color:#2a6496;border-bottom-color:#2a6496}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;z-index:1000;min-height:50px;margin-bottom:20px;border:1px solid transparent}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}@media(min-width:768px){.navbar{border-radius:4px}}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}@media(min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse.in{overflow-y:auto}@media(min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-collapse .navbar-nav.navbar-left:first-child{margin-left:-15px}.navbar-collapse .navbar-nav.navbar-right:last-child{margin-right:-15px}.navbar-collapse .navbar-text:last-child{margin-right:0}}.container>.navbar-header,.container>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media(min-width:768px){.container>.navbar-header,.container>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{border-width:0 0 1px}@media(min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;border-width:0 0 1px}@media(min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;z-index:1030}.navbar-fixed-bottom{bottom:0;margin-bottom:0}.navbar-brand{float:left;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media(min-width:768px){.navbar>.container .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;border:1px solid transparent;border-radius:4px}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media(min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media(max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media(min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}@media(min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}@media(min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{float:none;margin-left:0}}@media(max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media(min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-nav.pull-right>li>.dropdown-menu,.navbar-nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-text{float:left;margin-top:15px;margin-bottom:15px}@media(min-width:768px){.navbar-text{margin-right:15px;margin-left:15px}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#ccc}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e6e6e6}.navbar-default .navbar-nav>.dropdown>a:hover .caret,.navbar-default .navbar-nav>.dropdown>a:focus .caret{border-top-color:#333;border-bottom-color:#333}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.open>a .caret,.navbar-default .navbar-nav>.open>a:hover .caret,.navbar-default .navbar-nav>.open>a:focus .caret{border-top-color:#555;border-bottom-color:#555}.navbar-default .navbar-nav>.dropdown>a .caret{border-top-color:#777;border-bottom-color:#777}@media(max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.dropdown>a:hover .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .navbar-nav>.dropdown>a .caret{border-top-color:#999;border-bottom-color:#999}.navbar-inverse .navbar-nav>.open>a .caret,.navbar-inverse .navbar-nav>.open>a:hover .caret,.navbar-inverse .navbar-nav>.open>a:focus .caret{border-top-color:#fff;border-bottom-color:#fff}@media(max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.428571429;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{background-color:#eee}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;cursor:default;background-color:#428bca;border-color:#428bca}.pagination>.disabled>span,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:#808080}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#999;border-radius:10px}.badge:empty{display:none}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.btn .badge{position:relative;top:-1px}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;font-size:21px;font-weight:200;line-height:2.1428571435;color:inherit;background-color:#eee}.jumbotron h1{line-height:1;color:inherit}.jumbotron p{line-height:1.4}.container .jumbotron{border-radius:6px}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1{font-size:63px}}.thumbnail{display:inline-block;display:block;height:auto;max-width:100%;padding:4px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img{display:block;height:auto;max-width:100%}a.thumbnail:hover,a.thumbnail:focus{border-color:#428bca}.thumbnail>img{margin-right:auto;margin-left:auto}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#356635}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#2d6987}.alert-warning{color:#c09853;background-color:#fcf8e3;border-color:#fbeed5}.alert-warning hr{border-top-color:#f8e5be}.alert-warning .alert-link{color:#a47e3c}.alert-danger{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-danger hr{border-top-color:#e6c1c7}.alert-danger .alert-link{color:#953b39}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:0 0}to{background-position:40px 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar{background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0}.panel>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel>.list-group .list-group-item:last-child{border-bottom:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table{margin-bottom:0}.panel>.panel-body+.table{border-top:1px solid #ddd}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-title{margin-top:0;margin-bottom:0;font-size:16px}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-group .panel{margin-bottom:0;overflow:hidden;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:#428bca}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:#d6e9c6}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#d6e9c6}.panel-warning{border-color:#fbeed5}.panel-warning>.panel-heading{color:#c09853;background-color:#fcf8e3;border-color:#fbeed5}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:#fbeed5}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#fbeed5}.panel-danger{border-color:#eed3d7}.panel-danger>.panel-heading{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:#eed3d7}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#eed3d7}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:#bce8f1}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#bce8f1}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}body.modal-open,.modal-open .navbar-fixed-top,.modal-open .navbar-fixed-bottom{margin-right:15px}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;display:none;overflow:auto;overflow-y:scroll}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0)}.modal-dialog{z-index:1050;width:auto;padding:10px;margin-right:auto;margin-left:auto}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);background-clip:padding-box}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1030;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{min-height:16.428571429px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.428571429}.modal-body{position:relative;padding:20px}.modal-footer{padding:19px 20px 20px;margin-top:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media screen and (min-width:768px){.modal-dialog{right:auto;left:50%;width:600px;padding-top:30px;padding-bottom:30px}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}}.tooltip{position:absolute;z-index:1030;display:block;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.top-right .tooltip-arrow{right:5px;bottom:0;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-bottom-color:#000;border-width:0 5px 5px}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0;content:" "}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0;content:" "}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0;content:" "}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0;content:" "}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;height:auto;max-width:100%;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6);opacity:.5;filter:alpha(opacity=50)}.carousel-control.left{background-image:-webkit-gradient(linear,0 top,100% top,from(rgba(0,0,0,0.5)),to(rgba(0,0,0,0.0001)));background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,0.5) 0),color-stop(rgba(0,0,0,0.0001) 100%));background-image:-moz-linear-gradient(left,rgba(0,0,0,0.5) 0,rgba(0,0,0,0.0001) 100%);background-image:linear-gradient(to right,rgba(0,0,0,0.5) 0,rgba(0,0,0,0.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000',endColorstr='#00000000',GradientType=1)}.carousel-control.right{right:0;left:auto;background-image:-webkit-gradient(linear,0 top,100% top,from(rgba(0,0,0,0.0001)),to(rgba(0,0,0,0.5)));background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,0.0001) 0),color-stop(rgba(0,0,0,0.5) 100%));background-image:-moz-linear-gradient(left,rgba(0,0,0,0.0001) 0,rgba(0,0,0,0.5) 100%);background-image:linear-gradient(to right,rgba(0,0,0,0.0001) 0,rgba(0,0,0,0.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000',endColorstr='#80000000',GradientType=1)}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;left:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;margin-left:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;margin-left:-15px;font-size:30px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after{display:table;content:" "}.clearfix:after{clear:both}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.affix{position:fixed}@-ms-viewport{width:device-width}@media screen and (max-width:400px){@-ms-viewport{width:320px}}.hidden{display:none!important;visibility:hidden!important}.visible-xs{display:none!important}tr.visible-xs{display:none!important}th.visible-xs,td.visible-xs{display:none!important}@media(max-width:767px){.visible-xs{display:block!important}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-xs.visible-sm{display:block!important}tr.visible-xs.visible-sm{display:table-row!important}th.visible-xs.visible-sm,td.visible-xs.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-xs.visible-md{display:block!important}tr.visible-xs.visible-md{display:table-row!important}th.visible-xs.visible-md,td.visible-xs.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-xs.visible-lg{display:block!important}tr.visible-xs.visible-lg{display:table-row!important}th.visible-xs.visible-lg,td.visible-xs.visible-lg{display:table-cell!important}}.visible-sm{display:none!important}tr.visible-sm{display:none!important}th.visible-sm,td.visible-sm{display:none!important}@media(max-width:767px){.visible-sm.visible-xs{display:block!important}tr.visible-sm.visible-xs{display:table-row!important}th.visible-sm.visible-xs,td.visible-sm.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-sm{display:block!important}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-sm.visible-md{display:block!important}tr.visible-sm.visible-md{display:table-row!important}th.visible-sm.visible-md,td.visible-sm.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-sm.visible-lg{display:block!important}tr.visible-sm.visible-lg{display:table-row!important}th.visible-sm.visible-lg,td.visible-sm.visible-lg{display:table-cell!important}}.visible-md{display:none!important}tr.visible-md{display:none!important}th.visible-md,td.visible-md{display:none!important}@media(max-width:767px){.visible-md.visible-xs{display:block!important}tr.visible-md.visible-xs{display:table-row!important}th.visible-md.visible-xs,td.visible-md.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-md.visible-sm{display:block!important}tr.visible-md.visible-sm{display:table-row!important}th.visible-md.visible-sm,td.visible-md.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-md{display:block!important}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-md.visible-lg{display:block!important}tr.visible-md.visible-lg{display:table-row!important}th.visible-md.visible-lg,td.visible-md.visible-lg{display:table-cell!important}}.visible-lg{display:none!important}tr.visible-lg{display:none!important}th.visible-lg,td.visible-lg{display:none!important}@media(max-width:767px){.visible-lg.visible-xs{display:block!important}tr.visible-lg.visible-xs{display:table-row!important}th.visible-lg.visible-xs,td.visible-lg.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-lg.visible-sm{display:block!important}tr.visible-lg.visible-sm{display:table-row!important}th.visible-lg.visible-sm,td.visible-lg.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-lg.visible-md{display:block!important}tr.visible-lg.visible-md{display:table-row!important}th.visible-lg.visible-md,td.visible-lg.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-lg{display:block!important}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}.hidden-xs{display:block!important}tr.hidden-xs{display:table-row!important}th.hidden-xs,td.hidden-xs{display:table-cell!important}@media(max-width:767px){.hidden-xs{display:none!important}tr.hidden-xs{display:none!important}th.hidden-xs,td.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-xs.hidden-sm{display:none!important}tr.hidden-xs.hidden-sm{display:none!important}th.hidden-xs.hidden-sm,td.hidden-xs.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-xs.hidden-md{display:none!important}tr.hidden-xs.hidden-md{display:none!important}th.hidden-xs.hidden-md,td.hidden-xs.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-xs.hidden-lg{display:none!important}tr.hidden-xs.hidden-lg{display:none!important}th.hidden-xs.hidden-lg,td.hidden-xs.hidden-lg{display:none!important}}.hidden-sm{display:block!important}tr.hidden-sm{display:table-row!important}th.hidden-sm,td.hidden-sm{display:table-cell!important}@media(max-width:767px){.hidden-sm.hidden-xs{display:none!important}tr.hidden-sm.hidden-xs{display:none!important}th.hidden-sm.hidden-xs,td.hidden-sm.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}tr.hidden-sm{display:none!important}th.hidden-sm,td.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-sm.hidden-md{display:none!important}tr.hidden-sm.hidden-md{display:none!important}th.hidden-sm.hidden-md,td.hidden-sm.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-sm.hidden-lg{display:none!important}tr.hidden-sm.hidden-lg{display:none!important}th.hidden-sm.hidden-lg,td.hidden-sm.hidden-lg{display:none!important}}.hidden-md{display:block!important}tr.hidden-md{display:table-row!important}th.hidden-md,td.hidden-md{display:table-cell!important}@media(max-width:767px){.hidden-md.hidden-xs{display:none!important}tr.hidden-md.hidden-xs{display:none!important}th.hidden-md.hidden-xs,td.hidden-md.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-md.hidden-sm{display:none!important}tr.hidden-md.hidden-sm{display:none!important}th.hidden-md.hidden-sm,td.hidden-md.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}tr.hidden-md{display:none!important}th.hidden-md,td.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-md.hidden-lg{display:none!important}tr.hidden-md.hidden-lg{display:none!important}th.hidden-md.hidden-lg,td.hidden-md.hidden-lg{display:none!important}}.hidden-lg{display:block!important}tr.hidden-lg{display:table-row!important}th.hidden-lg,td.hidden-lg{display:table-cell!important}@media(max-width:767px){.hidden-lg.hidden-xs{display:none!important}tr.hidden-lg.hidden-xs{display:none!important}th.hidden-lg.hidden-xs,td.hidden-lg.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-lg.hidden-sm{display:none!important}tr.hidden-lg.hidden-sm{display:none!important}th.hidden-lg.hidden-sm,td.hidden-lg.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-lg.hidden-md{display:none!important}tr.hidden-lg.hidden-md{display:none!important}th.hidden-lg.hidden-md,td.hidden-lg.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-lg{display:none!important}tr.hidden-lg{display:none!important}th.hidden-lg,td.hidden-lg{display:none!important}}.visible-print{display:none!important}tr.visible-print{display:none!important}th.visible-print,td.visible-print{display:none!important}@media print{.visible-print{display:block!important}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}.hidden-print{display:none!important}tr.hidden-print{display:none!important}th.hidden-print,td.hidden-print{display:none!important}}
\ No newline at end of file
diff --git a/assets/fonts/glyphicons-halflings-regular.eot b/assets/fonts/glyphicons-halflings-regular.eot
new file mode 100644
index 000000000..87eaa4342
Binary files /dev/null and b/assets/fonts/glyphicons-halflings-regular.eot differ
diff --git a/assets/fonts/glyphicons-halflings-regular.svg b/assets/fonts/glyphicons-halflings-regular.svg
new file mode 100644
index 000000000..5fee06854
--- /dev/null
+++ b/assets/fonts/glyphicons-halflings-regular.svg
@@ -0,0 +1,228 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/assets/fonts/glyphicons-halflings-regular.ttf b/assets/fonts/glyphicons-halflings-regular.ttf
new file mode 100644
index 000000000..be784dc1d
Binary files /dev/null and b/assets/fonts/glyphicons-halflings-regular.ttf differ
diff --git a/assets/fonts/glyphicons-halflings-regular.woff b/assets/fonts/glyphicons-halflings-regular.woff
new file mode 100644
index 000000000..2cc3e4852
Binary files /dev/null and b/assets/fonts/glyphicons-halflings-regular.woff differ
diff --git a/assets/img/glyphicons-halflings-white.png b/assets/img/glyphicons-halflings-white.png
new file mode 100644
index 000000000..3bf6484a2
Binary files /dev/null and b/assets/img/glyphicons-halflings-white.png differ
diff --git a/assets/img/glyphicons-halflings.png b/assets/img/glyphicons-halflings.png
new file mode 100644
index 000000000..36c3b1ed9
Binary files /dev/null and b/assets/img/glyphicons-halflings.png differ
diff --git a/assets/img/ico/sitespeed.io-114.png b/assets/img/ico/sitespeed.io-114.png
new file mode 100644
index 000000000..8ffab826d
Binary files /dev/null and b/assets/img/ico/sitespeed.io-114.png differ
diff --git a/assets/img/ico/sitespeed.io-144.png b/assets/img/ico/sitespeed.io-144.png
new file mode 100644
index 000000000..74c96381f
Binary files /dev/null and b/assets/img/ico/sitespeed.io-144.png differ
diff --git a/assets/img/ico/sitespeed.io-72.png b/assets/img/ico/sitespeed.io-72.png
new file mode 100644
index 000000000..d996995d5
Binary files /dev/null and b/assets/img/ico/sitespeed.io-72.png differ
diff --git a/assets/img/ico/sitespeed.io.ico b/assets/img/ico/sitespeed.io.ico
new file mode 100644
index 000000000..0f528413b
Binary files /dev/null and b/assets/img/ico/sitespeed.io.ico differ
diff --git a/assets/img/sitespeed-logo.png b/assets/img/sitespeed-logo.png
new file mode 100644
index 000000000..8db557a01
Binary files /dev/null and b/assets/img/sitespeed-logo.png differ
diff --git a/assets/js/bootstrap.min.js b/assets/js/bootstrap.min.js
new file mode 100644
index 000000000..1765631f4
--- /dev/null
+++ b/assets/js/bootstrap.min.js
@@ -0,0 +1,6 @@
+/**
+* bootstrap.js v3.0.0 by @fat and @mdo
+* Copyright 2013 Twitter Inc.
+* http://www.apache.org/licenses/LICENSE-2.0
+*/
+if(!jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(window.jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(window.jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]');if(a.length){var b=this.$element.find("input").prop("checked",!this.$element.hasClass("active")).trigger("change");"radio"===b.prop("type")&&a.find(".active").removeClass("active")}this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(window.jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}this.sliding=!0,f&&this.pause();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)}).emulateTransitionEnd(600)}else{if(this.$element.trigger(j),j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(window.jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?(this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350),void 0):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(window.jQuery),+function(a){"use strict";function b(){a(d).remove(),a(e).each(function(b){var d=c(a(this));d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown")),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown"))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){if("ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('
').insertAfter(a(this)).on("click",b),f.trigger(d=a.Event("show.bs.dropdown")),d.isDefaultPrevented())return;f.toggleClass("open").trigger("shown.bs.dropdown"),e.focus()}return!1}},f.prototype.keydown=function(b){if(/(38|40|27)/.test(b.keyCode)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var f=c(d),g=f.hasClass("open");if(!g||g&&27==b.keyCode)return 27==b.which&&f.find(e).focus(),d.click();var h=a("[role=menu] li:not(.divider):visible a",f);if(h.length){var i=h.index(h.filter(":focus"));38==b.keyCode&&i>0&&i--,40==b.keyCode&&i ').appendTo(document.body),this.$element.on("click.dismiss.modal",a.proxy(function(a){a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus.call(this.$element[0]):this.hide.call(this))},this)),d&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;d?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()):b&&b()};var c=a.fn.modal;a.fn.modal=function(c,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},b.DEFAULTS,e.data(),"object"==typeof c&&c);f||e.data("bs.modal",f=new b(this,g)),"string"==typeof c?f[c](d):g.show&&f.show(d)})},a.fn.modal.Constructor=b,a.fn.modal.noConflict=function(){return a.fn.modal=c,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());b.preventDefault(),e.modal(f,this).one("hide",function(){c.is(":visible")&&c.focus()})}),a(document).on("show.bs.modal",".modal",function(){a(document.body).addClass("modal-open")}).on("hidden.bs.modal",".modal",function(){a(document.body).removeClass("modal-open")})}(window.jQuery),+function(a){"use strict";var b=function(a,b){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",a,b)};b.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},b.prototype.init=function(b,c,d){this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d);for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focus",i="hover"==g?"mouseleave":"blur";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},b.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},b.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show),void 0):c.show()},b.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide),void 0):c.hide()},b.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){if(this.$element.trigger(b),b.isDefaultPrevented())return;var c=this.tip();this.setContent(),this.options.animation&&c.addClass("fade");var d="function"==typeof this.options.placement?this.options.placement.call(this,c[0],this.$element[0]):this.options.placement,e=/\s?auto?\s?/i,f=e.test(d);f&&(d=d.replace(e,"")||"top"),c.detach().css({top:0,left:0,display:"block"}).addClass(d),this.options.container?c.appendTo(this.options.container):c.insertAfter(this.$element);var g=this.getPosition(),h=c[0].offsetWidth,i=c[0].offsetHeight;if(f){var j=this.$element.parent(),k=d,l=document.documentElement.scrollTop||document.body.scrollTop,m="body"==this.options.container?window.innerWidth:j.outerWidth(),n="body"==this.options.container?window.innerHeight:j.outerHeight(),o="body"==this.options.container?0:j.offset().left;d="bottom"==d&&g.top+g.height+i-l>n?"top":"top"==d&&g.top-l-i<0?"bottom":"right"==d&&g.right+h>m?"left":"left"==d&&g.left-h
'}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(window.jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(c).is("body")?a(window):a(c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#\w/.test(e)&&a(e);return f&&f.length&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parents(".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(window.jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.attr("data-target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(window.jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top()),"function"==typeof h&&(h=f.bottom());var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;this.affixed!==i&&(this.unpin&&this.$element.css("top",""),this.affixed=i,this.unpin="bottom"==i?e.top-d:null,this.$element.removeClass(b.RESET).addClass("affix"+(i?"-"+i:"")),"bottom"==i&&this.$element.offset({top:document.body.offsetHeight-h-this.$element.height()}))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(window.jQuery);
\ No newline at end of file
diff --git a/assets/js/jquery-1.10.2.min.js b/assets/js/jquery-1.10.2.min.js
new file mode 100644
index 000000000..da4170647
--- /dev/null
+++ b/assets/js/jquery-1.10.2.min.js
@@ -0,0 +1,6 @@
+/*! jQuery v1.10.2 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license
+//@ sourceMappingURL=jquery-1.10.2.min.map
+*/
+(function(e,t){var n,r,i=typeof t,o=e.location,a=e.document,s=a.documentElement,l=e.jQuery,u=e.$,c={},p=[],f="1.10.2",d=p.concat,h=p.push,g=p.slice,m=p.indexOf,y=c.toString,v=c.hasOwnProperty,b=f.trim,x=function(e,t){return new x.fn.init(e,t,r)},w=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=/\S+/g,C=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,N=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,k=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,E=/^[\],:{}\s]*$/,S=/(?:^|:|,)(?:\s*\[)+/g,A=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,j=/"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,D=/^-ms-/,L=/-([\da-z])/gi,H=function(e,t){return t.toUpperCase()},q=function(e){(a.addEventListener||"load"===e.type||"complete"===a.readyState)&&(_(),x.ready())},_=function(){a.addEventListener?(a.removeEventListener("DOMContentLoaded",q,!1),e.removeEventListener("load",q,!1)):(a.detachEvent("onreadystatechange",q),e.detachEvent("onload",q))};x.fn=x.prototype={jquery:f,constructor:x,init:function(e,n,r){var i,o;if(!e)return this;if("string"==typeof e){if(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:N.exec(e),!i||!i[1]&&n)return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e);if(i[1]){if(n=n instanceof x?n[0]:n,x.merge(this,x.parseHTML(i[1],n&&n.nodeType?n.ownerDocument||n:a,!0)),k.test(i[1])&&x.isPlainObject(n))for(i in n)x.isFunction(this[i])?this[i](n[i]):this.attr(i,n[i]);return this}if(o=a.getElementById(i[2]),o&&o.parentNode){if(o.id!==i[2])return r.find(e);this.length=1,this[0]=o}return this.context=a,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return g.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(g.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,n,r,i,o,a,s=arguments[0]||{},l=1,u=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},l=2),"object"==typeof s||x.isFunction(s)||(s={}),u===l&&(s=this,--l);u>l;l++)if(null!=(o=arguments[l]))for(i in o)e=s[i],r=o[i],s!==r&&(c&&r&&(x.isPlainObject(r)||(n=x.isArray(r)))?(n?(n=!1,a=e&&x.isArray(e)?e:[]):a=e&&x.isPlainObject(e)?e:{},s[i]=x.extend(c,a,r)):r!==t&&(s[i]=r));return s},x.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=l),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){if(e===!0?!--x.readyWait:!x.isReady){if(!a.body)return setTimeout(x.ready);x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(a,[x]),x.fn.trigger&&x(a).trigger("ready").off("ready"))}},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray||function(e){return"array"===x.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?c[y.call(e)]||"object":typeof e},isPlainObject:function(e){var n;if(!e||"object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!v.call(e,"constructor")&&!v.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(r){return!1}if(x.support.ownLast)for(n in e)return v.call(e,n);for(n in e);return n===t||v.call(e,n)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||a;var r=k.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:function(n){return e.JSON&&e.JSON.parse?e.JSON.parse(n):null===n?n:"string"==typeof n&&(n=x.trim(n),n&&E.test(n.replace(A,"@").replace(j,"]").replace(S,"")))?Function("return "+n)():(x.error("Invalid JSON: "+n),t)},parseXML:function(n){var r,i;if(!n||"string"!=typeof n)return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(o){r=t}return r&&r.documentElement&&!r.getElementsByTagName("parsererror").length||x.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&x.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(D,"ms-").replace(L,H)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,a=M(e);if(n){if(a){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(a){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:b&&!b.call("\ufeff\u00a0")?function(e){return null==e?"":b.call(e)}:function(e){return null==e?"":(e+"").replace(C,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(M(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){var r;if(t){if(m)return m.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,o=0;if("number"==typeof r)for(;r>o;o++)e[i++]=n[o];else while(n[o]!==t)e[i++]=n[o++];return e.length=i,e},grep:function(e,t,n){var r,i=[],o=0,a=e.length;for(n=!!n;a>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,a=M(e),s=[];if(a)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(s[s.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(s[s.length]=r);return d.apply([],s)},guid:1,proxy:function(e,n){var r,i,o;return"string"==typeof n&&(o=e[n],n=e,e=o),x.isFunction(e)?(r=g.call(arguments,2),i=function(){return e.apply(n||this,r.concat(g.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):t},access:function(e,n,r,i,o,a,s){var l=0,u=e.length,c=null==r;if("object"===x.type(r)){o=!0;for(l in r)x.access(e,n,l,r[l],!0,a,s)}else if(i!==t&&(o=!0,x.isFunction(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(e,t,n){return c.call(x(e),n)})),n))for(;u>l;l++)n(e[l],r,s?i:i.call(e[l],l,n(e[l],r)));return o?e:c?n.call(e):u?n(e[0],r):a},now:function(){return(new Date).getTime()},swap:function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i}}),x.ready.promise=function(t){if(!n)if(n=x.Deferred(),"complete"===a.readyState)setTimeout(x.ready);else if(a.addEventListener)a.addEventListener("DOMContentLoaded",q,!1),e.addEventListener("load",q,!1);else{a.attachEvent("onreadystatechange",q),e.attachEvent("onload",q);var r=!1;try{r=null==e.frameElement&&a.documentElement}catch(i){}r&&r.doScroll&&function o(){if(!x.isReady){try{r.doScroll("left")}catch(e){return setTimeout(o,50)}_(),x.ready()}}()}return n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){c["[object "+t+"]"]=t.toLowerCase()});function M(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}r=x(a),function(e,t){var n,r,i,o,a,s,l,u,c,p,f,d,h,g,m,y,v,b="sizzle"+-new Date,w=e.document,T=0,C=0,N=st(),k=st(),E=st(),S=!1,A=function(e,t){return e===t?(S=!0,0):0},j=typeof t,D=1<<31,L={}.hasOwnProperty,H=[],q=H.pop,_=H.push,M=H.push,O=H.slice,F=H.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},B="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",P="[\\x20\\t\\r\\n\\f]",R="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",W=R.replace("w","w#"),$="\\["+P+"*("+R+")"+P+"*(?:([*^$|!~]?=)"+P+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+W+")|)|)"+P+"*\\]",I=":("+R+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+$.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+P+"+|((?:^|[^\\\\])(?:\\\\.)*)"+P+"+$","g"),X=RegExp("^"+P+"*,"+P+"*"),U=RegExp("^"+P+"*([>+~]|"+P+")"+P+"*"),V=RegExp(P+"*[+~]"),Y=RegExp("="+P+"*([^\\]'\"]*)"+P+"*\\]","g"),J=RegExp(I),G=RegExp("^"+W+"$"),Q={ID:RegExp("^#("+R+")"),CLASS:RegExp("^\\.("+R+")"),TAG:RegExp("^("+R.replace("w","w*")+")"),ATTR:RegExp("^"+$),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+P+"*(even|odd|(([+-]|)(\\d*)n|)"+P+"*(?:([+-]|)"+P+"*(\\d+)|))"+P+"*\\)|)","i"),bool:RegExp("^(?:"+B+")$","i"),needsContext:RegExp("^"+P+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+P+"*((?:-\\d)?\\d*)"+P+"*\\)|)(?=[^-]|$)","i")},K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,et=/^(?:input|select|textarea|button)$/i,tt=/^h\d$/i,nt=/'|\\/g,rt=RegExp("\\\\([\\da-f]{1,6}"+P+"?|("+P+")|.)","ig"),it=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{M.apply(H=O.call(w.childNodes),w.childNodes),H[w.childNodes.length].nodeType}catch(ot){M={apply:H.length?function(e,t){_.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function at(e,t,n,i){var o,a,s,l,u,c,d,m,y,x;if((t?t.ownerDocument||t:w)!==f&&p(t),t=t||f,n=n||[],!e||"string"!=typeof e)return n;if(1!==(l=t.nodeType)&&9!==l)return[];if(h&&!i){if(o=Z.exec(e))if(s=o[1]){if(9===l){if(a=t.getElementById(s),!a||!a.parentNode)return n;if(a.id===s)return n.push(a),n}else if(t.ownerDocument&&(a=t.ownerDocument.getElementById(s))&&v(t,a)&&a.id===s)return n.push(a),n}else{if(o[2])return M.apply(n,t.getElementsByTagName(e)),n;if((s=o[3])&&r.getElementsByClassName&&t.getElementsByClassName)return M.apply(n,t.getElementsByClassName(s)),n}if(r.qsa&&(!g||!g.test(e))){if(m=d=b,y=t,x=9===l&&e,1===l&&"object"!==t.nodeName.toLowerCase()){c=mt(e),(d=t.getAttribute("id"))?m=d.replace(nt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",u=c.length;while(u--)c[u]=m+yt(c[u]);y=V.test(e)&&t.parentNode||t,x=c.join(",")}if(x)try{return M.apply(n,y.querySelectorAll(x)),n}catch(T){}finally{d||t.removeAttribute("id")}}}return kt(e.replace(z,"$1"),t,n,i)}function st(){var e=[];function t(n,r){return e.push(n+=" ")>o.cacheLength&&delete t[e.shift()],t[n]=r}return t}function lt(e){return e[b]=!0,e}function ut(e){var t=f.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ct(e,t){var n=e.split("|"),r=e.length;while(r--)o.attrHandle[n[r]]=t}function pt(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function ft(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function dt(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function ht(e){return lt(function(t){return t=+t,lt(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}s=at.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},r=at.support={},p=at.setDocument=function(e){var n=e?e.ownerDocument||e:w,i=n.defaultView;return n!==f&&9===n.nodeType&&n.documentElement?(f=n,d=n.documentElement,h=!s(n),i&&i.attachEvent&&i!==i.top&&i.attachEvent("onbeforeunload",function(){p()}),r.attributes=ut(function(e){return e.className="i",!e.getAttribute("className")}),r.getElementsByTagName=ut(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),r.getElementsByClassName=ut(function(e){return e.innerHTML="
",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),r.getById=ut(function(e){return d.appendChild(e).id=b,!n.getElementsByName||!n.getElementsByName(b).length}),r.getById?(o.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){return e.getAttribute("id")===t}}):(delete o.find.ID,o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),o.find.TAG=r.getElementsByTagName?function(e,n){return typeof n.getElementsByTagName!==j?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},o.find.CLASS=r.getElementsByClassName&&function(e,n){return typeof n.getElementsByClassName!==j&&h?n.getElementsByClassName(e):t},m=[],g=[],(r.qsa=K.test(n.querySelectorAll))&&(ut(function(e){e.innerHTML=" ",e.querySelectorAll("[selected]").length||g.push("\\["+P+"*(?:value|"+B+")"),e.querySelectorAll(":checked").length||g.push(":checked")}),ut(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&g.push("[*^$]="+P+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||g.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),g.push(",.*:")})),(r.matchesSelector=K.test(y=d.webkitMatchesSelector||d.mozMatchesSelector||d.oMatchesSelector||d.msMatchesSelector))&&ut(function(e){r.disconnectedMatch=y.call(e,"div"),y.call(e,"[s!='']:x"),m.push("!=",I)}),g=g.length&&RegExp(g.join("|")),m=m.length&&RegExp(m.join("|")),v=K.test(d.contains)||d.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},A=d.compareDocumentPosition?function(e,t){if(e===t)return S=!0,0;var i=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t);return i?1&i||!r.sortDetached&&t.compareDocumentPosition(e)===i?e===n||v(w,e)?-1:t===n||v(w,t)?1:c?F.call(c,e)-F.call(c,t):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,i=0,o=e.parentNode,a=t.parentNode,s=[e],l=[t];if(e===t)return S=!0,0;if(!o||!a)return e===n?-1:t===n?1:o?-1:a?1:c?F.call(c,e)-F.call(c,t):0;if(o===a)return pt(e,t);r=e;while(r=r.parentNode)s.unshift(r);r=t;while(r=r.parentNode)l.unshift(r);while(s[i]===l[i])i++;return i?pt(s[i],l[i]):s[i]===w?-1:l[i]===w?1:0},n):f},at.matches=function(e,t){return at(e,null,null,t)},at.matchesSelector=function(e,t){if((e.ownerDocument||e)!==f&&p(e),t=t.replace(Y,"='$1']"),!(!r.matchesSelector||!h||m&&m.test(t)||g&&g.test(t)))try{var n=y.call(e,t);if(n||r.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(i){}return at(t,f,null,[e]).length>0},at.contains=function(e,t){return(e.ownerDocument||e)!==f&&p(e),v(e,t)},at.attr=function(e,n){(e.ownerDocument||e)!==f&&p(e);var i=o.attrHandle[n.toLowerCase()],a=i&&L.call(o.attrHandle,n.toLowerCase())?i(e,n,!h):t;return a===t?r.attributes||!h?e.getAttribute(n):(a=e.getAttributeNode(n))&&a.specified?a.value:null:a},at.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},at.uniqueSort=function(e){var t,n=[],i=0,o=0;if(S=!r.detectDuplicates,c=!r.sortStable&&e.slice(0),e.sort(A),S){while(t=e[o++])t===e[o]&&(i=n.push(o));while(i--)e.splice(n[i],1)}return e},a=at.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=a(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=a(t);return n},o=at.selectors={cacheLength:50,createPseudo:lt,match:Q,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(rt,it),e[3]=(e[4]||e[5]||"").replace(rt,it),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||at.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&at.error(e[0]),e},PSEUDO:function(e){var n,r=!e[5]&&e[2];return Q.CHILD.test(e[0])?null:(e[3]&&e[4]!==t?e[2]=e[4]:r&&J.test(r)&&(n=mt(r,!0))&&(n=r.indexOf(")",r.length-n)-r.length)&&(e[0]=e[0].slice(0,n),e[2]=r.slice(0,n)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(rt,it).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+" "];return t||(t=RegExp("(^|"+P+")"+e+"("+P+"|$)"))&&N(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=at.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,p,f,d,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!l&&!s;if(m){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){c=m[b]||(m[b]={}),u=c[e]||[],d=u[0]===T&&u[1],f=u[0]===T&&u[2],p=d&&m.childNodes[d];while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[T,d,f];break}}else if(v&&(u=(t[b]||(t[b]={}))[e])&&u[0]===T)f=u[1];else while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(v&&((p[b]||(p[b]={}))[e]=[T,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=o.pseudos[e]||o.setFilters[e.toLowerCase()]||at.error("unsupported pseudo: "+e);return r[b]?r(t):r.length>1?(n=[e,e,"",t],o.setFilters.hasOwnProperty(e.toLowerCase())?lt(function(e,n){var i,o=r(e,t),a=o.length;while(a--)i=F.call(e,o[a]),e[i]=!(n[i]=o[a])}):function(e){return r(e,0,n)}):r}},pseudos:{not:lt(function(e){var t=[],n=[],r=l(e.replace(z,"$1"));return r[b]?lt(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:lt(function(e){return function(t){return at(e,t).length>0}}),contains:lt(function(e){return function(t){return(t.textContent||t.innerText||a(t)).indexOf(e)>-1}}),lang:lt(function(e){return G.test(e||"")||at.error("unsupported lang: "+e),e=e.replace(rt,it).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===d},focus:function(e){return e===f.activeElement&&(!f.hasFocus||f.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!o.pseudos.empty(e)},header:function(e){return tt.test(e.nodeName)},input:function(e){return et.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:ht(function(){return[0]}),last:ht(function(e,t){return[t-1]}),eq:ht(function(e,t,n){return[0>n?n+t:n]}),even:ht(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:ht(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:ht(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:ht(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}},o.pseudos.nth=o.pseudos.eq;for(n in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})o.pseudos[n]=ft(n);for(n in{submit:!0,reset:!0})o.pseudos[n]=dt(n);function gt(){}gt.prototype=o.filters=o.pseudos,o.setFilters=new gt;function mt(e,t){var n,r,i,a,s,l,u,c=k[e+" "];if(c)return t?0:c.slice(0);s=e,l=[],u=o.preFilter;while(s){(!n||(r=X.exec(s)))&&(r&&(s=s.slice(r[0].length)||s),l.push(i=[])),n=!1,(r=U.exec(s))&&(n=r.shift(),i.push({value:n,type:r[0].replace(z," ")}),s=s.slice(n.length));for(a in o.filter)!(r=Q[a].exec(s))||u[a]&&!(r=u[a](r))||(n=r.shift(),i.push({value:n,type:a,matches:r}),s=s.slice(n.length));if(!n)break}return t?s.length:s?at.error(e):k(e,l).slice(0)}function yt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function vt(e,t,n){var r=t.dir,o=n&&"parentNode"===r,a=C++;return t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||o)return e(t,n,i)}:function(t,n,s){var l,u,c,p=T+" "+a;if(s){while(t=t[r])if((1===t.nodeType||o)&&e(t,n,s))return!0}else while(t=t[r])if(1===t.nodeType||o)if(c=t[b]||(t[b]={}),(u=c[r])&&u[0]===p){if((l=u[1])===!0||l===i)return l===!0}else if(u=c[r]=[p],u[1]=e(t,n,s)||i,u[1]===!0)return!0}}function bt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xt(e,t,n,r,i){var o,a=[],s=0,l=e.length,u=null!=t;for(;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function wt(e,t,n,r,i,o){return r&&!r[b]&&(r=wt(r)),i&&!i[b]&&(i=wt(i,o)),lt(function(o,a,s,l){var u,c,p,f=[],d=[],h=a.length,g=o||Nt(t||"*",s.nodeType?[s]:s,[]),m=!e||!o&&t?g:xt(g,f,e,s,l),y=n?i||(o?e:h||r)?[]:a:m;if(n&&n(m,y,s,l),r){u=xt(y,d),r(u,[],s,l),c=u.length;while(c--)(p=u[c])&&(y[d[c]]=!(m[d[c]]=p))}if(o){if(i||e){if(i){u=[],c=y.length;while(c--)(p=y[c])&&u.push(m[c]=p);i(null,y=[],u,l)}c=y.length;while(c--)(p=y[c])&&(u=i?F.call(o,p):f[c])>-1&&(o[u]=!(a[u]=p))}}else y=xt(y===a?y.splice(h,y.length):y),i?i(null,a,y,l):M.apply(a,y)})}function Tt(e){var t,n,r,i=e.length,a=o.relative[e[0].type],s=a||o.relative[" "],l=a?1:0,c=vt(function(e){return e===t},s,!0),p=vt(function(e){return F.call(t,e)>-1},s,!0),f=[function(e,n,r){return!a&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;i>l;l++)if(n=o.relative[e[l].type])f=[vt(bt(f),n)];else{if(n=o.filter[e[l].type].apply(null,e[l].matches),n[b]){for(r=++l;i>r;r++)if(o.relative[e[r].type])break;return wt(l>1&&bt(f),l>1&&yt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&Tt(e.slice(l,r)),i>r&&Tt(e=e.slice(r)),i>r&&yt(e))}f.push(n)}return bt(f)}function Ct(e,t){var n=0,r=t.length>0,a=e.length>0,s=function(s,l,c,p,d){var h,g,m,y=[],v=0,b="0",x=s&&[],w=null!=d,C=u,N=s||a&&o.find.TAG("*",d&&l.parentNode||l),k=T+=null==C?1:Math.random()||.1;for(w&&(u=l!==f&&l,i=n);null!=(h=N[b]);b++){if(a&&h){g=0;while(m=e[g++])if(m(h,l,c)){p.push(h);break}w&&(T=k,i=++n)}r&&((h=!m&&h)&&v--,s&&x.push(h))}if(v+=b,r&&b!==v){g=0;while(m=t[g++])m(x,y,l,c);if(s){if(v>0)while(b--)x[b]||y[b]||(y[b]=q.call(p));y=xt(y)}M.apply(p,y),w&&!s&&y.length>0&&v+t.length>1&&at.uniqueSort(p)}return w&&(T=k,u=C),x};return r?lt(s):s}l=at.compile=function(e,t){var n,r=[],i=[],o=E[e+" "];if(!o){t||(t=mt(e)),n=t.length;while(n--)o=Tt(t[n]),o[b]?r.push(o):i.push(o);o=E(e,Ct(i,r))}return o};function Nt(e,t,n){var r=0,i=t.length;for(;i>r;r++)at(e,t[r],n);return n}function kt(e,t,n,i){var a,s,u,c,p,f=mt(e);if(!i&&1===f.length){if(s=f[0]=f[0].slice(0),s.length>2&&"ID"===(u=s[0]).type&&r.getById&&9===t.nodeType&&h&&o.relative[s[1].type]){if(t=(o.find.ID(u.matches[0].replace(rt,it),t)||[])[0],!t)return n;e=e.slice(s.shift().value.length)}a=Q.needsContext.test(e)?0:s.length;while(a--){if(u=s[a],o.relative[c=u.type])break;if((p=o.find[c])&&(i=p(u.matches[0].replace(rt,it),V.test(s[0].type)&&t.parentNode||t))){if(s.splice(a,1),e=i.length&&yt(s),!e)return M.apply(n,i),n;break}}}return l(e,f)(i,t,!h,n,V.test(e)),n}r.sortStable=b.split("").sort(A).join("")===b,r.detectDuplicates=S,p(),r.sortDetached=ut(function(e){return 1&e.compareDocumentPosition(f.createElement("div"))}),ut(function(e){return e.innerHTML=" ","#"===e.firstChild.getAttribute("href")})||ct("type|href|height|width",function(e,n,r){return r?t:e.getAttribute(n,"type"===n.toLowerCase()?1:2)}),r.attributes&&ut(function(e){return e.innerHTML=" ",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||ct("value",function(e,n,r){return r||"input"!==e.nodeName.toLowerCase()?t:e.defaultValue}),ut(function(e){return null==e.getAttribute("disabled")})||ct(B,function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&i.specified?i.value:e[n]===!0?n.toLowerCase():null}),x.find=at,x.expr=at.selectors,x.expr[":"]=x.expr.pseudos,x.unique=at.uniqueSort,x.text=at.getText,x.isXMLDoc=at.isXML,x.contains=at.contains}(e);var O={};function F(e){var t=O[e]={};return x.each(e.match(T)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?O[e]||F(e):x.extend({},e);var n,r,i,o,a,s,l=[],u=!e.once&&[],c=function(t){for(r=e.memory&&t,i=!0,a=s||0,s=0,o=l.length,n=!0;l&&o>a;a++)if(l[a].apply(t[0],t[1])===!1&&e.stopOnFalse){r=!1;break}n=!1,l&&(u?u.length&&c(u.shift()):r?l=[]:p.disable())},p={add:function(){if(l){var t=l.length;(function i(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&p.has(n)||l.push(n):n&&n.length&&"string"!==r&&i(n)})})(arguments),n?o=l.length:r&&(s=t,c(r))}return this},remove:function(){return l&&x.each(arguments,function(e,t){var r;while((r=x.inArray(t,l,r))>-1)l.splice(r,1),n&&(o>=r&&o--,a>=r&&a--)}),this},has:function(e){return e?x.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],o=0,this},disable:function(){return l=u=r=t,this},disabled:function(){return!l},lock:function(){return u=t,r||p.disable(),this},locked:function(){return!u},fireWith:function(e,t){return!l||i&&!u||(t=t||[],t=[e,t.slice?t.slice():t],n?u.push(t):c(t)),this},fire:function(){return p.fireWith(this,arguments),this},fired:function(){return!!i}};return p},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var a=o[0],s=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=g.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),a=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?g.call(arguments):r,n===s?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},s,l,u;if(r>1)for(s=Array(r),l=Array(r),u=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(a(t,u,n)).fail(o.reject).progress(a(t,l,s)):--i;return i||o.resolveWith(u,n),o.promise()}}),x.support=function(t){var n,r,o,s,l,u,c,p,f,d=a.createElement("div");if(d.setAttribute("className","t"),d.innerHTML=" a ",n=d.getElementsByTagName("*")||[],r=d.getElementsByTagName("a")[0],!r||!r.style||!n.length)return t;s=a.createElement("select"),u=s.appendChild(a.createElement("option")),o=d.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t.getSetAttribute="t"!==d.className,t.leadingWhitespace=3===d.firstChild.nodeType,t.tbody=!d.getElementsByTagName("tbody").length,t.htmlSerialize=!!d.getElementsByTagName("link").length,t.style=/top/.test(r.getAttribute("style")),t.hrefNormalized="/a"===r.getAttribute("href"),t.opacity=/^0.5/.test(r.style.opacity),t.cssFloat=!!r.style.cssFloat,t.checkOn=!!o.value,t.optSelected=u.selected,t.enctype=!!a.createElement("form").enctype,t.html5Clone="<:nav>"!==a.createElement("nav").cloneNode(!0).outerHTML,t.inlineBlockNeedsLayout=!1,t.shrinkWrapBlocks=!1,t.pixelPosition=!1,t.deleteExpando=!0,t.noCloneEvent=!0,t.reliableMarginRight=!0,t.boxSizingReliable=!0,o.checked=!0,t.noCloneChecked=o.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!u.disabled;try{delete d.test}catch(h){t.deleteExpando=!1}o=a.createElement("input"),o.setAttribute("value",""),t.input=""===o.getAttribute("value"),o.value="t",o.setAttribute("type","radio"),t.radioValue="t"===o.value,o.setAttribute("checked","t"),o.setAttribute("name","t"),l=a.createDocumentFragment(),l.appendChild(o),t.appendChecked=o.checked,t.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,d.attachEvent&&(d.attachEvent("onclick",function(){t.noCloneEvent=!1}),d.cloneNode(!0).click());for(f in{submit:!0,change:!0,focusin:!0})d.setAttribute(c="on"+f,"t"),t[f+"Bubbles"]=c in e||d.attributes[c].expando===!1;d.style.backgroundClip="content-box",d.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===d.style.backgroundClip;for(f in x(t))break;return t.ownLast="0"!==f,x(function(){var n,r,o,s="padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",l=a.getElementsByTagName("body")[0];l&&(n=a.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",l.appendChild(n).appendChild(d),d.innerHTML="",o=d.getElementsByTagName("td"),o[0].style.cssText="padding:0;margin:0;border:0;display:none",p=0===o[0].offsetHeight,o[0].style.display="",o[1].style.display="none",t.reliableHiddenOffsets=p&&0===o[0].offsetHeight,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",x.swap(l,null!=l.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===d.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(d,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(d,null)||{width:"4px"}).width,r=d.appendChild(a.createElement("div")),r.style.cssText=d.style.cssText=s,r.style.marginRight=r.style.width="0",d.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),typeof d.style.zoom!==i&&(d.innerHTML="",d.style.cssText=s+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=3===d.offsetWidth,d.style.display="block",d.innerHTML="
",d.firstChild.style.width="5px",t.shrinkWrapBlocks=3!==d.offsetWidth,t.inlineBlockNeedsLayout&&(l.style.zoom=1)),l.removeChild(n),n=d=o=r=null)}),n=s=l=u=r=o=null,t
+}({});var B=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;function R(e,n,r,i){if(x.acceptData(e)){var o,a,s=x.expando,l=e.nodeType,u=l?x.cache:e,c=l?e[s]:e[s]&&s;if(c&&u[c]&&(i||u[c].data)||r!==t||"string"!=typeof n)return c||(c=l?e[s]=p.pop()||x.guid++:s),u[c]||(u[c]=l?{}:{toJSON:x.noop}),("object"==typeof n||"function"==typeof n)&&(i?u[c]=x.extend(u[c],n):u[c].data=x.extend(u[c].data,n)),a=u[c],i||(a.data||(a.data={}),a=a.data),r!==t&&(a[x.camelCase(n)]=r),"string"==typeof n?(o=a[n],null==o&&(o=a[x.camelCase(n)])):o=a,o}}function W(e,t,n){if(x.acceptData(e)){var r,i,o=e.nodeType,a=o?x.cache:e,s=o?e[x.expando]:x.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){x.isArray(t)?t=t.concat(x.map(t,x.camelCase)):t in r?t=[t]:(t=x.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;while(i--)delete r[t[i]];if(n?!I(r):!x.isEmptyObject(r))return}(n||(delete a[s].data,I(a[s])))&&(o?x.cleanData([e],!0):x.support.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}x.extend({cache:{},noData:{applet:!0,embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?x.cache[e[x.expando]]:e[x.expando],!!e&&!I(e)},data:function(e,t,n){return R(e,t,n)},removeData:function(e,t){return W(e,t)},_data:function(e,t,n){return R(e,t,n,!0)},_removeData:function(e,t){return W(e,t,!0)},acceptData:function(e){if(e.nodeType&&1!==e.nodeType&&9!==e.nodeType)return!1;var t=e.nodeName&&x.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),x.fn.extend({data:function(e,n){var r,i,o=null,a=0,s=this[0];if(e===t){if(this.length&&(o=x.data(s),1===s.nodeType&&!x._data(s,"parsedAttrs"))){for(r=s.attributes;r.length>a;a++)i=r[a].name,0===i.indexOf("data-")&&(i=x.camelCase(i.slice(5)),$(s,i,o[i]));x._data(s,"parsedAttrs",!0)}return o}return"object"==typeof e?this.each(function(){x.data(this,e)}):arguments.length>1?this.each(function(){x.data(this,e,n)}):s?$(s,e,x.data(s,e)):null},removeData:function(e){return this.each(function(){x.removeData(this,e)})}});function $(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(P,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:B.test(r)?x.parseJSON(r):r}catch(o){}x.data(e,n,r)}else r=t}return r}function I(e){var t;for(t in e)if(("data"!==t||!x.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}x.extend({queue:function(e,n,r){var i;return e?(n=(n||"fx")+"queue",i=x._data(e,n),r&&(!i||x.isArray(r)?i=x._data(e,n,x.makeArray(r)):i.push(r)),i||[]):t},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),a=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return x._data(e,n)||x._data(e,n,{empty:x.Callbacks("once memory").add(function(){x._removeData(e,t+"queue"),x._removeData(e,n)})})}}),x.fn.extend({queue:function(e,n){var r=2;return"string"!=typeof e&&(n=e,e="fx",r--),r>arguments.length?x.queue(this[0],e):n===t?this:this.each(function(){var t=x.queue(this,e,n);x._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,o=x.Deferred(),a=this,s=this.length,l=function(){--i||o.resolveWith(a,[a])};"string"!=typeof e&&(n=e,e=t),e=e||"fx";while(s--)r=x._data(a[s],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(l));return l(),o.promise(n)}});var z,X,U=/[\t\r\n\f]/g,V=/\r/g,Y=/^(?:input|select|textarea|button|object)$/i,J=/^(?:a|area)$/i,G=/^(?:checked|selected)$/i,Q=x.support.getSetAttribute,K=x.support.input;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return e=x.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,o,a=0,s=this.length,l="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,a=0,s=this.length,l=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var t,r=0,o=x(this),a=e.match(T)||[];while(t=a[r++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else(n===i||"boolean"===n)&&(this.className&&x._data(this,"__className__",this.className),this.className=this.className||e===!1?"":x._data(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(U," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,o=this[0];{if(arguments.length)return i=x.isFunction(e),this.each(function(n){var o;1===this.nodeType&&(o=i?e.call(this,n,x(this).val()):e,null==o?o="":"number"==typeof o?o+="":x.isArray(o)&&(o=x.map(o,function(e){return null==e?"":e+""})),r=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],r&&"set"in r&&r.set(this,o,"value")!==t||(this.value=o))});if(o)return r=x.valHooks[o.type]||x.valHooks[o.nodeName.toLowerCase()],r&&"get"in r&&(n=r.get(o,"value"))!==t?n:(n=o.value,"string"==typeof n?n.replace(V,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=x.find.attr(e,"value");return null!=t?t:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;for(;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;a.push(t)}return a},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),a=i.length;while(a--)r=i[a],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,n,r){var o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===i?x.prop(e,n,r):(1===s&&x.isXMLDoc(e)||(n=n.toLowerCase(),o=x.attrHooks[n]||(x.expr.match.bool.test(n)?X:z)),r===t?o&&"get"in o&&null!==(a=o.get(e,n))?a:(a=x.find.attr(e,n),null==a?t:a):null!==r?o&&"set"in o&&(a=o.set(e,r,n))!==t?a:(e.setAttribute(n,r+""),r):(x.removeAttr(e,n),t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(T);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)?K&&Q||!G.test(n)?e[r]=!1:e[x.camelCase("default-"+n)]=e[r]=!1:x.attr(e,n,""),e.removeAttribute(Q?n:r)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,n,r){var i,o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return a=1!==s||!x.isXMLDoc(e),a&&(n=x.propFix[n]||n,o=x.propHooks[n]),r!==t?o&&"set"in o&&(i=o.set(e,r,n))!==t?i:e[n]=r:o&&"get"in o&&null!==(i=o.get(e,n))?i:e[n]},propHooks:{tabIndex:{get:function(e){var t=x.find.attr(e,"tabindex");return t?parseInt(t,10):Y.test(e.nodeName)||J.test(e.nodeName)&&e.href?0:-1}}}}),X={set:function(e,t,n){return t===!1?x.removeAttr(e,n):K&&Q||!G.test(n)?e.setAttribute(!Q&&x.propFix[n]||n,n):e[x.camelCase("default-"+n)]=e[n]=!0,n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,n){var r=x.expr.attrHandle[n]||x.find.attr;x.expr.attrHandle[n]=K&&Q||!G.test(n)?function(e,n,i){var o=x.expr.attrHandle[n],a=i?t:(x.expr.attrHandle[n]=t)!=r(e,n,i)?n.toLowerCase():null;return x.expr.attrHandle[n]=o,a}:function(e,n,r){return r?t:e[x.camelCase("default-"+n)]?n.toLowerCase():null}}),K&&Q||(x.attrHooks.value={set:function(e,n,r){return x.nodeName(e,"input")?(e.defaultValue=n,t):z&&z.set(e,n,r)}}),Q||(z={set:function(e,n,r){var i=e.getAttributeNode(r);return i||e.setAttributeNode(i=e.ownerDocument.createAttribute(r)),i.value=n+="","value"===r||n===e.getAttribute(r)?n:t}},x.expr.attrHandle.id=x.expr.attrHandle.name=x.expr.attrHandle.coords=function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&""!==i.value?i.value:null},x.valHooks.button={get:function(e,n){var r=e.getAttributeNode(n);return r&&r.specified?r.value:t},set:z.set},x.attrHooks.contenteditable={set:function(e,t,n){z.set(e,""===t?!1:t,n)}},x.each(["width","height"],function(e,n){x.attrHooks[n]={set:function(e,r){return""===r?(e.setAttribute(n,"auto"),r):t}}})),x.support.hrefNormalized||x.each(["href","src"],function(e,t){x.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),x.support.style||(x.attrHooks.style={get:function(e){return e.style.cssText||t},set:function(e,t){return e.style.cssText=t+""}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.support.enctype||(x.propFix.enctype="encoding"),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,n){return x.isArray(n)?e.checked=x.inArray(x(e).val(),n)>=0:t}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Z=/^(?:input|select|textarea)$/i,et=/^key/,tt=/^(?:mouse|contextmenu)|click/,nt=/^(?:focusinfocus|focusoutblur)$/,rt=/^([^.]*)(?:\.(.+)|)$/;function it(){return!0}function ot(){return!1}function at(){try{return a.activeElement}catch(e){}}x.event={global:{},add:function(e,n,r,o,a){var s,l,u,c,p,f,d,h,g,m,y,v=x._data(e);if(v){r.handler&&(c=r,r=c.handler,a=c.selector),r.guid||(r.guid=x.guid++),(l=v.events)||(l=v.events={}),(f=v.handle)||(f=v.handle=function(e){return typeof x===i||e&&x.event.triggered===e.type?t:x.event.dispatch.apply(f.elem,arguments)},f.elem=e),n=(n||"").match(T)||[""],u=n.length;while(u--)s=rt.exec(n[u])||[],g=y=s[1],m=(s[2]||"").split(".").sort(),g&&(p=x.event.special[g]||{},g=(a?p.delegateType:p.bindType)||g,p=x.event.special[g]||{},d=x.extend({type:g,origType:y,data:o,handler:r,guid:r.guid,selector:a,needsContext:a&&x.expr.match.needsContext.test(a),namespace:m.join(".")},c),(h=l[g])||(h=l[g]=[],h.delegateCount=0,p.setup&&p.setup.call(e,o,m,f)!==!1||(e.addEventListener?e.addEventListener(g,f,!1):e.attachEvent&&e.attachEvent("on"+g,f))),p.add&&(p.add.call(e,d),d.handler.guid||(d.handler.guid=r.guid)),a?h.splice(h.delegateCount++,0,d):h.push(d),x.event.global[g]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,p,f,d,h,g,m=x.hasData(e)&&x._data(e);if(m&&(c=m.events)){t=(t||"").match(T)||[""],u=t.length;while(u--)if(s=rt.exec(t[u])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){p=x.event.special[d]||{},d=(r?p.delegateType:p.bindType)||d,f=c[d]||[],s=s[2]&&RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;while(o--)a=f[o],!i&&g!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,p.remove&&p.remove.call(e,a));l&&!f.length&&(p.teardown&&p.teardown.call(e,h,m.handle)!==!1||x.removeEvent(e,d,m.handle),delete c[d])}else for(d in c)x.event.remove(e,d+t[u],n,r,!0);x.isEmptyObject(c)&&(delete m.handle,x._removeData(e,"events"))}},trigger:function(n,r,i,o){var s,l,u,c,p,f,d,h=[i||a],g=v.call(n,"type")?n.type:n,m=v.call(n,"namespace")?n.namespace.split("."):[];if(u=f=i=i||a,3!==i.nodeType&&8!==i.nodeType&&!nt.test(g+x.event.triggered)&&(g.indexOf(".")>=0&&(m=g.split("."),g=m.shift(),m.sort()),l=0>g.indexOf(":")&&"on"+g,n=n[x.expando]?n:new x.Event(g,"object"==typeof n&&n),n.isTrigger=o?2:3,n.namespace=m.join("."),n.namespace_re=n.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,n.result=t,n.target||(n.target=i),r=null==r?[n]:x.makeArray(r,[n]),p=x.event.special[g]||{},o||!p.trigger||p.trigger.apply(i,r)!==!1)){if(!o&&!p.noBubble&&!x.isWindow(i)){for(c=p.delegateType||g,nt.test(c+g)||(u=u.parentNode);u;u=u.parentNode)h.push(u),f=u;f===(i.ownerDocument||a)&&h.push(f.defaultView||f.parentWindow||e)}d=0;while((u=h[d++])&&!n.isPropagationStopped())n.type=d>1?c:p.bindType||g,s=(x._data(u,"events")||{})[n.type]&&x._data(u,"handle"),s&&s.apply(u,r),s=l&&u[l],s&&x.acceptData(u)&&s.apply&&s.apply(u,r)===!1&&n.preventDefault();if(n.type=g,!o&&!n.isDefaultPrevented()&&(!p._default||p._default.apply(h.pop(),r)===!1)&&x.acceptData(i)&&l&&i[g]&&!x.isWindow(i)){f=i[l],f&&(i[l]=null),x.event.triggered=g;try{i[g]()}catch(y){}x.event.triggered=t,f&&(i[l]=f)}return n.result}},dispatch:function(e){e=x.event.fix(e);var n,r,i,o,a,s=[],l=g.call(arguments),u=(x._data(this,"events")||{})[e.type]||[],c=x.event.special[e.type]||{};if(l[0]=e,e.delegateTarget=this,!c.preDispatch||c.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),n=0;while((o=s[n++])&&!e.isPropagationStopped()){e.currentTarget=o.elem,a=0;while((i=o.handlers[a++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(i.namespace))&&(e.handleObj=i,e.data=i.data,r=((x.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,l),r!==t&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,n){var r,i,o,a,s=[],l=n.delegateCount,u=e.target;if(l&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!=this;u=u.parentNode||this)if(1===u.nodeType&&(u.disabled!==!0||"click"!==e.type)){for(o=[],a=0;l>a;a++)i=n[a],r=i.selector+" ",o[r]===t&&(o[r]=i.needsContext?x(r,this).index(u)>=0:x.find(r,this,null,[u]).length),o[r]&&o.push(i);o.length&&s.push({elem:u,handlers:o})}return n.length>l&&s.push({elem:this,handlers:n.slice(l)}),s},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,o=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=tt.test(i)?this.mouseHooks:et.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new x.Event(o),t=r.length;while(t--)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||a),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,i,o,s=n.button,l=n.fromElement;return null==e.pageX&&null!=n.clientX&&(i=e.target.ownerDocument||a,o=i.documentElement,r=i.body,e.pageX=n.clientX+(o&&o.scrollLeft||r&&r.scrollLeft||0)-(o&&o.clientLeft||r&&r.clientLeft||0),e.pageY=n.clientY+(o&&o.scrollTop||r&&r.scrollTop||0)-(o&&o.clientTop||r&&r.clientTop||0)),!e.relatedTarget&&l&&(e.relatedTarget=l===e.target?n.toElement:l),e.which||s===t||(e.which=1&s?1:2&s?3:4&s?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==at()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===at()&&this.blur?(this.blur(),!1):t},delegateType:"focusout"},click:{trigger:function(){return x.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):t},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==t&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=a.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===i&&(e[r]=null),e.detachEvent(r,n))},x.Event=function(e,n){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?it:ot):this.type=e,n&&x.extend(this,n),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,t):new x.Event(e,n)},x.Event.prototype={isDefaultPrevented:ot,isPropagationStopped:ot,isImmediatePropagationStopped:ot,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=it,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=it,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=it,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.submitBubbles||(x.event.special.submit={setup:function(){return x.nodeName(this,"form")?!1:(x.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=x.nodeName(n,"input")||x.nodeName(n,"button")?n.form:t;r&&!x._data(r,"submitBubbles")&&(x.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),x._data(r,"submitBubbles",!0))}),t)},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&x.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return x.nodeName(this,"form")?!1:(x.event.remove(this,"._submit"),t)}}),x.support.changeBubbles||(x.event.special.change={setup:function(){return Z.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(x.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),x.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),x.event.simulate("change",this,e,!0)})),!1):(x.event.add(this,"beforeactivate._change",function(e){var t=e.target;Z.test(t.nodeName)&&!x._data(t,"changeBubbles")&&(x.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||x.event.simulate("change",this.parentNode,e,!0)}),x._data(t,"changeBubbles",!0))}),t)},handle:function(e){var n=e.target;return this!==n||e.isSimulated||e.isTrigger||"radio"!==n.type&&"checkbox"!==n.type?e.handleObj.handler.apply(this,arguments):t},teardown:function(){return x.event.remove(this,"._change"),!Z.test(this.nodeName)}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&a.addEventListener(e,r,!0)},teardown:function(){0===--n&&a.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,n,r,i,o){var a,s;if("object"==typeof e){"string"!=typeof n&&(r=r||n,n=t);for(a in e)this.on(a,n,r,e[a],o);return this}if(null==r&&null==i?(i=n,r=n=t):null==i&&("string"==typeof n?(i=r,r=t):(i=r,r=n,n=t)),i===!1)i=ot;else if(!i)return this;return 1===o&&(s=i,i=function(e){return x().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=x.guid++)),this.each(function(){x.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,o;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,x(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if("object"==typeof e){for(o in e)this.off(o,n,e[o]);return this}return(n===!1||"function"==typeof n)&&(r=n,n=t),r===!1&&(r=ot),this.each(function(){x.event.remove(this,e,r,n)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,n){var r=this[0];return r?x.event.trigger(e,n,r,!0):t}});var st=/^.[^:#\[\.,]*$/,lt=/^(?:parents|prev(?:Until|All))/,ut=x.expr.match.needsContext,ct={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t,n=x(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(x.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e||[],!0))},filter:function(e){return this.pushStack(ft(this,e||[],!1))},is:function(e){return!!ft(this,"string"==typeof e&&ut.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],a=ut.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(a?a.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?x.inArray(this[0],x(e)):x.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return pt(e,"nextSibling")},prev:function(e){return pt(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return x.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(ct[e]||(i=x.unique(i)),lt.test(e)&&(i=i.reverse())),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,n,r){var i=[],o=e[n];while(o&&9!==o.nodeType&&(r===t||1!==o.nodeType||!x(o).is(r)))1===o.nodeType&&i.push(o),o=o[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function ft(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(st.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return x.inArray(e,t)>=0!==n})}function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}var ht="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gt=/ jQuery\d+="(?:null|\d+)"/g,mt=RegExp("<(?:"+ht+")[\\s/>]","i"),yt=/^\s+/,vt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bt=/<([\w:]+)/,xt=/\s*$/g,At={option:[1,""," "],legend:[1,""," "],area:[1,""," "],param:[1,""," "],thead:[1,""],tr:[2,""],col:[2,""],td:[3,""],_default:x.support.htmlSerialize?[0,"",""]:[1,"X","
"]},jt=dt(a),Dt=jt.appendChild(a.createElement("div"));At.optgroup=At.option,At.tbody=At.tfoot=At.colgroup=At.caption=At.thead,At.th=At.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===t?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||a).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(Ft(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&_t(Ft(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++){1===e.nodeType&&x.cleanData(Ft(e,!1));while(e.firstChild)e.removeChild(e.firstChild);e.options&&x.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return 1===n.nodeType?n.innerHTML.replace(gt,""):t;if(!("string"!=typeof e||Tt.test(e)||!x.support.htmlSerialize&&mt.test(e)||!x.support.leadingWhitespace&&yt.test(e)||At[(bt.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(vt,"<$1>$2>");try{for(;i>r;r++)n=this[r]||{},1===n.nodeType&&(x.cleanData(Ft(n,!1)),n.innerHTML=e);n=0}catch(o){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=d.apply([],e);var r,i,o,a,s,l,u=0,c=this.length,p=this,f=c-1,h=e[0],g=x.isFunction(h);if(g||!(1>=c||"string"!=typeof h||x.support.checkClone)&&Nt.test(h))return this.each(function(r){var i=p.eq(r);g&&(e[0]=h.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(l=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),r=l.firstChild,1===l.childNodes.length&&(l=r),r)){for(a=x.map(Ft(l,"script"),Ht),o=a.length;c>u;u++)i=l,u!==f&&(i=x.clone(i,!0,!0),o&&x.merge(a,Ft(i,"script"))),t.call(this[u],i,u);if(o)for(s=a[a.length-1].ownerDocument,x.map(a,qt),u=0;o>u;u++)i=a[u],kt.test(i.type||"")&&!x._data(i,"globalEval")&&x.contains(s,i)&&(i.src?x._evalUrl(i.src):x.globalEval((i.text||i.textContent||i.innerHTML||"").replace(St,"")));l=r=null}return this}});function Lt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function Ht(e){return e.type=(null!==x.find.attr(e,"type"))+"/"+e.type,e}function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function _t(e,t){var n,r=0;for(;null!=(n=e[r]);r++)x._data(n,"globalEval",!t||x._data(t[r],"globalEval"))}function Mt(e,t){if(1===t.nodeType&&x.hasData(e)){var n,r,i,o=x._data(e),a=x._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)x.event.add(t,n,s[n][r])}a.data&&(a.data=x.extend({},a.data))}}function Ot(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!x.support.noCloneEvent&&t[x.expando]){i=x._data(t);for(r in i.events)x.removeEvent(t,r,i.handle);t.removeAttribute(x.expando)}"script"===n&&t.text!==e.text?(Ht(t).text=e.text,qt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),x.support.html5Clone&&e.innerHTML&&!x.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Ct.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=0,i=[],o=x(e),a=o.length-1;for(;a>=r;r++)n=r===a?this:this.clone(!0),x(o[r])[t](n),h.apply(i,n.get());return this.pushStack(i)}});function Ft(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!s)for(s=[],r=e.childNodes||e;null!=(o=r[a]);a++)!n||x.nodeName(o,n)?s.push(o):x.merge(s,Ft(o,n));return n===t||n&&x.nodeName(e,n)?x.merge([e],s):s}function Bt(e){Ct.test(e.type)&&(e.defaultChecked=e.checked)}x.extend({clone:function(e,t,n){var r,i,o,a,s,l=x.contains(e.ownerDocument,e);if(x.support.html5Clone||x.isXMLDoc(e)||!mt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Dt.innerHTML=e.outerHTML,Dt.removeChild(o=Dt.firstChild)),!(x.support.noCloneEvent&&x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(r=Ft(o),s=Ft(e),a=0;null!=(i=s[a]);++a)r[a]&&Ot(i,r[a]);if(t)if(n)for(s=s||Ft(e),r=r||Ft(o),a=0;null!=(i=s[a]);a++)Mt(i,r[a]);else Mt(e,o);return r=Ft(o,"script"),r.length>0&&_t(r,!l&&Ft(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){var i,o,a,s,l,u,c,p=e.length,f=dt(t),d=[],h=0;for(;p>h;h++)if(o=e[h],o||0===o)if("object"===x.type(o))x.merge(d,o.nodeType?[o]:o);else if(wt.test(o)){s=s||f.appendChild(t.createElement("div")),l=(bt.exec(o)||["",""])[1].toLowerCase(),c=At[l]||At._default,s.innerHTML=c[1]+o.replace(vt,"<$1>$2>")+c[2],i=c[0];while(i--)s=s.lastChild;if(!x.support.leadingWhitespace&&yt.test(o)&&d.push(t.createTextNode(yt.exec(o)[0])),!x.support.tbody){o="table"!==l||xt.test(o)?""!==c[1]||xt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;while(i--)x.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u)}x.merge(d,s.childNodes),s.textContent="";while(s.firstChild)s.removeChild(s.firstChild);s=f.lastChild}else d.push(t.createTextNode(o));s&&f.removeChild(s),x.support.appendChecked||x.grep(Ft(d,"input"),Bt),h=0;while(o=d[h++])if((!r||-1===x.inArray(o,r))&&(a=x.contains(o.ownerDocument,o),s=Ft(f.appendChild(o),"script"),a&&_t(s),n)){i=0;while(o=s[i++])kt.test(o.type||"")&&n.push(o)}return s=null,f},cleanData:function(e,t){var n,r,o,a,s=0,l=x.expando,u=x.cache,c=x.support.deleteExpando,f=x.event.special;for(;null!=(n=e[s]);s++)if((t||x.acceptData(n))&&(o=n[l],a=o&&u[o])){if(a.events)for(r in a.events)f[r]?x.event.remove(n,r):x.removeEvent(n,r,a.handle);
+u[o]&&(delete u[o],c?delete n[l]:typeof n.removeAttribute!==i?n.removeAttribute(l):n[l]=null,p.push(o))}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}}),x.fn.extend({wrapAll:function(e){if(x.isFunction(e))return this.each(function(t){x(this).wrapAll(e.call(this,t))});if(this[0]){var t=x(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&1===e.firstChild.nodeType)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var Pt,Rt,Wt,$t=/alpha\([^)]*\)/i,It=/opacity\s*=\s*([^)]*)/,zt=/^(top|right|bottom|left)$/,Xt=/^(none|table(?!-c[ea]).+)/,Ut=/^margin/,Vt=RegExp("^("+w+")(.*)$","i"),Yt=RegExp("^("+w+")(?!px)[a-z%]+$","i"),Jt=RegExp("^([+-])=("+w+")","i"),Gt={BODY:"block"},Qt={position:"absolute",visibility:"hidden",display:"block"},Kt={letterSpacing:0,fontWeight:400},Zt=["Top","Right","Bottom","Left"],en=["Webkit","O","Moz","ms"];function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=en.length;while(i--)if(t=en[i]+n,t in e)return t;return r}function nn(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function rn(e,t){var n,r,i,o=[],a=0,s=e.length;for(;s>a;a++)r=e[a],r.style&&(o[a]=x._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&nn(r)&&(o[a]=x._data(r,"olddisplay",ln(r.nodeName)))):o[a]||(i=nn(r),(n&&"none"!==n||!i)&&x._data(r,"olddisplay",i?n:x.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}x.fn.extend({css:function(e,n){return x.access(this,function(e,n,r){var i,o,a={},s=0;if(x.isArray(n)){for(o=Rt(e),i=n.length;i>s;s++)a[n[s]]=x.css(e,n[s],!1,o);return a}return r!==t?x.style(e,n,r):x.css(e,n)},e,n,arguments.length>1)},show:function(){return rn(this,!0)},hide:function(){return rn(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){nn(this)?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Wt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":x.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,s,l=x.camelCase(n),u=e.style;if(n=x.cssProps[l]||(x.cssProps[l]=tn(u,l)),s=x.cssHooks[n]||x.cssHooks[l],r===t)return s&&"get"in s&&(o=s.get(e,!1,i))!==t?o:u[n];if(a=typeof r,"string"===a&&(o=Jt.exec(r))&&(r=(o[1]+1)*o[2]+parseFloat(x.css(e,n)),a="number"),!(null==r||"number"===a&&isNaN(r)||("number"!==a||x.cssNumber[l]||(r+="px"),x.support.clearCloneStyle||""!==r||0!==n.indexOf("background")||(u[n]="inherit"),s&&"set"in s&&(r=s.set(e,r,i))===t)))try{u[n]=r}catch(c){}}},css:function(e,n,r,i){var o,a,s,l=x.camelCase(n);return n=x.cssProps[l]||(x.cssProps[l]=tn(e.style,l)),s=x.cssHooks[n]||x.cssHooks[l],s&&"get"in s&&(a=s.get(e,!0,r)),a===t&&(a=Wt(e,n,i)),"normal"===a&&n in Kt&&(a=Kt[n]),""===r||r?(o=parseFloat(a),r===!0||x.isNumeric(o)?o||0:a):a}}),e.getComputedStyle?(Rt=function(t){return e.getComputedStyle(t,null)},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s.getPropertyValue(n)||s[n]:t,u=e.style;return s&&(""!==l||x.contains(e.ownerDocument,e)||(l=x.style(e,n)),Yt.test(l)&&Ut.test(n)&&(i=u.width,o=u.minWidth,a=u.maxWidth,u.minWidth=u.maxWidth=u.width=l,l=s.width,u.width=i,u.minWidth=o,u.maxWidth=a)),l}):a.documentElement.currentStyle&&(Rt=function(e){return e.currentStyle},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s[n]:t,u=e.style;return null==l&&u&&u[n]&&(l=u[n]),Yt.test(l)&&!zt.test(n)&&(i=u.left,o=e.runtimeStyle,a=o&&o.left,a&&(o.left=e.currentStyle.left),u.left="fontSize"===n?"1em":l,l=u.pixelLeft+"px",u.left=i,a&&(o.left=a)),""===l?"auto":l});function on(e,t,n){var r=Vt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function an(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;for(;4>o;o+=2)"margin"===n&&(a+=x.css(e,n+Zt[o],!0,i)),r?("content"===n&&(a-=x.css(e,"padding"+Zt[o],!0,i)),"margin"!==n&&(a-=x.css(e,"border"+Zt[o]+"Width",!0,i))):(a+=x.css(e,"padding"+Zt[o],!0,i),"padding"!==n&&(a+=x.css(e,"border"+Zt[o]+"Width",!0,i)));return a}function sn(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Rt(e),a=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=Wt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Yt.test(i))return i;r=a&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+an(e,t,n||(a?"border":"content"),r,o)+"px"}function ln(e){var t=a,n=Gt[e];return n||(n=un(e,t),"none"!==n&&n||(Pt=(Pt||x("").css("cssText","display:block !important")).appendTo(t.documentElement),t=(Pt[0].contentWindow||Pt[0].contentDocument).document,t.write(""),t.close(),n=un(e,t),Pt.detach()),Gt[e]=n),n}function un(e,t){var n=x(t.createElement(e)).appendTo(t.body),r=x.css(n[0],"display");return n.remove(),r}x.each(["height","width"],function(e,n){x.cssHooks[n]={get:function(e,r,i){return r?0===e.offsetWidth&&Xt.test(x.css(e,"display"))?x.swap(e,Qt,function(){return sn(e,n,i)}):sn(e,n,i):t},set:function(e,t,r){var i=r&&Rt(e);return on(e,t,r?an(e,n,r,x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,i),i):0)}}}),x.support.opacity||(x.cssHooks.opacity={get:function(e,t){return It.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=x.isNumeric(t)?"alpha(opacity="+100*t+")":"",o=r&&r.filter||n.filter||"";n.zoom=1,(t>=1||""===t)&&""===x.trim(o.replace($t,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=$t.test(o)?o.replace($t,i):o+" "+i)}}),x(function(){x.support.reliableMarginRight||(x.cssHooks.marginRight={get:function(e,n){return n?x.swap(e,{display:"inline-block"},Wt,[e,"marginRight"]):t}}),!x.support.pixelPosition&&x.fn.position&&x.each(["top","left"],function(e,n){x.cssHooks[n]={get:function(e,r){return r?(r=Wt(e,n),Yt.test(r)?x(e).position()[n]+"px":r):t}}})}),x.expr&&x.expr.filters&&(x.expr.filters.hidden=function(e){return 0>=e.offsetWidth&&0>=e.offsetHeight||!x.support.reliableHiddenOffsets&&"none"===(e.style&&e.style.display||x.css(e,"display"))},x.expr.filters.visible=function(e){return!x.expr.filters.hidden(e)}),x.each({margin:"",padding:"",border:"Width"},function(e,t){x.cssHooks[e+t]={expand:function(n){var r=0,i={},o="string"==typeof n?n.split(" "):[n];for(;4>r;r++)i[e+Zt[r]+t]=o[r]||o[r-2]||o[0];return i}},Ut.test(e)||(x.cssHooks[e+t].set=on)});var cn=/%20/g,pn=/\[\]$/,fn=/\r?\n/g,dn=/^(?:submit|button|image|reset|file)$/i,hn=/^(?:input|select|textarea|keygen)/i;x.fn.extend({serialize:function(){return x.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=x.prop(this,"elements");return e?x.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!x(this).is(":disabled")&&hn.test(this.nodeName)&&!dn.test(e)&&(this.checked||!Ct.test(e))}).map(function(e,t){var n=x(this).val();return null==n?null:x.isArray(n)?x.map(n,function(e){return{name:t.name,value:e.replace(fn,"\r\n")}}):{name:t.name,value:n.replace(fn,"\r\n")}}).get()}}),x.param=function(e,n){var r,i=[],o=function(e,t){t=x.isFunction(t)?t():null==t?"":t,i[i.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(n===t&&(n=x.ajaxSettings&&x.ajaxSettings.traditional),x.isArray(e)||e.jquery&&!x.isPlainObject(e))x.each(e,function(){o(this.name,this.value)});else for(r in e)gn(r,e[r],n,o);return i.join("&").replace(cn,"+")};function gn(e,t,n,r){var i;if(x.isArray(t))x.each(t,function(t,i){n||pn.test(e)?r(e,i):gn(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==x.type(t))r(e,t);else for(i in t)gn(e+"["+i+"]",t[i],n,r)}x.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){x.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),x.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var mn,yn,vn=x.now(),bn=/\?/,xn=/#.*$/,wn=/([?&])_=[^&]*/,Tn=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Cn=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Nn=/^(?:GET|HEAD)$/,kn=/^\/\//,En=/^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,Sn=x.fn.load,An={},jn={},Dn="*/".concat("*");try{yn=o.href}catch(Ln){yn=a.createElement("a"),yn.href="",yn=yn.href}mn=En.exec(yn.toLowerCase())||[];function Hn(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(T)||[];if(x.isFunction(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function qn(e,n,r,i){var o={},a=e===jn;function s(l){var u;return o[l]=!0,x.each(e[l]||[],function(e,l){var c=l(n,r,i);return"string"!=typeof c||a||o[c]?a?!(u=c):t:(n.dataTypes.unshift(c),s(c),!1)}),u}return s(n.dataTypes[0])||!o["*"]&&s("*")}function _n(e,n){var r,i,o=x.ajaxSettings.flatOptions||{};for(i in n)n[i]!==t&&((o[i]?e:r||(r={}))[i]=n[i]);return r&&x.extend(!0,e,r),e}x.fn.load=function(e,n,r){if("string"!=typeof e&&Sn)return Sn.apply(this,arguments);var i,o,a,s=this,l=e.indexOf(" ");return l>=0&&(i=e.slice(l,e.length),e=e.slice(0,l)),x.isFunction(n)?(r=n,n=t):n&&"object"==typeof n&&(a="POST"),s.length>0&&x.ajax({url:e,type:a,dataType:"html",data:n}).done(function(e){o=arguments,s.html(i?x("").append(x.parseHTML(e)).find(i):e)}).complete(r&&function(e,t){s.each(r,o||[e.responseText,t,e])}),this},x.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){x.fn[t]=function(e){return this.on(t,e)}}),x.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:yn,type:"GET",isLocal:Cn.test(mn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Dn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":x.parseJSON,"text xml":x.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?_n(_n(e,x.ajaxSettings),t):_n(x.ajaxSettings,e)},ajaxPrefilter:Hn(An),ajaxTransport:Hn(jn),ajax:function(e,n){"object"==typeof e&&(n=e,e=t),n=n||{};var r,i,o,a,s,l,u,c,p=x.ajaxSetup({},n),f=p.context||p,d=p.context&&(f.nodeType||f.jquery)?x(f):x.event,h=x.Deferred(),g=x.Callbacks("once memory"),m=p.statusCode||{},y={},v={},b=0,w="canceled",C={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c){c={};while(t=Tn.exec(a))c[t[1].toLowerCase()]=t[2]}t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=v[n]=v[n]||e,y[e]=t),this},overrideMimeType:function(e){return b||(p.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>b)for(t in e)m[t]=[m[t],e[t]];else C.always(e[C.status]);return this},abort:function(e){var t=e||w;return u&&u.abort(t),k(0,t),this}};if(h.promise(C).complete=g.add,C.success=C.done,C.error=C.fail,p.url=((e||p.url||yn)+"").replace(xn,"").replace(kn,mn[1]+"//"),p.type=n.method||n.type||p.method||p.type,p.dataTypes=x.trim(p.dataType||"*").toLowerCase().match(T)||[""],null==p.crossDomain&&(r=En.exec(p.url.toLowerCase()),p.crossDomain=!(!r||r[1]===mn[1]&&r[2]===mn[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(mn[3]||("http:"===mn[1]?"80":"443")))),p.data&&p.processData&&"string"!=typeof p.data&&(p.data=x.param(p.data,p.traditional)),qn(An,p,n,C),2===b)return C;l=p.global,l&&0===x.active++&&x.event.trigger("ajaxStart"),p.type=p.type.toUpperCase(),p.hasContent=!Nn.test(p.type),o=p.url,p.hasContent||(p.data&&(o=p.url+=(bn.test(o)?"&":"?")+p.data,delete p.data),p.cache===!1&&(p.url=wn.test(o)?o.replace(wn,"$1_="+vn++):o+(bn.test(o)?"&":"?")+"_="+vn++)),p.ifModified&&(x.lastModified[o]&&C.setRequestHeader("If-Modified-Since",x.lastModified[o]),x.etag[o]&&C.setRequestHeader("If-None-Match",x.etag[o])),(p.data&&p.hasContent&&p.contentType!==!1||n.contentType)&&C.setRequestHeader("Content-Type",p.contentType),C.setRequestHeader("Accept",p.dataTypes[0]&&p.accepts[p.dataTypes[0]]?p.accepts[p.dataTypes[0]]+("*"!==p.dataTypes[0]?", "+Dn+"; q=0.01":""):p.accepts["*"]);for(i in p.headers)C.setRequestHeader(i,p.headers[i]);if(p.beforeSend&&(p.beforeSend.call(f,C,p)===!1||2===b))return C.abort();w="abort";for(i in{success:1,error:1,complete:1})C[i](p[i]);if(u=qn(jn,p,n,C)){C.readyState=1,l&&d.trigger("ajaxSend",[C,p]),p.async&&p.timeout>0&&(s=setTimeout(function(){C.abort("timeout")},p.timeout));try{b=1,u.send(y,k)}catch(N){if(!(2>b))throw N;k(-1,N)}}else k(-1,"No Transport");function k(e,n,r,i){var c,y,v,w,T,N=n;2!==b&&(b=2,s&&clearTimeout(s),u=t,a=i||"",C.readyState=e>0?4:0,c=e>=200&&300>e||304===e,r&&(w=Mn(p,C,r)),w=On(p,w,C,c),c?(p.ifModified&&(T=C.getResponseHeader("Last-Modified"),T&&(x.lastModified[o]=T),T=C.getResponseHeader("etag"),T&&(x.etag[o]=T)),204===e||"HEAD"===p.type?N="nocontent":304===e?N="notmodified":(N=w.state,y=w.data,v=w.error,c=!v)):(v=N,(e||!N)&&(N="error",0>e&&(e=0))),C.status=e,C.statusText=(n||N)+"",c?h.resolveWith(f,[y,N,C]):h.rejectWith(f,[C,N,v]),C.statusCode(m),m=t,l&&d.trigger(c?"ajaxSuccess":"ajaxError",[C,p,c?y:v]),g.fireWith(f,[C,N]),l&&(d.trigger("ajaxComplete",[C,p]),--x.active||x.event.trigger("ajaxStop")))}return C},getJSON:function(e,t,n){return x.get(e,t,n,"json")},getScript:function(e,n){return x.get(e,t,n,"script")}}),x.each(["get","post"],function(e,n){x[n]=function(e,r,i,o){return x.isFunction(r)&&(o=o||i,i=r,r=t),x.ajax({url:e,type:n,dataType:o,data:r,success:i})}});function Mn(e,n,r){var i,o,a,s,l=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),o===t&&(o=e.mimeType||n.getResponseHeader("Content-Type"));if(o)for(s in l)if(l[s]&&l[s].test(o)){u.unshift(s);break}if(u[0]in r)a=u[0];else{for(s in r){if(!u[0]||e.converters[s+" "+u[0]]){a=s;break}i||(i=s)}a=a||i}return a?(a!==u[0]&&u.unshift(a),r[a]):t}function On(e,t,n,r){var i,o,a,s,l,u={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)u[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(a=u[l+" "+o]||u["* "+o],!a)for(i in u)if(s=i.split(" "),s[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){a===!0?a=u[i]:u[i]!==!0&&(o=s[0],c.unshift(s[1]));break}if(a!==!0)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(p){return{state:"parsererror",error:a?p:"No conversion from "+l+" to "+o}}}return{state:"success",data:t}}x.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return x.globalEval(e),e}}}),x.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),x.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=a.head||x("head")[0]||a.documentElement;return{send:function(t,i){n=a.createElement("script"),n.async=!0,e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,t){(t||!n.readyState||/loaded|complete/.test(n.readyState))&&(n.onload=n.onreadystatechange=null,n.parentNode&&n.parentNode.removeChild(n),n=null,t||i(200,"success"))},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(t,!0)}}}});var Fn=[],Bn=/(=)\?(?=&|$)|\?\?/;x.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Fn.pop()||x.expando+"_"+vn++;return this[e]=!0,e}}),x.ajaxPrefilter("json jsonp",function(n,r,i){var o,a,s,l=n.jsonp!==!1&&(Bn.test(n.url)?"url":"string"==typeof n.data&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Bn.test(n.data)&&"data");return l||"jsonp"===n.dataTypes[0]?(o=n.jsonpCallback=x.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,l?n[l]=n[l].replace(Bn,"$1"+o):n.jsonp!==!1&&(n.url+=(bn.test(n.url)?"&":"?")+n.jsonp+"="+o),n.converters["script json"]=function(){return s||x.error(o+" was not called"),s[0]},n.dataTypes[0]="json",a=e[o],e[o]=function(){s=arguments},i.always(function(){e[o]=a,n[o]&&(n.jsonpCallback=r.jsonpCallback,Fn.push(o)),s&&x.isFunction(a)&&a(s[0]),s=a=t}),"script"):t});var Pn,Rn,Wn=0,$n=e.ActiveXObject&&function(){var e;for(e in Pn)Pn[e](t,!0)};function In(){try{return new e.XMLHttpRequest}catch(t){}}function zn(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}x.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&In()||zn()}:In,Rn=x.ajaxSettings.xhr(),x.support.cors=!!Rn&&"withCredentials"in Rn,Rn=x.support.ajax=!!Rn,Rn&&x.ajaxTransport(function(n){if(!n.crossDomain||x.support.cors){var r;return{send:function(i,o){var a,s,l=n.xhr();if(n.username?l.open(n.type,n.url,n.async,n.username,n.password):l.open(n.type,n.url,n.async),n.xhrFields)for(s in n.xhrFields)l[s]=n.xhrFields[s];n.mimeType&&l.overrideMimeType&&l.overrideMimeType(n.mimeType),n.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");try{for(s in i)l.setRequestHeader(s,i[s])}catch(u){}l.send(n.hasContent&&n.data||null),r=function(e,i){var s,u,c,p;try{if(r&&(i||4===l.readyState))if(r=t,a&&(l.onreadystatechange=x.noop,$n&&delete Pn[a]),i)4!==l.readyState&&l.abort();else{p={},s=l.status,u=l.getAllResponseHeaders(),"string"==typeof l.responseText&&(p.text=l.responseText);try{c=l.statusText}catch(f){c=""}s||!n.isLocal||n.crossDomain?1223===s&&(s=204):s=p.text?200:404}}catch(d){i||o(-1,d)}p&&o(s,c,p,u)},n.async?4===l.readyState?setTimeout(r):(a=++Wn,$n&&(Pn||(Pn={},x(e).unload($n)),Pn[a]=r),l.onreadystatechange=r):r()},abort:function(){r&&r(t,!0)}}}});var Xn,Un,Vn=/^(?:toggle|show|hide)$/,Yn=RegExp("^(?:([+-])=|)("+w+")([a-z%]*)$","i"),Jn=/queueHooks$/,Gn=[nr],Qn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=Yn.exec(t),o=i&&i[3]||(x.cssNumber[e]?"":"px"),a=(x.cssNumber[e]||"px"!==o&&+r)&&Yn.exec(x.css(n.elem,e)),s=1,l=20;if(a&&a[3]!==o){o=o||a[3],i=i||[],a=+r||1;do s=s||".5",a/=s,x.style(n.elem,e,a+o);while(s!==(s=n.cur()/r)&&1!==s&&--l)}return i&&(a=n.start=+a||+r||0,n.unit=o,n.end=i[1]?a+(i[1]+1)*i[2]:+i[2]),n}]};function Kn(){return setTimeout(function(){Xn=t}),Xn=x.now()}function Zn(e,t,n){var r,i=(Qn[t]||[]).concat(Qn["*"]),o=0,a=i.length;for(;a>o;o++)if(r=i[o].call(n,t,e))return r}function er(e,t,n){var r,i,o=0,a=Gn.length,s=x.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;var t=Xn||Kn(),n=Math.max(0,u.startTime+u.duration-t),r=n/u.duration||0,o=1-r,a=0,l=u.tweens.length;for(;l>a;a++)u.tweens[a].run(o);return s.notifyWith(e,[u,o,n]),1>o&&l?n:(s.resolveWith(e,[u]),!1)},u=s.promise({elem:e,props:x.extend({},t),opts:x.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:Xn||Kn(),duration:n.duration,tweens:[],createTween:function(t,n){var r=x.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)u.tweens[n].run(1);return t?s.resolveWith(e,[u,t]):s.rejectWith(e,[u,t]),this}}),c=u.props;for(tr(c,u.opts.specialEasing);a>o;o++)if(r=Gn[o].call(u,e,c,u.opts))return r;return x.map(c,Zn,u),x.isFunction(u.opts.start)&&u.opts.start.call(e,u),x.fx.timer(x.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always)}function tr(e,t){var n,r,i,o,a;for(n in e)if(r=x.camelCase(n),i=t[r],o=e[n],x.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),a=x.cssHooks[r],a&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}x.Animation=x.extend(er,{tweener:function(e,t){x.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;i>r;r++)n=e[r],Qn[n]=Qn[n]||[],Qn[n].unshift(t)},prefilter:function(e,t){t?Gn.unshift(e):Gn.push(e)}});function nr(e,t,n){var r,i,o,a,s,l,u=this,c={},p=e.style,f=e.nodeType&&nn(e),d=x._data(e,"fxshow");n.queue||(s=x._queueHooks(e,"fx"),null==s.unqueued&&(s.unqueued=0,l=s.empty.fire,s.empty.fire=function(){s.unqueued||l()}),s.unqueued++,u.always(function(){u.always(function(){s.unqueued--,x.queue(e,"fx").length||s.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],"inline"===x.css(e,"display")&&"none"===x.css(e,"float")&&(x.support.inlineBlockNeedsLayout&&"inline"!==ln(e.nodeName)?p.zoom=1:p.display="inline-block")),n.overflow&&(p.overflow="hidden",x.support.shrinkWrapBlocks||u.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t)if(i=t[r],Vn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(f?"hide":"show"))continue;c[r]=d&&d[r]||x.style(e,r)}if(!x.isEmptyObject(c)){d?"hidden"in d&&(f=d.hidden):d=x._data(e,"fxshow",{}),o&&(d.hidden=!f),f?x(e).show():u.done(function(){x(e).hide()}),u.done(function(){var t;x._removeData(e,"fxshow");for(t in c)x.style(e,t,c[t])});for(r in c)a=Zn(f?d[r]:0,r,u),r in d||(d[r]=a.start,f&&(a.end=a.start,a.start="width"===r||"height"===r?1:0))}}function rr(e,t,n,r,i){return new rr.prototype.init(e,t,n,r,i)}x.Tween=rr,rr.prototype={constructor:rr,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(x.cssNumber[n]?"":"px")},cur:function(){var e=rr.propHooks[this.prop];return e&&e.get?e.get(this):rr.propHooks._default.get(this)},run:function(e){var t,n=rr.propHooks[this.prop];return this.pos=t=this.options.duration?x.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):rr.propHooks._default.set(this),this}},rr.prototype.init.prototype=rr.prototype,rr.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=x.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){x.fx.step[e.prop]?x.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[x.cssProps[e.prop]]||x.cssHooks[e.prop])?x.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},rr.propHooks.scrollTop=rr.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},x.each(["toggle","show","hide"],function(e,t){var n=x.fn[t];x.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(ir(t,!0),e,r,i)}}),x.fn.extend({fadeTo:function(e,t,n,r){return this.filter(nn).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=x.isEmptyObject(e),o=x.speed(t,n,r),a=function(){var t=er(this,x.extend({},e),o);(i||x._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,n,r){var i=function(e){var t=e.stop;delete e.stop,t(r)};return"string"!=typeof e&&(r=n,n=e,e=t),n&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,n=null!=e&&e+"queueHooks",o=x.timers,a=x._data(this);if(n)a[n]&&a[n].stop&&i(a[n]);else for(n in a)a[n]&&a[n].stop&&Jn.test(n)&&i(a[n]);for(n=o.length;n--;)o[n].elem!==this||null!=e&&o[n].queue!==e||(o[n].anim.stop(r),t=!1,o.splice(n,1));(t||!r)&&x.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=x._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=x.timers,a=r?r.length:0;for(n.finish=!0,x.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}});function ir(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=Zt[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}x.each({slideDown:ir("show"),slideUp:ir("hide"),slideToggle:ir("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){x.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),x.speed=function(e,t,n){var r=e&&"object"==typeof e?x.extend({},e):{complete:n||!n&&t||x.isFunction(e)&&e,duration:e,easing:n&&t||t&&!x.isFunction(t)&&t};return r.duration=x.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in x.fx.speeds?x.fx.speeds[r.duration]:x.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){x.isFunction(r.old)&&r.old.call(this),r.queue&&x.dequeue(this,r.queue)},r},x.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},x.timers=[],x.fx=rr.prototype.init,x.fx.tick=function(){var e,n=x.timers,r=0;for(Xn=x.now();n.length>r;r++)e=n[r],e()||n[r]!==e||n.splice(r--,1);n.length||x.fx.stop(),Xn=t},x.fx.timer=function(e){e()&&x.timers.push(e)&&x.fx.start()},x.fx.interval=13,x.fx.start=function(){Un||(Un=setInterval(x.fx.tick,x.fx.interval))},x.fx.stop=function(){clearInterval(Un),Un=null},x.fx.speeds={slow:600,fast:200,_default:400},x.fx.step={},x.expr&&x.expr.filters&&(x.expr.filters.animated=function(e){return x.grep(x.timers,function(t){return e===t.elem}).length}),x.fn.offset=function(e){if(arguments.length)return e===t?this:this.each(function(t){x.offset.setOffset(this,e,t)});var n,r,o={top:0,left:0},a=this[0],s=a&&a.ownerDocument;if(s)return n=s.documentElement,x.contains(n,a)?(typeof a.getBoundingClientRect!==i&&(o=a.getBoundingClientRect()),r=or(s),{top:o.top+(r.pageYOffset||n.scrollTop)-(n.clientTop||0),left:o.left+(r.pageXOffset||n.scrollLeft)-(n.clientLeft||0)}):o},x.offset={setOffset:function(e,t,n){var r=x.css(e,"position");"static"===r&&(e.style.position="relative");var i=x(e),o=i.offset(),a=x.css(e,"top"),s=x.css(e,"left"),l=("absolute"===r||"fixed"===r)&&x.inArray("auto",[a,s])>-1,u={},c={},p,f;l?(c=i.position(),p=c.top,f=c.left):(p=parseFloat(a)||0,f=parseFloat(s)||0),x.isFunction(t)&&(t=t.call(e,n,o)),null!=t.top&&(u.top=t.top-o.top+p),null!=t.left&&(u.left=t.left-o.left+f),"using"in t?t.using.call(e,u):i.css(u)}},x.fn.extend({position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===x.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),x.nodeName(e[0],"html")||(n=e.offset()),n.top+=x.css(e[0],"borderTopWidth",!0),n.left+=x.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-x.css(r,"marginTop",!0),left:t.left-n.left-x.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||s;while(e&&!x.nodeName(e,"html")&&"static"===x.css(e,"position"))e=e.offsetParent;return e||s})}}),x.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);x.fn[e]=function(i){return x.access(this,function(e,i,o){var a=or(e);return o===t?a?n in a?a[n]:a.document.documentElement[i]:e[i]:(a?a.scrollTo(r?x(a).scrollLeft():o,r?o:x(a).scrollTop()):e[i]=o,t)},e,i,arguments.length,null)}});function or(e){return x.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}x.each({Height:"height",Width:"width"},function(e,n){x.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){x.fn[i]=function(i,o){var a=arguments.length&&(r||"boolean"!=typeof i),s=r||(i===!0||o===!0?"margin":"border");return x.access(this,function(n,r,i){var o;return x.isWindow(n)?n.document.documentElement["client"+e]:9===n.nodeType?(o=n.documentElement,Math.max(n.body["scroll"+e],o["scroll"+e],n.body["offset"+e],o["offset"+e],o["client"+e])):i===t?x.css(n,r,s):x.style(n,r,i,s)},n,a?i:t,a,null)}})}),x.fn.size=function(){return this.length},x.fn.andSelf=x.fn.addBack,"object"==typeof module&&module&&"object"==typeof module.exports?module.exports=x:(e.jQuery=e.$=x,"function"==typeof define&&define.amd&&define("jquery",[],function(){return x}))})(window);
diff --git a/assets/js/stupidtable.min.js b/assets/js/stupidtable.min.js
new file mode 100755
index 000000000..919af278d
--- /dev/null
+++ b/assets/js/stupidtable.min.js
@@ -0,0 +1,3 @@
+(function(f){f.fn.stupidtable=function(l){return this.each(function(){var j=f(this);l=l||{};l=f.extend({},{"int":function(e,b){return parseInt(e,10)-parseInt(b,10)},"float":function(e,b){return parseFloat(e)-parseFloat(b)},string:function(e,b){return e
b?1:0}},l);j.on("click","th",function(){var e=j.children("tbody").children("tr"),b=f(this),m=0;j.find("th").slice(0,b.index()).each(function(){var b=f(this).attr("colspan")||1;m+=parseInt(b,10)});var p=b.data("sort")||null;if(null!==p){var n=
+"asc"===b.data("sort-dir")?"desc":"asc";j.trigger("beforetablesort",{column:m,direction:n});setTimeout(function(){var g=[],c=l[p];e.each(function(b,c){var a=f(c).children().eq(m),d=a.data("sort-value"),a="undefined"!==typeof d?d:a.text();g.push(a)});var h=[],d=g.slice(0),a=g.slice(0).reverse(),k=g.slice(0).sort(c);if((d&&k&&!(d maxRequestsPerHost)
+ maxRequestsPerHost = hostAndRequests[keys[i]];
+ }
+ this.stats.push(maxRequestsPerHost);
+ }
+ });
diff --git a/lib/aggregators/yslow/noduplicates.js b/lib/aggregators/yslow/noduplicates.js
new file mode 100644
index 000000000..7b6604fa6
--- /dev/null
+++ b/lib/aggregators/yslow/noduplicates.js
@@ -0,0 +1,19 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var Aggregator = require('../../aggregator');
+
+module.exports = new Aggregator('noDuplicates', 'Remove duplicate JS and CSS',
+ 'It is bad practice include the same js or css twice since browsers will execute the code each toim',
+ '', 1,
+ function(pageData) {
+ if (pageData.yslow) {
+ if (pageData.yslow.g.noduplicates) {
+ this.stats.push(pageData.yslow.g.noduplicates.components.length);
+ } else this.stats.push(0);
+ }
+
+ });
diff --git a/lib/aggregators/yslow/numberOfDomains.js b/lib/aggregators/yslow/numberOfDomains.js
new file mode 100644
index 000000000..c26e63e79
--- /dev/null
+++ b/lib/aggregators/yslow/numberOfDomains.js
@@ -0,0 +1,17 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var util = require('../../util');
+var Aggregator = require('../../aggregator');
+
+module.exports = new Aggregator('numberOfDomains', 'Number Of Domains',
+ 'Many domains means many DNS lookups and that means slower pages.',
+ '', 2,
+ function(pageData) {
+ if (pageData.yslow) {
+ this.stats.push(util.getNumberOfDomains(pageData.yslow.comps));
+ }
+ });
diff --git a/lib/aggregators/yslow/pageWeight.js b/lib/aggregators/yslow/pageWeight.js
new file mode 100644
index 000000000..490a62a01
--- /dev/null
+++ b/lib/aggregators/yslow/pageWeight.js
@@ -0,0 +1,18 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var util = require('../../util');
+var Aggregator = require('../../aggregator');
+
+module.exports = new Aggregator('pageWeight',
+ 'Total page weight (including all assets)',
+ 'The total size is really important because of slow mobile networks, keep the size small.',
+ 'bytes', 2,
+ function(pageData) {
+ if (pageData.yslow) {
+ this.stats.push(util.getSize(pageData.yslow.comps));
+ }
+ });
diff --git a/lib/aggregators/yslow/pagesWithSPOF.js b/lib/aggregators/yslow/pagesWithSPOF.js
new file mode 100644
index 000000000..ec7185558
--- /dev/null
+++ b/lib/aggregators/yslow/pagesWithSPOF.js
@@ -0,0 +1,18 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var Aggregator = require('../../aggregator');
+
+module.exports = new Aggregator('pagesWithSPOF', 'Pages with SPOF',
+ "How many pages have a single point of failures (meaning if someone elses API/site is broken, it will break your page.",
+ '', 1,
+ function(pageData) {
+ if (pageData.yslow) {
+ if (pageData.yslow.g.spof) {
+ this.stats.push(1);
+ } else this.stats.push(0);
+ }
+ });
diff --git a/lib/aggregators/yslow/redirectsPerPage.js b/lib/aggregators/yslow/redirectsPerPage.js
new file mode 100644
index 000000000..3c00fbece
--- /dev/null
+++ b/lib/aggregators/yslow/redirectsPerPage.js
@@ -0,0 +1,18 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var Aggregator = require('../../aggregator');
+
+module.exports = new Aggregator('redirectsPerPage', 'Redirects Per Page',
+ 'Avoid doing redirects, it will slow down the page!',
+ '', 2,
+ function(pageData) {
+ if (pageData.yslow) {
+ if (pageData.yslow.g.redirects) {
+ this.stats.push(pageData.yslow.g.redirects.components.length);
+ } else this.stats.push(0);
+ }
+ });
diff --git a/lib/aggregators/yslow/requests.js b/lib/aggregators/yslow/requests.js
new file mode 100644
index 000000000..9cc884987
--- /dev/null
+++ b/lib/aggregators/yslow/requests.js
@@ -0,0 +1,15 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var Aggregator = require('../../aggregator');
+
+module.exports = new Aggregator('requests', 'Number of requests per page',
+ 'Fewer requests are always faster than many requests.',
+ '', 1,
+ function (pageData) {
+ if (pageData.yslow)
+ this.stats.push(pageData.yslow.r);
+ });
diff --git a/lib/aggregators/yslow/requestsWithoutExpires.js b/lib/aggregators/yslow/requestsWithoutExpires.js
new file mode 100644
index 000000000..5b9f47633
--- /dev/null
+++ b/lib/aggregators/yslow/requestsWithoutExpires.js
@@ -0,0 +1,24 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var util = require('../../util');
+var Aggregator = require('../../aggregator');
+
+module.exports = new Aggregator('requestsWithoutExpires',
+ 'Requests Without Expires',
+ 'Requests shall always have expire headers, so that they can be cached by the browser',
+ '', 2,
+ function(pageData) {
+ if (pageData.yslow) {
+ var requestsWithoutExpire = 0;
+ pageData.yslow.comps.forEach(function(comp) {
+ if (util.getCacheTime(comp) === 0)
+ requestsWithoutExpire++;
+ });
+
+ this.stats.push(requestsWithoutExpire);
+ }
+ });
diff --git a/lib/aggregators/yslow/requestsWithoutGzip.js b/lib/aggregators/yslow/requestsWithoutGzip.js
new file mode 100644
index 000000000..f9d81109f
--- /dev/null
+++ b/lib/aggregators/yslow/requestsWithoutGzip.js
@@ -0,0 +1,19 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var Aggregator = require('../../aggregator');
+
+module.exports = new Aggregator('requestsWithoutGzip', 'Requests without GZip',
+ 'All text content (JS/CSS/HTML) can and should be sent GZiped so that the size is as small as possible.',
+ '', 2,
+ function(pageData) {
+ if (pageData.yslow) {
+ if (pageData.yslow.g.ycompress) {
+ this.stats.push(pageData.yslow.g.ycompress.components.length);
+ } else this.stats.push(0);
+ }
+
+ });
diff --git a/lib/aggregators/yslow/ruleScore.js b/lib/aggregators/yslow/ruleScore.js
new file mode 100644
index 000000000..99a7ba2b7
--- /dev/null
+++ b/lib/aggregators/yslow/ruleScore.js
@@ -0,0 +1,9 @@
+var Aggregator = require('../../aggregator');
+
+module.exports = new Aggregator('ruleScore', 'Rule Score',
+ 'The sitespeed.io total rule score for all the pages',
+ '', 0,
+ function (pageData) {
+ if (pageData.yslow)
+ this.stats.push(pageData.yslow.o);
+ });
diff --git a/lib/aggregators/yslow/singleDomainRequests.js b/lib/aggregators/yslow/singleDomainRequests.js
new file mode 100644
index 000000000..17656eab9
--- /dev/null
+++ b/lib/aggregators/yslow/singleDomainRequests.js
@@ -0,0 +1,26 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var util = require('../../util');
+var Aggregator = require('../../aggregator');
+
+module.exports = new Aggregator('singleDomainRequests', 'Domains with only one request',
+ 'Many domains means many DNS lookups and that means slower pages. Only loading one request for one domain is wasteful.',
+ '', 2,
+ function (pageData) {
+ if (pageData.yslow) {
+ var hostAndRequests = util.getAssetsPerDomain(pageData.yslow.comps),
+ keys = Object.keys(hostAndRequests),
+ domainsWithOneRequest = 0;
+
+ // take the hosts with the most requests
+ for (var i = 0; i < keys.length; i++) {
+ if (hostAndRequests[keys[i]] === 1)
+ domainsWithOneRequest++;
+ }
+ this.stats.push(domainsWithOneRequest);
+ }
+ });
diff --git a/lib/aggregators/yslow/spofPerPage.js b/lib/aggregators/yslow/spofPerPage.js
new file mode 100644
index 000000000..fec77209e
--- /dev/null
+++ b/lib/aggregators/yslow/spofPerPage.js
@@ -0,0 +1,19 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var Aggregator = require('../../aggregator');
+
+module.exports = new Aggregator('spofPerPage',
+ "Number of SPOF per page",
+ "A Single Point Of Failure is a asset that is loaded (usually) from another domain before the page starts rendering, meaning if the asset isn't loaded, the page will be broken/slow.",
+ '', 1,
+ function(pageData) {
+ if (pageData.yslow) {
+ if (pageData.yslow.g.spof) {
+ this.stats.push(pageData.yslow.g.spof.components.length);
+ } else this.stats.push(0);
+ }
+ });
diff --git a/lib/aggregators/yslow/timeSinceLastMod.js b/lib/aggregators/yslow/timeSinceLastMod.js
new file mode 100644
index 000000000..e41e43b64
--- /dev/null
+++ b/lib/aggregators/yslow/timeSinceLastMod.js
@@ -0,0 +1,21 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var util = require('../../util');
+var Aggregator = require('../../aggregator');
+
+module.exports = new Aggregator('timeSinceLastMod',
+ 'Time since last modification',
+ 'The time since a file was changed the last time. If it was long ago, please make sure the cache time is high!',
+ 'seconds', 0,
+ function(pageData) {
+ if (pageData.yslow) {
+ var self = this;
+ pageData.yslow.comps.forEach(function(comp) {
+ self.stats.push(util.getTimeSinceLastMod(comp));
+ });
+ }
+ });
diff --git a/lib/analyze/analyzer.js b/lib/analyze/analyzer.js
new file mode 100644
index 000000000..37d2767bf
--- /dev/null
+++ b/lib/analyze/analyzer.js
@@ -0,0 +1,99 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var path = require('path'),
+ config = require('./../conf'),
+ yslow = require('./yslow'),
+ gpsi = require('./gpsi'),
+ browsertime = require('./browsertime'),
+ webpagetest = require('./webpagetest'),
+ screenshots = require('./screenshots'),
+ fs = require('fs-extra'),
+ log = require('winston'),
+ async = require("async");
+
+module.exports = Analyzer;
+
+function Analyzer(completionCallback) {
+ this.completionCallback = completionCallback;
+}
+
+Analyzer.prototype.analyze = function(urls, collector, urlAnalysedCallback) {
+ var self = this;
+ if (urls.length === 0) {
+ self.completionCallback();
+ }
+
+ var tasks = [
+ function(asyncDoneCallback) {
+ if (config.runYslow)
+ yslow.analyze(urls, asyncDoneCallback);
+ else asyncDoneCallback(undefined, {});
+ },
+ function(asyncDoneCallback) {
+ if (config.googleKey)
+ gpsi.analyze(urls, asyncDoneCallback);
+ else asyncDoneCallback(undefined, {});
+ },
+ function(asyncDoneCallback) {
+ if (config.browser)
+ browsertime.analyze(urls, asyncDoneCallback);
+ else asyncDoneCallback(undefined, {});
+ },
+ function(asyncDoneCallback) {
+ if (config.webpagetest)
+ webpagetest.analyze(urls, asyncDoneCallback);
+ else asyncDoneCallback(undefined, {});
+ },
+ function(asyncDoneCallback) {
+ if (config.screenshot)
+ screenshots.analyze(urls, asyncDoneCallback);
+ else asyncDoneCallback(undefined, {});
+ }
+ ];
+
+ async.series(tasks, function(errors, results) {
+ // Lets go through all the urls and create
+ // pageData and collect it
+ urls.forEach(function(url) {
+ var err = '';
+ var pageData = {};
+ results.forEach(function(result) {
+ // if the result is empty, take the next one
+ if (!result.type) return;
+ else {
+ Object.keys(result.data).forEach(function(dataUrl) {
+ // There's an ugly hack for browsertime, since we can have mutiple
+ // results, the url for BT also contains the browsername
+ if (result.type === 'browsertime-har') {
+ var urlWithoutBrowserName = dataUrl.substr(0, dataUrl.lastIndexOf(
+ '-'));
+ if (urlWithoutBrowserName === url) {
+ if (pageData.browsertime) pageData.browsertime.push(
+ result.data[dataUrl].browsertime);
+ else pageData.browsertime = [result.data[dataUrl].browsertime];
+ if (pageData.har) pageData.har.push(result.data[dataUrl].har);
+ else pageData.har = [result.data[dataUrl].har];
+ }
+ }
+ // take care of other data sources
+ else if (dataUrl === url)
+ pageData[result.type] = result.data[url];
+ });
+ }
+
+ Object.keys(result.errors).forEach(function(errorUrl) {
+ if (errorUrl === url)
+ err += result.type + ' ' + result.errors[url];
+ });
+
+ });
+ collector.collectPageData(pageData);
+ urlAnalysedCallback(err, url, pageData);
+ });
+ self.completionCallback(undefined);
+ });
+};
diff --git a/lib/analyze/browsertime.js b/lib/analyze/browsertime.js
new file mode 100644
index 000000000..c2142330c
--- /dev/null
+++ b/lib/analyze/browsertime.js
@@ -0,0 +1,121 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+
+var util = require('../util'),
+ fs = require('fs-extra'),
+ spawn = require('child_process').spawn,
+ config = require('../conf'),
+ path = require('path'),
+ log = require('winston'),
+ async = require('async');
+
+var dataPath = path.join(config.run.absResultDir, config.dataDir);
+
+module.exports = {
+ analyze: function(urls, asyncDoneCallback) {
+
+ var browsers = config.browser.split(",");
+
+ browsers.forEach(function(browser) {
+ fs.mkdirsSync(path.join(dataPath, 'browsertime', browser));
+ fs.mkdirsSync(path.join(dataPath, 'har', browser));
+ });
+
+ var queue = async.queue(runBrowsertime, 1);
+ var errors = {};
+ var pageData = {};
+
+ urls.forEach(function(u) {
+ browsers.forEach(function(browser) {
+ log.log('info', 'Queueing browsertime for ' + u + ' ' + browser);
+ queue.push({
+ 'url': u,
+ 'browser': browser
+ }, function(data, code) {
+ if (code) {
+ log.log('error', 'Error running browsertime: ' + code);
+ errors[u] = code;
+ } else
+ pageData[u + '-' + browser] = data;
+ });
+ });
+ });
+
+ queue.drain = function() {
+ asyncDoneCallback(undefined,{'type': 'browsertime-har', 'data':pageData, 'errors': errors});
+ };
+ }
+};
+
+function runBrowsertime(args, callback) {
+
+ var url = args.url;
+ var browser = args.browser;
+
+ log.log('info', "Running browsertime for " + browser + ' ' + url);
+
+ var jsonPath = path.join(dataPath, 'browsertime', browser,
+ util.getUrlHash(url) + '-browsertime.json');
+
+ var harPath = path.join(dataPath, 'har', browser,
+ util.getUrlHash(url) + '.har');
+
+ var childArgs = [];
+ childArgs.push('-Xmx' + config.memory + 'm', '-Xms' + config.memory + 'm');
+ childArgs.push('-jar');
+ childArgs.push(path.join(__dirname, '../browsertime-0.7-SNAPSHOT-full.jar'));
+ childArgs.push('--raw');
+ childArgs.push('-f', 'json');
+ childArgs.push('-o', jsonPath);
+ childArgs.push('-b', browser);
+ childArgs.push('-n', config.no);
+ childArgs.push('-ua', config.userAgent);
+ childArgs.push('-w', config.viewPort);
+ if (config.proxy)
+ childArgs.push('-p', config.urlProxyObject.hostname + ':' + config.urlProxyObject
+ .port);
+ if (config.basicAuth)
+ childArgs.push('--basic-auth', config.basicAuth);
+
+ childArgs.push('--har-file', harPath);
+
+ childArgs.push(url);
+
+ var bt = spawn('java', childArgs);
+
+ bt.stdout.on('data', function(data) {
+ /// console.log("stdout:" + data);
+ });
+
+ bt.stderr.on('data', function(data) {
+ // argh the BMP logs a lot on error
+ // console.log('Error from BrowserTime: ' + data);
+ });
+
+ bt.on('close', function(code) {
+ if (bt.exitCode!==0) {
+ callback(undefined, 'Could not fetch data using BrowserTime, exit code ' + bt.exitCode!==0);
+ return;
+ }
+ // TODO check code
+ fs.readFile(jsonPath, function(err, btData) {
+ if (err) {
+ log.log('error', "Couldn't read the file:" + jsonPath);
+ callback(undefined, err);
+ } else {
+ fs.readFile(harPath, function(err, harData) {
+ if (err) {
+ log.log('error', "Couldn't read the file:" + harPath);
+ callback(undefined, err);
+ } else {
+ callback({'browsertime': JSON.parse(btData), 'har': JSON.parse(harData)}, undefined);
+ }
+ });
+ }
+ });
+ });
+}
diff --git a/lib/analyze/gpsi.js b/lib/analyze/gpsi.js
new file mode 100644
index 000000000..32ebd0635
--- /dev/null
+++ b/lib/analyze/gpsi.js
@@ -0,0 +1,85 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+
+var path = require('path'),
+ config = require('./../conf'),
+ util = require('../util'),
+ fs = require('fs'),
+ pagespeed = require('gpagespeed'),
+ log = require('winston'),
+ async = require('async');
+
+var data_path = path.join(config.run.absResultDir, config.dataDir);
+
+module.exports = {
+ analyze: function(urls, callback) {
+ fs.mkdirSync(path.join(data_path, 'gpsi'));
+
+ var queue = async.queue(analyzeUrl, config.threads);
+ var errors = {};
+ var pageData = {};
+
+ urls.forEach(function(u) {
+ queue.push({
+ "url": u
+ }, function(data, err) {
+ if (err) {
+ log.log('error', 'Error running gpsi: ' + err);
+ errors[u] = err;
+ } else
+ pageData[u] = data;
+ });
+ });
+
+ queue.drain = function() {
+ callback(undefined, {'type': 'gpsi', 'data':pageData, 'errors': errors});
+ };
+ }
+};
+
+function analyzeUrl(args, asyncDoneCallback) {
+ var url = args.url;
+ var opts = {
+ url: url,
+ strategy: config.profile,
+ key: config.googleKey
+ };
+
+ log.log('info', 'Running Google Page Speed Insights for ' + url);
+
+ pagespeed(opts, function(err, data) {
+
+ if (err) {
+ log.log('error', 'Error running gpsi:' + url + '(' + err + ')');
+ asyncDoneCallback(undefined, err);
+ return;
+ }
+
+ // did we get an error JSON?
+ var result = JSON.parse(data);
+
+ if (result.error) {
+ // TODO parse the error
+ log.log('error', 'Error running gpsi:' + url + '(' + data + ')');
+ asyncDoneCallback(undefined, result.error.message);
+ } else {
+ var jsonPath = path.join(config.run.absResultDir, config.dataDir,
+ 'gpsi',
+ util.getUrlHash(url) + '-gpsi.json');
+
+ fs.writeFile(jsonPath, data, function(err) {
+ if (err) {
+ log.log('error', "GPSI coudn't store file for url " + url + '(' +
+ err + ')');
+ asyncDoneCallback(undefined, err);
+ }
+ else asyncDoneCallback(result, undefined);
+ });
+ }
+
+ });
+}
diff --git a/lib/analyze/screenshots.js b/lib/analyze/screenshots.js
new file mode 100644
index 000000000..5b7c98666
--- /dev/null
+++ b/lib/analyze/screenshots.js
@@ -0,0 +1,85 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var path = require('path'),
+ childProcess = require('child_process'),
+ config = require('./../conf'),
+ binPath = require('phantomjs').path,
+ util = require('../util'),
+ fs = require('fs'),
+ log = require('winston'),
+ async = require('async');
+
+var data_path = path.join(config.run.absResultDir, config.dataDir);
+
+module.exports = {
+ analyze: function(urls, callback) {
+ fs.mkdirSync(path.join(data_path, 'screenshots'));
+
+ var queue = async.queue(analyzeUrl, config.threads);
+
+ var errors = {};
+ var pageData = {};
+ urls.forEach(function(u) {
+ queue.push({
+ "url": u
+ }, function(err) {
+ if (err) {
+ errors[u] = err;
+ }
+ });
+ });
+
+ queue.drain = function() {
+ callback(undefined, {
+ 'type': 'screenshots',
+ 'data': {},
+ 'errors': errors
+ });
+ };
+ }
+};
+
+function analyzeUrl(args, asyncDoneCallback) {
+ var url = args.url;
+
+ // PhantomJS arguments
+ var childArgs = ['--ssl-protocol=any', '--ignore-ssl-errors=yes'];
+
+ //
+ childArgs.push(path.join(__dirname, '..', 'screenshot.js'));
+
+ childArgs.push(url);
+ childArgs.push(path.join(data_path, 'screenshots', util.getUrlHash(url) +
+ '.png'));
+ childArgs.push(config.viewPort.split("x")[0]);
+ childArgs.push(config.viewPort.split("x")[1]);
+ childArgs.push(config.userAgent);
+ childArgs.push(true);
+
+ if (config.basicAuth)
+ childArgs.push(config.basicAuth);
+
+ log.log('info', "Taking screenshots for " + url);
+
+ childProcess.execFile(binPath, childArgs, {
+ timeout: 60000
+ }, function(err, stdout, stderr) {
+
+ if (stderr) {
+ log.log('error', 'stderr: Error getting screenshots ' + url + ' (' + stderr +
+ ')');
+ }
+
+ if (err) {
+ log.log('error', 'Error getting screenshots: ' + url + ' (' + stdout + stderr +
+ err + ')');
+ asyncDoneCallback(undefined, err + stdout);
+ } else {
+ asyncDoneCallback(undefined, err);
+ }
+ });
+}
diff --git a/lib/analyze/webpagetest.js b/lib/analyze/webpagetest.js
new file mode 100644
index 000000000..184db9ff0
--- /dev/null
+++ b/lib/analyze/webpagetest.js
@@ -0,0 +1,73 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+
+var path = require('path'),
+ config = require('./../conf'),
+ util = require('../util'),
+ fs = require('fs'),
+ WebPageTest = require('webpagetest'),
+ log = require('winston'),
+ async = require('async');
+
+var data_path = path.join(config.run.absResultDir, config.dataDir);
+
+module.exports = {
+ analyze: function(urls, callback) {
+ fs.mkdirSync(path.join(data_path, 'webpagetest'));
+
+ var queue = async.queue(analyzeUrl, 1);
+ var errors = {};
+ var pageData = {};
+
+ var wpt = ((config.webpagetestKey) ? new WebPageTest(config.webpagetestUrl,config.webpagetestKey):
+ new WebPageTest(config.webpagetestUrl));
+
+
+ urls.forEach(function(u) {
+ queue.push({
+ "url": u,"wpt": wpt
+ }, function(data, err) {
+ if (err) {
+ log.log('error', 'Error running WebPageTest: ' + err);
+ errors[u] = err;
+ } else
+ pageData[u] = data;
+ });
+ });
+
+ queue.drain = function() {
+ callback(undefined, {'type': 'webpagetest', 'data':pageData, 'errors': errors});
+ };
+ }
+};
+
+function analyzeUrl(args, asyncDoneCallback) {
+ var url = args.url;
+ var wpt = args.wpt;
+
+ log.log('info', 'Running WebPageTest ' + url);
+ wpt.runTest(url, config.webpagetest, function(err, data) {
+
+ console.log(err);
+
+
+ var jsonPath = path.join(config.run.absResultDir, config.dataDir,
+ 'webpagetest',
+ util.getUrlHash(url) + '-webpagetest.json');
+
+ fs.writeFile(jsonPath, JSON.stringify(data), function(err) {
+ if (err) {
+ log.log('error', "WebPageTest coudn't store file for url " + url + '(' +
+ err + ')');
+ asyncDoneCallback(undefined, err);
+
+ }
+ else asyncDoneCallback(data, undefined);
+ });
+
+ });
+}
diff --git a/lib/analyze/yslow.js b/lib/analyze/yslow.js
new file mode 100644
index 000000000..3da5afc06
--- /dev/null
+++ b/lib/analyze/yslow.js
@@ -0,0 +1,110 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var path = require('path'),
+ childProcess = require('child_process'),
+ config = require('./../conf'),
+ binPath = require('phantomjs').path,
+ util = require('../util'),
+ fs = require('fs'),
+ log = require('winston'),
+ async = require('async');
+
+var data_path = path.join(config.run.absResultDir, config.dataDir);
+
+module.exports = {
+ analyze: function(urls, callback) {
+ fs.mkdirSync(path.join(data_path, 'yslow'));
+
+ var queue = async.queue(analyzeUrl, config.threads);
+
+ var errors = {};
+ var pageData = {};
+ urls.forEach(function(u) {
+ queue.push({
+ "url": u
+ }, function(data, err) {
+ if (err) {
+ errors[u] = err;
+ } else pageData[u] = data;
+ });
+ });
+
+ queue.drain = function() {
+ callback(undefined, {'type': 'yslow', 'data':pageData, 'errors': errors});
+ };
+ }
+};
+
+function analyzeUrl(args, asyncDoneCallback) {
+ var url = args.url;
+
+ // PhantomJS arguments
+ var childArgs = ['--ssl-protocol=any', '--ignore-ssl-errors=yes'];
+
+ if (config.proxy)
+ childArgs.push('--proxy', config.urlProxyObject.host, '--proxy-type',
+ config.urlProxyObject.protocol);
+
+ // arguments to YSlow
+ childArgs.push(path.join(__dirname, '..', config.yslow), '-d', '-r', config.ruleSet,
+ '--ua', config.userAgent);
+
+ childArgs.push('-c', '1');
+
+ if (config.basicAuth)
+ childArgs.push('-ba', config.basicAuth);
+
+ if (config.cdns)
+ childArgs.push('--cdns ', config.cdns);
+
+ var resultsPath = path.join(data_path, 'yslow', util.getUrlHash(url) +
+ '-yslow.json');
+ childArgs.push('--file', resultsPath);
+
+ childArgs.push(url);
+
+ log.log('info', "Running YSlow for " + url);
+
+ childProcess.execFile(binPath, childArgs, {
+ timeout: 60000
+ }, function(err, stdout, stderr) {
+
+ if (stderr) {
+ log.log('error', 'stderr: Error running Yslow: ' + url + ' (' + stderr +
+ ')');
+ }
+
+ if (err) {
+ // YSlow writes console.err but ends up in stdout
+ log.log('error', 'Error running YSlow: ' + url + ' (' + stdout + stderr +
+ err + ')');
+ asyncDoneCallback(undefined, err + stdout);
+ } else {
+ var file = path.join(config.run.absResultDir, config.dataDir, 'yslow',
+ util.getUrlHash(url) + '-yslow.json');
+
+ fs.readFile(file, function(err, data) {
+ if (err) {
+ log.log('error', "Couldn't read the file:" + file);
+ asyncDoneCallback(undefined, err);
+ } else {
+ var yslow = JSON.parse(data);
+ yslow.originalUrl = url;
+ /**
+ Ok, here's one thing that we have not been able to fix,
+ for some reasons sometimes the component array is returned as a String.
+ Would be nice to understand why, this is just a quick fix.
+ */
+ if (!Array.isArray(yslow.comps)) {
+ yslow.comps = JSON.parse(yslow.comps);
+ }
+ asyncDoneCallback(yslow, err);
+ }
+ });
+ }
+ });
+}
diff --git a/lib/browsertime-0.7-SNAPSHOT-full.jar b/lib/browsertime-0.7-SNAPSHOT-full.jar
new file mode 100644
index 000000000..4bc1f8d11
Binary files /dev/null and b/lib/browsertime-0.7-SNAPSHOT-full.jar differ
diff --git a/lib/collector.js b/lib/collector.js
new file mode 100644
index 000000000..b1c7eb3fe
--- /dev/null
+++ b/lib/collector.js
@@ -0,0 +1,106 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var fs = require('fs-extra'),
+ path = require('path'),
+ config = require('./conf'),
+ log = require('winston'),
+ aggregators = [],
+ collectors = [];
+
+module.exports = Collector;
+
+function Collector() {
+ registerAggregators();
+ registerCollectors();
+}
+
+function registerAggregators() {
+
+ var types = [];
+
+ if (config.runYslow)
+ types.push('yslow');
+ if (config.browser)
+ types.push('browsertime','har');
+ if (config.googleKey)
+ types.push('gpsi');
+ if (config.webpagetest)
+ types.push('webpagetest');
+
+ types.forEach(function (type) {
+ var rootPath = path.join(__dirname, "aggregators",type,path.sep);
+ fs.readdirSync(rootPath).forEach(function(file) {
+ aggregators.push(require(rootPath + file));
+ });
+ });
+
+ if (config.aggregators) {
+ fs.readdirSync(config.aggregators).forEach(function(file) {
+ aggregators.push(require(config.aggregators + file));
+ });
+ }
+}
+
+function registerCollectors() {
+ var rootPath = path.join(__dirname, "collectors", path.sep);
+ fs.readdirSync(rootPath).forEach(function(file) {
+ collectors.push(require(rootPath + file));
+ });
+ if (config.collectors) {
+ fs.readdirSync(config.collectors).forEach(function(file) {
+ collectors.push(require(config.collectors + file));
+ });
+ }
+}
+
+Collector.prototype.createAggregates = function() {
+ var aggregates = [];
+ aggregators.forEach(function(a) {
+ // if one of the values fails, we want to log & move on
+ try {
+ var result = a.generateResults();
+ if (Array.isArray(result)) {
+ result.forEach(function(b) {
+ aggregates.push(b);
+ });
+ } else
+ aggregates.push(result);
+ }
+ catch (err) {
+ log.log('error', 'Could not fetch data for aggregator:' + a.id + ' err:' + err);
+ }
+
+ });
+ return aggregates;
+};
+
+Collector.prototype.createCollections = function() {
+ var collections = {};
+
+ collectors.forEach(function(c) {
+ var collection = c.generateResults();
+ collections[collection.id] = collection.list;
+ });
+ return collections;
+};
+
+Collector.prototype.collectPageData = function(pageData) {
+ aggregators.forEach(function(a) {
+ try {
+ a.processPage(pageData);
+
+ } catch (err) {
+ log.log('error', 'Could not fetch data for aggregator:' + a.id + ' err:' +
+ err);
+ }
+ });
+
+ collectors.forEach(function(c) {
+ c.processPage(pageData);
+ });
+
+};
diff --git a/lib/collectors/assets.js b/lib/collectors/assets.js
new file mode 100644
index 000000000..a43582a6c
--- /dev/null
+++ b/lib/collectors/assets.js
@@ -0,0 +1,45 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+
+var util = require('../util');
+var assets = {};
+
+exports.processPage = function(pageData) {
+ if (pageData.yslow) {
+ pageData.yslow.comps.forEach(function(comp) {
+ if (comp.type != "doc") {
+ var url = comp.url;
+ var asset = assets[url];
+
+ if (asset) {
+ asset.count++;
+ } else {
+ assets[url] = {
+ url: url,
+ type: comp.type,
+ timeSinceLastModification: util.getTimeSinceLastMod(comp),
+ cacheTime: util.getCacheTime(comp),
+ size: comp.size,
+ count: 1,
+ headers: comp.headers
+ };
+ }
+ }
+ });
+ }
+};
+
+exports.generateResults = function() {
+ var values = Object.keys(assets).map(function (key) {
+ return assets[key];
+ });
+
+ return {
+ id: "assets",
+ list: values
+ };
+};
diff --git a/lib/collectors/pages.js b/lib/collectors/pages.js
new file mode 100644
index 000000000..e4f91ba78
--- /dev/null
+++ b/lib/collectors/pages.js
@@ -0,0 +1,210 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var util = require('../util');
+var pages = [];
+var isDoc = function(comp) {
+ return (comp.type == "doc");
+};
+
+exports.processPage = function(pageData) {
+
+ var p = {};
+
+ if (pageData.yslow) {
+ p = collectYSlowMetrics(pageData, p);
+ collectYSlowRules(pageData, p);
+ }
+
+ if (pageData.gpsi)
+ collectGPSI(pageData, p);
+
+ if (pageData.browsertime)
+ collectBrowserTime(pageData, p);
+
+ p.url = util.getURLFromPageData(pageData);
+
+ // TODO fix a cleaner check for this
+ // if an analyzed failed, skip it
+ if (p.url !== 'undefined') {
+ pages.push(p);
+ }
+
+};
+
+function collectYSlowMetrics(pageData, p) {
+
+ var docs = pageData.yslow.comps.filter(isDoc);
+
+ docs.forEach(function(doc) {
+ p = {
+ score: pageData.yslow.o,
+ // strip to only store response headers to save space?
+ headers: doc.headers,
+ yslow: {
+ requests: {
+ 'v': pageData.yslow.comps.length,
+ 'unit': ''
+ },
+ requestsMissingExpire: {
+ 'v': pageData.yslow.comps.filter(function(c) {
+ return util.getCacheTime(c) === 0;
+ }).length,
+ 'unit': ''
+ },
+ timeSinceLastModification: {
+ 'v': util.getTimeSinceLastMod(doc),
+ 'unit': 'seconds'
+ },
+ cacheTime: {
+ 'v': util.getCacheTime(doc),
+ 'unit': 'seconds'
+ },
+ docSize: {
+ 'v': doc.size,
+ 'unit': 'bytes'
+ },
+ pageSize: {
+ 'v': util.getSize(pageData.yslow.comps),
+ 'unit': 'bytes'
+ },
+ assets: {
+ js: {
+ 'v': pageData.yslow.comps.filter(function(c) {
+ return c.type === 'js';
+ }).length,
+ 'unit': ''
+ },
+ css: {
+ 'v': pageData.yslow.comps.filter(function(c) {
+ return c.type === 'css';
+ }).length,
+ 'unit': ''
+ },
+ img: {
+ 'v': pageData.yslow.comps.filter(function(c) {
+ return c.type === 'image';
+ }).length,
+ 'unit': ''
+ },
+ cssimg: {
+ 'v': pageData.yslow.comps.filter(function(c) {
+ return c.type === 'cssimage';
+ }).length,
+ 'unit': ''
+ },
+ font: {
+ 'v': pageData.yslow.comps.filter(function(c) {
+ return c.type === 'font';
+ }).length,
+ 'unit': ''
+ },
+ flash: {
+ 'v': pageData.yslow.comps.filter(function(c) {
+ return c.type === 'flash';
+ }).length,
+ 'unit': ''
+ },
+ iframe: {
+ 'v': pageData.yslow.comps.filter(function(c) {
+ return c.type === 'iframe';
+ }).length,
+ 'unit': ''
+ },
+ jsSize: {
+ 'v': util.getSize(pageData.yslow.comps.filter(function(c) {
+ return c.type === 'js';
+ })),
+ 'unit': 'bytes'
+ },
+ cssSize: {
+ 'v': util.getSize(pageData.yslow.comps.filter(function(c) {
+ return c.type === 'css';
+ })),
+ 'unit': 'bytes'
+ },
+ imgSize: {
+ 'v': util.getSize(pageData.yslow.comps.filter(function(c) {
+ return c.type === 'img';
+ })),
+ 'unit': 'bytes'
+ },
+ fontSize: {
+ 'v': util.getSize(pageData.yslow.comps.filter(function(c) {
+ return c.type === 'font';
+ })),
+ 'unit': 'bytes'
+ },
+ flashSize: {
+ 'v': util.getSize(pageData.yslow.comps.filter(function(c) {
+ return c.type === 'flash';
+ })),
+ 'unit': 'bytes'
+ }
+ }
+ }
+ };
+
+ });
+ return p;
+}
+
+
+function collectYSlowRules(pageData, p) {
+ p.rules = {};
+ // add all rule scores as fields
+ Object.keys(pageData.yslow.g).forEach(function(rule) {
+ p.rules[rule] = {
+ 'v': pageData.yslow.g[rule].score,
+ 'unit': ''
+ };
+ // TODO how should we name them
+ p.rules[rule].items = {
+ 'v': pageData.yslow.g[rule].components.length,
+ 'unit': ''
+ };
+ });
+}
+
+function collectGPSI(pageData, p) {
+ p.gpsi = {};
+ p.gpsi.gscore = {
+ 'v': pageData.gpsi.score,
+ 'unit': ''
+ };
+}
+
+function collectBrowserTime(pageData, p) {
+ p.timings = {};
+
+ var timingsWeWillPush = ['min', 'mean', 'median', 'p90', 'p99', 'max'];
+
+ pageData.browsertime.forEach(function(run) {
+ var browser = run.pageData.browserName;
+ p.timings[browser] = {};
+ run.statistics.forEach(function(stats) {
+ p.timings[browser][stats.name] = {};
+ p.timings[stats.name] = {};
+ timingsWeWillPush.forEach(function(number) {
+ p.timings[browser][stats.name][number] = {
+ 'v': stats[number],
+ 'unit': 'milliseconds'
+ };
+ p.timings[stats.name][number] = {
+ 'v': stats[number],
+ 'unit': 'milliseconds'
+ };
+ });
+ });
+ });
+}
+
+exports.generateResults = function() {
+ return {
+ id: "pages",
+ list: pages
+ };
+};
diff --git a/lib/conf.js b/lib/conf.js
new file mode 100644
index 000000000..0bc7f0e05
--- /dev/null
+++ b/lib/conf.js
@@ -0,0 +1,369 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var dateFormat = require('dateformat'),
+urlParser = require('url'),
+fs = require('fs-extra'),
+path = require('path'),
+supportedBrowsers = ['chrome','ie','firefox'],
+config = require("nomnom").options({
+ url: {
+ abbr: 'u',
+ metavar: '',
+ help: 'The start url'
+ },
+ file: {
+ abbr: 'f',
+ metavar: '',
+ help: 'The path to a plain text file with one URL on each row. Each line must end with a new line in the file.',
+ callback: function(file) {
+ if (!fs.existsSync(file))
+ return "Couldn't find the file:" + file;
+ }
+ },
+ version: {
+ flag: true,
+ abbr: 'v',
+ help: 'Display the sitespeed.io version',
+ callback: function() {
+ return require("../package.json").version;
+ }
+ },
+ deep: {
+ abbr: 'd',
+ metavar: '',
+ default: 1,
+ help: 'How deep to crawl.',
+ callback: function(deep) {
+ if (deep != parseInt(deep))
+ return "You must specify an integer";
+ }
+ },
+ containInPath: {
+ abbr: 'c',
+ metavar: '',
+ help: 'Only crawl URLs that contains this in the path'
+ },
+ skip: {
+ abbr: 's',
+ metavar: '',
+ help: 'Do not crawl pages that contains this in the path'
+ },
+ threads: {
+ abbr: 't',
+ metavar: '',
+ default: 5,
+ help: 'The number of that will analyze pages.',
+ callback: function(threads) {
+ if (threads != parseInt(threads))
+ return "You must specify an integer";
+ else if (parseInt(threads) <= 0)
+ return "You must specify a positive integer";
+ }
+ },
+ name: {
+ metavar: '',
+ help: 'Give your test a name, it will be added to all HTML pages'
+ },
+ memory: {
+ metavar: '',
+ default: 1024,
+ help: 'We still use Java for a couple of things and you can configure how much memory that the process will have (in mb).'
+ },
+ resultBaseDir: {
+ abbr: 'r',
+ metavar: '',
+ default: 'sitespeed-result',
+ help: 'The result base directory',
+ callback: function(file) {
+ if (!fs.existsSync(file))
+ return "Couldn't find the basedir:" + file;
+ }
+ },
+ userAgent: {
+ metavar: '',
+ default: "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",
+ help: 'The full User Agent string, default is Chrome for MacOSX. You can also set the value as iphone or ipad (will automagically change the viewport)'
+ },
+ viewPort: {
+ metavar: '',
+ default: '1280x800',
+ help: 'The view port, the page viewport size WidthxHeight, like 400x300.'
+ },
+ yslow: {
+ abbr: 'y',
+ metavar: '',
+ default: 'yslow-3.1.8-sitespeed.js',
+ help: 'The compiled YSlow file.',
+ callback: function(file) {
+ if (!fs.existsSync(file))
+ return "Couldn't find the file:" + fs.realpathSync(file);
+ }
+ },
+ ruleSet: {
+ metavar: '',
+ default: 'sitespeed.io-desktop',
+ help: 'Which ruleset to use.'
+ },
+ limitFile: {
+ metavar: '',
+ default: '../conf/desktop-rules.json',
+ help: 'The path to the limit configuration file'
+ },
+ basicAuth: {
+ metavar: '',
+ help: 'Basic auth user & password'
+ },
+ browser: {
+ abbr: 'b',
+ metavar: '',
+ help: 'Choose which browser to use to collect timing data. You can set multiple browsers in a comma separated list (firefox|chrome|ie)',
+ callback: function(browsers) {
+ b = browsers.split(","),
+ invalidBrowsers = b.filter(function(browser) {
+ return supportedBrowsers.indexOf(browser) < 0;
+ });
+
+ if (invalidBrowsers.length > 0)
+ return "You specified a browser that is not supported:" + invalidBrowsers;
+ }
+ },
+ profile: {
+ metavar: '',
+ choices: ['desktop','mobile'],
+ default: 'desktop',
+ help: 'Choose between testing for desktop or mobile. Testing for desktop will use desktop rules & user agents, and vice verca'
+ },
+ no: {
+ abbr: 'n',
+ metavar: '',
+ default: 3,
+ help: 'The number of times you should test each URL when fetching timing metrics. Default is three times',
+ callback: function(n) {
+ if (n != parseInt(n))
+ return "You must specify an integer";
+ else if (parseInt(n) <= 0)
+ return "You must specify a positive integer";
+ }
+ },
+ screenshot: {
+ flag: true,
+ help: 'Take screenshots for each page (using the configured view port).'
+ },
+ junit: {
+ flag: true,
+ help: 'Create JUnit output'
+ },
+ skipTest: {
+ metavar: '',
+ help: 'A comma separeted list of rules to skip when generating JUnit'
+ },
+ threshold: {
+ default: 90,
+ metavar: '[0-100]',
+ help: "Threshold score for tests, will be used of no mathing thresholdFile with values match. Use : --threshold 95"
+ },
+ thresholdFile: {
+ metavar: '',
+ help: "A file containing JSON like {\"overall\": 90, \"thirdpartyversions\": 85}"
+ },
+ timingsThresholdFile: {
+ metavar: '',
+ help: "A file containing JSON like ..."
+ },
+ csv: {
+ flag: true,
+ help: 'Also output CSV where applicable'
+ },
+ maxPagesToTest: {
+ abbr: 'm',
+ metavar: '',
+ help: 'The max number of pages to test. Default is no limit'
+ },
+ proxy: {
+ abbr: 'p',
+ metavar: '',
+ help: 'http://proxy.soulgalore.com:80'
+ },
+ cdns: {
+ metavar: '',
+ list: true,
+ help: 'A comma separated list of additional CDNs'
+ },
+ boxes: {
+ metavar: '',
+ list: true,
+ help: 'The boxes showed on site summary page, see http://www.sitespeed.io/documentation/#config-boxes for more info'
+ },
+ columns: {
+ abbr: 'c',
+ metavar: '',
+ list: true,
+ help: 'The columns showed on detailed page summary table, see http://www.sitespeed.io/documentation/#config-columns for more info'
+ },
+ configFile: {
+ metavar: '',
+ help: 'The path to a sitespeed.io config.json file, if it exists all other input parameters will be overidden'
+ },
+ // TODO How to override existing
+ aggregators: {
+ metavar: '',
+ help: 'The path to a directory with extra aggregators, see YYY'
+ },
+ // TODO maybe collectors are overkill
+ collectors: {
+ metavar: '',
+ help: 'The path to a directory with extra collectors, see YYY'
+ },
+ graphiteHost: {
+ metavar: '',
+ help: 'The Graphite host'
+ },
+ graphitePort: {
+ metavar: '',
+ default: 2003,
+ help: 'Graphite port'
+ },
+ graphiteNamespace: {
+ metavar: '',
+ default: 'sitespeed.io',
+ help: 'The namespace of the data sent to Graphite'
+ },
+ graphiteData: {
+ default: 'all',
+ help: 'Choose which data to send to Graphite by a comma separated list. Default all data is sent. [summary,rules,pagemetrics,timings]'
+ },
+ googleKey: {
+ help: 'Your Google Key, configure it to also fetch data from Google Page Speed Insights'
+ },
+ noYslow: {
+ flag: true,
+ help: 'Choose to run YSlow or not',
+ },
+ webpagetest: {
+ metavar: '',
+ help: 'WebPageTest configuration file, see ...',
+ callback: function(file) {
+ if (!fs.existsSync(file))
+ return "Couldn't find the file:" + fs.realpathSync(file);
+ },
+ webpagetestUrl: {
+ metavar: '',
+ help: 'The URL to your private webpagetest instance'
+ },
+ webpagetestKey: {
+ metavar: '',
+ help: 'The API key if running on the public instances'
+}
+}
+}).parse();
+
+if (config.webpagetest)
+ config.webpagetest = JSON.parse(fs.readFileSync(config.webpagetest));
+
+if ((!config.url) && (!config.file)) {
+ console.log('You must specify either a URL to test or a file with URL:s');
+ process.exit(1);
+}
+
+// If we supply a configuration file, then it will ovveride all current conf, meaning
+// we can easily use the same conf over and over again
+if (config.configFile) {
+ config = JSON.parse(fs.readFileSync(config.configFile));
+}
+
+config.runYslow = config.noYslow?false:true;
+
+// The run always has a fresh date
+config.run = {};
+config.run.date = new Date();
+
+// Setup the absolute result dir
+if (config.url) {
+ config.urlObject = urlParser.parse(config.url);
+ config.run.absResultDir = path.join(__dirname, '../',config.resultBaseDir, config.urlObject.hostname, dateFormat(config.date, "yyyy-mm-dd-HH-MM-ss") );
+} else if (config.file) {
+ // TODO handle the file name in a good way if it contains chars that will not fit in a dir
+ config.run.absResultDir = path.join(__dirname, '../',config.resultBaseDir, config.file, dateFormat(config.date, "yyyy-mm-dd-HH-MM-ss") );
+}
+
+// Parse the proxy info as a real URL
+if (config.proxy) {
+ config.urlProxyObject = urlParser.parse(config.proxy);
+}
+
+if (config.thresholdFile)
+ config.thresholds = require(config.thresholdFile);
+
+if (config.timingsThresholdFile)
+ config.timingThresholds = require(timingsThresholdFile);
+else
+ config.timingThresholds = require('../conf/junit-timings.json');
+
+// decide which rules to use ...
+if (config.profile === 'mobile') {
+ config.rules= require('../conf/mobile-rules.json');
+ config.ruleSet = "sitespeed.io-mobile";
+ config.userAgent = "Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25";
+ config.viewPort = "320x444";
+}
+else {
+ config.rules= require(config.limitFile);
+ // The rest use the default/configured one, will that work? :)
+}
+
+config.summaryBoxes = ['ruleScore'];
+
+if (config.googleKey)
+ config.summaryBoxes.push('gpsi.score');
+
+config.summaryBoxes.push('criticalPathScore', 'jsSyncInHead',
+ 'jsPerPage', 'cssPerPage', 'cssImagesPerPage', 'fontsPerPage',
+ 'imagesPerPage', 'requests', 'requestsWithoutExpires', 'requestsWithoutGzip',
+ 'docWeight', 'jsWeightPerPage', 'cssWeightPerPage', 'imageWeightPerPage',
+ 'pageWeight', 'browserScaledImages', 'spofPerPage', 'numberOfDomains',
+ 'singleDomainRequests', 'redirectsPerPage', 'cacheTime', 'timeSinceLastMod');
+
+if (config.webpagetest)
+ config.summaryBoxes.push('wpt.imageSavings','wpt.speedIndex','wpt.visualComplete');
+
+if (config.browser)Â {
+ config.summaryBoxes.push('serverResponseTime','backEndTime','frontEndTime','domContentLoadedTime','pageLoadTime','firstPaintTime');
+}
+
+// TODO add the default one when you run a BrowserTime
+
+if (config.boxes) {
+ if (config.boxes[0].indexOf('+')===0) {
+ config.boxes[0].split(',').forEach(function (box) {
+ if (box.indexOf('+')===0)
+ config.summaryBoxes.push(box.substring(1));
+ else
+ config.summaryBoxes.push(box);
+ });
+ }
+ else
+ config.summaryBoxes = config.boxes[0].split(',');
+}
+
+if (config.runYslow)
+ config.pageColumns = ['yslow.assets.js','yslow.assets.css','yslow.assets.img','yslow.requests','rules.expiresmod.items','yslow.pageSize', 'rules.avoidscalingimages.items','rules.criticalpath'];
+
+if (config.browser)
+ config.pageColumns.push('timings.serverResponseTime.median','timings.domContentLoadedTime.median');
+
+if (config.googleKey)
+ config.pageColumns.push('gpsi.gscore');
+
+if (config.columns) {
+ config.pageColumns = config.columns[0].split(',');
+}
+
+config.dataDir = 'data';
+
+config.supportedBrowsers = supportedBrowsers;
+
+module.exports = config;
diff --git a/lib/crawler-1.5.14-SNAPSHOT-full.jar b/lib/crawler-1.5.14-SNAPSHOT-full.jar
new file mode 100644
index 000000000..5c8b9993d
Binary files /dev/null and b/lib/crawler-1.5.14-SNAPSHOT-full.jar differ
diff --git a/lib/crawler.js b/lib/crawler.js
new file mode 100644
index 000000000..22bdc3f8b
--- /dev/null
+++ b/lib/crawler.js
@@ -0,0 +1,83 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var spawn = require('child_process').spawn,
+ path = require('path'),
+ config = require('./conf'),
+ urlParser = require('url'),
+ log = require('winston'),
+ fs = require('fs');
+
+module.exports.crawl = function(url, callback) {
+
+ var args = [
+ '-Xmx' + config.memory + 'm',
+ '-Xms' + config.memory + 'm'
+ ];
+
+ var urlFile = path.join(config.run.absResultDir, 'urls.txt');
+ var errorUrlFile = path.join(config.run.absResultDir, 'errorurls.txt');
+
+ if (config.basicAuth) {
+ pUrl = urlParser.parse(url);
+ args.push('-Dcom.soulgalore.crawler.auth=' + pUrl.hostname +':' + (pUrl.port||80) + ':' + config.basicAuth);
+ }
+ if (config.proxy) {
+ pUrl = urlParser.parse(config.proxy);
+ args.push('-Dcom.soulgalore.crawler.proxy=' + pUrl.protocol + pUrl.host);
+ }
+ args.push('-cp',
+ path.join(__dirname, "crawler-1.5.14-SNAPSHOT-full.jar"),
+ 'com.soulgalore.crawler.run.CrawlToFile',
+ '-u',
+ config.url,
+ '-l',
+ config.deep,
+ '-f',
+ urlFile,
+ '-ef',
+ errorUrlFile,
+ '-rh',
+ 'User-Agent:' + config.userAgent);
+
+ if (config.containInPath)
+ args.push('-p', config.containInPath);
+
+
+ if (config.skip)
+ args.push('-np', config.skip);
+
+ var crawl = spawn('java', args);
+
+ crawl.stdout.on('data', function(data) {
+ log.log('info', 'Output from the crawl:' + data);
+ });
+
+ crawl.stderr.on('data', function(data) {
+ log.log('error', 'Error from the crawl:' + data);
+ });
+
+ crawl.on('close', function(code) {
+ // console.log('child process exited with code ' + code);
+ var okUrls = fs.readFileSync(urlFile).toString().split("\n");
+ okUrls.pop();
+ // TODO handle URL & error type
+ var errorUrls = {};
+
+ if (fs.existsSync(errorUrlFile)) {
+ var lines = fs.readFileSync(errorUrlFile).toString().split("\n");
+ lines.forEach(function (line) {
+ // skip empty lines
+ if (line) {
+ var urlAndReason = line.split(',');
+ errorUrls[urlAndReason[1]] = urlAndReason[0];
+ }
+ });
+ }
+ callback(okUrls, errorUrls);
+ });
+
+};
diff --git a/lib/graphite.js b/lib/graphite.js
new file mode 100644
index 000000000..235df1dcd
--- /dev/null
+++ b/lib/graphite.js
@@ -0,0 +1,130 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var util = require('./util'),
+ config = require('./conf'),
+ net = require('net');
+
+module.exports = Graphite;
+
+function Graphite(host, port, namespace, collector) {
+ this.host = host;
+ this.port = port;
+ this.namespace = namespace;
+ this.collector = collector;
+}
+
+Graphite.prototype.sendPageData = function(aggregates,pages) {
+
+ var statistics = '';
+
+ var timeStamp = ' ' + Math.round(new Date().getTime() / 1000) + "\n";
+
+ var namespace = this.namespace;
+ pages.forEach(function(page) {
+ statistics += getPageStats(page, namespace, timeStamp);
+ });
+
+ if (config.graphiteData.indexOf('summary') > -1 || config.graphiteData.indexOf(
+ 'all') > -1) {
+ statistics += getSummaryStats(aggregates, namespace, timeStamp);
+ }
+
+ var server = net.createConnection(this.port, this.host);
+ server.addListener('error', function(connectionException) {
+ console.log("Couldn't send data to Graphite:" + connectionException);
+ });
+
+ server.on('connect', function() {
+ console.log("Sending to Grahite ...");
+ this.write(statistics);
+ this.end();
+ console.log("Data sent to Graphite");
+ });
+
+};
+
+function getSummaryStats(aggregates, namespace, timeStamp) {
+ var statistics = '';
+ var values = ['min', 'p10', 'median', 'mean', 'p90', 'p99', 'max'];
+ aggregates.forEach(function(aggregate) {
+ values.forEach(function(value) {
+ statistics += namespace + '.summary.' + aggregate.id + '.' + value +
+ ' ' +
+ aggregate.stats[value] + timeStamp;
+ });
+ });
+ return statistics;
+}
+
+function getPageStats(page, namespace, timeStamp) {
+
+ var statistics = '';
+ // timings
+ var timingsWeWillPush = ['min', 'median', 'p90', 'max'];
+ var urlKey = util.getGraphiteURLKey(decodeURIComponent(page.url));
+
+ // Get all rule data
+ if (config.graphiteData.indexOf('rules') > -1 || config.graphiteData.indexOf(
+ 'all') > -1) {
+ Object.keys(page.rules).forEach(function(rule) {
+ statistics += namespace + '.' + urlKey + 'rules.' + rule + ' ' +
+ page.rules[rule].v + timeStamp;
+ });
+ }
+
+ // the timings that are not browser specific
+ if (config.graphiteData.indexOf('timings') > -1 || config.graphiteData.indexOf(
+ 'all') > -1) {
+ Object.keys(page.timings).forEach(function(timing) {
+ timingsWeWillPush.forEach(function(val) {
+ // is it a browser?
+ if (config.supportedBrowsers.indexOf(timing) < 0)
+ statistics += namespace + '.' + urlKey + 'timings.' + timing +
+ '.' + val + ' ' + page.timings[timing][val].v + timeStamp;
+ });
+ });
+
+
+ // and the browsers
+ Object.keys(page.timings).forEach(function(browser) {
+ if (config.supportedBrowsers.indexOf(browser) > -1) {
+ Object.keys(page.timings[browser]).forEach(function(timing) {
+ timingsWeWillPush.forEach(function(val) {
+ statistics += namespace + '.' + urlKey + 'timings.' +
+ browser + '.' + timing + '.' + val + ' ' + page.timings[
+ browser][timing][val].v + timeStamp;
+ });
+ });
+ }
+ });
+ }
+
+
+ if (config.graphiteData.indexOf('pagemetrics') > -1 || config.graphiteData.indexOf(
+ 'all') > -1) {
+ // and all the assets
+ Object.keys(page.yslow.assets).forEach(function(asset) {
+ statistics += namespace + '.' + urlKey + 'assets.' + asset +
+ ' ' + page.yslow.assets[asset].v + timeStamp;
+ });
+
+ // and page specific
+ statistics += namespace + '.' + urlKey + 'score' + ' ' + page.score +
+ timeStamp;
+ statistics += namespace + '.' + urlKey + 'requests' + ' ' + page.yslow
+ .requests.v + timeStamp;
+ statistics += namespace + '.' + urlKey + 'requestsMissingExpire' +
+ ' ' + page.yslow.requestsMissingExpire.v + timeStamp;
+ statistics += namespace + '.' + urlKey +
+ 'timeSinceLastModification' + ' ' + page.yslow.timeSinceLastModification
+ .v + timeStamp;
+ statistics += namespace + '.' + urlKey + 'cacheTime' + ' ' + page.yslow
+ .cacheTime.v + timeStamp;
+ }
+ return statistics;
+
+}
diff --git a/lib/hb-helpers.js b/lib/hb-helpers.js
new file mode 100644
index 000000000..4ded7be7e
--- /dev/null
+++ b/lib/hb-helpers.js
@@ -0,0 +1,215 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var hb = require('handlebars'),
+util = require('./util');
+
+module.exports.registerHelpers = function registerHelpers() {
+
+ hb.registerHelper('getHumanReadable', function(box, value, showUnit) {
+ return util.getHumanReadable(box,value, showUnit);
+ });
+
+ hb.registerHelper('getKbSize', function(value) {
+ return util.getKbSize(value);
+ });
+
+ hb.registerHelper('getCacheTime', function(comp) {
+ return util.getCacheTime(comp);
+ });
+
+ hb.registerHelper('getTimeSinceLastMod', function(comp) {
+ return util.getTimeSinceLastMod(comp);
+ });
+
+ hb.registerHelper('getPrettyTimeSinceLastMod', function(comp) {
+ return util.prettyPrintSeconds(util.getTimeSinceLastMod(comp));
+ });
+
+ hb.registerHelper('getPrettyCacheTime', function(comp) {
+ return util.prettyPrintSeconds(util.getCacheTime(comp));
+ });
+
+ hb.registerHelper('getPrettyPrintSeconds', function(value) {
+ return util.prettyPrintSeconds(value);
+ });
+
+ hb.registerHelper('getRuleColor', function(rule, value) {
+ return util.getRuleColor(rule, value);
+ });
+
+ hb.registerHelper('decodeURIComponent', function(url) {
+ return util.decodeURIComponent(url);
+ });
+
+ hb.registerHelper('escapeExpression', function(text) {
+ return hb.Utils.escapeExpression(text);
+ });
+
+ hb.registerHelper('getUrlHash', function(url) {
+ return util.getUrlHash(decodeURIComponent(url));
+ });
+
+ hb.registerHelper('getPercentage', function(value, total, decimals) {
+ return ((value/total)*100).toFixed(decimals);
+ });
+
+ hb.registerHelper('getPrettySizeForDomain', function(domain, components) {
+ return util.getKbSize(util.getSizeForDomain(domain,components));
+ });
+
+
+ hb.registerHelper('isLowerThan', function(value, limit, options) {
+ if (value < limit)
+ return options.fn(this);
+ });
+
+ hb.registerHelper('getDecimals', function(value, decimals) {
+ return Number(value).toFixed(decimals);
+ });
+
+ hb.registerHelper('getPageColumnValue', function(column, page) {
+ return util.getHumanReadable(util.select(page,column,''), util.select(page,column+'.v',0));
+ });
+
+ hb.registerHelper('getMatchingRuleName', function(id, rules) {
+ if (rules.hasOwnProperty(id))
+ return rules[id].name;
+ else return id;
+ });
+
+ hb.registerHelper('getPlural', function(value) {
+ if (value>1)
+ return 's';
+ return '';
+ });
+
+ hb.registerHelper('formatGPSIResult', function(urlBlock) {
+ var result = '';
+
+ var format = urlBlock.header.format;
+ var args = urlBlock.header.args||[];
+ var urls = urlBlock.urls||[];
+
+ result += util.gpsiReplacer(args,format);
+
+ result += '';
+ urls.forEach(function (url) {
+ result += '' + util.gpsiReplacer(url.result.args,url.result.format) +' ';
+ });
+ result += ' ';
+
+ return result;
+ });
+
+ hb.registerHelper('getErrorHTML', function(errorObject) {
+ var html = '';
+ var urls = Object.keys(errorObject);
+ urls.forEach(function(url) {
+ html += '' + url + ' reason ' + errorObject[url] + '
';
+ });
+ return html;
+ });
+
+ hb.registerHelper('getWPTWaterFall', function(run, whichView) {
+ if (Array.isArray(run))
+ return run[0][whichView].images.waterfall;
+ else
+ return run[whichView].images.waterfall;
+ });
+
+
+ hb.registerHelper('getColumnsMeta', function(column, columnsMeta, ruleDictionary, type) {
+
+ // strip
+ if (column.indexOf('rules.')===0)
+ column = column.replace('rules.','');
+ else if (column.indexOf('yslow.assets.')===0)
+ column = column.replace('yslow.assets.','');
+ else if (column.indexOf('yslow.')===0)
+ column = column.replace('yslow.','');
+ else if (column.indexOf('timings.')===0)
+ column = column.replace('timings.','');
+ else if (column.indexOf('gpsi.')===0)
+ column = column.replace('gpsi.','');
+ else if (column.indexOf('wpt.')===0)
+ column = column.replace('wpt.','');
+
+ // If we have matching in the columns meta data use it
+ if (columnsMeta.hasOwnProperty(column))
+ return columnsMeta[column][type];
+ // else it is a rule or BT data, if it's a rule, use the rule title as description
+ else if (ruleDictionary.hasOwnProperty(column) && type === 'desc')
+ return ruleDictionary[column].name;
+ // if we display number of components returned for a rule
+ else if (column.indexOf('.items')>0) {
+ // display short version in title
+ if (type==='title')
+ return column.replace('.items','') + " components";
+ // and long in description
+ else
+ // TODO we need to change the impl of how the ruleDictionary is implemented,
+ // now one page needs to be run before it will work
+ /*
+ if (ruleDictionary[column.replace('-items','')].hasOwnProperty('name'))
+ return ruleDictionary[column.replace('-items','')].name;
+ else console.log("Column:" + column + " " + ruleDictionary[column.replace('-items','')] );
+ */
+ return column;
+ }
+ else if (ruleDictionary.hasOwnProperty(column))
+ return column + " score";
+ else return column.replace('.',' ');
+ });
+
+ /*
+ * Bootstrap has in a way a complicated handling of defining how many boxes
+ * that will be on each row. This function helps us to know if it is time
+ * to output the div for a new row.
+ */
+ hb.registerHelper('bootstrapIsNewRow', function(index, perRow, options) {
+ if(index % perRow === 0) {
+ return options.fn(this);
+ }
+ });
+
+ /*
+ * Bootstrap has in a way a complicated handling of defining how many boxes
+ * that will be on each row. This function helps us to know if it is time
+ * to output the div for ending a row.
+ */
+ hb.registerHelper('bootstrapIsEndRow', function(index, size, perRow, options) {
+ if((index+1) % perRow === 0) {
+ return options.fn(this);
+ }
+
+ // Taking care of the case for the last row
+ else if(size - (index+1) === 0) {
+ return options.fn(this);
+ }
+
+ });
+
+ /*
+ * Bootstrap has in a way a complicated handling of defining how many boxes
+ * that will be on each row. This function helps us to know how many boxes that
+ * will be on this row (or e.g defining how much we will span).
+ */
+ hb.registerHelper('getBootstrapSpan', function(index, perRow, size) {
+
+ var itemsLastRow = size % perRow;
+
+ // TODO make this generic, now it's hardcoded to 3 per row
+ if(size - index <= itemsLastRow) {
+ if (itemsLastRow === 2)
+ return 6;
+ else
+ return 12;
+ }
+ return 4;
+
+ });
+};
diff --git a/lib/htmlRenderer.js b/lib/htmlRenderer.js
new file mode 100644
index 000000000..36bf93adb
--- /dev/null
+++ b/lib/htmlRenderer.js
@@ -0,0 +1,217 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var hb = require('handlebars'),
+ fs = require('fs-extra'),
+ path = require('path'),
+ log = require('winston'),
+ config = require('./conf'),
+ helpers = require('./hb-helpers');
+ columnsMetaData = require('../conf/columnsMetaData.json');
+ util = require('./util');
+
+module.exports = HTMLRenderer;
+
+var compiledTemplates = {},
+ compiledPartials = {},
+ ruleDictionary = {};
+
+function HTMLRenderer() {
+ this.numberOfAnalyzedPages = 0;
+ precompileTemplates();
+ helpers.registerHelpers();
+ copyAssets();
+}
+
+function copyAssets() {
+ fs.copy(path.join(__dirname, '../assets/'), config.run.absResultDir, function (err) {
+ if (err) throw err;
+ });
+}
+
+HTMLRenderer.prototype.renderPage = function (url, pageData) {
+ this.numberOfAnalyzedPages++;
+
+ var renderData = {};
+ if (pageData.yslow) {
+ renderData = {
+ "url": pageData.yslow.originalUrl,
+ "score": pageData.yslow.o,
+ "size": util.getSize(pageData.yslow.comps),
+ "rules": pageData.yslow.g,
+ "assets": pageData.yslow.comps,
+ "noOfDomains": util.getNumberOfDomains(pageData.yslow.comps),
+ "timeSinceLastModificationStats": util.getLastModStats(pageData.yslow.comps),
+ "cacheTimeStats": util.getCacheTimeStats(pageData.yslow.comps),
+ "noOfAssetsThatIsCached": (pageData.yslow.comps.length - pageData.yslow.g.expiresmod.components.length),
+ "assetsPerDomain": util.getAssetsPerDomain(pageData.yslow.comps),
+ "assetsPerContentType": util.getAssetsPerContentType(pageData.yslow.comps),
+ "sizePerContentType": util.getAssetsSizePerContentType(pageData.yslow.comps),
+ "ruleDictionary": pageData.yslow.dictionary.rules,
+ };
+ }
+ else {
+ renderData.url = util.getURLFromPageData(pageData);
+ }
+
+ renderData.gpsiData = pageData.gpsi;
+ renderData.browsertimeData = pageData.browsertime;
+ renderData.wptData = pageData.webpagetest;
+ renderData.config = config;
+ renderData.pageMeta = {
+ "path": "../",
+ "title": "Page data, collected by sitespeed.io for page " + url,
+ "description": "All data collected for this individual page."
+ };
+ var hash = util.getUrlHash(url);
+ renderHtmlToFile('page', renderData, hash + '.html', 'pages');
+};
+
+HTMLRenderer.prototype.renderRules = function () {
+ var renderData = {
+ "ruleDictionary": ruleDictionary,
+ "config": config,
+ "pageMeta": {
+ "title": "The sitespeed.io rules used by this run",
+ "description": "",
+ "isRules": true
+ }
+ };
+renderHtmlToFile('rules', renderData);
+};
+
+HTMLRenderer.prototype.renderScreenshots = function (pages) {
+ var renderData = {
+ "pages": pages,
+ "config": config,
+ "pageMeta": {
+ "title": "",
+ "description": "",
+ "isScreenshots": true
+ }
+ };
+renderHtmlToFile('screenshots', renderData);
+};
+
+
+HTMLRenderer.prototype.renderErrors = function (downloadErrors, analysisErrors) {
+ var renderData = {
+ "errors": {
+ "downloadErrorUrls": downloadErrors,
+ "analysisErrorUrls": analysisErrors
+ },
+ "totalErrors": Object.keys(downloadErrors).length + Object.keys(analysisErrors).length ,
+ "config": config,
+ "numberOfPages":this.numberOfAnalyzedPages,
+ "pageMeta": {
+ "title": "Pages that couldn't be analyzed",
+ "description": "Here are the pages that couldn't be analyzed by sitespeed.io",
+ "isErrors": true
+ }
+};
+renderHtmlToFile('errors', renderData);
+};
+
+HTMLRenderer.prototype.renderSummary = function (aggregates) {
+ // TODO change to reduce
+ var filtered = aggregates.filter(function(box) {
+ return (config.summaryBoxes.indexOf(box.id) > -1);
+ }).sort(function(box, box2) {
+ return config.summaryBoxes.indexOf(box.id) - config.summaryBoxes.indexOf(box2.id);
+ });
+ var renderData = {
+ "aggregates": filtered,
+ "config": config,
+ "numberOfPages":this.numberOfAnalyzedPages,
+ "pageMeta": {
+ "title": "Summary of the sitespeed.io result",
+ "description": "A executive summary.",
+ "isSummary": true
+ }
+ };
+
+ renderHtmlToFile('site-summary', renderData, 'index.html');
+
+ renderData = {
+ "aggregates": aggregates,
+ "config": config,
+ "numberOfPages":this.numberOfAnalyzedPages,
+ "pageMeta": {
+ "title": "In details summary of the sitespeed.io result",
+ "description": "The summary in details.",
+ "isDetailedSummary": true
+ }
+ };
+ renderHtmlToFile('detailed-site-summary', renderData);
+};
+
+HTMLRenderer.prototype.renderPages = function (pages) {
+ var renderData = {
+ "pages": pages,
+ "columnsMeta": columnsMetaData,
+ "config": config,
+ "ruleDictionary": ruleDictionary,
+ "numberOfPages":this.numberOfAnalyzedPages,
+ "pageMeta": {
+ "title": "All pages information",
+ "description": "All request data, for all the pages",
+ "isPages": true
+ }
+ };
+
+ renderHtmlToFile('pages', renderData);
+};
+
+HTMLRenderer.prototype.renderAssets = function (assets) {
+
+ var sorted = assets.sort(function(asset, asset2) {
+ return asset2.count - asset.count;
+ });
+
+ if (sorted.length>200)
+ sorted.length = 200;
+
+ var renderData = {
+ "assets": sorted,
+ "config": config,
+ "numberOfPages":this.numberOfAnalyzedPages,
+ "pageMeta": {
+ "title": "The most used assets",
+ "description": "A list of the most used assets for the analyzed pages.",
+ "isAssets": true
+ }
+ };
+ renderHtmlToFile('assets', renderData);
+};
+
+function renderHtmlToFile(template, renderData, fileName, optionalPath) {
+ fileName = fileName || (template + ".html");
+ optionalPath = optionalPath || '';
+ var result = compiledTemplates[template](renderData);
+ var file = path.join(config.run.absResultDir, optionalPath, fileName);
+ fs.outputFile(file, result, function(err) {
+ if (err)
+ log.log('error', "Couldn't write the file " + file + ' err:' + err);
+ });
+}
+
+function precompileTemplates() {
+ compiledTemplates = compileTemplates(path.join(__dirname, "../templates/"));
+ compiledPartials = compileTemplates(path.join(__dirname, "../templates/partials/"));
+
+ for (var key in compiledPartials) {
+ hb.registerPartial(key, compiledPartials[key]);
+ }
+}
+
+function compileTemplates(folderPath) {
+ var templates = {};
+ fs.readdirSync(folderPath).forEach(function (file) {
+ if (!fs.lstatSync(path.join(folderPath + file)).isDirectory())
+ templates[path.basename(file, '.hb')] = hb.compile(fs.readFileSync(path.join(folderPath + file), 'utf8'));
+ });
+ return templates;
+}
diff --git a/lib/junitRenderer.js b/lib/junitRenderer.js
new file mode 100644
index 000000000..097ff7e57
--- /dev/null
+++ b/lib/junitRenderer.js
@@ -0,0 +1,200 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var builder = require('xmlbuilder'),
+ config = require('./conf'),
+ fs = require('fs-extra'),
+ path = require('path'),
+ util = require('./util');
+
+module.exports = JUnitRenderer;
+
+function JUnitRenderer(collector) {
+ this.collector = collector;
+ this.ruleTestsuites = builder.create('testsuites', {
+ version: '1.0',
+ encoding: 'UTF-8'
+ });
+ this.timingTestsuites = builder.create('testsuites', {
+ version: '1.0',
+ encoding: 'UTF-8'
+ });
+}
+
+JUnitRenderer.prototype.renderForEachPage = function(url, pageData) {
+
+ var yslowData = pageData.yslow,
+ ruleDictionary = yslowData.dictionary.rules,
+ rules = pageData.yslow.g,
+ score = pageData.yslow.o,
+ browserTimeData = pageData.browsertime;
+ generateRuleTestSuitePerPage(url, score, rules, ruleDictionary, this.ruleTestsuites);
+
+ if (browserTimeData)
+ generateBrowserTimeTestSuitePerPage(browserTimeData, this.timingTestsuites);
+
+};
+
+function generateBrowserTimeTestSuitePerPage(browserTimeData, timingTestsuites) {
+
+ browserTimeData.forEach(function(run) {
+ var testsuite = timingTestsuites.ele('testsuite', {
+ 'name': 'sitespeed.io.timings.' + run.pageData.url.replace(/\./g, '_')
+ });
+
+ var url = run.pageData.url;
+
+ // First check if we have specific values configured for that URL else use the default ones
+ if (config.timingThresholds.pages) {
+ if (config.timingThresholds.pages.hasOwnProperty(url)) {
+
+ Object.keys(config.timingThresholds.pages[url]).forEach(function(
+ timing) {
+ run.statistics.forEach(function(stats) {
+ if (stats.name === timing) {
+ generateTimingTestCase(stats, timing, run, testsuite,
+ config.timingThresholds
+ .pages[url][timing]);
+ }
+ });
+ });
+ }
+ // Use default values
+ else {
+ Object.keys(config.timingThresholds.
+ default).forEach(function(timing) {
+ run.statistics.forEach(function(stats) {
+ if (stats.name === timing) {
+ generateTimingTestCase(stats, timing, run, testsuite,
+ config.timingThresholds.
+ default [timing]);
+ }
+ });
+ });
+ }
+ }
+ });
+}
+
+function generateTimingTestCase(stats, timing, run, testsuite, limit) {
+ var browser = run.pageData.browserName,
+ version = run.pageData.browserVersion,
+ url = run.pageData.url;
+
+ // The time in Jenkins needs to be in seconds
+ var testCase = testsuite.ele('testcase', {
+ 'name': timing,
+ 'time': stats[config.timingThresholds.type] / 1000
+ });
+
+ // is it a failure
+ if (stats[config.timingThresholds.type] > limit) {
+ testCase.ele('failure', {
+ 'type': 'failedTiming',
+ 'message': 'The time for ' + timing + ' is ' + stats[
+ config.timingThresholds.type] +
+ ' ms, that is higher than your limit of ' + limit + ' ms. Using ' +
+ browser + ' ' +
+ version + ' ' + config.timingThresholds.type + ' value'
+ });
+
+ }
+}
+
+
+function generateRuleTestSuitePerPage(url, score, rules, ruleDictionary,
+ testsuites) {
+ var rule = Object.keys(rules);
+
+ var failures = 0,
+ skipped = 0;
+
+ var testsuite = testsuites.ele('testsuite', {
+ 'name': 'sitespeed.io.rules.' + url.replace(/\./g, '_'),
+ 'tests': (rule.length + 1)
+ });
+ var overallPageTestCase = testsuite.ele('testcase');
+
+ overallPageTestCase.att({
+ 'name': 'Overall page score',
+ 'status': score
+ });
+ if (isFailure("overall", score)) {
+ overallPageTestCase.ele('failure', {
+ 'type': 'failedRule',
+ 'message': 'The average overall page score ' + score +
+ ' is below your limit'
+ });
+ failures++;
+ }
+
+
+ for (var i = 0; i < rule.length; i++) {
+
+ // is this skippable?
+ if (config.skipTest) {
+ if (config.skipTest.indexOf(rule[i]) > -1) {
+ skipped++;
+ continue;
+ }
+ }
+
+ var testCase = testsuite.ele('testcase', {
+ 'name': '(' + rule[i] + ') ' + ruleDictionary[rule[i]].name,
+ 'status': rules[rule[i]].score
+ });
+
+ if (isFailure(rule[i], rules[rule[i]].score)) {
+ failures++;
+ var failure = testCase.ele('failure', {
+ 'type': 'failedRule',
+ 'message': 'Score ' + score + ' - ' + rules[rule[i]].message
+ });
+
+ var comps = '';
+ rules[rule[i]].components.forEach(function(comp) {
+ comps += util.decodeURIComponent(comp) + '\n';
+ });
+
+ failure.txt(comps);
+ }
+
+ }
+
+ testsuite.att('failures', failures);
+ testsuite.att('skipped', skipped);
+}
+
+function isFailure(ruleid, value) {
+ if (config.thresholds) {
+ if (config.thresholds.hasOwnProperty(ruleid))
+ return (value < config.thresholds[ruleid]);
+ else return (value < config.threshold);
+ } else return (value < config.threshold);
+}
+
+JUnitRenderer.prototype.renderAfterFullAnalyse = function() {
+ // create testsuites and write to disk
+ var rulesXML = this.ruleTestsuites.end({
+ pretty: true,
+ indent: ' ',
+ newline: '\n'
+ });
+ var timingXML = this.timingTestsuites.end({
+ pretty: true,
+ indent: ' ',
+ newline: '\n'
+ });
+
+ renderXMLFile(rulesXML,"junit.xml");
+ renderXMLFile(timingXML,"junit-timings.xml");
+
+};
+
+function renderXMLFile(xml, fileName) {
+ console.log("Writing " + fileName);
+ fs.outputFileSync(path.join(config.run.absResultDir,fileName), xml);
+}
diff --git a/lib/log.js b/lib/log.js
new file mode 100644
index 000000000..8e43640d8
--- /dev/null
+++ b/lib/log.js
@@ -0,0 +1,15 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var winston = require('winston'),
+ path = require('path'),
+ config = require('./conf');
+
+winston.add(winston.transports.File, {
+ filename: path.join(config.run.absResultDir, 'info.log'),
+ level: 'info'
+});
+// winston.remove(winston.transports.Console);
diff --git a/lib/runner.js b/lib/runner.js
new file mode 100644
index 000000000..e2e1d7a28
--- /dev/null
+++ b/lib/runner.js
@@ -0,0 +1,159 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var crawler = require('./crawler'),
+ Analyzer = require('./analyze/analyzer'),
+ HTMLRenderer = require('./htmlRenderer'),
+ Collector = require('./collector'),
+ JUnitRenderer = require('./junitRenderer'),
+ Graphite = require('./graphite'),
+ logger = require('./log'),
+ path = require('path'),
+ fs = require('fs-extra'),
+ config = require('./conf'),
+ log = require('winston');
+
+module.exports = Runner;
+
+function Runner() {
+ var self = this;
+ this.analyzer = new Analyzer(function(err) {
+ self.analysisComplete(err);
+ });
+ this.collector = new Collector();
+ this.htmlRenderer = new HTMLRenderer();
+ this.junitRenderer = new JUnitRenderer(this.collector);
+ this.graphite = new Graphite(config.graphiteHost, config.graphitePort, config
+ .graphiteNamespace, this.collector);
+ this.downloadErrors = {};
+ this.analysisErrors = {};
+}
+
+
+Runner.prototype.analysisComplete = function(err) {
+ log.log('info', 'Done analyzing urls');
+
+ log.log('info', 'Render summary');
+ var aggregates = this.collector.createAggregates();
+ this.htmlRenderer.renderSummary(aggregates);
+
+ log.log('info', 'Render assets');
+ var assets = this.collector.createCollections().assets;
+ this.htmlRenderer.renderAssets(assets);
+
+ log.log('info', 'Render pages');
+ var pages = this.collector.createCollections().pages;
+ this.htmlRenderer.renderPages(pages);
+
+ log.log('info', 'Render rules');
+ this.htmlRenderer.renderRules();
+
+ if (config.screenshot) {
+ this.htmlRenderer.renderScreenshots(pages);
+ }
+
+ log.log('info', 'Render errors');
+ this.htmlRenderer.renderErrors(this.downloadErrors, this.analysisErrors);
+ if (config.graphiteHost)
+ this.graphite.sendPageData(aggregates, pages);
+ if (config.junit)
+ this.junitRenderer.renderAfterFullAnalyse();
+
+ log.log('info', "Wrote results to " + config.run.absResultDir);
+
+};
+
+
+Runner.prototype.run = function() {
+
+ // setup the directories needed
+ fs.mkdirsSync(path.join(config.run.absResultDir, config.dataDir));
+
+ // store the config file, so we can backtrack errors and/or use it again
+ fs.writeFile(path.join(config.run.absResultDir, 'config.json'), JSON.stringify(
+ config), function(err) {
+ if (err) throw err;
+ });
+
+ console.time("sitespeed.io");
+ if (config.url) {
+ log.log('info', "Will crawl from start point " + config.url +
+ " with crawl depth " + config.deep);
+ this.crawl(config.urlObject);
+ } else {
+ log.log('info', "Will fetch urls from the file " + config.file);
+ this.readFromFile(config.file);
+ }
+};
+
+Runner.prototype.readFromFile = function(file) {
+ var urls = fs.readFileSync(file).toString().split("\n");
+ urls = urls.filter(function(l) {
+ return l.length > 0;
+ });
+ analyzeUrls(urls);
+};
+
+Runner.prototype.crawl = function(url) {
+ var self = this;
+ crawler.crawl(url, function(okUrls, errorUrls) {
+ self.handleResult(okUrls, errorUrls);
+ });
+};
+
+Runner.prototype.handleResult = function(okUrls, errorUrls) {
+ var downloadErrors = this.downloadErrors;
+ Object.keys(errorUrls).forEach(function(url) {
+ log.log('error', "Failed to download " + url);
+ downloadErrors[url] = errorUrls[url];
+ });
+
+ // limit
+ if (config.maxPagesToTest) {
+ if (okUrls.length > config.maxPagesToTest)
+ okUrls.length = config.maxPagesToTest;
+ }
+ if (okUrls.length === 0) {
+ log.log('info', "Didn't get any URLs from the crawl");
+ return;
+ }
+
+ saveUrls(okUrls);
+
+ this.analyzeUrls(okUrls);
+};
+
+function saveUrls(urls) {
+ fs.writeFile(path.join(config.run.absResultDir, 'data', 'urls.txt'), urls.join(
+ "\n"), function(err) {
+ if (err) {
+ throw err;
+ }
+ });
+}
+
+Runner.prototype.analyzeUrls = function(urls) {
+ console.log("Will analyze " + urls.length + " pages");
+ var junitRenderer = this.junitRenderer;
+ var htmlRenderer = this.htmlRenderer;
+ var analysisErrors = this.analysisErrors;
+ this.analyzer.analyze(urls, this.collector, function(err, url, pageData) {
+
+ if (err) {
+ log.log('error', 'Could not analyze ' + url + ' (' + JSON.stringify(err) +
+ ')');
+ analysisErrors[url] = err;
+ return;
+ }
+
+ if (config.junit)
+ junitRenderer.renderForEachPage(url, pageData);
+ htmlRenderer.renderPage(url, pageData);
+ }
+
+
+ );
+};
diff --git a/lib/screenshot.js b/lib/screenshot.js
new file mode 100644
index 000000000..a8744124b
--- /dev/null
+++ b/lib/screenshot.js
@@ -0,0 +1,47 @@
+var page = require('webpage').create(),
+ address, output, size, w, h, agent, full, basicauth, auth;
+
+
+if (phantom.args.length < 5 || phantom.args.length > 7) {
+ console.log('Usage: screenshot.js URL filename width height user-agent full basic:auth');
+ phantom.exit();
+} else {
+ address = phantom.args[0];
+ output = phantom.args[1];
+ w = phantom.args[2];
+ h = phantom.args[3];
+ agent = phantom.args[4];
+ full = phantom.args[5];
+ basicauth = phantom.args[6];
+
+ if (basicauth) {
+ auth = basicauth.split(":");
+ page.settings.userName = auth[0];
+ page.settings.password = auth[1];
+ }
+
+
+ page.viewportSize = {
+ width: w,
+ height: h
+ };
+ page.settings.userAgent = agent;
+ page.open(address, function(status) {
+ if (status !== 'success') {
+ console.log('Unable to load the address!');
+ } else {
+ window.setTimeout(function() {
+
+ if (full != 'true')
+ page.clipRect = {
+ left: 0,
+ top: 0,
+ width: w,
+ height: h
+ };
+ page.render(output);
+ phantom.exit();
+ }, 200);
+ }
+ });
+}
diff --git a/lib/util.js b/lib/util.js
new file mode 100644
index 000000000..a6db44013
--- /dev/null
+++ b/lib/util.js
@@ -0,0 +1,411 @@
+/**
+ * Sitespeed.io - How speedy is your site? (http://www.sitespeed.io)
+ * Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
+ * and other contributors
+ * Released under the Apache 2.0 License
+ */
+var Stats = require('fast-stats').Stats,
+ crypto = require('crypto'),
+ config = require('./conf'),
+ url = require('url');
+
+module.exports = {
+ /**
+ * Get the cache time in seconds for a YSlow component
+ * a.k.a asset.
+ *
+ *
+ * @param {YSLOW.component} comp a component
+ * @return {Integer} The cache time in seconds
+ */
+ getCacheTime: function(comp) {
+
+ // This is how we do it: max-age will always win (HTTP 1.1)
+ // If max-age is found use it, else expires
+ // if no cache header, return 0
+ var maxAgeRegExp = /max-age=(\d+)/,
+ expireTime = 0;
+
+ var response = comp.headers.response;
+ for (var headerName in response) {
+ if (! response.hasOwnProperty(headerName))
+ continue;
+
+ // Cache-control always wins before Expires
+ // in the HTTP spec
+ if ('cache-control' === headerName.toLowerCase()) {
+ var cacheControl = response[headerName];
+ if (cacheControl) {
+ if (cacheControl.indexOf('no-cache') !== -1 ||
+ cacheControl.indexOf('no-store') !== -1) {
+ return 0;
+ }
+ var matches = cacheControl.match(maxAgeRegExp);
+ if (matches) {
+ return parseInt(matches[1], 10);
+ }
+ }
+
+ } else if ('expires' === headerName.toLowerCase()) {
+ var expiresDate = new Date(response[headerName]);
+ var now = new Date().getTime();
+ expireTime = expiresDate.getTime() - now;
+ }
+ }
+
+ return expireTime;
+ },
+
+ /**
+ * Get the cache time statistics for
+ * all YSlow components.
+ */
+ getCacheTimeStats: function(components) {
+ var stats = new Stats();
+
+ components.forEach(function(comp) {
+ stats.push(module.exports.getCacheTime(comp));
+ });
+
+ return module.exports.getStatisticsObject(stats,0);
+ },
+
+ /**
+ * Get the last modification time statistics for
+ * all YSlow components.
+ */
+ getLastModStats: function(components) {
+ var stats = new Stats();
+
+ components.forEach(function(comp) {
+ stats.push(module.exports.getTimeSinceLastMod(comp));
+ });
+
+ return module.exports.getStatisticsObject(stats,0);
+ },
+
+ /**
+ * Get the time in seconds since a component was
+ * last modified. If the server doesn't send a
+ * last-modified header, the modified time will
+ * be set to now.
+ *
+ *
+ * @param the YSlow component
+ * @return {Integer} The time in seconds or -1 if unknown
+ */
+ getTimeSinceLastMod: function(comp) {
+ var now = new Date();
+ var lastModifiedDate;
+ var response = comp.headers.response;
+ for (var headerName in response) {
+ if (! response.hasOwnProperty(headerName))
+ continue;
+
+ if ('last-modified' === headerName.toLowerCase()) {
+ lastModifiedDate = new Date(response[headerName]);
+ } else if ('date' === headerName.toLowerCase()) {
+ now = new Date(response[headerName]);
+ }
+ }
+
+ // TODO how do we define this?
+ if (!lastModifiedDate)
+ return -1;
+
+ return (now.getTime() - lastModifiedDate.getTime()) / 1000;
+ },
+
+ /**
+ * Print seconds as the largest available time.
+ * @param {Integer} seconds A number in seconds
+ * @return {String} The time in nearest largest definition.
+ */
+ prettyPrintSeconds: function(seconds) {
+
+ if (seconds === -1) return -1;
+
+ var secondsPerYear = 365 * 24 * 60 * 60,
+ secondsPerWeek = 60 * 60 * 24 * 7,
+ secondsPerDay = 60 * 60 * 24,
+ secondsPerHour = 60 * 60,
+ secondsPerMinute = 60,
+ sign = (seconds < 0) ? "-" : "";
+
+ if (seconds < 0)
+ seconds = Math.abs(seconds);
+
+ if (seconds / secondsPerYear >= 1)
+ return sign + Math.round(seconds / secondsPerYear) + " year" + ((Math.round(
+ seconds / secondsPerYear) > 1) ? "s" : "");
+ else if (seconds / secondsPerWeek >= 1)
+ return sign + Math.round(seconds / secondsPerWeek) + " week" + ((Math.round(
+ seconds / secondsPerWeek) > 1) ? "s" : "");
+ else if (seconds / secondsPerDay >= 1)
+ return sign + Math.round(seconds / secondsPerDay) + " day" + ((Math.round(
+ seconds / secondsPerDay) > 1) ? "s" : "");
+ else if (seconds / secondsPerHour >= 1)
+ return sign + Math.round(seconds / secondsPerHour) + " hour" + ((Math.round(
+ seconds / secondsPerHour) > 1) ? "s" : "");
+ else if (seconds / secondsPerMinute >= 1)
+ return sign + Math.round(seconds / secondsPerMinute) + " minute" + ((
+ Math.round(seconds / secondsPerMinute) > 1) ? "s" : "");
+ else return sign + seconds + " second" + ((seconds > 1) ? "s" : "");
+ },
+
+ /**
+ * Get seconds, milliseconds or bytes in a human readable format.
+ * Will turn seconds into the largest avalible time format (minutes, hours etc),
+ * add ms to milliseconds and turn bytes into kiloytes.
+ */
+ getHumanReadable: function(data, value, showUnit) {
+ if (data.unit === 'seconds')
+ return this.prettyPrintSeconds(value);
+ else if (data.unit === 'milliseconds')
+ return value + (showUnit ? ' ms':'');
+ else if (data.unit === 'bytes')
+ return this.getKbSize(value, showUnit);
+ else return value;
+ },
+
+ /**
+ * If we have a matching rule definition, we will return the
+ * matching Bootstrap CSS name, so that the CSS will have the right color.
+ */
+ getRuleColor: function(rule, value) {
+ if (config.rules[rule]) {
+ var diff = config.rules[rule].warning - config.rules[rule].error;
+ if (diff > 0) {
+ if (value>config.rules[rule].warning)
+ return 'success';
+ else if (value>config.rules[rule].error)
+ return 'warning';
+ return 'danger';
+ }
+ else {
+ if (value 100 ? 100 : 10);
+ size -= remainder;
+ return parseFloat(size / 1000) + (0 === (size % 1000) ? ".0" : "") + (showUnit?' kb':'');
+ },
+
+ select: function(object, keyPath, defaultValue) {
+ return keyPath.split('.').reduce(function (result, key) {
+ result = result[key];
+ return result || defaultValue;
+ }, object);
+ },
+
+ decodeURIComponent: function(value) {
+ try {
+ return decodeURIComponent(value);
+ } catch (err) {
+ return value;
+ }
+ },
+
+ /**
+ * Get the URL as a hash so it can be stored on disk.
+ */
+ getUrlHash: function(u) {
+ var urlComponents = url.parse(u);
+ var hash = crypto.createHash('md5').update(u).digest('hex').substr(0, 7);
+ var name = urlComponents.pathname;
+ if (name == '/') {
+ name = urlComponents.hostname;
+ } else {
+ name = name.replace(/^\/|\/$/g, '').split('/').pop();
+ name = name.split('.')[0];
+ }
+ return encodeURIComponent(name) + '-' + hash;
+ },
+
+ /**
+ * Get the hostname from a URL String
+ */
+ getHostname: function(u) {
+ u = this.decodeURIComponent(u);
+ var hostname = u.split('/')[2];
+ return (hostname && hostname.split(':')[0]) || '';
+ },
+
+ /**
+ * Get a usable view of the statistics object. Will format the
+ * result to decimals.
+ */
+ getStatisticsObject: function(stats, decimals) {
+ return {
+ min: stats.percentile(0).toFixed(decimals),
+ max: stats.percentile(100).toFixed(decimals),
+ p10: stats.percentile(10).toFixed(decimals),
+ p70: stats.percentile(70).toFixed(decimals),
+ p80: stats.percentile(80).toFixed(decimals),
+ p90: stats.percentile(90).toFixed(decimals),
+ p99: stats.percentile(99).toFixed(decimals),
+ median: stats.median().toFixed(decimals),
+ mean: stats.amean().toFixed(decimals)
+ };
+ },
+
+ /**
+ * Get the number of domains used for YSlow
+ * components
+ */
+ getNumberOfDomains: function(components) {
+ var self = this;
+ var domains = this.aggregate(components,
+ function (comp) {
+ return self.getHostname(comp.url);
+ }
+ );
+
+ return Object.keys(domains).length;
+ },
+
+ /**
+ * Get the size in bytes for a specific domain
+ */
+ getSizeForDomain: function (domain, components) {
+ var self = this;
+ var hostAndSize = this.aggregate(components,
+ function (comp) {
+ return self.getHostname(comp.url);
+ },
+ function (comp) {
+ return comp.size;
+ }
+ );
+
+ return hostAndSize[domain];
+ },
+
+ /**
+ * Get the number of assets per domain.
+ */
+ getAssetsPerDomain: function(components) {
+ var self = this;
+ return this.aggregate(components, function (comp) {
+ return self.getHostname(comp.url);
+ });
+ },
+
+
+ /**
+ * Get the number of assets per content type.
+ */
+ getAssetsPerContentType: function(components) {
+ return this.aggregate(components, function (comp) {
+ return comp.type;
+ });
+ },
+
+ /**
+ * Get the size in bytes per content type.
+ */
+ getAssetsSizePerContentType: function(components) {
+ return this.aggregate(components,
+ function (comp) {
+ return comp.type;
+ },
+ function (comp) {
+ return comp.size;
+ }
+ );
+ },
+
+ aggregate: function (array, keyFunction, valueFunction) {
+ return array.reduce(function (result, item) {
+ var key = keyFunction ? keyFunction(item) : item;
+ var value = valueFunction ? valueFunction(item) : 1;
+
+ if (result.hasOwnProperty(key)) {
+ result[key] += value;
+ } else {
+ result[key] = value;
+ }
+ return result;
+ }, {});
+ },
+
+ /**
+ * Hack to format Google Page Speed Insights result
+ *
+ **/
+ gpsiReplacer: function(args, text) {
+
+ if (args.length === 1) {
+ return text.replace('$1', args[0].value);
+ } else if (args.length === 2) {
+ return text.replace('$1', args[0].value).replace('$2', args[1].value);
+ } else if (args.length === 3) {
+ return text.replace('$1', args[0].value).replace('$2', args[1].value).replace(
+ '$3', args[2].value);
+ } else return text;
+ },
+
+ /**
+ * Get the URL from pageData.
+ */
+ getURLFromPageData: function(pageData) {
+ if (pageData.yslow)
+ return pageData.yslow.originalUrl;
+ else if (pageData.browsertime)
+ return pageData.browsertime[0].pageData.url;
+ else if (pageData.gpsi)
+ return pageData.gpsi.id;
+ return 'undefined';
+ },
+
+ getGraphiteURLKey: function(theUrl) {
+ myUrl = url.parse(theUrl);
+ var protocol = myUrl.protocol.replace(':', '');
+ var hostname = myUrl.hostname;
+ var path = myUrl.pathname;
+
+
+ if (path.indexOf(".") > -1) path = path.replace(".", "_");
+ if (path.indexOf("~") > -1) path = path.replace("~", "_");
+
+
+ if (path === '' || path === '/')
+ return protocol + '.' + hostname + '.slash.';
+
+
+ var key = protocol + '.' + hostname + '.' + path.replace('/', '.');
+ if (key.indexOf('.', key.length - 1) !== -1)
+ return key;
+ else return key + '.';
+
+
+}
+};
diff --git a/lib/yslow-3.1.8-sitespeed.js b/lib/yslow-3.1.8-sitespeed.js
new file mode 100644
index 000000000..5ceb8347c
--- /dev/null
+++ b/lib/yslow-3.1.8-sitespeed.js
@@ -0,0 +1,11717 @@
+/**
+ * Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+ * Copyright (c) 2013, Marcel Duran and other contributors. All rights reserved.
+ * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
+ */
+
+/*global phantom, YSLOW*/
+/*jslint browser: true, evil: true, sloppy: true, regexp: true*/
+
+/**
+ * JSLint is tolerating evil because there's a Function constructor needed to
+ * inject the content coming from phantom arguments and page resources which is
+ * later evaluated into the page in order to run YSlow.
+ */
+
+// For using yslow in phantomjs, see instructions @ http://yslow.org/phantomjs/
+
+// parse args
+var i, arg, page, urlCount, viewport,
+ webpage = require('webpage'),
+ args = phantom.args,
+ len = args.length,
+ urls = [],
+ yslowArgs = {
+ info: 'all',
+ format: 'json',
+ ruleset: 'ydefault',
+ beacon: false,
+ ua: false,
+ viewport: false,
+ headers: false,
+ console: 0,
+ threshold: 80,
+ cdns: '',
+ file:'',
+ basicauth: ''
+ },
+ 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',
+ ba: 'basicauth'
+ };
+
+// loop args
+for (i = 0; i < len; i += 1) {
+ arg = args[i];
+ if (arg[0] !== '-') {
+ // url, normalize if needed
+ if (arg.indexOf('http') !== 0) {
+ arg = 'http://' + arg;
+ }
+ urls.push(arg);
+ }
+ arg = arg.replace(/^\-\-?/, '');
+ if (yslowArgs.hasOwnProperty(arg)) {
+ // yslow argument
+ i += 1;
+ yslowArgs[arg] = args[i];
+ } else if (yslowArgs.hasOwnProperty(argsAlias[arg])) {
+ // yslow argument alias
+ i += 1;
+ yslowArgs[argsAlias[arg]] = args[i];
+ } else if (unaryArgs.hasOwnProperty(arg)) {
+ // unary argument
+ unaryArgs[arg] = true;
+ } else if (unaryArgs.hasOwnProperty(argsAlias[arg])) {
+ // unary argument alias
+ unaryArgs[argsAlias[arg]] = true;
+ }
+}
+urlCount = urls.length;
+
+// check for version
+if (unaryArgs.version) {
+ console.log('3.1.8');
+ phantom.exit();
+}
+
+// print usage
+if (len === 0 || urlCount === 0 || unaryArgs.help) {
+ console.log([
+ '',
+ ' Usage: phantomjs [phantomjs options] ' + phantom.scriptName + ' [yslow options] [url ...]',
+ '',
+ ' PhantomJS Options:',
+ '',
+ ' http://y.ahoo.it/phantomjs/options',
+ '',
+ ' YSlow Options:',
+ '',
+ ' -h, --help output usage information',
+ ' -V, --version output the version number',
+ ' -i, --info specify the information to display/log (basic|grade|stats|comps|all) [all]',
+ ' -f, --format specify the output results format (json|xml|plain|tap|junit) [json]',
+ ' -r, --ruleset specify the YSlow performance ruleset to be used (ydefault|yslow1|yblog) [ydefault]',
+ ' -b, --beacon specify an URL to log the results',
+ ' -d, --dict include dictionary of results fields',
+ ' -v, --verbose output beacon response information',
+ ' -t, --threshold for test formats, the threshold to test scores ([0-100]|[A-F]|{JSON}) [80]',
+ ' e.g.: -t B or -t 75 or -t \'{"overall": "B", "ycdn": "F", "yexpires": 85}\'',
+ ' -u, --ua "" specify the user agent string sent to server when the page requests resources',
+ ' -vp, --viewport specify page viewport size WxY, where W = width and H = height [400x300]',
+ ' -ch, --headers specify custom request headers, e.g.: -ch \'{"Cookie": "foo=bar"}\'',
+ ' -c, --console output page console messages (0: none, 1: message, 2: message + line + source) [0]',
+ ' --cdns "" specify comma separated list of additional CDNs',
+ ' -ba, --basicauth "" username & password used for basic auth',
+ ' --file output the result to file',
+ '',
+ ' 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();
+
+ // set user agent string
+ if (yslowArgs.basicauth) {
+ auth = yslowArgs.basicauth.split(":");
+ page.settings.userName = auth[0];
+ page.settings.password = auth[1];
+ }
+
+ 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];
+ }
+ }
+ }
+ };
+
+ // used for better error messages
+ page.onResourceError = function(resourceError) {
+ page.reason = resourceError.errorString;
+ page.reason_url = resourceError.url;
+ };
+
+ // supressing all errors for now
+ /*
+ page.onConsoleMessage = function (msg){};
+ page.onAlert = function (msg) {};
+ page.onError = function(msg, trace) {};
+ */
+ // 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.error('FAIL to load ' + url + ' reason:' + page.reason + ' url:' + page.reason_url);
+ exitStatus += 1;
+ } 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.
+ * Copyright (c) 2013, Marcel Duran and other contributors. 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.
+ * Copyright (c) 2013, Marcel Duran and other contributors. 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.
+ * Copyright (c) 2013, Marcel Duran and other contributors. 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.
+ * Copyright (c) 2013, Marcel Duran and other contributors. 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 = / /,
+ util = YSLOW.util,
+ isArray = util.isArray,
+ stats = {},
+ stats_c = {},
+ comp_objs = [],
+ params = {},
+ g = {};
+
+ // default
+ info = (info || 'basic').split(',');
+
+ for (i = 0, len = info.length; i < len; i += 1) {
+ if (info[i] === 'all') {
+ include_grade = include_stats = include_comps = true;
+ break;
+ } else {
+ switch (info[i]) {
+ case 'grade':
+ include_grade = true;
+ break;
+ case 'stats':
+ include_stats = true;
+ break;
+ case 'comps':
+ include_comps = true;
+ break;
+ }
+ }
+ }
+
+ params.v = YSLOW.version;
+ params.w = parseInt(yscontext.PAGE.totalSize, 10);
+ params.o = parseInt(yscontext.PAGE.overallScore, 10);
+ params.u = encodeURIComponent(yscontext.result_set.url);
+ params.r = parseInt(yscontext.PAGE.totalRequests, 10);
+ spaceid = util.getPageSpaceid(yscontext.component_set);
+ if (spaceid) {
+ params.s = encodeURI(spaceid);
+ }
+ params.i = yscontext.result_set.getRulesetApplied().id;
+ if (yscontext.PAGE.t_done) {
+ params.lt = parseInt(yscontext.PAGE.t_done, 10);
+ }
+
+ if (include_grade) {
+ results = yscontext.result_set.getResults();
+ for (i = 0, len = results.length; i < len; i += 1) {
+ obj = {};
+ result = results[i];
+ if (result.hasOwnProperty('score')) {
+ if (result.score >= 0) {
+ obj.score = parseInt(result.score, 10);
+ } else if (result.score === -1) {
+ obj.score = 'n/a';
+ }
+ }
+ // removing hardcoded open link,
+ // TODO: remove those links from original messages
+ obj.message = result.message.replace(
+ /javascript:document\.ysview\.openLink\('(.+)'\)/,
+ '$1'
+ );
+ comps = result.components;
+ if (isArray(comps)) {
+ obj.components = [];
+ for (l = 0, len2 = comps.length; l < len2; l += 1) {
+ comp = comps[l];
+ if (typeof comp === 'string') {
+ url = comp;
+ } else if (typeof comp.url === 'string') {
+ url = comp.url;
+ }
+ if (url) {
+ url = encodeURIComponent(url.replace(reButton, ''));
+ obj.components.push(url);
+ }
+ }
+ }
+ g[result.rule_id] = obj;
+ }
+ params.g = g;
+ }
+
+ if (include_stats) {
+ params.w_c = parseInt(yscontext.PAGE.totalSizePrimed, 10);
+ params.r_c = parseInt(yscontext.PAGE.totalRequestsPrimed, 10);
+
+ for (type in yscontext.PAGE.totalObjCount) {
+ if (yscontext.PAGE.totalObjCount.hasOwnProperty(type)) {
+ stats[type] = {
+ 'r': yscontext.PAGE.totalObjCount[type],
+ 'w': yscontext.PAGE.totalObjSize[type]
+ };
+ }
+ }
+ params.stats = stats;
+
+ for (type in yscontext.PAGE.totalObjCountPrimed) {
+ if (yscontext.PAGE.totalObjCountPrimed.hasOwnProperty(type)) {
+ stats_c[type] = {
+ 'r': yscontext.PAGE.totalObjCountPrimed[type],
+ 'w': yscontext.PAGE.totalObjSizePrimed[type]
+ };
+ }
+ }
+ params.stats_c = stats_c;
+ }
+
+ if (include_comps) {
+ comps = yscontext.component_set.components;
+ for (i = 0, len = comps.length; i < len; i += 1) {
+ comp = comps[i];
+ encoded_url = encodeURIComponent(comp.url);
+ obj = {
+ 'type': comp.type,
+ 'url': encoded_url,
+ 'size': comp.size,
+ 'resp': comp.respTime
+ };
+ if (comp.size_compressed) {
+ obj.gzip = comp.size_compressed;
+ }
+ if (comp.expires && comp.expires instanceof Date) {
+ obj.expires = util.prettyExpiresDate(comp.expires);
+ }
+ cr = comp.getReceivedCookieSize();
+ if (cr > 0) {
+ obj.cr = cr;
+ }
+ cs = comp.getSetCookieSize();
+ if (cs > 0) {
+ obj.cs = cs;
+ }
+ etag = comp.getEtag();
+ if (typeof etag === 'string' && etag.length > 0) {
+ obj.etag = etag;
+ }
+ // format req/res headers
+ obj.headers = {};
+ if (comp.req_headers) {
+ sourceHeaders = comp.req_headers;
+ obj.headers.request = {};
+ targetHeaders = obj.headers.request;
+ for (header in sourceHeaders) {
+ if (sourceHeaders.hasOwnProperty(header)) {
+ targetHeaders[util.formatHeaderName(header)] =
+ sourceHeaders[header];
+ }
+ }
+ }
+ if (comp.headers) {
+ sourceHeaders = comp.headers;
+ obj.headers.response = {};
+ targetHeaders = obj.headers.response;
+ for (header in sourceHeaders) {
+ if (sourceHeaders.hasOwnProperty(header)) {
+ targetHeaders[util.formatHeaderName(header)] =
+ sourceHeaders[header];
+ }
+ }
+ }
+ comp_objs.push(obj);
+ }
+ params.comps = comp_objs;
+ }
+
+ return params;
+ },
+
+ /**
+ * Send YSlow beacon.
+ * @param {Object} results Results object
+ * generated by {@link YSLOW.util.getResults}.
+ * @param {String|Array} info Information to be beaconed
+ * (basic|grade|stats|comps|all).
+ * @param {String} url The URL to fire beacon to.
+ * @return {String} The beacon content sent.
+ */
+ sendBeacon: function (results, info, url) {
+ var i, len, req, name, img,
+ beacon = '',
+ util = YSLOW.util,
+ pref = util.Preference,
+ method = 'get';
+
+ // default
+ info = (info || 'basic').split(',');
+
+ for (i = 0, len = info.length; i < len; i += 1) {
+ if (info[i] === 'all') {
+ method = 'post';
+ break;
+ } else {
+ switch (info[i]) {
+ case 'grade':
+ method = 'post';
+ break;
+ case 'stats':
+ method = 'post';
+ break;
+ case 'comps':
+ method = 'post';
+ break;
+ }
+ }
+ }
+
+ if (method === 'post') {
+ beacon = JSON.stringify(results, null);
+ req = util.getXHR();
+ req.open('POST', url, true);
+ req.setRequestHeader('Content-Length', beacon.length);
+ req.setRequestHeader('Content-Type', 'application/json');
+ req.send(beacon);
+ } else {
+ for (name in results) {
+ if (results.hasOwnProperty(name)) {
+ beacon += name + '=' + results[name] + '&';
+ }
+ }
+ img = new Image();
+ img.src = url + '?' + beacon;
+ }
+
+ return beacon;
+ },
+
+ /**
+ * Get the dictionary of params used in results.
+ * @param {String|Array} info Results information
+ * (basic|grade|stats|comps|all).
+ * @param {String} ruleset The Results ruleset used
+ * (ydefault|yslow1|yblog).
+ * @return {Object} The dictionary object {key: value}.
+ */
+ getDict: function (info, ruleset) {
+ var i, len, include_grade, include_stats, include_comps,
+ weights, rs,
+ yslow = YSLOW,
+ controller = yslow.controller,
+ rules = yslow.doc.rules,
+ dict = {
+ v: 'version',
+ w: 'size',
+ o: 'overall score',
+ u: 'url',
+ r: 'total number of requests',
+ s: 'space id of the page',
+ i: 'id of the ruleset used',
+ lt: 'page load time',
+ grades: '100 >= A >= 90 > B >= 80 > C >= 70 > ' +
+ 'D >= 60 > E >= 50 > F >= 0 > N/A = -1'
+ };
+
+ // defaults
+ info = (info || 'basic').split(',');
+ ruleset = ruleset || 'ydefault';
+ weights = controller.rulesets[ruleset].weights;
+
+ // check which info will be included
+ for (i = 0, len = info.length; i < len; i += 1) {
+ if (info[i] === 'all') {
+ include_grade = include_stats = include_comps = true;
+ break;
+ } else {
+ switch (info[i]) {
+ case 'grade':
+ include_grade = true;
+ break;
+ case 'stats':
+ include_stats = true;
+ break;
+ case 'comps':
+ include_comps = true;
+ break;
+ }
+ }
+ }
+
+ // include dictionary
+ if (include_grade) {
+ dict.g = 'scores of all rules in the ruleset';
+ dict.rules = {};
+ for (rs in weights) {
+ if (weights.hasOwnProperty(rs)) {
+ dict.rules[rs] = rules[rs];
+ dict.rules[rs].weight = weights[rs];
+ }
+ }
+ }
+ if (include_stats) {
+ dict.w_c = 'page weight with primed cache';
+ dict.r_c = 'number of requests with primed cache';
+ dict.stats = 'number of requests and weight grouped by ' +
+ 'component type';
+ dict.stats_c = 'number of request and weight of ' +
+ 'components group by component type with primed cache';
+ }
+ if (include_comps) {
+ dict.comps = 'array of all the components found on the page';
+ }
+
+ return dict;
+ },
+
+ /**
+ * Check if input is an Object
+ * @param {Object} the input to check wheter it's an object or not
+ * @return {Booleam} true for Object
+ */
+ isObject: function (o) {
+ return Object.prototype.toString.call(o).indexOf('Object') > -1;
+ },
+
+ /**
+ * Check if input is an Array
+ * @param {Array} the input to check wheter it's an array or not
+ * @return {Booleam} true for Array
+ */
+ isArray: function (o) {
+ if (Array.isArray) {
+ return Array.isArray(o);
+ } else {
+ return Object.prototype.toString.call(o).indexOf('Array') > -1;
+ }
+ },
+
+
+ /**
+ * Wrapper for decodeURIComponent, try to decode
+ * otherwise return the input value.
+ * @param {String} value The value to be decoded.
+ * @return {String} The decoded value.
+ */
+ decodeURIComponent: function (value) {
+ try {
+ return decodeURIComponent(value);
+ } catch (err) {
+ return value;
+ }
+ },
+
+ /**
+ * Decode html entities. e.g.: < becomes <
+ * @param {String} str the html string to decode entities from.
+ * @return {String} the input html with entities decoded.
+ */
+ decodeEntities: function (str) {
+ return String(str)
+ .replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/"/g, '"');
+ },
+
+ safeXML: (function () {
+ var decodeComp = this.decodeURIComponent,
+ reInvalid = /[<&>]/;
+
+ return function (value, decode) {
+ if (decode) {
+ value = decodeComp(value);
+ }
+ if (reInvalid.test(value)) {
+ return '';
+ }
+ return value;
+ };
+ }()),
+
+ /**
+ * convert Object to XML
+ * @param {Object} obj the Object to be converted to XML
+ * @param {String} root the XML root (default = results)
+ * @return {String} the XML
+ */
+ objToXML: function (obj, root) {
+ var toXML,
+ util = YSLOW.util,
+ safeXML = util.safeXML,
+ xml = '';
+
+ toXML = function (o) {
+ var item, value, i, len, val, type;
+
+ for (item in o) {
+ if (o.hasOwnProperty(item)) {
+ value = o[item];
+ xml += '<' + item + '>';
+ if (util.isArray(value)) {
+ for (i = 0, len = value.length; i < len; i += 1) {
+ val = value[i];
+ type = typeof val;
+ xml += '- ';
+ if (type === 'string' || type === 'number') {
+ xml += safeXML(val, item === 'components');
+ } else {
+ toXML(val);
+ }
+ xml += '
';
+ }
+ } else if (util.isObject(value)) {
+ toXML(value);
+ } else {
+ xml += safeXML(value, item === 'u' || item === 'url');
+ }
+ xml += '' + item + '>';
+ }
+ }
+ };
+
+ root = root || 'results';
+ xml += '<' + root + '>';
+ toXML(obj);
+ xml += '' + root + '>';
+
+ return xml;
+ },
+
+ /**
+ * Pretty print results
+ * @param {Object} obj the Object with YSlow results
+ * @return {String} the results in plain text (pretty printed)
+ */
+ prettyPrintResults: function (obj) {
+ var pp,
+ util = YSLOW.util,
+ str = '',
+ mem = {},
+
+ dict = {
+ v: 'version',
+ w: 'size',
+ o: 'overall score',
+ u: 'url',
+ r: '# of requests',
+ s: 'space id',
+ i: 'ruleset',
+ lt: 'page load time',
+ g: 'scores',
+ w_c: 'page size (primed cache)',
+ r_c: '# of requests (primed cache)',
+ stats: 'statistics by component',
+ stats_c: 'statistics by component (primed cache)',
+ comps: 'components found on the page',
+ components: 'offenders',
+ cr: 'received cookie size',
+ cs: 'set cookie size',
+ resp: 'response time'
+ },
+
+ indent = function (n) {
+ var arr,
+ res = mem[n];
+
+ if (typeof res === 'undefined') {
+ arr = [];
+ arr.length = (4 * n) + 1;
+ mem[n] = res = arr.join(' ');
+ }
+
+ return res;
+ };
+
+ pp = function (o, level) {
+ var item, value, i, len, val, type;
+
+ for (item in o) {
+ if (o.hasOwnProperty(item)) {
+ value = o[item];
+ str += indent(level) + (dict[item] || item) + ':';
+ if (util.isArray(value)) {
+ str += '\n';
+ for (i = 0, len = value.length; i < len; i += 1) {
+ val = value[i];
+ type = typeof val;
+ if (type === 'string' || type === 'number') {
+ str += indent(level + 1) +
+ util.decodeURIComponent(val) + '\n';
+ } else {
+ pp(val, level + 1);
+ if (i < len - 1) {
+ str += '\n';
+ }
+ }
+ }
+ } else if (util.isObject(value)) {
+ str += '\n';
+ pp(value, level + 1);
+ } else {
+ if (item === 'score' || item === 'o') {
+ value = util.prettyScore(value) + ' (' + value + ')';
+ }
+ if (item === 'w' || item === 'w_c' ||
+ item === 'size' || item === 'gzip' ||
+ item === 'cr' || item === 'cs') {
+ value = util.kbSize(value) + ' (' + value + ' bytes)';
+ }
+ str += ' ' + util.decodeURIComponent(value) + '\n';
+ }
+ }
+ }
+ };
+
+ pp(obj, 0);
+
+ return str;
+ },
+
+ /**
+ * Test result against a certain threshold for CI
+ * @param {Object} obj the Object with YSlow results
+ * @param {String|Number|Object} threshold The definition of OK (inclusive)
+ * Anything >= threshold == OK. It can be a number [0-100],
+ * a letter [A-F] as follows:
+ * 100 >= A >= 90 > B >= 80 > C >= 70 > D >= 60 > E >= 50 > F >= 0 > N/A = -1
+ * It can also be a specific per rule. e.g:
+ * {overall: 80, ycdn: 65, ynumreq: 'B'}
+ * where overall is the common threshold to be
+ * used by all rules except those listed
+ * @return {Array} the test result array containing each test result details:
+ */
+ testResults: function (obj, threshold) {
+ var overall, g, grade, grades, score, commonScore, i, len,
+ tests = [],
+ scores = {
+ a: 90,
+ b: 80,
+ c: 70,
+ d: 60,
+ e: 50,
+ f: 0,
+ 'n/a': -1
+ },
+ yslow = YSLOW,
+ util = yslow.util,
+ isObj = util.isObject(threshold),
+ rules = yslow.doc.rules,
+
+ getScore = function (value) {
+ var score = parseInt(value, 10);
+
+ if (isNaN(score) && typeof value === 'string') {
+ score = scores[value.toLowerCase()];
+ }
+
+ // edge case for F or 0
+ if (score === 0) {
+ return 0;
+ }
+
+ return score || overall || scores.b;
+ },
+
+ getThreshold = function (name) {
+ if (commonScore) {
+ return commonScore;
+ }
+
+ if (!isObj) {
+ commonScore = getScore(threshold);
+ return commonScore;
+ } else if (threshold.hasOwnProperty(name)) {
+ return getScore(threshold[name]);
+ } else {
+ return overall || scores.b;
+ }
+ },
+
+ test = function (score, ts, name, message, offenders) {
+ var desc = rules.hasOwnProperty(name) && rules[name].name;
+
+ tests.push({
+ ok: score >= ts,
+ score: score,
+ grade: util.prettyScore(score),
+ name: name,
+ description: desc || '',
+ message: message,
+ offenders: offenders
+ });
+ };
+
+ // overall threshold (default b [80])
+ overall = getThreshold('overall');
+
+ // overall score
+ test(obj.o, overall, 'overall score');
+
+ // grades
+ grades = obj.g;
+ if (grades) {
+ for (grade in grades) {
+ if (grades.hasOwnProperty(grade)) {
+ g = grades[grade];
+ score = g.score;
+ if (typeof score === 'undefined') {
+ score = -1;
+ }
+ test(score, getThreshold(grade), grade,
+ g.message, g.components);
+ }
+ }
+ }
+
+ return tests;
+ },
+
+ /**
+ * Format test results as TAP for CI
+ * @see: http://testanything.org/wiki/index.php/TAP_specification
+ * @param {Array} tests the arrays containing the test results from testResults.
+ * @return {Object}:
+ * failures: {Number} total test failed,
+ * content: {String} the results as TAP plain text
+ */
+ formatAsTAP: function (results) {
+ var i, res, line, offenders, j, lenJ,
+ failures = 0,
+ len = results.length,
+ tap = [],
+ util = YSLOW.util,
+ decodeURI = util.decodeURIComponent;
+
+ // tap version
+ tap.push('TAP version 13');
+
+ // test plan
+ tap.push('1..' + len);
+
+ for (i = 0; i < len; i += 1) {
+ res = results[i];
+ line = res.ok || res.score < 0 ? 'ok' : 'not ok';
+ failures += (res.ok || res.score < 0) ? 0 : 1;
+ line += ' ' + (i + 1) + ' ' + res.grade +
+ ' (' + res.score + ') ' + res.name;
+ if (res.description) {
+ line += ': ' + res.description;
+ }
+ if (res.score < 0) {
+ line += ' # SKIP score N/A';
+ }
+ tap.push(line);
+
+ // message
+ if (res.message) {
+ tap.push(' ---');
+ tap.push(' message: ' + res.message);
+ }
+
+ // offenders
+ offenders = res.offenders;
+ if (offenders) {
+ lenJ = offenders.length;
+ if (lenJ > 0) {
+ if (!res.message) {
+ tap.push(' ---');
+ }
+ tap.push(' offenders:');
+ for (j = 0; j < lenJ; j += 1) {
+ tap.push(' - "' +
+ decodeURI(offenders[j]) + '"');
+ }
+ }
+ }
+
+ if (res.message || lenJ > 0) {
+ tap.push(' ...');
+ }
+ }
+
+ return {
+ failures: failures,
+ content: tap.join('\n')
+ };
+ },
+
+ /**
+ * Format test results as JUnit XML for CI
+ * @see: http://www.junit.org/
+ * @param {Array} tests the arrays containing the test results from testResults.
+ * @return {Object}:
+ * failures: {Number} total test failed,
+ * content: {String} the results as JUnit XML text
+ */
+ formatAsJUnit: function (results) {
+ var i, res, line, offenders, j, lenJ,
+ len = results.length,
+ skipped = 0,
+ failures = 0,
+ junit = [],
+ cases = [],
+ util = YSLOW.util,
+ decodeURI = util.decodeURIComponent,
+ safeXML = util.safeXML,
+
+ safeAttr = function (str) {
+ return str
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+ };
+
+ for (i = 0; i < len; i += 1) {
+ res = results[i];
+ line = ' ');
+ } else {
+ cases.push(line + '">');
+
+ // skipped
+ if (res.score < 0) {
+ skipped += 1;
+ cases.push(' score N/A ');
+ } else {
+ failures += 1;
+ }
+
+ line = ' ');
+ lenJ = offenders.length;
+ for (j = 0; j < lenJ; j += 1) {
+ cases.push(' ' + safeXML(decodeURI(offenders[j])));
+ }
+ cases.push(' ');
+ } else {
+ cases.push(line + '/>');
+ }
+
+ cases.push(' ');
+ }
+ }
+
+ // xml
+ junit.push('');
+
+ // open test suites wrapper
+ junit.push('');
+
+ // open test suite w/ summary
+ line = ' ';
+ junit.push(line);
+
+ // concat test cases
+ junit = junit.concat(cases);
+
+ // close test suite
+ junit.push(' ');
+
+ // close test suites wrapper
+ junit.push(' ');
+
+ return {
+ failures: failures,
+ content: junit.join('\n')
+ };
+ },
+
+ /**
+ * Try to find a spaceid in the HTML document source.
+ * @param {YSLOW.ComponentSet} cset Component set.
+ * @return spaceID string
+ * @type string
+ */
+ getPageSpaceid: function (cset) {
+ var sHtml, aDelims, aTerminators, i, sDelim, i1, i2, spaceid,
+ reDigits = /^\d+$/,
+ aComponents = cset.getComponentsByType('doc');
+
+ if (aComponents[0] && typeof aComponents[0].body === 'string' && aComponents[0].body.length > 0) {
+ sHtml = aComponents[0].body; // assume the first "doc" is the original HTML doc
+ aDelims = ["%2fE%3d", "/S=", "SpaceID=", "?f=", " sid="]; // the beginning delimiter
+ aTerminators = ["%2fR%3d", ":", " ", "&", " "]; // the terminating delimiter
+ // Client-side counting (yzq) puts the spaceid in it as "/E=95810469/R=" but it's escaped!
+ for (i = 0; i < aDelims.length; i += 1) {
+ sDelim = aDelims[i];
+ if (-1 !== sHtml.indexOf(sDelim)) { // if the delimiter is present
+ i1 = sHtml.indexOf(sDelim) + sDelim.length; // skip over the delimiter
+ i2 = sHtml.indexOf(aTerminators[i], i1); // find the terminator
+ if (-1 !== i2 && (i2 - i1) < 15) { // if the spaceid is < 15 digits
+ spaceid = sHtml.substring(i1, i2);
+ if (reDigits.test(spaceid)) { // make sure it's all digits
+ return spaceid;
+ }
+ }
+ }
+ }
+ }
+
+ return "";
+ },
+
+ /**
+ * Dynamically add a stylesheet to the document.
+ * @param {String} url URL of the css file
+ * @param {Document} doc Documnet object
+ * @return CSS element
+ * @type HTMLElement
+ */
+ loadCSS: function (url, doc) {
+ var newCss;
+
+ if (!doc) {
+ YSLOW.util.dump('YSLOW.util.loadCSS: doc is not specified');
+ return '';
+ }
+
+ newCss = doc.createElement("link");
+ newCss.rel = "stylesheet";
+ newCss.type = "text\/css";
+ newCss.href = url;
+ doc.body.appendChild(newCss);
+
+ return newCss;
+ },
+
+ /**
+ * Open a link.
+ * @param {String} url URL of page to be opened.
+ */
+ openLink: function (url) {
+ if (YSLOW.util.Preference.getPref("browser.link.open_external") === 3) {
+ gBrowser.selectedTab = gBrowser.addTab(url);
+ } else {
+ window.open(url, " blank");
+ }
+ },
+
+ /**
+ * Sends a URL to smush.it for optimization
+ * Example usage:
+ * YSLOW.util.smushIt('http://smush.it/css/skin/screenshot.png', function(resp){alert(resp.dest)});
+ * This code alerts the path to the optimized result image.
+ *
+ * @param {String} imgurl URL of the image to optimize
+ * @param {Function} callback Callback function that accepts an object returned from smush.it
+ */
+ smushIt: function (imgurl, callback) {
+ var xhr,
+ smushurl = this.getSmushUrl(),
+ url = smushurl + '/ws.php?img=' + encodeURIComponent(imgurl),
+ req = YSLOW.util.getXHR();
+
+ req.open('GET', url, true);
+ req.onreadystatechange = function (e) {
+ xhr = (e ? e.target : req);
+ if (xhr.readyState === 4) {
+ callback(JSON.parse(xhr.responseText));
+ }
+ };
+ req.send(null);
+ },
+
+ /**
+ * Get SmushIt server URL.
+ * @return URL of SmushIt server.
+ * @type String
+ */
+ getSmushUrl: function () {
+ var g_default_smushit_url = 'http://www.smushit.com/ysmush.it';
+
+ return YSLOW.util.Preference.getPref('smushItURL', g_default_smushit_url) + '/';
+ },
+
+ /**
+ * Create new tab and return its document object
+ * @return document object of the new tab content.
+ * @type Document
+ */
+ getNewDoc: function () {
+ var generatedPage = null,
+ request = new XMLHttpRequest();
+
+ getBrowser().selectedTab = getBrowser().addTab('about:blank');
+ generatedPage = window;
+ request.open("get", "about:blank", false);
+ request.overrideMimeType('text/html');
+ request.send(null);
+
+ return generatedPage.content.document;
+ },
+
+ /**
+ * Make absolute url.
+ * @param url
+ * @param base href
+ * @return absolute url built with base href.
+ */
+ makeAbsoluteUrl: function (url, baseHref) {
+ var hostIndex, path, lpath, protocol;
+
+ if (typeof url === 'string' && baseHref) {
+ hostIndex = baseHref.indexOf('://');
+ protocol = baseHref.slice(0, 4);
+ if (url.indexOf('://') < 0 && (protocol === 'http' ||
+ protocol === 'file')) {
+ // This is a relative url
+ if (url.slice(0, 1) === '/') {
+ // absolute path
+ path = baseHref.indexOf('/', hostIndex + 3);
+ if (path > -1) {
+ url = baseHref.slice(0, path) + url;
+ } else {
+ url = baseHref + url;
+ }
+ } else {
+ // relative path
+ lpath = baseHref.lastIndexOf('/');
+ if (lpath > hostIndex + 3) {
+ url = baseHref.slice(0, lpath + 1) + url;
+ } else {
+ url = baseHref + '/' + url;
+ }
+ }
+ }
+ }
+
+ return url;
+ },
+
+ /**
+ * Prevent event default action
+ * @param {Object} event the event to prevent default action from
+ */
+ preventDefault: function (event) {
+ if (typeof event.preventDefault === 'function') {
+ event.preventDefault();
+ } else {
+ event.returnValue = false;
+ }
+ },
+
+ /**
+ * String Trim
+ * @param string s the string to remove trail and header spaces
+ */
+ trim: function (s) {
+ try {
+ return (s && s.trim) ? s.trim() : s.replace(/^\s+|\s+$/g, '');
+ } catch (e) {
+ return s;
+ }
+ },
+
+ /**
+ * Add Event Listener
+ * @param HTMLElement el the element to add an event listener
+ * @param string ev the event name to be added
+ * @param function fn the function to be invoked by event listener
+ */
+ addEventListener: function (el, ev, fn) {
+ var util = YSLOW.util;
+
+ if (el.addEventListener) {
+ util.addEventListener = function (el, ev, fn) {
+ el.addEventListener(ev, fn, false);
+ };
+ } else if (el.attachEvent) {
+ util.addEventListener = function (el, ev, fn) {
+ el.attachEvent('on' + ev, fn);
+ };
+ } else {
+ util.addEventListener = function (el, ev, fn) {
+ el['on' + ev] = fn;
+ };
+ }
+ util.addEventListener(el, ev, fn);
+ },
+
+ /**
+ * Remove Event Listener
+ * @param HTMLElement el the element to remove event listener from
+ * @param string ev the event name to be removed
+ * @param function fn the function invoked by the removed listener
+ */
+ removeEventListener: function (el, ev, fn) {
+ var util = YSLOW.util;
+
+ if (el.removeEventListener) {
+ util.removeEventListener = function (el, ev, fn) {
+ el.removeEventListener(ev, fn, false);
+ };
+ } else if (el.detachEvent) {
+ util.removeEventListener = function (el, ev, fn) {
+ el.detachEvent('on' + ev, fn);
+ };
+ } else {
+ util.removeEventListener = function (el, ev, fn) {
+ delete el['on' + ev];
+ };
+ }
+ util.removeEventListener(el, ev, fn);
+ },
+
+ /**
+ * Normalize currentTarget
+ * @param evt the event received
+ * @return HTMLElement the normilized currentTarget
+ */
+ getCurrentTarget: function (evt) {
+ return evt.currentTarget || evt.srcElement;
+ },
+
+ /**
+ * Normalize target
+ * @param evt the event received
+ * @return HTMLElement the normilized target
+ */
+ getTarget: function (evt) {
+ return evt.target || evt.srcElement;
+ },
+
+ /**
+ * Get all inline elements (style and script) from a document
+ * @param doc (optional) the document to get all inline elements
+ * @param head (optional) the head node to get inline elements, ignores doc
+ * @param body (optional) the body node to get inline elements, ignores doc
+ * @return object with scripts and styles arrays with the following info:
+ * containerNode: either head or body
+ * body: the innerHTML content
+ */
+ getInlineTags: function (doc, head, body) {
+ var styles, scripts,
+
+ loop = function (node, tag, contentNode) {
+ var i, len, els, el,
+ objs = [];
+
+ if (!node) {
+ return objs;
+ }
+
+ els = node.getElementsByTagName(tag);
+ for (i = 0, len = els.length; i < len; i += 1) {
+ el = els[i];
+ if (!el.src) {
+ objs.push({
+ contentNode: contentNode,
+ body: el.innerHTML
+ });
+ }
+ }
+
+ return objs;
+ };
+
+ head = head || (doc && doc.getElementsByTagName('head')[0]);
+ body = body || (doc && doc.getElementsByTagName('body')[0]);
+
+ styles = loop(head, 'style', 'head');
+ styles = styles.concat(loop(body, 'style', 'body'));
+ scripts = loop(head, 'script', 'head');
+ scripts = scripts.concat(loop(body, 'script', 'body'));
+
+ return {
+ styles: styles,
+ scripts: scripts
+ };
+ },
+
+ /**
+ * Count all DOM elements from a node
+ * @param node the root node to count all DOM elements from
+ * @return number of DOM elements found on given node
+ */
+ countDOMElements: function (node) {
+ return (node && node.getElementsByTagName('*').length) || 0;
+ },
+
+ /**
+ * Get cookies from a document
+ * @param doc the document to get the cookies from
+ * @return the cookies string
+ */
+ getDocCookies: function (doc) {
+ return (doc && doc.cookie) || '';
+ },
+
+ /**
+ * identifies injected elements (js, css, iframe, flash, image)
+ * @param doc the document to create/manipulate dom elements
+ * @param comps the component set components
+ * @param body the root (raw) document body (html)
+ * @return the same components with injected info
+ */
+ setInjected: function (doc, comps, body) {
+ var i, len, els, el, src, comp, found, div,
+ nodes = {};
+
+ if (!body) {
+ return comps;
+ }
+
+ // har uses a temp div already, reuse it
+ if (typeof doc.createElement === 'function') {
+ div = doc.createElement('div');
+ div.innerHTML = body;
+ } else {
+ div = doc;
+ }
+
+ // js
+ els = div.getElementsByTagName('script');
+ for (i = 0, len = els.length; i < len; i += 1) {
+ el = els[i];
+ src = el.src || el.getAttribute('src');
+ if (src) {
+ nodes[src] = {
+ defer: el.defer || el.getAttribute('defer'),
+ async: el.async || el.getAttribute('async')
+ };
+ }
+ }
+
+ // css
+ els = div.getElementsByTagName('link');
+ for (i = 0, len = els.length; i < len; i += 1) {
+ el = els[i];
+ src = el.href || el.getAttribute('href');
+ if (src && (el.rel === 'stylesheet' || el.type === 'text/css')) {
+ nodes[src] = 1;
+ }
+ }
+
+ // iframe
+ els = div.getElementsByTagName('iframe');
+ for (i = 0, len = els.length; i < len; i += 1) {
+ el = els[i];
+ src = el.src || el.getAttribute('src');
+ if (src) {
+ nodes[src] = 1;
+ }
+ }
+
+ // flash
+ els = div.getElementsByTagName('embed');
+ for (i = 0, len = els.length; i < len; i += 1) {
+ el = els[i];
+ src = el.src || el.getAttribute('src');
+ if (src) {
+ nodes[src] = 1;
+ }
+ }
+ els = div.getElementsByTagName('param');
+ for (i = 0, len = els.length; i < len; i += 1) {
+ el = els[i];
+ src = el.value || el.getAttribute('value');
+ if (src) {
+ nodes[src] = 1;
+ }
+ }
+
+ // image
+ els = div.getElementsByTagName('img');
+ for (i = 0, len = els.length; i < len; i += 1) {
+ el = els[i];
+ src = el.src || el.getAttribute('src');
+ if (src) {
+ nodes[src] = 1;
+ }
+ }
+
+ // loop components and look it up on nodes
+ // if not found then component was injected
+ // for js, set defer and async attributes
+ for (i = 0, len = comps.length; i < len; i += 1) {
+ comp = comps[i];
+ if (comp.type === 'js' || comp.type === 'css' ||
+ comp.type === 'flash' || comp.type === 'flash' ||
+ comp.type === 'image') {
+ found = nodes[comp.url];
+ comp.injected = !found;
+ if (comp.type === 'js' && found) {
+ comp.defer = found.defer;
+ comp.async = found.async;
+ }
+ }
+ }
+
+ return comps;
+ },
+
+ // default setTimeout, FF overrides this with proprietary Mozilla timer
+ setTimer: function (callback, delay) {
+ setTimeout(callback, delay);
+ }
+};
+
+/**
+ * Class that implements the observer pattern.
+ *
+ * Oversimplified usage:
+ *
+ * // subscribe
+ * YSLOW.util.event.addListener('martiansAttack', alert);
+ * // fire the event
+ * YSLOW.util.event.fire('martiansAttack', 'panic!');
+ *
+ *
+ * More real life usage
+ *
+ * var myobj = {
+ * default_action: alert,
+ * panic: function(event) {
+ * this.default_action.call(null, event.message);
+ * }
+ * };
+ *
+ * // subscribe
+ * YSLOW.util.event.addListener('martiansAttack', myobj.panic, myobj);
+ * // somewhere someone fires the event
+ * YSLOW.util.event.fire('martiansAttack', {date: new Date(), message: 'panic!'});
+ *
+ *
+ * @namespace YSLOW.util
+ * @class event
+ * @static
+ */
+YSLOW.util.event = {
+ /**
+ * Hash of subscribers where the key is the event name and the value is an array of callbacks-type objects
+ * The callback objects have keys "callback" which is the function to be called and "that" which is the value
+ * to be assigned to the "this" object when the function is called
+ */
+ subscribers: {},
+
+ /**
+ * Adds a new 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
+ */
+ addListener: function (eventName, callback, that) {
+ var subs = this.subscribers,
+ subscribers = subs[eventName];
+
+ if (!subscribers) {
+ subscribers = subs[eventName] = [];
+ }
+ subscribers.push({
+ callback: callback,
+ that: that
+ });
+ },
+
+ /**
+ * Removes a 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)
+ */
+ removeListener: function (eventName, callback) {
+ var i,
+ subscribers = this.subscribers[eventName],
+ len = (subscribers && subscribers.length) || 0;
+
+ for (i = 0; i < len; i += 1) {
+ if (subscribers[i].callback === callback) {
+ subscribers.splice(i, 1);
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * Fires the event
+ *
+ * @param {String} event_nama Name of the event
+ * @param {Object} event_object Any object that will be passed to the subscribers, can be anything
+ */
+ fire: function (event_name, event_object) {
+ var i, listener;
+
+ if (typeof this.subscribers[event_name] === 'undefined') {
+ return false;
+ }
+
+ for (i = 0; i < this.subscribers[event_name].length; i += 1) {
+ listener = this.subscribers[event_name][i];
+ try {
+ listener.callback.call(listener.that, event_object);
+ } catch (e) {}
+ }
+
+ return true;
+ }
+
+};
+
+/**
+ * Class that implements setting and unsetting preferences
+ *
+ * @namespace YSLOW.util
+ * @class Preference
+ * @static
+ *
+ */
+YSLOW.util.Preference = {
+
+ /**
+ * @private
+ */
+ nativePref: null,
+
+ /**
+ * Register native preference mechanism.
+ */
+ registerNative: function (o) {
+ this.nativePref = o;
+ },
+
+ /**
+ * Get Preference with default value. If the preference does not exist,
+ * return the passed default_value.
+ * @param {String} name name of preference
+ * @return preference value or default value.
+ */
+ getPref: function (name, default_value) {
+ if (this.nativePref) {
+ return this.nativePref.getPref(name, default_value);
+ }
+ return default_value;
+ },
+
+ /**
+ * Get child preference list in branch.
+ * @param {String} branch_name
+ * @return array of preference values.
+ * @type Array
+ */
+ getPrefList: function (branch_name, default_value) {
+ if (this.nativePref) {
+ return this.nativePref.getPrefList(branch_name, default_value);
+ }
+ return default_value;
+ },
+
+ /**
+ * Set Preference with passed value.
+ * @param {String} name name of preference
+ * @param {value type} value value to be used to set the preference
+ */
+ setPref: function (name, value) {
+ if (this.nativePref) {
+ this.nativePref.setPref(name, value);
+ }
+ },
+
+ /**
+ * Delete Preference with passed name.
+ * @param {String} name name of preference to be deleted
+ */
+ deletePref: function (name) {
+ if (this.nativePref) {
+ this.nativePref.deletePref(name);
+ }
+ }
+};
+/**
+ * Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+ * Copyright (c) 2013, Marcel Duran and other contributors. 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 */
+
+/**
+ * A class that collects all in-product text.
+ * @namespace YSLOW
+ * @class doc
+ * @static
+ */
+YSLOW.doc = {
+
+ tools_desc: undefined,
+
+ view_names: {},
+
+ splash: {},
+
+ rules: {},
+
+ tools: {},
+
+ components_legend: {},
+
+ addRuleInfo: function (id, name, info) {
+ if (typeof id === "string" && typeof name === "string" && typeof info === "string") {
+ this.rules[id] = {
+ 'name': name,
+ 'info': info
+ };
+ }
+ },
+
+ addToolInfo: function (id, name, info) {
+ if (typeof id === "string" && typeof name === "string" && typeof info === "string") {
+ this.tools[id] = {
+ 'name': name,
+ 'info': info
+ };
+ }
+ }
+
+};
+
+//
+// Rules text
+//
+YSLOW.doc.addRuleInfo('ynumreq', 'Make fewer HTTP requests', 'Decreasing the number of components on a page reduces the number of HTTP requests required to render the page, resulting in faster page loads. Some ways to reduce the number of components include: combine files, combine multiple scripts into one script, combine multiple CSS files into one style sheet, and use CSS Sprites and image maps.');
+
+YSLOW.doc.addRuleInfo('ycdn', 'Use a Content Delivery Network (CDN)', 'User proximity to web servers impacts response times. Deploying content across multiple geographically dispersed servers helps users perceive that pages are loading faster.');
+
+YSLOW.doc.addRuleInfo('yexpires', 'Add Expires headers', 'Web pages are becoming increasingly complex with more scripts, style sheets, images, and Flash on them. A first-time visit to a page may require several HTTP requests to load all the components. By using Expires headers these components become cacheable, which avoids unnecessary HTTP requests on subsequent page views. Expires headers are most often associated with images, but they can and should be used on all page components including scripts, style sheets, and Flash.');
+
+YSLOW.doc.addRuleInfo('ycompress', 'Compress components with gzip', 'Compression reduces response times by reducing the size of the HTTP response. Gzip is the most popular and effective compression method currently available and generally reduces the response size by about 70%. Approximately 90% of today\'s Internet traffic travels through browsers that claim to support gzip.');
+
+YSLOW.doc.addRuleInfo('ycsstop', 'Put CSS at top', 'Moving style sheets to the document HEAD element helps pages appear to load quicker since this allows pages to render progressively.');
+
+YSLOW.doc.addRuleInfo('yjsbottom', 'Put JavaScript at bottom', 'JavaScript scripts block parallel downloads; that is, when a script is downloading, the browser will not start any other downloads. To help the page load faster, move scripts to the bottom of the page if they are deferrable.');
+
+YSLOW.doc.addRuleInfo('yexpressions', 'Avoid CSS expressions', 'CSS expressions (supported in IE beginning with Version 5) are a powerful, and dangerous, way to dynamically set CSS properties. These expressions are evaluated frequently: when the page is rendered and resized, when the page is scrolled, and even when the user moves the mouse over the page. These frequent evaluations degrade the user experience.');
+
+YSLOW.doc.addRuleInfo('yexternal', 'Make JavaScript and CSS external', 'Using external JavaScript and CSS files generally produces faster pages because the files are cached by the browser. JavaScript and CSS that are inlined in HTML documents get downloaded each time the HTML document is requested. This reduces the number of HTTP requests but increases the HTML document size. On the other hand, if the JavaScript and CSS are in external files cached by the browser, the HTML document size is reduced without increasing the number of HTTP requests.');
+
+YSLOW.doc.addRuleInfo('ydns', 'Reduce DNS lookups', 'The Domain Name System (DNS) maps hostnames to IP addresses, just like phonebooks map people\'s names to their phone numbers. When you type URL www.yahoo.com into the browser, the browser contacts a DNS resolver that returns the server\'s IP address. DNS has a cost; typically it takes 20 to 120 milliseconds for it to look up the IP address for a hostname. The browser cannot download anything from the host until the lookup completes.');
+
+YSLOW.doc.addRuleInfo('yminify', 'Minify JavaScript and CSS', 'Minification removes unnecessary characters from a file to reduce its size, thereby improving load times. When a file is minified, comments and unneeded white space characters (space, newline, and tab) are removed. This improves response time since the size of the download files is reduced.');
+
+YSLOW.doc.addRuleInfo('yredirects', 'Avoid URL redirects', 'URL redirects are made using HTTP status codes 301 and 302. They tell the browser to go to another location. Inserting a redirect between the user and the final HTML document delays everything on the page since nothing on the page can be rendered and no components can be downloaded until the HTML document arrives.');
+
+YSLOW.doc.addRuleInfo('ydupes', 'Remove duplicate JavaScript and CSS', 'Duplicate JavaScript and CSS files hurt performance by creating unnecessary HTTP requests (IE only) and wasted JavaScript execution (IE and Firefox). In IE, if an external script is included twice and is not cacheable, it generates two HTTP requests during page loading. Even if the script is cacheable, extra HTTP requests occur when the user reloads the page. In both IE and Firefox, duplicate JavaScript scripts cause wasted time evaluating the same scripts more than once. This redundant script execution happens regardless of whether the script is cacheable.');
+
+YSLOW.doc.addRuleInfo('yetags', 'Configure entity tags (ETags)', 'Entity tags (ETags) are a mechanism web servers and the browser use to determine whether a component in the browser\'s cache matches one on the origin server. Since ETags are typically constructed using attributes that make them unique to a specific server hosting a site, the tags will not match when a browser gets the original component from one server and later tries to validate that component on a different server.');
+
+YSLOW.doc.addRuleInfo('yxhr', 'Make AJAX cacheable', 'One of AJAX\'s benefits is it provides instantaneous feedback to the user because it requests information asynchronously from the backend web server. However, using AJAX does not guarantee the user will not wait for the asynchronous JavaScript and XML responses to return. Optimizing AJAX responses is important to improve performance, and making the responses cacheable is the best way to optimize them.');
+
+YSLOW.doc.addRuleInfo('yxhrmethod', 'Use GET for AJAX requests', 'When using the XMLHttpRequest object, the browser implements POST in two steps: (1) send the headers, and (2) send the data. It is better to use GET instead of POST since GET sends the headers and the data together (unless there are many cookies). IE\'s maximum URL length is 2 KB, so if you are sending more than this amount of data you may not be able to use GET.');
+
+YSLOW.doc.addRuleInfo('ymindom', 'Reduce the number of DOM elements', 'A complex page means more bytes to download, and it also means slower DOM access in JavaScript. Reduce the number of DOM elements on the page to improve performance.');
+
+YSLOW.doc.addRuleInfo('yno404', 'Avoid HTTP 404 (Not Found) error', 'Making an HTTP request and receiving a 404 (Not Found) error is expensive and degrades the user experience. Some sites have helpful 404 messages (for example, "Did you mean ...?"), which may assist the user, but server resources are still wasted.');
+
+YSLOW.doc.addRuleInfo('ymincookie', 'Reduce cookie size', 'HTTP cookies are used for authentication, personalization, and other purposes. Cookie information is exchanged in the HTTP headers between web servers and the browser, so keeping the cookie size small minimizes the impact on response time.');
+
+YSLOW.doc.addRuleInfo('ycookiefree', 'Use cookie-free domains', 'When the browser requests a static image and sends cookies with the request, the server ignores the cookies. These cookies are unnecessary network traffic. To workaround this problem, make sure that static components are requested with cookie-free requests by creating a subdomain and hosting them there.');
+
+YSLOW.doc.addRuleInfo('ynofilter', 'Avoid AlphaImageLoader filter', 'The IE-proprietary AlphaImageLoader filter attempts to fix a problem with semi-transparent true color PNG files in IE versions less than Version 7. However, this filter blocks rendering and freezes the browser while the image is being downloaded. Additionally, it increases memory consumption. The problem is further multiplied because it is applied per element, not per image.');
+
+YSLOW.doc.addRuleInfo('yimgnoscale', 'Do not scale images in HTML', 'Web page designers sometimes set image dimensions by using the width and height attributes of the HTML image element. Avoid doing this since it can result in images being larger than needed. For example, if your page requires image myimg.jpg which has dimensions 240x720 but displays it with dimensions 120x360 using the width and height attributes, then the browser will download an image that is larger than necessary.');
+
+YSLOW.doc.addRuleInfo('yfavicon', 'Make favicon small and cacheable', 'A favicon is an icon associated with a web page; this icon resides in the favicon.ico file in the server\'s root. Since the browser requests this file, it needs to be present; if it is missing, the browser returns a 404 error (see "Avoid HTTP 404 (Not Found) error" above). Since favicon.ico resides in the server\'s root, each time the browser requests this file, the cookies for the server\'s root are sent. Making the favicon small and reducing the cookie size for the server\'s root cookies improves performance for retrieving the favicon. Making favicon.ico cacheable avoids frequent requests for it.');
+
+YSLOW.doc.addRuleInfo('yemptysrc', 'Avoid empty src or href', 'You may expect a browser to do nothing when it encounters an empty image src. However, it is not the case in most browsers. IE makes a request to the directory in which the page is located; Safari, Chrome, Firefox 3 and earlier make a request to the actual page itself. This behavior could possibly corrupt user data, waste server computing cycles generating a page that will never be viewed, and in the worst case, cripple your servers by sending a large amount of unexpected traffic.');
+
+YSLOW.doc.addRuleInfo('thirdpartyasyncjs','Load third party javascript asynchronously','Always load third party javascript asynchronously. Third parties that will be checked are twitter, facebook, google (api, analythics, ajax), linkedin, disqus, pinterest & jquery.');
+YSLOW.doc.addRuleInfo('cssprint','Avoid loading specific css for print','Loading a specific stylesheet for print, can block rendering in your browser (depending on browser version) and will for almost all browsers, block the onload event to fire (even though the print stylesheet is not even used!).');
+YSLOW.doc.addRuleInfo('cssinheaddomain','Load CSS in head from document domain','CSS files inside of HEAD should be loaded from the same domain as the main document, in order to avoid DNS lookups, because you want to have the HEAD part of the page finished as fast as possible, for the browser to be abe to start render the page. This is extra important for mobile.');
+YSLOW.doc.addRuleInfo('syncjsinhead','Never load JS synchronously in head','Javascript files should never be loaded synchronously in HEAD, because it will block the rendering of the page.');
+YSLOW.doc.addRuleInfo('avoidfont','Avoid use of web fonts','Avoid use of webfonts because they will decrease the performance of the page.');
+YSLOW.doc.addRuleInfo('totalrequests','Reduce number of total requests','Avoid to have too many requests on your page. The more requests, the slower the page will be for the end user.');
+YSLOW.doc.addRuleInfo('expiresmod','Have expire headers for static components','By adding HTTP expires headers to your static files, the files will be cached in the end users browser.');
+YSLOW.doc.addRuleInfo('spof','Frontend single point of failure',' A page can be stopped to be loaded in the browser, if a single script, css and in some cases a font couldn\'t be fetched or loading slow (the white screen of death), and that is something you really want to avoid. Never load 3rd party components inside of head! One important note, right now this rule treats domain and subdomains as ok, that match the document domain, all other domains is treated as a SPOF. The score is calculated like this: Synchronously loaded javascripts inside of head, hurts you the most, then CSS files inside of head hurts a little less, font face inside of css files further less, and least inline font face files. One rule SPOF rule missing is the IE specific feature, that a font face will be SPOF if a script is requested before the font face file.');
+YSLOW.doc.addRuleInfo('nodnslookupswhenfewrequests','Avoid DNS lookups when a page has few requests','If you have few requests on a page, they should all be on the same domain to avoid DNS lookups, because the lookups will cost much.');
+YSLOW.doc.addRuleInfo('inlinecsswhenfewrequest','Do not load css files when the page has few request','When a page has few requests (or actually maybe always if you dont have a massive amount of css), it is better to inline the css, to make the page to start render as early as possible.');
+YSLOW.doc.addRuleInfo('criticalpath', 'Avoid slowing down the critical rendering path','Every request fetched inside of HEAD, will postpone the rendering of the page! Do not load javascript synchronously inside of head, load files from the same domain as the main document (to avoid DNS lookups) and inline CSS for a really fast rendering path. The scoring system for this rule, will give you minus score for synchronously loaded javascript inside of head, css files requested inside of head and minus score for every DNS lookup inside of head.');
+YSLOW.doc.addRuleInfo('textcontent','Have a reasonable percentage of textual content compared to the rest of the page','Make sure the amount of HTML elements are too many compared to text content.');
+YSLOW.doc.addRuleInfo('noduplicates', 'Remove duplicate JavaScript and CSS', 'Duplicate JavaScript and CSS files hurt performance by creating unnecessary HTTP requests (IE only) and wasted JavaScript execution (IE and Firefox). In IE, if an external script is included twice and is not cacheable, it generates two HTTP requests during page loading. Even if the script is cacheable, extra HTTP requests occur when the user reloads the page. In both IE and Firefox, duplicate JavaScript scripts cause wasted time evaluating the same scripts more than once. This redundant script execution happens regardless of whether the script is cacheable.');
+YSLOW.doc.addRuleInfo('cssnumreq','Make fewer HTTP requests for CSS files','Decreasing the number of components on a page reduces the number of HTTP requests required to render the page, resulting in faster page loads. Combine your CSS files into as few as possible.');
+YSLOW.doc.addRuleInfo('cssimagesnumreq','Make fewer HTTP requests for CSS image files','Decreasing the number of components on a page reduces the number of HTTP requests required to render the page, resulting in faster page loads. Combine your CSS images files into as few CSS sprites as possible.');
+YSLOW.doc.addRuleInfo('jsnumreq','Make fewer synchronously HTTP requests for Javascript files','Decreasing the number of components on a page reduces the number of HTTP requests required to render the page, resulting in faster page loads. Combine your Javascript files into as few as possible (and load them asynchronously).');
+YSLOW.doc.addRuleInfo('longexpirehead','Have expires headers equals or longer than one year','Having really long cache headers are beneficial for caching.');
+YSLOW.doc.addRuleInfo('mindom', 'Reduce the number of DOM elements', 'A complex page means more bytes to download, and it also means slower DOM access in JavaScript. Reduce the number of DOM elements on the page to improve performance.');
+YSLOW.doc.addRuleInfo('thirdpartyversions','Always use latest versions of third party javascripts','Always use the latest & greatest versions of third party javascripts, this is really important for JQuery, since the latest versions is always faster & better.');
+YSLOW.doc.addRuleInfo('avoidscalingimages', 'Never scale images in HTML', 'Images should always be sent with the correct size else the browser will download an image that is larger than necessary. This is more important today with responsive web design, meaning you want to avoid downloading non scaled images to a mobile phone or tablet. Note: This rule doesn\t check images with size 0 (images in carousels etc), so they will be missed by the rule.The rule also skip images where the difference between the sizes are less than a configurable value (default 100 pixels).');
+
+YSLOW.doc.addRuleInfo('ttfb','Keep the time to first byte low','The time to first byte should be as low as possible, so that the browser can start processing the content.');
+YSLOW.doc.addRuleInfo('redirects','Never do redirects','Redirects is bad for performance, specially for mobile.');
+
+//
+// Tools text
+//
+YSLOW.doc.tools_desc = 'Click on the tool name to launch the tool.';
+
+YSLOW.doc.addToolInfo('jslint', 'JSLint', 'Run JSLint on all JavaScript code in this document');
+
+YSLOW.doc.addToolInfo('alljs', 'All JS', 'Show all JavaScript code in this document');
+
+YSLOW.doc.addToolInfo('jsbeautified', 'All JS Beautified', 'Show all JavaScript code in this document in an easy to read format');
+
+YSLOW.doc.addToolInfo('jsminified', 'All JS Minified', 'Show all JavaScript code in this document in a minified (no comments or white space) format');
+
+YSLOW.doc.addToolInfo('allcss', 'All CSS', 'Show all CSS code in this document');
+
+YSLOW.doc.addToolInfo('cssmin', 'YUI CSS Compressor', 'Show all CSS code in the document in a minified format');
+
+YSLOW.doc.addToolInfo('smushItAll', 'All Smush.it™', 'Run Smush.it™ on all image components in this document');
+
+YSLOW.doc.addToolInfo('printableview', 'Printable View', 'Show a printable view of grades, component lists, and statistics');
+
+//
+// Splash text
+//
+YSLOW.doc.splash.title = 'Grade your web pages with YSlow';
+
+YSLOW.doc.splash.content = {
+ 'header': 'YSlow gives you:',
+ 'text': ' Grade based on the performance of the page (you can define your own ruleset) Summary of the page components Chart with statistics Tools for analyzing performance, including Smush.it™ and JSLint '
+};
+
+YSLOW.doc.splash.more_info = 'Learn more about YSlow and the Yahoo! Developer Network';
+
+//
+// Rule Settings
+//
+YSLOW.doc.rulesettings_desc = 'Choose which ruleset (Sitespeed, YSlow V2, Classic V1, or Small Site/Blog) best fits your specific needs. Or create a new set and click Save as... to save it.';
+
+//
+// Components table legend
+//
+YSLOW.doc.components_legend.beacon = 'type column indicates the component is loaded after window onload event';
+YSLOW.doc.components_legend.after_onload = 'denotes 1x1 pixels image that may be image beacon';
+
+//
+// View names
+//
+YSLOW.doc.view_names = {
+ grade: 'Grade',
+ components: 'Components',
+ stats: 'Statistics',
+ tools: 'Tools',
+ rulesetedit: 'Rule Settings'
+};
+
+// copyright text
+YSLOW.doc.copyright = 'Copyright © ' + (new Date()).getFullYear() + ' Yahoo! Inc. All rights reserved.';
+/**
+ * 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, regexp: true, continue: true, plusplus: true, bitwise: true, newcap: true, type: true, unparam: true, maxerr: 50, indent: 4*/
+
+/**
+ *
+ * Example of a rule object:
+ *
+ *
+ * YSLOW.registerRule({
+ *
+ * id: 'myrule',
+ * name: 'Never say never',
+ * url: 'http://never.never/never.html',
+ * info: 'Short description of the rule',
+ *
+ * config: {
+ * when: 'ever'
+ * },
+ *
+ * lint: function(doc, components, config) {
+ * return {
+ * score: 100,
+ * message: "Did you just say never?",
+ * components: []
+ * };
+ * }
+ * });
+
+ */
+
+//
+// 3/2/2009
+// Centralize all name and info of builtin tool to YSLOW.doc class.
+//
+YSLOW.registerRule({
+ id: 'ynumreq',
+ //name: 'Make fewer HTTP requests',
+ url: 'http://developer.yahoo.com/performance/rules.html#num_http',
+ category: ['content'],
+
+ config: {
+ max_js: 3,
+ // the number of scripts allowed before we start penalizing
+ points_js: 4,
+ // penalty points for each script over the maximum
+ max_css: 2,
+ // number of external stylesheets allowed before we start penalizing
+ points_css: 4,
+ // penalty points for each external stylesheet over the maximum
+ max_cssimages: 6,
+ // // number of background images allowed before we start penalizing
+ points_cssimages: 3 // penalty points for each bg image over the maximum
+ },
+
+ lint: function (doc, cset, config) {
+ var js = cset.getComponentsByType('js').length - config.max_js,
+ css = cset.getComponentsByType('css').length - config.max_css,
+ cssimg = cset.getComponentsByType('cssimage').length - config.max_cssimages,
+ score = 100,
+ messages = [];
+
+ if (js > 0) {
+ score -= js * config.points_js;
+ messages[messages.length] = 'This page has ' + YSLOW.util.plural('%num% external Javascript script%s%', (js + config.max_js)) + '. Try combining them into one.';
+ }
+ if (css > 0) {
+ score -= css * config.points_css;
+ messages[messages.length] = 'This page has ' + YSLOW.util.plural('%num% external stylesheet%s%', (css + config.max_css)) + '. Try combining them into one.';
+ }
+ if (cssimg > 0) {
+ score -= cssimg * config.points_cssimages;
+ messages[messages.length] = 'This page has ' + YSLOW.util.plural('%num% external background image%s%', (cssimg + config.max_cssimages)) + '. Try combining them with CSS sprites.';
+ }
+
+ return {
+ score: score,
+ message: messages.join('\n'),
+ components: []
+ };
+ }
+});
+
+YSLOW.registerRule({
+ id: 'ycdn',
+ //name: 'Use a CDN',
+ url: 'http://developer.yahoo.com/performance/rules.html#cdn',
+ category: ['server'],
+
+ config: {
+ // how many points to take out for each component not on CDN
+ points: 10,
+ // array of regexps that match CDN-ed components
+ patterns: [
+ '^([^\\.]*)\\.([^\\.]*)\\.yimg\\.com/[^/]*\\.yimg\\.com/.*$',
+ '^([^\\.]*)\\.([^\\.]*)\\.yimg\\.com/[^/]*\\.yahoo\\.com/.*$',
+ '^sec.yimg.com/',
+ '^a248.e.akamai.net',
+ '^[dehlps].yimg.com',
+ '^(ads|cn|mail|maps|s1).yimg.com',
+ '^[\\d\\w\\.]+.yimg.com',
+ '^a.l.yimg.com',
+ '^us.(js|a)2.yimg.com',
+ '^yui.yahooapis.com',
+ '^adz.kr.yahoo.com',
+ '^img.yahoo.co.kr',
+ '^img.(shopping|news|srch).yahoo.co.kr',
+ '^pimg.kr.yahoo.com',
+ '^kr.img.n2o.yahoo.com',
+ '^s3.amazonaws.com',
+ '^(www.)?google-analytics.com',
+ '.cloudfront.net', //Amazon CloudFront
+ '.ak.fbcdn.net', //Facebook images ebeded
+ 'platform.twitter.com', //Twitter widget - Always via a CDN
+ 'cdn.api.twitter.com', //Twitter API calls, served via Akamai
+ 'apis.google.com', //Google's API Hosting
+ '.akamaihd.net', //Akamai - Facebook uses this for SSL assets
+ '.rackcdn.com' //Generic RackSpace CloudFiles CDN
+ ],
+ // array of regexps that will be treated as exception.
+ exceptions: [
+ '^chart.yahoo.com',
+ '^(a1|f3|f5|f3c|f5c).yahoofs.com', // Images for 360 and YMDB
+ '^us.(a1c|f3).yahoofs.com' // Personals photos
+ ],
+ // array of regexps that match CDN Server HTTP headers
+ servers: [
+ 'cloudflare-nginx' // not using ^ and $ due to invisible
+ ],
+ // which component types should be on CDN
+ types: ['js', 'css', 'image', 'cssimage', 'flash', 'favicon']
+ },
+
+ lint: function (doc, cset, config) {
+ var i, j, url, re, match, hostname,
+ offender, len, lenJ, comp, patterns, headers,
+ score = 100,
+ offenders = [],
+ exceptions = [],
+ message = '',
+ util = YSLOW.util,
+ plural = util.plural,
+ kbSize = util.kbSize,
+ getHostname = util.getHostname,
+ docDomain = getHostname(cset.doc_comp.url),
+ comps = cset.getComponentsByType(config.types),
+ userCdns = util.Preference.getPref('cdnHostnames', ''),
+ hasPref = util.Preference.nativePref;
+
+ // array of custom cdns
+ if (userCdns) {
+ userCdns = userCdns.split(',');
+ }
+
+ for (i = 0, len = comps.length; i < len; i += 1) {
+ comp = comps[i];
+ url = comp.url;
+ hostname = getHostname(url);
+ headers = comp.headers;
+
+ // ignore /favicon.ico
+ if (comp.type === 'favicon' && hostname === docDomain) {
+ continue;
+ }
+
+ // experimental custom header, use lowercase
+ match = headers['x-cdn'] || headers['x-amz-cf-id'] || headers['x-edge-location'] || headers['powered-by-chinacache'];
+ if (match) {
+ continue;
+ }
+
+ // by hostname
+ patterns = config.patterns;
+ for (j = 0, lenJ = patterns.length; j < lenJ; j += 1) {
+ re = new RegExp(patterns[j]);
+ if (re.test(hostname)) {
+ match = 1;
+ break;
+ }
+ }
+ // by custom hostnames
+ if (userCdns) {
+ for (j = 0, lenJ = userCdns.length; j < lenJ; j += 1) {
+ re = new RegExp(util.trim(userCdns[j]));
+ if (re.test(hostname)) {
+ match = 1;
+ break;
+ }
+ }
+ }
+
+ if (!match) {
+ // by Server HTTP header
+ patterns = config.servers;
+ for (j = 0, lenJ = patterns.length; j < lenJ; j += 1) {
+ re = new RegExp(patterns[j]);
+ if (re.test(headers.server)) {
+ match = 1;
+ break;
+ }
+ }
+ if (!match) {
+ // by exception
+ patterns = config.exceptions;
+ for (j = 0, lenJ = patterns.length; j < lenJ; j += 1) {
+ re = new RegExp(patterns[j]);
+ if (re.test(hostname)) {
+ exceptions.push(comp);
+ match = 1;
+ break;
+ }
+ }
+ if (!match) {
+ offenders.push(comp);
+ }
+ }
+ }
+ }
+
+ score -= offenders.length * config.points;
+
+ offenders.concat(exceptions);
+
+ if (offenders.length > 0) {
+ message = plural('There %are% %num% static component%s% ' +
+ 'that %are% not on CDN. ', offenders.length);
+ }
+ if (exceptions.length > 0) {
+ message += plural('There %are% %num% component%s% that %are% not ' +
+ 'on CDN, but %are% exceptions:', exceptions.length) + '';
+ for (i = 0, len = offenders.length; i < len; i += 1) {
+ message += '' + util.prettyAnchor(exceptions[i].url,
+ exceptions[i].url, null, true, 120, null,
+ exceptions[i].type) + ' ';
+ }
+ message += ' ';
+ }
+
+ if (userCdns) {
+ message += 'Using these CDN hostnames from your preferences: ' +
+ userCdns + '
';
+ } else {
+ message += 'You can specify CDN hostnames in with the -C parameter when you run the script.' +
+ '
';
+ }
+
+ // list unique domains only to avoid long list of offenders
+ if (offenders.length) {
+ offenders = util.summaryByDomain(offenders,
+ ['size', 'size_compressed'], true);
+ for (i = 0, len = offenders.length; i < len; i += 1) {
+ offender = offenders[i];
+ offenders[i] = offender.domain + ': ' +
+ plural('%num% component%s%, ', offender.count) +
+ kbSize(offender.sum_size) + (
+ offender.sum_size_compressed > 0 ? ' (' +
+ kbSize(offender.sum_size_compressed) + ' GZip)' : ''
+ ) + (hasPref ? (
+ ' Add as CDN ') : '');
+ }
+ }
+
+ 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: 8, 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), score = 100, empty = [];
+
+ 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 ? empty : 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', 'font','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
+ reallyBadLimit: 100,
+ // points to take out for every images that is scaled more than config
+ points: 10
+ },
+
+ lint: function (doc, cset, config) {
+ var message = '',
+ score, offenders =[],
+ hash = {},
+ 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 + config.reallyBadLimit) < img.naturalWidth && img.clientWidth > 0) {
+ message = message + ' ' + img.src + ' [browserWidth:' + img.clientWidth + ' realImageWidth: ' + img.naturalWidth + ']';
+ hash[img.src] = 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('You have %num% image%s% that %are% scaled more than ' + config.reallyBadLimit + ' pixels 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, redirects = [];
+ score = 100 - cset.redirects.length * parseInt(config.points, 10);
+ redirects.push(cset.redirects.length.toFixed(0));
+
+ return {
+ score: score,
+ message: (cset.redirects.length > 0) ? YSLOW.util.plural(
+ 'There %are% %num% redirect%s%.',
+ cset.redirects.length
+ ) + " " + cset.redirects: '',
+ components: redirects
+ };
+ }
+});
+
+
+/* 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: {},
+ ycdn: {}
+
+ },
+ 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,
+ ycdn: 6
+ }
+
+});
+
+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: {},
+ ycdn: {}
+ },
+ 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,
+ ycdn: 6
+ }
+
+});
+
+
+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: {},
+ ycdn: {}
+ },
+ 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,
+ ycdn: 6
+ }
+
+});
+/**
+ * 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.
+ * Copyright (c) 2013, Marcel Duran and other contributors. 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 = '
';
+
+ 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 = '';
+
+ 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 += '' + rulesets[id].name + ' ';
+ }
+ }
+ return sHtml;
+ },
+
+ /**
+ * Refresh the Ruleset Dropdown list. This is usually called after a ruleset is created or deleted.
+ */
+ updateRulesetList: function () {
+ var i, div, new_select,
+ selects = this.panel_doc.getElementsByTagName('select'),
+ rulesets = YSLOW.controller.getRegisteredRuleset(),
+ sText = this.getRulesetListSource(rulesets),
+
+ onchangeFunc = function (event) {
+ var doc = FBL.getContentView(this.ownerDocument);
+ doc.ysview.onChangeRuleset(event);
+ };
+
+ for (i = 0; i < selects.length; i += 1) {
+ if (selects[i].id === "toolbar-rulesetList") {
+ div = selects[i].parentNode;
+ if (div && div.id === "toolbar-ruleset") {
+ new_select = this.panel_doc.createElement('select');
+ if (new_select) {
+ new_select.id = 'toolbar-rulesetList';
+ new_select.name = 'rulesets';
+ new_select.onchange = onchangeFunc;
+ new_select.innerHTML = sText;
+ }
+
+ div.replaceChild(new_select, selects[i]);
+ }
+ }
+ }
+ },
+
+ /**
+ * @private
+ */
+ getToolbarSource: function () {
+ var view, rulesets,
+ sHtml = '';
+
+ return sHtml;
+ },
+
+ /**
+ * Show the passed view. If nothing is passed, default view "Grade" will be shown.
+ * Possible sView values are: "ysCompsButton", "ysStatsButton", "ysToolButton", "ysRuleEditButton" and "ysPerfButton".
+ * If the page has not been peeled before this function is called, peeler will be run first and sView will not be displayed until
+ * peeler is done.
+ * @param {String} sView The view to be displayed.
+ */
+ show: function (sView) {
+ var format = 'html',
+ stext = "";
+
+ sView = sView || this.yscontext.defaultview;
+
+ if (this.yscontext.component_set === null) {
+ // need to run peeler first.
+ YSLOW.controller.run(window.top.content, this.yscontext, false);
+ this.yscontext.defaultview = sView;
+ } else {
+ if (this.getButtonView(sView)) {
+ // This view already exists, just toggle to it.
+ this.showButtonView(sView);
+ }
+ else if ("ysCompsButton" === sView) {
+ stext += this.yscontext.genComponents(format);
+ this.addButtonView("ysCompsButton", stext);
+ }
+ else if ("ysStatsButton" === sView) {
+ stext += this.yscontext.genStats(format);
+ this.addButtonView("ysStatsButton", stext);
+ YSLOW.renderer.plotComponents(this.getButtonView("ysStatsButton"), this.yscontext);
+ }
+ else if ("ysToolButton" === sView) {
+ stext += this.yscontext.genToolsView(format);
+ this.addButtonView("ysToolButton", stext);
+ }
+ else {
+ // Default is Performance.
+ stext += this.yscontext.genPerformance(format);
+ this.addButtonView("ysPerfButton", stext);
+ }
+
+ this.panelNode.scrollTop = 0;
+ this.panelNode.scrollLeft = 0;
+
+ this.updateStatusBar(this.yscontext.document);
+
+ // update toolbar selected tab.
+ this.updateToolbarSelection();
+ }
+ },
+
+ /**
+ * @private
+ */
+ updateToolbarSelection: function () {
+ var elem, ul_elem, child;
+
+ switch (this.curButtonId) {
+ case "ysCompsButton":
+ case "ysPerfButton":
+ case "ysStatsButton":
+ case "ysToolButton":
+ elem = this.getElementByTagNameAndId(this.panelNode, 'li', this.curButtonId);
+ if (elem) {
+ if (elem.className.indexOf("selected") !== -1) {
+ // no need to do anything.
+ return;
+ } else {
+ elem.className += " selected";
+ if (elem.previousSibling) {
+ elem.previousSibling.className += " off";
+ }
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ ul_elem = this.getElementByTagNameAndId(this.panelNode, 'ul', 'toolbarLinks');
+ child = ul_elem.firstChild;
+ while (child) {
+ if (child.id !== this.curButtonId && child.className.indexOf("selected") !== -1) {
+ this.unselect(child);
+ if (child.previousSibling) {
+ YSLOW.view.removeClassName(child.previousSibling, 'off');
+ }
+ }
+ child = child.nextSibling;
+ }
+ },
+
+ /**
+ * Show Grade screen. Use YSLOW.view.show(). Called from UI.
+ */
+ showPerformance: function () {
+ this.show('ysPerfButton');
+ },
+
+ /**
+ * Show Stats screen. Use YSLOW.view.show(). Called from UI.
+ */
+ showStats: function () {
+ this.show('ysStatsButton');
+ },
+
+ /**
+ * Show Components screen. Use YSLOW.view.show(). Called from UI.
+ */
+ showComponents: function () {
+ this.show('ysCompsButton');
+ },
+
+ /**
+ * Show Tools screen. Use YSLOW.view.show(). Called from UI.
+ */
+ showTools: function () {
+ this.show('ysToolButton');
+ },
+
+ /**
+ * Show Rule Settings screen. Use YSLOW.view.show(). Called from UI.
+ */
+ showRuleSettings: function () {
+ var stext = this.yscontext.genRulesetEditView('html');
+
+ this.addButtonView("ysRuleEditButton", stext);
+
+ this.panelNode.scrollTop = 0;
+ this.panelNode.scrollLeft = 0;
+
+ // update toolbar selected tab.
+ this.updateToolbarSelection();
+ },
+
+ /**
+ * Run YSlow. Called from UI.
+ */
+ runTest: function () {
+ YSLOW.controller.run(window.top.content, this.yscontext, false);
+ },
+
+ /**
+ * Set autorun preference. Called from UI.
+ * @param {boolean} set Pass true to turn autorun on, false otherwise.
+ */
+ setAutorun: function (set) {
+ YSLOW.util.Preference.setPref("extensions.yslow.autorun", set);
+ },
+
+ /**
+ * Set antiiframe preference. Called from UI.
+ * @param {boolean} set Pass true to use simple afterOnload verification, false otherwise.
+ */
+ setAntiIframe: function (set) {
+ YSLOW.antiIframe = set;
+ },
+
+ /**
+ * Add a custom CDN to custom CDN preference list
+ * @param {string} the CDN to be added
+ */
+ addCDN: function (cdn) {
+ var i, id,
+ that = this,
+ doc = document,
+ ctx = that.yscontext,
+ pref = YSLOW.util.Preference,
+ cdns = pref.getPref('cdnHostnames', ''),
+ panel = that.panel_doc,
+ el = panel.getElementById('tab-label-list'),
+ lis = el.getElementsByTagName('li'),
+ len = lis.length;
+
+ if (cdns) {
+ cdns = cdns.replace(/\s+/g, '').split(',');
+ cdns.push(cdn);
+ cdns = cdns.join();
+ } else {
+ cdns = cdn;
+ }
+ pref.setPref('extensions.yslow.cdnHostnames', cdns);
+
+ // get selected tab
+ for (i = 0; i < len; i+= 1) {
+ el = lis[i];
+ if (el.className.indexOf('selected') > -1) {
+ id = el.id;
+ break;
+ }
+ }
+ // re-run analysis
+ YSLOW.controller.lint(ctx.document, ctx);
+ that.addButtonView('ysPerfButton', ctx.genPerformance('html'));
+ // update score in status bar.
+ YSLOW.view.restoreStatusBar(ctx);
+ that.updateToolbarSelection();
+ // move tab
+ el = panel.getElementById(id);
+ that.onclickTabLabel({currentTarget: el}, true);
+ },
+
+ /**
+ * Handle Ruleset drop down list selection change. Update default ruleset and display
+ * dialog to ask users if they want to run new ruleset at once.
+ * @param {DOMEvent} event onchange event of Ruleset drop down list.
+ */
+ onChangeRuleset: function (event) {
+ var doc, line1, left_button_label, left_button_func,
+ select = YSLOW.util.getCurrentTarget(event),
+ option = select.options[select.selectedIndex];
+
+ YSLOW.controller.setDefaultRuleset(option.value);
+
+ // ask if want to rerun test with the selected ruleset.
+ doc = select.ownerDocument;
+ line1 = 'Do you want to run the selected ruleset now?';
+ left_button_label = 'Run Test';
+ left_button_func = function (e) {
+ var stext;
+
+ doc.ysview.closeDialog(doc);
+ if (doc.yslowContext.component_set === null) {
+ YSLOW.controller.run(doc.yslowContext.document.defaultView ||
+ doc.yslowContext.document.parentWindow, doc.yslowContext, false);
+ } else {
+ // page peeled, just run lint.
+ YSLOW.controller.lint(doc.yslowContext.document, doc.yslowContext);
+ }
+
+ stext = doc.yslowContext.genPerformance('html');
+ doc.ysview.addButtonView("ysPerfButton", stext);
+ doc.ysview.panelNode.scrollTop = 0;
+ doc.ysview.panelNode.scrollLeft = 0;
+ // update score in status bar.
+ YSLOW.view.restoreStatusBar(doc.yslowContext);
+ doc.ysview.updateToolbarSelection();
+ };
+ this.openDialog(doc, 389, 150, line1, undefined, left_button_label, left_button_func);
+ },
+
+ /**
+ * @private
+ * Implemented for handling onclick event of TabLabel in TabView.
+ * Hide current tab content and make content associated with the newly selected tab visible.
+ */
+ onclickTabLabel: function (event, move_tab) {
+ var child, hide_tab_id, show_tab_id, hide, show, show_tab, id_substring,
+ li_elem = YSLOW.util.getCurrentTarget(event),
+ ul_elem = li_elem.parentNode,
+ div_elem = ul_elem.nextSibling; // yui-content div
+
+ if (li_elem.className.indexOf('selected') !== -1 || li_elem.id.indexOf('label') === -1) {
+ return false;
+ }
+ if (ul_elem) {
+ child = ul_elem.firstChild;
+
+ while (child) {
+ if (this.unselect(child)) {
+ hide_tab_id = child.id.substring(5);
+ break;
+ }
+ child = child.nextSibling;
+ }
+
+ // select new tab selected.
+ li_elem.className += ' selected';
+ show_tab_id = li_elem.id.substring(5);
+
+ // Go through all the tabs in yui-content to hide the old tab and show the new tab.
+ child = div_elem.firstChild;
+ while (child) {
+ id_substring = child.id.substring(3);
+ if (!hide && hide_tab_id && id_substring === hide_tab_id) {
+ if (child.className.indexOf("yui-hidden") === -1) {
+ //set yui-hidden
+ child.className += " yui-hidden";
+ }
+ hide = true;
+ }
+ if (!show && show_tab_id && id_substring === show_tab_id) {
+ YSLOW.view.removeClassName(child, "yui-hidden");
+ show = true;
+ show_tab = child;
+ }
+ if ((hide || !hide_tab_id) && (show || !show_tab_id)) {
+ break;
+ }
+ child = child.nextSibling;
+ }
+
+ if (move_tab === true && show === true && show_tab) {
+ this.positionResultTab(show_tab, div_elem, li_elem);
+ }
+ }
+ return false;
+ },
+
+ positionResultTab: function (tab, container, label) {
+ var y, parent, delta,
+ padding = 5,
+ doc = this.panel_doc,
+ win = doc.defaultView || doc.parentWindow,
+ pageHeight = win.offsetHeight ? win.offsetHeight : win.innerHeight,
+ height = label.offsetTop + tab.offsetHeight;
+
+ container.style.height = height + 'px';
+ tab.style.position = "absolute";
+ tab.style.left = label.offsetLeft + label.offsetWidth + "px";
+ tab.style.top = label.offsetTop + "px";
+
+ /* now make sure tab is visible */
+ y = tab.offsetTop;
+ parent = tab.offsetParent;
+ while (parent !== null) {
+ y += parent.offsetTop;
+ parent = parent.offsetParent;
+ }
+
+ if (y < this.panelNode.scrollTop || y + tab.offsetHeight > this.panelNode.scrollTop + pageHeight) {
+
+ if (y < this.panelNode.scrollTop) {
+ // scroll up
+ this.panelNode.scrollTop = y - padding;
+ } else {
+ // scroll down
+ delta = y + tab.offsetHeight - this.panelNode.scrollTop - pageHeight + padding;
+ if (delta > y - this.panelNode.scrollTop) {
+ delta = y - this.panelNode.scrollTop;
+ }
+ this.panelNode.scrollTop += delta;
+ }
+ }
+ },
+
+ /**
+ * Event handling for onclick event on result tab (Grade screen). Called from UI.
+ * @param {DOMEevent} event onclick event
+ */
+ onclickResult: function (event) {
+ YSLOW.util.preventDefault(event);
+
+ return this.onclickTabLabel(event, true);
+ },
+
+ /**
+ * @private
+ * Helper function to unselect element.
+ */
+ unselect: function (elem) {
+ return YSLOW.view.removeClassName(elem, "selected");
+ },
+
+ /**
+ * @private
+ * Helper function to filter result based on its category. (Grade Screen)
+ */
+ filterResult: function (doc, category) {
+ var ul_elem, showAll, child, firstTab, tab, firstChild, div_elem,
+ view = this.getButtonView('ysPerfButton');
+
+ if (category === "all") {
+ showAll = true;
+ }
+
+ /* go through tab-label to re-adjust hidden state */
+ if (view) {
+ ul_elem = this.getElementByTagNameAndId(view, "ul", "tab-label-list");
+ }
+ if (ul_elem) {
+ child = ul_elem.firstChild;
+ div_elem = ul_elem.nextSibling; // yui-content div
+ tab = div_elem.firstChild;
+
+ while (child) {
+ YSLOW.view.removeClassName(child, 'first');
+ if (showAll || child.className.indexOf(category) !== -1) {
+ child.style.display = "block";
+ if (firstTab === undefined) {
+ firstTab = tab;
+ firstChild = child;
+ YSLOW.view.removeClassName(tab, "yui-hidden");
+ child.className += ' first';
+ if (child.className.indexOf("selected") === -1) { /* set selected class */
+ child.className += " selected";
+ }
+ child = child.nextSibling;
+ tab = tab.nextSibling;
+ continue;
+ }
+ } else {
+ child.style.display = "none";
+ }
+
+ /* hide non-first tab */
+ if (tab.className.indexOf("yui-hidden") === -1) {
+ tab.className += " yui-hidden";
+ }
+
+ /* remove selected from class */
+ this.unselect(child);
+
+ child = child.nextSibling;
+ tab = tab.nextSibling;
+ }
+
+ if (firstTab) { /* tab back to top position */
+ this.positionResultTab(firstTab, div_elem, firstChild);
+ }
+ }
+ },
+
+ /**
+ * Event handler of onclick event of category filter (Grade screen). Called from UI.
+ * @param {DOMEvent} event onclick event
+ */
+ updateFilterSelection: function (event) {
+ var li,
+ elem = YSLOW.util.getCurrentTarget(event);
+
+ YSLOW.util.preventDefault(event);
+
+ if (elem.className.indexOf("selected") !== -1) {
+ return; /* click on something already selected */
+ }
+ elem.className += " selected";
+
+ li = elem.parentNode.firstChild;
+ while (li) {
+ if (li !== elem && this.unselect(li)) {
+ break;
+ }
+ li = li.nextSibling;
+ }
+ this.filterResult(elem.ownerDocument, elem.id);
+ },
+
+ /**
+ * Event handler of toolbar menu.
+ * @param {DOMEvent} event onclick event
+ */
+ onclickToolbarMenu: function (event) {
+ var child,
+ a_elem = YSLOW.util.getCurrentTarget(event),
+ li_elem = a_elem.parentNode,
+ ul_elem = li_elem.parentNode;
+
+ if (li_elem.className.indexOf("selected") !== -1) { /* selecting an already selected target, do nothing. */
+ return;
+ }
+ li_elem.className += " selected";
+
+ if (li_elem.previousSibling) {
+ li_elem.previousSibling.className += " off";
+ }
+
+ if (ul_elem) {
+ child = ul_elem.firstChild;
+ while (child) {
+ if (child !== li_elem && this.unselect(child)) {
+ if (child.previousSibling) {
+ YSLOW.view.removeClassName(child.previousSibling, 'off');
+ }
+ break;
+ }
+ child = child.nextSibling;
+ }
+ }
+ },
+
+ /**
+ * Expand components with the passed type. (Components Screen)
+ * @param {Document} doc Document object of the YSlow Chrome window.
+ * @param {String} type Component type.
+ */
+ expandCollapseComponentType: function (doc, type) {
+ var table,
+ renderer = YSLOW.controller.getRenderer('html'),
+ view = this.getButtonView('ysCompsButton');
+
+ if (view) {
+ table = this.getElementByTagNameAndId(view, 'table', 'components-table');
+ renderer.expandCollapseComponentType(doc, table, type);
+ }
+ },
+
+ /**
+ * Expand all components. (Components Screen)
+ * @param {Document} doc Document object of the YSlow Chrome window.
+ */
+ expandAll: function (doc) {
+ var table,
+ renderer = YSLOW.controller.getRenderer('html'),
+ view = this.getButtonView('ysCompsButton');
+
+ if (view) {
+ table = this.getElementByTagNameAndId(view, 'table', 'components-table');
+ renderer.expandAllComponentType(doc, table);
+ }
+ },
+
+ /**
+ * Regenerate the components table. (Components Screen)
+ * @param {Document} doc Document object of the YSlow Chrome window.
+ * @param {String} column_name The column to sort by.
+ * @param {boolean} sortDesc true if to Sort descending order, false otherwise.
+ */
+ regenComponentsTable: function (doc, column_name, sortDesc) {
+ var table,
+ renderer = YSLOW.controller.getRenderer('html'),
+ view = this.getButtonView('ysCompsButton');
+
+ if (view) {
+ table = this.getElementByTagNameAndId(view, 'table', 'components-table');
+ renderer.regenComponentsTable(doc, table, column_name, sortDesc, this.yscontext.component_set);
+ }
+ },
+
+ /**
+ * Show Component header row. (Component Screen)
+ * @param {String} headersDivId id of the HTML TR element containing the component header.
+ */
+ showComponentHeaders: function (headersDivId) {
+ var elem, td,
+ view = this.getButtonView('ysCompsButton');
+
+ if (view) {
+ elem = this.getElementByTagNameAndId(view, "tr", headersDivId);
+ if (elem) {
+ td = elem.firstChild;
+ if (elem.style.display === "none") {
+ elem.style.display = "table-row";
+ } else {
+ elem.style.display = "none";
+ }
+ }
+ }
+ },
+
+ /**
+ * Open link in new tab.
+ * @param {String} url URL of the page to be opened.
+ */
+ openLink: function (url) {
+ YSLOW.util.openLink(url);
+ },
+
+ /**
+ * Open link in a popup window
+ * @param {String} url URL of the page to be opened.
+ * @param {String} name (optional) the window name.
+ * @param {Number} width (optional) the popup window width.
+ * @param {Number} height (optional) the popup window height.
+ */
+ openPopup: function (url, name, width, height, features) {
+ window.open(url, name || '_blank', 'width=' + (width || 626) +
+ ',height=' + (height || 436) + ',' + (features ||
+ 'toolbar=0,status=1,location=1,resizable=1'));
+ },
+
+ /**
+ * Launch tool.
+ * @param {String} tool_id
+ * @param {Object} param to be passed to tool's run method.
+ */
+ runTool: function (tool_id, param) {
+ YSLOW.controller.runTool(tool_id, this.yscontext, param);
+ },
+
+ /**
+ * Onclick event handler of Ruleset tab in Rule Settings screen.
+ * @param {DOMEvent} event onclick event
+ */
+ onclickRuleset: function (event) {
+ var ruleset_id, end, view, form,
+ li_elem = YSLOW.util.getCurrentTarget(event),
+ index = li_elem.className.indexOf('ruleset-');
+
+ YSLOW.util.preventDefault(event);
+ if (index !== -1) {
+ end = li_elem.className.indexOf(' ', index + 8);
+ if (end !== -1) {
+ ruleset_id = li_elem.className.substring(index + 8, end);
+ } else {
+ ruleset_id = li_elem.className.substring(index + 8);
+ }
+ view = this.getButtonView('ysRuleEditButton');
+ if (view) {
+ form = this.getElementByTagNameAndId(view, 'form', 'edit-form');
+ YSLOW.renderer.initRulesetEditForm(li_elem.ownerDocument, form, YSLOW.controller.getRuleset(ruleset_id));
+ }
+ }
+
+ return this.onclickTabLabel(event, false);
+ },
+
+ /**
+ * Display Save As Dialog
+ * @param {Document} doc Document object of YSlow Chrome window.
+ * @param {String} form_id id of the HTML form element that contains the ruleset settings to be submit (or saved).
+ */
+ openSaveAsDialog: function (doc, form_id) {
+ var line1 = 'Save ruleset as: ',
+ left_button_label = 'Save',
+
+ left_button_func = function (e) {
+ var textbox, line, view, form, input,
+ doc = YSLOW.util.getCurrentTarget(e).ownerDocument;
+
+ if (doc.ysview.modaldlg) {
+ textbox = doc.ysview.getElementByTagNameAndId(doc.ysview.modaldlg, 'input', 'saveas-name');
+ }
+ if (textbox) {
+ if (YSLOW.controller.checkRulesetName(textbox.value) === true) {
+ line = line1 + '' + textbox.value + ' ruleset already exists.
';
+ doc.ysview.closeDialog(doc);
+ doc.ysview.openDialog(doc, 389, 150, line, '', left_button_label, left_button_func);
+ } else {
+ view = doc.ysview.getButtonView('ysRuleEditButton');
+ if (view) {
+ form = doc.ysview.getElementByTagNameAndId(view, 'form', form_id);
+ input = doc.createElement('input');
+ input.type = 'hidden';
+ input.name = 'saveas-name';
+ input.value = textbox.value;
+ form.appendChild(input);
+ form.submit();
+ }
+ doc.ysview.closeDialog(doc);
+ }
+ }
+
+ };
+
+ this.openDialog(doc, 389, 150, line1, undefined, left_button_label, left_button_func);
+ },
+
+ /**
+ * Display Printable View Dialog
+ * @param {Document} doc Document object of YSlow Chrome window.
+ */
+ openPrintableDialog: function (doc) {
+ var line = 'Please run YSlow first before using Printable View.',
+ line1 = 'Check which information you want to view or print ',
+ line2 = '' + ' Grade ' + ' Components ' + ' Statistics
',
+ left_button_label = 'Ok',
+
+ left_button_func = function (e) {
+ var i,
+ doc = YSLOW.util.getCurrentTarget(e).ownerDocument,
+ doc = FBL.getContentView(doc);
+
+ aInputs = doc.getElementsByName('print-type'),
+ print_type = {};
+
+ for (i = 0; i < aInputs.length; i += 1) {
+ if (aInputs[i].checked) {
+ print_type[aInputs[i].value] = 1;
+ }
+ }
+ doc.ysview.closeDialog(doc);
+ doc.ysview.runTool('printableview', {
+ 'options': print_type,
+ 'yscontext': doc.yslowContext
+ });
+ };
+
+ if (doc.yslowContext.component_set === null) {
+ this.openDialog(doc, 389, 150, line, '', 'Ok');
+ return;
+ }
+
+ this.openDialog(doc, 389, 150, line1, line2, left_button_label, left_button_func);
+ },
+
+ /**
+ * @private
+ * helper function to get element with id and tagname in node.
+ */
+ getElementByTagNameAndId: function (node, tagname, id) {
+ var i, arrElements;
+
+ if (node) {
+ arrElements = node.getElementsByTagName(tagname);
+ if (arrElements.length > 0) {
+ for (i = 0; i < arrElements.length; i += 1) {
+ if (arrElements[i].id === id) {
+ return arrElements[i];
+ }
+ }
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Helper function for displaying dialog.
+ * @param {Document} doc Document object of YSlow Chrome window
+ * @param {Number} width desired width of the dialog
+ * @param {Number} height desired height of the dialog
+ * @param {String} text1 first line of text
+ * @param {String} text2 second line fo text
+ * @param {String} left_button_label left button label
+ * @param {Function} left_button_func onclick function of left button
+ */
+ openDialog: function (doc, width, height, text1, text2, left_button_label, left_button_func) {
+ var i, j, dialog, text, more_text, button, inputs, win, pageWidth, pageHeight, left, top,
+ overlay = this.modaldlg,
+ elems = overlay.getElementsByTagName('div');
+
+ for (i = 0; i < elems.length; i += 1) {
+ if (elems[i].className && elems[i].className.length > 0) {
+ if (elems[i].className === "dialog-box") {
+ dialog = elems[i];
+ } else if (elems[i].className === "dialog-text") {
+ text = elems[i];
+ } else if (elems[i].className === "dialog-more-text") {
+ more_text = elems[i];
+ }
+ }
+ }
+
+ if (overlay && dialog && text && more_text) {
+ text.innerHTML = (text1 ? text1 : '');
+ more_text.innerHTML = (text2 ? text2 : '');
+
+ inputs = overlay.getElementsByTagName('input');
+ for (j = 0; j < inputs.length; j += 1) {
+ if (inputs[j].className === "dialog-left-button") {
+ button = inputs[j];
+ }
+ }
+ if (button) {
+ button.value = left_button_label;
+ button.onclick = left_button_func || function (e) {
+ doc = FBL.getContentView(doc);
+ doc.ysview.closeDialog(doc);
+ };
+ }
+
+ // position dialog to center of panel.
+ win = doc.defaultView || doc.parentWindow;
+ pageWidth = win.innerWidth;
+ pageHeight = win.innerHeight;
+
+ left = Math.floor((pageWidth - width) / 2);
+ top = Math.floor((pageHeight - height) / 2);
+ dialog.style.left = ((left && left > 0) ? left : 225) + 'px';
+ dialog.style.top = ((top && top > 0) ? top : 80) + 'px';
+
+ overlay.style.left = this.panelNode.scrollLeft + 'px';
+ overlay.style.top = this.panelNode.scrollTop + 'px';
+ overlay.style.display = 'block';
+
+ // put focus on the first input.
+ if (inputs.length > 0) {
+ inputs[0].focus();
+ }
+ }
+
+ },
+
+ /**
+ * Close the dialog.
+ * @param {Document} doc Document object of YSlow Chrome window
+ */
+ closeDialog: function (doc) {
+ var dialog = this.modaldlg;
+
+ dialog.style.display = "none";
+ },
+
+ /**
+ * Save the modified changes in the selected ruleset in Rule settings screen.
+ * @param {Document} doc Document object of YSlow Chrome window
+ * @param {String} form_id ID of Form element
+ */
+ saveRuleset: function (doc, form_id) {
+ var form,
+ renderer = YSLOW.controller.getRenderer('html'),
+ view = this.getButtonView('ysRuleEditButton');
+
+ if (view) {
+ form = this.getElementByTagNameAndId(view, 'form', form_id);
+ renderer.saveRuleset(doc, form);
+ }
+ },
+
+ /**
+ * Delete the selected ruleset in Rule Settings screen.
+ * @param {Document} doc Document object of YSlow Chrome window
+ * @param {String} form_id ID of Form element
+ */
+ deleteRuleset: function (doc, form_id) {
+ var form,
+ renderer = YSLOW.controller.getRenderer('html'),
+ view = this.getButtonView('ysRuleEditButton');
+
+ if (view) {
+ form = this.getElementByTagNameAndId(view, 'form', form_id);
+ renderer.deleteRuleset(doc, form);
+ }
+ },
+
+ /**
+ * Share the selected ruleset in Rule Settings screen. Create a .XPI file on Desktop.
+ * @param {Document} doc Document object of YSlow Chrome window
+ * @param {String} form_id ID of Form element
+ */
+ shareRuleset: function (doc, form_id) {
+ var form, ruleset_id, ruleset, result, line1,
+ renderer = YSLOW.controller.getRenderer('html'),
+ view = this.getButtonView('ysRuleEditButton');
+
+ if (view) {
+ form = this.getElementByTagNameAndId(view, 'form', form_id);
+ ruleset_id = renderer.getEditFormRulesetId(form);
+ ruleset = YSLOW.controller.getRuleset(ruleset_id);
+
+ if (ruleset) {
+ result = YSLOW.Exporter.exportRuleset(ruleset);
+
+ if (result) {
+ line1 = '' + result.message + ' ';
+ this.openDialog(doc, 389, 150, line1, '', "Ok");
+ }
+ }
+ }
+ },
+
+ /**
+ * Reset the form selection for creating a new ruleset.
+ * @param {HTMLElement} button New Set button
+ * @param {String} form_id ID of Form element
+ */
+ createRuleset: function (button, form_id) {
+ var view, form,
+ li_elem = button.parentNode,
+ ul_elem = li_elem.parentNode,
+ child = ul_elem.firstChild;
+
+ // unselect ruleset
+ while (child) {
+ this.unselect(child);
+ child = child.nextSibling;
+ }
+
+ view = this.getButtonView('ysRuleEditButton');
+ if (view) {
+ form = this.getElementByTagNameAndId(view, 'form', form_id);
+ YSLOW.renderer.initRulesetEditForm(this.panel_doc, form);
+ }
+ },
+
+ /**
+ * Show/Hide the help menu.
+ */
+ showHideHelp: function () {
+ var help,
+ toolbar = this.getElementByTagNameAndId(this.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) {
+ help = this.getElementByTagNameAndId(toolbar, "div", "helpDiv");
+ }
+ if (help) {
+ if (help.style.visibility === "visible") {
+ help.style.visibility = "hidden";
+ } else {
+ help.style.visibility = "visible";
+ }
+ }
+ },
+
+ /**
+ * Run smushIt.
+ * @param {Document} doc Document object of YSlow Chrome window
+ * @param {String} url URL of the image to be smushed.
+ */
+ smushIt: function (doc, url) {
+ YSLOW.util.smushIt(url,
+ function (resp) {
+ var line1, line2, smushurl, dest_url,
+ txt = '';
+
+ if (resp.error) {
+ txt += '' + resp.error + '
';
+ } else {
+ smushurl = YSLOW.util.getSmushUrl();
+ dest_url = YSLOW.util.makeAbsoluteUrl(resp.dest, smushurl);
+ txt += 'Original size: ' + resp.src_size + ' bytes
' + 'Result size: ' + resp.dest_size + ' bytes
' + '% Savings: ' + resp.percent + '%
' + '';
+ }
+
+ line1 = 'Image: ' + YSLOW.util.briefUrl(url, 250) + '
';
+ line2 = txt;
+ doc.ysview.openDialog(doc, 389, 150, line1, line2, "Ok");
+ }
+ );
+ },
+
+ checkAllRules: function (doc, form_id, check) {
+ var i, view, form, aElements;
+
+ if (typeof check !== "boolean") {
+ return;
+ }
+ view = this.getButtonView('ysRuleEditButton');
+ if (view) {
+ form = this.getElementByTagNameAndId(view, 'form', form_id);
+ aElements = form.elements;
+ for (i = 0; i < aElements.length; i += 1) {
+ if (aElements[i].type === "checkbox") {
+ aElements[i].checked = check;
+ }
+ }
+ }
+ },
+
+ // exposed for access from content scope (Firebug UI, panel.html)
+ // See: https://blog.mozilla.org/addons/2012/08/20/exposing-objects-to-content-safely/
+ __exposedProps__: {
+ "openLink": "rw",
+ "showComponentHeaders": "rw",
+ "smushIt": "rw",
+ "saveRuleset": "rw",
+ "regenComponentsTable": "rw",
+ "expandCollapseComponentType": "rw",
+ "expandAll": "rw",
+ "updateFilterSelection": "rw",
+ "openPopup": "rw",
+ "runTool": "rw",
+ "onclickRuleset": "rw",
+ "createRuleset": "rw",
+ "addCDN": "rw",
+ "closeDialog": "rw",
+ "setAutorun": "rw",
+ "setAntiIframe": "rw",
+ "runTest": "rw",
+ "onChangeRuleset": "rw",
+ "showRuleSettings": "rw",
+ "openPrintableDialog": "rw",
+ "showHideHelp": "rw",
+ "setSplashView": "rw",
+ "onclickToolbarMenu": "rw",
+ "showPerformance": "rw",
+ "showComponents": "rw",
+ "showStats": "rw",
+ "showTools": "rw",
+ "onclickResult": "rw",
+ },
+};
+
+YSLOW.view.Tooltip = function (panel_doc, parentNode) {
+ this.tooltip = panel_doc.createElement('div');
+ if (this.tooltip) {
+ this.tooltip.id = "tooltipDiv";
+ this.tooltip.innerHTML = '';
+ this.tooltip.style.display = "none";
+ if (parentNode) {
+ parentNode.appendChild(this.tooltip);
+ }
+ }
+ this.timer = null;
+};
+
+YSLOW.view.Tooltip.prototype = {
+
+ show: function (text, target) {
+ var tooltip = this;
+
+ this.text = text;
+ this.target = target;
+ this.tooltipData = {
+ 'text': text,
+ 'target': target
+ };
+ this.timer = YSLOW.util.setTimer(function () {
+ tooltip.showX();
+ }, 500);
+ },
+
+ showX: function () {
+ if (this.tooltipData) {
+ this.showTooltip(this.tooltipData.text, this.tooltipData.target);
+ }
+ this.timer = null;
+ },
+
+ showTooltip: function (text, target) {
+ var tooltipWidth, tooltipHeight, parent, midpt_x, midpt_y, sClass, new_x,
+ padding = 10,
+ x = 0,
+ y = 0,
+ doc = target.ownerDocument,
+ win = doc.defaultView || doc.parentWindow,
+ pageWidth = win.offsetWidth ? win.offsetWidth : win.innerWidth,
+ pageHeight = win.offsetHeight ? win.offsetHeight : win.innerHeight;
+
+ this.tooltip.innerHTML = text;
+ this.tooltip.style.display = "block";
+
+ tooltipWidth = this.tooltip.offsetWidth;
+ tooltipHeight = this.tooltip.offsetHeight;
+
+ if (tooltipWidth > pageWidth || tooltipHeight > pageHeight) {
+ // forget it, the viewport is too small, don't bother.
+ this.tooltip.style.display = "none";
+ return;
+ }
+
+ parent = target.offsetParent;
+ while (parent !== null) {
+ x += parent.offsetLeft;
+ y += parent.offsetTop;
+ parent = parent.offsetParent;
+ }
+ x += target.offsetLeft;
+ y += target.offsetTop;
+
+ if (x < doc.ysview.panelNode.scrollLeft || y < doc.ysview.panelNode.scrollTop || (y + target.offsetHeight > doc.ysview.panelNode.scrollTop + pageHeight)) {
+ // target is not fully visible.
+ this.tooltip.style.display = "none";
+ return;
+ }
+
+ midpt_x = x + target.offsetWidth / 2;
+ midpt_y = y + target.offsetHeight / 2;
+
+ //decide if tooltip will fit to the right
+ if (x + target.offsetWidth + padding + tooltipWidth < pageWidth) {
+ // fit to the right?
+ x += target.offsetWidth + padding;
+ // check vertical alignment
+ if ((y >= doc.ysview.panelNode.scrollTop) && (y - padding + tooltipHeight + padding <= doc.ysview.panelNode.scrollTop + pageHeight)) {
+ y = y - padding;
+ sClass = 'right top';
+ } else {
+ // align bottom
+ y += target.offsetHeight - tooltipHeight;
+ sClass = 'right bottom';
+ }
+ } else {
+ if (y - tooltipHeight - padding >= doc.ysview.panelNode.scrollTop) {
+ // put it to the top.
+ y -= tooltipHeight + padding;
+ sClass = 'top';
+ } else {
+ // put it to the bottom.
+ y += target.offsetHeight + padding;
+ sClass = 'bottom';
+ }
+ new_x = Math.floor(midpt_x - tooltipWidth / 2);
+ if ((new_x >= doc.ysview.panelNode.scrollLeft) && (new_x + tooltipWidth <= doc.ysview.panelNode.scrollLeft + pageWidth)) {
+ x = new_x;
+ } else if (new_x < doc.ysview.panelNode.scrollLeft) {
+ x = doc.ysview.panelNode.scrollLeft;
+ } else {
+ x = doc.ysview.panelNode.scrollLeft + pageWidth - padding - tooltipWidth;
+ }
+ }
+
+ if (sClass) {
+ this.tooltip.className = sClass;
+ }
+ this.tooltip.style.left = x + 'px';
+ this.tooltip.style.top = y + 'px';
+ },
+
+ hide: function () {
+ if (this.timer) {
+ clearTimeout(this.timer);
+ }
+ this.tooltip.style.display = "none";
+ }
+
+};
+
+/**
+ * Set YSlow status bar text.
+ * @param {String} text text to put on status bar
+ * @param {String} sId id of the status bar element to put the text.
+ */
+YSLOW.view.setStatusBar = function (text, sId) {
+ var el = document.getElementById(sId || 'yslow_status_grade');
+
+ if (el) {
+ el.value = text;
+ }
+};
+
+/**
+ * Clear YSlow status bar text.
+ */
+YSLOW.view.clearStatusBar = function () {
+ this.setStatusBar("", "yslow_status_time");
+ this.setStatusBar("YSlow", "yslow_status_grade");
+ this.setStatusBar("", "yslow_status_size");
+};
+
+/**
+ * Restore YSlow status bar text
+ * @param {YSLOW.context} yscontext YSlow context that contains page result and statistics.
+ */
+YSLOW.view.restoreStatusBar = function (yscontext) {
+ var grade, size, t_done;
+
+ if (yscontext) {
+ if (yscontext.PAGE.overallScore) {
+ grade = YSLOW.util.prettyScore(yscontext.PAGE.overallScore);
+ this.setStatusBar(grade, "yslow_status_grade");
+ }
+ if (yscontext.PAGE.totalSize) {
+ size = YSLOW.util.kbSize(yscontext.PAGE.totalSize);
+ this.setStatusBar(size, "yslow_status_size");
+ }
+ if (yscontext.PAGE.t_done) {
+ t_done = yscontext.PAGE.t_done / 1000 + "s";
+ this.setStatusBar(t_done, "yslow_status_time");
+ }
+ }
+};
+
+/**
+ * Toggle YSlow in status bar.
+ * @param {Boolean} bhide show or hide YSlow in status bar.
+ */
+YSLOW.view.toggleStatusBar = function (bHide) {
+ document.getElementById('yslow-status-bar').hidden = bHide;
+};
+
+/**
+ * Remove name from element's className.
+ * @param {HTMLElement} element
+ * @param {String} name name to be removed from className.
+ * @return true if name is found in element's classname
+ */
+YSLOW.view.removeClassName = function (element, name) {
+ var i, names;
+
+ if (element && element.className && element.className.length > 0 && name && name.length > 0) {
+ names = element.className.split(" ");
+ for (i = 0; i < names.length; i += 1) {
+ if (names[i] === name) {
+ names.splice(i, 1);
+ element.className = names.join(" ");
+ return true;
+ }
+ }
+ }
+
+ return false;
+};
+/**
+ * Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+ * Copyright (c) 2013, Marcel Duran and other contributors. 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 */
+
+/**
+ * YSlow context object that holds components set, result set and statistics of current page.
+ *
+ * @constructor
+ * @param {Document} doc Document object of current page.
+ */
+YSLOW.context = function (doc) {
+ this.document = doc;
+ this.component_set = null;
+ this.result_set = null;
+ this.onloadTimestamp = null;
+
+ // reset renderer variables
+ if (YSLOW.renderer) {
+ YSLOW.renderer.reset();
+ }
+
+ this.PAGE = {
+ totalSize: 0,
+ totalRequests: 0,
+ totalObjCount: {},
+ totalObjSize: {},
+
+ totalSizePrimed: 0,
+ totalRequestsPrimed: 0,
+ totalObjCountPrimed: {},
+ totalObjSizePrimed: {},
+
+ canvas_data: {},
+
+ statusbar: false,
+ overallScore: 0,
+
+ t_done: undefined,
+ loaded: false
+ };
+
+};
+
+YSLOW.context.prototype = {
+
+ defaultview: "ysPerfButton",
+
+ /**
+ * @private
+ * Compute statistics of current page.
+ * @param {Boolean} bCacheFull set to true if based on primed cache, false for empty cache.
+ * @return stats object
+ * @type Object
+ */
+ computeStats: function (bCacheFull) {
+ var comps, comp, compType, i, len, size, totalSize, aTypes,
+ canvas_data, sType,
+ hCount = {},
+ hSize = {}, // hashes where the key is the object type
+ nHttpRequests = 0;
+
+ if (!this.component_set) {
+ /* need to run peeler first */
+ return;
+ }
+
+ comps = this.component_set.components;
+ if (!comps) {
+ return;
+ }
+
+ // SUMMARY - Find the number and total size for the categories.
+ // Iterate over all the components and add things up.
+ for (i = 0, len = comps.length; i < len; i += 1) {
+ comp = comps[i];
+
+ if (!bCacheFull || !comp.hasFarFutureExpiresOrMaxAge()) {
+ // If the object has a far future Expires date it won't add any HTTP requests nor size to the page.
+ // It adds to the HTTP requests (at least a condition GET request).
+ nHttpRequests += 1;
+ compType = comp.type;
+ hCount[compType] = (typeof hCount[compType] === 'undefined' ? 1 : hCount[compType] + 1);
+ size = 0;
+ if (!bCacheFull || !comp.hasOldModifiedDate()) {
+ // If we're doing EMPTY cache stats OR this component is newly modified (so is probably changing).
+ if (comp.compressed === 'gzip' || comp.compressed === 'deflate') {
+ if (comp.size_compressed) {
+ size = parseInt(comp.size_compressed, 10);
+ }
+ } else {
+ size = comp.size;
+ }
+ }
+ hSize[compType] = (typeof hSize[compType] === 'undefined' ? size : hSize[compType] + size);
+ }
+ }
+
+ totalSize = 0;
+ aTypes = YSLOW.peeler.types;
+ canvas_data = {};
+ for (i = 0; i < aTypes.length; i += 1) {
+ sType = aTypes[i];
+ if (typeof hCount[sType] !== "undefined") {
+ // canvas
+ if (hSize[sType] > 0) {
+ canvas_data[sType] = hSize[sType];
+ }
+ totalSize += hSize[sType];
+ }
+ }
+
+ return {
+ 'total_size': totalSize,
+ 'num_requests': nHttpRequests,
+ 'count_obj': hCount,
+ 'size_obj': hSize,
+ 'canvas_data': canvas_data
+ };
+ },
+
+ /**
+ * Collect Statistics of the current page.
+ */
+ collectStats: function () {
+ var stats = this.computeStats();
+ if (stats !== undefined) {
+ this.PAGE.totalSize = stats.total_size;
+ this.PAGE.totalRequests = stats.num_requests;
+ this.PAGE.totalObjCount = stats.count_obj;
+ this.PAGE.totalObjSize = stats.size_obj;
+ this.PAGE.canvas_data.empty = stats.canvas_data;
+ }
+
+ stats = this.computeStats(true);
+ if (stats) {
+ this.PAGE.totalSizePrimed = stats.total_size;
+ this.PAGE.totalRequestsPrimed = stats.num_requests;
+ this.PAGE.totalObjCountPrimed = stats.count_obj;
+ this.PAGE.totalObjSizePrimed = stats.size_obj;
+ this.PAGE.canvas_data.primed = stats.canvas_data;
+ }
+ },
+
+ /**
+ * Call registered renderer to generate Grade view with the passed output format.
+ *
+ * @param {String} output_format output format, e.g. 'html', 'xml'
+ * @return Grade in the passed output format.
+ */
+ genPerformance: function (output_format, doc) {
+ if (this.result_set === null) {
+ if (!doc) {
+ doc = this.document;
+ }
+ YSLOW.controller.lint(doc, this);
+ }
+ return YSLOW.controller.render(output_format, 'reportcard', {
+ 'result_set': this.result_set
+ });
+ },
+
+ /**
+ * Call registered renderer to generate Stats view with the passed output format.
+ *
+ * @param {String} output_format output format, e.g. 'html', 'xml'
+ * @return Stats in the passed output format.
+ */
+ genStats: function (output_format) {
+ var stats = {};
+ if (!this.PAGE.totalSize) {
+ // collect stats
+ this.collectStats();
+ }
+ stats.PAGE = this.PAGE;
+ return YSLOW.controller.render(output_format, 'stats', {
+ 'stats': stats
+ });
+ },
+
+ /**
+ * Call registered renderer to generate Components view with the passed output format.
+ *
+ * @param {String} output_format output format, e.g. 'html', 'xml'
+ * @return Components in the passed output format.
+ */
+ genComponents: function (output_format) {
+ if (!this.PAGE.totalSize) {
+ // collect stats
+ this.collectStats();
+ }
+ return YSLOW.controller.render(output_format, 'components', {
+ 'comps': this.component_set.components,
+ 'total_size': this.PAGE.totalSize
+ });
+ },
+
+ /**
+ * Call registered renderer to generate Tools view with the passed output format.
+ *
+ * @param {String} output_format output format, e.g. 'html'
+ * @return Tools in the passed output format.
+ */
+ genToolsView: function (output_format) {
+ var tools = YSLOW.Tools.getAllTools();
+ return YSLOW.controller.render(output_format, 'tools', {
+ 'tools': tools
+ });
+ },
+
+ /**
+ * Call registered renderer to generate Ruleset Settings view with the passed output format.
+ *
+ * @param {String} output_format output format, e.g. 'html'
+ * @return Ruleset Settings in the passed output format.
+ */
+ genRulesetEditView: function (output_format) {
+ return YSLOW.controller.render(output_format, 'rulesetEdit', {
+ 'rulesets': YSLOW.controller.getRegisteredRuleset()
+ });
+ }
+
+};
+/**
+ * Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+ * Copyright (c) 2013, Marcel Duran and other contributors. All rights reserved.
+ * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
+ */
+
+/*global YSLOW*/
+/*jslint unparam: true, continue: true, sloppy: true, type: true, maxerr: 50, indent: 4 */
+
+/**
+ * Renderer class
+ *
+ * @class
+ */
+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 (stats, bCacheFull) {
+ var hCount, hSize, nHttpRequests, aTypes, cache_type, i, sType, sText,
+ tableHtml = '',
+ totalSize = 0;
+
+ if (!stats.PAGE) {
+ return '';
+ }
+
+ if (bCacheFull) {
+ hCount = stats.PAGE.totalObjCountPrimed;
+ hSize = stats.PAGE.totalObjSizePrimed;
+ nHttpRequests = stats.PAGE.totalRequestsPrimed;
+ totalSize = stats.PAGE.totalSizePrimed;
+ } else {
+ hCount = stats.PAGE.totalObjCount;
+ hSize = stats.PAGE.totalObjSize;
+ nHttpRequests = stats.PAGE.totalRequests;
+ totalSize = stats.PAGE.totalSize;
+ }
+
+ // Iterate over the component types and format the SUMMARY results.
+
+ // One row for each component type.
+ aTypes = YSLOW.peeler.types;
+ cache_type = (bCacheFull) ? 'primed' : 'empty';
+ for (i = 0; i < aTypes.length; i += 1) {
+ sType = aTypes[i];
+ if (typeof hCount[sType] !== 'undefined') {
+ tableHtml += '' +
+ '
' +
+ hCount[sType] +
+ ' ' +
+ YSLOW.util.prettyType(sType) +
+ ' ' +
+ YSLOW.util.kbSize(hSize[sType]) +
+ ' ';
+ }
+ }
+
+ sText = '' +
+ '
HTTP Requests - ' +
+ nHttpRequests +
+ '
Total Weight - ' +
+ YSLOW.util.kbSize(totalSize) +
+ '
';
+
+ return sText;
+ },
+
+ plotComponents: function (stats_view, yscontext) {
+ if (typeof stats_view !== "object") {
+ return;
+ }
+ this.plotOne(stats_view, yscontext.PAGE.canvas_data.empty, yscontext.PAGE.totalSize, 'comp-canvas-empty');
+ this.plotOne(stats_view, yscontext.PAGE.canvas_data.primed, yscontext.PAGE.totalSizePrimed, 'comp-canvas-primed');
+ },
+
+ plotOne: function (stats_view, data, total, canvas_id) {
+ var canvas, i, ctx, canvas_size, radius, center, sofar, piece, thisvalue,
+ aElements = stats_view.getElementsByTagName('canvas');
+
+ for (i = 0; i < aElements.length; i += 1) {
+ if (aElements[i].id === canvas_id) {
+ canvas = aElements[i];
+ }
+ }
+ if (!canvas) {
+ return;
+ }
+
+ ctx = canvas.getContext('2d');
+ canvas_size = [canvas.width, canvas.height];
+ radius = Math.min(canvas_size[0], canvas_size[1]) / 2;
+ center = [canvas_size[0] / 2, canvas_size[1] / 2];
+
+
+ sofar = 0; // keep track of progress
+ // loop the data[]
+ for (piece in data) {
+ if (data.hasOwnProperty(piece) && data[piece]) {
+ thisvalue = data[piece] / total;
+
+ ctx.beginPath();
+ // center of the pie
+ ctx.moveTo(center[0], center[1]);
+ // draw next arc
+ ctx.arc(
+ center[0],
+ center[1],
+ radius,
+ // -0.5 sets set the start to be top
+ Math.PI * (-0.5 + 2 * sofar),
+ Math.PI * (-0.5 + 2 * (sofar + thisvalue)),
+ false
+ );
+ ctx.lineTo(center[0], center[1]); // line back to the center
+ ctx.closePath();
+ ctx.fillStyle = this.colors[piece]; // color
+ ctx.fill();
+
+ sofar += thisvalue; // increment progress tracker
+ }
+ }
+ },
+
+ getComponentHeadersTable: function (comp) {
+ var field,
+ sText = '';
+
+ for (field in comp.headers) {
+ if (comp.headers.hasOwnProperty(field) && comp.headers[field]) {
+ sText += '' +
+ YSLOW.util.escapeHtml(YSLOW.util.formatHeaderName(field)) +
+ ' ' +
+ YSLOW.util.escapeHtml(comp.headers[field]) +
+ ' ';
+ }
+ }
+
+ if (comp.req_headers) {
+ sText += '';
+ for (field in comp.req_headers) {
+ if (comp.req_headers.hasOwnProperty(field) &&
+ comp.req_headers[field]) {
+ sText += '' +
+ YSLOW.util.escapeHtml(YSLOW.util.formatHeaderName(field)) +
+ ' ' +
+ YSLOW.util.escapeHtml(comp.req_headers[field]) +
+ '
';
+ }
+ }
+ }
+
+ sText += '
';
+ return sText;
+ },
+
+ /**
+ * Generate HTML table row code for a component.
+ * @param fields table columns
+ * @param comp Component
+ * @param row_class 'odd' or 'even'
+ * @param hidden
+ * @return html code
+ */
+ genComponentRow: function (fields, comp, row_class, hidden) {
+ var headersDivId, sHtml, i, sClass, value, sent, recv;
+
+ if (typeof row_class !== "string") {
+ row_class = '';
+ }
+ if (comp.status >= 400 && comp.status < 500) {
+ row_class += ' compError';
+ }
+ if (comp.after_onload === true) {
+ row_class += ' afteronload';
+ }
+
+ headersDivId = 'compHeaders' + comp.id;
+
+ sHtml = '';
+ for (i in fields) {
+ if (fields.hasOwnProperty(i)) {
+ sClass = i;
+ value = '';
+
+ if (i === "type") {
+ value += comp[i];
+ if (comp.is_beacon) {
+ value += ' †';
+ }
+ if (comp.after_onload) {
+ value += ' *';
+ }
+ } else if (i === "size") {
+ value += YSLOW.util.kbSize(comp.size);
+ } else if (i === "url") {
+ if (comp.status >= 400 && comp.status < 500) {
+ sHtml += '' + comp[i] + ' (status: ' + comp.status + ') ';
+ // skip the rest of the fields if this component has error.
+ continue;
+ } else {
+ value += YSLOW.util.prettyAnchor(comp[i], comp[i], undefined, !YSLOW.renderer.bPrintable, 100, 1, comp.type);
+ }
+ } else if (i === "gzip" && (comp.compressed === "gzip" || comp.compressed === "deflate")) {
+ value += (comp.size_compressed !== undefined ? YSLOW.util.kbSize(comp.size_compressed) : 'uncertain');
+ } else if (i === "set-cookie") {
+ sent = comp.getSetCookieSize();
+ value += sent > 0 ? sent : '';
+ } else if (i === "cookie") {
+ recv = comp.getReceivedCookieSize();
+ value += recv > 0 ? recv : '';
+ } else if (i === "etag") {
+ value += comp.getEtag();
+ } else if (i === "expires") {
+ value += YSLOW.util.prettyExpiresDate(comp.expires);
+ } else if (i === "headers") {
+ if (YSLOW.renderer.bPrintable) {
+ continue;
+ }
+ if (comp.raw_headers && comp.raw_headers.length > 0) {
+ value += ' ';
+ }
+ } else if (i === "action") {
+ if (YSLOW.renderer.bPrintable) {
+ continue;
+ }
+ if (comp.type === 'cssimage' || comp.type === 'image') {
+ // for security reason, don't display smush.it unless it's image mime type.
+ if (comp.response_type === undefined || comp.response_type === "image") {
+ value += 'smush.it ';
+ }
+ }
+ } else if (comp[i] !== undefined) {
+ value += comp[i];
+ }
+ sHtml += '' + value + ' ';
+ }
+ }
+ sHtml += ' ';
+
+ if (comp.raw_headers && comp.raw_headers.length > 0) {
+ sHtml += '';
+ }
+ return sHtml;
+ },
+
+ componentSortCallback: function (comp1, comp2) {
+ var i, types, max,
+ a = '',
+ b = '',
+ sortBy = YSLOW.renderer.sortBy,
+ desc = YSLOW.renderer.sortDesc;
+
+ switch (sortBy) {
+ case 'type':
+ a = comp1.type;
+ b = comp2.type;
+ break;
+ case 'size':
+ a = comp1.size ? Number(comp1.size) : 0;
+ b = comp2.size ? Number(comp2.size) : 0;
+ break;
+ case 'gzip':
+ a = comp1.size_compressed ? Number(comp1.size_compressed) : 0;
+ b = comp2.size_compressed ? Number(comp2.size_compressed) : 0;
+ break;
+ case 'set-cookie':
+ a = comp1.getSetCookieSize();
+ b = comp2.getSetCookieSize();
+ break;
+ case 'cookie':
+ a = comp1.getReceivedCookieSize();
+ b = comp2.getReceivedCookieSize();
+ break;
+ case 'headers':
+ // header exist?
+ break;
+ case 'url':
+ a = comp1.url;
+ b = comp2.url;
+ break;
+ case 'respTime':
+ a = comp1.respTime ? Number(comp1.respTime) : 0;
+ b = comp2.respTime ? Number(comp2.respTime) : 0;
+ break;
+ case 'etag':
+ a = comp1.getEtag();
+ b = comp2.getEtag();
+ break;
+ case 'action':
+ if (comp1.type === 'cssimage' || comp1.type === 'image') {
+ a = 'smush.it';
+ }
+ if (comp2.type === 'cssimage' || comp2.type === 'image') {
+ b = 'smush.it';
+ }
+ break;
+ case 'expires':
+ // special case - date type
+ a = comp1.expires || 0;
+ b = comp2.expires || 0;
+ break;
+ }
+
+ if (a === b) {
+ // secondary sorting by ID to stablize the sorting algorithm.
+ if (comp1.id > comp2.id) {
+ return (desc) ? -1 : 1;
+ }
+ if (comp1.id < comp2.id) {
+ return (desc) ? 1 : -1;
+ }
+ }
+
+ // special case for sorting by type.
+ if (sortBy === 'type') {
+ types = YSLOW.peeler.types;
+ for (i = 0, max = types.length; i < max; i += 1) {
+ if (comp1.type === types[i]) {
+ return (desc) ? 1 : -1;
+ }
+ if (comp2.type === types[i]) {
+ return (desc) ? -1 : 1;
+ }
+ }
+ }
+
+ // normal comparison
+ if (a > b) {
+ return (desc) ? -1 : 1;
+ }
+ if (a < b) {
+ return (desc) ? 1 : -1;
+ }
+
+ return 0;
+
+ },
+
+ /**
+ * Sort components, return a new array, the passed array is unchanged.
+ * @param array of components to be sorted
+ * @param field to sort by.
+ * @return a new array of the sorted components.
+ */
+ sortComponents: function (comps, sortBy, desc) {
+ var arr_comps = comps;
+
+ this.sortBy = sortBy;
+ this.sortDesc = desc;
+ arr_comps.sort(this.componentSortCallback);
+
+ return arr_comps;
+ },
+
+ genRulesCheckbox: function (ruleset) {
+ var sText, id, rule, column_id,
+ weightsText = '',
+ numRules = 0,
+ rules = YSLOW.controller.getRegisteredRules(),
+ j = 0,
+ col1Text = '',
+ col2Text = '
',
+ col3Text = '
';
+
+ for (id in rules) {
+ if (rules.hasOwnProperty(id) && rules[id]) {
+ rule = rules[id];
+
+ sText = ' ' +
+ rule.name +
+ ' ';
+
+ if (ruleset.rules[id] !== undefined) {
+ numRules += 1;
+ }
+
+ if (ruleset.weights !== undefined && ruleset.weights[id] !== undefined) {
+ weightsText += ' ';
+ }
+
+ column_id = (j % 3);
+ switch (column_id) {
+ case 0:
+ col1Text += sText;
+ break;
+ case 1:
+ col2Text += sText;
+ break;
+ case 2:
+ col3Text += sText;
+ break;
+ }
+ j += 1;
+ }
+ }
+
+ col1Text += '
';
+ col2Text += '
';
+ col3Text += '
';
+
+ return '' + ruleset.name + ' Ruleset (includes ' + parseInt(numRules, 10) + ' of ' + parseInt(j, 10) + ' rules) ' + '' + col1Text + ' ' + col2Text + ' ' + col3Text + '
' + weightsText + '
';
+ },
+
+ genRulesetEditForm: function (ruleset) {
+ var contentHtml = '';
+
+ contentHtml += '';
+
+ return contentHtml;
+ },
+
+ initRulesetEditForm: function (doc, form, ruleset) {
+ var divs, i, j, id, buttons, rulesetId, rulesetName, title, weightsDiv,
+ rules, numRulesSpan, spans, checkbox,
+ aElements = form.elements,
+ weightsText = '',
+ checkboxes = [],
+ numRules = 0,
+ totalRules = 0;
+
+ // uncheck all rules
+ for (i = 0; i < aElements.length; i += 1) {
+ if (aElements[i].name === "rules") {
+ aElements[i].checked = false;
+ checkboxes[aElements[i].id] = aElements[i];
+ totalRules += 1;
+ } else if (aElements[i].name === "saveas-name") {
+ // clear saveas-name
+ form.removeChild(aElements[i]);
+ }
+ }
+
+ divs = form.getElementsByTagName('div');
+ for (i = 0; i < divs.length; i += 1) {
+ if (divs[i].id === "rulesetEditWeightsDiv") {
+ weightsDiv = divs[i];
+ } else if (divs[i].id === "rulesetEditRulesetId") {
+ rulesetId = divs[i];
+ } else if (divs[i].id === "rulesetEditRulesetName") {
+ rulesetName = divs[i];
+ }
+ }
+
+ spans = form.parentNode.getElementsByTagName('span');
+ for (j = 0; j < spans.length; j += 1) {
+ if (spans[j].id === "rulesetEditFormTitle") {
+ title = spans[j];
+ } else if (spans[j].id === "rulesetEditCustomButtons") {
+ // show save, delete and share for custom rules
+ buttons = spans[j];
+ if (ruleset !== undefined && ruleset.custom === true) {
+ // show the buttons
+ buttons.style.visibility = 'visible';
+ } else {
+ // hide the buttons
+ buttons.style.visibility = 'hidden';
+ }
+ } else if (spans[j].id === "rulesetEditFormNumRules") {
+ numRulesSpan = spans[j];
+ }
+ }
+
+ if (ruleset) {
+ rules = ruleset.rules;
+ for (id in rules) {
+ if (rules.hasOwnProperty(id) && rules[id]) {
+ // check the checkbox.
+ checkbox = checkboxes['rulesetEditRule' + id];
+ if (checkbox) {
+ checkbox.checked = true;
+ }
+ if (ruleset.weights !== undefined && ruleset.weights[id] !== undefined) {
+ weightsText += ' ';
+ }
+ numRules += 1;
+ }
+ }
+ numRulesSpan.innerHTML = '(includes ' + parseInt(numRules, 10) + ' of ' + parseInt(totalRules, 10) + ' rules)';
+ rulesetId.innerHTML = ' ';
+ rulesetName.innerHTML = ' ';
+ title.innerHTML = ruleset.name;
+ } else {
+ rulesetId.innerHTML = '';
+ rulesetName.innerHTML = '';
+ title.innerHTML = 'New';
+ numRulesSpan.innerHTML = '';
+ }
+ weightsDiv.innerHTML = weightsText;
+ }
+};
+
+YSLOW.registerRenderer({
+ /**
+ * @member YSLOW.HTMLRenderer
+ * @final
+ */
+ id: 'html',
+ /**
+ * @member YSLOW.HTMLRenderer
+ * @final
+ */
+ supports: {
+ components: 1,
+ reportcard: 1,
+ stats: 1,
+ tools: 1,
+ rulesetEdit: 1
+ },
+
+ /**
+ * @private
+ */
+ genComponentsTable: function (comps, sortBy, sortDesc) {
+ var f, j, type, comp,
+ headers = {
+ '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'
+ },
+ collapsed = false,
+ tableHtml = '',
+ rowHtml = '',
+ numComponentsByType = 0,
+ sizeByType = 0;
+
+ if (sortBy !== undefined && headers[sortBy] === undefined) {
+ return ''; // Invalid column name, don't do anything.
+ }
+
+ if (YSLOW.renderer.bPrintable) {
+ sortBy = YSLOW.renderer.sortBy;
+ sortDesc = YSLOW.renderer.sortDesc;
+ } else if (sortBy === undefined || sortBy === "type") {
+ sortBy = "type";
+ collapsed = true;
+ }
+
+ comps = YSLOW.renderer.sortComponents(comps, sortBy, sortDesc);
+
+
+ // table headers
+ tableHtml += '';
+ for (f in headers) {
+ if (headers.hasOwnProperty(f) && headers[f]) {
+ if (YSLOW.renderer.bPrintable &&
+ (f === "action" || f === "components" ||
+ f === "headers")) {
+ continue;
+ }
+ tableHtml += '';
+ if (YSLOW.renderer.bPrintable) {
+ tableHtml += headers[f];
+ } else {
+ tableHtml += '';
+
+ }
+ }
+ }
+ tableHtml += ' ';
+
+ // component data
+ for (j = 0; j < comps.length; j += 1) {
+ comp = comps[j];
+ if ((sortBy === undefined || sortBy === "type") && !YSLOW.renderer.bPrintable) {
+ if (type === undefined) {
+ type = comp.type;
+ } else if (type !== comp.type) { /* add type summary row */
+ tableHtml += '' + '' + type + ' (' + numComponentsByType + ') ' + YSLOW.util.kbSize(sizeByType) + ' ' + ' ' + ' '; /* flush to tableHtml */
+ tableHtml += rowHtml;
+ rowHtml = '';
+ numComponentsByType = 0;
+ sizeByType = 0;
+ type = comp.type;
+ }
+ rowHtml += YSLOW.renderer.genComponentRow(headers, comp, (numComponentsByType % 2 === 0 ? 'even' : 'odd'), collapsed);
+ numComponentsByType += 1;
+ sizeByType += comp.size;
+ } else {
+ tableHtml += YSLOW.renderer.genComponentRow(headers, comp, (j % 2 === 0 ? 'even' : 'odd'), false);
+ }
+ }
+ if (rowHtml.length > 0) {
+ tableHtml += '' + '' + type + ' (' + numComponentsByType + ') ' + YSLOW.util.kbSize(sizeByType) + ' ' + ' ' + ' ';
+ tableHtml += rowHtml;
+ }
+ tableHtml += '
';
+ return tableHtml;
+
+ },
+
+ /**
+ * @member YSLOW.HTMLRenderer
+ * Generate HTML code for Components tab
+ * @param {YSLOW.ComponentSet} comps array of components
+ * @param {Number} totalSize total page size
+ * @return html code for Components tab
+ * @type String
+ */
+ componentsView: function (comps, totalSize) {
+ var sText,
+ tableHtml = this.genComponentsTable(comps, YSLOW.renderer.sortBy, false),
+ beacon_legend = 'in type column indicates the component is loaded after window onload event.',
+ after_onload_legend = 'denotes 1x1 pixels image that may be image beacon',
+ title = 'Components';
+
+ if (YSLOW.doc) {
+ if (YSLOW.doc.components_legend) {
+ if (YSLOW.doc.components_legend.beacon) {
+ beacon_legend = YSLOW.doc.components_legend.beacon;
+ }
+ if (YSLOW.doc.components_legend.after_onload) {
+ after_onload_legend = YSLOW.doc.components_legend.after_onload;
+ }
+ }
+ if (YSLOW.doc.view_names && YSLOW.doc.view_names.components) {
+ title = YSLOW.doc.view_names.components;
+ }
+ }
+
+ sText = '' + '
' + title + ' The page has a total of ' + '' + comps.length + ' ' + ' components and a total weight of ' + '' + YSLOW.util.kbSize(totalSize) + ' bytes
' + '
' + '
' + tableHtml + '
* ' + beacon_legend + ' ' + '† ' + after_onload_legend + '
';
+
+ return sText;
+ },
+
+ /**
+ * @private
+ */
+ reportcardPrintableView: function (results, overall_grade, ruleset) {
+ var i, j, result, grade, grade_class,
+ html = '';
+
+ for (i = 0; i < results.length; i += 1) {
+ result = results[i];
+ if (typeof result === "object") {
+ grade = YSLOW.util.prettyScore(result.score);
+ grade_class = 'grade-' + (grade === "N/A" ? 'NA' : grade);
+
+ html += '' + grade + ' ' + '' + result.name + '
' + result.message + '
';
+
+ if (result.components && result.components.length > 0) {
+ html += '';
+ for (j = 0; j < result.components.length; j += 1) {
+ if (typeof result.components[j] === "string") {
+ html += '' + result.components[j] + ' ';
+ } else if (result.components[j].url !== undefined) {
+ html += '' + YSLOW.util.briefUrl(result.components[j].url, 60) + ' ';
+ }
+ }
+ html += ' ';
+ }
+
+ html += ' ';
+ }
+ }
+ html += '
';
+ return html;
+ },
+
+ getFilterCode: function (categories, results, grade, url) {
+ var html, id, i, len, link, result, score,
+ total = results.length,
+ array = [];
+
+ for (id in categories) {
+ if (categories.hasOwnProperty(id) && categories[id]) {
+ array.push(id);
+ }
+ }
+ array.sort();
+
+ html = '' + 'ALL (' + total + ') ' + 'FILTER BY: ';
+
+ for (i = 0, len = array.length; i < len; i += 1) {
+ html += '' + array[i].toUpperCase() + ' (' + categories[array[i]] + ') ';
+ }
+
+ // social
+ link = 'http://yslow.org/scoremeter/?url=' +
+ encodeURIComponent(url) + '&grade=' + grade;
+ for (i = 0; i < total; i += 1) {
+ result = results[i];
+ score = parseInt(result.score, 10);
+ if (score >= 0 && score < 100) {
+ link += '&' + result.rule_id.toLowerCase() + '=' + score;
+ }
+ }
+
+ // for some reason window.open mess with decoding, thus encoding twice
+ link = encodeURIComponent(encodeURIComponent(link));
+ url = encodeURIComponent(encodeURIComponent(url.slice(0, 60) + (url.length > 60 ? '...' : '')));
+
+ html += 'Share ';
+ html += ' ';
+
+ html += ' ';
+
+ return html;
+ },
+
+ /**
+ * @member YSLOW.HTMLRenderer
+ * Generate HTML code for Grade screen
+ * @param {YSLOW.ResultSet} resultset
+ * @return html code for Grade screen
+ * @type String
+ */
+ reportcardView: function (resultset) {
+ var overall_grade, i, j, k, result, grade, index, sClass, grade_class, score, messages, comp, string, rule,
+ html = '',
+ appliedRuleset = resultset.getRulesetApplied(),
+ results = resultset.getResults(),
+ url = resultset.url,
+ title = 'Grade',
+ tab_label_html = '',
+ tab_html = '',
+ categories = {};
+
+
+ if (YSLOW.doc) {
+ if (YSLOW.doc.view_names && YSLOW.doc.view_names.grade) {
+ title = YSLOW.doc.view_names.grade;
+ }
+ }
+
+ overall_grade = YSLOW.util.prettyScore(resultset.getOverallScore());
+
+ if (YSLOW.renderer.bPrintable) {
+ return this.reportcardPrintableView(results, overall_grade, appliedRuleset);
+ }
+
+ html += '
' + title + '
' + '' + overall_grade + '
' + 'Overall performance score ' + Math.round(resultset.getOverallScore()) + ' ' + 'Ruleset applied: ' + appliedRuleset.name + ' ' + 'URL: ' + YSLOW.util.briefUrl(url, 100) + ' ' + '
';
+
+
+ for (i = 0; i < results.length; i += 1) {
+ result = results[i];
+ if (typeof result === "object") {
+ grade = YSLOW.util.prettyScore(result.score);
+ index = i + 1;
+ sClass = '';
+ grade_class = 'grade-' + (grade === "N/A" ? 'NA' : grade);
+ score = parseInt(result.score, 10);
+ if (isNaN(score) || result.score === -1) {
+ score = "n/a";
+ } else {
+ score += "%";
+ }
+
+ tab_label_html += '
0) {
+ sClass += ' ';
+ }
+ sClass += result.category[k];
+ // update filter categories
+ if (categories[result.category[k]] === undefined) {
+ categories[result.category[k]] = 0;
+ }
+ categories[result.category[k]] += 1;
+ }
+ }
+ if (sClass.length > 0) {
+ tab_label_html += ' class="' + sClass + '"';
+ }
+ tab_label_html += ' onclick="javascript:document.ysview.onclickResult(event)">' + '' + '' + '' + grade + ' ' + '' + result.name + '
';
+
+ tab_html += '
Grade ' + grade + ' on ' + result.name + ' ' + result.message + ' ';
+
+ if (result.components && result.components.length > 0) {
+ tab_html += '
';
+ for (j = 0; j < result.components.length; j += 1) {
+ comp = result.components[j];
+ if (typeof comp === "string") {
+ tab_html += '' + comp + ' ';
+ } else if (comp.url !== undefined) {
+ tab_html += '';
+ string = result.rule_id.toLowerCase();
+ if (result.rule_id.match('expires')) {
+ tab_html += '(' + YSLOW.util.prettyExpiresDate(comp.expires) + ') ';
+ }
+ tab_html += YSLOW.util.prettyAnchor(comp.url, comp.url, undefined, true, 120, undefined, comp.type) + ' ';
+ }
+ }
+ tab_html += ' ';
+ }
+ tab_html += '';
+
+ rule = YSLOW.controller.getRule(result.rule_id);
+
+ if (rule) {
+ tab_html += '
' + (rule.info || '** To be added **') + '
';
+
+ if (rule.url !== undefined) {
+ tab_html += '
» Read More
';
+
+ }
+ }
+
+ tab_html += '
';
+ }
+ }
+
+ html += '
' + this.getFilterCode(categories, results, overall_grade, url) + '
' + '
' + '
' + tab_html + '
' + '
' + YSLOW.doc.copyright + '
' + '
';
+
+ return html;
+ },
+
+ /**
+ * @member YSLOW.HTMLRenderer
+ * Generate HTML code for Stats screen
+ * @param {Object} stats page stats
+ *
+ * PAGE.totalObjCountPrimed a hash of components count group by type (primed cache)
+ * PAGE.totalObjSizePrimed a hash of components size group by type (primed cache)
+ * PAGE.totalObjRequestsPrimed total number of requests (primed cache)
+ * PAGE.totalSizePrimed total size of all components (primed cache)
+ * PAGE.totalObjCount a hash of components count group by type (empty cache)
+ * PAGE.totalObjSize a hash of components size group by type (empty cache)
+ * PAGE.totalObjRequests total number of requests (empty cache)
+ * PAGE.totalSize total size of all components (empty cache)
+ *
+ * @return html code for Stats screen
+ * @type String
+ */
+ statsView: function (stats) {
+ var sText = '',
+ title = 'Stats';
+
+ if (YSLOW.doc) {
+ if (YSLOW.doc.view_names && YSLOW.doc.view_names.stats) {
+ title = YSLOW.doc.view_names.stats;
+ }
+ }
+
+ sText += '' + '
' + title + ' The page has a total of ' + '' + stats.PAGE.totalRequests + ' ' + ' HTTP requests and a total weight of ' + '' + YSLOW.util.kbSize(stats.PAGE.totalSize) + ' ' + ' bytes with empty cache
';
+
+ // Page summary.
+ sText += '';
+
+ sText += '
' + '
' + '
' + YSLOW.renderer.genStats(stats, false) + '
';
+
+ sText += '
' + '
' + '
' + YSLOW.renderer.genStats(stats, true) + '
';
+
+ sText += '
';
+
+ return sText;
+ },
+
+ /**
+ * @member YSLOW.HTMLRenderer
+ * Generate Html for Tools tab
+ * @param {Array} tools array of tools
+ * @return html for Tools tab
+ * @type String
+ */
+ toolsView: function (tools) {
+ var i, sText, tool,
+ tableHtml = '',
+ title = 'Tools',
+ desc = '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) {
+ desc = YSLOW.doc.tools_desc;
+ }
+ if (YSLOW.doc.view_names && YSLOW.doc.view_names.tools) {
+ title = YSLOW.doc.view_names.tools;
+ }
+ }
+
+ for (i = 0; i < tools.length; i += 1) {
+ tool = tools[i];
+ tableHtml += '' + tool.name + ' - ' + (tool.short_desc || 'Short text here explaining what are the main benefits of running this App') + ' ';
+ }
+
+ tableHtml += '
';
+
+ sText = '';
+
+ return sText;
+ },
+
+ /**
+ * @member YSLOW.HTMLRenderer
+ * Generate Html for Ruleset Settings Screen
+ * @param {Object} rulesets a hash of rulesets with { ruleset-name => ruleset }
+ * @return html code for Ruleset Settings screen
+ * @type String
+ */
+ rulesetEditView: function (rulesets) {
+ var id, ruleset, tab_id, sText,
+ settingsHtml = '',
+ navHtml, contentHtml,
+ index = 0,
+ custom = false,
+ selectedRuleset,
+ defaultRulesetId,
+ title = 'Rule Settings',
+ desc = '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) {
+ desc = YSLOW.doc.rulesettings_desc;
+ }
+ if (YSLOW.doc.view_names && YSLOW.doc.view_names.rulesetedit) {
+ title = YSLOW.doc.view_names.rulesetedit;
+ }
+ }
+
+ defaultRulesetId = YSLOW.controller.getDefaultRulesetId();
+
+ navHtml = '
';
+ contentHtml = '
' + YSLOW.renderer.genRulesetEditForm(selectedRuleset) + '
';
+
+ settingsHtml += navHtml + contentHtml;
+
+ sText = '
' + '
' + title + ' ' + desc + '
' + settingsHtml + '
';
+
+ return sText;
+ },
+
+ /**
+ * @private
+ */
+ rulesetEditUpdateTab: function (doc, form, ruleset, updateAction, updateSelection) {
+ var ul_elem, content, li_elem, index, id, tab_id, event, custom_set_title,
+ label_id, idx, prev_li_elem, header, event2,
+ container = form.parentNode.parentNode.parentNode;
+
+ if (container && container.id === 'settingsDiv' && ruleset.custom === true) {
+ ul_elem = container.firstChild;
+ content = ul_elem.nextSibling;
+
+ if (updateAction < 1) {
+ // for delete, we'll need to identify the tab to update.
+ li_elem = ul_elem.firstChild;
+ while (li_elem) {
+ index = li_elem.className.indexOf('ruleset-');
+ if (index !== -1) {
+ id = li_elem.className.substring(index + 8);
+ index = id.indexOf(" ");
+ if (index !== -1) {
+ id = id.substring(0, index);
+ }
+ if (ruleset.id === id) {
+ index = li_elem.id.indexOf('label');
+ if (index !== -1) {
+ tab_id = li_elem.id.substring(index + 5);
+ if (li_elem.className.indexOf('selected') !== -1) {
+ // the tab we're removing is the selected tab, select the last non-header tab.
+ event = {};
+ event.currentTarget = prev_li_elem;
+ doc.ysview.onclickRuleset(event);
+ }
+ // check if we are removing the last custom ruleset.
+ if (li_elem.previousSibling && li_elem.previousSibling.id === 'custom-set-title' && li_elem.nextSibling && li_elem.nextSibling.id === 'create-ruleset') {
+ custom_set_title = li_elem.previousSibling;
+ }
+ ul_elem.removeChild(li_elem);
+ if (custom_set_title) {
+ ul_elem.removeChild(custom_set_title);
+ }
+ }
+ break;
+ } else {
+ prev_li_elem = li_elem;
+ }
+ }
+ li_elem = li_elem.nextSibling;
+ }
+ } else {
+ li_elem = ul_elem.lastChild;
+ while (li_elem) {
+ idx = li_elem.id.indexOf('label');
+ if (idx !== -1) {
+ label_id = li_elem.id.substring(idx + 5);
+ break;
+ }
+ li_elem = li_elem.previousSibling;
+ }
+
+ label_id = Number(label_id) + 1;
+ li_elem = doc.createElement('li');
+ li_elem.className = 'ruleset-' + ruleset.id;
+ li_elem.id = 'label' + label_id;
+ li_elem.onclick = function (event) {
+ doc.ysview.onclickRuleset(event);
+ };
+ li_elem.innerHTML = '
' + ruleset.name + ' ';
+ ul_elem.insertBefore(li_elem, ul_elem.lastChild); // lastChild is the "New Set" button.
+ header = ul_elem.firstChild;
+ while (header) {
+ if (header.id && header.id === 'custom-set-title') {
+ custom_set_title = header;
+ break;
+ }
+ header = header.nextSibling;
+ }
+ if (!custom_set_title) {
+ custom_set_title = doc.createElement('li');
+ custom_set_title.className = 'new-section header';
+ custom_set_title.id = 'custom-set-title';
+ custom_set_title.innerHTML = 'CUSTOM SETS';
+ ul_elem.insertBefore(custom_set_title, li_elem);
+ }
+
+ if (updateSelection) {
+ event2 = {};
+ event2.currentTarget = li_elem;
+ doc.ysview.onclickRuleset(event2);
+ }
+ }
+ }
+
+ },
+
+ /**
+ * @private
+ * Helper function to find if name is in class_name.
+ * @param {String} class_name
+ * @param {String} name
+ * @return true if name is a substring of class_name, false otherwise.
+ * @type Boolean
+ */
+ hasClassName: function (class_name, name) {
+ var i,
+ arr_class = class_name.split(" ");
+
+ if (arr_class) {
+ for (i = 0; i < arr_class.length; i += 1) {
+ if (arr_class[i] === name) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * @member YSLOW.HTMLRenderer
+ * Expand or collapse the rows in components table that matches type.
+ * @param {Document} doc Document object of YSlow Chrome Window.
+ * @param {HTMLElement} table table element
+ * @param {String} type component type of the rows to be expanded or collapsed
+ * @param {Boolean} expand true to expand, false to collapse. This can be undefined.
+ * @param {Boolean} all true to expand/collapse all, can be undefined.
+ */
+ expandCollapseComponentType: function (doc, table, type, expand, all) {
+ var hiding, i, j, do_all, row, span, names, header, className, len,
+ expandAllDiv, elems, expandAllText, checkExpand,
+ hasClass = this.hasClassName,
+ summary = {
+ expand: 0,
+ collapse: 0
+ };
+
+ if (typeof all === "boolean" && all === true) {
+ do_all = true;
+ }
+
+ if (table) {
+ for (i = 0, len = table.rows.length; i < len; i += 1) {
+ row = table.rows[i];
+ className = row.className;
+ if (hasClass(className, 'type-summary')) {
+ if (hasClass(className, 'expand')) {
+ summary.expand += 1;
+ hiding = false;
+ } else if (hasClass(className, 'collapse')) {
+ summary.collapse += 1;
+ hiding = true;
+ }
+ span = row.getElementsByTagName('span')[0];
+ if (do_all || hasClass(span.className, 'type-' + type)) {
+ if (do_all) {
+ names = span.className.split(' ');
+ for (j = 0; j < names.length; j += 1) {
+ if (names[j].substring(0, 5) === 'type-') {
+ type = names[j].substring(5);
+ }
+ }
+ }
+ if (typeof hiding !== "boolean" || (typeof expand === "boolean" && expand === hiding)) {
+ if (do_all) {
+ hiding = !expand;
+ continue;
+ } else {
+ return;
+ }
+ }
+ YSLOW.view.removeClassName(row, (hiding ? 'collapse' : 'expand'));
+ row.className += (hiding ? ' expand' : ' collapse');
+ if (hiding) {
+ summary.collapse -= 1;
+ summary.expand += 1;
+ } else {
+ summary.collapse += 1;
+ summary.expand -= 1;
+ }
+ }
+ } else if (hasClass(className, 'type-' + type)) {
+ if (hiding) {
+ row.style.display = "none";
+ // next sibling should be its header, collapse it too.
+ header = row.nextSibling;
+ if (header.id.indexOf('compHeaders') !== -1) {
+ header.style.display = "none";
+ }
+ } else {
+ row.style.display = "table-row";
+ }
+ }
+ }
+ }
+
+ // now check all type and see if we need to toggle "expand all" and "collapse all".
+ if (summary.expand === 0 || summary.collapse === 0) {
+ expandAllDiv = table.parentNode.previousSibling;
+ if (expandAllDiv) {
+ elems = expandAllDiv.getElementsByTagName('span');
+ for (i = 0; i < elems.length; i += 1) {
+ if (elems[i].id === "expand-all-text") {
+ expandAllText = elems[i];
+ }
+ }
+
+ checkExpand = false;
+
+ if (expandAllText.innerHTML.indexOf('Expand') !== -1) {
+ checkExpand = true;
+ }
+
+ // toggle
+ if (checkExpand) {
+ if (summary.expand === 0) {
+ expandAllText.innerHTML = 'Collapse All';
+ }
+ } else if (summary.collapse === 0) {
+ expandAllText.innerHTML = 'Expand All';
+ }
+ }
+ }
+ },
+
+ /**
+ * @member YSLOW.HTMLRenderer
+ * Expand all component rows in components table.
+ * @param {Document} doc Document object of YSlow Chrome Window.
+ * @param {HTMLElement} table table element
+ */
+ expandAllComponentType: function (doc, table) {
+ var elem, i,
+ expand = false,
+ expandAllDiv = table.parentNode.previousSibling,
+ elems = expandAllDiv.getElementsByTagName('span');
+
+ for (i = 0; i < elems.length; i += 1) {
+ if (elems[i].id === "expand-all-text") {
+ elem = elems[i];
+ }
+ }
+ if (elem) {
+ if (elem.innerHTML.indexOf('Expand') !== -1) {
+ expand = true;
+ }
+ }
+
+ this.expandCollapseComponentType(doc, table, undefined, expand, true);
+
+ if (elem) {
+ elem.innerHTML = (expand ? 'Collapse All' : 'Expand All');
+ }
+ },
+
+ /**
+ * @member YSLOW.HTMLRenderer
+ * Regenerate Components Table.
+ * @param {Document} doc Document object of YSlow Chrome Window
+ * @param {HTMLElement} table table element
+ * @param {String} column_name Column to sort by
+ * @param {Boolean} sortDesc true if sort descending order, false otherwise
+ * @param {YSlow.ComponentSet} cset ComponentSet object
+ */
+ regenComponentsTable: function (doc, table, column_name, sortDesc, cset) {
+ var show, elem, tableHtml;
+
+ if (table) {
+ if (sortDesc === undefined) {
+ sortDesc = false;
+ }
+ // hide or show expand-all
+ if (column_name === "type") {
+ show = true;
+ }
+ elem = table.parentNode.previousSibling;
+ if (elem.id === 'expand-all') {
+ elem.style.visibility = (show ? 'visible' : 'hidden');
+ }
+
+ tableHtml = this.genComponentsTable(cset.components, column_name, sortDesc);
+ table.parentNode.innerHTML = tableHtml;
+ }
+ },
+
+ /**
+ * @member YSLOW.HTMLRenderer
+ * Save Ruleset.
+ * @param {Document} doc Document Object of YSlow Chrome Window
+ * @param {HTMLElement} form Form element
+ */
+ saveRuleset: function (doc, form) {
+ var i, elem, index, id, saveas_name, ruleset_name, ruleset_id, rules,
+ ruleset = {},
+ weights = {};
+
+ if (form) {
+ ruleset.custom = true;
+ ruleset.rules = {};
+ ruleset.weights = {};
+
+ for (i = 0; i < form.elements.length; i += 1) {
+ elem = form.elements[i];
+
+ // build out ruleset object with the form elements.
+ if (elem.name === 'rules' && elem.type === 'checkbox') {
+ if (elem.checked) {
+ ruleset.rules[elem.value] = {};
+ }
+ } else if (elem.name === 'saveas-name') {
+ saveas_name = elem.value;
+ } else if (elem.name === 'ruleset-name') {
+ ruleset_name = elem.value;
+ } else if (elem.name === 'ruleset-id') {
+ ruleset_id = elem.value;
+ } else if ((index = elem.name.indexOf('weight-')) !== -1) {
+ weights[elem.name.substring(index)] = elem.value;
+ }
+ }
+ rules = ruleset.rules;
+ for (id in rules) {
+ if (rules.hasOwnProperty(id) && weights['weight-' + id]) {
+ ruleset.weights[id] = parseInt(weights['weight-' + id], 10);
+ }
+ }
+
+ if (saveas_name) {
+ ruleset.id = saveas_name.replace(/\s/g, "-");
+ ruleset.name = saveas_name;
+ } else {
+ ruleset.id = ruleset_id;
+ ruleset.name = ruleset_name;
+ }
+
+ // register ruleset
+ if (ruleset.id && ruleset.name) {
+ YSLOW.controller.addRuleset(ruleset, true);
+
+ // save to pref
+ YSLOW.controller.saveRulesetToPref(ruleset);
+
+ // update UI
+ if (saveas_name !== undefined) {
+ this.updateRulesetUI(doc, form, ruleset, 1);
+ }
+ }
+ }
+ },
+
+ updateRulesetUI: function (doc, form, ruleset, updateAction) {
+ var i, forms = doc.getElementsByTagName('form');
+
+ for (i = 0; i < forms.length; i += 1) {
+ if (forms[i].id === form.id) {
+ this.rulesetEditUpdateTab(doc, forms[i], ruleset, updateAction, (forms[i] === form));
+ }
+ }
+ doc.ysview.updateRulesetList();
+ },
+
+ /**
+ * @member YSLOW.HTMLRenderer
+ * Delete the current selected ruleset in Ruleset settings screen.
+ * @param {Document} doc Document object of YSlow Chrome Window.
+ * @param {HTMLElement} form Form element
+ */
+ deleteRuleset: function (doc, form) {
+ var ruleset_id = this.getEditFormRulesetId(form),
+ ruleset = YSLOW.controller.removeRuleset(ruleset_id);
+
+ if (ruleset && ruleset.custom) {
+ // remove from pref
+ YSLOW.controller.deleteRulesetFromPref(ruleset);
+
+ // update UI
+ this.updateRulesetUI(doc, form, ruleset, -1);
+ }
+ },
+
+ /**
+ * @member YSLOW.HTMLRenderer
+ * Get form id from Ruleset Settings screen.
+ * @param {DOMElement} form Form element
+ */
+ getEditFormRulesetId: function (form) {
+ var i,
+ aInputs = form.getElementsByTagName('input');
+
+ for (i = 0; i < aInputs.length; i += 1) {
+ if (aInputs[i].name === 'ruleset-id') {
+ return aInputs[i].value;
+ }
+ }
+
+ return undefined;
+ }
+
+});
+
+YSLOW.registerRenderer({
+ /**
+ * @member YSLOW.XMLRenderer
+ * @final
+ */
+ id: 'xml',
+ /**
+ * @member YSLOW.XMLRenderer
+ * @final
+ */
+ supports: {
+ components: 1,
+ reportcard: 1,
+ stats: 1
+ },
+
+ /**
+ * @member YSLOW.XMLRenderer
+ * Generate XML code for Components tab
+ * @param {Array} comps array of components
+ * @param {Number} totalSize total page size
+ * @return XML code for Components tab
+ * @type String
+ */
+ componentsView: function (comps, totalSize) {
+ var i, cookieSize,
+ sText = '
';
+
+ for (i = 0; i < comps.length; i += 1) {
+ sText += '';
+ sText += '' + comps[i].type + ' ';
+ sText += '' + comps[i].size + ' ';
+ if (comps[i].compressed === false) {
+ sText += ' ';
+ } else {
+ sText += '' + (comps[i].size_compressed !== undefined ? parseInt(comps[i].size_compressed, 10) : 'uncertain') + ' ';
+ }
+ cookieSize = comps[i].getSetCookieSize();
+ if (cookieSize > 0) {
+ sText += '' + parseInt(cookieSize, 10) + ' ';
+ }
+ cookieSize = comps[i].getReceivedCookieSize();
+ if (cookieSize > 0) {
+ sText += '' + parseInt(cookieSize, 10) + ' ';
+ }
+ sText += '' + encodeURI(comps[i].url) + ' ';
+ sText += '' + comps[i].expires + ' ';
+ sText += '' + comps[i].respTime + ' ';
+ sText += '' + comps[i].getEtag() + ' ';
+ sText += ' ';
+ }
+ sText += ' ';
+ return sText;
+ },
+
+ /**
+ * @member YSLOW.XMLRenderer
+ * Generate XML code for Grade tab
+ * @param {YSlow.ResultSet} resultset object containing result.
+ * @return xml code for Grades tab
+ * @type String
+ */
+ reportcardView: function (resultset) {
+ var i, j, result,
+ overall_score = resultset.getOverallScore(),
+ overall_grade = YSLOW.util.prettyScore(overall_score),
+ appliedRuleset = resultset.getRulesetApplied(),
+ results = resultset.getResults(),
+ sText = '
';
+
+ sText += ' ';
+
+ for (i = 0; i < results.length; i += 1) {
+ result = results[i];
+
+ sText += '';
+
+ sText += '' + result.message + ' ';
+ if (results.components && results.components.length > 0) {
+ sText += '';
+ for (j = 0; j < result.components.length; j += 1) {
+ if (typeof result.components[j] === "string") {
+ sText += '' + result.components[j] + ' ';
+ } else if (result.components[j].url !== undefined) {
+ sText += '' + result.components[j].url + ' ';
+ }
+ }
+ sText += ' ';
+ }
+ sText += ' ';
+ }
+ sText += ' ';
+ return sText;
+ },
+
+ /**
+ * @member YSLOW.XMLRenderer
+ * Generate XML code for Stats tab
+ * @param {Object} stats page stats
+ *
+ * PAGE.totalObjCountPrimed a hash of components count group by type (primed cache)
+ * PAGE.totalObjSizePrimed a hash of components size group by type (primed cache)
+ * PAGE.totalObjRequestsPrimed total number of requests (primed cache)
+ * PAGE.totalSizePrimed total size of all components (primed cache)
+ * PAGE.totalObjCount a hash of components count group by type (empty cache)
+ * PAGE.totalObjSize a hash of components size group by type (empty cache)
+ * PAGE.totalObjRequests total number of requests (empty cache)
+ * PAGE.totalSize total size of all components (empty cache)
+ *
+ * @return xml code for Stats tab
+ * @type String
+ */
+ statsView: function (stats) {
+ var i, sType, sText,
+ primed_cache_items = '
',
+ empty_cache_items = '',
+ aTypes = YSLOW.peeler.types;
+
+ for (i = 0; i < aTypes.length; i += 1) {
+ sType = aTypes[i];
+ if ((stats.PAGE.totalObjCountPrimed[sType]) !== undefined) {
+ primed_cache_items += ' ';
+ }
+ if ((stats.PAGE.totalObjCount[sType]) !== undefined) {
+ empty_cache_items += ' ';
+ }
+ }
+ primed_cache_items += ' ';
+ empty_cache_items += ' ';
+
+ sText = '
' + primed_cache_items + empty_cache_items + ' ';
+
+ return sText;
+ }
+});
+/**
+ * Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+ * Copyright (c) 2013, Marcel Duran and other contributors. 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, continue: true, maxerr: 50, indent: 4 */
+
+/**
+ * @todo:
+ * - need better way to discover @import stylesheets, the current one doesn't find them
+ * - add request type - post|get - when possible, maybe in the net part of the peeling process
+ *
+ */
+
+/**
+ * Peeler singleton
+ * @class
+ * @static
+ */
+YSLOW.peeler = {
+
+ /**
+ * @final
+ */
+ types: ['doc', 'js', 'css', 'iframe', 'flash', 'cssimage', 'image',
+ 'favicon', 'xhr', 'redirect', 'font'],
+
+ NODETYPE: {
+ ELEMENT: 1,
+ DOCUMENT: 9
+ },
+
+/*
+ * http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSRule
+ */
+ CSSRULE: {
+ IMPORT_RULE: 3,
+ FONT_FACE_RULE: 5
+ },
+
+ /**
+ * Start peeling the document in passed window object.
+ * The component may be requested asynchronously.
+ *
+ * @param {DOMElement} node object
+ * @param {Number} onloadTimestamp onload timestamp
+ * @return ComponentSet
+ * @type YSLOW.ComponentSet
+ */
+ peel: function (node, onloadTimestamp) {
+ // platform implementation goes here
+ },
+
+ /**
+ * @private
+ * Finds all frames/iframes recursively
+ * @param {DOMElement} node object
+ * @return an array of documents in the passed DOM node.
+ * @type Array
+ */
+ findDocuments: function (node) {
+ var frames, doc, docUrl, type, i, len, el, frameDocs, parentDoc,
+ allDocs = {};
+
+ YSLOW.util.event.fire('peelProgress', {
+ 'total_step': 7,
+ 'current_step': 1,
+ 'message': 'Finding documents'
+ });
+
+ if (!node) {
+ return;
+ }
+
+ // check if frame digging was disabled, if so, return the top doc and return.
+ if (!YSLOW.util.Preference.getPref('extensions.yslow.getFramesComponents', true)) {
+ allDocs[node.URL] = {
+ 'document': node,
+ 'type': 'doc'
+ };
+ return allDocs;
+ }
+
+ type = 'doc';
+ if (node.nodeType === this.NODETYPE.DOCUMENT) {
+ // Document node
+ doc = node;
+ docUrl = node.URL;
+ } else if (node.nodeType === this.NODETYPE.ELEMENT &&
+ node.nodeName.toLowerCase() === 'frame') {
+ // Frame node
+ doc = node.contentDocument;
+ docUrl = node.src;
+ } else if (node.nodeType === this.NODETYPE.ELEMENT &&
+ node.nodeName.toLowerCase() === 'iframe') {
+ doc = node.contentDocument;
+ docUrl = node.src;
+ type = 'iframe';
+ try {
+ parentDoc = node.contentWindow;
+ parentDoc = parentDoc && parentDoc.parent;
+ parentDoc = parentDoc && parentDoc.document;
+ parentDoc = parentDoc || node.ownerDocument;
+ if (parentDoc && parentDoc.URL === docUrl) {
+ // check attribute
+ docUrl = !node.getAttribute('src') ? '' : 'about:blank';
+ }
+ } catch (err) {
+ YSLOW.util.dump(err);
+ }
+ } else {
+ return allDocs;
+ }
+ allDocs[docUrl] = {
+ 'document': doc,
+ 'type': type
+ };
+
+ try {
+ frames = doc.getElementsByTagName('iframe');
+ for (i = 0, len = frames.length; i < len; i += 1) {
+ el = frames[i];
+ if (el.src) {
+ frameDocs = this.findDocuments(el);
+ if (frameDocs) {
+ allDocs = YSLOW.util.merge(allDocs, frameDocs);
+ }
+ }
+ }
+
+ frames = doc.getElementsByTagName('frame');
+ for (i = 0, len = frames.length; i < len; i += 1) {
+ el = frames[i];
+ frameDocs = this.findDocuments(el);
+ if (frameDocs) {
+ allDocs = YSLOW.util.merge(allDocs, frameDocs);
+ }
+ }
+ } catch (e) {
+ YSLOW.util.dump(e);
+ }
+
+ return allDocs;
+ },
+
+ /**
+ * @private
+ * Find all components in the passed node.
+ * @param {DOMElement} node DOM object
+ * @param {String} doc_location document.location
+ * @param {String} baseHref href
+ * @return array of object (array[] = {'type': object.type, 'href': object.href } )
+ * @type Array
+ */
+ findComponentsInNode: function (node, baseHref, type) {
+ var comps = [];
+
+ try {
+ comps = this.findStyleSheets(node, baseHref);
+ } catch (e1) {
+ YSLOW.util.dump(e1);
+ }
+ try {
+ comps = comps.concat(this.findScripts(node));
+ } catch (e2) {
+ YSLOW.util.dump(e2);
+ }
+ try {
+ comps = comps.concat(this.findFlash(node));
+ } catch (e3) {
+ YSLOW.util.dump(e3);
+ }
+ try {
+ comps = comps.concat(this.findCssImages(node));
+ } catch (e4) {
+ YSLOW.util.dump(e4);
+ }
+ try {
+ comps = comps.concat(this.findImages(node));
+ } catch (e5) {
+ YSLOW.util.dump(e5);
+ }
+ try {
+ if (type === 'doc') {
+ comps = comps.concat(this.findFavicon(node, baseHref));
+ }
+ } catch (e6) {
+ YSLOW.util.dump(e6);
+ }
+
+ return comps;
+ },
+
+ /**
+ * @private
+ * Add components in Net component that are not component list found by
+ * peeler. These can be xhr requests or images that are preloaded by
+ * javascript.
+ *
+ * @param {YSLOW.ComponentSet} component_set ComponentSet to be checked
+ * against.
+ * @param {String} base_herf base href
+ */
+ addComponentsNotInNode: function (component_set, base_href) {
+ var i, j, imgs, type, objs,
+ types = ['flash', 'js', 'css', 'doc', 'redirect'],
+ xhrs = YSLOW.net.getResponseURLsByType('xhr');
+
+ // Now, check net module for xhr component.
+ if (xhrs.length > 0) {
+ for (j = 0; j < xhrs.length; j += 1) {
+ component_set.addComponent(xhrs[j], 'xhr', base_href);
+ }
+ }
+
+ // check image beacons
+ imgs = YSLOW.net.getResponseURLsByType('image');
+ if (imgs.length > 0) {
+ for (j = 0; j < imgs.length; j += 1) {
+ type = 'image';
+ if (imgs[j].indexOf("favicon.ico") !== -1) {
+ type = 'favicon';
+ }
+ component_set.addComponentNoDuplicate(imgs[j], type, base_href);
+ }
+ }
+
+ // should we check other types?
+ for (i = 0; i < types.length; i += 1) {
+ objs = YSLOW.net.getResponseURLsByType(types[i]);
+ for (j = 0; j < objs.length; j += 1) {
+ component_set.addComponentNoDuplicate(objs[j], types[i], base_href);
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Find all stylesheets in the passed DOM node.
+ * @param {DOMElement} node DOM object
+ * @param {String} doc_location document.location
+ * @param {String} base_href base href
+ * @return array of object (array[] = {'type' : 'css', 'href': object.href})
+ * @type Array
+ */
+ findStyleSheets: function (node, baseHref) {
+ var styles, style, i, len,
+ head = node.getElementsByTagName('head')[0],
+ body = node.getElementsByTagName('body')[0],
+ comps = [],
+ that = this,
+
+ loop = function (els, container) {
+ var i, len, el, href, cssUrl;
+
+ for (i = 0, len = els.length; i < len; i += 1) {
+ el = els[i];
+ href = el.href || el.getAttribute('href');
+ if (href && (el.rel === 'stylesheet' ||
+ el.type === 'text/css')) {
+ comps.push({
+ type: 'css',
+ href: href === node.URL ? '' : href,
+ containerNode: container
+ });
+ cssUrl = YSLOW.util.makeAbsoluteUrl(href, baseHref);
+ comps = comps.concat(that.findImportedStyleSheets(el.sheet, cssUrl));
+ }
+ }
+ };
+
+ YSLOW.util.event.fire('peelProgress', {
+ 'total_step': 7,
+ 'current_step': 2,
+ 'message': 'Finding StyleSheets'
+ });
+
+ if (head || body) {
+ if (head) {
+ loop(head.getElementsByTagName('link'), 'head');
+ }
+ if (body) {
+ loop(body.getElementsByTagName('link'), 'body');
+ }
+ } else {
+ loop(node.getElementsByTagName('link'));
+ }
+
+ styles = node.getElementsByTagName('style');
+ for (i = 0, len = styles.length; i < len; i += 1) {
+ style = styles[i];
+ comps = comps.concat(that.findImportedStyleSheets(style.sheet, baseHref));
+ }
+
+ return comps;
+ },
+
+ /**
+ * @private
+ * Given a css rule, if it's an "@import" rule then add the style sheet
+ * component. Also, do a recursive check to see if this imported stylesheet
+ * itself contains an imported stylesheet. (FF only)
+ * @param {DOMElement} stylesheet DOM stylesheet object
+ * @return array of object
+ * @type Array
+ */
+ findImportedStyleSheets: function (styleSheet, parentUrl) {
+ var i, rules, rule, cssUrl, ff, len,
+ reFile = /url\s*\(["']*([^"'\)]+)["']*\)/i,
+ comps = [];
+
+ try {
+ if (!(rules = styleSheet.cssRules)) {
+ return comps;
+ }
+ for (i = 0, len = rules.length; i < len; i += 1) {
+ rule = rules[i];
+ if (rule.type === YSLOW.peeler.CSSRULE.IMPORT_RULE && rule.styleSheet && rule.href) {
+ // It is an imported stylesheet!
+ comps.push({
+ type: 'css',
+ href: rule.href,
+ base: parentUrl
+ });
+ // Recursively check if this stylesheet itself imports any other stylesheets.
+ cssUrl = YSLOW.util.makeAbsoluteUrl(rule.href, parentUrl);
+ comps = comps.concat(this.findImportedStyleSheets(rule.styleSheet, cssUrl));
+ } else if (rule.type === YSLOW.peeler.CSSRULE.FONT_FACE_RULE) {
+ if (rule.style && typeof rule.style.getPropertyValue === 'function') {
+ ff = rule.style.getPropertyValue('src');
+ ff = reFile.exec(ff);
+ if (ff) {
+ ff = ff[1];
+ comps.push({
+ type: 'font',
+ href: ff,
+ base: parentUrl
+ });
+ }
+ }
+ } else {
+ break;
+ }
+ }
+ } catch (e) {
+ YSLOW.util.dump(e);
+ }
+
+ return comps;
+ },
+
+ /**
+ * @private
+ * Find all scripts in the passed DOM node.
+ * @param {DOMElement} node DOM object
+ * @return array of object (array[] = {'type': 'js', 'href': object.href})
+ * @type Array
+ */
+ findScripts: function (node) {
+ var comps = [],
+ head = node.getElementsByTagName('head')[0],
+ body = node.getElementsByTagName('body')[0],
+
+ loop = function (scripts, container) {
+ var i, len, script, type, src;
+
+ for (i = 0, len = scripts.length; i < len; i += 1) {
+ script = scripts[i];
+ type = script.type;
+ if (type &&
+ type.toLowerCase().indexOf('javascript') < 0) {
+ continue;
+ }
+ src = script.src || script.getAttribute('src');
+ if (src) {
+ comps.push({
+ type: 'js',
+ href: src === node.URL ? '' : src,
+ containerNode: container
+ });
+ }
+ }
+ };
+
+ YSLOW.util.event.fire('peelProgress', {
+ 'total_step': 7,
+ 'current_step': 3,
+ 'message': 'Finding JavaScripts'
+ });
+
+ if (head || body) {
+ if (head) {
+ loop(head.getElementsByTagName('script'), 'head');
+ }
+ if (body) {
+ loop(body.getElementsByTagName('script'), 'body');
+ }
+ } else {
+ loop(node.getElementsByTagName('script'));
+ }
+
+ return comps;
+ },
+
+ /**
+ * @private
+ * Find all flash in the passed DOM node.
+ * @param {DOMElement} node DOM object
+ * @return array of object (array[] = {'type' : 'flash', 'href': object.href } )
+ * @type Array
+ */
+ findFlash: function (node) {
+ var i, el, els, len,
+ comps = [];
+
+ YSLOW.util.event.fire('peelProgress', {
+ 'total_step': 7,
+ 'current_step': 4,
+ 'message': 'Finding Flash'
+ });
+
+ els = node.getElementsByTagName('embed');
+ for (i = 0, len = els.length; i < len; i += 1) {
+ el = els[i];
+ if (el.src) {
+ comps.push({
+ type: 'flash',
+ href: el.src
+ });
+ }
+ }
+
+ els = node.getElementsByTagName('object');
+ for (i = 0, len = els.length; i < len; i += 1) {
+ el = els[i];
+ if (el.data && el.type === 'application/x-shockwave-flash') {
+ comps.push({
+ type: 'flash',
+ href: el.data
+ });
+ }
+ }
+
+ return comps;
+ },
+
+ /**
+ * @private
+ * Find all css images in the passed DOM node.
+ * @param {DOMElement} node DOM object
+ * @return array of object (array[] = {'type' : 'cssimage', 'href': object.href } )
+ * @type Array
+ */
+ findCssImages: function (node) {
+ var i, j, el, els, prop, url, len,
+ comps = [],
+ hash = {},
+ props = ['backgroundImage', 'listStyleImage', 'content', 'cursor'],
+ lenJ = props.length;
+
+ YSLOW.util.event.fire('peelProgress', {
+ 'total_step': 7,
+ 'current_step': 5,
+ 'message': 'Finding CSS Images'
+ });
+
+ els = node.getElementsByTagName('*');
+ for (i = 0, len = els.length; i < len; i += 1) {
+ el = els[i];
+ for (j = 0; j < lenJ; j += 1) {
+ prop = props[j];
+ url = YSLOW.util.getComputedStyle(el, prop, true);
+ if (url && !hash[url]) {
+ comps.push({
+ type: 'cssimage',
+ href: url
+ });
+ hash[url] = 1;
+ }
+ }
+ }
+
+ return comps;
+ },
+
+ /**
+ * @private
+ * Find all images in the passed DOM node.
+ * @param {DOMElement} node DOM object
+ * @return array of object (array[] = {'type': 'image', 'href': object.href} )
+ * @type Array
+ */
+ findImages: function (node) {
+ var i, img, imgs, src, len,
+ comps = [],
+ hash = {};
+
+ YSLOW.util.event.fire('peelProgress', {
+ 'total_step': 7,
+ 'current_step': 6,
+ 'message': 'Finding Images'
+ });
+
+ imgs = node.getElementsByTagName('img');
+ for (i = 0, len = imgs.length; i < len; i += 1) {
+ img = imgs[i];
+ src = img.src;
+ if (src && !hash[src]) {
+ comps.push({
+ type: 'image',
+ href: src,
+ obj: {
+ width: img.width,
+ height: img.height
+ }
+ });
+ hash[src] = 1;
+ }
+ }
+
+ return comps;
+ },
+
+ /**
+ * @private
+ * Find favicon link.
+ * @param {DOMElement} node DOM object
+ * @return array of object (array[] = {'type': 'favicon', 'href': object.href} )
+ * @type Array
+ */
+ findFavicon: function (node, baseHref) {
+ var i, len, link, links, rel,
+ comps = [];
+
+ YSLOW.util.event.fire('peelProgress', {
+ 'total_step': 7,
+ 'current_step': 7,
+ 'message': 'Finding favicon'
+ });
+
+ links = node.getElementsByTagName('link');
+ for (i = 0, len = links.length; i < len; i += 1) {
+ link = links[i];
+ rel = (link.rel || '').toLowerCase();
+ if (link.href && (rel === 'icon' ||
+ rel === 'shortcut icon')) {
+ comps.push({
+ type: 'favicon',
+ href: link.href
+ });
+ }
+ }
+
+ // add default /favicon.ico if none informed
+ if (!comps.length) {
+ comps.push({
+ type: 'favicon',
+ href: YSLOW.util.makeAbsoluteUrl('/favicon.ico', baseHref)
+ });
+ }
+
+ return comps;
+ },
+
+ /**
+ * @private
+ * Get base href of document. If
element is not found, use doc.location.
+ * @param {Document} doc Document object
+ * @return base href
+ * @type String
+ */
+ getBaseHref: function (doc) {
+ var base;
+
+ try {
+ base = doc.getElementsByTagName('base')[0];
+ base = (base && base.href) || doc.URL;
+ } catch (e) {
+ YSLOW.util.dump(e);
+ }
+
+ return base;
+ }
+};
+/**
+ * Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+ * Copyright (c) 2013, Marcel Duran and other contributors. All rights reserved.
+ * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
+ */
+
+/*global YSLOW*/
+
+YSLOW.peeler.peel = function (node) {
+ var url, docs, doc, doct, baseHref,
+ comps = [];
+
+ try {
+ // Find all documents in the window.
+ docs = this.findDocuments(node);
+
+ for (url in docs) {
+ if (docs.hasOwnProperty(url)) {
+ doc = docs[url];
+ if (doc) {
+ // add the document.
+ comps.push({
+ type: doc.type,
+ href: url
+ });
+
+ doct = doc.document;
+ if (doct && url) {
+ baseHref = this.getBaseHref(doct);
+ comps = comps.concat(this.findComponentsInNode(doct,
+ baseHref, doc.type));
+ }
+ }
+ }
+ }
+ } catch (err) {
+ YSLOW.util.dump(err);
+ YSLOW.util.event.fire('peelError', {
+ 'message': err
+ });
+ }
+
+ return comps;
+};
+ };
+
+ // serialize YSlow phantomjs object
+ // resources, yslow args and page load time
+ ysphantomjs = 'YSLOW.phantomjs = {' +
+ 'resources: ' + JSON.stringify(resources) + ',' +
+ 'args: ' + JSON.stringify(yslowArgs) + ',' +
+ 'loadTime: ' + JSON.stringify(loadTime) + ',' +
+ 'redirects: ' + JSON.stringify(page.redirects)
+ + '};';
+
+ // YSlow phantomjs controller
+ controller = function () {
+ YSLOW.phantomjs.run = function () {
+ try {
+ var results, xhr, output, threshold,
+ doc = document,
+ ys = YSLOW,
+ yscontext = new ys.context(doc),
+ yspeeler = ys.peeler,
+ comps = yspeeler.peel(doc),
+ baseHref = yspeeler.getBaseHref(doc),
+ cset = new ys.ComponentSet(doc),
+ ysphantomjs = ys.phantomjs,
+ resources = ysphantomjs.resources,
+ args = ysphantomjs.args,
+ ysutil = ys.util,
+ preferences,
+
+ // format out with appropriate content type
+ formatOutput = function (content) {
+ var testResults,
+ format = (args.format || '').toLowerCase(),
+ harness = {
+ 'tap': {
+ func: ysutil.formatAsTAP,
+ contentType: 'text/plain'
+ },
+ 'junit': {
+ func: ysutil.formatAsJUnit,
+ contentType: 'text/xml'
+ }
+ };
+
+ switch (format) {
+ case 'xml':
+ return {
+ content: ysutil.objToXML(content),
+ contentType: 'text/xml'
+ };
+ case 'plain':
+ return {
+ content: ysutil.prettyPrintResults(
+ content
+ ),
+ contentType: 'text/plain'
+ };
+ // test formats
+ case 'tap':
+ case 'junit':
+ try {
+ threshold = JSON.parse(args.threshold);
+ } catch (err) {
+ threshold = args.threshold;
+ }
+ testResults = harness[format].func(
+ ysutil.testResults(
+ content,
+ threshold
+ )
+ );
+ return {
+ content: testResults.content,
+ contentType: harness[format].contentType,
+ failures: testResults.failures
+ };
+ default:
+ return {
+ content: JSON.stringify(content),
+ contentType: 'application/json'
+ };
+ }
+ },
+
+ // format raw headers into object
+ formatHeaders = function (headers) {
+ var reHeader = /^([^:]+):\s*([\s\S]+)$/,
+ reLineBreak = /[\n\r]/g,
+ header = {};
+
+ headers.split('\n').forEach(function (h) {
+ var m = reHeader.exec(
+ h.replace(reLineBreak, '')
+ );
+
+ if (m) {
+ header[m[1]] = m[2];
+ }
+ });
+
+ return header;
+ };
+
+ comps.forEach(function (comp) {
+ var res = resources[comp.href] ||
+ resources[ys.util.makeAbsoluteUrl(comp.href, comp.base)] || {};
+
+ // if the component hasn't been fetched by phantomjs but discovered by yslow
+ if (res.response === undefined) {
+ try {
+ var headerName, h, i, len, m, startTime, endTime, headers,
+ reHeader = /^([^:]+):\s*([\s\S]+)$/,
+ response = {},
+ request = {};
+ // fetch the asset
+ xhr = new XMLHttpRequest();
+ startTime = new Date().getTime();
+ xhr.open('GET', ys.util.makeAbsoluteUrl(comp.href, comp.base), false);
+ // these are unsafe
+ // if (args.ua) {
+ // xhr.setRequestHeader('User-Agent',args.ua);
+ // }
+ // xhr.setRequestHeader('Access-Control-Request-Method','GET');
+ // xhr.setRequestHeader('Origin',baseHref);
+ xhr.send();
+ endTime = new Date().getTime();
+ headers = xhr.getAllResponseHeaders();
+ h = headers.split('\n');
+
+ // fake the request
+ request.headers = [];
+ request.url = ys.util.makeAbsoluteUrl(comp.href, comp.base);
+ request.method = 'GET';
+ request.time = '2013-05-22T20:40:33.381Z';
+
+ // setup the response
+ // real values will be added to the component
+ // from the header
+ response.bodySize = '-1';
+ response.contentType = '';
+ response.headers = [];
+ response.id = '-1';
+ response.redirectURL = null;
+ response.stage = 'end';
+ response.status = xhr.status;
+ response.time = endTime - startTime;
+ response.url = ys.util.makeAbsoluteUrl(comp.href, comp.base);
+
+ // get the headers
+ h = headers.split('\n');
+ for (i = 0, len = h.length; i < len; i += 1) {
+ m = reHeader.exec(h[i]);
+ if (m) {
+ response.headers.push({'name': m[1], 'value': m[2]});
+ }
+ }
+
+ res.response = response;
+ res.request = request;
+
+ } catch (err) {
+ console.log(err);
+ }
+ }
+
+ cset.addComponent(
+ comp.href,
+ comp.type,
+ comp.base || baseHref,
+ {
+ obj: comp.obj,
+ request: res.request,
+ response: res.response
+ }
+ );
+ });
+
+ preferences = new Preferences();
+ preferences.setPref('cdnHostnames', args.cdns);
+ ysutil.Preference.registerNative(preferences);
+
+ // refinement
+ cset.inline = ysutil.getInlineTags(doc);
+ cset.domElementsCount = ysutil.countDOMElements(doc);
+ cset.cookies = cset.doc_comp.cookie;
+ cset.components = ysutil.setInjected(doc,
+ cset.components, cset.doc_comp.body);
+
+ // hack for sitespeed.io 2.0
+ cset.redirects = ysphantomjs.redirects;
+
+ // run analysis
+ yscontext.component_set = cset;
+ ys.controller.lint(doc, yscontext, args.ruleset);
+ yscontext.result_set.url = baseHref;
+ yscontext.PAGE.t_done = ysphantomjs.loadTime;
+ yscontext.collectStats();
+ results = ysutil.getResults(yscontext, args.info);
+
+ // prepare output results
+ if (args.dict && args.format !== 'plain') {
+ results.dictionary = ysutil.getDict(args.info,
+ args.ruleset);
+ }
+ output = formatOutput(results);
+
+ // send beacon
+ if (args.beacon) {
+ try {
+ xhr = new XMLHttpRequest();
+ xhr.onreadystatechange = function () {
+ // in verbose mode, include
+ // beacon response info
+ if (xhr.readyState === 4 && args.verbose) {
+ results.beacon = {
+ status: xhr.status,
+ headers: formatHeaders(
+ xhr.getAllResponseHeaders()
+ ),
+ body: xhr.responseText
+ };
+ output = formatOutput(results);
+ }
+ };
+ xhr.open('POST', args.beacon, false);
+ xhr.setRequestHeader('Content-Type',
+ output.contentType);
+ xhr.send(output.content);
+ } catch (xhrerr) {
+ // include error on beacon
+ if (args.verbose) {
+ results.beacon = {
+ error: xhrerr
+ };
+ output = formatOutput(results);
+ }
+ }
+ }
+
+ return output;
+ } catch (err) {
+ return err;
+ }
+ };
+
+ // Implement a bare minimum preferences object to be able to use custom CDN URLs
+ function Preferences() {
+ this.prefs = {};
+ }
+ Preferences.prototype.getPref = function (name, defaultValue) {
+ return this.prefs.hasOwnProperty(name) ? this.prefs[name] : defaultValue;
+ };
+ Preferences.prototype.setPref = function (name, value) {
+ this.prefs[name] = value;
+ };
+ Preferences.prototype.deletePref = function (name) {
+ delete this.prefs[name];
+ };
+ Preferences.prototype.getPrefList = function (branch_name, default_value) {
+ var values = [], key;
+ for (key in this.prefs) {
+ if (this.prefs.hasOwnProperty(key) && key.indexOf(branch_name) === 0) {
+ values.push({ 'name': key, 'value': this.prefs[key] });
+ }
+ }
+ return values.length === 0 ? default_value : values;
+ };
+
+ return YSLOW.phantomjs.run();
+ };
+
+ // serialize then combine:
+ // YSlow + page resources + args + loadtime + controller
+ yslow = yslow.toString();
+ yslow = yslow.slice(13, yslow.length - 1);
+ // minification removes last ';'
+ if (yslow.slice(yslow.length - 1) !== ';') {
+ yslow += ';';
+ }
+ controller = controller.toString();
+ controller = controller.slice(13, controller.length - 1);
+ evalFunc = new Function(yslow + ysphantomjs + controller);
+
+ // evaluate script and log results
+ output = page.evaluate(evalFunc);
+ exitStatus += output.failures || 0;
+
+ if (yslowArgs.file) {
+ var fs = require('fs');
+ try {
+ fs.write(yslowArgs.file, output.content, 'w');
+ } catch(e) {
+ console.log(e);
+ exitStatus += 1;
+ }
+ }
+ else
+ console.log(output.content);
+ }
+
+ // finish phantomjs
+ urlCount -= 1;
+ if (urlCount === 0) {
+ phantom.exit(exitStatus);
+ }
+ });
+});
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..fdd7a1291
--- /dev/null
+++ b/package.json
@@ -0,0 +1,58 @@
+{
+ "name": "sitespeed.io",
+ "version": "3.0.0-alpha",
+ "bin": "./bin/sitespeed.js",
+ "description": "Analyze your web performance of your site",
+ "keywords": [
+ "performance",
+ "web",
+ "rules",
+ "speed",
+ "navigation-timing",
+ "browser"
+ ],
+ "homepage": "http://www.sitespeed.io",
+ "license": "Apache-2.0",
+ "author": {
+ "name": "Peter Hedenskog",
+ "url": "http://www.peterhedenskog.com"
+ },
+ "contributors": {
+ "name": "Tobias Lidskog"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/sitespeedio/sitespeed.io.git"
+ },
+ "bugs": {
+ "url": "https://github.com/sitespeedio/sitespeed.io/issues"
+ },
+ "files": ["bin","conf","assets","lib","templates"],
+
+ "scripts": {
+ "test": "./node_modules/.bin/mocha"
+ },
+ "engines": {
+ "node": ">=0.8.1"
+ },
+ "devDependencies": {
+ "mocha": "1.20.x",
+ "expect.js": "0.3.x"
+ },
+ "main": "./lib/runner.js",
+ "dependencies": {
+ "handlebars": "1.3.x",
+ "phantomjs": "1.9.x",
+ "async": "0.9.x",
+ "fast-stats": "0.0.2",
+ "fs-extra": "0.9.x",
+ "winston": "0.7.x",
+ "nomnom": "1.7.x",
+ "dateformat": "1.0.x",
+ "xmlbuilder": "2.2.x",
+ "whereis": "0.2.x",
+ "gpagespeed": "0.0.6",
+ "webpagetest": "0.2.x",
+ "simplehar": "0.14.x"
+ }
+}
diff --git a/templates/assets.hb b/templates/assets.hb
new file mode 100644
index 000000000..df9c9a9a3
--- /dev/null
+++ b/templates/assets.hb
@@ -0,0 +1,47 @@
+
+
+{{> header}}
+
+
+
+ {{> assetsSummary}}
+
+
+
+
+
+ asset
+ type
+ time since last modification
+ cache time
+ size (kb)
+ nr of requests
+
+
+
+ {{#each assets}}
+
+ {{> displayUrlHeaders}}{{> displayAssetUrl}}
+ {{this.type}}
+ {{getPrettyPrintSeconds this.timeSinceLastModification}}
+ {{getPrettyPrintSeconds this.cacheTime}}
+ {{getKbSize this.size}}
+ {{this.count}}
+
+ {{/each}}
+
+
+
+
+
+
+
+{{> footer}}
+
+
+
+
diff --git a/templates/detailed-site-summary.hb b/templates/detailed-site-summary.hb
new file mode 100644
index 000000000..4ad877863
--- /dev/null
+++ b/templates/detailed-site-summary.hb
@@ -0,0 +1,45 @@
+
+
+{{> header}}
+
+
+ {{> runSummary}}
+
Detailed summary
+
+
+
+
+
+
+
+
+ name
+ min
+ p10
+ median
+ p80
+ p90
+ p99
+ max
+
+
+
+
+ {{#aggregates}}
+ {{> detailedSummaryRow}}
+ {{/aggregates}}
+
+
+
+
+
+
+{{> footer}}
+
+
+
+
diff --git a/templates/errors.hb b/templates/errors.hb
new file mode 100644
index 000000000..b0a5b8327
--- /dev/null
+++ b/templates/errors.hb
@@ -0,0 +1,20 @@
+
+
+{{> header}}
+
+
+ {{> runSummary}}
+
+
+ Got {{totalErrors}} error{{getPlural totalErrors}}.
+
+ {{{getErrorHTML errors.downloadErrorUrls}}}
+
+ {{{getErrorHTML errors.analysisErrorUrls}}}
+
+
+
+
+{{> footer}}
+
+
diff --git a/templates/page.hb b/templates/page.hb
new file mode 100644
index 000000000..27ca710d4
--- /dev/null
+++ b/templates/page.hb
@@ -0,0 +1,121 @@
+
+
+{{> header}}
+
+
+
+
+
+{{#if config.runYslow}}
+
+
+ {{> displayRulesBelowGrade}}
+
+
+
+
+
+
+
+
+ {{> requestsPerContentType}}
+
+
+
+ {{> sizePerContentType}}
+
+
+
+
+
+ {{> requestsPerDomain}}
+
+
+ {{> pageContentInfoBox}}
+
+
+
+
+
+ {{> cacheBox}}
+
+
+
+
+
+ {{/if}}
+
+ {{#if config.googleKey}}
+
+ {{/if}}
+
+ {{#if config.webpagetest}}
+
+ {{/if}}
+
+ {{#if config.browser}}
+ {{#each browsertimeData}}
+ {{>browserMeasurements}}
+ {{/each}}
+ {{/if}}
+{{> footer}}
+
+
+
+