├── README.md ├── index.html ├── js ├── docs │ ├── docco.css │ └── dtv.remote.api.html ├── dtv.remote.api-min.js ├── dtv.remote.api.js ├── examples │ ├── css │ │ └── dtv.remote.api.example.css │ ├── index.html │ └── js │ │ └── dtv.remote.api.example.js └── test │ ├── index.html │ ├── tests.js │ └── vendor │ ├── jquery-1.7.js │ ├── qunit.css │ ├── qunit.js │ └── underscore-1.2.2.js └── website ├── css ├── bootstrap-custom.css ├── bootstrap-responsive.css └── bootstrap.css └── js ├── bootstrap-dropdown.js ├── bootstrap-modal.js ├── bootstrap-scrollspy.js └── jquery-1.7.1.js /README.md: -------------------------------------------------------------------------------- 1 | # DirecTV Remote API 2 | 3 | This project takes advantage of [GitHub Pages](http://pages.github.com/) and its documentation can be found here: [http://whitlockjc.github.com/directv-remote-api/](http://whitlockjc.github.com/directv-remote-api/) 4 | 5 | ## Tested Hardware 6 | 7 | The DirecTV Remote APIs has been tested on the following platforms: 8 | 9 | * HR24-200 (Software Version: 1.3) 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DirecTV Remote API 5 | 6 | 7 | 8 | 9 | 10 | 21 | 22 | 23 | 24 | 25 | Fork me on GitHub 26 | 27 | 28 | 29 | 49 | 50 | 51 |
52 | 53 | 54 |
55 |
56 |

DirecTV Remote API

57 |

58 | Providing consistent APIs for interacting with your DirecTV STB, because someone had to do it. 59 |

60 |

61 |
62 | 63 | 64 |
65 | 68 |
69 |
70 |

History

71 |

72 | I've often found myself wanting to have a smarter remote control. Sure, I can go buy some multi-device 73 | remote and all but with my smartphone, I already have remotes for most of my media center already. All 74 | that I needed was a remote for my DirecTV STB. 75 |

76 |

77 | As most others do, I looked in my respective smartphone app stores and saw nothing compelling. Being a 78 | hacker, like most reading this, I looked into writing my own and that is when I realized something: DirecTV 79 | doesn't have an official API nor is there any official documentation on the subject. So how is everyone 80 | else doing it? 81 |

82 |

83 | After some port scanning and Google searching, I found out that I 84 | can indeed control my DirecTV STB programmatically but most of the 85 | code I found on the subject were one-off hacks only solving the needs of the project the code was 86 | associated with. So here I am, hoping to provide you with an API, a consistent/common API, for 87 | interacting with the DirecTV STB programmatically. 88 |

89 |
90 |
91 |

Goals

92 |

93 | The goals for this project are to design a very simple, yet full-featured, API for using every 94 | available API provided by DirecTV for interating with your STB 95 | programmatically and to provide this API across a few languages: 96 |

97 |
    98 |
  • Java
  • 99 |
  • JavaScript
  • 100 |
  • Objective-C
  • 101 |
102 |

103 | NOTE The list of proposed/supported languages is not set in stone. 104 |

105 |

106 | The main focus for this project is to provider APIs for languages with a mobile presence which is 107 | why you see the proposed langauges above. 108 |

109 |
110 |
111 |
112 | 113 | 114 |
115 | 118 |
119 |
120 |

Resources

121 |

122 | To the right you will see information on how to download the JavaScript API, documentation on 123 | how to use the JavaScript API and example applications. 124 |

125 |
126 |
127 |

Documentation

128 |

129 | Documentation for the JavaScript API right now consists of two items: 130 |

131 |
132 |
Online Test Suite
133 |
This is a test suite that runs in your browser.
134 |
Annotated Source Code
135 |
This is the actual source code for the JavaScript API presented in annotated form.
136 |
137 |

138 | The annotated source will help you not only see what APIs are available but also what options 139 | are available, or required, when calling the particular functions or creating the particular 140 | objects. Pretty soon we'll have a API documentation but until then, the annotated source and 141 | test suite should show you how to use the API properly. 142 |

143 |

Downloads (Right-click and use "Save as")

144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 |
File nameVersionSizeDescription
dtv.remote.api.js0.0.120kbDevelopment version (Uncompressed with comments)
dtv.remote.api-min.js0.0.18kbProduction version (Minified)
168 |

169 | The JavaScript API has two hard dependencies: 170 |

171 | 175 |

Examples

176 |

177 | The example application is a work in progress that will allow you to 178 | connect to a DirecTV STB on your local network and interact with 179 | it: View information about the currently tuned channel, change channels, see what's currently on 180 | other channels and many more. You can also use the online test suite sources 181 | as a low-level, code-based example on how to use the full JavaScript API. 182 |

183 |
184 |
185 |
186 | 187 | 188 |
189 | 192 |
193 |
194 |

195 | Below you will find links, with explanation, to some very useful resources that either helped in 196 | the creation of these APIs, are using these APIs or are tools/libraries/apis being used by this 197 | project: 198 |

199 |
200 |
201 |

Tools Used

202 |
203 |
Bootstrap
204 |
Bootstrap is a "toolkit from Twitter designed to kickstart development of webapps and sites" and is used for the UI of this site and the JavaScript API example application.
205 |
Docco
206 |
Docco is the tool used to generated the annotated source for the JavaScript API.
207 |
jQuery
208 |
jQuery is the "Write less, do more, JavaScript library" that provides the AJAX functionality for the JavaScript API.
209 |
QUnit
210 |
QUnit is a JavaScript unit test library written by the jQuery guys and is used by our JavaScript API to run its in-browser test suite.
211 |
Underscore
212 |
Underscore is "a utility-belt library for JavaScript" used by the JavaScript API.
213 |
214 |
215 |
216 |

Reading Materials

217 |
218 |
DirecTV SHEF Command Set Public Beta
219 |
This PDF documents the available HTTP-based API for interacting with your DirecTV STB.
220 |
DirecTV Set-Top-Box Information for the Installer
221 |
This PDF documents the available serial commands available to be invoked via the HTTP API documented above.
222 |
223 |

224 | NOTE The DirecTV SHEF Command Set Public Beta and DirecTV Set-Top-Box Information for the Installer 225 | documents are out of date. That being said, with these documents being the closest thing to real DirecTV API documentation they were still essential in 226 | helping create these APIs. 227 |

228 |
229 |
230 |
231 |
232 |
233 |
234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | -------------------------------------------------------------------------------- /js/docs/docco.css: -------------------------------------------------------------------------------- 1 | /*--------------------- Layout and Typography ----------------------------*/ 2 | body { 3 | font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; 4 | font-size: 15px; 5 | line-height: 22px; 6 | color: #252519; 7 | margin: 0; padding: 0; 8 | } 9 | a { 10 | color: #261a3b; 11 | } 12 | a:visited { 13 | color: #261a3b; 14 | } 15 | p { 16 | margin: 0 0 15px 0; 17 | } 18 | h1, h2, h3, h4, h5, h6 { 19 | margin: 0px 0 15px 0; 20 | } 21 | h1 { 22 | margin-top: 40px; 23 | } 24 | #container { 25 | position: relative; 26 | } 27 | #background { 28 | position: fixed; 29 | top: 0; left: 525px; right: 0; bottom: 0; 30 | background: #f5f5ff; 31 | border-left: 1px solid #e5e5ee; 32 | z-index: -1; 33 | } 34 | #jump_to, #jump_page { 35 | background: white; 36 | -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; 37 | -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; 38 | font: 10px Arial; 39 | text-transform: uppercase; 40 | cursor: pointer; 41 | text-align: right; 42 | } 43 | #jump_to, #jump_wrapper { 44 | position: fixed; 45 | right: 0; top: 0; 46 | padding: 5px 10px; 47 | } 48 | #jump_wrapper { 49 | padding: 0; 50 | display: none; 51 | } 52 | #jump_to:hover #jump_wrapper { 53 | display: block; 54 | } 55 | #jump_page { 56 | padding: 5px 0 3px; 57 | margin: 0 0 25px 25px; 58 | } 59 | #jump_page .source { 60 | display: block; 61 | padding: 5px 10px; 62 | text-decoration: none; 63 | border-top: 1px solid #eee; 64 | } 65 | #jump_page .source:hover { 66 | background: #f5f5ff; 67 | } 68 | #jump_page .source:first-child { 69 | } 70 | table td { 71 | border: 0; 72 | outline: 0; 73 | } 74 | td.docs, th.docs { 75 | max-width: 450px; 76 | min-width: 450px; 77 | min-height: 5px; 78 | padding: 10px 25px 1px 50px; 79 | overflow-x: hidden; 80 | vertical-align: top; 81 | text-align: left; 82 | } 83 | .docs pre { 84 | margin: 15px 0 15px; 85 | padding-left: 15px; 86 | } 87 | .docs p tt, .docs p code { 88 | background: #f8f8ff; 89 | border: 1px solid #dedede; 90 | font-size: 12px; 91 | padding: 0 0.2em; 92 | } 93 | .pilwrap { 94 | position: relative; 95 | } 96 | .pilcrow { 97 | font: 12px Arial; 98 | text-decoration: none; 99 | color: #454545; 100 | position: absolute; 101 | top: 3px; left: -20px; 102 | padding: 1px 2px; 103 | opacity: 0; 104 | -webkit-transition: opacity 0.2s linear; 105 | } 106 | td.docs:hover .pilcrow { 107 | opacity: 1; 108 | } 109 | td.code, th.code { 110 | padding: 14px 15px 16px 25px; 111 | width: 100%; 112 | vertical-align: top; 113 | background: #f5f5ff; 114 | border-left: 1px solid #e5e5ee; 115 | } 116 | pre, tt, code { 117 | font-size: 12px; line-height: 18px; 118 | font-family: Monaco, Consolas, "Lucida Console", monospace; 119 | margin: 0; padding: 0; 120 | } 121 | 122 | 123 | /*---------------------- Syntax Highlighting -----------------------------*/ 124 | td.linenos { background-color: #f0f0f0; padding-right: 10px; } 125 | span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } 126 | body .hll { background-color: #ffffcc } 127 | body .c { color: #408080; font-style: italic } /* Comment */ 128 | body .err { border: 1px solid #FF0000 } /* Error */ 129 | body .k { color: #954121 } /* Keyword */ 130 | body .o { color: #666666 } /* Operator */ 131 | body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ 132 | body .cp { color: #BC7A00 } /* Comment.Preproc */ 133 | body .c1 { color: #408080; font-style: italic } /* Comment.Single */ 134 | body .cs { color: #408080; font-style: italic } /* Comment.Special */ 135 | body .gd { color: #A00000 } /* Generic.Deleted */ 136 | body .ge { font-style: italic } /* Generic.Emph */ 137 | body .gr { color: #FF0000 } /* Generic.Error */ 138 | body .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 139 | body .gi { color: #00A000 } /* Generic.Inserted */ 140 | body .go { color: #808080 } /* Generic.Output */ 141 | body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ 142 | body .gs { font-weight: bold } /* Generic.Strong */ 143 | body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 144 | body .gt { color: #0040D0 } /* Generic.Traceback */ 145 | body .kc { color: #954121 } /* Keyword.Constant */ 146 | body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */ 147 | body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */ 148 | body .kp { color: #954121 } /* Keyword.Pseudo */ 149 | body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */ 150 | body .kt { color: #B00040 } /* Keyword.Type */ 151 | body .m { color: #666666 } /* Literal.Number */ 152 | body .s { color: #219161 } /* Literal.String */ 153 | body .na { color: #7D9029 } /* Name.Attribute */ 154 | body .nb { color: #954121 } /* Name.Builtin */ 155 | body .nc { color: #0000FF; font-weight: bold } /* Name.Class */ 156 | body .no { color: #880000 } /* Name.Constant */ 157 | body .nd { color: #AA22FF } /* Name.Decorator */ 158 | body .ni { color: #999999; font-weight: bold } /* Name.Entity */ 159 | body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ 160 | body .nf { color: #0000FF } /* Name.Function */ 161 | body .nl { color: #A0A000 } /* Name.Label */ 162 | body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ 163 | body .nt { color: #954121; font-weight: bold } /* Name.Tag */ 164 | body .nv { color: #19469D } /* Name.Variable */ 165 | body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ 166 | body .w { color: #bbbbbb } /* Text.Whitespace */ 167 | body .mf { color: #666666 } /* Literal.Number.Float */ 168 | body .mh { color: #666666 } /* Literal.Number.Hex */ 169 | body .mi { color: #666666 } /* Literal.Number.Integer */ 170 | body .mo { color: #666666 } /* Literal.Number.Oct */ 171 | body .sb { color: #219161 } /* Literal.String.Backtick */ 172 | body .sc { color: #219161 } /* Literal.String.Char */ 173 | body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */ 174 | body .s2 { color: #219161 } /* Literal.String.Double */ 175 | body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ 176 | body .sh { color: #219161 } /* Literal.String.Heredoc */ 177 | body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ 178 | body .sx { color: #954121 } /* Literal.String.Other */ 179 | body .sr { color: #BB6688 } /* Literal.String.Regex */ 180 | body .s1 { color: #219161 } /* Literal.String.Single */ 181 | body .ss { color: #19469D } /* Literal.String.Symbol */ 182 | body .bp { color: #954121 } /* Name.Builtin.Pseudo */ 183 | body .vc { color: #19469D } /* Name.Variable.Class */ 184 | body .vg { color: #19469D } /* Name.Variable.Global */ 185 | body .vi { color: #19469D } /* Name.Variable.Instance */ 186 | body .il { color: #666666 } /* Literal.Number.Integer.Long */ 187 | -------------------------------------------------------------------------------- /js/dtv.remote.api-min.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Jeremy Whitlock 2 | 3 | (function(){var root=this;var previousDirecTV=root.DirecTV;var DirecTV=root.DirecTV={};var _=root._;var $=root.jQuery;DirecTV.VERSION='0.0.1';DirecTV.noConflict=function(){root.DirecTV=previousDirecTV;return this;};DirecTV.Remote=function(options){options||(options={});if(!options.ipAddress){throw new Error('options.ipAddress is not optional.');} 4 | this.rid=_.uniqueId('dtvr');this.ipAddress=options.ipAddress;};_.extend(DirecTV.Remote.prototype,{Commands:['FA81','FA82','FA83','FA84','FA87','FA90','FA91','FA92','FA93','FA94','FA95','FA96','FAA5','FAA6','FA9A','FA8A','FA8B','FA9D','FA9F',],Holds:['keyUp','keyDown','keyPress'],Keys:['power','poweron','poweroff','format','pause','rew','replay','stop','advance','ffwd','record','play','guide','active','list','exit','back','menu','info','up','down','left','right','select','red','green','yellow','blue','chanup','chandown','prev','0','1','2','3','4','5','6','7','8','9','dash','enter'],validate:function(options){var path='/info/getOptions';var oldCallback;if(!/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/.test(this.ipAddress)){options.callback({status:{code:405,commandResult:1,msg:'Not a valid IP address',query:path}});} 5 | if(!options.callback||typeof options.callback!=='function'){throw new Error('options.callback is not optional and must be a function.');} 6 | oldCallback=options.callback;options.callback=function(data,textStatus,jqXHR){var response;if(data.options){response={status:{code:200,commandResult:0,msg:'Host is a DirecTV set-top-box',query:path}};}else{response={status:{code:404,commandResult:1,msg:'Host does not appear to be a DirecTV set-top-box',query:path}};} 7 | oldCallback(response);};this.makeRequest({path:path,options:options});},getLocations:function(options){this.makeRequest({path:'/info/getLocations',options:options});},getVersion:function(options){this.makeRequest({path:'/info/getVersion',options:options,});},getMode:function(options){this.makeRequest({path:'/info/mode',options:options});},processKey:function(options){if(options&&!options.key){throw new Error('options.key is not optional.');} 8 | this.makeRequest({path:'/remote/processKey',options:options});},processCommand:function(options){if(options&&!options.cmd){throw new Error('options.cmd is not optional.');} 9 | this.makeRequest({path:'/serial/processCommand',options:options});},getOptions:function(options){this.makeRequest({path:'/info/getOptions',options:options});},getProgInfo:function(options){if(options&&!options.major){throw new Error('options.major is not optional.');} 10 | this.makeRequest({path:'/tv/getProgInfo',options:options});},getTuned:function(options){this.makeRequest({path:'/tv/getTuned',options:options});},tune:function(options){if(options&&!options.major){throw new Error('options.major is not optional.');} 11 | this.makeRequest({path:'/tv/tune',options:options});},makeRequest:function(requestOptions){requestOptions||(requestOptions={});requestOptions.options||(requestOptions.options={});var requestUrl='http://'+this.ipAddress+':8080';var requestQuery=requestOptions.path;var requestComplete=false;var knownRequestParams=['clientAddr','cmd','hold','key','major','minor','time','videoWindow','wrapper'];var i;if(!requestOptions.options.callback||typeof requestOptions.options.callback!=='function'){throw new Error('requestOptions.options.callback is not optional and must be a function.');} 12 | if(!requestOptions.path){throw new Error('requestOptions.path is not optional.');} 13 | if(_.intersection(knownRequestParams,_.keys(requestOptions.options)).length>0){requestQuery+='?';} 14 | for(i=0;i 24 | // * [DirecTV Set-Top-Box Information for the Installer](http://www.sbcatest.com/DTV-MD-0058-DIRECTVSet-topInformationforInstallers-V2.2.pdf) 25 | 26 | (function() { 27 | 28 | // Initialization 29 | // -------------- 30 | 31 | // Establish the root object, `window` in the browser, or `global` on the server. 32 | var root = this; 33 | 34 | // Save the previous value of the `DirecTV` variable. 35 | var previousDirecTV = root.DirecTV; 36 | 37 | // The top-level namespace. All public DirecTV classes and modules will 38 | // be attached to this. 39 | // 40 | // **Note:** This needs to be exported for [CommonJS](http://www.commonjs.org/ "CommonJS Homepage") 41 | // and [Node.js](http://nodejs.org "Node.js Homepage"). 42 | var DirecTV = root.DirecTV = {}; 43 | 44 | // [Underscore](http://documentcloud.github.com/underscore/ "Underscore.js Homepage") owns the `_` variable. 45 | var _ = root._; 46 | 47 | // [jQuery](http://jquery.com/ "jQuery Homepage") owns the `$` variable. 48 | // 49 | // **Note:** This needs to be handled on the server-side via a special object. 50 | var $ = root.jQuery; 51 | 52 | // Current version number. 53 | DirecTV.VERSION = '0.0.1'; 54 | 55 | // Run DirecTV in *noConflict* mode, returning the previous value of 56 | // DirecTV. 57 | DirecTV.noConflict = function() { 58 | root.DirecTV = previousDirecTV; 59 | return this; 60 | }; 61 | 62 | // This is **the** DirecTV remote object. The options object *must* have an 63 | // `ipAddress` attribute that is a valid IP address for a DirecTV 64 | // set-top-box. An `Error` will be thrown if the `ipAddress` attribute is 65 | // not passed. No validation of the `ipAddress` occurs at this stage nor is 66 | // validation automatic. If you wish to validate the `ipAddress`, use the 67 | // [DirecTV.Remote.validate()](#validate) method. 68 | DirecTV.Remote = function(options) { 69 | options || (options = {}); // Default options to an empty object if not supplied 70 | 71 | // Require that the `ipAddress` option be passed _(unvalidated)_. 72 | if (!options.ipAddress) { 73 | throw new Error('options.ipAddress is not optional.'); 74 | } 75 | 76 | // Create a unique identifier 77 | this.rid = _.uniqueId('dtvr'); 78 | this.ipAddress = options.ipAddress; 79 | }; 80 | 81 | // Attach all inheritable methods to the Remote prototype. 82 | _.extend(DirecTV.Remote.prototype, { 83 | 84 | // 85 | // These commands are based on the documentation listed in the 86 | // [DirecTV Set-Top-Box Information for the Installer](#dtv_installer_pdf) 87 | // document. Since this document was generated in 2008, I am sure this 88 | // list could be out of date but for now I can only add what is 89 | // documented. This list is here purely for reference. 90 | Commands : [ 91 | 'FA81', // Standby 92 | 'FA82', // Active 93 | 'FA83', // GetPrimaryStatus 94 | 'FA84', // GetCommandVersion 95 | 'FA87', // GetCurrentChannel 96 | 'FA90', // GetSignalQuality 97 | 'FA91', // GetCurrentTime 98 | 'FA92', // GetUserCommand 99 | 'FA93', // EnableUserEntry 100 | 'FA94', // DisableUserEntry 101 | 'FA95', // GetReturnValue 102 | 'FA96', // Reboot 103 | 'FAA5', // SendUserCommand 104 | 'FAA6', // OpenUserChannel 105 | 'FA9A', // GetTuner 106 | 'FA8A', // GetPrimaryStatusMT 107 | 'FA8B', // GetCurrentChannelMT 108 | 'FA9D', // GetSignalQualityMT 109 | 'FA9F', // OpenUserChannelMT 110 | ], 111 | 112 | // 113 | // These holds are based on the documentation listed in the 114 | // [DirecTV SHEF Command Set Public Beta](#dtv_shef_pdf) document. 115 | // Since this document was generated in 2010, I am sure this list 116 | // could be out of date but fornow I can only add what is documented. 117 | // This list is here purely for reference. 118 | Holds : [ 119 | 'keyUp', // Simulates releasing a key 120 | 'keyDown', // Simulates pressing and holding a key 121 | 'keyPress' // Simulates pressing a key and then releasing a key 122 | ], 123 | 124 | // 125 | // These keys are based on the documentation listed in the 126 | // [DirecTV SHEF Command Set Public Beta](#dtv_shef_pdf) document. 127 | // Since this document was generated in 2010, I am sure this list could 128 | // be out of date but for now I can only add what is documented. This 129 | // list is here purely for reference. 130 | Keys : [ 131 | 'power', 132 | 'poweron', 133 | 'poweroff', 134 | 'format', 135 | 'pause', 136 | 'rew', 137 | 'replay', 138 | 'stop', 139 | 'advance', 140 | 'ffwd', 141 | 'record', 142 | 'play', 143 | 'guide', 144 | 'active', 145 | 'list', 146 | 'exit', 147 | 'back', 148 | 'menu', 149 | 'info', 150 | 'up', 151 | 'down', 152 | 'left', 153 | 'right', 154 | 'select', 155 | 'red', 156 | 'green', 157 | 'yellow', 158 | 'blue', 159 | 'chanup', 160 | 'chandown', 161 | 'prev', 162 | '0', 163 | '1', 164 | '2', 165 | '3', 166 | '4', 167 | '5', 168 | '6', 169 | '7', 170 | '8', 171 | '9', 172 | 'dash', 173 | 'enter' 174 | ], 175 | 176 | // DirecTV.Remote Methods 177 | // ---------------------- 178 | // 179 | // All methods in this section are required to pass in a `callback` 180 | // function in the options for each method invocation. Since we're 181 | // using **jsonp** in the browser, which mandates an asynchronous 182 | // request, the callback is used to handle the request's response. The 183 | // response object will either be a valid payload from the DirecTV API 184 | // call or an object mirroring the `status` object in every DirecTV API 185 | // response which contains the following attributes: 186 | // 187 | // * **code:** The HTTP status code of the request 188 | // * **commandResult:** The result of the command, UNIX style 189 | // * **msg:** The human readable message indicating the request status 190 | // * **query:** The portion of the request after the port in the URL 191 | // 192 | // Every response to every callback will have at least the `status` 193 | // attribute mentioned above and it will be what you use to check if a 194 | // request was a succes or failure. 195 | 196 | // 197 | // Validate the `ipAddress` configured with this DirecTV.Remote 198 | // instance. This is done first by using a regular expression to 199 | // validate that the `ipAddress` is a valid IPv4 IP address. If that 200 | // succeeds, we will then try to contact the DirecTV set-top-box. 201 | 202 | validate : function(options) { 203 | var path = '/info/getOptions'; 204 | var oldCallback; 205 | 206 | if (!/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/.test(this.ipAddress)) { 207 | options.callback({ 208 | status : { 209 | code : 405, 210 | commandResult : 1, 211 | msg : 'Not a valid IP address', 212 | query : path 213 | } 214 | }); 215 | } 216 | 217 | // We have to validate the `callback` option here since we have to 218 | // do some callback juggling so we can deliver customized messages 219 | // based on the result of the request. 220 | if (!options.callback || typeof options.callback !== 'function') { 221 | throw new Error('options.callback is not optional and must be a function.'); 222 | } 223 | 224 | // The callback passed in so we can invoke it later with our 225 | // customized response object. 226 | oldCallback = options.callback; 227 | 228 | options.callback = function(data, textStatus, jqXHR) { 229 | var response; 230 | 231 | if (data.options) { 232 | // We got a valid response. 233 | response = { 234 | status : { 235 | code : 200, 236 | commandResult : 0, 237 | msg : 'Host is a DirecTV set-top-box', 238 | query : path 239 | } 240 | }; 241 | } else { 242 | // We got a valid response but it did not have the expected 243 | // payload. 244 | response = { 245 | status : { 246 | code : 404, 247 | commandResult : 1, 248 | msg : 'Host does not appear to be a DirecTV set-top-box', 249 | query : path 250 | } 251 | }; 252 | } 253 | 254 | // Fire the callback with our customized response 255 | oldCallback(response); 256 | }; 257 | 258 | this.makeRequest({ 259 | path : path, 260 | options : options 261 | }); 262 | }, 263 | 264 | // Retrieve the list of DirecTV set-top-box locations the set-top-box 265 | // is aware of. 266 | getLocations : function(options) { 267 | this.makeRequest({ 268 | path : '/info/getLocations', 269 | options : options 270 | }); 271 | }, 272 | 273 | // Retrieve the current software version information for the DirecTV 274 | // set-top-box. 275 | getVersion : function(options) { 276 | this.makeRequest({ 277 | path : '/info/getVersion', 278 | options : options, 279 | }); 280 | }, 281 | 282 | // Retrieve the operating mode the DirecTV set-top-box is currently 283 | // operating in. 284 | getMode : function(options) { 285 | this.makeRequest({ 286 | path : '/info/mode', 287 | options : options 288 | }); 289 | }, 290 | 291 | // Send a remote key press request to the DirecTV set-top-box. The 292 | // `key` option is required and must be valid. For available keys see 293 | // the [available keys](#keys) portion of the documentaiton. You can 294 | // also pass in a `hold` option but it is not required. For available 295 | // holds, see the [available holds](#holds) portion of the 296 | // documentation. 297 | processKey : function(options) { 298 | if (options && !options.key) { 299 | throw new Error('options.key is not optional.'); 300 | } 301 | 302 | this.makeRequest({ 303 | path : '/remote/processKey', 304 | options : options 305 | }); 306 | }, 307 | 308 | // Sends a serial command request to the DirecTV set-top-box. The 309 | // `cmd` option is required and must be valid. For available commands 310 | // see the [available commands](#commands) portion of the documentaiton. 311 | processCommand : function(options) { 312 | if (options && !options.cmd) { 313 | throw new Error('options.cmd is not optional.'); 314 | } 315 | 316 | this.makeRequest({ 317 | path : '/serial/processCommand', 318 | options : options 319 | }); 320 | }, 321 | 322 | // Retrieve the list of available APIs the DirecTV set-top-box supports. 323 | getOptions : function(options) { 324 | this.makeRequest({ 325 | path : '/info/getOptions', 326 | options : options 327 | }); 328 | }, 329 | 330 | // Retrieve the current program information for the channel given. The 331 | // `major` option is required and corresponds to the channel you want to 332 | // get programming information for. 333 | getProgInfo : function(options) { 334 | if (options && !options.major) { 335 | throw new Error('options.major is not optional.'); 336 | } 337 | 338 | this.makeRequest({ 339 | path : '/tv/getProgInfo', 340 | options : options 341 | }); 342 | }, 343 | 344 | // Retrieves the current program information for the currently tuned 345 | // channel. 346 | getTuned : function(options) { 347 | this.makeRequest({ 348 | path : '/tv/getTuned', 349 | options : options 350 | }); 351 | }, 352 | 353 | // Send a request to tune the DirecTV set-top-box to the channel 354 | // specified. The `major` option is required and corresponds to the 355 | // channel you wish to tune to. 356 | tune : function(options) { 357 | if (options && !options.major) { 358 | throw new Error('options.major is not optional.'); 359 | } 360 | 361 | this.makeRequest({ 362 | path : '/tv/tune', 363 | options : options 364 | }); 365 | }, 366 | 367 | // Helper Methods 368 | // -------------- 369 | // 370 | // The methods in this section are helper methods. 371 | 372 | makeRequest : function(requestOptions) { 373 | requestOptions || (requestOptions = {}); // Default options to an empty object if not supplied 374 | requestOptions.options || (requestOptions.options = {}); // Default options to an empty object if not supplied 375 | 376 | var requestUrl = 'http://' + this.ipAddress + ':8080'; 377 | var requestQuery = requestOptions.path; 378 | var requestComplete = false; 379 | // This is the currently known list of available request parameters 380 | // that can be specified in all DirecTV.Remote methods. 381 | var knownRequestParams = [ 382 | 'clientAddr', 383 | 'cmd', 384 | 'hold', 385 | 'key', 386 | 'major', 387 | 'minor', 388 | 'time', 389 | 'videoWindow', 390 | 'wrapper' 391 | ]; 392 | var i; 393 | 394 | // This should never happen but just in case someone uses makeRequest directly in an invalid way... 395 | if (!requestOptions.options.callback || typeof requestOptions.options.callback !== 'function') { 396 | throw new Error('requestOptions.options.callback is not optional and must be a function.'); 397 | } 398 | 399 | // This should never happen but just in case someone uses makeRequest directly in an invalid way... 400 | if (!requestOptions.path) { 401 | throw new Error('requestOptions.path is not optional.'); 402 | } 403 | 404 | // Create query params based on known request options 405 | if (_.intersection(knownRequestParams, _.keys(requestOptions.options)).length > 0) { 406 | requestQuery += '?'; 407 | } 408 | 409 | for (i = 0; i < knownRequestParams.length; i++) { 410 | var requestParam = knownRequestParams[i]; 411 | 412 | if (requestOptions.options.hasOwnProperty(requestParam)) { 413 | if (requestQuery.charAt(requestQuery.length - 1) !== '?') { 414 | requestQuery += '&'; 415 | } 416 | 417 | requestQuery += (requestParam + '=' + requestOptions.options[requestParam]); 418 | } 419 | } 420 | 421 | // Make the call, asynchronously, and fire the callback with the results. 422 | $.ajax({ 423 | cache : true, 424 | dataType : 'jsonp', 425 | url : requestUrl + requestQuery, 426 | type : 'GET', 427 | success : function(data, textStatus, jqXHR) { 428 | requestComplete = true; 429 | 430 | requestOptions.options.callback(data); 431 | } 432 | }); 433 | 434 | // jQuery doesn't call the `error` or `complete` callbacks for 435 | // **jsonp** requests when things go wrong. This is our backup for 436 | // such situations. 437 | setTimeout(function() { 438 | if (!requestComplete) { 439 | var result = { 440 | status : { 441 | code : 404, 442 | msg : 'Invalid request (' + requestUrl + 443 | '): The request was either for a non-DirecTV ' + 444 | 'set-top-box, was for an invalid endpoint of ' + 445 | 'a DirecTV set-top-box or the parameter(s) ' + 446 | 'passed to the endpoint were invalid.', 447 | commandResult : 1, 448 | query : requestQuery 449 | } 450 | }; 451 | 452 | requestOptions.options.callback(result); 453 | } 454 | }, 5000); 455 | } 456 | }); 457 | 458 | }).call(this); 459 | -------------------------------------------------------------------------------- /js/examples/css/dtv.remote.api.example.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 60px; /* 60px to make the container go all the way to the bottom of the topbar and then 20px */ 3 | } 4 | .text-right { 5 | text-align: right; 6 | } 7 | .text-center { 8 | text-align: center; 9 | padding: 10px 0px 10px 0px; 10 | } 11 | 12 | /* Page header tweaks */ 13 | #main-page-header { 14 | background-color: #f5f5f5; 15 | padding: 20px; 16 | margin: -20px -20px 20px -20px; 17 | -webkit-border-radius: 0 0 6px 6px; 18 | -moz-border-radius: 0 0 6px 6px; 19 | border-radius: 0 0 6px 6px; 20 | -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.15); 21 | -moz-box-shadow: 0 1px 2px rgba(0,0,0,.15); 22 | box-shadow: 0 1px 2px rgba(0,0,0,.15); 23 | } 24 | -------------------------------------------------------------------------------- /js/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | DirecTV Remote API Example 9 | 10 | 11 | 12 | 13 | 14 | 25 |
26 |
27 |

Web-Based DirecTV Remote Control

28 |

29 | Welcome to the example application for the JavaScript DirecTV Remote API. 30 | This application will turn your web browser into a DirecTV STB remote control, as if you had the remote in your hand. 31 |

32 |

33 | Click the button below to enter your DirecTV STB IP address. 34 |

35 |

Get Started

36 |
37 |
38 | 39 | 40 |
41 |
42 |
43 |
44 |

Program Title Episode title

45 |
46 |
47 |

Channel Callsign Channel

48 |
49 |
50 |
51 |
52 |
53 |
54 | format 55 | power 56 |
57 |
58 |
59 |
60 | rew 61 | pause 62 | play 63 | stop 64 | ffwd 65 |
66 |
67 |
68 |
69 | replay 70 | advance 71 | record 72 |
73 |
74 |
75 |
76 | guide 77 | active 78 | list 79 | exit 80 |
81 |
82 |
83 |
84 | up 85 | down 86 | select 87 | left 88 | right 89 |
90 |
91 |
92 |
93 | back 94 | menu 95 | info 96 |
97 |
98 |
99 |
100 |   101 |   102 |   103 |   104 |
105 |
106 |
107 |
108 | chan/page up 109 | chan/page down 110 | 111 |
112 |
113 |
114 |
115 | 1 116 | 2 117 | 3 118 |
119 |
120 |
121 |
122 | 4 123 | 5 124 | 6 125 |
126 |
127 |
128 |
129 | 7 130 | 8 131 | 9 132 |
133 |
134 |
135 |
136 | dash 137 | 0 138 | enter 139 |
140 |
141 |
142 |
143 | 144 | 145 | 154 | 163 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /js/examples/js/dtv.remote.api.example.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | // Global variable for the remote. (I realize this is a faux pas but this is a quick example.) 3 | var dtvRemote; 4 | 5 | var refreshCurrentChannel = function() { 6 | dtvRemote.getTuned({callback: function(result) { 7 | if (result.episodeTitle) { 8 | $('#program-title').html(result.title + ' ' + result.episodeTitle + ''); 9 | } else { 10 | $('#program-title').text(result.title); 11 | } 12 | 13 | $('#program-callsign').html(result.callsign + ' ' + result.major + ''); 14 | }}); 15 | }; 16 | 17 | // Click handler for the remote buttons 18 | $('#remote-buttons a.btn').click(function(e) { 19 | dtvRemote.processKey({key: this.id, callback: function(result) { 20 | if (result.status.code !== 200) { 21 | alert(result.status.msg); 22 | } 23 | }}); 24 | }); 25 | 26 | // Enter handler 27 | $('#ip-address-input').keypress(function(e) { 28 | if(e.which === 13) { 29 | $('#ip-address-submit').click(); 30 | e.preventDefault(); 31 | } 32 | }); 33 | 34 | // Click handler for the IP address modal 35 | $('#ip-address-submit').click(function(e) { 36 | $('#ip-address-dialog button.btn').toggleClass('disabled'); 37 | 38 | try { 39 | dtvRemote = new DirecTV.Remote({ipAddress: $('#ip-address-input').val()}); 40 | } catch (err) { 41 | alert(err); 42 | $('#ip-address-dialog button.btn').toggleClass('disabled'); 43 | } 44 | 45 | dtvRemote.validate({callback: function(result) { 46 | if (result.status.code === 200) { 47 | // Initialize the remote 48 | dtvRemote.getTuned({callback: function(result) { 49 | if (result.episodeTitle) { 50 | $('#program-title').html(result.title + ' ' + result.episodeTitle + ''); 51 | } else { 52 | $('#program-title').text(result.title); 53 | } 54 | 55 | $('#program-callsign').html(result.callsign + ' ' + result.major + ''); 56 | 57 | $('#main-container').empty(); 58 | $('#main-container').append($('#main-content')); 59 | $('#main-content').toggleClass('hide'); 60 | 61 | dtvRemote.ccJob = setInterval(refreshCurrentChannel, 5000); 62 | 63 | $('#ip-address-dialog').modal('hide'); 64 | }}); 65 | } else { 66 | alert(result.status.msg); 67 | $('#ip-address-dialog button.btn').toggleClass('disabled'); 68 | } 69 | }}); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /js/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DirecTV Remote API Test Suite 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 |

DirecTV Remote API Test Suite

19 |

20 |
21 |

22 |
    23 | 24 | 25 | -------------------------------------------------------------------------------- /js/test/tests.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | var validIPAddress = prompt('Please enter a known valid IP address of an accessible DirecTV set-top-box'); 3 | 4 | module('DirecTV.Remote()'); 5 | 6 | test('DirecTV.Remote()', function() { 7 | var dtvr; 8 | 9 | raises(function() { 10 | dtvr = new DirecTV.Remote(); 11 | }, 'Should not be able to create a DirecTV.Remote without the ipAddress option.'); 12 | 13 | dtvr = new DirecTV.Remote({ipAddress: validIPAddress}); 14 | }); 15 | 16 | module('DirecTV.Remote.validate()'); 17 | 18 | asyncTest('DirecTV.Remote.validate() [invalid ip address]', function() { 19 | var dtvr = new DirecTV.Remote({ipAddress: '192.168.0.1'}); 20 | 21 | raises(function() { 22 | dtvr.validate(); 23 | }, 'Validation should not run if a callback is not supplied.'); 24 | 25 | dtvr.validate({callback: function(result) { 26 | ok(result.status.code !== 200, 'Validation result should be negative.'); 27 | start(); 28 | }}); 29 | }); 30 | 31 | asyncTest('DirecTV.Remote.validate()', function() { 32 | var dtvr = new DirecTV.Remote({ipAddress: validIPAddress}); 33 | 34 | dtvr.validate({callback: function(result) { 35 | ok(result.status.code === 200, 'Validation result should be positive.'); 36 | start(); 37 | }}); 38 | }); 39 | 40 | module('DirecTV.Remote.getLocations()'); 41 | 42 | asyncTest('DirecTV.Remote.getLocations()', function() { 43 | var dtvr = new DirecTV.Remote({ipAddress: validIPAddress}); 44 | 45 | dtvr.getLocations({callback: function(result) { 46 | ok(result.locations && result.locations.length >= 1, 'At least one location should be returned.'); 47 | start(); 48 | }}); 49 | }); 50 | 51 | module('DirecTV.Remote.getMode()'); 52 | 53 | asyncTest('DirecTV.Remote.getMode()', function() { 54 | var dtvr = new DirecTV.Remote({ipAddress: validIPAddress}); 55 | 56 | dtvr.getMode({callback: function(result) { 57 | ok(_.contains(_.keys(result), 'mode'), 'Expects response key mode.'); 58 | 59 | start(); 60 | }}); 61 | }); 62 | 63 | module('DirecTV.Remote.getVersion()'); 64 | 65 | asyncTest('DirecTV.Remote.getVersion()', function() { 66 | var dtvr = new DirecTV.Remote({ipAddress: validIPAddress}); 67 | var expectedKeys = ['accessCardId', 'receiverId', 'stbSoftwareVersion', 'systemTime', 'version']; 68 | var keys; 69 | var key; 70 | var i; 71 | 72 | dtvr.getVersion({callback: function(result) { 73 | keys = _.keys(result); 74 | 75 | for (i = 0; i < expectedKeys.length; i++) { 76 | key = expectedKeys[i]; 77 | 78 | ok(_.contains(keys, key), 'Expected response key (' + key + ') found.'); 79 | } 80 | 81 | start(); 82 | }}); 83 | }); 84 | 85 | module('DirecTV.Remote.processKey()'); 86 | 87 | asyncTest('DirecTV.Remote.processKey() [invalid key]', function() { 88 | var dtvr = new DirecTV.Remote({ipAddress: validIPAddress}); 89 | 90 | dtvr.processKey({key: 'exitt', callback: function(result) { 91 | ok(result.status.code !== 200, 'Invalid key should result in invalid response.'); 92 | 93 | start(); 94 | }}); 95 | }); 96 | 97 | asyncTest('DirecTV.Remote.processKey() [invalid hold]', function() { 98 | var dtvr = new DirecTV.Remote({ipAddress: validIPAddress}); 99 | 100 | dtvr.processKey({key: 'exit', hold: 'keyPresss', callback: function(result) { 101 | ok(result.status.code !== 200, 'Invalid key should result in invalid response.'); 102 | 103 | start(); 104 | }}); 105 | }); 106 | 107 | asyncTest('DirecTV.Remote.processKey()', function() { 108 | var dtvr = new DirecTV.Remote({ipAddress: validIPAddress}); 109 | var expectedKeys = ['hold', 'key']; 110 | var keys; 111 | var key; 112 | var i; 113 | 114 | dtvr.processKey({key: 'exit', callback: function(result) { 115 | keys = _.keys(result); 116 | 117 | for (i = 0; i < expectedKeys.length; i++) { 118 | key = expectedKeys[i]; 119 | 120 | ok(_.contains(keys, key), 'Expected response key (' + key + ') found.'); 121 | 122 | if (key === 'key') { 123 | ok('exit' === result.key, 'Key sent should be the key received.'); 124 | } 125 | 126 | if (key === 'hold') { 127 | ok('keyPress' === result.hold, 'Default hold (keyPress) should be received.'); 128 | } 129 | } 130 | 131 | start(); 132 | }}); 133 | }); 134 | 135 | module('DirecTV.Remote.processCommand()'); 136 | 137 | asyncTest('DirecTV.Remote.processCommand() [invalid cmd]', function() { 138 | var dtvr = new DirecTV.Remote({ipAddress: validIPAddress}); 139 | 140 | dtvr.processCommand({cmd: 'FAFA', callback: function(result) { 141 | ok(result.status.code !== 200, 'Invalid key should result in invalid response.'); 142 | 143 | start(); 144 | }}); 145 | }); 146 | 147 | asyncTest('DirecTV.Remote.processCommand()', function() { 148 | var dtvr = new DirecTV.Remote({ipAddress: validIPAddress}); 149 | var expectedKeys = ['command', 'param', 'prefix', 'return']; 150 | var keys; 151 | var key; 152 | var i; 153 | 154 | dtvr.processCommand({cmd: 'FA83', callback: function(result) { 155 | keys = _.keys(result); 156 | 157 | for (i = 0; i < expectedKeys.length; i++) { 158 | key = expectedKeys[i]; 159 | 160 | ok(_.contains(keys, key), 'Expected response key (' + key + ') found.'); 161 | 162 | if (key === 'command') { 163 | ok(true === result.command, 'Command value should be true.'); 164 | } 165 | } 166 | 167 | start(); 168 | }}); 169 | }); 170 | 171 | module('DirecTV.Remote.getOptions()'); 172 | 173 | asyncTest('DirecTV.Remote.getOptions()', function() { 174 | var dtvr = new DirecTV.Remote({ipAddress: validIPAddress}); 175 | 176 | dtvr.getOptions({callback: function(results) { 177 | ok(8 === results.options.length, 'There should be 8 commands available.'); 178 | 179 | start(); 180 | }}); 181 | }); 182 | 183 | module('DirecTV.Remote.getProgInfo()'); 184 | 185 | asyncTest('DirecTV.Remote.getProgInfo() [invalid major]', function() { 186 | var dtvr = new DirecTV.Remote({ipAddress: validIPAddress}); 187 | 188 | dtvr.getProgInfo({major: '55555', callback: function(result) { 189 | ok(result.status.code !== 200, 'Invalid key should result in invalid response.'); 190 | 191 | start(); 192 | }}); 193 | }); 194 | 195 | asyncTest('DirecTV.Remote.getProgInfo()', function() { 196 | var dtvr = new DirecTV.Remote({ipAddress: validIPAddress}); 197 | var expectedKeys = ['callsign', 'date', 'duration', 'episodeTitle', 'isOffAir', 'isPclocked', 198 | 'isPpv', 'isRecording', 'isVod', 'major', 'minor', 'programId', 'rating', 199 | 'startTime', 'stationId', 'title']; 200 | var keys; 201 | var key; 202 | var i; 203 | 204 | dtvr.getProgInfo({major: '299', callback: function(result) { 205 | keys = _.keys(result); 206 | 207 | for (i = 0; i < expectedKeys.length; i++) { 208 | key = expectedKeys[i]; 209 | 210 | ok(_.contains(keys, key), 'Expected response key (' + key + ') found.'); 211 | 212 | if (key === 'major') { 213 | ok(result.major === 299, 'Major sent should be the major received.'); 214 | } 215 | } 216 | 217 | start(); 218 | }}); 219 | }); 220 | 221 | module('DirecTV.Remote.getTuned()'); 222 | 223 | asyncTest('DirecTV.Remote.getTuned()', function() { 224 | var dtvr = new DirecTV.Remote({ipAddress: validIPAddress}); 225 | var expectedKeys = ['callsign', 'date', 'duration', 'isOffAir', 'isPclocked', 'isPpv', 226 | 'isRecording', 'isVod', 'major', 'minor', 'offset', 'programId', 227 | 'rating', 'startTime', 'stationId', 'title']; 228 | var keys; 229 | var key; 230 | var i; 231 | 232 | dtvr.getTuned({callback: function(result) { 233 | keys = _.keys(result); 234 | 235 | for (i = 0; i < expectedKeys.length; i++) { 236 | key = expectedKeys[i]; 237 | 238 | ok(_.contains(keys, key), 'Expected response key (' + key + ') found.'); 239 | } 240 | 241 | start(); 242 | }}); 243 | }); 244 | 245 | module('DirecTV.Remote.tune()'); 246 | 247 | asyncTest('DirecTV.Remote.tune() [invalid major]', function() { 248 | var dtvr = new DirecTV.Remote({ipAddress: validIPAddress}); 249 | 250 | dtvr.tune({major: '55555', callback: function(result) { 251 | ok(200 !== result.status.code, 'Invalid major should result in invalid response.'); 252 | 253 | start(); 254 | }}); 255 | }); 256 | 257 | asyncTest('DirecTV.Remote.tune()', function() { 258 | var dtvr = new DirecTV.Remote({ipAddress: validIPAddress}); 259 | 260 | dtvr.getTuned({callback: function(result) { 261 | dtvr.tune({major: result.major, callback: function(xresult) { 262 | ok(200 === xresult.status.code, 'Tuning to a valid channel.'); 263 | 264 | start(); 265 | }}); 266 | }}); 267 | }); 268 | }); 269 | -------------------------------------------------------------------------------- /js/test/vendor/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.3.0pre - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | * Pulled Live from Git Wed Dec 14 17:15:01 UTC 2011 10 | * Last Commit: 0712230bb203c262211649b32bd712ec7df5f857 11 | */ 12 | 13 | /** Font Family and Sizes */ 14 | 15 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 16 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 17 | } 18 | 19 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 20 | #qunit-tests { font-size: smaller; } 21 | 22 | 23 | /** Resets */ 24 | 25 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 26 | margin: 0; 27 | padding: 0; 28 | } 29 | 30 | 31 | /** Header */ 32 | 33 | #qunit-header { 34 | padding: 0.5em 0 0.5em 1em; 35 | 36 | color: #8699a4; 37 | background-color: #0d3349; 38 | 39 | font-size: 1.5em; 40 | line-height: 1em; 41 | font-weight: normal; 42 | 43 | border-radius: 15px 15px 0 0; 44 | -moz-border-radius: 15px 15px 0 0; 45 | -webkit-border-top-right-radius: 15px; 46 | -webkit-border-top-left-radius: 15px; 47 | } 48 | 49 | #qunit-header a { 50 | text-decoration: none; 51 | color: #c2ccd1; 52 | } 53 | 54 | #qunit-header a:hover, 55 | #qunit-header a:focus { 56 | color: #fff; 57 | } 58 | 59 | #qunit-banner { 60 | height: 5px; 61 | } 62 | 63 | #qunit-testrunner-toolbar { 64 | padding: 0.5em 0 0.5em 2em; 65 | color: #5E740B; 66 | background-color: #eee; 67 | } 68 | 69 | #qunit-userAgent { 70 | padding: 0.5em 0 0.5em 2.5em; 71 | background-color: #2b81af; 72 | color: #fff; 73 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 74 | } 75 | 76 | 77 | /** Tests: Pass/Fail */ 78 | 79 | #qunit-tests { 80 | list-style-position: inside; 81 | } 82 | 83 | #qunit-tests li { 84 | padding: 0.4em 0.5em 0.4em 2.5em; 85 | border-bottom: 1px solid #fff; 86 | list-style-position: inside; 87 | } 88 | 89 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 90 | display: none; 91 | } 92 | 93 | #qunit-tests li strong { 94 | cursor: pointer; 95 | } 96 | 97 | #qunit-tests li a { 98 | padding: 0.5em; 99 | color: #c2ccd1; 100 | text-decoration: none; 101 | } 102 | #qunit-tests li a:hover, 103 | #qunit-tests li a:focus { 104 | color: #000; 105 | } 106 | 107 | #qunit-tests ol { 108 | margin-top: 0.5em; 109 | padding: 0.5em; 110 | 111 | background-color: #fff; 112 | 113 | border-radius: 15px; 114 | -moz-border-radius: 15px; 115 | -webkit-border-radius: 15px; 116 | 117 | box-shadow: inset 0px 2px 13px #999; 118 | -moz-box-shadow: inset 0px 2px 13px #999; 119 | -webkit-box-shadow: inset 0px 2px 13px #999; 120 | } 121 | 122 | #qunit-tests table { 123 | border-collapse: collapse; 124 | margin-top: .2em; 125 | } 126 | 127 | #qunit-tests th { 128 | text-align: right; 129 | vertical-align: top; 130 | padding: 0 .5em 0 0; 131 | } 132 | 133 | #qunit-tests td { 134 | vertical-align: top; 135 | } 136 | 137 | #qunit-tests pre { 138 | margin: 0; 139 | white-space: pre-wrap; 140 | word-wrap: break-word; 141 | } 142 | 143 | #qunit-tests del { 144 | background-color: #e0f2be; 145 | color: #374e0c; 146 | text-decoration: none; 147 | } 148 | 149 | #qunit-tests ins { 150 | background-color: #ffcaca; 151 | color: #500; 152 | text-decoration: none; 153 | } 154 | 155 | /*** Test Counts */ 156 | 157 | #qunit-tests b.counts { color: black; } 158 | #qunit-tests b.passed { color: #5E740B; } 159 | #qunit-tests b.failed { color: #710909; } 160 | 161 | #qunit-tests li li { 162 | margin: 0.5em; 163 | padding: 0.4em 0.5em 0.4em 0.5em; 164 | background-color: #fff; 165 | border-bottom: none; 166 | list-style-position: inside; 167 | } 168 | 169 | /*** Passing Styles */ 170 | 171 | #qunit-tests li li.pass { 172 | color: #5E740B; 173 | background-color: #fff; 174 | border-left: 26px solid #C6E746; 175 | } 176 | 177 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 178 | #qunit-tests .pass .test-name { color: #366097; } 179 | 180 | #qunit-tests .pass .test-actual, 181 | #qunit-tests .pass .test-expected { color: #999999; } 182 | 183 | #qunit-banner.qunit-pass { background-color: #C6E746; } 184 | 185 | /*** Failing Styles */ 186 | 187 | #qunit-tests li li.fail { 188 | color: #710909; 189 | background-color: #fff; 190 | border-left: 26px solid #EE5757; 191 | white-space: pre; 192 | } 193 | 194 | #qunit-tests > li:last-child { 195 | border-radius: 0 0 15px 15px; 196 | -moz-border-radius: 0 0 15px 15px; 197 | -webkit-border-bottom-right-radius: 15px; 198 | -webkit-border-bottom-left-radius: 15px; 199 | } 200 | 201 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 202 | #qunit-tests .fail .test-name, 203 | #qunit-tests .fail .module-name { color: #000000; } 204 | 205 | #qunit-tests .fail .test-actual { color: #EE5757; } 206 | #qunit-tests .fail .test-expected { color: green; } 207 | 208 | #qunit-banner.qunit-fail { background-color: #EE5757; } 209 | 210 | 211 | /** Result */ 212 | 213 | #qunit-testresult { 214 | padding: 0.5em 0.5em 0.5em 2.5em; 215 | 216 | color: #2b81af; 217 | background-color: #D2E0E6; 218 | 219 | border-bottom: 1px solid white; 220 | } 221 | 222 | /** Fixture */ 223 | 224 | #qunit-fixture { 225 | position: absolute; 226 | top: -10000px; 227 | left: -10000px; 228 | } 229 | -------------------------------------------------------------------------------- /js/test/vendor/qunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.3.0pre - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | * Pulled Live from Git Wed Dec 14 17:15:01 UTC 2011 10 | * Last Commit: 0712230bb203c262211649b32bd712ec7df5f857 11 | */ 12 | 13 | (function(window) { 14 | 15 | var defined = { 16 | setTimeout: typeof window.setTimeout !== "undefined", 17 | sessionStorage: (function() { 18 | try { 19 | return !!sessionStorage.getItem; 20 | } catch(e) { 21 | return false; 22 | } 23 | })() 24 | }; 25 | 26 | var testId = 0, 27 | toString = Object.prototype.toString, 28 | hasOwn = Object.prototype.hasOwnProperty; 29 | 30 | var Test = function(name, testName, expected, testEnvironmentArg, async, callback) { 31 | this.name = name; 32 | this.testName = testName; 33 | this.expected = expected; 34 | this.testEnvironmentArg = testEnvironmentArg; 35 | this.async = async; 36 | this.callback = callback; 37 | this.assertions = []; 38 | }; 39 | Test.prototype = { 40 | init: function() { 41 | var tests = id("qunit-tests"); 42 | if (tests) { 43 | var b = document.createElement("strong"); 44 | b.innerHTML = "Running " + this.name; 45 | var li = document.createElement("li"); 46 | li.appendChild( b ); 47 | li.className = "running"; 48 | li.id = this.id = "test-output" + testId++; 49 | tests.appendChild( li ); 50 | } 51 | }, 52 | setup: function() { 53 | if (this.module != config.previousModule) { 54 | if ( config.previousModule ) { 55 | runLoggingCallbacks('moduleDone', QUnit, { 56 | name: config.previousModule, 57 | failed: config.moduleStats.bad, 58 | passed: config.moduleStats.all - config.moduleStats.bad, 59 | total: config.moduleStats.all 60 | } ); 61 | } 62 | config.previousModule = this.module; 63 | config.moduleStats = { all: 0, bad: 0 }; 64 | runLoggingCallbacks( 'moduleStart', QUnit, { 65 | name: this.module 66 | } ); 67 | } 68 | 69 | config.current = this; 70 | this.testEnvironment = extend({ 71 | setup: function() {}, 72 | teardown: function() {} 73 | }, this.moduleTestEnvironment); 74 | if (this.testEnvironmentArg) { 75 | extend(this.testEnvironment, this.testEnvironmentArg); 76 | } 77 | 78 | runLoggingCallbacks( 'testStart', QUnit, { 79 | name: this.testName, 80 | module: this.module 81 | }); 82 | 83 | // allow utility functions to access the current test environment 84 | // TODO why?? 85 | QUnit.current_testEnvironment = this.testEnvironment; 86 | 87 | try { 88 | if ( !config.pollution ) { 89 | saveGlobal(); 90 | } 91 | 92 | this.testEnvironment.setup.call(this.testEnvironment); 93 | } catch(e) { 94 | QUnit.ok( false, "Setup failed on " + this.testName + ": " + e.message ); 95 | } 96 | }, 97 | run: function() { 98 | config.current = this; 99 | if ( this.async ) { 100 | QUnit.stop(); 101 | } 102 | 103 | if ( config.notrycatch ) { 104 | this.callback.call(this.testEnvironment); 105 | return; 106 | } 107 | try { 108 | this.callback.call(this.testEnvironment); 109 | } catch(e) { 110 | fail("Test " + this.testName + " died, exception and test follows", e, this.callback); 111 | QUnit.ok( false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e) ); 112 | // else next test will carry the responsibility 113 | saveGlobal(); 114 | 115 | // Restart the tests if they're blocking 116 | if ( config.blocking ) { 117 | QUnit.start(); 118 | } 119 | } 120 | }, 121 | teardown: function() { 122 | config.current = this; 123 | try { 124 | this.testEnvironment.teardown.call(this.testEnvironment); 125 | checkPollution(); 126 | } catch(e) { 127 | QUnit.ok( false, "Teardown failed on " + this.testName + ": " + e.message ); 128 | } 129 | }, 130 | finish: function() { 131 | config.current = this; 132 | if ( this.expected != null && this.expected != this.assertions.length ) { 133 | QUnit.ok( false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" ); 134 | } 135 | 136 | var good = 0, bad = 0, 137 | tests = id("qunit-tests"); 138 | 139 | config.stats.all += this.assertions.length; 140 | config.moduleStats.all += this.assertions.length; 141 | 142 | if ( tests ) { 143 | var ol = document.createElement("ol"); 144 | 145 | for ( var i = 0; i < this.assertions.length; i++ ) { 146 | var assertion = this.assertions[i]; 147 | 148 | var li = document.createElement("li"); 149 | li.className = assertion.result ? "pass" : "fail"; 150 | li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); 151 | ol.appendChild( li ); 152 | 153 | if ( assertion.result ) { 154 | good++; 155 | } else { 156 | bad++; 157 | config.stats.bad++; 158 | config.moduleStats.bad++; 159 | } 160 | } 161 | 162 | // store result when possible 163 | if ( QUnit.config.reorder && defined.sessionStorage ) { 164 | if (bad) { 165 | sessionStorage.setItem("qunit-" + this.module + "-" + this.testName, bad); 166 | } else { 167 | sessionStorage.removeItem("qunit-" + this.module + "-" + this.testName); 168 | } 169 | } 170 | 171 | if (bad == 0) { 172 | ol.style.display = "none"; 173 | } 174 | 175 | var b = document.createElement("strong"); 176 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 177 | 178 | var a = document.createElement("a"); 179 | a.innerHTML = "Rerun"; 180 | a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 181 | 182 | addEvent(b, "click", function() { 183 | var next = b.nextSibling.nextSibling, 184 | display = next.style.display; 185 | next.style.display = display === "none" ? "block" : "none"; 186 | }); 187 | 188 | addEvent(b, "dblclick", function(e) { 189 | var target = e && e.target ? e.target : window.event.srcElement; 190 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 191 | target = target.parentNode; 192 | } 193 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 194 | window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 195 | } 196 | }); 197 | 198 | var li = id(this.id); 199 | li.className = bad ? "fail" : "pass"; 200 | li.removeChild( li.firstChild ); 201 | li.appendChild( b ); 202 | li.appendChild( a ); 203 | li.appendChild( ol ); 204 | 205 | } else { 206 | for ( var i = 0; i < this.assertions.length; i++ ) { 207 | if ( !this.assertions[i].result ) { 208 | bad++; 209 | config.stats.bad++; 210 | config.moduleStats.bad++; 211 | } 212 | } 213 | } 214 | 215 | try { 216 | QUnit.reset(); 217 | } catch(e) { 218 | fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset); 219 | } 220 | 221 | runLoggingCallbacks( 'testDone', QUnit, { 222 | name: this.testName, 223 | module: this.module, 224 | failed: bad, 225 | passed: this.assertions.length - bad, 226 | total: this.assertions.length 227 | } ); 228 | }, 229 | 230 | queue: function() { 231 | var test = this; 232 | synchronize(function() { 233 | test.init(); 234 | }); 235 | function run() { 236 | // each of these can by async 237 | synchronize(function() { 238 | test.setup(); 239 | }); 240 | synchronize(function() { 241 | test.run(); 242 | }); 243 | synchronize(function() { 244 | test.teardown(); 245 | }); 246 | synchronize(function() { 247 | test.finish(); 248 | }); 249 | } 250 | // defer when previous test run passed, if storage is available 251 | var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.module + "-" + this.testName); 252 | if (bad) { 253 | run(); 254 | } else { 255 | synchronize(run, true); 256 | }; 257 | } 258 | 259 | }; 260 | 261 | var QUnit = { 262 | 263 | // call on start of module test to prepend name to all tests 264 | module: function(name, testEnvironment) { 265 | config.currentModule = name; 266 | config.currentModuleTestEnviroment = testEnvironment; 267 | }, 268 | 269 | asyncTest: function(testName, expected, callback) { 270 | if ( arguments.length === 2 ) { 271 | callback = expected; 272 | expected = null; 273 | } 274 | 275 | QUnit.test(testName, expected, callback, true); 276 | }, 277 | 278 | test: function(testName, expected, callback, async) { 279 | var name = '' + escapeInnerText(testName) + '', testEnvironmentArg; 280 | 281 | if ( arguments.length === 2 ) { 282 | callback = expected; 283 | expected = null; 284 | } 285 | // is 2nd argument a testEnvironment? 286 | if ( expected && typeof expected === 'object') { 287 | testEnvironmentArg = expected; 288 | expected = null; 289 | } 290 | 291 | if ( config.currentModule ) { 292 | name = '' + config.currentModule + ": " + name; 293 | } 294 | 295 | if ( !validTest(config.currentModule + ": " + testName) ) { 296 | return; 297 | } 298 | 299 | var test = new Test(name, testName, expected, testEnvironmentArg, async, callback); 300 | test.module = config.currentModule; 301 | test.moduleTestEnvironment = config.currentModuleTestEnviroment; 302 | test.queue(); 303 | }, 304 | 305 | /** 306 | * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 307 | */ 308 | expect: function(asserts) { 309 | config.current.expected = asserts; 310 | }, 311 | 312 | /** 313 | * Asserts true. 314 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 315 | */ 316 | ok: function(a, msg) { 317 | a = !!a; 318 | var details = { 319 | result: a, 320 | message: msg 321 | }; 322 | msg = escapeInnerText(msg); 323 | runLoggingCallbacks( 'log', QUnit, details ); 324 | config.current.assertions.push({ 325 | result: a, 326 | message: msg 327 | }); 328 | }, 329 | 330 | /** 331 | * Checks that the first two arguments are equal, with an optional message. 332 | * Prints out both actual and expected values. 333 | * 334 | * Prefered to ok( actual == expected, message ) 335 | * 336 | * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 337 | * 338 | * @param Object actual 339 | * @param Object expected 340 | * @param String message (optional) 341 | */ 342 | equal: function(actual, expected, message) { 343 | QUnit.push(expected == actual, actual, expected, message); 344 | }, 345 | 346 | notEqual: function(actual, expected, message) { 347 | QUnit.push(expected != actual, actual, expected, message); 348 | }, 349 | 350 | deepEqual: function(actual, expected, message) { 351 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); 352 | }, 353 | 354 | notDeepEqual: function(actual, expected, message) { 355 | QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); 356 | }, 357 | 358 | strictEqual: function(actual, expected, message) { 359 | QUnit.push(expected === actual, actual, expected, message); 360 | }, 361 | 362 | notStrictEqual: function(actual, expected, message) { 363 | QUnit.push(expected !== actual, actual, expected, message); 364 | }, 365 | 366 | raises: function(block, expected, message) { 367 | var actual, ok = false; 368 | 369 | if (typeof expected === 'string') { 370 | message = expected; 371 | expected = null; 372 | } 373 | 374 | try { 375 | block(); 376 | } catch (e) { 377 | actual = e; 378 | } 379 | 380 | if (actual) { 381 | // we don't want to validate thrown error 382 | if (!expected) { 383 | ok = true; 384 | // expected is a regexp 385 | } else if (QUnit.objectType(expected) === "regexp") { 386 | ok = expected.test(actual); 387 | // expected is a constructor 388 | } else if (actual instanceof expected) { 389 | ok = true; 390 | // expected is a validation function which returns true is validation passed 391 | } else if (expected.call({}, actual) === true) { 392 | ok = true; 393 | } 394 | } 395 | 396 | QUnit.ok(ok, message); 397 | }, 398 | 399 | start: function(count) { 400 | config.semaphore -= count || 1; 401 | if (config.semaphore > 0) { 402 | // don't start until equal number of stop-calls 403 | return; 404 | } 405 | if (config.semaphore < 0) { 406 | // ignore if start is called more often then stop 407 | config.semaphore = 0; 408 | } 409 | // A slight delay, to avoid any current callbacks 410 | if ( defined.setTimeout ) { 411 | window.setTimeout(function() { 412 | if (config.semaphore > 0) { 413 | return; 414 | } 415 | if ( config.timeout ) { 416 | clearTimeout(config.timeout); 417 | } 418 | 419 | config.blocking = false; 420 | process(true); 421 | }, 13); 422 | } else { 423 | config.blocking = false; 424 | process(true); 425 | } 426 | }, 427 | 428 | stop: function(count) { 429 | config.semaphore += count || 1; 430 | config.blocking = true; 431 | 432 | if ( config.testTimeout && defined.setTimeout ) { 433 | clearTimeout(config.timeout); 434 | config.timeout = window.setTimeout(function() { 435 | QUnit.ok( false, "Test timed out" ); 436 | config.semaphore = 1; 437 | QUnit.start(); 438 | }, config.testTimeout); 439 | } 440 | } 441 | }; 442 | 443 | //We want access to the constructor's prototype 444 | (function() { 445 | function F(){}; 446 | F.prototype = QUnit; 447 | QUnit = new F(); 448 | //Make F QUnit's constructor so that we can add to the prototype later 449 | QUnit.constructor = F; 450 | })(); 451 | 452 | // Backwards compatibility, deprecated 453 | QUnit.equals = QUnit.equal; 454 | QUnit.same = QUnit.deepEqual; 455 | 456 | // Maintain internal state 457 | var config = { 458 | // The queue of tests to run 459 | queue: [], 460 | 461 | // block until document ready 462 | blocking: true, 463 | 464 | // when enabled, show only failing tests 465 | // gets persisted through sessionStorage and can be changed in UI via checkbox 466 | hidepassed: false, 467 | 468 | // by default, run previously failed tests first 469 | // very useful in combination with "Hide passed tests" checked 470 | reorder: true, 471 | 472 | // by default, modify document.title when suite is done 473 | altertitle: true, 474 | 475 | urlConfig: ['noglobals', 'notrycatch'], 476 | 477 | //logging callback queues 478 | begin: [], 479 | done: [], 480 | log: [], 481 | testStart: [], 482 | testDone: [], 483 | moduleStart: [], 484 | moduleDone: [] 485 | }; 486 | 487 | // Load paramaters 488 | (function() { 489 | var location = window.location || { search: "", protocol: "file:" }, 490 | params = location.search.slice( 1 ).split( "&" ), 491 | length = params.length, 492 | urlParams = {}, 493 | current; 494 | 495 | if ( params[ 0 ] ) { 496 | for ( var i = 0; i < length; i++ ) { 497 | current = params[ i ].split( "=" ); 498 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 499 | // allow just a key to turn on a flag, e.g., test.html?noglobals 500 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 501 | urlParams[ current[ 0 ] ] = current[ 1 ]; 502 | } 503 | } 504 | 505 | QUnit.urlParams = urlParams; 506 | config.filter = urlParams.filter; 507 | 508 | // Figure out if we're running the tests from a server or not 509 | QUnit.isLocal = !!(location.protocol === 'file:'); 510 | })(); 511 | 512 | // Expose the API as global variables, unless an 'exports' 513 | // object exists, in that case we assume we're in CommonJS 514 | if ( typeof exports === "undefined" || typeof require === "undefined" ) { 515 | extend(window, QUnit); 516 | window.QUnit = QUnit; 517 | } else { 518 | extend(exports, QUnit); 519 | exports.QUnit = QUnit; 520 | } 521 | 522 | // define these after exposing globals to keep them in these QUnit namespace only 523 | extend(QUnit, { 524 | config: config, 525 | 526 | // Initialize the configuration options 527 | init: function() { 528 | extend(config, { 529 | stats: { all: 0, bad: 0 }, 530 | moduleStats: { all: 0, bad: 0 }, 531 | started: +new Date, 532 | updateRate: 1000, 533 | blocking: false, 534 | autostart: true, 535 | autorun: false, 536 | filter: "", 537 | queue: [], 538 | semaphore: 0 539 | }); 540 | 541 | var tests = id( "qunit-tests" ), 542 | banner = id( "qunit-banner" ), 543 | result = id( "qunit-testresult" ); 544 | 545 | if ( tests ) { 546 | tests.innerHTML = ""; 547 | } 548 | 549 | if ( banner ) { 550 | banner.className = ""; 551 | } 552 | 553 | if ( result ) { 554 | result.parentNode.removeChild( result ); 555 | } 556 | 557 | if ( tests ) { 558 | result = document.createElement( "p" ); 559 | result.id = "qunit-testresult"; 560 | result.className = "result"; 561 | tests.parentNode.insertBefore( result, tests ); 562 | result.innerHTML = 'Running...
     '; 563 | } 564 | }, 565 | 566 | /** 567 | * Resets the test setup. Useful for tests that modify the DOM. 568 | * 569 | * If jQuery is available, uses jQuery's html(), otherwise just innerHTML. 570 | */ 571 | reset: function() { 572 | if ( window.jQuery ) { 573 | jQuery( "#qunit-fixture" ).html( config.fixture ); 574 | } else { 575 | var main = id( 'qunit-fixture' ); 576 | if ( main ) { 577 | main.innerHTML = config.fixture; 578 | } 579 | } 580 | }, 581 | 582 | /** 583 | * Trigger an event on an element. 584 | * 585 | * @example triggerEvent( document.body, "click" ); 586 | * 587 | * @param DOMElement elem 588 | * @param String type 589 | */ 590 | triggerEvent: function( elem, type, event ) { 591 | if ( document.createEvent ) { 592 | event = document.createEvent("MouseEvents"); 593 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 594 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 595 | elem.dispatchEvent( event ); 596 | 597 | } else if ( elem.fireEvent ) { 598 | elem.fireEvent("on"+type); 599 | } 600 | }, 601 | 602 | // Safe object type checking 603 | is: function( type, obj ) { 604 | return QUnit.objectType( obj ) == type; 605 | }, 606 | 607 | objectType: function( obj ) { 608 | if (typeof obj === "undefined") { 609 | return "undefined"; 610 | 611 | // consider: typeof null === object 612 | } 613 | if (obj === null) { 614 | return "null"; 615 | } 616 | 617 | var type = toString.call( obj ).match(/^\[object\s(.*)\]$/)[1] || ''; 618 | 619 | switch (type) { 620 | case 'Number': 621 | if (isNaN(obj)) { 622 | return "nan"; 623 | } else { 624 | return "number"; 625 | } 626 | case 'String': 627 | case 'Boolean': 628 | case 'Array': 629 | case 'Date': 630 | case 'RegExp': 631 | case 'Function': 632 | return type.toLowerCase(); 633 | } 634 | if (typeof obj === "object") { 635 | return "object"; 636 | } 637 | return undefined; 638 | }, 639 | 640 | push: function(result, actual, expected, message) { 641 | var details = { 642 | result: result, 643 | message: message, 644 | actual: actual, 645 | expected: expected 646 | }; 647 | 648 | message = escapeInnerText(message) || (result ? "okay" : "failed"); 649 | message = '' + message + ""; 650 | expected = escapeInnerText(QUnit.jsDump.parse(expected)); 651 | actual = escapeInnerText(QUnit.jsDump.parse(actual)); 652 | var output = message + ''; 653 | if (actual != expected) { 654 | output += ''; 655 | output += ''; 656 | } 657 | if (!result) { 658 | var source = sourceFromStacktrace(); 659 | if (source) { 660 | details.source = source; 661 | output += ''; 662 | } 663 | } 664 | output += "
    Expected:
    ' + expected + '
    Result:
    ' + actual + '
    Diff:
    ' + QUnit.diff(expected, actual) +'
    Source:
    ' + escapeInnerText(source) + '
    "; 665 | 666 | runLoggingCallbacks( 'log', QUnit, details ); 667 | 668 | config.current.assertions.push({ 669 | result: !!result, 670 | message: output 671 | }); 672 | }, 673 | 674 | url: function( params ) { 675 | params = extend( extend( {}, QUnit.urlParams ), params ); 676 | var querystring = "?", 677 | key; 678 | for ( key in params ) { 679 | if ( !hasOwn.call( params, key ) ) { 680 | continue; 681 | } 682 | querystring += encodeURIComponent( key ) + "=" + 683 | encodeURIComponent( params[ key ] ) + "&"; 684 | } 685 | return window.location.pathname + querystring.slice( 0, -1 ); 686 | }, 687 | 688 | extend: extend, 689 | id: id, 690 | addEvent: addEvent 691 | }); 692 | 693 | //QUnit.constructor is set to the empty F() above so that we can add to it's prototype later 694 | //Doing this allows us to tell if the following methods have been overwritten on the actual 695 | //QUnit object, which is a deprecated way of using the callbacks. 696 | extend(QUnit.constructor.prototype, { 697 | // Logging callbacks; all receive a single argument with the listed properties 698 | // run test/logs.html for any related changes 699 | begin: registerLoggingCallback('begin'), 700 | // done: { failed, passed, total, runtime } 701 | done: registerLoggingCallback('done'), 702 | // log: { result, actual, expected, message } 703 | log: registerLoggingCallback('log'), 704 | // testStart: { name } 705 | testStart: registerLoggingCallback('testStart'), 706 | // testDone: { name, failed, passed, total } 707 | testDone: registerLoggingCallback('testDone'), 708 | // moduleStart: { name } 709 | moduleStart: registerLoggingCallback('moduleStart'), 710 | // moduleDone: { name, failed, passed, total } 711 | moduleDone: registerLoggingCallback('moduleDone') 712 | }); 713 | 714 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 715 | config.autorun = true; 716 | } 717 | 718 | QUnit.load = function() { 719 | runLoggingCallbacks( 'begin', QUnit, {} ); 720 | 721 | // Initialize the config, saving the execution queue 722 | var oldconfig = extend({}, config); 723 | QUnit.init(); 724 | extend(config, oldconfig); 725 | 726 | config.blocking = false; 727 | 728 | var urlConfigHtml = '', len = config.urlConfig.length; 729 | for ( var i = 0, val; i < len, val = config.urlConfig[i]; i++ ) { 730 | config[val] = QUnit.urlParams[val]; 731 | urlConfigHtml += ''; 732 | } 733 | 734 | var userAgent = id("qunit-userAgent"); 735 | if ( userAgent ) { 736 | userAgent.innerHTML = navigator.userAgent; 737 | } 738 | var banner = id("qunit-header"); 739 | if ( banner ) { 740 | banner.innerHTML = ' ' + banner.innerHTML + ' ' + urlConfigHtml; 741 | addEvent( banner, "change", function( event ) { 742 | var params = {}; 743 | params[ event.target.name ] = event.target.checked ? true : undefined; 744 | window.location = QUnit.url( params ); 745 | }); 746 | } 747 | 748 | var toolbar = id("qunit-testrunner-toolbar"); 749 | if ( toolbar ) { 750 | var filter = document.createElement("input"); 751 | filter.type = "checkbox"; 752 | filter.id = "qunit-filter-pass"; 753 | addEvent( filter, "click", function() { 754 | var ol = document.getElementById("qunit-tests"); 755 | if ( filter.checked ) { 756 | ol.className = ol.className + " hidepass"; 757 | } else { 758 | var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 759 | ol.className = tmp.replace(/ hidepass /, " "); 760 | } 761 | if ( defined.sessionStorage ) { 762 | if (filter.checked) { 763 | sessionStorage.setItem("qunit-filter-passed-tests", "true"); 764 | } else { 765 | sessionStorage.removeItem("qunit-filter-passed-tests"); 766 | } 767 | } 768 | }); 769 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { 770 | filter.checked = true; 771 | var ol = document.getElementById("qunit-tests"); 772 | ol.className = ol.className + " hidepass"; 773 | } 774 | toolbar.appendChild( filter ); 775 | 776 | var label = document.createElement("label"); 777 | label.setAttribute("for", "qunit-filter-pass"); 778 | label.innerHTML = "Hide passed tests"; 779 | toolbar.appendChild( label ); 780 | } 781 | 782 | var main = id('qunit-fixture'); 783 | if ( main ) { 784 | config.fixture = main.innerHTML; 785 | } 786 | 787 | if (config.autostart) { 788 | QUnit.start(); 789 | } 790 | }; 791 | 792 | addEvent(window, "load", QUnit.load); 793 | 794 | // addEvent(window, "error") gives us a useless event object 795 | window.onerror = function( message, file, line ) { 796 | if ( QUnit.config.current ) { 797 | ok( false, message + ", " + file + ":" + line ); 798 | } else { 799 | test( "global failure", function() { 800 | ok( false, message + ", " + file + ":" + line ); 801 | }); 802 | } 803 | }; 804 | 805 | function done() { 806 | config.autorun = true; 807 | 808 | // Log the last module results 809 | if ( config.currentModule ) { 810 | runLoggingCallbacks( 'moduleDone', QUnit, { 811 | name: config.currentModule, 812 | failed: config.moduleStats.bad, 813 | passed: config.moduleStats.all - config.moduleStats.bad, 814 | total: config.moduleStats.all 815 | } ); 816 | } 817 | 818 | var banner = id("qunit-banner"), 819 | tests = id("qunit-tests"), 820 | runtime = +new Date - config.started, 821 | passed = config.stats.all - config.stats.bad, 822 | html = [ 823 | 'Tests completed in ', 824 | runtime, 825 | ' milliseconds.
    ', 826 | '', 827 | passed, 828 | ' tests of ', 829 | config.stats.all, 830 | ' passed, ', 831 | config.stats.bad, 832 | ' failed.' 833 | ].join(''); 834 | 835 | if ( banner ) { 836 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 837 | } 838 | 839 | if ( tests ) { 840 | id( "qunit-testresult" ).innerHTML = html; 841 | } 842 | 843 | if ( config.altertitle && typeof document !== "undefined" && document.title ) { 844 | // show ✖ for good, ✔ for bad suite result in title 845 | // use escape sequences in case file gets loaded with non-utf-8-charset 846 | document.title = [ 847 | (config.stats.bad ? "\u2716" : "\u2714"), 848 | document.title.replace(/^[\u2714\u2716] /i, "") 849 | ].join(" "); 850 | } 851 | 852 | runLoggingCallbacks( 'done', QUnit, { 853 | failed: config.stats.bad, 854 | passed: passed, 855 | total: config.stats.all, 856 | runtime: runtime 857 | } ); 858 | } 859 | 860 | function validTest( name ) { 861 | var filter = config.filter, 862 | run = false; 863 | 864 | if ( !filter ) { 865 | return true; 866 | } 867 | 868 | var not = filter.charAt( 0 ) === "!"; 869 | if ( not ) { 870 | filter = filter.slice( 1 ); 871 | } 872 | 873 | if ( name.indexOf( filter ) !== -1 ) { 874 | return !not; 875 | } 876 | 877 | if ( not ) { 878 | run = true; 879 | } 880 | 881 | return run; 882 | } 883 | 884 | // so far supports only Firefox, Chrome and Opera (buggy) 885 | // could be extended in the future to use something like https://github.com/csnover/TraceKit 886 | function sourceFromStacktrace() { 887 | try { 888 | throw new Error(); 889 | } catch ( e ) { 890 | if (e.stacktrace) { 891 | // Opera 892 | return e.stacktrace.split("\n")[6]; 893 | } else if (e.stack) { 894 | // Firefox, Chrome 895 | return e.stack.split("\n")[4]; 896 | } else if (e.sourceURL) { 897 | // Safari, PhantomJS 898 | // TODO sourceURL points at the 'throw new Error' line above, useless 899 | //return e.sourceURL + ":" + e.line; 900 | } 901 | } 902 | } 903 | 904 | function escapeInnerText(s) { 905 | if (!s) { 906 | return ""; 907 | } 908 | s = s + ""; 909 | return s.replace(/[\&<>]/g, function(s) { 910 | switch(s) { 911 | case "&": return "&"; 912 | case "<": return "<"; 913 | case ">": return ">"; 914 | default: return s; 915 | } 916 | }); 917 | } 918 | 919 | function synchronize( callback, last ) { 920 | config.queue.push( callback ); 921 | 922 | if ( config.autorun && !config.blocking ) { 923 | process(last); 924 | } 925 | } 926 | 927 | function process( last ) { 928 | var start = new Date().getTime(); 929 | config.depth = config.depth ? config.depth + 1 : 1; 930 | 931 | while ( config.queue.length && !config.blocking ) { 932 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { 933 | config.queue.shift()(); 934 | } else { 935 | window.setTimeout( function(){ 936 | process( last ); 937 | }, 13 ); 938 | break; 939 | } 940 | } 941 | config.depth--; 942 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 943 | done(); 944 | } 945 | } 946 | 947 | function saveGlobal() { 948 | config.pollution = []; 949 | 950 | if ( config.noglobals ) { 951 | for ( var key in window ) { 952 | if ( !hasOwn.call( window, key ) ) { 953 | continue; 954 | } 955 | config.pollution.push( key ); 956 | } 957 | } 958 | } 959 | 960 | function checkPollution( name ) { 961 | var old = config.pollution; 962 | saveGlobal(); 963 | 964 | var newGlobals = diff( config.pollution, old ); 965 | if ( newGlobals.length > 0 ) { 966 | ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); 967 | } 968 | 969 | var deletedGlobals = diff( old, config.pollution ); 970 | if ( deletedGlobals.length > 0 ) { 971 | ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); 972 | } 973 | } 974 | 975 | // returns a new Array with the elements that are in a but not in b 976 | function diff( a, b ) { 977 | var result = a.slice(); 978 | for ( var i = 0; i < result.length; i++ ) { 979 | for ( var j = 0; j < b.length; j++ ) { 980 | if ( result[i] === b[j] ) { 981 | result.splice(i, 1); 982 | i--; 983 | break; 984 | } 985 | } 986 | } 987 | return result; 988 | } 989 | 990 | function fail(message, exception, callback) { 991 | if ( typeof console !== "undefined" && console.error && console.warn ) { 992 | console.error(message); 993 | console.error(exception); 994 | console.error(exception.stack); 995 | console.warn(callback.toString()); 996 | 997 | } else if ( window.opera && opera.postError ) { 998 | opera.postError(message, exception, callback.toString); 999 | } 1000 | } 1001 | 1002 | function extend(a, b) { 1003 | for ( var prop in b ) { 1004 | if ( b[prop] === undefined ) { 1005 | delete a[prop]; 1006 | 1007 | // Avoid "Member not found" error in IE8 caused by setting window.constructor 1008 | } else if ( prop !== "constructor" || a !== window ) { 1009 | a[prop] = b[prop]; 1010 | } 1011 | } 1012 | 1013 | return a; 1014 | } 1015 | 1016 | function addEvent(elem, type, fn) { 1017 | if ( elem.addEventListener ) { 1018 | elem.addEventListener( type, fn, false ); 1019 | } else if ( elem.attachEvent ) { 1020 | elem.attachEvent( "on" + type, fn ); 1021 | } else { 1022 | fn(); 1023 | } 1024 | } 1025 | 1026 | function id(name) { 1027 | return !!(typeof document !== "undefined" && document && document.getElementById) && 1028 | document.getElementById( name ); 1029 | } 1030 | 1031 | function registerLoggingCallback(key){ 1032 | return function(callback){ 1033 | config[key].push( callback ); 1034 | }; 1035 | } 1036 | 1037 | // Supports deprecated method of completely overwriting logging callbacks 1038 | function runLoggingCallbacks(key, scope, args) { 1039 | //debugger; 1040 | var callbacks; 1041 | if ( QUnit.hasOwnProperty(key) ) { 1042 | QUnit[key].call(scope, args); 1043 | } else { 1044 | callbacks = config[key]; 1045 | for( var i = 0; i < callbacks.length; i++ ) { 1046 | callbacks[i].call( scope, args ); 1047 | } 1048 | } 1049 | } 1050 | 1051 | // Test for equality any JavaScript type. 1052 | // Author: Philippe Rathé 1053 | QUnit.equiv = function () { 1054 | 1055 | var innerEquiv; // the real equiv function 1056 | var callers = []; // stack to decide between skip/abort functions 1057 | var parents = []; // stack to avoiding loops from circular referencing 1058 | 1059 | // Call the o related callback with the given arguments. 1060 | function bindCallbacks(o, callbacks, args) { 1061 | var prop = QUnit.objectType(o); 1062 | if (prop) { 1063 | if (QUnit.objectType(callbacks[prop]) === "function") { 1064 | return callbacks[prop].apply(callbacks, args); 1065 | } else { 1066 | return callbacks[prop]; // or undefined 1067 | } 1068 | } 1069 | } 1070 | 1071 | var getProto = Object.getPrototypeOf || function (obj) { 1072 | return obj.__proto__; 1073 | }; 1074 | 1075 | var callbacks = function () { 1076 | 1077 | // for string, boolean, number and null 1078 | function useStrictEquality(b, a) { 1079 | if (b instanceof a.constructor || a instanceof b.constructor) { 1080 | // to catch short annotaion VS 'new' annotation of a 1081 | // declaration 1082 | // e.g. var i = 1; 1083 | // var j = new Number(1); 1084 | return a == b; 1085 | } else { 1086 | return a === b; 1087 | } 1088 | } 1089 | 1090 | return { 1091 | "string" : useStrictEquality, 1092 | "boolean" : useStrictEquality, 1093 | "number" : useStrictEquality, 1094 | "null" : useStrictEquality, 1095 | "undefined" : useStrictEquality, 1096 | 1097 | "nan" : function(b) { 1098 | return isNaN(b); 1099 | }, 1100 | 1101 | "date" : function(b, a) { 1102 | return QUnit.objectType(b) === "date" 1103 | && a.valueOf() === b.valueOf(); 1104 | }, 1105 | 1106 | "regexp" : function(b, a) { 1107 | return QUnit.objectType(b) === "regexp" 1108 | && a.source === b.source && // the regex itself 1109 | a.global === b.global && // and its modifers 1110 | // (gmi) ... 1111 | a.ignoreCase === b.ignoreCase 1112 | && a.multiline === b.multiline; 1113 | }, 1114 | 1115 | // - skip when the property is a method of an instance (OOP) 1116 | // - abort otherwise, 1117 | // initial === would have catch identical references anyway 1118 | "function" : function() { 1119 | var caller = callers[callers.length - 1]; 1120 | return caller !== Object && typeof caller !== "undefined"; 1121 | }, 1122 | 1123 | "array" : function(b, a) { 1124 | var i, j, loop; 1125 | var len; 1126 | 1127 | // b could be an object literal here 1128 | if (!(QUnit.objectType(b) === "array")) { 1129 | return false; 1130 | } 1131 | 1132 | len = a.length; 1133 | if (len !== b.length) { // safe and faster 1134 | return false; 1135 | } 1136 | 1137 | // track reference to avoid circular references 1138 | parents.push(a); 1139 | for (i = 0; i < len; i++) { 1140 | loop = false; 1141 | for (j = 0; j < parents.length; j++) { 1142 | if (parents[j] === a[i]) { 1143 | loop = true;// dont rewalk array 1144 | } 1145 | } 1146 | if (!loop && !innerEquiv(a[i], b[i])) { 1147 | parents.pop(); 1148 | return false; 1149 | } 1150 | } 1151 | parents.pop(); 1152 | return true; 1153 | }, 1154 | 1155 | "object" : function(b, a) { 1156 | var i, j, loop; 1157 | var eq = true; // unless we can proove it 1158 | var aProperties = [], bProperties = []; // collection of 1159 | // strings 1160 | 1161 | // comparing constructors is more strict than using 1162 | // instanceof 1163 | if (a.constructor !== b.constructor) { 1164 | // Allow objects with no prototype to be equivalent to 1165 | // objects with Object as their constructor. 1166 | if (!((getProto(a) === null && getProto(b) === Object.prototype) || 1167 | (getProto(b) === null && getProto(a) === Object.prototype))) 1168 | { 1169 | return false; 1170 | } 1171 | } 1172 | 1173 | // stack constructor before traversing properties 1174 | callers.push(a.constructor); 1175 | // track reference to avoid circular references 1176 | parents.push(a); 1177 | 1178 | for (i in a) { // be strict: don't ensures hasOwnProperty 1179 | // and go deep 1180 | loop = false; 1181 | for (j = 0; j < parents.length; j++) { 1182 | if (parents[j] === a[i]) 1183 | loop = true; // don't go down the same path 1184 | // twice 1185 | } 1186 | aProperties.push(i); // collect a's properties 1187 | 1188 | if (!loop && !innerEquiv(a[i], b[i])) { 1189 | eq = false; 1190 | break; 1191 | } 1192 | } 1193 | 1194 | callers.pop(); // unstack, we are done 1195 | parents.pop(); 1196 | 1197 | for (i in b) { 1198 | bProperties.push(i); // collect b's properties 1199 | } 1200 | 1201 | // Ensures identical properties name 1202 | return eq 1203 | && innerEquiv(aProperties.sort(), bProperties 1204 | .sort()); 1205 | } 1206 | }; 1207 | }(); 1208 | 1209 | innerEquiv = function() { // can take multiple arguments 1210 | var args = Array.prototype.slice.apply(arguments); 1211 | if (args.length < 2) { 1212 | return true; // end transition 1213 | } 1214 | 1215 | return (function(a, b) { 1216 | if (a === b) { 1217 | return true; // catch the most you can 1218 | } else if (a === null || b === null || typeof a === "undefined" 1219 | || typeof b === "undefined" 1220 | || QUnit.objectType(a) !== QUnit.objectType(b)) { 1221 | return false; // don't lose time with error prone cases 1222 | } else { 1223 | return bindCallbacks(a, callbacks, [ b, a ]); 1224 | } 1225 | 1226 | // apply transition with (1..n) arguments 1227 | })(args[0], args[1]) 1228 | && arguments.callee.apply(this, args.splice(1, 1229 | args.length - 1)); 1230 | }; 1231 | 1232 | return innerEquiv; 1233 | 1234 | }(); 1235 | 1236 | /** 1237 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1238 | * http://flesler.blogspot.com Licensed under BSD 1239 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1240 | * 1241 | * @projectDescription Advanced and extensible data dumping for Javascript. 1242 | * @version 1.0.0 1243 | * @author Ariel Flesler 1244 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1245 | */ 1246 | QUnit.jsDump = (function() { 1247 | function quote( str ) { 1248 | return '"' + str.toString().replace(/"/g, '\\"') + '"'; 1249 | }; 1250 | function literal( o ) { 1251 | return o + ''; 1252 | }; 1253 | function join( pre, arr, post ) { 1254 | var s = jsDump.separator(), 1255 | base = jsDump.indent(), 1256 | inner = jsDump.indent(1); 1257 | if ( arr.join ) 1258 | arr = arr.join( ',' + s + inner ); 1259 | if ( !arr ) 1260 | return pre + post; 1261 | return [ pre, inner + arr, base + post ].join(s); 1262 | }; 1263 | function array( arr, stack ) { 1264 | var i = arr.length, ret = Array(i); 1265 | this.up(); 1266 | while ( i-- ) 1267 | ret[i] = this.parse( arr[i] , undefined , stack); 1268 | this.down(); 1269 | return join( '[', ret, ']' ); 1270 | }; 1271 | 1272 | var reName = /^function (\w+)/; 1273 | 1274 | var jsDump = { 1275 | parse:function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance 1276 | stack = stack || [ ]; 1277 | var parser = this.parsers[ type || this.typeOf(obj) ]; 1278 | type = typeof parser; 1279 | var inStack = inArray(obj, stack); 1280 | if (inStack != -1) { 1281 | return 'recursion('+(inStack - stack.length)+')'; 1282 | } 1283 | //else 1284 | if (type == 'function') { 1285 | stack.push(obj); 1286 | var res = parser.call( this, obj, stack ); 1287 | stack.pop(); 1288 | return res; 1289 | } 1290 | // else 1291 | return (type == 'string') ? parser : this.parsers.error; 1292 | }, 1293 | typeOf:function( obj ) { 1294 | var type; 1295 | if ( obj === null ) { 1296 | type = "null"; 1297 | } else if (typeof obj === "undefined") { 1298 | type = "undefined"; 1299 | } else if (QUnit.is("RegExp", obj)) { 1300 | type = "regexp"; 1301 | } else if (QUnit.is("Date", obj)) { 1302 | type = "date"; 1303 | } else if (QUnit.is("Function", obj)) { 1304 | type = "function"; 1305 | } else if (typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined") { 1306 | type = "window"; 1307 | } else if (obj.nodeType === 9) { 1308 | type = "document"; 1309 | } else if (obj.nodeType) { 1310 | type = "node"; 1311 | } else if ( 1312 | // native arrays 1313 | toString.call( obj ) === "[object Array]" || 1314 | // NodeList objects 1315 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) 1316 | ) { 1317 | type = "array"; 1318 | } else { 1319 | type = typeof obj; 1320 | } 1321 | return type; 1322 | }, 1323 | separator:function() { 1324 | return this.multiline ? this.HTML ? '
    ' : '\n' : this.HTML ? ' ' : ' '; 1325 | }, 1326 | indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1327 | if ( !this.multiline ) 1328 | return ''; 1329 | var chr = this.indentChar; 1330 | if ( this.HTML ) 1331 | chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 1332 | return Array( this._depth_ + (extra||0) ).join(chr); 1333 | }, 1334 | up:function( a ) { 1335 | this._depth_ += a || 1; 1336 | }, 1337 | down:function( a ) { 1338 | this._depth_ -= a || 1; 1339 | }, 1340 | setParser:function( name, parser ) { 1341 | this.parsers[name] = parser; 1342 | }, 1343 | // The next 3 are exposed so you can use them 1344 | quote:quote, 1345 | literal:literal, 1346 | join:join, 1347 | // 1348 | _depth_: 1, 1349 | // This is the list of parsers, to modify them, use jsDump.setParser 1350 | parsers:{ 1351 | window: '[Window]', 1352 | document: '[Document]', 1353 | error:'[ERROR]', //when no parser is found, shouldn't happen 1354 | unknown: '[Unknown]', 1355 | 'null':'null', 1356 | 'undefined':'undefined', 1357 | 'function':function( fn ) { 1358 | var ret = 'function', 1359 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 1360 | if ( name ) 1361 | ret += ' ' + name; 1362 | ret += '('; 1363 | 1364 | ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); 1365 | return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); 1366 | }, 1367 | array: array, 1368 | nodelist: array, 1369 | arguments: array, 1370 | object:function( map, stack ) { 1371 | var ret = [ ]; 1372 | QUnit.jsDump.up(); 1373 | for ( var key in map ) { 1374 | var val = map[key]; 1375 | ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(val, undefined, stack)); 1376 | } 1377 | QUnit.jsDump.down(); 1378 | return join( '{', ret, '}' ); 1379 | }, 1380 | node:function( node ) { 1381 | var open = QUnit.jsDump.HTML ? '<' : '<', 1382 | close = QUnit.jsDump.HTML ? '>' : '>'; 1383 | 1384 | var tag = node.nodeName.toLowerCase(), 1385 | ret = open + tag; 1386 | 1387 | for ( var a in QUnit.jsDump.DOMAttrs ) { 1388 | var val = node[QUnit.jsDump.DOMAttrs[a]]; 1389 | if ( val ) 1390 | ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); 1391 | } 1392 | return ret + close + open + '/' + tag + close; 1393 | }, 1394 | functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function 1395 | var l = fn.length; 1396 | if ( !l ) return ''; 1397 | 1398 | var args = Array(l); 1399 | while ( l-- ) 1400 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1401 | return ' ' + args.join(', ') + ' '; 1402 | }, 1403 | key:quote, //object calls it internally, the key part of an item in a map 1404 | functionCode:'[code]', //function calls it internally, it's the content of the function 1405 | attribute:quote, //node calls it internally, it's an html attribute value 1406 | string:quote, 1407 | date:quote, 1408 | regexp:literal, //regex 1409 | number:literal, 1410 | 'boolean':literal 1411 | }, 1412 | DOMAttrs:{//attributes to dump from nodes, name=>realName 1413 | id:'id', 1414 | name:'name', 1415 | 'class':'className' 1416 | }, 1417 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) 1418 | indentChar:' ',//indentation unit 1419 | multiline:true //if true, items in a collection, are separated by a \n, else just a space. 1420 | }; 1421 | 1422 | return jsDump; 1423 | })(); 1424 | 1425 | // from Sizzle.js 1426 | function getText( elems ) { 1427 | var ret = "", elem; 1428 | 1429 | for ( var i = 0; elems[i]; i++ ) { 1430 | elem = elems[i]; 1431 | 1432 | // Get the text from text nodes and CDATA nodes 1433 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1434 | ret += elem.nodeValue; 1435 | 1436 | // Traverse everything else, except comment nodes 1437 | } else if ( elem.nodeType !== 8 ) { 1438 | ret += getText( elem.childNodes ); 1439 | } 1440 | } 1441 | 1442 | return ret; 1443 | }; 1444 | 1445 | //from jquery.js 1446 | function inArray( elem, array ) { 1447 | if ( array.indexOf ) { 1448 | return array.indexOf( elem ); 1449 | } 1450 | 1451 | for ( var i = 0, length = array.length; i < length; i++ ) { 1452 | if ( array[ i ] === elem ) { 1453 | return i; 1454 | } 1455 | } 1456 | 1457 | return -1; 1458 | } 1459 | 1460 | /* 1461 | * Javascript Diff Algorithm 1462 | * By John Resig (http://ejohn.org/) 1463 | * Modified by Chu Alan "sprite" 1464 | * 1465 | * Released under the MIT license. 1466 | * 1467 | * More Info: 1468 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1469 | * 1470 | * Usage: QUnit.diff(expected, actual) 1471 | * 1472 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" 1473 | */ 1474 | QUnit.diff = (function() { 1475 | function diff(o, n) { 1476 | var ns = {}; 1477 | var os = {}; 1478 | 1479 | for (var i = 0; i < n.length; i++) { 1480 | if (ns[n[i]] == null) 1481 | ns[n[i]] = { 1482 | rows: [], 1483 | o: null 1484 | }; 1485 | ns[n[i]].rows.push(i); 1486 | } 1487 | 1488 | for (var i = 0; i < o.length; i++) { 1489 | if (os[o[i]] == null) 1490 | os[o[i]] = { 1491 | rows: [], 1492 | n: null 1493 | }; 1494 | os[o[i]].rows.push(i); 1495 | } 1496 | 1497 | for (var i in ns) { 1498 | if ( !hasOwn.call( ns, i ) ) { 1499 | continue; 1500 | } 1501 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1502 | n[ns[i].rows[0]] = { 1503 | text: n[ns[i].rows[0]], 1504 | row: os[i].rows[0] 1505 | }; 1506 | o[os[i].rows[0]] = { 1507 | text: o[os[i].rows[0]], 1508 | row: ns[i].rows[0] 1509 | }; 1510 | } 1511 | } 1512 | 1513 | for (var i = 0; i < n.length - 1; i++) { 1514 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && 1515 | n[i + 1] == o[n[i].row + 1]) { 1516 | n[i + 1] = { 1517 | text: n[i + 1], 1518 | row: n[i].row + 1 1519 | }; 1520 | o[n[i].row + 1] = { 1521 | text: o[n[i].row + 1], 1522 | row: i + 1 1523 | }; 1524 | } 1525 | } 1526 | 1527 | for (var i = n.length - 1; i > 0; i--) { 1528 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 1529 | n[i - 1] == o[n[i].row - 1]) { 1530 | n[i - 1] = { 1531 | text: n[i - 1], 1532 | row: n[i].row - 1 1533 | }; 1534 | o[n[i].row - 1] = { 1535 | text: o[n[i].row - 1], 1536 | row: i - 1 1537 | }; 1538 | } 1539 | } 1540 | 1541 | return { 1542 | o: o, 1543 | n: n 1544 | }; 1545 | } 1546 | 1547 | return function(o, n) { 1548 | o = o.replace(/\s+$/, ''); 1549 | n = n.replace(/\s+$/, ''); 1550 | var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); 1551 | 1552 | var str = ""; 1553 | 1554 | var oSpace = o.match(/\s+/g); 1555 | if (oSpace == null) { 1556 | oSpace = [" "]; 1557 | } 1558 | else { 1559 | oSpace.push(" "); 1560 | } 1561 | var nSpace = n.match(/\s+/g); 1562 | if (nSpace == null) { 1563 | nSpace = [" "]; 1564 | } 1565 | else { 1566 | nSpace.push(" "); 1567 | } 1568 | 1569 | if (out.n.length == 0) { 1570 | for (var i = 0; i < out.o.length; i++) { 1571 | str += '' + out.o[i] + oSpace[i] + ""; 1572 | } 1573 | } 1574 | else { 1575 | if (out.n[0].text == null) { 1576 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1577 | str += '' + out.o[n] + oSpace[n] + ""; 1578 | } 1579 | } 1580 | 1581 | for (var i = 0; i < out.n.length; i++) { 1582 | if (out.n[i].text == null) { 1583 | str += '' + out.n[i] + nSpace[i] + ""; 1584 | } 1585 | else { 1586 | var pre = ""; 1587 | 1588 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1589 | pre += '' + out.o[n] + oSpace[n] + ""; 1590 | } 1591 | str += " " + out.n[i].text + nSpace[i] + pre; 1592 | } 1593 | } 1594 | } 1595 | 1596 | return str; 1597 | }; 1598 | })(); 1599 | 1600 | })(this); 1601 | -------------------------------------------------------------------------------- /js/test/vendor/underscore-1.2.2.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.2.2 2 | // (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Underscore is freely distributable under the MIT license. 4 | // Portions of Underscore are inspired or borrowed from Prototype, 5 | // Oliver Steele's Functional, and John Resig's Micro-Templating. 6 | // For all details and documentation: 7 | // http://documentcloud.github.com/underscore 8 | 9 | (function() { 10 | 11 | // Baseline setup 12 | // -------------- 13 | 14 | // Establish the root object, `window` in the browser, or `global` on the server. 15 | var root = this; 16 | 17 | // Save the previous value of the `_` variable. 18 | var previousUnderscore = root._; 19 | 20 | // Establish the object that gets returned to break out of a loop iteration. 21 | var breaker = {}; 22 | 23 | // Save bytes in the minified (but not gzipped) version: 24 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; 25 | 26 | // Create quick reference variables for speed access to core prototypes. 27 | var slice = ArrayProto.slice, 28 | unshift = ArrayProto.unshift, 29 | toString = ObjProto.toString, 30 | hasOwnProperty = ObjProto.hasOwnProperty; 31 | 32 | // All **ECMAScript 5** native function implementations that we hope to use 33 | // are declared here. 34 | var 35 | nativeForEach = ArrayProto.forEach, 36 | nativeMap = ArrayProto.map, 37 | nativeReduce = ArrayProto.reduce, 38 | nativeReduceRight = ArrayProto.reduceRight, 39 | nativeFilter = ArrayProto.filter, 40 | nativeEvery = ArrayProto.every, 41 | nativeSome = ArrayProto.some, 42 | nativeIndexOf = ArrayProto.indexOf, 43 | nativeLastIndexOf = ArrayProto.lastIndexOf, 44 | nativeIsArray = Array.isArray, 45 | nativeKeys = Object.keys, 46 | nativeBind = FuncProto.bind; 47 | 48 | // Create a safe reference to the Underscore object for use below. 49 | var _ = function(obj) { return new wrapper(obj); }; 50 | 51 | // Export the Underscore object for **Node.js** and **"CommonJS"**, with 52 | // backwards-compatibility for the old `require()` API. If we're not in 53 | // CommonJS, add `_` to the global object. 54 | if (typeof exports !== 'undefined') { 55 | if (typeof module !== 'undefined' && module.exports) { 56 | exports = module.exports = _; 57 | } 58 | exports._ = _; 59 | } else if (typeof define === 'function' && define.amd) { 60 | // Register as a named module with AMD. 61 | define('underscore', function() { 62 | return _; 63 | }); 64 | } else { 65 | // Exported as a string, for Closure Compiler "advanced" mode. 66 | root['_'] = _; 67 | } 68 | 69 | // Current version. 70 | _.VERSION = '1.2.2'; 71 | 72 | // Collection Functions 73 | // -------------------- 74 | 75 | // The cornerstone, an `each` implementation, aka `forEach`. 76 | // Handles objects with the built-in `forEach`, arrays, and raw objects. 77 | // Delegates to **ECMAScript 5**'s native `forEach` if available. 78 | var each = _.each = _.forEach = function(obj, iterator, context) { 79 | if (obj == null) return; 80 | if (nativeForEach && obj.forEach === nativeForEach) { 81 | obj.forEach(iterator, context); 82 | } else if (obj.length === +obj.length) { 83 | for (var i = 0, l = obj.length; i < l; i++) { 84 | if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return; 85 | } 86 | } else { 87 | for (var key in obj) { 88 | if (hasOwnProperty.call(obj, key)) { 89 | if (iterator.call(context, obj[key], key, obj) === breaker) return; 90 | } 91 | } 92 | } 93 | }; 94 | 95 | // Return the results of applying the iterator to each element. 96 | // Delegates to **ECMAScript 5**'s native `map` if available. 97 | _.map = function(obj, iterator, context) { 98 | var results = []; 99 | if (obj == null) return results; 100 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); 101 | each(obj, function(value, index, list) { 102 | results[results.length] = iterator.call(context, value, index, list); 103 | }); 104 | return results; 105 | }; 106 | 107 | // **Reduce** builds up a single result from a list of values, aka `inject`, 108 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. 109 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { 110 | var initial = memo !== void 0; 111 | if (obj == null) obj = []; 112 | if (nativeReduce && obj.reduce === nativeReduce) { 113 | if (context) iterator = _.bind(iterator, context); 114 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); 115 | } 116 | each(obj, function(value, index, list) { 117 | if (!initial) { 118 | memo = value; 119 | initial = true; 120 | } else { 121 | memo = iterator.call(context, memo, value, index, list); 122 | } 123 | }); 124 | if (!initial) throw new TypeError("Reduce of empty array with no initial value"); 125 | return memo; 126 | }; 127 | 128 | // The right-associative version of reduce, also known as `foldr`. 129 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available. 130 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) { 131 | if (obj == null) obj = []; 132 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { 133 | if (context) iterator = _.bind(iterator, context); 134 | return memo !== void 0 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); 135 | } 136 | var reversed = (_.isArray(obj) ? obj.slice() : _.toArray(obj)).reverse(); 137 | return _.reduce(reversed, iterator, memo, context); 138 | }; 139 | 140 | // Return the first value which passes a truth test. Aliased as `detect`. 141 | _.find = _.detect = function(obj, iterator, context) { 142 | var result; 143 | any(obj, function(value, index, list) { 144 | if (iterator.call(context, value, index, list)) { 145 | result = value; 146 | return true; 147 | } 148 | }); 149 | return result; 150 | }; 151 | 152 | // Return all the elements that pass a truth test. 153 | // Delegates to **ECMAScript 5**'s native `filter` if available. 154 | // Aliased as `select`. 155 | _.filter = _.select = function(obj, iterator, context) { 156 | var results = []; 157 | if (obj == null) return results; 158 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); 159 | each(obj, function(value, index, list) { 160 | if (iterator.call(context, value, index, list)) results[results.length] = value; 161 | }); 162 | return results; 163 | }; 164 | 165 | // Return all the elements for which a truth test fails. 166 | _.reject = function(obj, iterator, context) { 167 | var results = []; 168 | if (obj == null) return results; 169 | each(obj, function(value, index, list) { 170 | if (!iterator.call(context, value, index, list)) results[results.length] = value; 171 | }); 172 | return results; 173 | }; 174 | 175 | // Determine whether all of the elements match a truth test. 176 | // Delegates to **ECMAScript 5**'s native `every` if available. 177 | // Aliased as `all`. 178 | _.every = _.all = function(obj, iterator, context) { 179 | var result = true; 180 | if (obj == null) return result; 181 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); 182 | each(obj, function(value, index, list) { 183 | if (!(result = result && iterator.call(context, value, index, list))) return breaker; 184 | }); 185 | return result; 186 | }; 187 | 188 | // Determine if at least one element in the object matches a truth test. 189 | // Delegates to **ECMAScript 5**'s native `some` if available. 190 | // Aliased as `any`. 191 | var any = _.some = _.any = function(obj, iterator, context) { 192 | iterator = iterator || _.identity; 193 | var result = false; 194 | if (obj == null) return result; 195 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); 196 | each(obj, function(value, index, list) { 197 | if (result || (result = iterator.call(context, value, index, list))) return breaker; 198 | }); 199 | return !!result; 200 | }; 201 | 202 | // Determine if a given value is included in the array or object using `===`. 203 | // Aliased as `contains`. 204 | _.include = _.contains = function(obj, target) { 205 | var found = false; 206 | if (obj == null) return found; 207 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; 208 | found = any(obj, function(value) { 209 | return value === target; 210 | }); 211 | return found; 212 | }; 213 | 214 | // Invoke a method (with arguments) on every item in a collection. 215 | _.invoke = function(obj, method) { 216 | var args = slice.call(arguments, 2); 217 | return _.map(obj, function(value) { 218 | return (method.call ? method || value : value[method]).apply(value, args); 219 | }); 220 | }; 221 | 222 | // Convenience version of a common use case of `map`: fetching a property. 223 | _.pluck = function(obj, key) { 224 | return _.map(obj, function(value){ return value[key]; }); 225 | }; 226 | 227 | // Return the maximum element or (element-based computation). 228 | _.max = function(obj, iterator, context) { 229 | if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj); 230 | if (!iterator && _.isEmpty(obj)) return -Infinity; 231 | var result = {computed : -Infinity}; 232 | each(obj, function(value, index, list) { 233 | var computed = iterator ? iterator.call(context, value, index, list) : value; 234 | computed >= result.computed && (result = {value : value, computed : computed}); 235 | }); 236 | return result.value; 237 | }; 238 | 239 | // Return the minimum element (or element-based computation). 240 | _.min = function(obj, iterator, context) { 241 | if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj); 242 | if (!iterator && _.isEmpty(obj)) return Infinity; 243 | var result = {computed : Infinity}; 244 | each(obj, function(value, index, list) { 245 | var computed = iterator ? iterator.call(context, value, index, list) : value; 246 | computed < result.computed && (result = {value : value, computed : computed}); 247 | }); 248 | return result.value; 249 | }; 250 | 251 | // Shuffle an array. 252 | _.shuffle = function(obj) { 253 | var shuffled = [], rand; 254 | each(obj, function(value, index, list) { 255 | if (index == 0) { 256 | shuffled[0] = value; 257 | } else { 258 | rand = Math.floor(Math.random() * (index + 1)); 259 | shuffled[index] = shuffled[rand]; 260 | shuffled[rand] = value; 261 | } 262 | }); 263 | return shuffled; 264 | }; 265 | 266 | // Sort the object's values by a criterion produced by an iterator. 267 | _.sortBy = function(obj, iterator, context) { 268 | return _.pluck(_.map(obj, function(value, index, list) { 269 | return { 270 | value : value, 271 | criteria : iterator.call(context, value, index, list) 272 | }; 273 | }).sort(function(left, right) { 274 | var a = left.criteria, b = right.criteria; 275 | return a < b ? -1 : a > b ? 1 : 0; 276 | }), 'value'); 277 | }; 278 | 279 | // Groups the object's values by a criterion. Pass either a string attribute 280 | // to group by, or a function that returns the criterion. 281 | _.groupBy = function(obj, val) { 282 | var result = {}; 283 | var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; }; 284 | each(obj, function(value, index) { 285 | var key = iterator(value, index); 286 | (result[key] || (result[key] = [])).push(value); 287 | }); 288 | return result; 289 | }; 290 | 291 | // Use a comparator function to figure out at what index an object should 292 | // be inserted so as to maintain order. Uses binary search. 293 | _.sortedIndex = function(array, obj, iterator) { 294 | iterator || (iterator = _.identity); 295 | var low = 0, high = array.length; 296 | while (low < high) { 297 | var mid = (low + high) >> 1; 298 | iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid; 299 | } 300 | return low; 301 | }; 302 | 303 | // Safely convert anything iterable into a real, live array. 304 | _.toArray = function(iterable) { 305 | if (!iterable) return []; 306 | if (iterable.toArray) return iterable.toArray(); 307 | if (_.isArray(iterable)) return slice.call(iterable); 308 | if (_.isArguments(iterable)) return slice.call(iterable); 309 | return _.values(iterable); 310 | }; 311 | 312 | // Return the number of elements in an object. 313 | _.size = function(obj) { 314 | return _.toArray(obj).length; 315 | }; 316 | 317 | // Array Functions 318 | // --------------- 319 | 320 | // Get the first element of an array. Passing **n** will return the first N 321 | // values in the array. Aliased as `head`. The **guard** check allows it to work 322 | // with `_.map`. 323 | _.first = _.head = function(array, n, guard) { 324 | return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; 325 | }; 326 | 327 | // Returns everything but the last entry of the array. Especcialy useful on 328 | // the arguments object. Passing **n** will return all the values in 329 | // the array, excluding the last N. The **guard** check allows it to work with 330 | // `_.map`. 331 | _.initial = function(array, n, guard) { 332 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); 333 | }; 334 | 335 | // Get the last element of an array. Passing **n** will return the last N 336 | // values in the array. The **guard** check allows it to work with `_.map`. 337 | _.last = function(array, n, guard) { 338 | if ((n != null) && !guard) { 339 | return slice.call(array, Math.max(array.length - n, 0)); 340 | } else { 341 | return array[array.length - 1]; 342 | } 343 | }; 344 | 345 | // Returns everything but the first entry of the array. Aliased as `tail`. 346 | // Especially useful on the arguments object. Passing an **index** will return 347 | // the rest of the values in the array from that index onward. The **guard** 348 | // check allows it to work with `_.map`. 349 | _.rest = _.tail = function(array, index, guard) { 350 | return slice.call(array, (index == null) || guard ? 1 : index); 351 | }; 352 | 353 | // Trim out all falsy values from an array. 354 | _.compact = function(array) { 355 | return _.filter(array, function(value){ return !!value; }); 356 | }; 357 | 358 | // Return a completely flattened version of an array. 359 | _.flatten = function(array, shallow) { 360 | return _.reduce(array, function(memo, value) { 361 | if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value)); 362 | memo[memo.length] = value; 363 | return memo; 364 | }, []); 365 | }; 366 | 367 | // Return a version of the array that does not contain the specified value(s). 368 | _.without = function(array) { 369 | return _.difference(array, slice.call(arguments, 1)); 370 | }; 371 | 372 | // Produce a duplicate-free version of the array. If the array has already 373 | // been sorted, you have the option of using a faster algorithm. 374 | // Aliased as `unique`. 375 | _.uniq = _.unique = function(array, isSorted, iterator) { 376 | var initial = iterator ? _.map(array, iterator) : array; 377 | var result = []; 378 | _.reduce(initial, function(memo, el, i) { 379 | if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) { 380 | memo[memo.length] = el; 381 | result[result.length] = array[i]; 382 | } 383 | return memo; 384 | }, []); 385 | return result; 386 | }; 387 | 388 | // Produce an array that contains the union: each distinct element from all of 389 | // the passed-in arrays. 390 | _.union = function() { 391 | return _.uniq(_.flatten(arguments, true)); 392 | }; 393 | 394 | // Produce an array that contains every item shared between all the 395 | // passed-in arrays. (Aliased as "intersect" for back-compat.) 396 | _.intersection = _.intersect = function(array) { 397 | var rest = slice.call(arguments, 1); 398 | return _.filter(_.uniq(array), function(item) { 399 | return _.every(rest, function(other) { 400 | return _.indexOf(other, item) >= 0; 401 | }); 402 | }); 403 | }; 404 | 405 | // Take the difference between one array and another. 406 | // Only the elements present in just the first array will remain. 407 | _.difference = function(array, other) { 408 | return _.filter(array, function(value){ return !_.include(other, value); }); 409 | }; 410 | 411 | // Zip together multiple lists into a single array -- elements that share 412 | // an index go together. 413 | _.zip = function() { 414 | var args = slice.call(arguments); 415 | var length = _.max(_.pluck(args, 'length')); 416 | var results = new Array(length); 417 | for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i); 418 | return results; 419 | }; 420 | 421 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), 422 | // we need this function. Return the position of the first occurrence of an 423 | // item in an array, or -1 if the item is not included in the array. 424 | // Delegates to **ECMAScript 5**'s native `indexOf` if available. 425 | // If the array is large and already in sort order, pass `true` 426 | // for **isSorted** to use binary search. 427 | _.indexOf = function(array, item, isSorted) { 428 | if (array == null) return -1; 429 | var i, l; 430 | if (isSorted) { 431 | i = _.sortedIndex(array, item); 432 | return array[i] === item ? i : -1; 433 | } 434 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); 435 | for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i; 436 | return -1; 437 | }; 438 | 439 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. 440 | _.lastIndexOf = function(array, item) { 441 | if (array == null) return -1; 442 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item); 443 | var i = array.length; 444 | while (i--) if (array[i] === item) return i; 445 | return -1; 446 | }; 447 | 448 | // Generate an integer Array containing an arithmetic progression. A port of 449 | // the native Python `range()` function. See 450 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 451 | _.range = function(start, stop, step) { 452 | if (arguments.length <= 1) { 453 | stop = start || 0; 454 | start = 0; 455 | } 456 | step = arguments[2] || 1; 457 | 458 | var len = Math.max(Math.ceil((stop - start) / step), 0); 459 | var idx = 0; 460 | var range = new Array(len); 461 | 462 | while(idx < len) { 463 | range[idx++] = start; 464 | start += step; 465 | } 466 | 467 | return range; 468 | }; 469 | 470 | // Function (ahem) Functions 471 | // ------------------ 472 | 473 | // Reusable constructor function for prototype setting. 474 | var ctor = function(){}; 475 | 476 | // Create a function bound to a given object (assigning `this`, and arguments, 477 | // optionally). Binding with arguments is also known as `curry`. 478 | // Delegates to **ECMAScript 5**'s native `Function.bind` if available. 479 | // We check for `func.bind` first, to fail fast when `func` is undefined. 480 | _.bind = function bind(func, context) { 481 | var bound, args; 482 | if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); 483 | if (!_.isFunction(func)) throw new TypeError; 484 | args = slice.call(arguments, 2); 485 | return bound = function() { 486 | if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); 487 | ctor.prototype = func.prototype; 488 | var self = new ctor; 489 | var result = func.apply(self, args.concat(slice.call(arguments))); 490 | if (Object(result) === result) return result; 491 | return self; 492 | }; 493 | }; 494 | 495 | // Bind all of an object's methods to that object. Useful for ensuring that 496 | // all callbacks defined on an object belong to it. 497 | _.bindAll = function(obj) { 498 | var funcs = slice.call(arguments, 1); 499 | if (funcs.length == 0) funcs = _.functions(obj); 500 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); 501 | return obj; 502 | }; 503 | 504 | // Memoize an expensive function by storing its results. 505 | _.memoize = function(func, hasher) { 506 | var memo = {}; 507 | hasher || (hasher = _.identity); 508 | return function() { 509 | var key = hasher.apply(this, arguments); 510 | return hasOwnProperty.call(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); 511 | }; 512 | }; 513 | 514 | // Delays a function for the given number of milliseconds, and then calls 515 | // it with the arguments supplied. 516 | _.delay = function(func, wait) { 517 | var args = slice.call(arguments, 2); 518 | return setTimeout(function(){ return func.apply(func, args); }, wait); 519 | }; 520 | 521 | // Defers a function, scheduling it to run after the current call stack has 522 | // cleared. 523 | _.defer = function(func) { 524 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); 525 | }; 526 | 527 | // Returns a function, that, when invoked, will only be triggered at most once 528 | // during a given window of time. 529 | _.throttle = function(func, wait) { 530 | var context, args, timeout, throttling, more; 531 | var whenDone = _.debounce(function(){ more = throttling = false; }, wait); 532 | return function() { 533 | context = this; args = arguments; 534 | var later = function() { 535 | timeout = null; 536 | if (more) func.apply(context, args); 537 | whenDone(); 538 | }; 539 | if (!timeout) timeout = setTimeout(later, wait); 540 | if (throttling) { 541 | more = true; 542 | } else { 543 | func.apply(context, args); 544 | } 545 | whenDone(); 546 | throttling = true; 547 | }; 548 | }; 549 | 550 | // Returns a function, that, as long as it continues to be invoked, will not 551 | // be triggered. The function will be called after it stops being called for 552 | // N milliseconds. 553 | _.debounce = function(func, wait) { 554 | var timeout; 555 | return function() { 556 | var context = this, args = arguments; 557 | var later = function() { 558 | timeout = null; 559 | func.apply(context, args); 560 | }; 561 | clearTimeout(timeout); 562 | timeout = setTimeout(later, wait); 563 | }; 564 | }; 565 | 566 | // Returns a function that will be executed at most one time, no matter how 567 | // often you call it. Useful for lazy initialization. 568 | _.once = function(func) { 569 | var ran = false, memo; 570 | return function() { 571 | if (ran) return memo; 572 | ran = true; 573 | return memo = func.apply(this, arguments); 574 | }; 575 | }; 576 | 577 | // Returns the first function passed as an argument to the second, 578 | // allowing you to adjust arguments, run code before and after, and 579 | // conditionally execute the original function. 580 | _.wrap = function(func, wrapper) { 581 | return function() { 582 | var args = [func].concat(slice.call(arguments)); 583 | return wrapper.apply(this, args); 584 | }; 585 | }; 586 | 587 | // Returns a function that is the composition of a list of functions, each 588 | // consuming the return value of the function that follows. 589 | _.compose = function() { 590 | var funcs = slice.call(arguments); 591 | return function() { 592 | var args = slice.call(arguments); 593 | for (var i = funcs.length - 1; i >= 0; i--) { 594 | args = [funcs[i].apply(this, args)]; 595 | } 596 | return args[0]; 597 | }; 598 | }; 599 | 600 | // Returns a function that will only be executed after being called N times. 601 | _.after = function(times, func) { 602 | if (times <= 0) return func(); 603 | return function() { 604 | if (--times < 1) { return func.apply(this, arguments); } 605 | }; 606 | }; 607 | 608 | // Object Functions 609 | // ---------------- 610 | 611 | // Retrieve the names of an object's properties. 612 | // Delegates to **ECMAScript 5**'s native `Object.keys` 613 | _.keys = nativeKeys || function(obj) { 614 | if (obj !== Object(obj)) throw new TypeError('Invalid object'); 615 | var keys = []; 616 | for (var key in obj) if (hasOwnProperty.call(obj, key)) keys[keys.length] = key; 617 | return keys; 618 | }; 619 | 620 | // Retrieve the values of an object's properties. 621 | _.values = function(obj) { 622 | return _.map(obj, _.identity); 623 | }; 624 | 625 | // Return a sorted list of the function names available on the object. 626 | // Aliased as `methods` 627 | _.functions = _.methods = function(obj) { 628 | var names = []; 629 | for (var key in obj) { 630 | if (_.isFunction(obj[key])) names.push(key); 631 | } 632 | return names.sort(); 633 | }; 634 | 635 | // Extend a given object with all the properties in passed-in object(s). 636 | _.extend = function(obj) { 637 | each(slice.call(arguments, 1), function(source) { 638 | for (var prop in source) { 639 | if (source[prop] !== void 0) obj[prop] = source[prop]; 640 | } 641 | }); 642 | return obj; 643 | }; 644 | 645 | // Fill in a given object with default properties. 646 | _.defaults = function(obj) { 647 | each(slice.call(arguments, 1), function(source) { 648 | for (var prop in source) { 649 | if (obj[prop] == null) obj[prop] = source[prop]; 650 | } 651 | }); 652 | return obj; 653 | }; 654 | 655 | // Create a (shallow-cloned) duplicate of an object. 656 | _.clone = function(obj) { 657 | if (!_.isObject(obj)) return obj; 658 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 659 | }; 660 | 661 | // Invokes interceptor with the obj, and then returns obj. 662 | // The primary purpose of this method is to "tap into" a method chain, in 663 | // order to perform operations on intermediate results within the chain. 664 | _.tap = function(obj, interceptor) { 665 | interceptor(obj); 666 | return obj; 667 | }; 668 | 669 | // Internal recursive comparison function. 670 | function eq(a, b, stack) { 671 | // Identical objects are equal. `0 === -0`, but they aren't identical. 672 | // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. 673 | if (a === b) return a !== 0 || 1 / a == 1 / b; 674 | // A strict comparison is necessary because `null == undefined`. 675 | if (a == null || b == null) return a === b; 676 | // Unwrap any wrapped objects. 677 | if (a._chain) a = a._wrapped; 678 | if (b._chain) b = b._wrapped; 679 | // Invoke a custom `isEqual` method if one is provided. 680 | if (_.isFunction(a.isEqual)) return a.isEqual(b); 681 | if (_.isFunction(b.isEqual)) return b.isEqual(a); 682 | // Compare `[[Class]]` names. 683 | var className = toString.call(a); 684 | if (className != toString.call(b)) return false; 685 | switch (className) { 686 | // Strings, numbers, dates, and booleans are compared by value. 687 | case '[object String]': 688 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 689 | // equivalent to `new String("5")`. 690 | return String(a) == String(b); 691 | case '[object Number]': 692 | a = +a; 693 | b = +b; 694 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for 695 | // other numeric values. 696 | return a != a ? b != b : (a == 0 ? 1 / a == 1 / b : a == b); 697 | case '[object Date]': 698 | case '[object Boolean]': 699 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their 700 | // millisecond representations. Note that invalid dates with millisecond representations 701 | // of `NaN` are not equivalent. 702 | return +a == +b; 703 | // RegExps are compared by their source patterns and flags. 704 | case '[object RegExp]': 705 | return a.source == b.source && 706 | a.global == b.global && 707 | a.multiline == b.multiline && 708 | a.ignoreCase == b.ignoreCase; 709 | } 710 | if (typeof a != 'object' || typeof b != 'object') return false; 711 | // Assume equality for cyclic structures. The algorithm for detecting cyclic 712 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 713 | var length = stack.length; 714 | while (length--) { 715 | // Linear search. Performance is inversely proportional to the number of 716 | // unique nested structures. 717 | if (stack[length] == a) return true; 718 | } 719 | // Add the first object to the stack of traversed objects. 720 | stack.push(a); 721 | var size = 0, result = true; 722 | // Recursively compare objects and arrays. 723 | if (className == '[object Array]') { 724 | // Compare array lengths to determine if a deep comparison is necessary. 725 | size = a.length; 726 | result = size == b.length; 727 | if (result) { 728 | // Deep compare the contents, ignoring non-numeric properties. 729 | while (size--) { 730 | // Ensure commutative equality for sparse arrays. 731 | if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break; 732 | } 733 | } 734 | } else { 735 | // Objects with different constructors are not equivalent. 736 | if ("constructor" in a != "constructor" in b || a.constructor != b.constructor) return false; 737 | // Deep compare objects. 738 | for (var key in a) { 739 | if (hasOwnProperty.call(a, key)) { 740 | // Count the expected number of properties. 741 | size++; 742 | // Deep compare each member. 743 | if (!(result = hasOwnProperty.call(b, key) && eq(a[key], b[key], stack))) break; 744 | } 745 | } 746 | // Ensure that both objects contain the same number of properties. 747 | if (result) { 748 | for (key in b) { 749 | if (hasOwnProperty.call(b, key) && !(size--)) break; 750 | } 751 | result = !size; 752 | } 753 | } 754 | // Remove the first object from the stack of traversed objects. 755 | stack.pop(); 756 | return result; 757 | } 758 | 759 | // Perform a deep comparison to check if two objects are equal. 760 | _.isEqual = function(a, b) { 761 | return eq(a, b, []); 762 | }; 763 | 764 | // Is a given array, string, or object empty? 765 | // An "empty" object has no enumerable own-properties. 766 | _.isEmpty = function(obj) { 767 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 768 | for (var key in obj) if (hasOwnProperty.call(obj, key)) return false; 769 | return true; 770 | }; 771 | 772 | // Is a given value a DOM element? 773 | _.isElement = function(obj) { 774 | return !!(obj && obj.nodeType == 1); 775 | }; 776 | 777 | // Is a given value an array? 778 | // Delegates to ECMA5's native Array.isArray 779 | _.isArray = nativeIsArray || function(obj) { 780 | return toString.call(obj) == '[object Array]'; 781 | }; 782 | 783 | // Is a given variable an object? 784 | _.isObject = function(obj) { 785 | return obj === Object(obj); 786 | }; 787 | 788 | // Is a given variable an arguments object? 789 | if (toString.call(arguments) == '[object Arguments]') { 790 | _.isArguments = function(obj) { 791 | return toString.call(obj) == '[object Arguments]'; 792 | }; 793 | } else { 794 | _.isArguments = function(obj) { 795 | return !!(obj && hasOwnProperty.call(obj, 'callee')); 796 | }; 797 | } 798 | 799 | // Is a given value a function? 800 | _.isFunction = function(obj) { 801 | return toString.call(obj) == '[object Function]'; 802 | }; 803 | 804 | // Is a given value a string? 805 | _.isString = function(obj) { 806 | return toString.call(obj) == '[object String]'; 807 | }; 808 | 809 | // Is a given value a number? 810 | _.isNumber = function(obj) { 811 | return toString.call(obj) == '[object Number]'; 812 | }; 813 | 814 | // Is the given value `NaN`? 815 | _.isNaN = function(obj) { 816 | // `NaN` is the only value for which `===` is not reflexive. 817 | return obj !== obj; 818 | }; 819 | 820 | // Is a given value a boolean? 821 | _.isBoolean = function(obj) { 822 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; 823 | }; 824 | 825 | // Is a given value a date? 826 | _.isDate = function(obj) { 827 | return toString.call(obj) == '[object Date]'; 828 | }; 829 | 830 | // Is the given value a regular expression? 831 | _.isRegExp = function(obj) { 832 | return toString.call(obj) == '[object RegExp]'; 833 | }; 834 | 835 | // Is a given value equal to null? 836 | _.isNull = function(obj) { 837 | return obj === null; 838 | }; 839 | 840 | // Is a given variable undefined? 841 | _.isUndefined = function(obj) { 842 | return obj === void 0; 843 | }; 844 | 845 | // Utility Functions 846 | // ----------------- 847 | 848 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 849 | // previous owner. Returns a reference to the Underscore object. 850 | _.noConflict = function() { 851 | root._ = previousUnderscore; 852 | return this; 853 | }; 854 | 855 | // Keep the identity function around for default iterators. 856 | _.identity = function(value) { 857 | return value; 858 | }; 859 | 860 | // Run a function **n** times. 861 | _.times = function (n, iterator, context) { 862 | for (var i = 0; i < n; i++) iterator.call(context, i); 863 | }; 864 | 865 | // Escape a string for HTML interpolation. 866 | _.escape = function(string) { 867 | return (''+string).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/'); 868 | }; 869 | 870 | // Add your own custom functions to the Underscore object, ensuring that 871 | // they're correctly added to the OOP wrapper as well. 872 | _.mixin = function(obj) { 873 | each(_.functions(obj), function(name){ 874 | addToWrapper(name, _[name] = obj[name]); 875 | }); 876 | }; 877 | 878 | // Generate a unique integer id (unique within the entire client session). 879 | // Useful for temporary DOM ids. 880 | var idCounter = 0; 881 | _.uniqueId = function(prefix) { 882 | var id = idCounter++; 883 | return prefix ? prefix + id : id; 884 | }; 885 | 886 | // By default, Underscore uses ERB-style template delimiters, change the 887 | // following template settings to use alternative delimiters. 888 | _.templateSettings = { 889 | evaluate : /<%([\s\S]+?)%>/g, 890 | interpolate : /<%=([\s\S]+?)%>/g, 891 | escape : /<%-([\s\S]+?)%>/g 892 | }; 893 | 894 | // JavaScript micro-templating, similar to John Resig's implementation. 895 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 896 | // and correctly escapes quotes within interpolated code. 897 | _.template = function(str, data) { 898 | var c = _.templateSettings; 899 | var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' + 900 | 'with(obj||{}){__p.push(\'' + 901 | str.replace(/\\/g, '\\\\') 902 | .replace(/'/g, "\\'") 903 | .replace(c.escape, function(match, code) { 904 | return "',_.escape(" + code.replace(/\\'/g, "'") + "),'"; 905 | }) 906 | .replace(c.interpolate, function(match, code) { 907 | return "'," + code.replace(/\\'/g, "'") + ",'"; 908 | }) 909 | .replace(c.evaluate || null, function(match, code) { 910 | return "');" + code.replace(/\\'/g, "'") 911 | .replace(/[\r\n\t]/g, ' ') + ";__p.push('"; 912 | }) 913 | .replace(/\r/g, '\\r') 914 | .replace(/\n/g, '\\n') 915 | .replace(/\t/g, '\\t') 916 | + "');}return __p.join('');"; 917 | var func = new Function('obj', '_', tmpl); 918 | return data ? func(data, _) : function(data) { return func(data, _) }; 919 | }; 920 | 921 | // The OOP Wrapper 922 | // --------------- 923 | 924 | // If Underscore is called as a function, it returns a wrapped object that 925 | // can be used OO-style. This wrapper holds altered versions of all the 926 | // underscore functions. Wrapped objects may be chained. 927 | var wrapper = function(obj) { this._wrapped = obj; }; 928 | 929 | // Expose `wrapper.prototype` as `_.prototype` 930 | _.prototype = wrapper.prototype; 931 | 932 | // Helper function to continue chaining intermediate results. 933 | var result = function(obj, chain) { 934 | return chain ? _(obj).chain() : obj; 935 | }; 936 | 937 | // A method to easily add functions to the OOP wrapper. 938 | var addToWrapper = function(name, func) { 939 | wrapper.prototype[name] = function() { 940 | var args = slice.call(arguments); 941 | unshift.call(args, this._wrapped); 942 | return result(func.apply(_, args), this._chain); 943 | }; 944 | }; 945 | 946 | // Add all of the Underscore functions to the wrapper object. 947 | _.mixin(_); 948 | 949 | // Add all mutator Array functions to the wrapper. 950 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 951 | var method = ArrayProto[name]; 952 | wrapper.prototype[name] = function() { 953 | method.apply(this._wrapped, arguments); 954 | return result(this._wrapped, this._chain); 955 | }; 956 | }); 957 | 958 | // Add all accessor Array functions to the wrapper. 959 | each(['concat', 'join', 'slice'], function(name) { 960 | var method = ArrayProto[name]; 961 | wrapper.prototype[name] = function() { 962 | return result(method.apply(this._wrapped, arguments), this._chain); 963 | }; 964 | }); 965 | 966 | // Start chaining a wrapped Underscore object. 967 | wrapper.prototype.chain = function() { 968 | this._chain = true; 969 | return this; 970 | }; 971 | 972 | // Extracts the result from a wrapped and chained object. 973 | wrapper.prototype.value = function() { 974 | return this._wrapped; 975 | }; 976 | 977 | }).call(this); 978 | -------------------------------------------------------------------------------- /website/css/bootstrap-custom.css: -------------------------------------------------------------------------------- 1 | /* Tweak body padding to handle the static top bar */ 2 | body { 3 | padding-top: 60px; 4 | } 5 | 6 | /* Tweak navbar brand link to be super sleek (from the Bootstrap docs) */ 7 | .navbar-fixed-top .brand { 8 | padding-right: 0; 9 | padding-left: 0; 10 | margin-left: 20px; 11 | float: right; 12 | font-weight: bold; 13 | color: #000; 14 | text-shadow: 0 1px 0 rgba(255,255,255,.1), 0 0 30px rgba(255,255,255,.125); 15 | -webkit-transition: all .2s linear; 16 | -moz-transition: all .2s linear; 17 | transition: all .2s linear; 18 | } 19 | .navbar-fixed-top .brand:hover { 20 | text-decoration: none; 21 | } 22 | 23 | /* Tweak GitHub 'Fork Me' to be on top */ 24 | a.navbar-fixed-top, a.navbar-fixed-top img { 25 | border: 0; 26 | position: fixed; 27 | top: 0; 28 | right: 0; 29 | z-index: 1031; 30 | } 31 | -------------------------------------------------------------------------------- /website/css/bootstrap-responsive.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.0.0 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | .hidden { 11 | display: none; 12 | visibility: hidden; 13 | } 14 | @media (max-width: 480px) { 15 | .nav-collapse { 16 | -webkit-transform: translate3d(0, 0, 0); 17 | } 18 | .page-header h1 small { 19 | display: block; 20 | line-height: 18px; 21 | } 22 | input[class*="span"], 23 | select[class*="span"], 24 | textarea[class*="span"], 25 | .uneditable-input { 26 | display: block; 27 | width: 100%; 28 | height: 28px; 29 | /* Make inputs at least the height of their button counterpart */ 30 | 31 | /* Makes inputs behave like true block-level elements */ 32 | 33 | -webkit-box-sizing: border-box; 34 | /* Older Webkit */ 35 | 36 | -moz-box-sizing: border-box; 37 | /* Older FF */ 38 | 39 | -ms-box-sizing: border-box; 40 | /* IE8 */ 41 | 42 | box-sizing: border-box; 43 | /* CSS3 spec*/ 44 | 45 | } 46 | .input-prepend input[class*="span"], .input-append input[class*="span"] { 47 | width: auto; 48 | } 49 | input[type="checkbox"], input[type="radio"] { 50 | border: 1px solid #ccc; 51 | } 52 | .form-horizontal .control-group > label { 53 | float: none; 54 | width: auto; 55 | padding-top: 0; 56 | text-align: left; 57 | } 58 | .form-horizontal .controls { 59 | margin-left: 0; 60 | } 61 | .form-horizontal .control-list { 62 | padding-top: 0; 63 | } 64 | .form-horizontal .form-actions { 65 | padding-left: 10px; 66 | padding-right: 10px; 67 | } 68 | .modal { 69 | position: absolute; 70 | top: 10px; 71 | left: 10px; 72 | right: 10px; 73 | width: auto; 74 | margin: 0; 75 | } 76 | .modal.fade.in { 77 | top: auto; 78 | } 79 | .modal-header .close { 80 | padding: 10px; 81 | margin: -10px; 82 | } 83 | .carousel-caption { 84 | position: static; 85 | } 86 | } 87 | @media (max-width: 768px) { 88 | .container { 89 | width: auto; 90 | padding: 0 20px; 91 | } 92 | .row-fluid { 93 | width: 100%; 94 | } 95 | .row { 96 | margin-left: 0; 97 | } 98 | .row > [class*="span"], .row-fluid > [class*="span"] { 99 | float: none; 100 | display: block; 101 | width: auto; 102 | margin: 0; 103 | } 104 | } 105 | @media (min-width: 768px) and (max-width: 980px) { 106 | .row { 107 | margin-left: -20px; 108 | *zoom: 1; 109 | } 110 | .row:before, .row:after { 111 | display: table; 112 | content: ""; 113 | } 114 | .row:after { 115 | clear: both; 116 | } 117 | [class*="span"] { 118 | float: left; 119 | margin-left: 20px; 120 | } 121 | .span1 { 122 | width: 42px; 123 | } 124 | .span2 { 125 | width: 104px; 126 | } 127 | .span3 { 128 | width: 166px; 129 | } 130 | .span4 { 131 | width: 228px; 132 | } 133 | .span5 { 134 | width: 290px; 135 | } 136 | .span6 { 137 | width: 352px; 138 | } 139 | .span7 { 140 | width: 414px; 141 | } 142 | .span8 { 143 | width: 476px; 144 | } 145 | .span9 { 146 | width: 538px; 147 | } 148 | .span10 { 149 | width: 600px; 150 | } 151 | .span11 { 152 | width: 662px; 153 | } 154 | .span12, .container { 155 | width: 724px; 156 | } 157 | .offset1 { 158 | margin-left: 82px; 159 | } 160 | .offset2 { 161 | margin-left: 144px; 162 | } 163 | .offset3 { 164 | margin-left: 206px; 165 | } 166 | .offset4 { 167 | margin-left: 268px; 168 | } 169 | .offset5 { 170 | margin-left: 330px; 171 | } 172 | .offset6 { 173 | margin-left: 392px; 174 | } 175 | .offset7 { 176 | margin-left: 454px; 177 | } 178 | .offset8 { 179 | margin-left: 516px; 180 | } 181 | .offset9 { 182 | margin-left: 578px; 183 | } 184 | .offset10 { 185 | margin-left: 640px; 186 | } 187 | .offset11 { 188 | margin-left: 702px; 189 | } 190 | .row-fluid { 191 | width: 100%; 192 | *zoom: 1; 193 | } 194 | .row-fluid:before, .row-fluid:after { 195 | display: table; 196 | content: ""; 197 | } 198 | .row-fluid:after { 199 | clear: both; 200 | } 201 | .row-fluid > [class*="span"] { 202 | float: left; 203 | margin-left: 2.762430939%; 204 | } 205 | .row-fluid > [class*="span"]:first-child { 206 | margin-left: 0; 207 | } 208 | .row-fluid .span1 { 209 | width: 5.801104972%; 210 | } 211 | .row-fluid .span2 { 212 | width: 14.364640883%; 213 | } 214 | .row-fluid .span3 { 215 | width: 22.928176794%; 216 | } 217 | .row-fluid .span4 { 218 | width: 31.491712705%; 219 | } 220 | .row-fluid .span5 { 221 | width: 40.055248616%; 222 | } 223 | .row-fluid .span6 { 224 | width: 48.618784527%; 225 | } 226 | .row-fluid .span7 { 227 | width: 57.182320438000005%; 228 | } 229 | .row-fluid .span8 { 230 | width: 65.74585634900001%; 231 | } 232 | .row-fluid .span9 { 233 | width: 74.30939226%; 234 | } 235 | .row-fluid .span10 { 236 | width: 82.87292817100001%; 237 | } 238 | .row-fluid .span11 { 239 | width: 91.436464082%; 240 | } 241 | .row-fluid .span12 { 242 | width: 99.999999993%; 243 | } 244 | input.span1, textarea.span1, .uneditable-input.span1 { 245 | width: 32px; 246 | } 247 | input.span2, textarea.span2, .uneditable-input.span2 { 248 | width: 94px; 249 | } 250 | input.span3, textarea.span3, .uneditable-input.span3 { 251 | width: 156px; 252 | } 253 | input.span4, textarea.span4, .uneditable-input.span4 { 254 | width: 218px; 255 | } 256 | input.span5, textarea.span5, .uneditable-input.span5 { 257 | width: 280px; 258 | } 259 | input.span6, textarea.span6, .uneditable-input.span6 { 260 | width: 342px; 261 | } 262 | input.span7, textarea.span7, .uneditable-input.span7 { 263 | width: 404px; 264 | } 265 | input.span8, textarea.span8, .uneditable-input.span8 { 266 | width: 466px; 267 | } 268 | input.span9, textarea.span9, .uneditable-input.span9 { 269 | width: 528px; 270 | } 271 | input.span10, textarea.span10, .uneditable-input.span10 { 272 | width: 590px; 273 | } 274 | input.span11, textarea.span11, .uneditable-input.span11 { 275 | width: 652px; 276 | } 277 | input.span12, textarea.span12, .uneditable-input.span12 { 278 | width: 714px; 279 | } 280 | } 281 | @media (max-width: 980px) { 282 | body { 283 | padding-top: 0; 284 | } 285 | .navbar-fixed-top { 286 | position: static; 287 | margin-bottom: 18px; 288 | } 289 | .navbar-fixed-top .navbar-inner { 290 | padding: 5px; 291 | } 292 | .navbar .container { 293 | width: auto; 294 | padding: 0; 295 | } 296 | .navbar .brand { 297 | padding-left: 10px; 298 | padding-right: 10px; 299 | margin: 0 0 0 -5px; 300 | } 301 | .navbar .nav-collapse { 302 | clear: left; 303 | } 304 | .navbar .nav { 305 | float: none; 306 | margin: 0 0 9px; 307 | } 308 | .navbar .nav > li { 309 | float: none; 310 | } 311 | .navbar .nav > li > a { 312 | margin-bottom: 2px; 313 | } 314 | .navbar .nav > .divider-vertical { 315 | display: none; 316 | } 317 | .navbar .nav > li > a, .navbar .dropdown-menu a { 318 | padding: 6px 15px; 319 | font-weight: bold; 320 | color: #999999; 321 | -webkit-border-radius: 3px; 322 | -moz-border-radius: 3px; 323 | border-radius: 3px; 324 | } 325 | .navbar .dropdown-menu li + li a { 326 | margin-bottom: 2px; 327 | } 328 | .navbar .nav > li > a:hover, .navbar .dropdown-menu a:hover { 329 | background-color: #222222; 330 | } 331 | .navbar .dropdown-menu { 332 | position: static; 333 | top: auto; 334 | left: auto; 335 | float: none; 336 | display: block; 337 | max-width: none; 338 | margin: 0 15px; 339 | padding: 0; 340 | background-color: transparent; 341 | border: none; 342 | -webkit-border-radius: 0; 343 | -moz-border-radius: 0; 344 | border-radius: 0; 345 | -webkit-box-shadow: none; 346 | -moz-box-shadow: none; 347 | box-shadow: none; 348 | } 349 | .navbar .dropdown-menu:before, .navbar .dropdown-menu:after { 350 | display: none; 351 | } 352 | .navbar .dropdown-menu .divider { 353 | display: none; 354 | } 355 | .navbar-form, .navbar-search { 356 | float: none; 357 | padding: 9px 15px; 358 | margin: 9px 0; 359 | border-top: 1px solid #222222; 360 | border-bottom: 1px solid #222222; 361 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 362 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 363 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 364 | } 365 | .navbar .nav.pull-right { 366 | float: none; 367 | margin-left: 0; 368 | } 369 | .navbar-static .navbar-inner { 370 | padding-left: 10px; 371 | padding-right: 10px; 372 | } 373 | .btn-navbar { 374 | display: block; 375 | } 376 | .nav-collapse { 377 | overflow: hidden; 378 | height: 0; 379 | } 380 | } 381 | @media (min-width: 980px) { 382 | .nav-collapse.collapse { 383 | height: auto !important; 384 | } 385 | } 386 | @media (min-width: 1200px) { 387 | .row { 388 | margin-left: -30px; 389 | *zoom: 1; 390 | } 391 | .row:before, .row:after { 392 | display: table; 393 | content: ""; 394 | } 395 | .row:after { 396 | clear: both; 397 | } 398 | [class*="span"] { 399 | float: left; 400 | margin-left: 30px; 401 | } 402 | .span1 { 403 | width: 70px; 404 | } 405 | .span2 { 406 | width: 170px; 407 | } 408 | .span3 { 409 | width: 270px; 410 | } 411 | .span4 { 412 | width: 370px; 413 | } 414 | .span5 { 415 | width: 470px; 416 | } 417 | .span6 { 418 | width: 570px; 419 | } 420 | .span7 { 421 | width: 670px; 422 | } 423 | .span8 { 424 | width: 770px; 425 | } 426 | .span9 { 427 | width: 870px; 428 | } 429 | .span10 { 430 | width: 970px; 431 | } 432 | .span11 { 433 | width: 1070px; 434 | } 435 | .span12, .container { 436 | width: 1170px; 437 | } 438 | .offset1 { 439 | margin-left: 130px; 440 | } 441 | .offset2 { 442 | margin-left: 230px; 443 | } 444 | .offset3 { 445 | margin-left: 330px; 446 | } 447 | .offset4 { 448 | margin-left: 430px; 449 | } 450 | .offset5 { 451 | margin-left: 530px; 452 | } 453 | .offset6 { 454 | margin-left: 630px; 455 | } 456 | .offset7 { 457 | margin-left: 730px; 458 | } 459 | .offset8 { 460 | margin-left: 830px; 461 | } 462 | .offset9 { 463 | margin-left: 930px; 464 | } 465 | .offset10 { 466 | margin-left: 1030px; 467 | } 468 | .offset11 { 469 | margin-left: 1130px; 470 | } 471 | .row-fluid { 472 | width: 100%; 473 | *zoom: 1; 474 | } 475 | .row-fluid:before, .row-fluid:after { 476 | display: table; 477 | content: ""; 478 | } 479 | .row-fluid:after { 480 | clear: both; 481 | } 482 | .row-fluid > [class*="span"] { 483 | float: left; 484 | margin-left: 2.564102564%; 485 | } 486 | .row-fluid > [class*="span"]:first-child { 487 | margin-left: 0; 488 | } 489 | .row-fluid .span1 { 490 | width: 5.982905983%; 491 | } 492 | .row-fluid .span2 { 493 | width: 14.529914530000001%; 494 | } 495 | .row-fluid .span3 { 496 | width: 23.076923077%; 497 | } 498 | .row-fluid .span4 { 499 | width: 31.623931624%; 500 | } 501 | .row-fluid .span5 { 502 | width: 40.170940171000005%; 503 | } 504 | .row-fluid .span6 { 505 | width: 48.717948718%; 506 | } 507 | .row-fluid .span7 { 508 | width: 57.264957265%; 509 | } 510 | .row-fluid .span8 { 511 | width: 65.81196581200001%; 512 | } 513 | .row-fluid .span9 { 514 | width: 74.358974359%; 515 | } 516 | .row-fluid .span10 { 517 | width: 82.905982906%; 518 | } 519 | .row-fluid .span11 { 520 | width: 91.45299145300001%; 521 | } 522 | .row-fluid .span12 { 523 | width: 100%; 524 | } 525 | input.span1, textarea.span1, .uneditable-input.span1 { 526 | width: 60px; 527 | } 528 | input.span2, textarea.span2, .uneditable-input.span2 { 529 | width: 160px; 530 | } 531 | input.span3, textarea.span3, .uneditable-input.span3 { 532 | width: 260px; 533 | } 534 | input.span4, textarea.span4, .uneditable-input.span4 { 535 | width: 360px; 536 | } 537 | input.span5, textarea.span5, .uneditable-input.span5 { 538 | width: 460px; 539 | } 540 | input.span6, textarea.span6, .uneditable-input.span6 { 541 | width: 560px; 542 | } 543 | input.span7, textarea.span7, .uneditable-input.span7 { 544 | width: 660px; 545 | } 546 | input.span8, textarea.span8, .uneditable-input.span8 { 547 | width: 760px; 548 | } 549 | input.span9, textarea.span9, .uneditable-input.span9 { 550 | width: 860px; 551 | } 552 | input.span10, textarea.span10, .uneditable-input.span10 { 553 | width: 960px; 554 | } 555 | input.span11, textarea.span11, .uneditable-input.span11 { 556 | width: 1060px; 557 | } 558 | input.span12, textarea.span12, .uneditable-input.span12 { 559 | width: 1160px; 560 | } 561 | .thumbnails { 562 | margin-left: -30px; 563 | } 564 | .thumbnails > li { 565 | margin-left: 30px; 566 | } 567 | } 568 | -------------------------------------------------------------------------------- /website/js/bootstrap-dropdown.js: -------------------------------------------------------------------------------- 1 | /* ============================================================ 2 | * bootstrap-dropdown.js v2.0.0 3 | * http://twitter.github.com/bootstrap/javascript.html#dropdowns 4 | * ============================================================ 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ============================================================ */ 19 | 20 | 21 | !function( $ ){ 22 | 23 | "use strict" 24 | 25 | /* DROPDOWN CLASS DEFINITION 26 | * ========================= */ 27 | 28 | var toggle = '[data-toggle="dropdown"]' 29 | , Dropdown = function ( element ) { 30 | var $el = $(element).on('click.dropdown.data-api', this.toggle) 31 | $('html').on('click.dropdown.data-api', function () { 32 | $el.parent().removeClass('open') 33 | }) 34 | } 35 | 36 | Dropdown.prototype = { 37 | 38 | constructor: Dropdown 39 | 40 | , toggle: function ( e ) { 41 | var $this = $(this) 42 | , selector = $this.attr('data-target') 43 | , $parent 44 | , isActive 45 | 46 | if (!selector) { 47 | selector = $this.attr('href') 48 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 49 | } 50 | 51 | $parent = $(selector) 52 | $parent.length || ($parent = $this.parent()) 53 | 54 | isActive = $parent.hasClass('open') 55 | 56 | clearMenus() 57 | !isActive && $parent.toggleClass('open') 58 | 59 | return false 60 | } 61 | 62 | } 63 | 64 | function clearMenus() { 65 | $(toggle).parent().removeClass('open') 66 | } 67 | 68 | 69 | /* DROPDOWN PLUGIN DEFINITION 70 | * ========================== */ 71 | 72 | $.fn.dropdown = function ( option ) { 73 | return this.each(function () { 74 | var $this = $(this) 75 | , data = $this.data('dropdown') 76 | if (!data) $this.data('dropdown', (data = new Dropdown(this))) 77 | if (typeof option == 'string') data[option].call($this) 78 | }) 79 | } 80 | 81 | $.fn.dropdown.Constructor = Dropdown 82 | 83 | 84 | /* APPLY TO STANDARD DROPDOWN ELEMENTS 85 | * =================================== */ 86 | 87 | $(function () { 88 | $('html').on('click.dropdown.data-api', clearMenus) 89 | $('body').on('click.dropdown.data-api', toggle, Dropdown.prototype.toggle) 90 | }) 91 | 92 | }( window.jQuery ) 93 | -------------------------------------------------------------------------------- /website/js/bootstrap-modal.js: -------------------------------------------------------------------------------- 1 | /* ========================================================= 2 | * bootstrap-modal.js v2.0.0 3 | * http://twitter.github.com/bootstrap/javascript.html#modals 4 | * ========================================================= 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================= */ 19 | 20 | 21 | !function( $ ){ 22 | 23 | "use strict" 24 | 25 | /* MODAL CLASS DEFINITION 26 | * ====================== */ 27 | 28 | var Modal = function ( content, options ) { 29 | this.options = $.extend({}, $.fn.modal.defaults, options) 30 | this.$element = $(content) 31 | .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) 32 | } 33 | 34 | Modal.prototype = { 35 | 36 | constructor: Modal 37 | 38 | , toggle: function () { 39 | return this[!this.isShown ? 'show' : 'hide']() 40 | } 41 | 42 | , show: function () { 43 | var that = this 44 | 45 | if (this.isShown) return 46 | 47 | $('body').addClass('modal-open') 48 | 49 | this.isShown = true 50 | this.$element.trigger('show') 51 | 52 | escape.call(this) 53 | backdrop.call(this, function () { 54 | var transition = $.support.transition && that.$element.hasClass('fade') 55 | 56 | !that.$element.parent().length && that.$element.appendTo(document.body) //don't move modals dom position 57 | 58 | that.$element 59 | .show() 60 | 61 | if (transition) { 62 | that.$element[0].offsetWidth // force reflow 63 | } 64 | 65 | that.$element.addClass('in') 66 | 67 | transition ? 68 | that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) : 69 | that.$element.trigger('shown') 70 | 71 | }) 72 | } 73 | 74 | , hide: function ( e ) { 75 | e && e.preventDefault() 76 | 77 | if (!this.isShown) return 78 | 79 | var that = this 80 | this.isShown = false 81 | 82 | $('body').removeClass('modal-open') 83 | 84 | escape.call(this) 85 | 86 | this.$element 87 | .trigger('hide') 88 | .removeClass('in') 89 | 90 | $.support.transition && this.$element.hasClass('fade') ? 91 | hideWithTransition.call(this) : 92 | hideModal.call(this) 93 | } 94 | 95 | } 96 | 97 | 98 | /* MODAL PRIVATE METHODS 99 | * ===================== */ 100 | 101 | function hideWithTransition() { 102 | var that = this 103 | , timeout = setTimeout(function () { 104 | that.$element.off($.support.transition.end) 105 | hideModal.call(that) 106 | }, 500) 107 | 108 | this.$element.one($.support.transition.end, function () { 109 | clearTimeout(timeout) 110 | hideModal.call(that) 111 | }) 112 | } 113 | 114 | function hideModal( that ) { 115 | this.$element 116 | .hide() 117 | .trigger('hidden') 118 | 119 | backdrop.call(this) 120 | } 121 | 122 | function backdrop( callback ) { 123 | var that = this 124 | , animate = this.$element.hasClass('fade') ? 'fade' : '' 125 | 126 | if (this.isShown && this.options.backdrop) { 127 | var doAnimate = $.support.transition && animate 128 | 129 | this.$backdrop = $('