├── _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 |
6 | {{#each messages}}
7 | -
8 | {{format date "HH:mm"}} {{unbound username}} {{parse text}}
9 |
10 | {{/each}}
11 |
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 |
22 |
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 [](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 | '' +
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 += '| Expected: | ' + expected + ' |
';
658 | if (actual != expected) {
659 | output += '| Result: | ' + actual + ' |
';
660 | output += '| Diff: | ' + QUnit.diff(expected, actual) +' |
';
661 | }
662 | var source = sourceFromStacktrace();
663 | if (source) {
664 | details.source = source;
665 | output += '| Source: | ' + escapeInnerText(source) + ' |
';
666 | }
667 | output += "
";
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 |
--------------------------------------------------------------------------------