├── _id ├── language ├── app ├── tests │ ├── views_test.js │ ├── templates_test.js │ ├── application_test.js │ └── controller_test.js ├── css │ └── main.css ├── templates │ ├── test_template.handlebars │ ├── messages.handlebars │ └── days.handlebars ├── static │ └── img │ │ ├── glyphicons-halflings.png │ │ └── glyphicons-halflings-white.png ├── lib │ ├── controller.js │ ├── main.js │ ├── templates.js │ ├── core.js │ └── couchdb_datasource.js ├── index.html ├── plugins │ └── loader.js └── vendor │ ├── moment.js │ └── jquery.couch.js ├── Guardfile ├── views ├── user │ ├── reduce.js │ └── map.js ├── messages │ ├── reduce.js │ └── map.js ├── messagesTime │ ├── reduce.js │ └── map.js ├── userMessages │ ├── reduce.js │ └── map.js ├── messagesDayOfWeek │ ├── reduce.js │ └── map.js ├── userTimeMessages │ ├── reduce.js │ └── map.js └── userDayOfWeekMessages │ ├── reduce.js │ └── map.js ├── couchapp.json ├── .gitignore ├── .travis.yml ├── .couchappignore ├── Gemfile ├── config.ru ├── Rakefile ├── LICENSE ├── tests ├── index.html └── qunit │ ├── run-qunit.js │ ├── qunit.css │ └── qunit.js ├── Gemfile.lock ├── README.md └── Assetfile /_id: -------------------------------------------------------------------------------- 1 | _design/viewer -------------------------------------------------------------------------------- /language: -------------------------------------------------------------------------------- 1 | javascript -------------------------------------------------------------------------------- /app/tests/views_test.js: -------------------------------------------------------------------------------- 1 | require('irc/core'); -------------------------------------------------------------------------------- /app/css/main.css: -------------------------------------------------------------------------------- 1 | /* TODO: Add your app CSS here */ 2 | -------------------------------------------------------------------------------- /app/templates/test_template.handlebars: -------------------------------------------------------------------------------- 1 | hello from handlebars -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :rake, :task => :test do 2 | watch(%r{^app/.+\.js$}) 3 | end 4 | -------------------------------------------------------------------------------- /views/user/reduce.js: -------------------------------------------------------------------------------- 1 | function(key, values, rereduce) { 2 | return sum(values); 3 | }; -------------------------------------------------------------------------------- /views/messages/reduce.js: -------------------------------------------------------------------------------- 1 | function(key, values, rereduce) { 2 | return sum(values); 3 | }; -------------------------------------------------------------------------------- /couchapp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "IRC log viewer", 3 | "description": "IRC log viewer" 4 | } -------------------------------------------------------------------------------- /views/messagesTime/reduce.js: -------------------------------------------------------------------------------- 1 | function(key, values, rereduce) { 2 | return sum(values); 3 | }; -------------------------------------------------------------------------------- /views/userMessages/reduce.js: -------------------------------------------------------------------------------- 1 | function(key, values, rereduce) { 2 | return sum(values); 3 | }; -------------------------------------------------------------------------------- /views/messagesDayOfWeek/reduce.js: -------------------------------------------------------------------------------- 1 | function(key, values, rereduce) { 2 | return sum(values); 3 | }; -------------------------------------------------------------------------------- /views/userTimeMessages/reduce.js: -------------------------------------------------------------------------------- 1 | function(key, values, rereduce) { 2 | return sum(values); 3 | }; -------------------------------------------------------------------------------- /views/userDayOfWeekMessages/reduce.js: -------------------------------------------------------------------------------- 1 | function(key, values, rereduce) { 2 | return sum(values); 3 | }; -------------------------------------------------------------------------------- /app/static/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangratz/irc-log-viewer/dev/app/static/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /views/user/map.js: -------------------------------------------------------------------------------- 1 | function(doc) { 2 | if (doc.user) { 3 | var user = doc.user; 4 | emit(user.name, 1); 5 | } 6 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS X 2 | .DS_Store 3 | 4 | # Couchapp 5 | .couchapprc 6 | 7 | # Rack 8 | tmp 9 | 10 | # Asset output 11 | _attachments -------------------------------------------------------------------------------- /app/static/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangratz/irc-log-viewer/dev/app/static/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /views/messagesDayOfWeek/map.js: -------------------------------------------------------------------------------- 1 | function(doc) { 2 | if (doc.date) { 3 | var d = new Date(doc.date), 4 | day = d.getUTCDay(); 5 | 6 | emit(day, 1); 7 | } 8 | }; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.3 3 | bundler_args: --without development 4 | before_script: 5 | - "export DISPLAY=:99.0" 6 | - "sh -e /etc/init.d/xvfb start" 7 | - "rake clean" 8 | script: "bundle exec rake test" -------------------------------------------------------------------------------- /.couchappignore: -------------------------------------------------------------------------------- 1 | [ 2 | "Assetfile", 3 | "Gemfile*", 4 | "Guardfile", 5 | "Rakefile", 6 | "LICENSE", 7 | "README", 8 | "config.ru", 9 | "app$", 10 | "app-tests.js", 11 | "tmp", 12 | "tests$" 13 | ] -------------------------------------------------------------------------------- /views/userDayOfWeekMessages/map.js: -------------------------------------------------------------------------------- 1 | function(doc) { 2 | if (doc.user) { 3 | var d = new Date(doc.date), 4 | day = d.getUTCDay(); 5 | 6 | var user = doc.user; 7 | 8 | emit([user.name, day], 1); 9 | } 10 | }; -------------------------------------------------------------------------------- /views/messagesTime/map.js: -------------------------------------------------------------------------------- 1 | function(doc) { 2 | if (doc.date) { 3 | var d = new Date(doc.date), 4 | h = d.getUTCHours(), 5 | m = d.getUTCMinutes(), 6 | s = d.getUTCSeconds(), 7 | ms = d.getUTCMilliseconds(); 8 | 9 | emit([h, m, s, ms], 1); 10 | } 11 | }; -------------------------------------------------------------------------------- /views/userTimeMessages/map.js: -------------------------------------------------------------------------------- 1 | function(doc) { 2 | if (doc.user) { 3 | var d = new Date(doc.date), 4 | h = d.getUTCHours(), 5 | m = d.getUTCMinutes(), 6 | s = d.getUTCSeconds(), 7 | ms = d.getUTCMilliseconds(); 8 | 9 | var user = doc.user; 10 | 11 | emit([user.name, h, m, s, ms], 1); 12 | } 13 | }; -------------------------------------------------------------------------------- /app/templates/messages.handlebars: -------------------------------------------------------------------------------- 1 | {{#if loading}} 2 | loading messages ... 3 | {{else}} 4 |

{{format date "YYYY-MM-DD"}}

5 | 12 | {{/if}} -------------------------------------------------------------------------------- /app/templates/days.handlebars: -------------------------------------------------------------------------------- 1 | {{#if loading}} 2 | loading days ... 3 | {{else}} 4 | 13 | {{/if}} -------------------------------------------------------------------------------- /views/messages/map.js: -------------------------------------------------------------------------------- 1 | function(doc) { 2 | if (doc.date && doc.user && doc.user.room) { 3 | var d = new Date(doc.date), 4 | Y = d.getUTCFullYear(), 5 | M = d.getUTCMonth(), 6 | D = d.getUTCDate(), 7 | h = d.getUTCHours(), 8 | m = d.getUTCMinutes(), 9 | s = d.getUTCSeconds(), 10 | ms = d.getUTCMilliseconds(); 11 | 12 | emit([Y, M, D, h, m, s, ms], 1); 13 | } 14 | }; -------------------------------------------------------------------------------- /views/userMessages/map.js: -------------------------------------------------------------------------------- 1 | function(doc) { 2 | if (doc.user) { 3 | var d = new Date(doc.date), 4 | Y = d.getUTCFullYear(), 5 | M = d.getUTCMonth(), 6 | D = d.getUTCDate(), 7 | h = d.getUTCHours(), 8 | m = d.getUTCMinutes(), 9 | s = d.getUTCSeconds(), 10 | ms = d.getUTCMilliseconds(); 11 | 12 | var user = doc.user; 13 | 14 | emit([user.name, Y, M, D, h, m, s, ms], 1); 15 | } 16 | }; -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'colored' 4 | 5 | gem 'guard' 6 | gem 'guard-rake' 7 | 8 | gem 'rack' 9 | gem 'rack-rewrite' 10 | gem 'rack-streaming-proxy' 11 | 12 | gem 'kicker' 13 | 14 | gem 'sass' 15 | gem 'compass' 16 | 17 | gem 'uglifier' 18 | gem 'yui-compressor' 19 | 20 | gem 'rake-pipeline', :git => 'https://github.com/livingsocial/rake-pipeline.git' 21 | gem 'rake-pipeline-web-filters', :git => 'https://github.com/wycats/rake-pipeline-web-filters.git' 22 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require 'rake-pipeline' 2 | require 'rake-pipeline/middleware' 3 | use Rake::Pipeline::Middleware, 'Assetfile' 4 | 5 | require 'rack/streaming_proxy' 6 | use Rack::StreamingProxy do |request| 7 | if request.path.start_with?('/irc') 8 | "http://127.0.0.1:5984#{request.path}?#{request.query_string}" 9 | end 10 | end 11 | 12 | require 'rack-rewrite' 13 | use Rack::Rewrite do 14 | rewrite %r{^(.*)\/$}, '$1/index.html' 15 | end 16 | 17 | run Rack::Directory.new('.') 18 | -------------------------------------------------------------------------------- /app/lib/controller.js: -------------------------------------------------------------------------------- 1 | IRC.set('MessagesController', Ember.ArrayProxy.extend({ 2 | content: [], 3 | loading: true, 4 | 5 | addMessage: function(msg) { 6 | var obj = Ember.Object.create({ 7 | id: msg.id, 8 | username: msg.user.name, 9 | text: msg.text, 10 | date: IRC.createDate(msg.date) 11 | }); 12 | this.pushObject(obj); 13 | }, 14 | 15 | clear: function() { 16 | this.set('content', []); 17 | } 18 | })); 19 | 20 | IRC.set('DaysController', Ember.ArrayProxy.extend({ 21 | content: [], 22 | loading: true, 23 | 24 | addDay: function(day) { 25 | this.pushObject(Ember.Object.create({ 26 | date: IRC.createDate(day.date), 27 | count: day.count 28 | })); 29 | }, 30 | 31 | clear: function() { 32 | this.set('content', []); 33 | } 34 | })); -------------------------------------------------------------------------------- /app/lib/main.js: -------------------------------------------------------------------------------- 1 | require('irc/core'); 2 | require('irc/controller'); 3 | require('irc/templates'); 4 | require('irc/couchdb_datasource'); 5 | 6 | IRC.daysController = IRC.DaysController.create(); 7 | IRC.messagesController = IRC.MessagesController.create(); 8 | 9 | IRC.dataSource = IRC.CouchDBDataSource.create({ 10 | messagesController: IRC.messagesController, 11 | daysController: IRC.daysController 12 | }); 13 | 14 | Ember.View.create({ 15 | templateName: 'messages'.tmpl(), 16 | messagesBinding: 'IRC.messagesController', 17 | loadingBinding: 'IRC.messagesController.loading', 18 | dateBinding: 'IRC.messagesController.date' 19 | }).appendTo('#messages'); 20 | 21 | Ember.View.create({ 22 | templateName: 'days'.tmpl(), 23 | daysBinding: 'IRC.daysController', 24 | loadingBinding: 'IRC.daysController.loading' 25 | }).appendTo('#days'); 26 | 27 | Ember.run(function() { 28 | IRC.dataSource.loadDay(IRC.createDate()); 29 | IRC.dataSource.loadDays(); 30 | }); -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | APPNAME = 'irc' 2 | 3 | require "bundler/setup" 4 | require 'rake-pipeline' 5 | require 'colored' 6 | 7 | desc "Build #{APPNAME}" 8 | task :build => :clean do 9 | Rake::Pipeline::Project.new('Assetfile').invoke 10 | end 11 | 12 | desc "Build #{APPNAME}" 13 | task :clean do 14 | Rake::Pipeline::Project.new('Assetfile').clean 15 | end 16 | 17 | 18 | desc "Run tests with PhantomJS" 19 | task :test => :build do 20 | unless system("which phantomjs > /dev/null 2>&1") 21 | abort "PhantomJS is not installed. Download from http://phantomjs.org/" 22 | end 23 | 24 | cmd = "phantomjs tests/qunit/run-qunit.js \"file://#{File.dirname(__FILE__)}/tests/index.html\"" 25 | 26 | # Run the tests 27 | puts "Running #{APPNAME} tests" 28 | success = system(cmd) 29 | 30 | if success 31 | puts "Tests Passed".green 32 | else 33 | puts "Tests Failed".red 34 | exit(1) 35 | end 36 | end 37 | 38 | desc "Automatically run tests (Mac OS X only)" 39 | task :autotest do 40 | system("kicker -e 'rake test' app") 41 | end 42 | 43 | task :default => :test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 Clemens Müller 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /app/lib/templates.js: -------------------------------------------------------------------------------- 1 | /*jshint sub: true */ 2 | String.prototype.tmpl = function() { 3 | if (!Ember.TEMPLATES[this]) { 4 | Ember.TEMPLATES[this] = require('irc/~templates/' + this); 5 | } 6 | return this; 7 | }; 8 | 9 | String.prototype.parseURL = function() { 10 | return this.replace(/[A-Za-z]+:\/\/[A-Za-z0-9-_]+\.[A-Za-z0-9-_:%&#~\?\/.=]+/g, 11 | function(url) { 12 | return url.link(url); 13 | }); 14 | }; 15 | 16 | Handlebars.registerHelper('format', 17 | function(property, format) { 18 | var dateFormat; 19 | if (Ember.typeOf(format) === 'string') { 20 | dateFormat = format; 21 | } 22 | var value = Ember.getPath(this, property); 23 | if (value) { 24 | value = moment(value).utc().format(dateFormat); 25 | } else { 26 | value = undefined; 27 | } 28 | return new Handlebars.SafeString(value); 29 | }); 30 | 31 | Handlebars.registerHelper('parse', 32 | function(property) { 33 | var value = Ember.getPath(this, property); 34 | value = Handlebars.Utils.escapeExpression(value); 35 | value = value.parseURL(); 36 | return new Handlebars.SafeString(value); 37 | }); -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QUnit Test Suite 6 | 7 | 8 | 9 | 10 |
11 |
test markup
12 | 13 | 14 | 15 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | irc 5 | 6 | 19 | 20 | 21 | Fork me on GitHub 22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 | 30 | 31 | 34 | 35 | -------------------------------------------------------------------------------- /app/lib/core.js: -------------------------------------------------------------------------------- 1 | require('jquery'); 2 | require('ember'); 3 | require('moment'); 4 | require('jquery.couch'); 5 | 6 | IRC = Ember.Application.create({ 7 | VERSION: '0.0.1-snapshot', 8 | 9 | dateStrDict: { 10 | year: 'year', 11 | month: 'month', 12 | day: 'date', 13 | hour: 'hours', 14 | minute: 'minutes', 15 | second: 'seconds', 16 | millisecond: 'milliseconds' 17 | }, 18 | 19 | createDate: function(date) { 20 | var src = date; 21 | if (Ember.typeOf(date) === 'array') { 22 | src = Date.UTC.apply({}, date); 23 | } 24 | return moment(src).utc().toDate(); 25 | }, 26 | 27 | getNextDay: function(day) { 28 | return moment(day).clone().add('days', 1).toDate(); 29 | }, 30 | 31 | getDateArray: function() { 32 | var date = moment(arguments[0]).utc(); 33 | var a = []; 34 | for (var i = 1; i < arguments.length; i++) { 35 | var dateStr = IRC.dateStrDict[arguments[i]]; 36 | if (!dateStr) { 37 | a.push(null); 38 | } else { 39 | var value = date[dateStr](); 40 | a.push(value); 41 | } 42 | } 43 | return a; 44 | }, 45 | 46 | loadDate: function(event) { 47 | var day = event.context; 48 | var date = Ember.getPath(day, 'date'); 49 | var dataSource = Ember.getPath(this, 'dataSource'); 50 | if (dataSource) { 51 | dataSource.loadDay(date); 52 | } 53 | } 54 | 55 | }); -------------------------------------------------------------------------------- /app/plugins/loader.js: -------------------------------------------------------------------------------- 1 | (function(window) { 2 | function requireWrapper(self) { 3 | var require = function() { 4 | return self.require.apply(self, arguments); 5 | }; 6 | require.exists = function() { 7 | return self.exists.apply(self, arguments); 8 | }; 9 | return require; 10 | } 11 | 12 | var Context = function() { 13 | return this; 14 | }; 15 | 16 | var Loader = function() { 17 | this.modules = {}; 18 | this.loaded = {}; 19 | this.exports = {}; 20 | return this; 21 | }; 22 | 23 | Loader.prototype.require = function(name) { 24 | if (!this.loaded[name]) { 25 | var module = this.modules[name]; 26 | if (module) { 27 | var require = requireWrapper(this); 28 | try { 29 | this.exports[name] = module.call(new Context(), require); 30 | return this.exports[name]; 31 | } finally { 32 | this.loaded[name] = true; 33 | } 34 | } else { 35 | throw "The module '" + name + "' has not been registered"; 36 | } 37 | } 38 | return this.exports[name]; 39 | }; 40 | 41 | Loader.prototype.register = function(name, module) { 42 | if (this.exists(name)) { 43 | throw "The module '"+ "' has already been registered"; 44 | } 45 | this.modules[name] = module; 46 | return true; 47 | }; 48 | 49 | Loader.prototype.unregister = function(name) { 50 | var loaded = !!this.loaded[name]; 51 | if (loaded) { 52 | delete this.exports[name]; 53 | delete this.modules[name]; 54 | delete this.loaded[name]; 55 | } 56 | return loaded; 57 | }; 58 | 59 | Loader.prototype.exists = function(name) { 60 | return name in this.modules; 61 | }; 62 | 63 | window.loader = new Loader(); 64 | })(this); 65 | -------------------------------------------------------------------------------- /app/lib/couchdb_datasource.js: -------------------------------------------------------------------------------- 1 | require('irc/core'); 2 | 3 | IRC.CouchDBDataSource = Ember.Object.extend({ 4 | 5 | loadDay: function(day) { 6 | var messagesController = this.get('messagesController'); 7 | messagesController.clear(); 8 | messagesController.set('loading', true); 9 | var from = day || IRC.createDate(); 10 | var to = IRC.getNextDay(from); 11 | messagesController.set('date', from); 12 | $.couch.db('irc').view('viewer/messages', { 13 | success: function(data) { 14 | if (data && data.rows && data.rows.length > 0) { 15 | data.rows.forEach(function(row) { 16 | messagesController.addMessage(row.doc); 17 | }); 18 | } 19 | messagesController.set('loading', false); 20 | }, 21 | include_docs: true, 22 | reduce: false, 23 | startkey: IRC.getDateArray(from, 'year', 'month', 'day'), 24 | endkey: IRC.getDateArray(to, 'year', 'month', 'day') 25 | }); 26 | }, 27 | 28 | loadDays: function() { 29 | var daysController = this.get('daysController'); 30 | $.couch.db('irc').view('viewer/messages', { 31 | success: function(data) { 32 | if (data && data.rows && data.rows.length > 0) { 33 | data.rows.forEach(function(doc) { 34 | var key = doc.key; 35 | var date = IRC.createDate([key[0], key[1], key[2], 0]); 36 | daysController.addDay({ 37 | date: date, 38 | count: doc.value 39 | }); 40 | }); 41 | } 42 | daysController.set('loading', false); 43 | }, 44 | group_level: 3, 45 | descending: true 46 | }); 47 | } 48 | 49 | }); -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/livingsocial/rake-pipeline.git 3 | revision: 543f4322fe70facee9572d29ddabf7f090dad68a 4 | specs: 5 | rake-pipeline (0.6.0) 6 | rake (~> 0.9.0) 7 | thor 8 | 9 | GIT 10 | remote: https://github.com/wycats/rake-pipeline-web-filters.git 11 | revision: 81a22fb0808dfdeab8ed92d5d8c898ad198b9938 12 | specs: 13 | rake-pipeline-web-filters (0.6.0) 14 | rack 15 | rake-pipeline (~> 0.6) 16 | 17 | GEM 18 | remote: http://rubygems.org/ 19 | specs: 20 | POpen4 (0.1.4) 21 | Platform (>= 0.4.0) 22 | open4 23 | Platform (0.4.0) 24 | chunky_png (1.2.5) 25 | colored (1.2) 26 | compass (0.12.2) 27 | chunky_png (~> 1.2) 28 | fssm (>= 0.2.7) 29 | sass (~> 3.1) 30 | execjs (1.4.0) 31 | multi_json (~> 1.0) 32 | ffi (1.0.11) 33 | fssm (0.2.9) 34 | guard (1.2.3) 35 | listen (>= 0.4.2) 36 | thor (>= 0.14.6) 37 | guard-rake (0.0.7) 38 | guard 39 | rake 40 | kicker (2.6.0) 41 | listen 42 | listen (0.4.7) 43 | rb-fchange (~> 0.0.5) 44 | rb-fsevent (~> 0.9.1) 45 | rb-inotify (~> 0.8.8) 46 | multi_json (1.3.6) 47 | open4 (1.3.0) 48 | rack (1.4.1) 49 | rack-rewrite (1.2.1) 50 | rack-streaming-proxy (1.0.3) 51 | rack (>= 1.0) 52 | servolux (~> 0.8.1) 53 | rake (0.9.2.2) 54 | rb-fchange (0.0.5) 55 | ffi 56 | rb-fsevent (0.9.1) 57 | rb-inotify (0.8.8) 58 | ffi (>= 0.5.0) 59 | sass (3.1.20) 60 | servolux (0.8.1) 61 | thor (0.15.4) 62 | uglifier (1.2.6) 63 | execjs (>= 0.3.0) 64 | multi_json (~> 1.3) 65 | yui-compressor (0.9.6) 66 | POpen4 (>= 0.1.4) 67 | 68 | PLATFORMS 69 | ruby 70 | 71 | DEPENDENCIES 72 | colored 73 | compass 74 | guard 75 | guard-rake 76 | kicker 77 | rack 78 | rack-rewrite 79 | rack-streaming-proxy 80 | rake-pipeline! 81 | rake-pipeline-web-filters! 82 | sass 83 | uglifier 84 | yui-compressor 85 | -------------------------------------------------------------------------------- /tests/qunit/run-qunit.js: -------------------------------------------------------------------------------- 1 | // PhantomJS QUnit Test Runner 2 | 3 | var args = phantom.args; 4 | if (args.length < 1 || args.length > 2) { 5 | console.log("Usage: " + phantom.scriptName + " "); 6 | phantom.exit(1); 7 | } 8 | 9 | var page = require('webpage').create(); 10 | 11 | var depRe = /^DEPRECATION:/; 12 | page.onConsoleMessage = function(msg) { 13 | if (!depRe.test(msg)) console.log(msg); 14 | }; 15 | 16 | page.open(args[0], function(status) { 17 | if (status !== 'success') { 18 | console.error("Unable to access network"); 19 | phantom.exit(1); 20 | } else { 21 | page.evaluate(addLogging); 22 | 23 | var timeout = parseInt(args[1] || 30000, 10); 24 | var start = Date.now(); 25 | var interval = setInterval(function() { 26 | if (Date.now() > start + timeout) { 27 | console.error("Tests timed out"); 28 | phantom.exit(1); 29 | } else { 30 | var qunitDone = page.evaluate(function() { 31 | return window.qunitDone; 32 | }); 33 | 34 | if (qunitDone) { 35 | clearInterval(interval); 36 | if (qunitDone.failed > 0) { 37 | phantom.exit(1); 38 | } else { 39 | phantom.exit(); 40 | } 41 | } 42 | } 43 | }, 500); 44 | } 45 | }); 46 | 47 | function addLogging() { 48 | var testErrors = []; 49 | var assertionErrors = []; 50 | 51 | QUnit.moduleDone(function(context) { 52 | if (context.failed) { 53 | var msg = "Module Failed: " + context.name + "\n" + testErrors.join("\n"); 54 | console.error(msg); 55 | testErrors = []; 56 | } 57 | }); 58 | 59 | QUnit.testDone(function(context) { 60 | if (context.failed) { 61 | var msg = " Test Failed: " + context.name + assertionErrors.join(" "); 62 | testErrors.push(msg); 63 | assertionErrors = []; 64 | } 65 | }); 66 | 67 | QUnit.log(function(context) { 68 | if (context.result) return; 69 | 70 | var msg = "\n Assertion Failed:"; 71 | if (context.message) { 72 | msg += " " + context.message; 73 | } 74 | 75 | if (context.expected) { 76 | msg += "\n Expected: " + context.expected + ", Actual: " + context.actual; 77 | } 78 | 79 | assertionErrors.push(msg); 80 | }); 81 | 82 | QUnit.done(function(context) { 83 | var stats = [ 84 | "Time: " + context.runtime + "ms", 85 | "Total: " + context.total, 86 | "Passed: " + context.passed, 87 | "Failed: " + context.failed 88 | ]; 89 | console.log(stats.join(", ")); 90 | window.qunitDone = context; 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /app/tests/templates_test.js: -------------------------------------------------------------------------------- 1 | /*jshint sub: true */ 2 | require('irc/core'); 3 | require('irc/templates'); 4 | 5 | module('IRC templates', { 6 | setup: function() { 7 | // clear all templates 8 | Ember.TEMPLATES = []; 9 | } 10 | }); 11 | 12 | test('tmpl adds the template to Ember.TEMPLATES', 3, 13 | function() { 14 | var testTemplateName = 'test_template'; 15 | ok(!Ember.TEMPLATES['test_template'], 'precond: template not loaded'); 16 | var main_page = 'test_template'.tmpl(); 17 | equal(main_page, 'test_template'); 18 | ok(Ember.TEMPLATES['test_template'], 'template is available'); 19 | }); 20 | 21 | module('Handlebars Helper'); 22 | 23 | test('Handlebars format helper works with default format', 1, 24 | function() { 25 | var view = Ember.View.create({ 26 | elementId: 'dateView', 27 | template: Ember.Handlebars.compile('{{format date}}'), 28 | date: IRC.createDate() 29 | }); 30 | 31 | Ember.run(function() { 32 | view.appendTo('#qunit-fixture'); 33 | }); 34 | 35 | ok($('#dateView').html().trim()); 36 | }); 37 | 38 | 39 | test('Handlebars format helper works with specified format', 1, 40 | function() { 41 | var view = Ember.View.create({ 42 | elementId: 'dateView', 43 | template: Ember.Handlebars.compile('{{format date "YYYY-MM-DD"}}'), 44 | date: IRC.createDate('2012-12-21T12:00:00') 45 | }); 46 | 47 | Ember.run(function() { 48 | view.appendTo('#qunit-fixture'); 49 | }); 50 | 51 | equal($('#dateView').html().trim(), '2012-12-21'); 52 | }); 53 | 54 | test('Handlebars parse helper', 1, 55 | function() { 56 | var view = Ember.View.create({ 57 | elementId: 'formatView', 58 | template: Ember.Handlebars.compile('{{parse text}}'), 59 | text: 'hello http://www.google.com you' 60 | }); 61 | 62 | Ember.run(function() { 63 | view.appendTo('#qunit-fixture'); 64 | }); 65 | 66 | equal($('#formatView').html().trim(), 'hello http://www.google.com you', 'link is replaced'); 67 | }); 68 | 69 | module('String.prototype.parseURL'); 70 | 71 | test('wraps urls in anchor tags', 6, 72 | function() { 73 | var testAnchor = function(url, description) { 74 | equal(url.parseURL(), '' + url + '', description); 75 | }; 76 | testAnchor('https://emberjs.com', 'recognizes https domains'); 77 | testAnchor('http://emberjs.com', 'recognizes http domains'); 78 | testAnchor('http://emberjs.com/documentation', 'recognizes urls without specific file'); 79 | testAnchor('http://emberjs.com/documentation/index.html', 'recognizes urls with file'); 80 | testAnchor('https://github.com/emberjs/emberjs.github.com', 'recognizes urls within urls'); 81 | testAnchor('http://emberjs.com/documentation/index.html#views', 'recognizes urls with hash tag'); 82 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## irc-log-viewer [![Build Status](https://secure.travis-ci.org/pangratz/irc-log-viewer.png)](http://travis-ci.org/pangratz/irc-log-viewer) 2 | 3 | This is a viewer for IRC logs which are stored in a CouchDB. It's basically a couchapp using Ember.js. Demo? Here you go: [http://emberjs.iriscouch.com/irc/_design/viewer/index.html](http://emberjs.iriscouch.com/irc/_design/viewer/index.html) (transcript of #emberjs channel) 4 | 5 | ### Awesome stuff used 6 | 7 | - [sexy Ember.js](https://github.com/emberjs/ember.js) 8 | - [sexy interline/ember-skeleton](https://github.com/interline/ember-skeleton) 9 | - [sexy CouchDB](http://couchdb.apache.org/) 10 | - [sexy Couchapp](http://couchapp.org/) 11 | - [Twitter's sexy Bootstrap](http://twitter.github.com/bootstrap/) 12 | 13 | ## Stored IRC messages 14 | 15 | The IRC messages saved in the CouchDB have the following format: 16 | 17 | ```javascript 18 | { 19 | "user": { 20 | "id": "123", 21 | "name": "GOB" 22 | }, 23 | "text": "Come on!", 24 | "date": "2012-12-21T12:34:56.789Z" 25 | } 26 | ``` 27 | 28 | One example of filling the CouchDB with the IRC messages would be a [Hubot](https://github.com/github/hubot) configured with the [store-messages-couchdb.coffee](https://github.com/github/hubot-scripts/blob/master/src/scripts/store-messages-couchdb.coffee) script and using an [IRC Adapter](https://github.com/nandub/hubot-irc) 29 | 30 | ## CouchDB Views 31 | 32 | The `messages` view returns all messages, where the key is the date structured as an array with the year as first element, month as second and so forth. This allows you to get messages for a specific period. 33 | 34 | ## Front end 35 | 36 | Inside the `app` folder is the basic application. 37 | 38 | ## Development 39 | 40 | ### Prerequisites: 41 | - installed Ruby 42 | - installed CouchDB where app can be deployed or use a free hosting service like the excellent [Iris Couch](http://www.iriscouch.com/) 43 | - installed `couchapp` command line tool for easy pushing of the app to a CouchDB, see [installation](http://couchapp.org/page/installing) 44 | 45 | ### Developing 46 | 47 | - Clone this repo, obviously 48 | - execute `couchapp init` to create an empty `.couchapprc` file inside the project (See section `.couchapprc` on http://couchapp.org/page/couchapp-config) 49 | - execute `bundle install` 50 | - Tests are in located in the `tests` folder 51 | - Execute `bundle execute rackup` to start test server 52 | - Access [http://localhost:4020/tests.html](http://localhost:4020/tests.html) to execute the tests 53 | 54 | or 55 | 56 | - Execute `bundle execute rake test` to run the tests from command line 57 | 58 | ### Deploy 59 | 60 | - execute `bundle exec rake build` 61 | - push the Couchapp to your CouchDB; if you have `couchapp` installed, do a `couchapp push http://localhost:5984/irc` 62 | - access the IRC log viewer at `http://localhost:5984/irc/_design/viewer/index.html` 63 | 64 | -------------------------------------------------------------------------------- /Assetfile: -------------------------------------------------------------------------------- 1 | APPNAME = 'irc' 2 | 3 | require 'json' 4 | require 'rake-pipeline-web-filters' 5 | 6 | WebFilters = Rake::Pipeline::Web::Filters 7 | 8 | class LoaderFilter < WebFilters::MinispadeFilter 9 | def generate_output(inputs, output) 10 | inputs.each do |input| 11 | code = input.read 12 | module_id = @module_id_generator.call(input) 13 | contents = "function(require) {\n#{code}\n}" 14 | ret = "\nloader.register('#{module_id}', #{contents});\n" 15 | output.write ret 16 | end 17 | end 18 | end 19 | 20 | class EmberAssertFilter < Filter 21 | def generate_output(inputs, output) 22 | inputs.each do |input| 23 | result = input.read 24 | result.gsub!(/ember_assert\((.*)\);/, '') 25 | output.write(result) 26 | end 27 | end 28 | end 29 | 30 | class HandlebarsFilter < Filter 31 | def generate_output(inputs, output) 32 | inputs.each do |input| 33 | code = input.read.to_json 34 | name = File.basename(input.path, '.handlebars') 35 | output.write "\nreturn Ember.Handlebars.compile(#{code});\n" 36 | end 37 | end 38 | end 39 | 40 | output '_attachments' 41 | 42 | input 'app' do 43 | 44 | match 'index.html' do 45 | concat 'index.html' 46 | end 47 | 48 | match 'lib/**/*.js' do 49 | filter LoaderFilter, 50 | :module_id_generator => proc { |input| 51 | input.path.sub(/^lib\//, "#{APPNAME}/").sub(/\.js$/, '') 52 | } 53 | 54 | if ENV['RAKEP_MODE'] == 'production' 55 | filter EmberAssertFilter 56 | uglify {|input| input} 57 | end 58 | concat 'app.js' 59 | end 60 | 61 | match 'vendor/**/*.js' do 62 | filter LoaderFilter, 63 | :module_id_generator => proc { |input| 64 | input.path.sub(/^vendor\//, '').sub(/\.js$/, '') 65 | } 66 | 67 | if ENV['RAKEP_MODE'] == 'production' 68 | filter EmberAssertFilter 69 | uglify {|input| input} 70 | end 71 | concat %w[ 72 | vendor/jquery.js 73 | vendor/ember.js 74 | vendor/ember-data.js 75 | vendor/sproutcore-routing.js 76 | ], 'app.js' 77 | end 78 | 79 | match 'modules/**/*.js' do 80 | if ENV['RAKEP_MODE'] == 'production' 81 | filter EmberAssertFilter 82 | uglify {|input| input} 83 | end 84 | concat 'app.js' 85 | end 86 | 87 | match 'plugins/**/*.js' do 88 | if ENV['RAKEP_MODE'] == 'production' 89 | uglify {|input| input} 90 | end 91 | concat do |input| 92 | input.sub(/plugins\//, '') 93 | end 94 | end 95 | 96 | match 'templates/**/*.handlebars' do 97 | filter HandlebarsFilter 98 | filter LoaderFilter, 99 | :module_id_generator => proc { |input| 100 | input.path.sub(/^templates\//, "#{APPNAME}/~templates/").sub(/\.handlebars$/, '') 101 | } 102 | if ENV['RAKEP_MODE'] == 'production' 103 | uglify {|input| input} 104 | end 105 | concat 'app.js' 106 | end 107 | 108 | match 'tests/**/*.js' do 109 | filter LoaderFilter, 110 | :module_id_generator => proc { |input| 111 | input.path.sub(/^lib\//, "#{APPNAME}/").sub(/\.js$/, '') 112 | } 113 | concat 'app-tests.js' 114 | end 115 | 116 | match 'css/**/*.css' do 117 | if ENV['RAKEP_MODE'] == 'production' 118 | yui_css 119 | end 120 | concat ['bootstrap.css', 'main.css'], 'app.css' 121 | end 122 | 123 | match 'css/**/*.scss' do 124 | sass 125 | if ENV['RAKEP_MODE'] == 'production' 126 | yui_css 127 | end 128 | concat 'app.css' 129 | end 130 | 131 | match "static/**/*" do 132 | concat do |input| 133 | input.sub(/static\//, '') 134 | end 135 | end 136 | end 137 | 138 | # vim: filetype=ruby 139 | -------------------------------------------------------------------------------- /app/tests/application_test.js: -------------------------------------------------------------------------------- 1 | require('irc/core'); 2 | 3 | module('IRC'); 4 | 5 | test('exists', 2, 6 | function() { 7 | ok(IRC, 'IRC exists'); 8 | ok(Ember.Application.detectInstance(IRC), 'IRC is an instance of Ember.Application'); 9 | }); 10 | 11 | test('has a version', 1, 12 | function() { 13 | ok(IRC.VERSION, 'version is available'); 14 | }); 15 | 16 | module('IRC#createDate'); 17 | test('has a method createDate', 1, 18 | function() { 19 | ok(IRC.createDate, 'method exists'); 20 | }); 21 | 22 | test('returnes the UTC version of passed Date object', 4, 23 | function() { 24 | var date = new Date('2012-05-25T13:32:18+02:00'); 25 | var utcDate = new Date('2012-05-25T11:32:18+00:00'); 26 | 27 | var createdDate = IRC.createDate(date); 28 | ok(createdDate, 'returned date exists'); 29 | ok(createdDate instanceof Date, 'created object is an instance of Date'); 30 | equal(utcDate.getTime(), createdDate.getTime(), 'created date is equal to the source date'); 31 | equal(createdDate.getUTCHours(), 11, 'created date is in timezone 0'); 32 | }); 33 | 34 | test('works with string argument', 4, 35 | function() { 36 | var dateStr = '2012-05-25T13:32:18+02:00'; 37 | 38 | var createdDate = IRC.createDate(dateStr); 39 | ok(createdDate, 'returned object exists'); 40 | ok(createdDate instanceof Date, 'returned object is an instance of Date'); 41 | equal(createdDate.getTime(), new Date(dateStr).getTime(), 'created date is equal to the source date'); 42 | equal(createdDate.getUTCHours(), 11, 'created date is in timezone 0'); 43 | }); 44 | 45 | test('creates a Date with current time if no argument is passed', 4, 46 | function() { 47 | var now = new Date(); 48 | var createdDate = IRC.createDate(); 49 | ok(createdDate, 'returned object exists'); 50 | ok(createdDate instanceof Date, 'returned object is an instance of Date'); 51 | ok(Math.abs(now.getTime() - createdDate.getTime()) <= 1000, 'created date is the current date'); 52 | equal(createdDate.getUTCHours(), now.getUTCHours(), 'created date is in timezone 0'); 53 | }); 54 | 55 | test('returns the Date in UTC time', 3, 56 | function() { 57 | var now = new Date('2012-05-25T13:32:18+02:00'); 58 | var createdDate = IRC.createDate(now); 59 | ok(createdDate, 'returned object exists'); 60 | ok(createdDate instanceof Date, 'returned object is an instance of Date'); 61 | equal(createdDate.getUTCHours(), 11, 'created date is in timezone 0'); 62 | }); 63 | 64 | module('IRC#getNextDay'); 65 | 66 | test('returns the next day', 67 | function() { 68 | var date = new Date('2012-05-05T12:00:00+00:00'); 69 | equal(date.getDate(), 5, 'precond - date is the 5th'); 70 | 71 | var nextDay = IRC.getNextDay(date); 72 | 73 | equal(date.getDate(), 5, 'date is not modified'); 74 | ok(nextDay, 'returned object exists'); 75 | equal(nextDay.getDate(), 6, 'next day is the 6th'); 76 | }); 77 | 78 | module('IRC#getDateArray'); 79 | 80 | test('method getDateArray exists', 1, 81 | function() { 82 | ok(IRC.getDateArray, 'exists'); 83 | }); 84 | 85 | test('returns array with specified date properties', 6, 86 | function() { 87 | var date = IRC.createDate('2012-12-21T12:34:56.789Z'); 88 | deepEqual(IRC.getDateArray(date, 'year'), [2012], 'single property work'); 89 | deepEqual(IRC.getDateArray(date, 'year', 'month', 'day', 'hour', 'minute', 'second', 'millisecond'), [2012, 11, 21, 12, 34, 56, 789], 'all properties work'); 90 | deepEqual(IRC.getDateArray(date, 'year', 'year'), [2012, 2012], 'duplicate properties work'); 91 | deepEqual(IRC.getDateArray(date, 'unknown'), [null], 'unknown properties are null in the array'); 92 | deepEqual(IRC.getDateArray(date, 'year', 'unknown', 'day'), [2012, null, 21], 'unknown properties work with valid ones'); 93 | deepEqual(IRC.getDateArray(date, 'month'), [11], 'month is 0-based'); 94 | }); -------------------------------------------------------------------------------- /app/tests/controller_test.js: -------------------------------------------------------------------------------- 1 | require('irc/core'); 2 | require('irc/controller'); 3 | 4 | var controller; 5 | module('IRC.MessagesController', { 6 | setup: function() { 7 | controller = IRC.MessagesController.create({ 8 | content: [] 9 | }); 10 | }, 11 | teardown: function() { 12 | controller.clear(); 13 | } 14 | }); 15 | 16 | test('exists', 1, 17 | function() { 18 | ok(IRC.MessagesController, 'it exists'); 19 | }); 20 | 21 | test('add new message', 7, 22 | function() { 23 | ok(controller.addMessage, 'has a method addMessage'); 24 | var message = { 25 | date: '2012-12-21T12:34:56.789Z', 26 | text: 'test message', 27 | user: { 28 | name: 'username' 29 | } 30 | }; 31 | 32 | controller.addMessage(message); 33 | equal(controller.get('length'), 1, 'addMessage adds message to content'); 34 | 35 | var addedMessage = controller.objectAt(0); 36 | ok(addedMessage); 37 | 38 | var addedDate = addedMessage.get('date'); 39 | ok(addedDate instanceof Date, 'date of the message is a Date'); 40 | var date = IRC.createDate('2012-12-21T12:34:56.789Z'); 41 | equal(date.getTime(), addedDate.getTime(), 'added date is the correct Date object'); 42 | equal(addedMessage.get('username'), message.user.name, 'addedMessage has the username'); 43 | equal(addedMessage.get('text'), message.text, 'addedMessage has the text'); 44 | }); 45 | 46 | test('add a new message with empty text string', 47 | function() { 48 | var message = { 49 | date: '2012-12-21T12:34:56.789Z', 50 | text: '', 51 | user: { 52 | name: 'username' 53 | } 54 | }; 55 | 56 | controller.addMessage(message); 57 | equal(controller.get('length'), 1, 'length is 1'); 58 | equal(controller.objectAt(0).get('text'), '', 'text of message is an empty string'); 59 | }); 60 | 61 | test('clear', 3, 62 | function() { 63 | ok(controller.clear, 'has a clear method'); 64 | 65 | var message = { 66 | date: '2012-12-21T12:34:56.789Z', 67 | text: 'test message', 68 | user: { 69 | name: 'username' 70 | } 71 | }; 72 | controller.addMessage(message); 73 | ok(controller.get('length') > 0); 74 | 75 | controller.clear(); 76 | equal(controller.get('length'), 0, 'after clear there are no messages in the controller'); 77 | }); 78 | 79 | module('IRC.DaysController', { 80 | setup: function() { 81 | controller = IRC.DaysController.create(); 82 | }, 83 | teardown: function() { 84 | controller = null; 85 | } 86 | }); 87 | 88 | test('exists', 1, 89 | function() { 90 | ok(IRC.DaysController, 'it exists'); 91 | }); 92 | 93 | test('add new day', 5, 94 | function() { 95 | ok(controller.addDay, 'it has a method addDay'); 96 | 97 | var day = { 98 | date: '2012-12-21T12:34:56.789Z', 99 | count: 123 100 | }; 101 | 102 | controller.addDay(day); 103 | equal(controller.get('length'), 1, 'added day'); 104 | 105 | var addedDay = controller.objectAt('0'); 106 | ok(addedDay); 107 | equal(IRC.createDate('2012-12-21T12:34:56.789Z').getTime(), addedDay.get('date').getTime(), 'added date is equal to original'); 108 | equal(addedDay.get('count'), 123, 'count of added day is the same as original'); 109 | }); 110 | 111 | test('clear', 3, 112 | function() { 113 | ok(controller.clear, 'has a clear method'); 114 | 115 | var day = { 116 | date: '2012-12-21T12:34:56.789Z', 117 | count: 123 118 | }; 119 | controller.addDay(day); 120 | ok(controller.get('length') > 0); 121 | 122 | controller.clear(); 123 | equal(controller.get('length'), 0, 'after clear there are no days in the controller'); 124 | }); -------------------------------------------------------------------------------- /tests/qunit/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.4.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2012 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 15px 15px 0 0; 42 | -moz-border-radius: 15px 15px 0 0; 43 | -webkit-border-top-right-radius: 15px; 44 | -webkit-border-top-left-radius: 15px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-header label { 58 | display: inline-block; 59 | } 60 | 61 | #qunit-banner { 62 | height: 5px; 63 | } 64 | 65 | #qunit-testrunner-toolbar { 66 | padding: 0.5em 0 0.5em 2em; 67 | color: #5E740B; 68 | background-color: #eee; 69 | } 70 | 71 | #qunit-userAgent { 72 | padding: 0.5em 0 0.5em 2.5em; 73 | background-color: #2b81af; 74 | color: #fff; 75 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 76 | } 77 | 78 | 79 | /** Tests: Pass/Fail */ 80 | 81 | #qunit-tests { 82 | list-style-position: inside; 83 | } 84 | 85 | #qunit-tests li { 86 | padding: 0.4em 0.5em 0.4em 2.5em; 87 | border-bottom: 1px solid #fff; 88 | list-style-position: inside; 89 | } 90 | 91 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 92 | display: none; 93 | } 94 | 95 | #qunit-tests li strong { 96 | cursor: pointer; 97 | } 98 | 99 | #qunit-tests li a { 100 | padding: 0.5em; 101 | color: #c2ccd1; 102 | text-decoration: none; 103 | } 104 | #qunit-tests li a:hover, 105 | #qunit-tests li a:focus { 106 | color: #000; 107 | } 108 | 109 | #qunit-tests ol { 110 | margin-top: 0.5em; 111 | padding: 0.5em; 112 | 113 | background-color: #fff; 114 | 115 | border-radius: 15px; 116 | -moz-border-radius: 15px; 117 | -webkit-border-radius: 15px; 118 | 119 | box-shadow: inset 0px 2px 13px #999; 120 | -moz-box-shadow: inset 0px 2px 13px #999; 121 | -webkit-box-shadow: inset 0px 2px 13px #999; 122 | } 123 | 124 | #qunit-tests table { 125 | border-collapse: collapse; 126 | margin-top: .2em; 127 | } 128 | 129 | #qunit-tests th { 130 | text-align: right; 131 | vertical-align: top; 132 | padding: 0 .5em 0 0; 133 | } 134 | 135 | #qunit-tests td { 136 | vertical-align: top; 137 | } 138 | 139 | #qunit-tests pre { 140 | margin: 0; 141 | white-space: pre-wrap; 142 | word-wrap: break-word; 143 | } 144 | 145 | #qunit-tests del { 146 | background-color: #e0f2be; 147 | color: #374e0c; 148 | text-decoration: none; 149 | } 150 | 151 | #qunit-tests ins { 152 | background-color: #ffcaca; 153 | color: #500; 154 | text-decoration: none; 155 | } 156 | 157 | /*** Test Counts */ 158 | 159 | #qunit-tests b.counts { color: black; } 160 | #qunit-tests b.passed { color: #5E740B; } 161 | #qunit-tests b.failed { color: #710909; } 162 | 163 | #qunit-tests li li { 164 | margin: 0.5em; 165 | padding: 0.4em 0.5em 0.4em 0.5em; 166 | background-color: #fff; 167 | border-bottom: none; 168 | list-style-position: inside; 169 | } 170 | 171 | /*** Passing Styles */ 172 | 173 | #qunit-tests li li.pass { 174 | color: #5E740B; 175 | background-color: #fff; 176 | border-left: 26px solid #C6E746; 177 | } 178 | 179 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 180 | #qunit-tests .pass .test-name { color: #366097; } 181 | 182 | #qunit-tests .pass .test-actual, 183 | #qunit-tests .pass .test-expected { color: #999999; } 184 | 185 | #qunit-banner.qunit-pass { background-color: #C6E746; } 186 | 187 | /*** Failing Styles */ 188 | 189 | #qunit-tests li li.fail { 190 | color: #710909; 191 | background-color: #fff; 192 | border-left: 26px solid #EE5757; 193 | white-space: pre; 194 | } 195 | 196 | #qunit-tests > li:last-child { 197 | border-radius: 0 0 15px 15px; 198 | -moz-border-radius: 0 0 15px 15px; 199 | -webkit-border-bottom-right-radius: 15px; 200 | -webkit-border-bottom-left-radius: 15px; 201 | } 202 | 203 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 204 | #qunit-tests .fail .test-name, 205 | #qunit-tests .fail .module-name { color: #000000; } 206 | 207 | #qunit-tests .fail .test-actual { color: #EE5757; } 208 | #qunit-tests .fail .test-expected { color: green; } 209 | 210 | #qunit-banner.qunit-fail { background-color: #EE5757; } 211 | 212 | 213 | /** Result */ 214 | 215 | #qunit-testresult { 216 | padding: 0.5em 0.5em 0.5em 2.5em; 217 | 218 | color: #2b81af; 219 | background-color: #D2E0E6; 220 | 221 | border-bottom: 1px solid white; 222 | } 223 | 224 | /** Fixture */ 225 | 226 | #qunit-fixture { 227 | position: absolute; 228 | top: -10000px; 229 | left: -10000px; 230 | width: 1000px; 231 | height: 1000px; 232 | } 233 | -------------------------------------------------------------------------------- /app/vendor/moment.js: -------------------------------------------------------------------------------- 1 | // moment.js 2 | // version : 1.6.2 3 | // author : Tim Wood 4 | // license : MIT 5 | // momentjs.com 6 | 7 | (function (Date, undefined) { 8 | 9 | var moment, 10 | VERSION = "1.6.2", 11 | round = Math.round, i, 12 | // internal storage for language config files 13 | languages = {}, 14 | currentLanguage = 'en', 15 | 16 | // check for nodeJS 17 | hasModule = (typeof module !== 'undefined'), 18 | 19 | // parameters to check for on the lang config 20 | langConfigProperties = 'months|monthsShort|monthsParse|weekdays|weekdaysShort|longDateFormat|calendar|relativeTime|ordinal|meridiem'.split('|'), 21 | 22 | // ASP.NET json date format regex 23 | aspNetJsonRegex = /^\/?Date\((\-?\d+)/i, 24 | 25 | // format tokens 26 | formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|dddd?|do?|w[o|w]?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|zz?|ZZ?|LT|LL?L?L?)/g, 27 | 28 | // parsing tokens 29 | parseMultipleFormatChunker = /([0-9a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)/gi, 30 | 31 | // parsing token regexes 32 | parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99 33 | parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999 34 | parseTokenThreeDigits = /\d{3}/, // 000 - 999 35 | parseTokenFourDigits = /\d{4}/, // 0000 - 9999 36 | parseTokenWord = /[0-9a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+/i, // any word characters or numbers 37 | parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z 38 | parseTokenT = /T/i, // T (ISO seperator) 39 | 40 | // preliminary iso regex 41 | // 0000-00-00 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 42 | isoRegex = /^\s*\d{4}-\d\d-\d\d(T(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/, 43 | isoFormat = 'YYYY-MM-DDTHH:mm:ssZ', 44 | 45 | // iso time formats and regexes 46 | isoTimes = [ 47 | ['HH:mm:ss.S', /T\d\d:\d\d:\d\d\.\d{1,3}/], 48 | ['HH:mm:ss', /T\d\d:\d\d:\d\d/], 49 | ['HH:mm', /T\d\d:\d\d/], 50 | ['HH', /T\d\d/] 51 | ], 52 | 53 | // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"] 54 | parseTimezoneChunker = /([\+\-]|\d\d)/gi, 55 | 56 | // getter and setter names 57 | proxyGettersAndSetters = 'Month|Date|Hours|Minutes|Seconds|Milliseconds'.split('|'), 58 | unitMillisecondFactors = { 59 | 'Milliseconds' : 1, 60 | 'Seconds' : 1e3, 61 | 'Minutes' : 6e4, 62 | 'Hours' : 36e5, 63 | 'Days' : 864e5, 64 | 'Months' : 2592e6, 65 | 'Years' : 31536e6 66 | }; 67 | 68 | // Moment prototype object 69 | function Moment(date, isUTC) { 70 | this._d = date; 71 | this._isUTC = !!isUTC; 72 | } 73 | 74 | function absRound(number) { 75 | if (number < 0) { 76 | return Math.ceil(number); 77 | } else { 78 | return Math.floor(number); 79 | } 80 | } 81 | 82 | // Duration Constructor 83 | function Duration(duration) { 84 | var data = this._data = {}, 85 | years = duration.years || duration.y || 0, 86 | months = duration.months || duration.M || 0, 87 | weeks = duration.weeks || duration.w || 0, 88 | days = duration.days || duration.d || 0, 89 | hours = duration.hours || duration.h || 0, 90 | minutes = duration.minutes || duration.m || 0, 91 | seconds = duration.seconds || duration.s || 0, 92 | milliseconds = duration.milliseconds || duration.ms || 0; 93 | 94 | // representation for dateAddRemove 95 | this._milliseconds = milliseconds + 96 | seconds * 1e3 + // 1000 97 | minutes * 6e4 + // 1000 * 60 98 | hours * 36e5; // 1000 * 60 * 60 99 | // Because of dateAddRemove treats 24 hours as different from a 100 | // day when working around DST, we need to store them separately 101 | this._days = days + 102 | weeks * 7; 103 | // It is impossible translate months into days without knowing 104 | // which months you are are talking about, so we have to store 105 | // it separately. 106 | this._months = months + 107 | years * 12; 108 | 109 | // The following code bubbles up values, see the tests for 110 | // examples of what that means. 111 | data.milliseconds = milliseconds % 1000; 112 | seconds += absRound(milliseconds / 1000); 113 | 114 | data.seconds = seconds % 60; 115 | minutes += absRound(seconds / 60); 116 | 117 | data.minutes = minutes % 60; 118 | hours += absRound(minutes / 60); 119 | 120 | data.hours = hours % 24; 121 | days += absRound(hours / 24); 122 | 123 | days += weeks * 7; 124 | data.days = days % 30; 125 | 126 | months += absRound(days / 30); 127 | 128 | data.months = months % 12; 129 | years += absRound(months / 12); 130 | 131 | data.years = years; 132 | } 133 | 134 | // left zero fill a number 135 | // see http://jsperf.com/left-zero-filling for performance comparison 136 | function leftZeroFill(number, targetLength) { 137 | var output = number + ''; 138 | while (output.length < targetLength) { 139 | output = '0' + output; 140 | } 141 | return output; 142 | } 143 | 144 | // helper function for _.addTime and _.subtractTime 145 | function addOrSubtractDurationFromMoment(mom, duration, isAdding) { 146 | var ms = duration._milliseconds, 147 | d = duration._days, 148 | M = duration._months, 149 | currentDate; 150 | 151 | if (ms) { 152 | mom._d.setTime(+mom + ms * isAdding); 153 | } 154 | if (d) { 155 | mom.date(mom.date() + d * isAdding); 156 | } 157 | if (M) { 158 | currentDate = mom.date(); 159 | mom.date(1) 160 | .month(mom.month() + M * isAdding) 161 | .date(Math.min(currentDate, mom.daysInMonth())); 162 | } 163 | } 164 | 165 | // check if is an array 166 | function isArray(input) { 167 | return Object.prototype.toString.call(input) === '[object Array]'; 168 | } 169 | 170 | // convert an array to a date. 171 | // the array should mirror the parameters below 172 | // note: all values past the year are optional and will default to the lowest possible value. 173 | // [year, month, day , hour, minute, second, millisecond] 174 | function dateFromArray(input) { 175 | return new Date(input[0], input[1] || 0, input[2] || 1, input[3] || 0, input[4] || 0, input[5] || 0, input[6] || 0); 176 | } 177 | 178 | // format date using native date object 179 | function formatMoment(m, inputString) { 180 | var currentMonth = m.month(), 181 | currentDate = m.date(), 182 | currentYear = m.year(), 183 | currentDay = m.day(), 184 | currentHours = m.hours(), 185 | currentMinutes = m.minutes(), 186 | currentSeconds = m.seconds(), 187 | currentMilliseconds = m.milliseconds(), 188 | currentZone = -m.zone(), 189 | ordinal = moment.ordinal, 190 | meridiem = moment.meridiem; 191 | // check if the character is a format 192 | // return formatted string or non string. 193 | // 194 | // uses switch/case instead of an object of named functions (like http://phpjs.org/functions/date:380) 195 | // for minification and performance 196 | // see http://jsperf.com/object-of-functions-vs-switch for performance comparison 197 | function replaceFunction(input) { 198 | // create a couple variables to be used later inside one of the cases. 199 | var a, b; 200 | switch (input) { 201 | // MONTH 202 | case 'M' : 203 | return currentMonth + 1; 204 | case 'Mo' : 205 | return (currentMonth + 1) + ordinal(currentMonth + 1); 206 | case 'MM' : 207 | return leftZeroFill(currentMonth + 1, 2); 208 | case 'MMM' : 209 | return moment.monthsShort[currentMonth]; 210 | case 'MMMM' : 211 | return moment.months[currentMonth]; 212 | // DAY OF MONTH 213 | case 'D' : 214 | return currentDate; 215 | case 'Do' : 216 | return currentDate + ordinal(currentDate); 217 | case 'DD' : 218 | return leftZeroFill(currentDate, 2); 219 | // DAY OF YEAR 220 | case 'DDD' : 221 | a = new Date(currentYear, currentMonth, currentDate); 222 | b = new Date(currentYear, 0, 1); 223 | return ~~ (((a - b) / 864e5) + 1.5); 224 | case 'DDDo' : 225 | a = replaceFunction('DDD'); 226 | return a + ordinal(a); 227 | case 'DDDD' : 228 | return leftZeroFill(replaceFunction('DDD'), 3); 229 | // WEEKDAY 230 | case 'd' : 231 | return currentDay; 232 | case 'do' : 233 | return currentDay + ordinal(currentDay); 234 | case 'ddd' : 235 | return moment.weekdaysShort[currentDay]; 236 | case 'dddd' : 237 | return moment.weekdays[currentDay]; 238 | // WEEK OF YEAR 239 | case 'w' : 240 | a = new Date(currentYear, currentMonth, currentDate - currentDay + 5); 241 | b = new Date(a.getFullYear(), 0, 4); 242 | return ~~ ((a - b) / 864e5 / 7 + 1.5); 243 | case 'wo' : 244 | a = replaceFunction('w'); 245 | return a + ordinal(a); 246 | case 'ww' : 247 | return leftZeroFill(replaceFunction('w'), 2); 248 | // YEAR 249 | case 'YY' : 250 | return leftZeroFill(currentYear % 100, 2); 251 | case 'YYYY' : 252 | return currentYear; 253 | // AM / PM 254 | case 'a' : 255 | return meridiem ? meridiem(currentHours, currentMinutes, false) : (currentHours > 11 ? 'pm' : 'am'); 256 | case 'A' : 257 | return meridiem ? meridiem(currentHours, currentMinutes, true) : (currentHours > 11 ? 'PM' : 'AM'); 258 | // 24 HOUR 259 | case 'H' : 260 | return currentHours; 261 | case 'HH' : 262 | return leftZeroFill(currentHours, 2); 263 | // 12 HOUR 264 | case 'h' : 265 | return currentHours % 12 || 12; 266 | case 'hh' : 267 | return leftZeroFill(currentHours % 12 || 12, 2); 268 | // MINUTE 269 | case 'm' : 270 | return currentMinutes; 271 | case 'mm' : 272 | return leftZeroFill(currentMinutes, 2); 273 | // SECOND 274 | case 's' : 275 | return currentSeconds; 276 | case 'ss' : 277 | return leftZeroFill(currentSeconds, 2); 278 | // MILLISECONDS 279 | case 'S' : 280 | return ~~ (currentMilliseconds / 100); 281 | case 'SS' : 282 | return leftZeroFill(~~(currentMilliseconds / 10), 2); 283 | case 'SSS' : 284 | return leftZeroFill(currentMilliseconds, 3); 285 | // TIMEZONE 286 | case 'Z' : 287 | return (currentZone < 0 ? '-' : '+') + leftZeroFill(~~(Math.abs(currentZone) / 60), 2) + ':' + leftZeroFill(~~(Math.abs(currentZone) % 60), 2); 288 | case 'ZZ' : 289 | return (currentZone < 0 ? '-' : '+') + leftZeroFill(~~(10 * Math.abs(currentZone) / 6), 4); 290 | // LONG DATES 291 | case 'L' : 292 | case 'LL' : 293 | case 'LLL' : 294 | case 'LLLL' : 295 | case 'LT' : 296 | return formatMoment(m, moment.longDateFormat[input]); 297 | // DEFAULT 298 | default : 299 | return input.replace(/(^\[)|(\\)|\]$/g, ""); 300 | } 301 | } 302 | return inputString.replace(formattingTokens, replaceFunction); 303 | } 304 | 305 | // get the regex to find the next token 306 | function getParseRegexForToken(token) { 307 | switch (token) { 308 | case 'DDDD': 309 | return parseTokenThreeDigits; 310 | case 'YYYY': 311 | return parseTokenFourDigits; 312 | case 'S': 313 | case 'SS': 314 | case 'SSS': 315 | case 'DDD': 316 | return parseTokenOneToThreeDigits; 317 | case 'MMM': 318 | case 'MMMM': 319 | case 'ddd': 320 | case 'dddd': 321 | case 'a': 322 | case 'A': 323 | return parseTokenWord; 324 | case 'Z': 325 | case 'ZZ': 326 | return parseTokenTimezone; 327 | case 'T': 328 | return parseTokenT; 329 | case 'MM': 330 | case 'DD': 331 | case 'dd': 332 | case 'YY': 333 | case 'HH': 334 | case 'hh': 335 | case 'mm': 336 | case 'ss': 337 | case 'M': 338 | case 'D': 339 | case 'd': 340 | case 'H': 341 | case 'h': 342 | case 'm': 343 | case 's': 344 | return parseTokenOneOrTwoDigits; 345 | default : 346 | return new RegExp(token.replace('\\', '')); 347 | } 348 | } 349 | 350 | // function to convert string input to date 351 | function addTimeToArrayFromToken(token, input, datePartArray, config) { 352 | var a; 353 | //console.log('addTime', format, input); 354 | switch (token) { 355 | // MONTH 356 | case 'M' : // fall through to MM 357 | case 'MM' : 358 | datePartArray[1] = (input == null) ? 0 : ~~input - 1; 359 | break; 360 | case 'MMM' : // fall through to MMMM 361 | case 'MMMM' : 362 | for (a = 0; a < 12; a++) { 363 | if (moment.monthsParse[a].test(input)) { 364 | datePartArray[1] = a; 365 | break; 366 | } 367 | } 368 | break; 369 | // DAY OF MONTH 370 | case 'D' : // fall through to DDDD 371 | case 'DD' : // fall through to DDDD 372 | case 'DDD' : // fall through to DDDD 373 | case 'DDDD' : 374 | datePartArray[2] = ~~input; 375 | break; 376 | // YEAR 377 | case 'YY' : 378 | input = ~~input; 379 | datePartArray[0] = input + (input > 70 ? 1900 : 2000); 380 | break; 381 | case 'YYYY' : 382 | datePartArray[0] = ~~Math.abs(input); 383 | break; 384 | // AM / PM 385 | case 'a' : // fall through to A 386 | case 'A' : 387 | config.isPm = ((input + '').toLowerCase() === 'pm'); 388 | break; 389 | // 24 HOUR 390 | case 'H' : // fall through to hh 391 | case 'HH' : // fall through to hh 392 | case 'h' : // fall through to hh 393 | case 'hh' : 394 | datePartArray[3] = ~~input; 395 | break; 396 | // MINUTE 397 | case 'm' : // fall through to mm 398 | case 'mm' : 399 | datePartArray[4] = ~~input; 400 | break; 401 | // SECOND 402 | case 's' : // fall through to ss 403 | case 'ss' : 404 | datePartArray[5] = ~~input; 405 | break; 406 | // MILLISECOND 407 | case 'S' : 408 | case 'SS' : 409 | case 'SSS' : 410 | datePartArray[6] = ~~ (('0.' + input) * 1000); 411 | break; 412 | // TIMEZONE 413 | case 'Z' : // fall through to ZZ 414 | case 'ZZ' : 415 | config.isUTC = true; 416 | a = (input + '').match(parseTimezoneChunker); 417 | if (a && a[1]) { 418 | config.tzh = ~~a[1]; 419 | } 420 | if (a && a[2]) { 421 | config.tzm = ~~a[2]; 422 | } 423 | // reverse offsets 424 | if (a && a[0] === '+') { 425 | config.tzh = -config.tzh; 426 | config.tzm = -config.tzm; 427 | } 428 | break; 429 | } 430 | } 431 | 432 | // date from string and format string 433 | function makeDateFromStringAndFormat(string, format) { 434 | var datePartArray = [0, 0, 1, 0, 0, 0, 0], 435 | config = { 436 | tzh : 0, // timezone hour offset 437 | tzm : 0 // timezone minute offset 438 | }, 439 | tokens = format.match(formattingTokens), 440 | i, parsedInput; 441 | 442 | for (i = 0; i < tokens.length; i++) { 443 | parsedInput = (getParseRegexForToken(tokens[i]).exec(string) || [])[0]; 444 | string = string.replace(getParseRegexForToken(tokens[i]), ''); 445 | addTimeToArrayFromToken(tokens[i], parsedInput, datePartArray, config); 446 | } 447 | // handle am pm 448 | if (config.isPm && datePartArray[3] < 12) { 449 | datePartArray[3] += 12; 450 | } 451 | // if is 12 am, change hours to 0 452 | if (config.isPm === false && datePartArray[3] === 12) { 453 | datePartArray[3] = 0; 454 | } 455 | // handle timezone 456 | datePartArray[3] += config.tzh; 457 | datePartArray[4] += config.tzm; 458 | // return 459 | return config.isUTC ? new Date(Date.UTC.apply({}, datePartArray)) : dateFromArray(datePartArray); 460 | } 461 | 462 | // compare two arrays, return the number of differences 463 | function compareArrays(array1, array2) { 464 | var len = Math.min(array1.length, array2.length), 465 | lengthDiff = Math.abs(array1.length - array2.length), 466 | diffs = 0, 467 | i; 468 | for (i = 0; i < len; i++) { 469 | if (~~array1[i] !== ~~array2[i]) { 470 | diffs++; 471 | } 472 | } 473 | return diffs + lengthDiff; 474 | } 475 | 476 | // date from string and array of format strings 477 | function makeDateFromStringAndArray(string, formats) { 478 | var output, 479 | inputParts = string.match(parseMultipleFormatChunker) || [], 480 | formattedInputParts, 481 | scoreToBeat = 99, 482 | i, 483 | currentDate, 484 | currentScore; 485 | for (i = 0; i < formats.length; i++) { 486 | currentDate = makeDateFromStringAndFormat(string, formats[i]); 487 | formattedInputParts = formatMoment(new Moment(currentDate), formats[i]).match(parseMultipleFormatChunker) || []; 488 | currentScore = compareArrays(inputParts, formattedInputParts); 489 | if (currentScore < scoreToBeat) { 490 | scoreToBeat = currentScore; 491 | output = currentDate; 492 | } 493 | } 494 | return output; 495 | } 496 | 497 | // date from iso format 498 | function makeDateFromString(string) { 499 | var format = 'YYYY-MM-DDT', 500 | i; 501 | if (isoRegex.exec(string)) { 502 | for (i = 0; i < 4; i++) { 503 | if (isoTimes[i][1].exec(string)) { 504 | format += isoTimes[i][0]; 505 | break; 506 | } 507 | } 508 | return parseTokenTimezone.exec(string) ? 509 | makeDateFromStringAndFormat(string, format + ' Z') : 510 | makeDateFromStringAndFormat(string, format); 511 | } 512 | return new Date(string); 513 | } 514 | 515 | // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize 516 | function substituteTimeAgo(string, number, withoutSuffix, isFuture) { 517 | var rt = moment.relativeTime[string]; 518 | return (typeof rt === 'function') ? 519 | rt(number || 1, !!withoutSuffix, string, isFuture) : 520 | rt.replace(/%d/i, number || 1); 521 | } 522 | 523 | function relativeTime(milliseconds, withoutSuffix) { 524 | var seconds = round(Math.abs(milliseconds) / 1000), 525 | minutes = round(seconds / 60), 526 | hours = round(minutes / 60), 527 | days = round(hours / 24), 528 | years = round(days / 365), 529 | args = seconds < 45 && ['s', seconds] || 530 | minutes === 1 && ['m'] || 531 | minutes < 45 && ['mm', minutes] || 532 | hours === 1 && ['h'] || 533 | hours < 22 && ['hh', hours] || 534 | days === 1 && ['d'] || 535 | days <= 25 && ['dd', days] || 536 | days <= 45 && ['M'] || 537 | days < 345 && ['MM', round(days / 30)] || 538 | years === 1 && ['y'] || ['yy', years]; 539 | args[2] = withoutSuffix; 540 | args[3] = milliseconds > 0; 541 | return substituteTimeAgo.apply({}, args); 542 | } 543 | 544 | moment = function (input, format) { 545 | if (input === null || input === '') { 546 | return null; 547 | } 548 | var date, 549 | matched, 550 | isUTC; 551 | // parse Moment object 552 | if (moment.isMoment(input)) { 553 | date = new Date(+input._d); 554 | isUTC = input._isUTC; 555 | // parse string and format 556 | } else if (format) { 557 | if (isArray(format)) { 558 | date = makeDateFromStringAndArray(input, format); 559 | } else { 560 | date = makeDateFromStringAndFormat(input, format); 561 | } 562 | // evaluate it as a JSON-encoded date 563 | } else { 564 | matched = aspNetJsonRegex.exec(input); 565 | date = input === undefined ? new Date() : 566 | matched ? new Date(+matched[1]) : 567 | input instanceof Date ? input : 568 | isArray(input) ? dateFromArray(input) : 569 | typeof input === 'string' ? makeDateFromString(input) : 570 | new Date(input); 571 | } 572 | return new Moment(date, isUTC); 573 | }; 574 | 575 | // creating with utc 576 | moment.utc = function (input, format) { 577 | if (isArray(input)) { 578 | return new Moment(new Date(Date.UTC.apply({}, input)), true); 579 | } 580 | return (format && input) ? 581 | moment(input + ' +0000', format + ' Z').utc() : 582 | moment(input && !parseTokenTimezone.exec(input) ? input + '+0000' : input).utc(); 583 | }; 584 | 585 | // creating with unix timestamp (in seconds) 586 | moment.unix = function (input) { 587 | return moment(input * 1000); 588 | }; 589 | 590 | // duration 591 | moment.duration = function (input, key) { 592 | var isDuration = moment.isDuration(input), 593 | isNumber = (typeof input === 'number'), 594 | duration = (isDuration ? input._data : (isNumber ? {} : input)); 595 | 596 | if (isNumber) { 597 | if (key) { 598 | duration[key] = input; 599 | } else { 600 | duration.milliseconds = input; 601 | } 602 | } 603 | 604 | return new Duration(duration); 605 | }; 606 | 607 | // humanizeDuration 608 | // This method is deprecated in favor of the new Duration object. Please 609 | // see the moment.duration method. 610 | moment.humanizeDuration = function (num, type, withSuffix) { 611 | return moment.duration(num, type === true ? null : type).humanize(type === true ? true : withSuffix); 612 | }; 613 | 614 | // version number 615 | moment.version = VERSION; 616 | 617 | // default format 618 | moment.defaultFormat = isoFormat; 619 | 620 | // language switching and caching 621 | moment.lang = function (key, values) { 622 | var i, req, 623 | parse = []; 624 | if (!key) { 625 | return currentLanguage; 626 | } 627 | if (values) { 628 | for (i = 0; i < 12; i++) { 629 | parse[i] = new RegExp('^' + values.months[i] + '|^' + values.monthsShort[i].replace('.', ''), 'i'); 630 | } 631 | values.monthsParse = values.monthsParse || parse; 632 | languages[key] = values; 633 | } 634 | if (languages[key]) { 635 | for (i = 0; i < langConfigProperties.length; i++) { 636 | moment[langConfigProperties[i]] = languages[key][langConfigProperties[i]] || 637 | languages.en[langConfigProperties[i]]; 638 | } 639 | currentLanguage = key; 640 | } else { 641 | if (hasModule) { 642 | req = require('./lang/' + key); 643 | moment.lang(key, req); 644 | } 645 | } 646 | }; 647 | 648 | // set default language 649 | moment.lang('en', { 650 | months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), 651 | monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"), 652 | weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), 653 | weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"), 654 | longDateFormat : { 655 | LT : "h:mm A", 656 | L : "MM/DD/YYYY", 657 | LL : "MMMM D YYYY", 658 | LLL : "MMMM D YYYY LT", 659 | LLLL : "dddd, MMMM D YYYY LT" 660 | }, 661 | meridiem : false, 662 | calendar : { 663 | sameDay : '[Today at] LT', 664 | nextDay : '[Tomorrow at] LT', 665 | nextWeek : 'dddd [at] LT', 666 | lastDay : '[Yesterday at] LT', 667 | lastWeek : '[last] dddd [at] LT', 668 | sameElse : 'L' 669 | }, 670 | relativeTime : { 671 | future : "in %s", 672 | past : "%s ago", 673 | s : "a few seconds", 674 | m : "a minute", 675 | mm : "%d minutes", 676 | h : "an hour", 677 | hh : "%d hours", 678 | d : "a day", 679 | dd : "%d days", 680 | M : "a month", 681 | MM : "%d months", 682 | y : "a year", 683 | yy : "%d years" 684 | }, 685 | ordinal : function (number) { 686 | var b = number % 10; 687 | return (~~ (number % 100 / 10) === 1) ? 'th' : 688 | (b === 1) ? 'st' : 689 | (b === 2) ? 'nd' : 690 | (b === 3) ? 'rd' : 'th'; 691 | } 692 | }); 693 | 694 | // compare moment object 695 | moment.isMoment = function (obj) { 696 | return obj instanceof Moment; 697 | }; 698 | 699 | // for typechecking Duration objects 700 | moment.isDuration = function (obj) { 701 | return obj instanceof Duration; 702 | }; 703 | 704 | // shortcut for prototype 705 | moment.fn = Moment.prototype = { 706 | 707 | clone : function () { 708 | return moment(this); 709 | }, 710 | 711 | valueOf : function () { 712 | return +this._d; 713 | }, 714 | 715 | unix : function () { 716 | return Math.floor(+this._d / 1000); 717 | }, 718 | 719 | toString : function () { 720 | return this._d.toString(); 721 | }, 722 | 723 | toDate : function () { 724 | return this._d; 725 | }, 726 | 727 | utc : function () { 728 | this._isUTC = true; 729 | return this; 730 | }, 731 | 732 | local : function () { 733 | this._isUTC = false; 734 | return this; 735 | }, 736 | 737 | format : function (inputString) { 738 | return formatMoment(this, inputString ? inputString : moment.defaultFormat); 739 | }, 740 | 741 | add : function (input, val) { 742 | var dur = val ? moment.duration(+val, input) : moment.duration(input); 743 | addOrSubtractDurationFromMoment(this, dur, 1); 744 | return this; 745 | }, 746 | 747 | subtract : function (input, val) { 748 | var dur = val ? moment.duration(+val, input) : moment.duration(input); 749 | addOrSubtractDurationFromMoment(this, dur, -1); 750 | return this; 751 | }, 752 | 753 | diff : function (input, val, asFloat) { 754 | var inputMoment = this._isUTC ? moment(input).utc() : moment(input).local(), 755 | zoneDiff = (this.zone() - inputMoment.zone()) * 6e4, 756 | diff = this._d - inputMoment._d - zoneDiff, 757 | year = this.year() - inputMoment.year(), 758 | month = this.month() - inputMoment.month(), 759 | date = this.date() - inputMoment.date(), 760 | output; 761 | if (val === 'months') { 762 | output = year * 12 + month + date / 30; 763 | } else if (val === 'years') { 764 | output = year + (month + date / 30) / 12; 765 | } else { 766 | output = val === 'seconds' ? diff / 1e3 : // 1000 767 | val === 'minutes' ? diff / 6e4 : // 1000 * 60 768 | val === 'hours' ? diff / 36e5 : // 1000 * 60 * 60 769 | val === 'days' ? diff / 864e5 : // 1000 * 60 * 60 * 24 770 | val === 'weeks' ? diff / 6048e5 : // 1000 * 60 * 60 * 24 * 7 771 | diff; 772 | } 773 | return asFloat ? output : round(output); 774 | }, 775 | 776 | from : function (time, withoutSuffix) { 777 | return moment.duration(this.diff(time)).humanize(!withoutSuffix); 778 | }, 779 | 780 | fromNow : function (withoutSuffix) { 781 | return this.from(moment(), withoutSuffix); 782 | }, 783 | 784 | calendar : function () { 785 | var diff = this.diff(moment().sod(), 'days', true), 786 | calendar = moment.calendar, 787 | allElse = calendar.sameElse, 788 | format = diff < -6 ? allElse : 789 | diff < -1 ? calendar.lastWeek : 790 | diff < 0 ? calendar.lastDay : 791 | diff < 1 ? calendar.sameDay : 792 | diff < 2 ? calendar.nextDay : 793 | diff < 7 ? calendar.nextWeek : allElse; 794 | return this.format(typeof format === 'function' ? format.apply(this) : format); 795 | }, 796 | 797 | isLeapYear : function () { 798 | var year = this.year(); 799 | return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; 800 | }, 801 | 802 | isDST : function () { 803 | return (this.zone() < moment([this.year()]).zone() || 804 | this.zone() < moment([this.year(), 5]).zone()); 805 | }, 806 | 807 | day : function (input) { 808 | var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); 809 | return input == null ? day : 810 | this.add({ d : input - day }); 811 | }, 812 | 813 | sod: function () { 814 | return moment(this) 815 | .hours(0) 816 | .minutes(0) 817 | .seconds(0) 818 | .milliseconds(0); 819 | }, 820 | 821 | eod: function () { 822 | // end of day = start of day plus 1 day, minus 1 millisecond 823 | return this.sod().add({ 824 | d : 1, 825 | ms : -1 826 | }); 827 | }, 828 | 829 | zone : function () { 830 | return this._isUTC ? 0 : this._d.getTimezoneOffset(); 831 | }, 832 | 833 | daysInMonth : function () { 834 | return moment(this).month(this.month() + 1).date(0).date(); 835 | } 836 | }; 837 | 838 | // helper for adding shortcuts 839 | function makeGetterAndSetter(name, key) { 840 | moment.fn[name] = function (input) { 841 | var utc = this._isUTC ? 'UTC' : ''; 842 | if (input != null) { 843 | this._d['set' + utc + key](input); 844 | return this; 845 | } else { 846 | return this._d['get' + utc + key](); 847 | } 848 | }; 849 | } 850 | 851 | // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds) 852 | for (i = 0; i < proxyGettersAndSetters.length; i ++) { 853 | makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase(), proxyGettersAndSetters[i]); 854 | } 855 | 856 | // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear') 857 | makeGetterAndSetter('year', 'FullYear'); 858 | 859 | moment.duration.fn = Duration.prototype = { 860 | weeks : function () { 861 | return absRound(this.days() / 7); 862 | }, 863 | 864 | valueOf : function () { 865 | return this._milliseconds + 866 | this._days * 864e5 + 867 | this._months * 2592e6; 868 | }, 869 | 870 | humanize : function (withSuffix) { 871 | var difference = +this, 872 | rel = moment.relativeTime, 873 | output = relativeTime(difference, !withSuffix); 874 | 875 | if (withSuffix) { 876 | output = (difference <= 0 ? rel.past : rel.future).replace(/%s/i, output); 877 | } 878 | 879 | return output; 880 | } 881 | }; 882 | 883 | function makeDurationGetter(name) { 884 | moment.duration.fn[name] = function () { 885 | return this._data[name]; 886 | }; 887 | } 888 | 889 | function makeDurationAsGetter(name, factor) { 890 | moment.duration.fn['as' + name] = function () { 891 | return +this / factor; 892 | }; 893 | } 894 | 895 | for (i in unitMillisecondFactors) { 896 | if (unitMillisecondFactors.hasOwnProperty(i)) { 897 | makeDurationAsGetter(i, unitMillisecondFactors[i]); 898 | makeDurationGetter(i.toLowerCase()); 899 | } 900 | } 901 | 902 | makeDurationAsGetter('Weeks', 6048e5); 903 | 904 | // CommonJS module is defined 905 | if (hasModule) { 906 | module.exports = moment; 907 | } 908 | /*global ender:false */ 909 | if (typeof window !== 'undefined' && typeof ender === 'undefined') { 910 | window.moment = moment; 911 | } 912 | /*global define:false */ 913 | if (typeof define === "function" && define.amd) { 914 | define("moment", [], function () { 915 | return moment; 916 | }); 917 | } 918 | })(Date); 919 | -------------------------------------------------------------------------------- /app/vendor/jquery.couch.js: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 | // use this file except in compliance with the License. You may obtain a copy of 3 | // the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | // License for the specific language governing permissions and limitations under 11 | // the License. 12 | 13 | /** 14 | * @namespace 15 | * $.couch is used to communicate with a CouchDB server, the server methods can 16 | * be called directly without creating an instance. Typically all methods are 17 | * passed an options object which defines a success callback which 18 | * is called with the data returned from the http request to CouchDB, you can 19 | * find the other settings that can be used in the options object 20 | * from 21 | * jQuery.ajax settings 22 | *
$.couch.activeTasks({
  23 |  *   success: function (data) {
  24 |  *     console.log(data);
  25 |  *   }
  26 |  * });
27 | * Outputs (for example): 28 | *
[
  29 |  *  {
  30 |  *   "pid" : "<0.11599.0>",
  31 |  *   "status" : "Copied 0 of 18369 changes (0%)",
  32 |  *   "task" : "recipes",
  33 |  *   "type" : "Database Compaction"
  34 |  *  }
  35 |  *]
36 | */ 37 | (function($) { 38 | 39 | $.couch = $.couch || {}; 40 | /** @lends $.couch */ 41 | 42 | /** 43 | * @private 44 | */ 45 | function encodeDocId(docID) { 46 | var parts = docID.split("/"); 47 | if (parts[0] == "_design") { 48 | parts.shift(); 49 | return "_design/" + encodeURIComponent(parts.join('/')); 50 | } 51 | return encodeURIComponent(docID); 52 | } 53 | 54 | /** 55 | * @private 56 | */ 57 | 58 | var uuidCache = []; 59 | 60 | $.extend($.couch, { 61 | urlPrefix: '', 62 | 63 | /** 64 | * You can obtain a list of active tasks by using the /_active_tasks URL. 65 | * The result is a JSON array of the currently running tasks, with each task 66 | * being described with a single object. 67 | * @see docs for /_active_tasks 70 | * @param {ajaxSettings} options jQuery ajax settings 72 | */ 73 | activeTasks: function(options) { 74 | ajax( 75 | {url: this.urlPrefix + "/_active_tasks"}, 76 | options, 77 | "Active task status could not be retrieved" 78 | ); 79 | }, 80 | 81 | /** 82 | * Returns a list of all the databases in the CouchDB instance 83 | * @see docs for /_all_dbs 86 | * @param {ajaxSettings} options jQuery ajax settings 88 | */ 89 | allDbs: function(options) { 90 | ajax( 91 | {url: this.urlPrefix + "/_all_dbs"}, 92 | options, 93 | "An error occurred retrieving the list of all databases" 94 | ); 95 | }, 96 | 97 | /** 98 | * View and edit the CouchDB configuration, called with just the options 99 | * parameter the entire config is returned, you can be more specific by 100 | * passing the section and option parameters, if you specify a value that 101 | * value will be stored in the configuration. 102 | * @see docs for /_config 105 | * @param {ajaxSettings} options 106 | * 107 | * jQuery ajax settings 108 | * @param {String} [section] the section of the config 109 | * @param {String} [option] the particular config option 110 | * @param {String} [value] value to be set 111 | */ 112 | config: function(options, section, option, value) { 113 | var req = {url: this.urlPrefix + "/_config/"}; 114 | if (section) { 115 | req.url += encodeURIComponent(section) + "/"; 116 | if (option) { 117 | req.url += encodeURIComponent(option); 118 | } 119 | } 120 | if (value === null) { 121 | req.type = "DELETE"; 122 | } else if (value !== undefined) { 123 | req.type = "PUT"; 124 | req.data = toJSON(value); 125 | req.contentType = "application/json"; 126 | req.processData = false 127 | } 128 | 129 | ajax(req, options, 130 | "An error occurred retrieving/updating the server configuration" 131 | ); 132 | }, 133 | 134 | /** 135 | * Returns the session information for the currently logged in user. 136 | * @param {ajaxSettings} options 137 | * 138 | * jQuery ajax settings 139 | */ 140 | session: function(options) { 141 | options = options || {}; 142 | $.ajax({ 143 | type: "GET", url: this.urlPrefix + "/_session", 144 | beforeSend: function(xhr) { 145 | xhr.setRequestHeader('Accept', 'application/json'); 146 | }, 147 | complete: function(req) { 148 | var resp = $.parseJSON(req.responseText); 149 | if (req.status == 200) { 150 | if (options.success) options.success(resp); 151 | } else if (options.error) { 152 | options.error(req.status, resp.error, resp.reason); 153 | } else { 154 | throw "An error occurred getting session info: " + resp.reason; 155 | } 156 | } 157 | }); 158 | }, 159 | 160 | /** 161 | * @private 162 | */ 163 | userDb : function(callback) { 164 | $.couch.session({ 165 | success : function(resp) { 166 | var userDb = $.couch.db(resp.info.authentication_db); 167 | callback(userDb); 168 | } 169 | }); 170 | }, 171 | 172 | /** 173 | * Create a new user on the CouchDB server, user_doc is an 174 | * object with a name field and other information you want 175 | * to store relating to that user, for example 176 | * {"name": "daleharvey"} 177 | * @param {Object} user_doc Users details 178 | * @param {String} password Users password 179 | * @param {ajaxSettings} options 180 | * 181 | * jQuery ajax settings 182 | */ 183 | signup: function(user_doc, password, options) { 184 | options = options || {}; 185 | user_doc.password = password; 186 | user_doc.roles = user_doc.roles || []; 187 | user_doc.type = user_doc.type = "user" || []; 188 | var user_prefix = "org.couchdb.user:"; 189 | user_doc._id = user_doc._id || user_prefix + user_doc.name; 190 | 191 | $.couch.userDb(function(db) { 192 | db.saveDoc(user_doc, options); 193 | }); 194 | }, 195 | 196 | /** 197 | * Authenticate against CouchDB, the options parameter is 198 | *expected to have name and password fields. 199 | * @param {ajaxSettings} options 200 | * 201 | * jQuery ajax settings 202 | */ 203 | login: function(options) { 204 | options = options || {}; 205 | $.ajax({ 206 | type: "POST", url: this.urlPrefix + "/_session", dataType: "json", 207 | data: {name: options.name, password: options.password}, 208 | beforeSend: function(xhr) { 209 | xhr.setRequestHeader('Accept', 'application/json'); 210 | }, 211 | complete: function(req) { 212 | var resp = $.parseJSON(req.responseText); 213 | if (req.status == 200) { 214 | if (options.success) options.success(resp); 215 | } else if (options.error) { 216 | options.error(req.status, resp.error, resp.reason); 217 | } else { 218 | throw 'An error occurred logging in: ' + resp.reason; 219 | } 220 | } 221 | }); 222 | }, 223 | 224 | 225 | /** 226 | * Delete your current CouchDB user session 227 | * @param {ajaxSettings} options 228 | * 229 | * jQuery ajax settings 230 | */ 231 | logout: function(options) { 232 | options = options || {}; 233 | $.ajax({ 234 | type: "DELETE", url: this.urlPrefix + "/_session", dataType: "json", 235 | username : "_", password : "_", 236 | beforeSend: function(xhr) { 237 | xhr.setRequestHeader('Accept', 'application/json'); 238 | }, 239 | complete: function(req) { 240 | var resp = $.parseJSON(req.responseText); 241 | if (req.status == 200) { 242 | if (options.success) options.success(resp); 243 | } else if (options.error) { 244 | options.error(req.status, resp.error, resp.reason); 245 | } else { 246 | throw 'An error occurred logging out: ' + resp.reason; 247 | } 248 | } 249 | }); 250 | }, 251 | 252 | /** 253 | * @namespace 254 | * $.couch.db is used to communicate with a specific CouchDB database 255 | *
var $db = $.couch.db("mydatabase");
 256 |      *$db.allApps({
 257 |      *  success: function (data) {
 258 |      *    ... process data ...
 259 |      *  }
 260 |      *});
 261 |      * 
262 | */ 263 | db: function(name, db_opts) { 264 | db_opts = db_opts || {}; 265 | var rawDocs = {}; 266 | function maybeApplyVersion(doc) { 267 | if (doc._id && doc._rev && rawDocs[doc._id] && 268 | rawDocs[doc._id].rev == doc._rev) { 269 | // todo: can we use commonjs require here? 270 | if (typeof Base64 == "undefined") { 271 | throw 'Base64 support not found.'; 272 | } else { 273 | doc._attachments = doc._attachments || {}; 274 | doc._attachments["rev-"+doc._rev.split("-")[0]] = { 275 | content_type :"application/json", 276 | data : Base64.encode(rawDocs[doc._id].raw) 277 | }; 278 | return true; 279 | } 280 | } 281 | }; 282 | return /** @lends $.couch.db */{ 283 | name: name, 284 | uri: this.urlPrefix + "/" + encodeURIComponent(name) + "/", 285 | 286 | /** 287 | * Request compaction of the specified database. 288 | * @see docs for /db/_compact 291 | * @param {ajaxSettings} options 292 | * 293 | * jQuery ajax settings 294 | */ 295 | compact: function(options) { 296 | $.extend(options, {successStatus: 202}); 297 | ajax({ 298 | type: "POST", url: this.uri + "_compact", 299 | data: "", processData: false 300 | }, 301 | options, 302 | "The database could not be compacted" 303 | ); 304 | }, 305 | 306 | /** 307 | * Cleans up the cached view output on disk for a given view. 308 | * @see docs for /db/_compact 311 | * @param {ajaxSettings} options jQuery ajax settings 313 | */ 314 | viewCleanup: function(options) { 315 | $.extend(options, {successStatus: 202}); 316 | ajax({ 317 | type: "POST", url: this.uri + "_view_cleanup", 318 | data: "", processData: false 319 | }, 320 | options, 321 | "The views could not be cleaned up" 322 | ); 323 | }, 324 | 325 | /** 326 | * Compacts the view indexes associated with the specified design 327 | * document. You can use this in place of the full database compaction 328 | * if you know a specific set of view indexes have been affected by a 329 | * recent database change. 330 | * @see docs for /db/_compact/design-doc 333 | * @param {String} groupname Name of design-doc to compact 334 | * @param {ajaxSettings} options jQuery ajax settings 336 | */ 337 | compactView: function(groupname, options) { 338 | $.extend(options, {successStatus: 202}); 339 | ajax({ 340 | type: "POST", url: this.uri + "_compact/" + groupname, 341 | data: "", processData: false 342 | }, 343 | options, 344 | "The view could not be compacted" 345 | ); 346 | }, 347 | 348 | /** 349 | * Create a new database 350 | * @see docs for PUT /db/ 353 | * @param {ajaxSettings} options jQuery ajax settings 355 | */ 356 | create: function(options) { 357 | $.extend(options, {successStatus: 201}); 358 | ajax({ 359 | type: "PUT", url: this.uri, contentType: "application/json", 360 | data: "", processData: false 361 | }, 362 | options, 363 | "The database could not be created" 364 | ); 365 | }, 366 | 367 | /** 368 | * Deletes the specified database, and all the documents and 369 | * attachments contained within it. 370 | * @see docs for DELETE /db/ 373 | * @param {ajaxSettings} options jQuery ajax settings 375 | */ 376 | drop: function(options) { 377 | ajax( 378 | {type: "DELETE", url: this.uri}, 379 | options, 380 | "The database could not be deleted" 381 | ); 382 | }, 383 | 384 | /** 385 | * Gets information about the specified database. 386 | * @see docs for GET /db/ 389 | * @param {ajaxSettings} options jQuery ajax settings 391 | */ 392 | info: function(options) { 393 | ajax( 394 | {url: this.uri}, 395 | options, 396 | "Database information could not be retrieved" 397 | ); 398 | }, 399 | 400 | /** 401 | * @namespace 402 | * $.couch.db.changes provides an API for subscribing to the changes 403 | * feed 404 | *
var $changes = $.couch.db("mydatabase").changes();
 405 |          *$changes.onChange = function (data) {
 406 |          *    ... process data ...
 407 |          * }
 408 |          * $changes.stop();
 409 |          * 
410 | */ 411 | changes: function(since, options) { 412 | 413 | options = options || {}; 414 | // set up the promise object within a closure for this handler 415 | var timeout = 100, db = this, active = true, 416 | listeners = [], 417 | promise = /** @lends $.couch.db.changes */ { 418 | /** 419 | * Add a listener callback 420 | * @see docs for /db/_changes 423 | * @param {Function} fun Callback function to run when 424 | * notified of changes. 425 | */ 426 | onChange : function(fun) { 427 | listeners.push(fun); 428 | }, 429 | /** 430 | * Stop subscribing to the changes feed 431 | */ 432 | stop : function() { 433 | active = false; 434 | } 435 | }; 436 | // call each listener when there is a change 437 | function triggerListeners(resp) { 438 | $.each(listeners, function() { 439 | this(resp); 440 | }); 441 | }; 442 | // when there is a change, call any listeners, then check for 443 | // another change 444 | options.success = function(resp) { 445 | timeout = 100; 446 | if (active) { 447 | since = resp.last_seq; 448 | triggerListeners(resp); 449 | getChangesSince(); 450 | }; 451 | }; 452 | options.error = function() { 453 | if (active) { 454 | setTimeout(getChangesSince, timeout); 455 | timeout = timeout * 2; 456 | } 457 | }; 458 | // actually make the changes request 459 | function getChangesSince() { 460 | var opts = $.extend({heartbeat : 10 * 1000}, options, { 461 | feed : "longpoll", 462 | since : since 463 | }); 464 | ajax( 465 | {url: db.uri + "_changes"+encodeOptions(opts)}, 466 | options, 467 | "Error connecting to "+db.uri+"/_changes." 468 | ); 469 | } 470 | // start the first request 471 | if (since) { 472 | getChangesSince(); 473 | } else { 474 | db.info({ 475 | success : function(info) { 476 | since = info.update_seq; 477 | getChangesSince(); 478 | } 479 | }); 480 | } 481 | return promise; 482 | }, 483 | 484 | /** 485 | * Fetch all the docs in this db, you can specify an array of keys to 486 | * fetch by passing the keys field in the 487 | * options 488 | * parameter. 489 | * @see docs for /db/all_docs/ 492 | * @param {ajaxSettings} options jQuery ajax settings 494 | */ 495 | allDocs: function(options) { 496 | var type = "GET"; 497 | var data = null; 498 | if (options["keys"]) { 499 | type = "POST"; 500 | var keys = options["keys"]; 501 | delete options["keys"]; 502 | data = toJSON({ "keys": keys }); 503 | } 504 | ajax({ 505 | type: type, 506 | data: data, 507 | url: this.uri + "_all_docs" + encodeOptions(options) 508 | }, 509 | options, 510 | "An error occurred retrieving a list of all documents" 511 | ); 512 | }, 513 | 514 | /** 515 | * Fetch all the design docs in this db 516 | * @param {ajaxSettings} options jQuery ajax settings 518 | */ 519 | allDesignDocs: function(options) { 520 | this.allDocs($.extend( 521 | {startkey:"_design", endkey:"_design0"}, options)); 522 | }, 523 | 524 | /** 525 | * Fetch all the design docs with an index.html, options 526 | * parameter expects an eachApp field which is a callback 527 | * called on each app found. 528 | * @param {ajaxSettings} options jQuery ajax settings 530 | */ 531 | allApps: function(options) { 532 | options = options || {}; 533 | var self = this; 534 | if (options.eachApp) { 535 | this.allDesignDocs({ 536 | success: function(resp) { 537 | $.each(resp.rows, function() { 538 | self.openDoc(this.id, { 539 | success: function(ddoc) { 540 | var index, appPath, appName = ddoc._id.split('/'); 541 | appName.shift(); 542 | appName = appName.join('/'); 543 | index = ddoc.couchapp && ddoc.couchapp.index; 544 | if (index) { 545 | appPath = ['', name, ddoc._id, index].join('/'); 546 | } else if (ddoc._attachments && 547 | ddoc._attachments["index.html"]) { 548 | appPath = ['', name, ddoc._id, "index.html"].join('/'); 549 | } 550 | if (appPath) options.eachApp(appName, appPath, ddoc); 551 | } 552 | }); 553 | }); 554 | } 555 | }); 556 | } else { 557 | throw 'Please provide an eachApp function for allApps()'; 558 | } 559 | }, 560 | 561 | /** 562 | * Returns the specified doc from the specified db. 563 | * @see docs for GET /db/doc 566 | * @param {String} docId id of document to fetch 567 | * @param {ajaxSettings} options jQuery ajax settings 569 | * @param {ajaxSettings} ajaxOptions jQuery ajax settings 571 | */ 572 | openDoc: function(docId, options, ajaxOptions) { 573 | options = options || {}; 574 | if (db_opts.attachPrevRev || options.attachPrevRev) { 575 | $.extend(options, { 576 | beforeSuccess : function(req, doc) { 577 | rawDocs[doc._id] = { 578 | rev : doc._rev, 579 | raw : req.responseText 580 | }; 581 | } 582 | }); 583 | } else { 584 | $.extend(options, { 585 | beforeSuccess : function(req, doc) { 586 | if (doc["jquery.couch.attachPrevRev"]) { 587 | rawDocs[doc._id] = { 588 | rev : doc._rev, 589 | raw : req.responseText 590 | }; 591 | } 592 | } 593 | }); 594 | } 595 | ajax({url: this.uri + encodeDocId(docId) + encodeOptions(options)}, 596 | options, 597 | "The document could not be retrieved", 598 | ajaxOptions 599 | ); 600 | }, 601 | 602 | /** 603 | * Create a new document in the specified database, using the supplied 604 | * JSON document structure. If the JSON structure includes the _id 605 | * field, then the document will be created with the specified document 606 | * ID. If the _id field is not specified, a new unique ID will be 607 | * generated. 608 | * @see docs for GET /db/doc 611 | * @param {String} doc document to save 612 | * @param {ajaxSettings} options jQuery ajax settings 614 | */ 615 | saveDoc: function(doc, options) { 616 | options = options || {}; 617 | var db = this; 618 | var beforeSend = fullCommit(options); 619 | if (doc._id === undefined) { 620 | var method = "POST"; 621 | var uri = this.uri; 622 | } else { 623 | var method = "PUT"; 624 | var uri = this.uri + encodeDocId(doc._id); 625 | } 626 | var versioned = maybeApplyVersion(doc); 627 | $.ajax({ 628 | type: method, url: uri + encodeOptions(options), 629 | contentType: "application/json", 630 | dataType: "json", data: toJSON(doc), 631 | beforeSend : beforeSend, 632 | complete: function(req) { 633 | var resp = $.parseJSON(req.responseText); 634 | if (req.status == 200 || req.status == 201 || req.status == 202) { 635 | doc._id = resp.id; 636 | doc._rev = resp.rev; 637 | if (versioned) { 638 | db.openDoc(doc._id, { 639 | attachPrevRev : true, 640 | success : function(d) { 641 | doc._attachments = d._attachments; 642 | if (options.success) options.success(resp); 643 | } 644 | }); 645 | } else { 646 | if (options.success) options.success(resp); 647 | } 648 | } else if (options.error) { 649 | options.error(req.status, resp.error, resp.reason); 650 | } else { 651 | throw "The document could not be saved: " + resp.reason; 652 | } 653 | } 654 | }); 655 | }, 656 | 657 | /** 658 | * Save a list of documents 659 | * @see docs for /db/_bulk_docs 662 | * @param {Object[]} docs List of documents to save 663 | * @param {ajaxSettings} options jQuery ajax settings 665 | */ 666 | bulkSave: function(docs, options) { 667 | var beforeSend = fullCommit(options); 668 | $.extend(options, {successStatus: 201, beforeSend : beforeSend}); 669 | ajax({ 670 | type: "POST", 671 | url: this.uri + "_bulk_docs" + encodeOptions(options), 672 | contentType: "application/json", data: toJSON(docs) 673 | }, 674 | options, 675 | "The documents could not be saved" 676 | ); 677 | }, 678 | 679 | /** 680 | * Deletes the specified document from the database. You must supply 681 | * the current (latest) revision and id of the document 682 | * to delete eg removeDoc({_id:"mydoc", _rev: "1-2345"}) 683 | * @see docs for DELETE /db/doc 686 | * @param {Object} doc Document to delete 687 | * @param {ajaxSettings} options jQuery ajax settings 689 | */ 690 | removeDoc: function(doc, options) { 691 | ajax({ 692 | type: "DELETE", 693 | url: this.uri + 694 | encodeDocId(doc._id) + 695 | encodeOptions({rev: doc._rev}) 696 | }, 697 | options, 698 | "The document could not be deleted" 699 | ); 700 | }, 701 | 702 | /** 703 | * Remove a set of documents 704 | * @see docs for /db/_bulk_docs 707 | * @param {String[]} docs List of document id's to remove 708 | * @param {ajaxSettings} options jQuery ajax settings 710 | */ 711 | bulkRemove: function(docs, options){ 712 | docs.docs = $.each( 713 | docs.docs, function(i, doc){ 714 | doc._deleted = true; 715 | } 716 | ); 717 | $.extend(options, {successStatus: 201}); 718 | ajax({ 719 | type: "POST", 720 | url: this.uri + "_bulk_docs" + encodeOptions(options), 721 | data: toJSON(docs) 722 | }, 723 | options, 724 | "The documents could not be deleted" 725 | ); 726 | }, 727 | 728 | /** 729 | * The COPY command (which is non-standard HTTP) copies an existing 730 | * document to a new or existing document. 731 | * @see docs for COPY /db/doc 734 | * @param {String[]} docId document id to copy 735 | * @param {ajaxSettings} options jQuery ajax settings 737 | * @param {ajaxSettings} options jQuery ajax settings 739 | */ 740 | copyDoc: function(docId, options, ajaxOptions) { 741 | ajaxOptions = $.extend(ajaxOptions, { 742 | complete: function(req) { 743 | var resp = $.parseJSON(req.responseText); 744 | if (req.status == 201) { 745 | if (options.success) options.success(resp); 746 | } else if (options.error) { 747 | options.error(req.status, resp.error, resp.reason); 748 | } else { 749 | throw "The document could not be copied: " + resp.reason; 750 | } 751 | } 752 | }); 753 | ajax({ 754 | type: "COPY", 755 | url: this.uri + encodeDocId(docId) 756 | }, 757 | options, 758 | "The document could not be copied", 759 | ajaxOptions 760 | ); 761 | }, 762 | 763 | /** 764 | * Creates (and executes) a temporary view based on the view function 765 | * supplied in the JSON request. 766 | * @see docs for /db/_temp_view 769 | * @param {Function} mapFun Map function 770 | * @param {Function} reduceFun Reduce function 771 | * @param {Function} language Language the map / reduce funs are 772 | * implemented in 773 | * @param {ajaxSettings} options jQuery ajax settings 775 | */ 776 | query: function(mapFun, reduceFun, language, options) { 777 | language = language || "javascript"; 778 | if (typeof(mapFun) !== "string") { 779 | mapFun = mapFun.toSource ? mapFun.toSource() 780 | : "(" + mapFun.toString() + ")"; 781 | } 782 | var body = {language: language, map: mapFun}; 783 | if (reduceFun != null) { 784 | if (typeof(reduceFun) !== "string") 785 | reduceFun = reduceFun.toSource ? reduceFun.toSource() 786 | : "(" + reduceFun.toString() + ")"; 787 | body.reduce = reduceFun; 788 | } 789 | ajax({ 790 | type: "POST", 791 | url: this.uri + "_temp_view" + encodeOptions(options), 792 | contentType: "application/json", data: toJSON(body) 793 | }, 794 | options, 795 | "An error occurred querying the database" 796 | ); 797 | }, 798 | 799 | /** 800 | * Fetch a _list view output, you can specify a list of 801 | * keys in the options object to recieve only those keys. 802 | * @see 805 | * docs for /db/_design/design-doc/_list/l1/v1 806 | * @param {String} list Listname in the form of ddoc/listname 807 | * @param {String} view View to run list against 808 | * @param {options} CouchDB View Options 810 | * @param {ajaxSettings} options jQuery ajax settings 812 | */ 813 | list: function(list, view, options, ajaxOptions) { 814 | var list = list.split('/'); 815 | var options = options || {}; 816 | var type = 'GET'; 817 | var data = null; 818 | if (options['keys']) { 819 | type = 'POST'; 820 | var keys = options['keys']; 821 | delete options['keys']; 822 | data = toJSON({'keys': keys }); 823 | } 824 | ajax({ 825 | type: type, 826 | data: data, 827 | url: this.uri + '_design/' + list[0] + 828 | '/_list/' + list[1] + '/' + view + encodeOptions(options) 829 | }, 830 | ajaxOptions, 'An error occured accessing the list' 831 | ); 832 | }, 833 | 834 | /** 835 | * Executes the specified view-name from the specified design-doc 836 | * design document, you can specify a list of keys 837 | * in the options object to recieve only those keys. 838 | * @see docs for /db/ 841 | * _design/design-doc/_list/l1/v1 842 | * @param {String} name View to run list against 843 | * @param {ajaxSettings} options jQuery ajax settings 845 | */ 846 | view: function(name, options) { 847 | var name = name.split('/'); 848 | var options = options || {}; 849 | var type = "GET"; 850 | var data= null; 851 | if (options["keys"]) { 852 | type = "POST"; 853 | var keys = options["keys"]; 854 | delete options["keys"]; 855 | data = toJSON({ "keys": keys }); 856 | } 857 | ajax({ 858 | type: type, 859 | data: data, 860 | url: this.uri + "_design/" + name[0] + 861 | "/_view/" + name[1] + encodeOptions(options) 862 | }, 863 | options, "An error occurred accessing the view" 864 | ); 865 | }, 866 | 867 | /** 868 | * Fetch an arbitrary CouchDB database property 869 | * @see docs for /db/_prop 871 | * @param {String} propName Propery name to fetch 872 | * @param {ajaxSettings} options jQuery ajax settings 874 | * @param {ajaxSettings} ajaxOptions jQuery ajax settings 876 | */ 877 | getDbProperty: function(propName, options, ajaxOptions) { 878 | ajax({url: this.uri + propName + encodeOptions(options)}, 879 | options, 880 | "The property could not be retrieved", 881 | ajaxOptions 882 | ); 883 | }, 884 | 885 | /** 886 | * Set an arbitrary CouchDB database property 887 | * @see docs for /db/_prop 889 | * @param {String} propName Propery name to fetch 890 | * @param {String} propValue Propery value to set 891 | * @param {ajaxSettings} options jQuery ajax settings 893 | * @param {ajaxSettings} ajaxOptions jQuery ajax settings 895 | */ 896 | setDbProperty: function(propName, propValue, options, ajaxOptions) { 897 | ajax({ 898 | type: "PUT", 899 | url: this.uri + propName + encodeOptions(options), 900 | data : JSON.stringify(propValue) 901 | }, 902 | options, 903 | "The property could not be updated", 904 | ajaxOptions 905 | ); 906 | } 907 | }; 908 | }, 909 | 910 | encodeDocId: encodeDocId, 911 | 912 | /** 913 | * Accessing the root of a CouchDB instance returns meta information about 914 | * the instance. The response is a JSON structure containing information 915 | * about the server, including a welcome message and the version of the 916 | * server. 917 | * @see 919 | * docs for GET / 920 | * @param {ajaxSettings} options jQuery ajax settings 922 | */ 923 | info: function(options) { 924 | ajax( 925 | {url: this.urlPrefix + "/"}, 926 | options, 927 | "Server information could not be retrieved" 928 | ); 929 | }, 930 | 931 | /** 932 | * Request, configure, or stop, a replication operation. 933 | * @see docs for POST /_replicate 936 | * @param {String} source Path or url to source database 937 | * @param {String} target Path or url to target database 938 | * @param {ajaxSettings} ajaxOptions jQuery ajax settings 940 | * @param {Object} repOpts Additional replication options 941 | */ 942 | replicate: function(source, target, ajaxOptions, repOpts) { 943 | repOpts = $.extend({source: source, target: target}, repOpts); 944 | if (repOpts.continuous && !repOpts.cancel) { 945 | ajaxOptions.successStatus = 202; 946 | } 947 | ajax({ 948 | type: "POST", url: this.urlPrefix + "/_replicate", 949 | data: JSON.stringify(repOpts), 950 | contentType: "application/json" 951 | }, 952 | ajaxOptions, 953 | "Replication failed" 954 | ); 955 | }, 956 | 957 | /** 958 | * Fetch a new UUID 959 | * @see docs for /_uuids 962 | * @param {Int} cacheNum Number of uuids to keep cached for future use 963 | */ 964 | newUUID: function(cacheNum) { 965 | if (cacheNum === undefined) { 966 | cacheNum = 1; 967 | } 968 | if (!uuidCache.length) { 969 | ajax({url: this.urlPrefix + "/_uuids", data: {count: cacheNum}, async: 970 | false}, { 971 | success: function(resp) { 972 | uuidCache = resp.uuids; 973 | } 974 | }, 975 | "Failed to retrieve UUID batch." 976 | ); 977 | } 978 | return uuidCache.shift(); 979 | } 980 | }); 981 | 982 | /** 983 | * @private 984 | */ 985 | function ajax(obj, options, errorMessage, ajaxOptions) { 986 | 987 | var defaultAjaxOpts = { 988 | contentType: "application/json", 989 | headers:{"Accept": "application/json"} 990 | }; 991 | 992 | options = $.extend({successStatus: 200}, options); 993 | ajaxOptions = $.extend(defaultAjaxOpts, ajaxOptions); 994 | errorMessage = errorMessage || "Unknown error"; 995 | $.ajax($.extend($.extend({ 996 | type: "GET", dataType: "json", cache : !$.browser.msie, 997 | beforeSend: function(xhr){ 998 | if(ajaxOptions && ajaxOptions.headers){ 999 | for (var header in ajaxOptions.headers){ 1000 | xhr.setRequestHeader(header, ajaxOptions.headers[header]); 1001 | } 1002 | } 1003 | }, 1004 | complete: function(req) { 1005 | try { 1006 | var resp = $.parseJSON(req.responseText); 1007 | } catch(e) { 1008 | if (options.error) { 1009 | options.error(req.status, req, e); 1010 | } else { 1011 | throw errorMessage + ': ' + e; 1012 | } 1013 | return; 1014 | } 1015 | if (options.ajaxStart) { 1016 | options.ajaxStart(resp); 1017 | } 1018 | if (req.status == options.successStatus) { 1019 | if (options.beforeSuccess) options.beforeSuccess(req, resp); 1020 | if (options.success) options.success(resp); 1021 | } else if (options.error) { 1022 | options.error(req.status, resp && resp.error || 1023 | errorMessage, resp && resp.reason || "no response"); 1024 | } else { 1025 | throw errorMessage + ": " + resp.reason; 1026 | } 1027 | } 1028 | }, obj), ajaxOptions)); 1029 | } 1030 | 1031 | /** 1032 | * @private 1033 | */ 1034 | function fullCommit(options) { 1035 | var options = options || {}; 1036 | if (typeof options.ensure_full_commit !== "undefined") { 1037 | var commit = options.ensure_full_commit; 1038 | delete options.ensure_full_commit; 1039 | return function(xhr) { 1040 | xhr.setRequestHeader('Accept', 'application/json'); 1041 | xhr.setRequestHeader("X-Couch-Full-Commit", commit.toString()); 1042 | }; 1043 | } 1044 | }; 1045 | 1046 | /** 1047 | * @private 1048 | */ 1049 | // Convert a options object to an url query string. 1050 | // ex: {key:'value',key2:'value2'} becomes '?key="value"&key2="value2"' 1051 | function encodeOptions(options) { 1052 | var buf = []; 1053 | if (typeof(options) === "object" && options !== null) { 1054 | for (var name in options) { 1055 | if ($.inArray(name, 1056 | ["error", "success", "beforeSuccess", "ajaxStart"]) >= 0) 1057 | continue; 1058 | var value = options[name]; 1059 | if ($.inArray(name, ["key", "startkey", "endkey"]) >= 0) { 1060 | value = toJSON(value); 1061 | } 1062 | buf.push(encodeURIComponent(name) + "=" + encodeURIComponent(value)); 1063 | } 1064 | } 1065 | return buf.length ? "?" + buf.join("&") : ""; 1066 | } 1067 | 1068 | /** 1069 | * @private 1070 | */ 1071 | function toJSON(obj) { 1072 | return obj !== null ? JSON.stringify(obj) : null; 1073 | } 1074 | 1075 | })(jQuery); 1076 | -------------------------------------------------------------------------------- /tests/qunit/qunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.4.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2012 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | (function(window) { 12 | 13 | var defined = { 14 | setTimeout: typeof window.setTimeout !== "undefined", 15 | sessionStorage: (function() { 16 | var x = "qunit-test-string"; 17 | try { 18 | sessionStorage.setItem(x, x); 19 | sessionStorage.removeItem(x); 20 | return true; 21 | } catch(e) { 22 | return false; 23 | } 24 | }()) 25 | }; 26 | 27 | var testId = 0, 28 | toString = Object.prototype.toString, 29 | hasOwn = Object.prototype.hasOwnProperty; 30 | 31 | var Test = function(name, testName, expected, async, callback) { 32 | this.name = name; 33 | this.testName = testName; 34 | this.expected = expected; 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 | } else if (config.autorun) { 68 | runLoggingCallbacks( 'moduleStart', QUnit, { 69 | name: this.module 70 | } ); 71 | } 72 | 73 | config.current = this; 74 | this.testEnvironment = extend({ 75 | setup: function() {}, 76 | teardown: function() {} 77 | }, this.moduleTestEnvironment); 78 | 79 | runLoggingCallbacks( 'testStart', QUnit, { 80 | name: this.testName, 81 | module: this.module 82 | }); 83 | 84 | // allow utility functions to access the current test environment 85 | // TODO why?? 86 | QUnit.current_testEnvironment = this.testEnvironment; 87 | 88 | if ( !config.pollution ) { 89 | saveGlobal(); 90 | } 91 | if ( config.notrycatch ) { 92 | this.testEnvironment.setup.call(this.testEnvironment); 93 | return; 94 | } 95 | try { 96 | this.testEnvironment.setup.call(this.testEnvironment); 97 | } catch(e) { 98 | QUnit.pushFailure( "Setup failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) ); 99 | } 100 | }, 101 | run: function() { 102 | config.current = this; 103 | if ( this.async ) { 104 | QUnit.stop(); 105 | } 106 | 107 | if ( config.notrycatch ) { 108 | this.callback.call(this.testEnvironment); 109 | return; 110 | } 111 | try { 112 | this.callback.call(this.testEnvironment); 113 | } catch(e) { 114 | QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + ": " + e.message, extractStacktrace( e, 1 ) ); 115 | // else next test will carry the responsibility 116 | saveGlobal(); 117 | 118 | // Restart the tests if they're blocking 119 | if ( config.blocking ) { 120 | QUnit.start(); 121 | } 122 | } 123 | }, 124 | teardown: function() { 125 | config.current = this; 126 | if ( config.notrycatch ) { 127 | this.testEnvironment.teardown.call(this.testEnvironment); 128 | return; 129 | } else { 130 | try { 131 | this.testEnvironment.teardown.call(this.testEnvironment); 132 | } catch(e) { 133 | QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) ); 134 | } 135 | } 136 | checkPollution(); 137 | }, 138 | finish: function() { 139 | config.current = this; 140 | if ( this.expected != null && this.expected != this.assertions.length ) { 141 | QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" ); 142 | } else if ( this.expected == null && !this.assertions.length ) { 143 | QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions." ); 144 | } 145 | 146 | var good = 0, bad = 0, 147 | li, i, 148 | tests = id("qunit-tests"); 149 | 150 | config.stats.all += this.assertions.length; 151 | config.moduleStats.all += this.assertions.length; 152 | 153 | if ( tests ) { 154 | var ol = document.createElement("ol"); 155 | 156 | for ( i = 0; i < this.assertions.length; i++ ) { 157 | var assertion = this.assertions[i]; 158 | 159 | li = document.createElement("li"); 160 | li.className = assertion.result ? "pass" : "fail"; 161 | li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); 162 | ol.appendChild( li ); 163 | 164 | if ( assertion.result ) { 165 | good++; 166 | } else { 167 | bad++; 168 | config.stats.bad++; 169 | config.moduleStats.bad++; 170 | } 171 | } 172 | 173 | // store result when possible 174 | if ( QUnit.config.reorder && defined.sessionStorage ) { 175 | if (bad) { 176 | sessionStorage.setItem("qunit-test-" + this.module + "-" + this.testName, bad); 177 | } else { 178 | sessionStorage.removeItem("qunit-test-" + this.module + "-" + this.testName); 179 | } 180 | } 181 | 182 | if (bad === 0) { 183 | ol.style.display = "none"; 184 | } 185 | 186 | var b = document.createElement("strong"); 187 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 188 | 189 | var a = document.createElement("a"); 190 | a.innerHTML = "Rerun"; 191 | a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 192 | 193 | addEvent(b, "click", function() { 194 | var next = b.nextSibling.nextSibling, 195 | display = next.style.display; 196 | next.style.display = display === "none" ? "block" : "none"; 197 | }); 198 | 199 | addEvent(b, "dblclick", function(e) { 200 | var target = e && e.target ? e.target : window.event.srcElement; 201 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 202 | target = target.parentNode; 203 | } 204 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 205 | window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 206 | } 207 | }); 208 | 209 | li = id(this.id); 210 | li.className = bad ? "fail" : "pass"; 211 | li.removeChild( li.firstChild ); 212 | li.appendChild( b ); 213 | li.appendChild( a ); 214 | li.appendChild( ol ); 215 | 216 | } else { 217 | for ( i = 0; i < this.assertions.length; i++ ) { 218 | if ( !this.assertions[i].result ) { 219 | bad++; 220 | config.stats.bad++; 221 | config.moduleStats.bad++; 222 | } 223 | } 224 | } 225 | 226 | QUnit.reset(); 227 | 228 | runLoggingCallbacks( 'testDone', QUnit, { 229 | name: this.testName, 230 | module: this.module, 231 | failed: bad, 232 | passed: this.assertions.length - bad, 233 | total: this.assertions.length 234 | } ); 235 | }, 236 | 237 | queue: function() { 238 | var test = this; 239 | synchronize(function() { 240 | test.init(); 241 | }); 242 | function run() { 243 | // each of these can by async 244 | synchronize(function() { 245 | test.setup(); 246 | }); 247 | synchronize(function() { 248 | test.run(); 249 | }); 250 | synchronize(function() { 251 | test.teardown(); 252 | }); 253 | synchronize(function() { 254 | test.finish(); 255 | }); 256 | } 257 | // defer when previous test run passed, if storage is available 258 | var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-test-" + this.module + "-" + this.testName); 259 | if (bad) { 260 | run(); 261 | } else { 262 | synchronize(run, true); 263 | } 264 | } 265 | 266 | }; 267 | 268 | var QUnit = { 269 | 270 | // call on start of module test to prepend name to all tests 271 | module: function(name, testEnvironment) { 272 | config.currentModule = name; 273 | config.currentModuleTestEnviroment = testEnvironment; 274 | }, 275 | 276 | asyncTest: function(testName, expected, callback) { 277 | if ( arguments.length === 2 ) { 278 | callback = expected; 279 | expected = null; 280 | } 281 | 282 | QUnit.test(testName, expected, callback, true); 283 | }, 284 | 285 | test: function(testName, expected, callback, async) { 286 | var name = '' + escapeInnerText(testName) + ''; 287 | 288 | if ( arguments.length === 2 ) { 289 | callback = expected; 290 | expected = null; 291 | } 292 | 293 | if ( config.currentModule ) { 294 | name = '' + config.currentModule + ": " + name; 295 | } 296 | 297 | if ( !validTest(config.currentModule + ": " + testName) ) { 298 | return; 299 | } 300 | 301 | var test = new Test(name, testName, expected, async, callback); 302 | test.module = config.currentModule; 303 | test.moduleTestEnvironment = config.currentModuleTestEnviroment; 304 | test.queue(); 305 | }, 306 | 307 | // Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 308 | expect: function(asserts) { 309 | config.current.expected = asserts; 310 | }, 311 | 312 | // Asserts true. 313 | // @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 314 | ok: function(result, msg) { 315 | if (!config.current) { 316 | throw new Error("ok() assertion outside test context, was " + sourceFromStacktrace(2)); 317 | } 318 | result = !!result; 319 | var details = { 320 | result: result, 321 | message: msg 322 | }; 323 | msg = escapeInnerText(msg || (result ? "okay" : "failed")); 324 | if ( !result ) { 325 | var source = sourceFromStacktrace(2); 326 | if (source) { 327 | details.source = source; 328 | msg += '
Source:
' + escapeInnerText(source) + '
'; 329 | } 330 | } 331 | runLoggingCallbacks( 'log', QUnit, details ); 332 | config.current.assertions.push({ 333 | result: result, 334 | message: msg 335 | }); 336 | }, 337 | 338 | // Checks that the first two arguments are equal, with an optional message. Prints out both actual and expected values. 339 | // @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 340 | equal: function(actual, expected, message) { 341 | QUnit.push(expected == actual, actual, expected, message); 342 | }, 343 | 344 | notEqual: function(actual, expected, message) { 345 | QUnit.push(expected != actual, actual, expected, message); 346 | }, 347 | 348 | deepEqual: function(actual, expected, message) { 349 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); 350 | }, 351 | 352 | notDeepEqual: function(actual, expected, message) { 353 | QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); 354 | }, 355 | 356 | strictEqual: function(actual, expected, message) { 357 | QUnit.push(expected === actual, actual, expected, message); 358 | }, 359 | 360 | notStrictEqual: function(actual, expected, message) { 361 | QUnit.push(expected !== actual, actual, expected, message); 362 | }, 363 | 364 | raises: function(block, expected, message) { 365 | var actual, ok = false; 366 | 367 | if (typeof expected === 'string') { 368 | message = expected; 369 | expected = null; 370 | } 371 | 372 | try { 373 | block(); 374 | } catch (e) { 375 | actual = e; 376 | } 377 | 378 | if (actual) { 379 | // we don't want to validate thrown error 380 | if (!expected) { 381 | ok = true; 382 | // expected is a regexp 383 | } else if (QUnit.objectType(expected) === "regexp") { 384 | ok = expected.test(actual); 385 | // expected is a constructor 386 | } else if (actual instanceof expected) { 387 | ok = true; 388 | // expected is a validation function which returns true is validation passed 389 | } else if (expected.call({}, actual) === true) { 390 | ok = true; 391 | } 392 | } 393 | 394 | QUnit.ok(ok, message); 395 | }, 396 | 397 | start: function(count) { 398 | config.semaphore -= count || 1; 399 | if (config.semaphore > 0) { 400 | // don't start until equal number of stop-calls 401 | return; 402 | } 403 | if (config.semaphore < 0) { 404 | // ignore if start is called more often then stop 405 | config.semaphore = 0; 406 | } 407 | // A slight delay, to avoid any current callbacks 408 | if ( defined.setTimeout ) { 409 | window.setTimeout(function() { 410 | if (config.semaphore > 0) { 411 | return; 412 | } 413 | if ( config.timeout ) { 414 | clearTimeout(config.timeout); 415 | } 416 | 417 | config.blocking = false; 418 | process(true); 419 | }, 13); 420 | } else { 421 | config.blocking = false; 422 | process(true); 423 | } 424 | }, 425 | 426 | stop: function(count) { 427 | config.semaphore += count || 1; 428 | config.blocking = true; 429 | 430 | if ( config.testTimeout && defined.setTimeout ) { 431 | clearTimeout(config.timeout); 432 | config.timeout = window.setTimeout(function() { 433 | QUnit.ok( false, "Test timed out" ); 434 | config.semaphore = 1; 435 | QUnit.start(); 436 | }, config.testTimeout); 437 | } 438 | } 439 | }; 440 | 441 | //We want access to the constructor's prototype 442 | (function() { 443 | function F(){} 444 | F.prototype = QUnit; 445 | QUnit = new F(); 446 | //Make F QUnit's constructor so that we can add to the prototype later 447 | QUnit.constructor = F; 448 | }()); 449 | 450 | // deprecated; still export them to window to provide clear error messages 451 | // next step: remove entirely 452 | QUnit.equals = function() { 453 | QUnit.push(false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead"); 454 | }; 455 | QUnit.same = function() { 456 | QUnit.push(false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead"); 457 | }; 458 | 459 | // Maintain internal state 460 | var config = { 461 | // The queue of tests to run 462 | queue: [], 463 | 464 | // block until document ready 465 | blocking: true, 466 | 467 | // when enabled, show only failing tests 468 | // gets persisted through sessionStorage and can be changed in UI via checkbox 469 | hidepassed: false, 470 | 471 | // by default, run previously failed tests first 472 | // very useful in combination with "Hide passed tests" checked 473 | reorder: true, 474 | 475 | // by default, modify document.title when suite is done 476 | altertitle: true, 477 | 478 | urlConfig: ['noglobals', 'notrycatch'], 479 | 480 | //logging callback queues 481 | begin: [], 482 | done: [], 483 | log: [], 484 | testStart: [], 485 | testDone: [], 486 | moduleStart: [], 487 | moduleDone: [] 488 | }; 489 | 490 | // Load paramaters 491 | (function() { 492 | var location = window.location || { search: "", protocol: "file:" }, 493 | params = location.search.slice( 1 ).split( "&" ), 494 | length = params.length, 495 | urlParams = {}, 496 | current; 497 | 498 | if ( params[ 0 ] ) { 499 | for ( var i = 0; i < length; i++ ) { 500 | current = params[ i ].split( "=" ); 501 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 502 | // allow just a key to turn on a flag, e.g., test.html?noglobals 503 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 504 | urlParams[ current[ 0 ] ] = current[ 1 ]; 505 | } 506 | } 507 | 508 | QUnit.urlParams = urlParams; 509 | config.filter = urlParams.filter; 510 | 511 | // Figure out if we're running the tests from a server or not 512 | QUnit.isLocal = location.protocol === 'file:'; 513 | }()); 514 | 515 | // Expose the API as global variables, unless an 'exports' 516 | // object exists, in that case we assume we're in CommonJS - export everything at the end 517 | if ( typeof exports === "undefined" || typeof require === "undefined" ) { 518 | extend(window, QUnit); 519 | window.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 qunit = id( "qunit" ); 542 | if ( qunit ) { 543 | qunit.innerHTML = 544 | '

' + escapeInnerText( document.title ) + '

' + 545 | '

' + 546 | '
' + 547 | '

' + 548 | '
    '; 549 | } 550 | 551 | var tests = id( "qunit-tests" ), 552 | banner = id( "qunit-banner" ), 553 | result = id( "qunit-testresult" ); 554 | 555 | if ( tests ) { 556 | tests.innerHTML = ""; 557 | } 558 | 559 | if ( banner ) { 560 | banner.className = ""; 561 | } 562 | 563 | if ( result ) { 564 | result.parentNode.removeChild( result ); 565 | } 566 | 567 | if ( tests ) { 568 | result = document.createElement( "p" ); 569 | result.id = "qunit-testresult"; 570 | result.className = "result"; 571 | tests.parentNode.insertBefore( result, tests ); 572 | result.innerHTML = 'Running...
     '; 573 | } 574 | }, 575 | 576 | // Resets the test setup. Useful for tests that modify the DOM. 577 | // If jQuery is available, uses jQuery's html(), otherwise just innerHTML. 578 | reset: function() { 579 | if ( window.jQuery ) { 580 | jQuery( "#qunit-fixture" ).html( config.fixture ); 581 | } else { 582 | var main = id( 'qunit-fixture' ); 583 | if ( main ) { 584 | main.innerHTML = config.fixture; 585 | } 586 | } 587 | }, 588 | 589 | // Trigger an event on an element. 590 | // @example triggerEvent( document.body, "click" ); 591 | triggerEvent: function( elem, type, event ) { 592 | if ( document.createEvent ) { 593 | event = document.createEvent("MouseEvents"); 594 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 595 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 596 | elem.dispatchEvent( event ); 597 | 598 | } else if ( elem.fireEvent ) { 599 | elem.fireEvent("on"+type); 600 | } 601 | }, 602 | 603 | // Safe object type checking 604 | is: function( type, obj ) { 605 | return QUnit.objectType( obj ) == type; 606 | }, 607 | 608 | objectType: function( obj ) { 609 | if (typeof obj === "undefined") { 610 | return "undefined"; 611 | 612 | // consider: typeof null === object 613 | } 614 | if (obj === null) { 615 | return "null"; 616 | } 617 | 618 | var type = toString.call( obj ).match(/^\[object\s(.*)\]$/)[1] || ''; 619 | 620 | switch (type) { 621 | case 'Number': 622 | if (isNaN(obj)) { 623 | return "nan"; 624 | } 625 | return "number"; 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 | if (!config.current) { 642 | throw new Error("assertion outside test context, was " + sourceFromStacktrace()); 643 | } 644 | var details = { 645 | result: result, 646 | message: message, 647 | actual: actual, 648 | expected: expected 649 | }; 650 | 651 | message = escapeInnerText(message) || (result ? "okay" : "failed"); 652 | message = '' + message + ""; 653 | var output = message; 654 | if (!result) { 655 | expected = escapeInnerText(QUnit.jsDump.parse(expected)); 656 | actual = escapeInnerText(QUnit.jsDump.parse(actual)); 657 | output += ''; 658 | if (actual != expected) { 659 | output += ''; 660 | output += ''; 661 | } 662 | var source = sourceFromStacktrace(); 663 | if (source) { 664 | details.source = source; 665 | output += ''; 666 | } 667 | output += "
    Expected:
    ' + expected + '
    Result:
    ' + actual + '
    Diff:
    ' + QUnit.diff(expected, actual) +'
    Source:
    ' + escapeInnerText(source) + '
    "; 668 | } 669 | 670 | runLoggingCallbacks( 'log', QUnit, details ); 671 | 672 | config.current.assertions.push({ 673 | result: !!result, 674 | message: output 675 | }); 676 | }, 677 | 678 | pushFailure: function(message, source) { 679 | var details = { 680 | result: false, 681 | message: message 682 | }; 683 | var output = escapeInnerText(message); 684 | if (source) { 685 | details.source = source; 686 | output += '
    Source:
    ' + escapeInnerText(source) + '
    '; 687 | } 688 | runLoggingCallbacks( 'log', QUnit, details ); 689 | config.current.assertions.push({ 690 | result: false, 691 | message: output 692 | }); 693 | }, 694 | 695 | url: function( params ) { 696 | params = extend( extend( {}, QUnit.urlParams ), params ); 697 | var querystring = "?", 698 | key; 699 | for ( key in params ) { 700 | if ( !hasOwn.call( params, key ) ) { 701 | continue; 702 | } 703 | querystring += encodeURIComponent( key ) + "=" + 704 | encodeURIComponent( params[ key ] ) + "&"; 705 | } 706 | return window.location.pathname + querystring.slice( 0, -1 ); 707 | }, 708 | 709 | extend: extend, 710 | id: id, 711 | addEvent: addEvent 712 | }); 713 | 714 | //QUnit.constructor is set to the empty F() above so that we can add to it's prototype later 715 | //Doing this allows us to tell if the following methods have been overwritten on the actual 716 | //QUnit object, which is a deprecated way of using the callbacks. 717 | extend(QUnit.constructor.prototype, { 718 | // Logging callbacks; all receive a single argument with the listed properties 719 | // run test/logs.html for any related changes 720 | begin: registerLoggingCallback('begin'), 721 | // done: { failed, passed, total, runtime } 722 | done: registerLoggingCallback('done'), 723 | // log: { result, actual, expected, message } 724 | log: registerLoggingCallback('log'), 725 | // testStart: { name } 726 | testStart: registerLoggingCallback('testStart'), 727 | // testDone: { name, failed, passed, total } 728 | testDone: registerLoggingCallback('testDone'), 729 | // moduleStart: { name } 730 | moduleStart: registerLoggingCallback('moduleStart'), 731 | // moduleDone: { name, failed, passed, total } 732 | moduleDone: registerLoggingCallback('moduleDone') 733 | }); 734 | 735 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 736 | config.autorun = true; 737 | } 738 | 739 | QUnit.load = function() { 740 | runLoggingCallbacks( 'begin', QUnit, {} ); 741 | 742 | // Initialize the config, saving the execution queue 743 | var oldconfig = extend({}, config); 744 | QUnit.init(); 745 | extend(config, oldconfig); 746 | 747 | config.blocking = false; 748 | 749 | var urlConfigHtml = '', len = config.urlConfig.length; 750 | for ( var i = 0, val; i < len; i++ ) { 751 | val = config.urlConfig[i]; 752 | config[val] = QUnit.urlParams[val]; 753 | urlConfigHtml += ''; 754 | } 755 | 756 | var userAgent = id("qunit-userAgent"); 757 | if ( userAgent ) { 758 | userAgent.innerHTML = navigator.userAgent; 759 | } 760 | var banner = id("qunit-header"); 761 | if ( banner ) { 762 | banner.innerHTML = ' ' + banner.innerHTML + ' ' + urlConfigHtml; 763 | addEvent( banner, "change", function( event ) { 764 | var params = {}; 765 | params[ event.target.name ] = event.target.checked ? true : undefined; 766 | window.location = QUnit.url( params ); 767 | }); 768 | } 769 | 770 | var toolbar = id("qunit-testrunner-toolbar"); 771 | if ( toolbar ) { 772 | var filter = document.createElement("input"); 773 | filter.type = "checkbox"; 774 | filter.id = "qunit-filter-pass"; 775 | addEvent( filter, "click", function() { 776 | var ol = document.getElementById("qunit-tests"); 777 | if ( filter.checked ) { 778 | ol.className = ol.className + " hidepass"; 779 | } else { 780 | var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 781 | ol.className = tmp.replace(/ hidepass /, " "); 782 | } 783 | if ( defined.sessionStorage ) { 784 | if (filter.checked) { 785 | sessionStorage.setItem("qunit-filter-passed-tests", "true"); 786 | } else { 787 | sessionStorage.removeItem("qunit-filter-passed-tests"); 788 | } 789 | } 790 | }); 791 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { 792 | filter.checked = true; 793 | var ol = document.getElementById("qunit-tests"); 794 | ol.className = ol.className + " hidepass"; 795 | } 796 | toolbar.appendChild( filter ); 797 | 798 | var label = document.createElement("label"); 799 | label.setAttribute("for", "qunit-filter-pass"); 800 | label.innerHTML = "Hide passed tests"; 801 | toolbar.appendChild( label ); 802 | } 803 | 804 | var main = id('qunit-fixture'); 805 | if ( main ) { 806 | config.fixture = main.innerHTML; 807 | } 808 | 809 | if (config.autostart) { 810 | QUnit.start(); 811 | } 812 | }; 813 | 814 | addEvent(window, "load", QUnit.load); 815 | 816 | // addEvent(window, "error") gives us a useless event object 817 | window.onerror = function( message, file, line ) { 818 | if ( QUnit.config.current ) { 819 | QUnit.pushFailure( message, file + ":" + line ); 820 | } else { 821 | QUnit.test( "global failure", function() { 822 | QUnit.pushFailure( message, file + ":" + line ); 823 | }); 824 | } 825 | }; 826 | 827 | function done() { 828 | config.autorun = true; 829 | 830 | // Log the last module results 831 | if ( config.currentModule ) { 832 | runLoggingCallbacks( 'moduleDone', QUnit, { 833 | name: config.currentModule, 834 | failed: config.moduleStats.bad, 835 | passed: config.moduleStats.all - config.moduleStats.bad, 836 | total: config.moduleStats.all 837 | } ); 838 | } 839 | 840 | var banner = id("qunit-banner"), 841 | tests = id("qunit-tests"), 842 | runtime = +new Date() - config.started, 843 | passed = config.stats.all - config.stats.bad, 844 | html = [ 845 | 'Tests completed in ', 846 | runtime, 847 | ' milliseconds.
    ', 848 | '', 849 | passed, 850 | ' tests of ', 851 | config.stats.all, 852 | ' passed, ', 853 | config.stats.bad, 854 | ' failed.' 855 | ].join(''); 856 | 857 | if ( banner ) { 858 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 859 | } 860 | 861 | if ( tests ) { 862 | id( "qunit-testresult" ).innerHTML = html; 863 | } 864 | 865 | if ( config.altertitle && typeof document !== "undefined" && document.title ) { 866 | // show ✖ for good, ✔ for bad suite result in title 867 | // use escape sequences in case file gets loaded with non-utf-8-charset 868 | document.title = [ 869 | (config.stats.bad ? "\u2716" : "\u2714"), 870 | document.title.replace(/^[\u2714\u2716] /i, "") 871 | ].join(" "); 872 | } 873 | 874 | // clear own sessionStorage items if all tests passed 875 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { 876 | for (var key in sessionStorage) { 877 | if (sessionStorage.hasOwnProperty(key) && key.indexOf("qunit-test-") === 0 ) { 878 | sessionStorage.removeItem(key); 879 | } 880 | } 881 | } 882 | 883 | runLoggingCallbacks( 'done', QUnit, { 884 | failed: config.stats.bad, 885 | passed: passed, 886 | total: config.stats.all, 887 | runtime: runtime 888 | } ); 889 | } 890 | 891 | function validTest( name ) { 892 | var filter = config.filter, 893 | run = false; 894 | 895 | if ( !filter ) { 896 | return true; 897 | } 898 | 899 | var not = filter.charAt( 0 ) === "!"; 900 | if ( not ) { 901 | filter = filter.slice( 1 ); 902 | } 903 | 904 | if ( name.indexOf( filter ) !== -1 ) { 905 | return !not; 906 | } 907 | 908 | if ( not ) { 909 | run = true; 910 | } 911 | 912 | return run; 913 | } 914 | 915 | // so far supports only Firefox, Chrome and Opera (buggy) 916 | // could be extended in the future to use something like https://github.com/csnover/TraceKit 917 | function extractStacktrace( e, offset ) { 918 | offset = offset || 3; 919 | if (e.stacktrace) { 920 | // Opera 921 | return e.stacktrace.split("\n")[offset + 3]; 922 | } else if (e.stack) { 923 | // Firefox, Chrome 924 | var stack = e.stack.split("\n"); 925 | if (/^error$/i.test(stack[0])) { 926 | stack.shift(); 927 | } 928 | return stack[offset]; 929 | } else if (e.sourceURL) { 930 | // Safari, PhantomJS 931 | // hopefully one day Safari provides actual stacktraces 932 | // exclude useless self-reference for generated Error objects 933 | if ( /qunit.js$/.test( e.sourceURL ) ) { 934 | return; 935 | } 936 | // for actual exceptions, this is useful 937 | return e.sourceURL + ":" + e.line; 938 | } 939 | } 940 | function sourceFromStacktrace(offset) { 941 | try { 942 | throw new Error(); 943 | } catch ( e ) { 944 | return extractStacktrace( e, offset ); 945 | } 946 | } 947 | 948 | function escapeInnerText(s) { 949 | if (!s) { 950 | return ""; 951 | } 952 | s = s + ""; 953 | return s.replace(/[\&<>]/g, function(s) { 954 | switch(s) { 955 | case "&": return "&"; 956 | case "<": return "<"; 957 | case ">": return ">"; 958 | default: return s; 959 | } 960 | }); 961 | } 962 | 963 | function synchronize( callback, last ) { 964 | config.queue.push( callback ); 965 | 966 | if ( config.autorun && !config.blocking ) { 967 | process(last); 968 | } 969 | } 970 | 971 | function process( last ) { 972 | function next() { 973 | process( last ); 974 | } 975 | var start = new Date().getTime(); 976 | config.depth = config.depth ? config.depth + 1 : 1; 977 | 978 | while ( config.queue.length && !config.blocking ) { 979 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { 980 | config.queue.shift()(); 981 | } else { 982 | window.setTimeout( next, 13 ); 983 | break; 984 | } 985 | } 986 | config.depth--; 987 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 988 | done(); 989 | } 990 | } 991 | 992 | function saveGlobal() { 993 | config.pollution = []; 994 | 995 | if ( config.noglobals ) { 996 | for ( var key in window ) { 997 | if ( !hasOwn.call( window, key ) ) { 998 | continue; 999 | } 1000 | config.pollution.push( key ); 1001 | } 1002 | } 1003 | } 1004 | 1005 | function checkPollution( name ) { 1006 | var old = config.pollution; 1007 | saveGlobal(); 1008 | 1009 | var newGlobals = diff( config.pollution, old ); 1010 | if ( newGlobals.length > 0 ) { 1011 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); 1012 | } 1013 | 1014 | var deletedGlobals = diff( old, config.pollution ); 1015 | if ( deletedGlobals.length > 0 ) { 1016 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); 1017 | } 1018 | } 1019 | 1020 | // returns a new Array with the elements that are in a but not in b 1021 | function diff( a, b ) { 1022 | var result = a.slice(); 1023 | for ( var i = 0; i < result.length; i++ ) { 1024 | for ( var j = 0; j < b.length; j++ ) { 1025 | if ( result[i] === b[j] ) { 1026 | result.splice(i, 1); 1027 | i--; 1028 | break; 1029 | } 1030 | } 1031 | } 1032 | return result; 1033 | } 1034 | 1035 | function extend(a, b) { 1036 | for ( var prop in b ) { 1037 | if ( b[prop] === undefined ) { 1038 | delete a[prop]; 1039 | 1040 | // Avoid "Member not found" error in IE8 caused by setting window.constructor 1041 | } else if ( prop !== "constructor" || a !== window ) { 1042 | a[prop] = b[prop]; 1043 | } 1044 | } 1045 | 1046 | return a; 1047 | } 1048 | 1049 | function addEvent(elem, type, fn) { 1050 | if ( elem.addEventListener ) { 1051 | elem.addEventListener( type, fn, false ); 1052 | } else if ( elem.attachEvent ) { 1053 | elem.attachEvent( "on" + type, fn ); 1054 | } else { 1055 | fn(); 1056 | } 1057 | } 1058 | 1059 | function id(name) { 1060 | return !!(typeof document !== "undefined" && document && document.getElementById) && 1061 | document.getElementById( name ); 1062 | } 1063 | 1064 | function registerLoggingCallback(key){ 1065 | return function(callback){ 1066 | config[key].push( callback ); 1067 | }; 1068 | } 1069 | 1070 | // Supports deprecated method of completely overwriting logging callbacks 1071 | function runLoggingCallbacks(key, scope, args) { 1072 | //debugger; 1073 | var callbacks; 1074 | if ( QUnit.hasOwnProperty(key) ) { 1075 | QUnit[key].call(scope, args); 1076 | } else { 1077 | callbacks = config[key]; 1078 | for( var i = 0; i < callbacks.length; i++ ) { 1079 | callbacks[i].call( scope, args ); 1080 | } 1081 | } 1082 | } 1083 | 1084 | // Test for equality any JavaScript type. 1085 | // Author: Philippe Rathé 1086 | QUnit.equiv = (function() { 1087 | 1088 | var innerEquiv; // the real equiv function 1089 | var callers = []; // stack to decide between skip/abort functions 1090 | var parents = []; // stack to avoiding loops from circular referencing 1091 | 1092 | // Call the o related callback with the given arguments. 1093 | function bindCallbacks(o, callbacks, args) { 1094 | var prop = QUnit.objectType(o); 1095 | if (prop) { 1096 | if (QUnit.objectType(callbacks[prop]) === "function") { 1097 | return callbacks[prop].apply(callbacks, args); 1098 | } else { 1099 | return callbacks[prop]; // or undefined 1100 | } 1101 | } 1102 | } 1103 | 1104 | var getProto = Object.getPrototypeOf || function (obj) { 1105 | return obj.__proto__; 1106 | }; 1107 | 1108 | var callbacks = (function () { 1109 | 1110 | // for string, boolean, number and null 1111 | function useStrictEquality(b, a) { 1112 | if (b instanceof a.constructor || a instanceof b.constructor) { 1113 | // to catch short annotaion VS 'new' annotation of a 1114 | // declaration 1115 | // e.g. var i = 1; 1116 | // var j = new Number(1); 1117 | return a == b; 1118 | } else { 1119 | return a === b; 1120 | } 1121 | } 1122 | 1123 | return { 1124 | "string" : useStrictEquality, 1125 | "boolean" : useStrictEquality, 1126 | "number" : useStrictEquality, 1127 | "null" : useStrictEquality, 1128 | "undefined" : useStrictEquality, 1129 | 1130 | "nan" : function(b) { 1131 | return isNaN(b); 1132 | }, 1133 | 1134 | "date" : function(b, a) { 1135 | return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf(); 1136 | }, 1137 | 1138 | "regexp" : function(b, a) { 1139 | return QUnit.objectType(b) === "regexp" && 1140 | // the regex itself 1141 | a.source === b.source && 1142 | // and its modifers 1143 | a.global === b.global && 1144 | // (gmi) ... 1145 | a.ignoreCase === b.ignoreCase && 1146 | a.multiline === b.multiline; 1147 | }, 1148 | 1149 | // - skip when the property is a method of an instance (OOP) 1150 | // - abort otherwise, 1151 | // initial === would have catch identical references anyway 1152 | "function" : function() { 1153 | var caller = callers[callers.length - 1]; 1154 | return caller !== Object && typeof caller !== "undefined"; 1155 | }, 1156 | 1157 | "array" : function(b, a) { 1158 | var i, j, loop; 1159 | var len; 1160 | 1161 | // b could be an object literal here 1162 | if (QUnit.objectType(b) !== "array") { 1163 | return false; 1164 | } 1165 | 1166 | len = a.length; 1167 | if (len !== b.length) { // safe and faster 1168 | return false; 1169 | } 1170 | 1171 | // track reference to avoid circular references 1172 | parents.push(a); 1173 | for (i = 0; i < len; i++) { 1174 | loop = false; 1175 | for (j = 0; j < parents.length; j++) { 1176 | if (parents[j] === a[i]) { 1177 | loop = true;// dont rewalk array 1178 | } 1179 | } 1180 | if (!loop && !innerEquiv(a[i], b[i])) { 1181 | parents.pop(); 1182 | return false; 1183 | } 1184 | } 1185 | parents.pop(); 1186 | return true; 1187 | }, 1188 | 1189 | "object" : function(b, a) { 1190 | var i, j, loop; 1191 | var eq = true; // unless we can proove it 1192 | var aProperties = [], bProperties = []; // collection of 1193 | // strings 1194 | 1195 | // comparing constructors is more strict than using 1196 | // instanceof 1197 | if (a.constructor !== b.constructor) { 1198 | // Allow objects with no prototype to be equivalent to 1199 | // objects with Object as their constructor. 1200 | if (!((getProto(a) === null && getProto(b) === Object.prototype) || 1201 | (getProto(b) === null && getProto(a) === Object.prototype))) 1202 | { 1203 | return false; 1204 | } 1205 | } 1206 | 1207 | // stack constructor before traversing properties 1208 | callers.push(a.constructor); 1209 | // track reference to avoid circular references 1210 | parents.push(a); 1211 | 1212 | for (i in a) { // be strict: don't ensures hasOwnProperty 1213 | // and go deep 1214 | loop = false; 1215 | for (j = 0; j < parents.length; j++) { 1216 | if (parents[j] === a[i]) { 1217 | // don't go down the same path twice 1218 | loop = true; 1219 | } 1220 | } 1221 | aProperties.push(i); // collect a's properties 1222 | 1223 | if (!loop && !innerEquiv(a[i], b[i])) { 1224 | eq = false; 1225 | break; 1226 | } 1227 | } 1228 | 1229 | callers.pop(); // unstack, we are done 1230 | parents.pop(); 1231 | 1232 | for (i in b) { 1233 | bProperties.push(i); // collect b's properties 1234 | } 1235 | 1236 | // Ensures identical properties name 1237 | return eq && innerEquiv(aProperties.sort(), bProperties.sort()); 1238 | } 1239 | }; 1240 | }()); 1241 | 1242 | innerEquiv = function() { // can take multiple arguments 1243 | var args = Array.prototype.slice.apply(arguments); 1244 | if (args.length < 2) { 1245 | return true; // end transition 1246 | } 1247 | 1248 | return (function(a, b) { 1249 | if (a === b) { 1250 | return true; // catch the most you can 1251 | } else if (a === null || b === null || typeof a === "undefined" || 1252 | typeof b === "undefined" || 1253 | QUnit.objectType(a) !== QUnit.objectType(b)) { 1254 | return false; // don't lose time with error prone cases 1255 | } else { 1256 | return bindCallbacks(a, callbacks, [ b, a ]); 1257 | } 1258 | 1259 | // apply transition with (1..n) arguments 1260 | }(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length - 1))); 1261 | }; 1262 | 1263 | return innerEquiv; 1264 | 1265 | }()); 1266 | 1267 | /** 1268 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1269 | * http://flesler.blogspot.com Licensed under BSD 1270 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1271 | * 1272 | * @projectDescription Advanced and extensible data dumping for Javascript. 1273 | * @version 1.0.0 1274 | * @author Ariel Flesler 1275 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1276 | */ 1277 | QUnit.jsDump = (function() { 1278 | function quote( str ) { 1279 | return '"' + str.toString().replace(/"/g, '\\"') + '"'; 1280 | } 1281 | function literal( o ) { 1282 | return o + ''; 1283 | } 1284 | function join( pre, arr, post ) { 1285 | var s = jsDump.separator(), 1286 | base = jsDump.indent(), 1287 | inner = jsDump.indent(1); 1288 | if ( arr.join ) { 1289 | arr = arr.join( ',' + s + inner ); 1290 | } 1291 | if ( !arr ) { 1292 | return pre + post; 1293 | } 1294 | return [ pre, inner + arr, base + post ].join(s); 1295 | } 1296 | function array( arr, stack ) { 1297 | var i = arr.length, ret = new Array(i); 1298 | this.up(); 1299 | while ( i-- ) { 1300 | ret[i] = this.parse( arr[i] , undefined , stack); 1301 | } 1302 | this.down(); 1303 | return join( '[', ret, ']' ); 1304 | } 1305 | 1306 | var reName = /^function (\w+)/; 1307 | 1308 | var jsDump = { 1309 | parse: function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance 1310 | stack = stack || [ ]; 1311 | var parser = this.parsers[ type || this.typeOf(obj) ]; 1312 | type = typeof parser; 1313 | var inStack = inArray(obj, stack); 1314 | if (inStack != -1) { 1315 | return 'recursion('+(inStack - stack.length)+')'; 1316 | } 1317 | //else 1318 | if (type == 'function') { 1319 | stack.push(obj); 1320 | var res = parser.call( this, obj, stack ); 1321 | stack.pop(); 1322 | return res; 1323 | } 1324 | // else 1325 | return (type == 'string') ? parser : this.parsers.error; 1326 | }, 1327 | typeOf: function( obj ) { 1328 | var type; 1329 | if ( obj === null ) { 1330 | type = "null"; 1331 | } else if (typeof obj === "undefined") { 1332 | type = "undefined"; 1333 | } else if (QUnit.is("RegExp", obj)) { 1334 | type = "regexp"; 1335 | } else if (QUnit.is("Date", obj)) { 1336 | type = "date"; 1337 | } else if (QUnit.is("Function", obj)) { 1338 | type = "function"; 1339 | } else if (typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined") { 1340 | type = "window"; 1341 | } else if (obj.nodeType === 9) { 1342 | type = "document"; 1343 | } else if (obj.nodeType) { 1344 | type = "node"; 1345 | } else if ( 1346 | // native arrays 1347 | toString.call( obj ) === "[object Array]" || 1348 | // NodeList objects 1349 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) 1350 | ) { 1351 | type = "array"; 1352 | } else { 1353 | type = typeof obj; 1354 | } 1355 | return type; 1356 | }, 1357 | separator: function() { 1358 | return this.multiline ? this.HTML ? '
    ' : '\n' : this.HTML ? ' ' : ' '; 1359 | }, 1360 | indent: function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1361 | if ( !this.multiline ) { 1362 | return ''; 1363 | } 1364 | var chr = this.indentChar; 1365 | if ( this.HTML ) { 1366 | chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 1367 | } 1368 | return new Array( this._depth_ + (extra||0) ).join(chr); 1369 | }, 1370 | up: function( a ) { 1371 | this._depth_ += a || 1; 1372 | }, 1373 | down: function( a ) { 1374 | this._depth_ -= a || 1; 1375 | }, 1376 | setParser: function( name, parser ) { 1377 | this.parsers[name] = parser; 1378 | }, 1379 | // The next 3 are exposed so you can use them 1380 | quote: quote, 1381 | literal: literal, 1382 | join: join, 1383 | // 1384 | _depth_: 1, 1385 | // This is the list of parsers, to modify them, use jsDump.setParser 1386 | parsers: { 1387 | window: '[Window]', 1388 | document: '[Document]', 1389 | error: '[ERROR]', //when no parser is found, shouldn't happen 1390 | unknown: '[Unknown]', 1391 | 'null': 'null', 1392 | 'undefined': 'undefined', 1393 | 'function': function( fn ) { 1394 | var ret = 'function', 1395 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 1396 | if ( name ) { 1397 | ret += ' ' + name; 1398 | } 1399 | ret += '('; 1400 | 1401 | ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); 1402 | return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); 1403 | }, 1404 | array: array, 1405 | nodelist: array, 1406 | 'arguments': array, 1407 | object: function( map, stack ) { 1408 | var ret = [ ], keys, key, val, i; 1409 | QUnit.jsDump.up(); 1410 | if (Object.keys) { 1411 | keys = Object.keys( map ); 1412 | } else { 1413 | keys = []; 1414 | for (key in map) { keys.push( key ); } 1415 | } 1416 | keys.sort(); 1417 | for (i = 0; i < keys.length; i++) { 1418 | key = keys[ i ]; 1419 | val = map[ key ]; 1420 | ret.push( QUnit.jsDump.parse( key, 'key' ) + ': ' + QUnit.jsDump.parse( val, undefined, stack ) ); 1421 | } 1422 | QUnit.jsDump.down(); 1423 | return join( '{', ret, '}' ); 1424 | }, 1425 | node: function( node ) { 1426 | var open = QUnit.jsDump.HTML ? '<' : '<', 1427 | close = QUnit.jsDump.HTML ? '>' : '>'; 1428 | 1429 | var tag = node.nodeName.toLowerCase(), 1430 | ret = open + tag; 1431 | 1432 | for ( var a in QUnit.jsDump.DOMAttrs ) { 1433 | var val = node[QUnit.jsDump.DOMAttrs[a]]; 1434 | if ( val ) { 1435 | ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); 1436 | } 1437 | } 1438 | return ret + close + open + '/' + tag + close; 1439 | }, 1440 | functionArgs: function( fn ) {//function calls it internally, it's the arguments part of the function 1441 | var l = fn.length; 1442 | if ( !l ) { 1443 | return ''; 1444 | } 1445 | 1446 | var args = new Array(l); 1447 | while ( l-- ) { 1448 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1449 | } 1450 | return ' ' + args.join(', ') + ' '; 1451 | }, 1452 | key: quote, //object calls it internally, the key part of an item in a map 1453 | functionCode: '[code]', //function calls it internally, it's the content of the function 1454 | attribute: quote, //node calls it internally, it's an html attribute value 1455 | string: quote, 1456 | date: quote, 1457 | regexp: literal, //regex 1458 | number: literal, 1459 | 'boolean': literal 1460 | }, 1461 | DOMAttrs:{//attributes to dump from nodes, name=>realName 1462 | id:'id', 1463 | name:'name', 1464 | 'class':'className' 1465 | }, 1466 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) 1467 | indentChar:' ',//indentation unit 1468 | multiline:true //if true, items in a collection, are separated by a \n, else just a space. 1469 | }; 1470 | 1471 | return jsDump; 1472 | }()); 1473 | 1474 | // from Sizzle.js 1475 | function getText( elems ) { 1476 | var ret = "", elem; 1477 | 1478 | for ( var i = 0; elems[i]; i++ ) { 1479 | elem = elems[i]; 1480 | 1481 | // Get the text from text nodes and CDATA nodes 1482 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1483 | ret += elem.nodeValue; 1484 | 1485 | // Traverse everything else, except comment nodes 1486 | } else if ( elem.nodeType !== 8 ) { 1487 | ret += getText( elem.childNodes ); 1488 | } 1489 | } 1490 | 1491 | return ret; 1492 | } 1493 | 1494 | //from jquery.js 1495 | function inArray( elem, array ) { 1496 | if ( array.indexOf ) { 1497 | return array.indexOf( elem ); 1498 | } 1499 | 1500 | for ( var i = 0, length = array.length; i < length; i++ ) { 1501 | if ( array[ i ] === elem ) { 1502 | return i; 1503 | } 1504 | } 1505 | 1506 | return -1; 1507 | } 1508 | 1509 | /* 1510 | * Javascript Diff Algorithm 1511 | * By John Resig (http://ejohn.org/) 1512 | * Modified by Chu Alan "sprite" 1513 | * 1514 | * Released under the MIT license. 1515 | * 1516 | * More Info: 1517 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1518 | * 1519 | * Usage: QUnit.diff(expected, actual) 1520 | * 1521 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" 1522 | */ 1523 | QUnit.diff = (function() { 1524 | function diff(o, n) { 1525 | var ns = {}; 1526 | var os = {}; 1527 | var i; 1528 | 1529 | for (i = 0; i < n.length; i++) { 1530 | if (ns[n[i]] == null) { 1531 | ns[n[i]] = { 1532 | rows: [], 1533 | o: null 1534 | }; 1535 | } 1536 | ns[n[i]].rows.push(i); 1537 | } 1538 | 1539 | for (i = 0; i < o.length; i++) { 1540 | if (os[o[i]] == null) { 1541 | os[o[i]] = { 1542 | rows: [], 1543 | n: null 1544 | }; 1545 | } 1546 | os[o[i]].rows.push(i); 1547 | } 1548 | 1549 | for (i in ns) { 1550 | if ( !hasOwn.call( ns, i ) ) { 1551 | continue; 1552 | } 1553 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1554 | n[ns[i].rows[0]] = { 1555 | text: n[ns[i].rows[0]], 1556 | row: os[i].rows[0] 1557 | }; 1558 | o[os[i].rows[0]] = { 1559 | text: o[os[i].rows[0]], 1560 | row: ns[i].rows[0] 1561 | }; 1562 | } 1563 | } 1564 | 1565 | for (i = 0; i < n.length - 1; i++) { 1566 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && 1567 | n[i + 1] == o[n[i].row + 1]) { 1568 | n[i + 1] = { 1569 | text: n[i + 1], 1570 | row: n[i].row + 1 1571 | }; 1572 | o[n[i].row + 1] = { 1573 | text: o[n[i].row + 1], 1574 | row: i + 1 1575 | }; 1576 | } 1577 | } 1578 | 1579 | for (i = n.length - 1; i > 0; i--) { 1580 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 1581 | n[i - 1] == o[n[i].row - 1]) { 1582 | n[i - 1] = { 1583 | text: n[i - 1], 1584 | row: n[i].row - 1 1585 | }; 1586 | o[n[i].row - 1] = { 1587 | text: o[n[i].row - 1], 1588 | row: i - 1 1589 | }; 1590 | } 1591 | } 1592 | 1593 | return { 1594 | o: o, 1595 | n: n 1596 | }; 1597 | } 1598 | 1599 | return function(o, n) { 1600 | o = o.replace(/\s+$/, ''); 1601 | n = n.replace(/\s+$/, ''); 1602 | var out = diff(o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/)); 1603 | 1604 | var str = ""; 1605 | var i; 1606 | 1607 | var oSpace = o.match(/\s+/g); 1608 | if (oSpace == null) { 1609 | oSpace = [" "]; 1610 | } 1611 | else { 1612 | oSpace.push(" "); 1613 | } 1614 | var nSpace = n.match(/\s+/g); 1615 | if (nSpace == null) { 1616 | nSpace = [" "]; 1617 | } 1618 | else { 1619 | nSpace.push(" "); 1620 | } 1621 | 1622 | if (out.n.length === 0) { 1623 | for (i = 0; i < out.o.length; i++) { 1624 | str += '' + out.o[i] + oSpace[i] + ""; 1625 | } 1626 | } 1627 | else { 1628 | if (out.n[0].text == null) { 1629 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1630 | str += '' + out.o[n] + oSpace[n] + ""; 1631 | } 1632 | } 1633 | 1634 | for (i = 0; i < out.n.length; i++) { 1635 | if (out.n[i].text == null) { 1636 | str += '' + out.n[i] + nSpace[i] + ""; 1637 | } 1638 | else { 1639 | var pre = ""; 1640 | 1641 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1642 | pre += '' + out.o[n] + oSpace[n] + ""; 1643 | } 1644 | str += " " + out.n[i].text + nSpace[i] + pre; 1645 | } 1646 | } 1647 | } 1648 | 1649 | return str; 1650 | }; 1651 | }()); 1652 | 1653 | // for CommonJS enviroments, export everything 1654 | if ( typeof exports !== "undefined" || typeof require !== "undefined" ) { 1655 | extend(exports, QUnit); 1656 | } 1657 | 1658 | // get at whatever the global object is, like window in browsers 1659 | }( (function() {return this;}.call()) )); 1660 | --------------------------------------------------------------------------------