├── .gitignore
├── app
├── css
│ ├── main.css
│ └── style.css
├── templates
│ ├── github
│ │ ├── WatchEvent-template.handlebars
│ │ ├── actor.handlebars
│ │ ├── DeleteEvent-template.handlebars
│ │ ├── ForkEvent-template.handlebars
│ │ ├── CommitCommentEvent-template.handlebars
│ │ ├── DownloadEvent-template.handlebars
│ │ ├── MemberEvent-template.handlebars
│ │ ├── IssuesEvent-template.handlebars
│ │ ├── PushEvent-template.handlebars
│ │ ├── PullRequestEvent-template.handlebars
│ │ ├── githubEvent.handlebars
│ │ ├── CreateEvent-template.handlebars
│ │ ├── PullRequestReviewCommentEvent-template.handlebars
│ │ ├── GollumEvent-template.handlebars
│ │ └── IssueCommentEvent-template.handlebars
│ ├── tweet.handlebars
│ ├── tweets.handlebars
│ ├── github.handlebars
│ ├── reddits.handlebars
│ ├── questions.handlebars
│ ├── reddit.handlebars
│ └── question.handlebars
├── static
│ └── img
│ │ ├── fork_me_ribbon.png
│ │ ├── glyphicons-halflings.png
│ │ └── glyphicons-halflings-white.png
├── lib
│ ├── core.js
│ ├── ext.js
│ ├── datasource.js
│ ├── handlebars-helpers.js
│ ├── controllers.js
│ ├── main.js
│ └── views.js
├── tests
│ ├── core_test.js
│ ├── datasource_test.js
│ ├── controllers_test.js
│ ├── views_test.js
│ └── handlebars-helpers_test.js
├── plugins
│ └── loader.js
└── vendor
│ ├── moment.js
│ └── twitter-text.js
├── Guardfile
├── Gemfile
├── config.ru
├── LICENSE
├── tests
├── index.html
└── qunit
│ ├── run-qunit.js
│ └── qunit.css
├── Rakefile
├── README.md
├── Gemfile.lock
├── index.html
├── Assetfile
└── test_sources
├── stackoverflow.json
├── twitter.json
└── reddit.json
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | assets
3 | tmp
--------------------------------------------------------------------------------
/app/css/main.css:
--------------------------------------------------------------------------------
1 | a {
2 | cursor: pointer;
3 | }
4 |
--------------------------------------------------------------------------------
/Guardfile:
--------------------------------------------------------------------------------
1 | guard :rake, :task => :test do
2 | watch(%r{^app/.+\.js$})
3 | end
4 |
--------------------------------------------------------------------------------
/app/templates/github/WatchEvent-template.handlebars:
--------------------------------------------------------------------------------
1 | {{#view ActorView}} {{event.payload.action}} watching{{/view}}
--------------------------------------------------------------------------------
/app/templates/github/actor.handlebars:
--------------------------------------------------------------------------------
1 | {{actor.login}} {{yield}}
--------------------------------------------------------------------------------
/app/templates/tweet.handlebars:
--------------------------------------------------------------------------------
1 | {{event.from_user}} {{parseTweet event.text}}
--------------------------------------------------------------------------------
/app/static/img/fork_me_ribbon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pangratz/ember.js-dashboard/HEAD/app/static/img/fork_me_ribbon.png
--------------------------------------------------------------------------------
/app/static/img/glyphicons-halflings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pangratz/ember.js-dashboard/HEAD/app/static/img/glyphicons-halflings.png
--------------------------------------------------------------------------------
/app/templates/github/DeleteEvent-template.handlebars:
--------------------------------------------------------------------------------
1 | {{#view ActorView}}
2 | deleted {{event.payload.ref_type}} {{event.payload.ref}}
3 | {{/view}}
--------------------------------------------------------------------------------
/app/static/img/glyphicons-halflings-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pangratz/ember.js-dashboard/HEAD/app/static/img/glyphicons-halflings-white.png
--------------------------------------------------------------------------------
/app/templates/github/ForkEvent-template.handlebars:
--------------------------------------------------------------------------------
1 | {{#view ActorView}}
2 | forked
3 | repository
4 | {{/view}}
--------------------------------------------------------------------------------
/app/templates/github/CommitCommentEvent-template.handlebars:
--------------------------------------------------------------------------------
1 | {{#view ActorView actorBinding="event.actor"}}
2 | commented on commit
3 | {{/view}}
--------------------------------------------------------------------------------
/app/templates/github/DownloadEvent-template.handlebars:
--------------------------------------------------------------------------------
1 | {{#view ActorView}}
2 | created download {{event.payload.download.name}}
3 | {{/view}}
--------------------------------------------------------------------------------
/app/templates/github/MemberEvent-template.handlebars:
--------------------------------------------------------------------------------
1 | {{#view ActorView}}
2 | added {{event.payload.member.login}} as a collaborator
3 | {{/view}}
--------------------------------------------------------------------------------
/app/templates/github/IssuesEvent-template.handlebars:
--------------------------------------------------------------------------------
1 | {{#view ActorView}}
2 | {{event.payload.action}} issue
3 | #{{event.payload.issue.number}}
4 | {{/view}}
--------------------------------------------------------------------------------
/app/templates/github/PushEvent-template.handlebars:
--------------------------------------------------------------------------------
1 | {{#view ActorView}}
2 | pushed to
3 | {{event.payload.ref}}
4 | {{/view}}
--------------------------------------------------------------------------------
/app/templates/tweets.handlebars:
--------------------------------------------------------------------------------
1 |
2 | {{#each tweets}}
3 | {{event Dashboard.EventView eventBinding="this"}}
4 | {{else}}
5 | loading latest Tweets ...
6 | {{/each}}
7 |
--------------------------------------------------------------------------------
/app/templates/github.handlebars:
--------------------------------------------------------------------------------
1 |
2 | {{#each events}}
3 | {{event Dashboard.EventView eventBinding="this" }}
4 | {{else}}
5 | loading latest events on GitHub
6 | {{/each}}
7 |
--------------------------------------------------------------------------------
/app/templates/reddits.handlebars:
--------------------------------------------------------------------------------
1 |
2 | {{#each entries}}
3 | {{event Dashboard.EventView eventBinding="this"}}
4 | {{else}}
5 | loading latest reddit entries ...
6 | {{/each}}
7 |
--------------------------------------------------------------------------------
/app/templates/github/PullRequestEvent-template.handlebars:
--------------------------------------------------------------------------------
1 | {{#view ActorView}}
2 | {{event.payload.action}} Pull Request
3 | #{{event.payload.number}}
4 | {{/view}}
--------------------------------------------------------------------------------
/app/templates/github/githubEvent.handlebars:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | {{view DetailView eventBinding="event" }}
6 |
7 |
--------------------------------------------------------------------------------
/app/lib/core.js:
--------------------------------------------------------------------------------
1 | require('jquery');
2 | require('ember');
3 | require('twitter-text');
4 | require('moment');
5 |
6 | require('dashboard/ext');
7 | require('dashboard/handlebars-helpers');
8 |
9 | Dashboard = D = Ember.Application.create();
--------------------------------------------------------------------------------
/app/templates/questions.handlebars:
--------------------------------------------------------------------------------
1 |
2 | {{#each questions}}
3 | {{event Dashboard.EventView eventBinding="this"}}
4 | {{else}}
5 | loading latest questions on StackOverflow ...
6 | {{/each}}
7 |
--------------------------------------------------------------------------------
/app/templates/github/CreateEvent-template.handlebars:
--------------------------------------------------------------------------------
1 | {{#view ActorView}} created {{event.payload.ref_type}}{{/view}}
2 |
--------------------------------------------------------------------------------
/app/templates/github/PullRequestReviewCommentEvent-template.handlebars:
--------------------------------------------------------------------------------
1 | {{#view ActorView}}
2 | reviewed on commit
3 |
4 | {{trim event.payload.comment.commit_id length=7}}
5 |
6 | {{/view}}
7 |
8 | {{event.payload.comment.body}}
9 |
--------------------------------------------------------------------------------
/app/templates/github/GollumEvent-template.handlebars:
--------------------------------------------------------------------------------
1 | {{#view ActorView}}
2 | modified the wiki
3 | {{/view}}
4 |
5 | {{#each event.payload.pages }}
6 |
{{echo "action"}} {{page_name}}
7 | {{/each}}
8 |
9 |
--------------------------------------------------------------------------------
/app/templates/github/IssueCommentEvent-template.handlebars:
--------------------------------------------------------------------------------
1 | {{#view ActorView}}
2 | commented on issue
3 |
4 | #{{event.payload.issue.number}}
5 |
6 | {{/view}}
7 |
8 | {{event.payload.issue.title}}
9 |
--------------------------------------------------------------------------------
/app/templates/reddit.handlebars:
--------------------------------------------------------------------------------
1 | {{#with event}}
2 | {{ups}} -
3 | {{author}}
4 | {{title}} -
5 |
6 | {{num_comments}}
7 |
8 | {{/with}}
--------------------------------------------------------------------------------
/app/tests/core_test.js:
--------------------------------------------------------------------------------
1 | require('dashboard/core');
2 |
3 | module("core");
4 |
5 | test("Dashboard namespace is available",
6 | function() {
7 | ok(Dashboard, "namespace is available ");
8 | });
9 |
10 | test("Dashboard namespace has a shortcut D",
11 | function() {
12 | ok(D, "namespace is available");
13 | equal(Dashboard, D, "Dashboard equals D");
14 | });
--------------------------------------------------------------------------------
/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 'sass'
13 | gem 'compass'
14 |
15 | gem 'uglifier'
16 | gem 'yui-compressor'
17 |
18 | gem 'rake-pipeline', :git => 'https://github.com/livingsocial/rake-pipeline.git'
19 | gem 'rake-pipeline-web-filters', :git => 'https://github.com/wycats/rake-pipeline-web-filters.git'
20 |
--------------------------------------------------------------------------------
/app/tests/datasource_test.js:
--------------------------------------------------------------------------------
1 | require('dashboard/datasource');
2 |
3 | module("Dashboard.DataSource");
4 |
5 | test("it exists",
6 | function() {
7 | ok(Dashboard.DataSource, "datasource exists");
8 | ok(Ember.Object.detect(Dashboard.DataSource), "it is a subclass of Ember.Object");
9 | });
10 |
11 | test("has a method for getting latest Tweets",
12 | function() {
13 | ok(Dashboard.DataSource.create().getLatestTweets, "method exists");
14 | });
--------------------------------------------------------------------------------
/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?('/proxy')
8 | # "http://127.0.0.1:8080#{request.path}"
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/templates/question.handlebars:
--------------------------------------------------------------------------------
1 | {{#with event}}
2 |
3 |
{{score}} /
4 |
{{answer_count}} -
5 |
{{{title}}}
6 | {{#if accepted_answer_id}}
7 |
8 | {{/if}}
9 |
10 |
11 | {{#each tags}}
12 | {{this}}
13 | {{/each}}
14 |
15 | {{/with}}
--------------------------------------------------------------------------------
/app/lib/ext.js:
--------------------------------------------------------------------------------
1 | var get = Ember.get, fmt = Ember.String.fmt;
2 |
3 | Ember.View.reopen({
4 | templateForName: function(name, type) {
5 | if (!name) {
6 | return;
7 | }
8 |
9 | var templates = get(this, 'templates'),
10 | template = get(templates, name);
11 |
12 | if (!template) {
13 | try {
14 | template = require(name);
15 | } catch (error) { throw error; }
16 |
17 | if (!template) {
18 | throw new Ember.Error(fmt('%@ - Unable to find %@ "%@".', [this, type, name]));
19 | }
20 | }
21 |
22 | return template;
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/app/tests/controllers_test.js:
--------------------------------------------------------------------------------
1 | require('dashboard/controllers');
2 |
3 | module("Dashboard.GitHubController");
4 |
5 | test("it exists",
6 | function() {
7 | ok(Dashboard.GitHubController, "controller exists");
8 | });
9 |
10 | module("Dashboard.TwitterController");
11 |
12 | test("it exists",
13 | function() {
14 | ok(Dashboard.TwitterController, "controller exists");
15 | });
16 |
17 | module("Dashboard.StackOverflowController");
18 |
19 | test("it exists",
20 | function() {
21 | ok(Dashboard.StackOverflowController, "controller exists");
22 | });
23 |
24 | module("Dashboard.RedditController");
25 |
26 | test("it exists",
27 | function() {
28 | ok(Dashboard.RedditController, "controller exists");
29 | });
30 |
--------------------------------------------------------------------------------
/app/css/style.css:
--------------------------------------------------------------------------------
1 | div.container-fluid > h1 {
2 | color: #CD4F1A;
3 | text-transform: uppercase;
4 | text-shadow: 2px 2px 0px #FFF, 3px 3px 0px #CD4F1A;
5 | }
6 |
7 | li {
8 | background-color: #F7F7F9;
9 | -webkit-border-radius: 5px;
10 | -moz-border-radius: 5px;
11 | border-radius: 5px;
12 | margin-bottom: 1em;
13 | padding: 0.5em;
14 | border: 1px solid #E1E1E8;
15 | }
16 |
17 | .profilePic {
18 | float: left;
19 | margin: 3px;
20 | margin-right: 10px;
21 | }
22 |
23 | .profilePic > img {
24 | -webkit-border-radius: 3px;
25 | -moz-border-radius: 3px;
26 | border-radius: 3px;
27 | }
28 |
29 | .info {
30 | float: none;
31 | }
32 |
33 | div.additional-info {
34 | margin-left: 5px;
35 | margin-top: 5px;
36 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) Clemens Müller and contributors
2 |
3 | Permission to use, copy, modify, and/or distribute this software for any
4 | purpose with or without fee is hereby granted, provided that the above
5 | copyright notice and this permission notice appear in all copies.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 |
--------------------------------------------------------------------------------
/app/lib/datasource.js:
--------------------------------------------------------------------------------
1 | require('dashboard/core');
2 |
3 | Dashboard.DataSource = Ember.Object.extend({
4 | getLatestTweets: function(callback) {
5 | Ember.$.getJSON('http://search.twitter.com/search.json?callback=?&q=ember.js%20OR%20emberjs%20OR%20ember-data%20OR%20emberjs', callback);
6 | },
7 |
8 | getLatestStackOverflowQuestions: function(callback) {
9 | Ember.$.getJSON('https://api.stackexchange.com/2.0/search?pagesize=20&order=desc&sort=activity&tagged=ember.js&site=stackoverflow&callback=?', callback);
10 | },
11 |
12 | getLatestRedditEntries: function(callback) {
13 | Ember.$.getJSON('http://www.reddit.com/r/emberjs/new.json?sort=new&jsonp=?', callback);
14 | },
15 |
16 | getLatestGitHubEvents: function(callback) {
17 | Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/events?page=1&per_page=100&callback=?', callback);
18 | }
19 | });
--------------------------------------------------------------------------------
/tests/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Dashboard Test Suite
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | APPNAME = 'ember-skeleton'
2 |
3 | require 'colored'
4 | require 'rake-pipeline'
5 |
6 | desc "Build #{APPNAME}"
7 | task :build do
8 | Rake::Pipeline::Project.new('Assetfile').invoke
9 | end
10 |
11 | desc "Deploy #{APPNAME} to gh-pages branch"
12 | task :deploy do
13 | `rm -rf build`
14 | `mkdir build`
15 | origin = `git config remote.origin.url`.chomp
16 | ENV["RAKEP_MODE"] = "production"
17 | Rake::Task["build"].invoke
18 | Dir.chdir "build" do
19 | `git init`
20 | `git remote add origin #{origin}`
21 | `git checkout -b gh-pages`
22 | `cp ../index.html .`
23 | `cp -r ../assets .`
24 | `rm assets/app-tests.js`
25 | `git add .`
26 | `git commit -m 'Site updated at #{Time.now.utc}'`
27 | `git push -f origin gh-pages`
28 | end
29 | `rm -rf build`
30 | end
31 |
32 | desc "Run tests with PhantomJS"
33 | task :test => :build do
34 | unless system("which phantomjs > /dev/null 2>&1")
35 | abort "PhantomJS is not installed. Download from http://phantomjs.org/"
36 | end
37 |
38 | cmd = "phantomjs tests/qunit/run-qunit.js \"file://#{File.dirname(__FILE__)}/tests/index.html\""
39 |
40 | # Run the tests
41 | puts "Running #{APPNAME} tests"
42 | success = system(cmd)
43 |
44 | if success
45 | puts "Tests Passed".green
46 | else
47 | puts "Tests Failed".red
48 | exit(1)
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/app/tests/views_test.js:
--------------------------------------------------------------------------------
1 | require('dashboard/views');
2 |
3 | module("Dashboard.ActorView");
4 |
5 | test("it exists",
6 | function() {
7 | ok(Dashboard.ActorView, "view exists");
8 | ok(Ember.View.detect(Dashboard.ActorView), "it is a subclass of Ember.View");
9 | });
10 |
11 | test("has a property href which returns link to GitHub profile",
12 | function() {
13 | var view = Dashboard.ActorView.create({
14 | actor: Ember.Object.create({
15 | login: 'buster'
16 | })
17 | });
18 |
19 | Ember.run(function() {
20 | view.appendTo('#qunit-fixture');
21 | });
22 |
23 | equal(Ember.$('#qunit-fixture').find('a[href="https://github.com/buster"]').length, 1, "rendered view has a link to GitHub repo");
24 | });
25 |
26 | module("Dashboard.EventView");
27 |
28 | test("it exists",
29 | function() {
30 | ok(Dashboard.EventView, "view exists");
31 | ok(Ember.View.detect(Dashboard.EventView), "it is a sublcass of Ember.View");
32 | });
33 |
34 | test("it returns a templateName based on the event.type property",
35 | function() {
36 | var view = Dashboard.EventView.create({
37 | event: Ember.Object.create({
38 | type: 'CommitCommentEvent'
39 | })
40 | });
41 |
42 | Ember.run(function() {
43 | view.appendTo('#qunit-fixture');
44 | });
45 |
46 | equal(view.get('templateName'), 'dashboard/~templates/github/CommitCommentEvent-template', "returns template for given event type");
47 | });
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ember.js Dashboard
2 |
3 | This is the repo for [Ember.js Dashboard](http://pangratz.github.com/ember.js-dashboard), which shows the activity of the community unified at one glance. It aggregates the latest stuff from [Twitter](http://twitter.com/#!/search/realtime/emberjs), events in the ember.js [GitHub](https://github.com/emberjs/ember.js) repository, latest questions on [StackOverflow](http://stackoverflow.com/questions/tagged/emberjs) and stuff from [reddit/emberjs](http://www.reddit.com/r/emberjs/).
4 |
5 | I wrote this dashboard because I constantly hit those 4 pages in intervals of 1 minute, just to see if there's something new. With this dashboard I can check out all this 4 times more in the minute :pensive:
6 |
7 | ## Hack
8 |
9 | The project is organized with the layout from the excellent [interline/ember-skeleton](https://github.com/interline/ember-skeleton).
10 |
11 | To hack around clone this very repo and do a `bundle install` and afterwards a simple `bundle exec rackup` to start your local server. Then go to `http://localhost:9292`, hack around and hit refresh to see the changes.
12 |
13 | Pull Requests are **very** welcome! Questions? Find me on Twitter @pangratz.
14 |
15 | ## Deploy your own dashboard
16 |
17 | There is a `deploy` task in the `Rakefile`. Simply execute `bundle exec rake deploy` and the dashboard will be pushed to the `gh-pages` branch in your `ember.js-dashboard` repository, which is afterwards available via `http://YOURSELF.github.com/ember.js-dashboard` ...
18 |
19 |
20 | ----
21 |
22 | Holy Moly.
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GIT
2 | remote: https://github.com/livingsocial/rake-pipeline.git
3 | revision: b70ca6cad7655e58d13031f3e24df7dfc74f9030
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: ba0b8a00356b4c854930a8e849b5629d51ffd70f
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.1)
27 | chunky_png (~> 1.2)
28 | fssm (>= 0.2.7)
29 | sass (~> 3.1)
30 | execjs (1.3.0)
31 | multi_json (~> 1.0)
32 | ffi (1.0.11)
33 | fssm (0.2.8.1)
34 | guard (1.0.1)
35 | ffi (>= 0.5.0)
36 | thor (~> 0.14.6)
37 | guard-rake (0.0.5)
38 | guard
39 | rake
40 | multi_json (1.2.0)
41 | open4 (1.3.0)
42 | rack (1.4.1)
43 | rack-rewrite (1.2.1)
44 | rake (0.9.2.2)
45 | sass (3.1.15)
46 | thor (0.14.6)
47 | uglifier (1.2.4)
48 | execjs (>= 0.3.0)
49 | multi_json (>= 1.0.2)
50 | yui-compressor (0.9.6)
51 | POpen4 (>= 0.1.4)
52 |
53 | PLATFORMS
54 | ruby
55 |
56 | DEPENDENCIES
57 | colored
58 | compass
59 | guard
60 | guard-rake
61 | rack
62 | rack-rewrite
63 | rake-pipeline!
64 | rake-pipeline-web-filters!
65 | sass
66 | uglifier
67 | yui-compressor
68 |
--------------------------------------------------------------------------------
/app/lib/handlebars-helpers.js:
--------------------------------------------------------------------------------
1 | Ember.Handlebars.registerHelper('echo',
2 | function(propertyName, options) {
3 | return Ember.getPath(options.contexts[0], propertyName);
4 | });
5 |
6 | Ember.Handlebars.registerHelper('parseTweet',
7 | function(propertyPath, options) {
8 | var tweet;
9 | if (!options) {
10 | options = propertyPath;
11 | tweet = options.contexts[0].text;
12 | } else {
13 | tweet = Ember.getPath(options.contexts[0], propertyPath);
14 | }
15 | var parsed = twttr.txt.autoLink(tweet);
16 | return new Handlebars.SafeString(parsed);
17 | });
18 |
19 | Ember.Handlebars.registerHelper('ago',
20 | function(propertyName, options) {
21 | var timestamp = Ember.getPath(options.contexts[0], propertyName);
22 | if (options.hash.isSeconds) {
23 | // the given property represents seconds since UNIX epoch, so we multiply
24 | // by 1000 to get the date in milliseconds since UNIX epoch
25 | timestamp *= 1000;
26 | }
27 | return moment(new Date(timestamp)).fromNow();
28 | });
29 |
30 | Ember.Handlebars.registerHelper('event',
31 | function(path, options) {
32 | var eventType = Ember.getPath(options.contexts[0], 'TYPE');
33 | var viewClass = 'Dashboard.%@View'.fmt(eventType);
34 | return Ember.Handlebars.ViewHelper.helper(this, viewClass, options);
35 | });
36 |
37 | Ember.Handlebars.registerHelper('trim',
38 | function(path, options) {
39 | var length = options.hash.length;
40 | var string = Ember.getPath(options.contexts[0], path);
41 | if (length) {
42 | string = string.substring(0, length);
43 | }
44 | return new Handlebars.SafeString(string);
45 | });
--------------------------------------------------------------------------------
/app/lib/controllers.js:
--------------------------------------------------------------------------------
1 | require('dashboard/core');
2 |
3 | Dashboard.RedditController = Ember.ArrayProxy.extend({
4 | content: [],
5 | loadLatestEntries: function() {
6 | var that = this;
7 | var ds = this.get('dataSource');
8 | ds.getLatestRedditEntries(function(response) {
9 | that.pushObjects(response.data.children.getEach('data'));
10 | that.setEach('TYPE', 'RedditEvent');
11 | });
12 | }
13 | });
14 |
15 | Dashboard.GitHubController = Ember.ArrayProxy.extend({
16 | content: [],
17 | loadLatestEvents: function() {
18 | var that = this;
19 | var ds = this.get('dataSource');
20 | ds.getLatestGitHubEvents(function(response) {
21 | that.pushObjects(response.data);
22 | that.setEach('TYPE', 'GitHubEvent');
23 | });
24 | }
25 | });
26 |
27 | Dashboard.StackOverflowController = Ember.ArrayProxy.extend({
28 | content: [],
29 | loadLatestQuestions: function() {
30 | var that = this;
31 | var ds = this.get('dataSource');
32 | ds.getLatestStackOverflowQuestions(function(response) {
33 | that.pushObjects(response.items);
34 | that.setEach('TYPE', 'StackOverflowEvent');
35 | });
36 | }
37 | });
38 |
39 | Dashboard.TwitterController = Ember.ArrayProxy.extend({
40 | content: [],
41 | loadLatestTweets: function() {
42 | var that = this;
43 | var ds = this.get('dataSource');
44 | ds.getLatestTweets(function(response) {
45 | that.pushObjects(response.results);
46 | that.setEach('TYPE', 'TwitterEvent');
47 | });
48 | }
49 | });
50 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Ember.js Dashboard
5 |
6 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
Ember.js Dashboard
26 |
27 |
31 |
32 |
33 |
loading latest events on GitHub
34 |
35 |
36 |
37 |
loading latest questions on StackOverflow ...
38 |
39 |
40 |
41 |
loading latest reddit entries ...
42 |
43 |
44 |
45 |
46 |
47 |
50 |
51 |
--------------------------------------------------------------------------------
/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/lib/main.js:
--------------------------------------------------------------------------------
1 | require('dashboard/core');
2 | require('dashboard/controllers');
3 | require('dashboard/views');
4 | require('dashboard/datasource');
5 |
6 | // this is for testing purpose: if you add ?fixtures=true to your local url,
7 | // the pre-fetched sources are used and so you don't waste API calls during development
8 | if (window.location.search.indexOf('fixtures') !== -1) {
9 | Dashboard.dataSource = Dashboard.DataSource.create({
10 | getLatestTweets: function(callback) {
11 | this.getJSON('twitter.json', callback);
12 | },
13 | getLatestGitHubEvents: function(callback) {
14 | this.getJSON('github.json', callback);
15 | },
16 | getLatestStackOverflowQuestions: function(callback) {
17 | this.getJSON('stackoverflow.json', callback);
18 | },
19 | getLatestRedditEntries: function(callback) {
20 | this.getJSON('reddit.json', callback);
21 | },
22 | getJSON: function(json, callback) {
23 | Ember.$.getJSON('/test_sources/' + json, callback);
24 | }
25 | });
26 | } else {
27 | Dashboard.dataSource = Dashboard.DataSource.create();
28 | }
29 |
30 | // create da controllers
31 | Dashboard.redditController = Dashboard.RedditController.create({
32 | dataSourceBinding: 'Dashboard.dataSource'
33 | });
34 | Dashboard.githubEventsController = Dashboard.GitHubController.create({
35 | dataSourceBinding: 'Dashboard.dataSource'
36 | });
37 | Dashboard.questionsController = Dashboard.StackOverflowController.create({
38 | dataSourceBinding: 'Dashboard.dataSource'
39 | });
40 | Dashboard.tweetsController = Dashboard.TwitterController.create({
41 | dataSourceBinding: 'Dashboard.dataSource'
42 | });
43 |
44 | Ember.run.sync();
45 |
46 | // fetch initial data
47 | Dashboard.githubEventsController.loadLatestEvents();
48 | Dashboard.tweetsController.loadLatestTweets();
49 | Dashboard.questionsController.loadLatestQuestions();
50 | Dashboard.redditController.loadLatestEntries();
51 |
52 | // create da views
53 | Ember.View.create({
54 | tweetsBinding: 'Dashboard.tweetsController',
55 | templateName: 'dashboard/~templates/tweets'
56 | }).replaceIn('.tweets');
57 |
58 | Ember.View.create({
59 | questionsBinding: 'Dashboard.questionsController',
60 | templateName: 'dashboard/~templates/questions'
61 | }).replaceIn('.stackoverflow');
62 |
63 | Ember.View.create({
64 | eventsBinding: 'Dashboard.githubEventsController',
65 | templateName: 'dashboard/~templates/github'
66 | }).replaceIn('.github');
67 |
68 | Ember.View.create({
69 | entriesBinding: 'Dashboard.redditController',
70 | templateName: 'dashboard/~templates/reddits'
71 | }).replaceIn('.reddit');
72 |
--------------------------------------------------------------------------------
/app/lib/views.js:
--------------------------------------------------------------------------------
1 | require('dashboard/core');
2 |
3 | Dashboard.EventView = Ember.View.extend({
4 | layout: Ember.Handlebars.compile('{{timeAgoString}} {{yield}}'),
5 | defaultTemplate: Ember.Handlebars.compile('{{event.TYPE}}'),
6 | classNameBindings: 'event.TYPE'.w(),
7 | timestampProperty: 'event.created_at',
8 |
9 | timeAgo: function() {
10 | timestamp = Ember.getPath(this, this.get('timestampProperty'));
11 | if (this.get('isSeconds')) {
12 | // the given property represents seconds since UNIX epoch, so we multiply
13 | // by 1000 to get the date in milliseconds since UNIX epoch
14 | timestamp *= 1000;
15 | }
16 | return new Date(timestamp);
17 | }.property('timestampProperty', 'isSeconds').cacheable(),
18 |
19 | timeAgoString: function() {
20 | var timeAgo = this.get('timeAgo');
21 | return moment(timeAgo).fromNow();
22 | }.property('timeAgo')
23 | });
24 |
25 | Dashboard.StackOverflowEventView = Dashboard.EventView.extend({
26 | timestampProperty: 'event.last_activity_date',
27 | isSeconds: true,
28 | templateName: 'dashboard/~templates/question'
29 | });
30 |
31 | Dashboard.RedditEventView = Dashboard.EventView.extend({
32 | timestampProperty: 'event.created_utc',
33 | isSeconds: true,
34 | templateName: 'dashboard/~templates/reddit'
35 | });
36 |
37 | Dashboard.TwitterEventView = Dashboard.EventView.extend({
38 | templateName: 'dashboard/~templates/tweet',
39 | tweetUrl: function() {
40 | var user = Ember.getPath(this, 'event.from_user');
41 | var id = Ember.getPath(this, 'event.id_str');
42 | return 'http://twitter.com/#!/%@/status/%@'.fmt(user, id);
43 | }.property()
44 | });
45 |
46 | Dashboard.GitHubEventView = Dashboard.EventView.extend({
47 | templateName: 'dashboard/~templates/github/githubEvent',
48 |
49 | avatarUrl: function() {
50 | var gravatarId = Ember.getPath(this, 'event.actor.gravatar_id');
51 | return 'http://www.gravatar.com/avatar/%@'.fmt(gravatarId);
52 | }.property('event.actor.gravatar_id'),
53 |
54 | DetailView: Ember.View.extend({
55 | classNames: 'info'.w(),
56 | templateName: function() {
57 | var type = Ember.getPath(this, 'event.type');
58 | return 'dashboard/~templates/github/%@-template'.fmt(type);
59 | }.property('event.type').cacheable(),
60 |
61 | ActorView: Ember.View.extend({
62 | layoutName: 'dashboard/~templates/github/actor',
63 | defaultTemplate: Ember.Handlebars.compile(''),
64 | tagName: '',
65 |
66 | actorBinding: 'event.actor',
67 | eventBinding: 'parentView.event',
68 |
69 | href: function() {
70 | var login = Ember.getPath(this, 'actor.login');
71 | return 'https://github.com/%@'.fmt(login);
72 | }.property('actor.login')
73 | })
74 | })
75 |
76 |
77 | });
--------------------------------------------------------------------------------
/Assetfile:
--------------------------------------------------------------------------------
1 | APPNAME = 'dashboard'
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 'assets'
41 |
42 | input 'app' do
43 | match 'lib/**/*.js' do
44 | filter LoaderFilter,
45 | :module_id_generator => proc { |input|
46 | input.path.sub(/^lib\//, "#{APPNAME}/").sub(/\.js$/, '')
47 | }
48 |
49 | if ENV['RAKEP_MODE'] == 'production'
50 | filter EmberAssertFilter
51 | uglify {|input| input}
52 | end
53 | concat 'app.js'
54 | end
55 |
56 | match 'vendor/**/*.js' do
57 | filter LoaderFilter,
58 | :module_id_generator => proc { |input|
59 | input.path.sub(/^vendor\//, '').sub(/\.js$/, '')
60 | }
61 |
62 | if ENV['RAKEP_MODE'] == 'production'
63 | filter EmberAssertFilter
64 | uglify {|input| input}
65 | end
66 | concat %w[
67 | vendor/jquery.js
68 | vendor/ember.js
69 | vendor/moment.js
70 | vendor/twitter-text.js
71 | ], 'app.js'
72 | end
73 |
74 | match 'modules/**/*.js' do
75 | if ENV['RAKEP_MODE'] == 'production'
76 | filter EmberAssertFilter
77 | uglify {|input| input}
78 | end
79 | concat 'app.js'
80 | end
81 |
82 | match 'plugins/**/*.js' do
83 | if ENV['RAKEP_MODE'] == 'production'
84 | uglify {|input| input}
85 | end
86 | concat do |input|
87 | input.sub(/plugins\//, '')
88 | end
89 | end
90 |
91 | match 'templates/**/*.handlebars' do
92 | filter HandlebarsFilter
93 | filter LoaderFilter,
94 | :module_id_generator => proc { |input|
95 | input.path.sub(/^templates\//, "#{APPNAME}/~templates/").sub(/\.handlebars$/, '')
96 | }
97 | if ENV['RAKEP_MODE'] == 'production'
98 | uglify {|input| input}
99 | end
100 | concat 'app.js'
101 | end
102 |
103 | match 'tests/**/*.js' do
104 | filter LoaderFilter,
105 | :module_id_generator => proc { |input|
106 | input.path.sub(/^lib\//, "#{APPNAME}/").sub(/\.js$/, '')
107 | }
108 | concat 'app-tests.js'
109 | end
110 |
111 | match 'css/**/*.css' do
112 | if ENV['RAKEP_MODE'] == 'production'
113 | yui_css
114 | end
115 | concat ['bootstrap.css', 'main.css'], 'app.css'
116 | end
117 |
118 | match 'css/**/*.scss' do
119 | sass
120 | if ENV['RAKEP_MODE'] == 'production'
121 | yui_css
122 | end
123 | concat 'app.css'
124 | end
125 |
126 | match "static/**/*" do
127 | concat do |input|
128 | input.sub(/static\//, '')
129 | end
130 | end
131 | end
132 |
133 | # vim: filetype=ruby
134 |
--------------------------------------------------------------------------------
/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/tests/handlebars-helpers_test.js:
--------------------------------------------------------------------------------
1 | require('dashboard/handlebars-helpers');
2 |
3 | module("{{echo}}");
4 |
5 | test("helper is available",
6 | function() {
7 | ok(Ember.Handlebars.helpers.echo, "echo helper is available");
8 | });
9 |
10 | test("returns the specified property",
11 | function() {
12 | var view = Ember.View.create({
13 | obj: Ember.Object.create({
14 | action: 'dingdong'
15 | }),
16 | template: Ember.Handlebars.compile('{{#with obj}}{{echo "action"}}{{/with}}')
17 | });
18 |
19 | Ember.run(function() {
20 | view.appendTo('#qunit-fixture');
21 | });
22 |
23 | equal(Ember.$('#qunit-fixture').text(), "dingdong", "echo helper outputs correct value");
24 | });
25 |
26 | module("{{ago}}");
27 |
28 | test("helper is available",
29 | function() {
30 | ok(Ember.Handlebars.helpers.ago, "ago helper is availbale");
31 | });
32 |
33 | test("ago can handle Date objects",
34 | function() {
35 | var view = Ember.View.create({
36 | time: new Date(),
37 | template: Ember.Handlebars.compile('{{ago time}}')
38 | });
39 |
40 | Ember.run(function() {
41 | view.appendTo('#qunit-fixture');
42 | });
43 |
44 | equal(Ember.$('#qunit-fixture').text(), "a few seconds ago", "ago helper outputs correct value");
45 | });
46 |
47 | test("ago can handle numbers",
48 | function() {
49 | var view = Ember.View.create({
50 | time: new Date().getTime(),
51 | template: Ember.Handlebars.compile('{{ago time}}')
52 | });
53 |
54 | Ember.run(function() {
55 | view.appendTo('#qunit-fixture');
56 | });
57 |
58 | equal(Ember.$('#qunit-fixture').text(), "a few seconds ago", "ago helper outputs correct value");
59 | });
60 |
61 | test("ago respects 'isSeconds' parameter",
62 | function() {
63 | var nowInSeconds = new Date().getTime() / 1000;
64 | var view = Ember.View.create({
65 | time: nowInSeconds,
66 | template: Ember.Handlebars.compile('{{ago time isSeconds=true}}')
67 | });
68 |
69 | Ember.run(function() {
70 | view.appendTo('#qunit-fixture');
71 | });
72 |
73 | equal(Ember.$('#qunit-fixture').text(), "a few seconds ago", "ago helper outputs correct value");
74 | });
75 |
76 | module("{{parseTweet}}");
77 |
78 | test("helper is available",
79 | function() {
80 | ok(Ember.Handlebars.helpers.parseTweet, "parseTweet helper is available");
81 | });
82 |
83 | test("parseTweet uses context.text as tweet text",
84 | function() {
85 | var view = Ember.View.create({
86 | tweet: Ember.Object.create({
87 | text: "@user #tag http://google.com"
88 | }),
89 | template: Ember.Handlebars.compile('{{#with tweet}}{{parseTweet}}{{/with}}')
90 | });
91 |
92 | Ember.run(function() {
93 | view.appendTo('#qunit-fixture');
94 | });
95 |
96 | equal(Ember.$('#qunit-fixture').text(), "@user #tag http://google.com", "output text is the same");
97 | var links = Ember.$('#qunit-fixture').find('a');
98 | equal(links.length, 3, "parsed tweet should contain 3 links");
99 | equal(Ember.$(links[0]).text(), "user", "first link has the user as text");
100 | equal(Ember.$(links[1]).text(), "#tag", "second link has the #tag as text");
101 | equal(Ember.$(links[2]).text(), "http://google.com", "third link has the url as text");
102 | });
103 |
104 | test("if a parameter is specified, parseTweet uses this as path to the tweet",
105 | function() {
106 | var view = Ember.View.create({
107 | tweet: "@user #tag http://google.com",
108 | template: Ember.Handlebars.compile('{{parseTweet tweet}}')
109 | });
110 |
111 | Ember.run(function() {
112 | view.appendTo('#qunit-fixture');
113 | });
114 |
115 | equal(Ember.$('#qunit-fixture').text(), "@user #tag http://google.com", "output text is the same");
116 | var links = Ember.$('#qunit-fixture').find('a');
117 | equal(links.length, 3, "parsed tweet should contain 3 links");
118 | equal(Ember.$(links[0]).text(), "user", "first link has the user as text");
119 | equal(Ember.$(links[1]).text(), "#tag", "second link has the #tag as text");
120 | equal(Ember.$(links[2]).text(), "http://google.com", "third link has the url as text");
121 | });
122 |
123 | module("{{parseTweet}}");
124 |
125 | test("helper is available",
126 | function() {
127 | ok(Ember.Handlebars.helpers.parseTweet, "parseTweet helper is available");
128 | });
129 |
130 | test("parseTweet uses context.text as tweet text",
131 | function() {
132 | var view = Ember.View.create({
133 | tweet: Ember.Object.create({
134 | text: "@user #tag http://google.com"
135 | }),
136 | template: Ember.Handlebars.compile('{{#with tweet}}{{parseTweet}}{{/with}}')
137 | });
138 |
139 | Ember.run(function() {
140 | view.appendTo('#qunit-fixture');
141 | });
142 |
143 | equal(Ember.$('#qunit-fixture').text(), "@user #tag http://google.com", "output text is the same");
144 | var links = Ember.$('#qunit-fixture').find('a');
145 | equal(links.length, 3, "parsed tweet should contain 3 links");
146 | equal(Ember.$(links[0]).text(), "user", "first link has the user as text");
147 | equal(Ember.$(links[1]).text(), "#tag", "second link has the #tag as text");
148 | equal(Ember.$(links[2]).text(), "http://google.com", "third link has the url as text");
149 | });
150 |
151 | test("if a parameter is specified, parseTweet uses this as path to the tweet",
152 | function() {
153 | var view = Ember.View.create({
154 | tweet: "@user #tag http://google.com",
155 | template: Ember.Handlebars.compile('{{parseTweet tweet}}')
156 | });
157 |
158 | Ember.run(function() {
159 | view.appendTo('#qunit-fixture');
160 | });
161 |
162 | equal(Ember.$('#qunit-fixture').text(), "@user #tag http://google.com", "output text is the same");
163 | var links = Ember.$('#qunit-fixture').find('a');
164 | equal(links.length, 3, "parsed tweet should contain 3 links");
165 | equal(Ember.$(links[0]).text(), "user", "first link has the user as text");
166 | equal(Ember.$(links[1]).text(), "#tag", "second link has the #tag as text");
167 | equal(Ember.$(links[2]).text(), "http://google.com", "third link has the url as text");
168 | });
169 |
170 | module("{{trim}}");
171 |
172 | test("trim is available",
173 | function() {
174 | ok(Ember.Handlebars.helpers.trim, "trim helper is available");
175 | });
176 |
177 | test("outputs by default given string",
178 | function() {
179 | var view = Ember.View.create({
180 | property: 'mystring',
181 | template: Ember.Handlebars.compile('{{trim property}}')
182 | });
183 |
184 | Ember.run(function() {
185 | view.appendTo('#qunit-fixture');
186 | });
187 |
188 | equal(Ember.$('#qunit-fixture').text(), 'mystring', "by default doesn't trim anything");
189 | });
190 |
191 | test("trims string to specific length",
192 | function() {
193 | var view = Ember.View.create({
194 | property: 'mystring',
195 | template: Ember.Handlebars.compile('{{trim property length=2}}')
196 | });
197 |
198 | Ember.run(function() {
199 | view.appendTo('#qunit-fixture');
200 | });
201 |
202 | equal(Ember.$('#qunit-fixture').text(), 'my', "it trims the string to given length");
203 | });
204 |
205 | test("exceeding length does not blow up the handler",
206 | function() {
207 | var view = Ember.View.create({
208 | property: 'mystring',
209 | template: Ember.Handlebars.compile('{{trim property length=100}}')
210 | });
211 |
212 | Ember.run(function() {
213 | view.appendTo('#qunit-fixture');
214 | });
215 |
216 | equal(Ember.$('#qunit-fixture').text(), 'mystring', "it trims the string to given length");
217 | });
--------------------------------------------------------------------------------
/test_sources/stackoverflow.json:
--------------------------------------------------------------------------------
1 | {
2 | "items": [{
3 | "question_id": 10048691,
4 | "last_edit_date": 1333746525,
5 | "creation_date": 1333742816,
6 | "last_activity_date": 1333746525,
7 | "score": 0,
8 | "answer_count": 1,
9 | "title": "Headless testing Ember application with guard and jasmine",
10 | "tags": ["emberjs", "jasmine", "phantomjs"],
11 | "view_count": 9,
12 | "owner": {
13 | "user_id": 1259390,
14 | "display_name": "jrabary",
15 | "reputation": 6,
16 | "user_type": "registered",
17 | "profile_image": "http://www.gravatar.com/avatar/ab64211b24abeb1e283c0ea1326d6ad0?d=identicon&r=PG",
18 | "link": "http://stackoverflow.com/users/1259390/jrabary",
19 | "accept_rate": 0
20 | },
21 | "link": "http://stackoverflow.com/questions/10048691/headless-testing-ember-application-with-guard-and-jasmine",
22 | "is_answered": false
23 | },
24 | {
25 | "question_id": 10047447,
26 | "creation_date": 1333736006,
27 | "last_activity_date": 1333745211,
28 | "score": 0,
29 | "answer_count": 1,
30 | "title": "Binding to a nested model in Ember.js",
31 | "tags": ["javascript", "emberjs"],
32 | "view_count": 15,
33 | "owner": {
34 | "user_id": 1293561,
35 | "display_name": "sirvine",
36 | "reputation": 1,
37 | "user_type": "registered",
38 | "profile_image": "http://www.gravatar.com/avatar/31654782c71cfd0073e7c7394815e299?d=identicon&r=PG",
39 | "link": "http://stackoverflow.com/users/1293561/sirvine"
40 | },
41 | "link": "http://stackoverflow.com/questions/10047447/binding-to-a-nested-model-in-ember-js",
42 | "is_answered": false
43 | },
44 | {
45 | "question_id": 9991749,
46 | "last_edit_date": 1333526671,
47 | "creation_date": 1333450277,
48 | "last_activity_date": 1333742528,
49 | "score": 5,
50 | "answer_count": 1,
51 | "title": "Delete created uncommited record?",
52 | "tags": ["emberjs", "emberdata"],
53 | "view_count": 46,
54 | "owner": {
55 | "user_id": 219931,
56 | "display_name": "Dziamid",
57 | "reputation": 2159,
58 | "user_type": "registered",
59 | "profile_image": "http://www.gravatar.com/avatar/3430e60d8dd7c8d8d2b0c377e0c6cc57?d=identicon&r=PG",
60 | "link": "http://stackoverflow.com/users/219931/dziamid",
61 | "accept_rate": 92
62 | },
63 | "link": "http://stackoverflow.com/questions/9991749/delete-created-uncommited-record",
64 | "is_answered": false
65 | },
66 | {
67 | "question_id": 10046699,
68 | "creation_date": 1333731967,
69 | "last_activity_date": 1333735105,
70 | "score": 1,
71 | "answer_count": 1,
72 | "title": "Has anyone successfully integrated Ember.js - Phonegap (and jQuery Mobile)?",
73 | "tags": ["ios", "phonegap", "emberjs", "cordova"],
74 | "view_count": 18,
75 | "owner": {
76 | "user_id": 359104,
77 | "display_name": "Zack",
78 | "reputation": 597,
79 | "user_type": "registered",
80 | "profile_image": "http://www.gravatar.com/avatar/0d2643856090c41deef2b021c93764b3?d=identicon&r=PG",
81 | "link": "http://stackoverflow.com/users/359104/zack",
82 | "accept_rate": 94
83 | },
84 | "link": "http://stackoverflow.com/questions/10046699/has-anyone-successfully-integrated-ember-js-phonegap-and-jquery-mobile",
85 | "is_answered": true
86 | },
87 | {
88 | "question_id": 10045619,
89 | "creation_date": 1333726203,
90 | "last_activity_date": 1333727370,
91 | "score": 1,
92 | "answer_count": 1,
93 | "title": "Controller Strategy / Garbage Collection (destroy)",
94 | "tags": ["emberjs"],
95 | "view_count": 28,
96 | "owner": {
97 | "user_id": 130221,
98 | "display_name": "Nick Franceschina",
99 | "reputation": 847,
100 | "user_type": "registered",
101 | "profile_image": "http://www.gravatar.com/avatar/19f7f12138e2a60930b56253e6f54b11?d=identicon&r=PG",
102 | "link": "http://stackoverflow.com/users/130221/nick-franceschina",
103 | "accept_rate": 95
104 | },
105 | "link": "http://stackoverflow.com/questions/10045619/controller-strategy-garbage-collection-destroy",
106 | "is_answered": true
107 | },
108 | {
109 | "question_id": 10002737,
110 | "creation_date": 1333494362,
111 | "last_activity_date": 1333719471,
112 | "score": 0,
113 | "answer_count": 2,
114 | "title": "emberjs: when I try to use the action helper, i get - Handlebars error: Could not find property 'action' on object",
115 | "tags": ["emberjs"],
116 | "view_count": 46,
117 | "owner": {
118 | "user_id": 1311561,
119 | "display_name": "Robert Beaupre",
120 | "reputation": 1,
121 | "user_type": "registered",
122 | "profile_image": "http://www.gravatar.com/avatar/3b449142aef0ee76ee7e09396b6805dc?d=identicon&r=PG",
123 | "link": "http://stackoverflow.com/users/1311561/robert-beaupre"
124 | },
125 | "link": "http://stackoverflow.com/questions/10002737/emberjs-when-i-try-to-use-the-action-helper-i-get-handlebars-error-could-no",
126 | "is_answered": true
127 | },
128 | {
129 | "question_id": 9445676,
130 | "last_edit_date": 1330206430,
131 | "creation_date": 1330186510,
132 | "last_activity_date": 1333719455,
133 | "score": 1,
134 | "answer_count": 2,
135 | "accepted_answer_id": 9450608,
136 | "title": "Computed property being observed doesn't fire if changed twice in a row",
137 | "tags": ["emberjs"],
138 | "view_count": 87,
139 | "owner": {
140 | "user_id": 88170,
141 | "display_name": "thedesertfox",
142 | "reputation": 131,
143 | "user_type": "registered",
144 | "profile_image": "http://www.gravatar.com/avatar/0e0476bc1ab931a7382caae4fa7a537f?d=identicon&r=PG",
145 | "link": "http://stackoverflow.com/users/88170/thedesertfox",
146 | "accept_rate": 73
147 | },
148 | "link": "http://stackoverflow.com/questions/9445676/computed-property-being-observed-doesnt-fire-if-changed-twice-in-a-row",
149 | "is_answered": true
150 | },
151 | {
152 | "question_id": 10043951,
153 | "creation_date": 1333717838,
154 | "last_activity_date": 1333719181,
155 | "score": 1,
156 | "answer_count": 2,
157 | "accepted_answer_id": 10044059,
158 | "title": "Ember.js init behaviour",
159 | "tags": ["javascript", "emberjs"],
160 | "view_count": 13,
161 | "owner": {
162 | "user_id": 172439,
163 | "display_name": "spacevillain",
164 | "reputation": 412,
165 | "user_type": "registered",
166 | "profile_image": "http://www.gravatar.com/avatar/37c374a1516b645b0ee0e6363c82d887?d=identicon&r=PG",
167 | "link": "http://stackoverflow.com/users/172439/spacevillain",
168 | "accept_rate": 64
169 | },
170 | "link": "http://stackoverflow.com/questions/10043951/ember-js-init-behaviour",
171 | "is_answered": true
172 | },
173 | {
174 | "question_id": 10043451,
175 | "creation_date": 1333715112,
176 | "last_activity_date": 1333716683,
177 | "score": 3,
178 | "answer_count": 1,
179 | "accepted_answer_id": 10043731,
180 | "title": "Ember.ArrayProxy changes not triggering handlebars #each update",
181 | "tags": ["emberjs"],
182 | "view_count": 14,
183 | "owner": {
184 | "user_id": 993270,
185 | "display_name": "alexrothenberg",
186 | "reputation": 18,
187 | "user_type": "registered",
188 | "profile_image": "http://www.gravatar.com/avatar/f4252ea05ddfbeb7b1803e9941ea46aa?d=identicon&r=PG",
189 | "link": "http://stackoverflow.com/users/993270/alexrothenberg"
190 | },
191 | "link": "http://stackoverflow.com/questions/10043451/ember-arrayproxy-changes-not-triggering-handlebars-each-update",
192 | "is_answered": true
193 | },
194 | {
195 | "question_id": 10040339,
196 | "creation_date": 1333696437,
197 | "last_activity_date": 1333700173,
198 | "score": 2,
199 | "answer_count": 1,
200 | "title": "Ember Views, Handlebars and jQuery Effects",
201 | "tags": ["emberjs"],
202 | "view_count": 25,
203 | "owner": {
204 | "user_id": 996265,
205 | "display_name": "mike",
206 | "reputation": 43,
207 | "user_type": "registered",
208 | "profile_image": "http://www.gravatar.com/avatar/dbc7ce5d92647532b49bbed08a75cfca?d=identicon&r=PG",
209 | "link": "http://stackoverflow.com/users/996265/mike"
210 | },
211 | "link": "http://stackoverflow.com/questions/10040339/ember-views-handlebars-and-jquery-effects",
212 | "is_answered": true
213 | }],
214 | "quota_remaining": 261,
215 | "quota_max": 300,
216 | "has_more": true
217 | }
--------------------------------------------------------------------------------
/test_sources/twitter.json:
--------------------------------------------------------------------------------
1 | {
2 | "completed_in": 0.023,
3 | "max_id": 188372280315871234,
4 | "max_id_str": "188372280315871234",
5 | "next_page": "?page=2&max_id=188372280315871234&q=emberjs",
6 | "page": 1,
7 | "query": "emberjs",
8 | "refresh_url": "?since_id=188372280315871234&q=emberjs",
9 | "results": [{
10 | "created_at": "Fri, 06 Apr 2012 21:07:06 +0000",
11 | "from_user": "pangratz",
12 | "from_user_id": 49576387,
13 | "from_user_id_str": "49576387",
14 | "from_user_name": "Clemens M\u00fcller",
15 | "geo": null,
16 | "id": 188372280315871234,
17 | "id_str": "188372280315871234",
18 | "iso_language_code": "in",
19 | "metadata": {
20 | "result_type": "recent"
21 | },
22 | "profile_image_url": "http:\/\/a0.twimg.com\/profile_images\/1866862141\/IMG_3719_normal.JPG",
23 | "profile_image_url_https": "https:\/\/si0.twimg.com\/profile_images\/1866862141\/IMG_3719_normal.JPG",
24 | "source": "<a href="http:\/\/www.echofon.com\/" rel="nofollow">Echofon<\/a>",
25 | "text": "Very good explanation about the difference of \u007b\u007bbind\u007d\u007d and \u007b\u007bbindAttr\u007d\u007d by @wagenet https:\/\/t.co\/7NrNOByV #emberjs",
26 | "to_user": null,
27 | "to_user_id": null,
28 | "to_user_id_str": null,
29 | "to_user_name": null
30 | },
31 | {
32 | "created_at": "Fri, 06 Apr 2012 19:34:41 +0000",
33 | "from_user": "som32dt",
34 | "from_user_id": 41788781,
35 | "from_user_id_str": "41788781",
36 | "from_user_name": "som32dt",
37 | "geo": null,
38 | "id": 188349023655165952,
39 | "id_str": "188349023655165952",
40 | "iso_language_code": "en",
41 | "metadata": {
42 | "result_type": "recent"
43 | },
44 | "profile_image_url": "http:\/\/a0.twimg.com\/sticky\/default_profile_images\/default_profile_6_normal.png",
45 | "profile_image_url_https": "https:\/\/si0.twimg.com\/sticky\/default_profile_images\/default_profile_6_normal.png",
46 | "source": "<a href="http:\/\/twitter.com\/">web<\/a>",
47 | "text": "JS : Ember - A JavaScript framework for creating ambitious web applications - http:\/\/t.co\/5VmiSqPD",
48 | "to_user": null,
49 | "to_user_id": null,
50 | "to_user_id_str": null,
51 | "to_user_name": null
52 | },
53 | {
54 | "created_at": "Fri, 06 Apr 2012 17:55:20 +0000",
55 | "from_user": "agilemeister",
56 | "from_user_id": 68325967,
57 | "from_user_id_str": "68325967",
58 | "from_user_name": "Angeline Tan",
59 | "geo": null,
60 | "id": 188324023304855552,
61 | "id_str": "188324023304855552",
62 | "iso_language_code": "en",
63 | "metadata": {
64 | "result_type": "recent"
65 | },
66 | "profile_image_url": "http:\/\/a0.twimg.com\/profile_images\/1170469190\/twitter44_normal.jpg",
67 | "profile_image_url_https": "https:\/\/si0.twimg.com\/profile_images\/1170469190\/twitter44_normal.jpg",
68 | "source": "<a href="http:\/\/twitter.com\/">web<\/a>",
69 | "text": "RT @bmelton: @agilemeister Working on easing the learning curve for Ember. Hopefully it will help some? -- http:\/\/t.co\/BdcXr7mG",
70 | "to_user": null,
71 | "to_user_id": null,
72 | "to_user_id_str": null,
73 | "to_user_name": null,
74 | "in_reply_to_status_id": 188083765459951617,
75 | "in_reply_to_status_id_str": "188083765459951617"
76 | },
77 | {
78 | "created_at": "Fri, 06 Apr 2012 17:53:30 +0000",
79 | "from_user": "shoanm",
80 | "from_user_id": 14292596,
81 | "from_user_id_str": "14292596",
82 | "from_user_name": "Shoan",
83 | "geo": null,
84 | "id": 188323562141122560,
85 | "id_str": "188323562141122560",
86 | "iso_language_code": "en",
87 | "metadata": {
88 | "result_type": "recent"
89 | },
90 | "profile_image_url": "http:\/\/a0.twimg.com\/profile_images\/53089644\/tilted_crop_normal.jpg",
91 | "profile_image_url_https": "https:\/\/si0.twimg.com\/profile_images\/53089644\/tilted_crop_normal.jpg",
92 | "source": "<a href="http:\/\/www.echofon.com\/" rel="nofollow">Echofon<\/a>",
93 | "text": "RT @pangratz: An Extremely Gentle Introduction to Ember.js - Part 1 by @bmelton http:\/\/t.co\/UVKmMUNR",
94 | "to_user": null,
95 | "to_user_id": null,
96 | "to_user_id_str": null,
97 | "to_user_name": null
98 | },
99 | {
100 | "created_at": "Fri, 06 Apr 2012 16:12:24 +0000",
101 | "from_user": "quartzmo",
102 | "from_user_id": 116046841,
103 | "from_user_id_str": "116046841",
104 | "from_user_name": "Chris Smith",
105 | "geo": null,
106 | "id": 188298118301892609,
107 | "id_str": "188298118301892609",
108 | "iso_language_code": "en",
109 | "metadata": {
110 | "result_type": "recent"
111 | },
112 | "profile_image_url": "http:\/\/a0.twimg.com\/profile_images\/708441488\/chris_avatar_photo_facebook_normal.png",
113 | "profile_image_url_https": "https:\/\/si0.twimg.com\/profile_images\/708441488\/chris_avatar_photo_facebook_normal.png",
114 | "source": "<a href="http:\/\/itunes.apple.com\/us\/app\/twitter\/id409789998?mt=12" rel="nofollow">Twitter for Mac<\/a>",
115 | "text": "My understanding is that if we start using #emberjs and #handlebarsjs, we all magically get mustaches. Poof! Awesome.",
116 | "to_user": null,
117 | "to_user_id": null,
118 | "to_user_id_str": null,
119 | "to_user_name": null
120 | },
121 | {
122 | "created_at": "Fri, 06 Apr 2012 14:53:57 +0000",
123 | "from_user": "pangratz",
124 | "from_user_id": 49576387,
125 | "from_user_id_str": "49576387",
126 | "from_user_name": "Clemens M\u00fcller",
127 | "geo": null,
128 | "id": 188278375767478272,
129 | "id_str": "188278375767478272",
130 | "iso_language_code": "en",
131 | "metadata": {
132 | "result_type": "recent"
133 | },
134 | "profile_image_url": "http:\/\/a0.twimg.com\/profile_images\/1866862141\/IMG_3719_normal.JPG",
135 | "profile_image_url_https": "https:\/\/si0.twimg.com\/profile_images\/1866862141\/IMG_3719_normal.JPG",
136 | "source": "<a href="http:\/\/www.echofon.com\/" rel="nofollow">Echofon<\/a>",
137 | "text": "An Extremely Gentle Introduction to Ember.js - Part 1 by @bmelton http:\/\/t.co\/UVKmMUNR",
138 | "to_user": null,
139 | "to_user_id": null,
140 | "to_user_id_str": null,
141 | "to_user_name": null
142 | },
143 | {
144 | "created_at": "Fri, 06 Apr 2012 14:45:36 +0000",
145 | "from_user": "bmelton",
146 | "from_user_id": 17073443,
147 | "from_user_id_str": "17073443",
148 | "from_user_name": "bmelton",
149 | "geo": null,
150 | "id": 188276276417667072,
151 | "id_str": "188276276417667072",
152 | "iso_language_code": "en",
153 | "metadata": {
154 | "result_type": "recent"
155 | },
156 | "profile_image_url": "http:\/\/a0.twimg.com\/profile_images\/952738636\/calvin_and_hobbes_normal.jpg",
157 | "profile_image_url_https": "https:\/\/si0.twimg.com\/profile_images\/952738636\/calvin_and_hobbes_normal.jpg",
158 | "source": "<a href="http:\/\/twitter.com\/">web<\/a>",
159 | "text": "@agilemeister Working on easing the learning curve for Ember. Hopefully it will help some? -- http:\/\/t.co\/BdcXr7mG",
160 | "to_user": "agilemeister",
161 | "to_user_id": 68325967,
162 | "to_user_id_str": "68325967",
163 | "to_user_name": "Angeline Tan",
164 | "in_reply_to_status_id": 188083765459951617,
165 | "in_reply_to_status_id_str": "188083765459951617"
166 | },
167 | {
168 | "created_at": "Fri, 06 Apr 2012 05:14:18 +0000",
169 | "from_user": "brianvoss",
170 | "from_user_id": 14836811,
171 | "from_user_id_str": "14836811",
172 | "from_user_name": "Brian Voss",
173 | "geo": null,
174 | "id": 188132500667772928,
175 | "id_str": "188132500667772928",
176 | "iso_language_code": "en",
177 | "metadata": {
178 | "result_type": "recent"
179 | },
180 | "profile_image_url": "http:\/\/a0.twimg.com\/profile_images\/1333296999\/headshot_normal.jpg",
181 | "profile_image_url_https": "https:\/\/si0.twimg.com\/profile_images\/1333296999\/headshot_normal.jpg",
182 | "source": "<a href="http:\/\/twitter.com\/">web<\/a>",
183 | "text": "@petele Thanks again for a great session on modern web apps! Going to be playing with EmberJS and LawnChair a lot more! #GTUG",
184 | "to_user": "petele",
185 | "to_user_id": 757903,
186 | "to_user_id_str": "757903",
187 | "to_user_name": "Pete LePage"
188 | },
189 | {
190 | "created_at": "Fri, 06 Apr 2012 03:37:41 +0000",
191 | "from_user": "BenATkin",
192 | "from_user_id": 64218381,
193 | "from_user_id_str": "64218381",
194 | "from_user_name": "benatkin.com",
195 | "geo": null,
196 | "id": 188108187650629632,
197 | "id_str": "188108187650629632",
198 | "iso_language_code": "en",
199 | "metadata": {
200 | "result_type": "recent"
201 | },
202 | "profile_image_url": "http:\/\/a0.twimg.com\/profile_images\/1794836902\/twitter_normal.jpg",
203 | "profile_image_url_https": "https:\/\/si0.twimg.com\/profile_images\/1794836902\/twitter_normal.jpg",
204 | "source": "<a href="http:\/\/twitter.com\/">web<\/a>",
205 | "text": "skimmed this morning, didn't grok; read tonight, grokked http:\/\/t.co\/WSSXVZ6G #emberjs",
206 | "to_user": null,
207 | "to_user_id": null,
208 | "to_user_id_str": null,
209 | "to_user_name": null
210 | },
211 | {
212 | "created_at": "Thu, 05 Apr 2012 21:55:48 +0000",
213 | "from_user": "lukemelia",
214 | "from_user_id": 6134412,
215 | "from_user_id_str": "6134412",
216 | "from_user_name": "Luke Melia",
217 | "geo": null,
218 | "id": 188022149666189313,
219 | "id_str": "188022149666189313",
220 | "iso_language_code": "en",
221 | "metadata": {
222 | "result_type": "recent"
223 | },
224 | "profile_image_url": "http:\/\/a0.twimg.com\/profile_images\/1768827648\/image1327076921_normal.png",
225 | "profile_image_url_https": "https:\/\/si0.twimg.com\/profile_images\/1768827648\/image1327076921_normal.png",
226 | "source": "<a href="http:\/\/itunes.apple.com\/us\/app\/twitter\/id409789998?mt=12" rel="nofollow">Twitter for Mac<\/a>",
227 | "text": "The intersection of routing and statemachines is important to the next wave of JS browser apps: see @ghempton http:\/\/t.co\/MJ05YVvk #emberjs",
228 | "to_user": null,
229 | "to_user_id": null,
230 | "to_user_id_str": null,
231 | "to_user_name": null
232 | },
233 | {
234 | "created_at": "Thu, 05 Apr 2012 20:42:06 +0000",
235 | "from_user": "climboid",
236 | "from_user_id": 214605119,
237 | "from_user_id_str": "214605119",
238 | "from_user_name": "Oscar Villarreal",
239 | "geo": null,
240 | "id": 188003604240666624,
241 | "id_str": "188003604240666624",
242 | "iso_language_code": "en",
243 | "metadata": {
244 | "result_type": "recent"
245 | },
246 | "profile_image_url": "http:\/\/a0.twimg.com\/profile_images\/1844431824\/me_normal.jpg",
247 | "profile_image_url_https": "https:\/\/si0.twimg.com\/profile_images\/1844431824\/me_normal.jpg",
248 | "source": "<a href="http:\/\/twitter.com\/tweetbutton" rel="nofollow">Tweet Button<\/a>",
249 | "text": "events on arrayproxy emberjs http:\/\/t.co\/3XQbZQfA",
250 | "to_user": null,
251 | "to_user_id": null,
252 | "to_user_id_str": null,
253 | "to_user_name": null
254 | },
255 | {
256 | "created_at": "Thu, 05 Apr 2012 17:24:28 +0000",
257 | "from_user": "alexrothenberg",
258 | "from_user_id": 17340043,
259 | "from_user_id_str": "17340043",
260 | "from_user_name": "alexrothenberg",
261 | "geo": null,
262 | "id": 187953868125114368,
263 | "id_str": "187953868125114368",
264 | "iso_language_code": "en",
265 | "metadata": {
266 | "result_type": "recent"
267 | },
268 | "profile_image_url": "http:\/\/a0.twimg.com\/profile_images\/518386110\/alex_bw_normal.jpg",
269 | "profile_image_url_https": "https:\/\/si0.twimg.com\/profile_images\/518386110\/alex_bw_normal.jpg",
270 | "source": "<a href="http:\/\/itunes.apple.com\/us\/app\/twitter\/id409789998?mt=12" rel="nofollow">Twitter for Mac<\/a>",
271 | "text": "Nice article on testing #emberjs apps and the run loop \u201c@dagda1: How I Learned to Stop Worrying and Love the Runloop\" http:\/\/t.co\/PH3sEiNS\u201d",
272 | "to_user": null,
273 | "to_user_id": null,
274 | "to_user_id_str": null,
275 | "to_user_name": null
276 | },
277 | {
278 | "created_at": "Thu, 05 Apr 2012 16:27:28 +0000",
279 | "from_user": "distracteddev",
280 | "from_user_id": 272517143,
281 | "from_user_id_str": "272517143",
282 | "from_user_name": "Zeus Lalkaka",
283 | "geo": null,
284 | "id": 187939522925969408,
285 | "id_str": "187939522925969408",
286 | "iso_language_code": "en",
287 | "metadata": {
288 | "result_type": "recent"
289 | },
290 | "profile_image_url": "http:\/\/a0.twimg.com\/profile_images\/1681547896\/image_normal.jpg",
291 | "profile_image_url_https": "https:\/\/si0.twimg.com\/profile_images\/1681547896\/image_normal.jpg",
292 | "source": "<a href="http:\/\/www.apple.com" rel="nofollow">Safari on iOS<\/a>",
293 | "text": "RIL http:\/\/t.co\/DKL2dUtQ",
294 | "to_user": null,
295 | "to_user_id": null,
296 | "to_user_id_str": null,
297 | "to_user_name": null
298 | },
299 | {
300 | "created_at": "Thu, 05 Apr 2012 08:27:01 +0000",
301 | "from_user": "NewsNodejs",
302 | "from_user_id": 486450041,
303 | "from_user_id_str": "486450041",
304 | "from_user_name": "nodejs-news",
305 | "geo": null,
306 | "id": 187818613749714944,
307 | "id_str": "187818613749714944",
308 | "iso_language_code": "en",
309 | "metadata": {
310 | "result_type": "recent"
311 | },
312 | "profile_image_url": "http:\/\/a0.twimg.com\/profile_images\/1812607216\/nodejs-news_normal.png",
313 | "profile_image_url_https": "https:\/\/si0.twimg.com\/profile_images\/1812607216\/nodejs-news_normal.png",
314 | "source": "<a href="http:\/\/www.scoop.it" rel="nofollow">Scoop.it<\/a>",
315 | "text": "Ember and D3: Building responsive analytics | #javascript #3Djs #emberjs http:\/\/t.co\/4LIJRJ9U",
316 | "to_user": null,
317 | "to_user_id": null,
318 | "to_user_id_str": null,
319 | "to_user_name": null
320 | },
321 | {
322 | "created_at": "Thu, 05 Apr 2012 07:15:07 +0000",
323 | "from_user": "mathrobin",
324 | "from_user_id": 133651103,
325 | "from_user_id_str": "133651103",
326 | "from_user_name": "mathrobin",
327 | "geo": null,
328 | "id": 187800519392239616,
329 | "id_str": "187800519392239616",
330 | "iso_language_code": "fr",
331 | "metadata": {
332 | "result_type": "recent"
333 | },
334 | "profile_image_url": "http:\/\/a0.twimg.com\/profile_images\/1369959547\/Avatar_normal.png",
335 | "profile_image_url_https": "https:\/\/si0.twimg.com\/profile_images\/1369959547\/Avatar_normal.png",
336 | "source": "<a href="http:\/\/www.hootsuite.com" rel="nofollow">HootSuite<\/a>",
337 | "text": "Astuce #Emberjs : r\u00e9ussir vos tableaux http:\/\/t.co\/IcM530nz",
338 | "to_user": null,
339 | "to_user_id": null,
340 | "to_user_id_str": null,
341 | "to_user_name": null
342 | }],
343 | "results_per_page": 15,
344 | "since_id": 0,
345 | "since_id_str": "0"
346 | }
--------------------------------------------------------------------------------
/app/vendor/moment.js:
--------------------------------------------------------------------------------
1 | // moment.js
2 | // version : 1.5.0
3 | // author : Tim Wood
4 | // license : MIT
5 | // momentjs.com
6 |
7 | (function (Date, undefined) {
8 |
9 | var moment,
10 | round = Math.round,
11 | languages = {},
12 | hasModule = (typeof module !== 'undefined'),
13 | paramsToParse = 'months|monthsShort|monthsParse|weekdays|weekdaysShort|longDateFormat|calendar|relativeTime|ordinal|meridiem'.split('|'),
14 | i,
15 | jsonRegex = /^\/?Date\((\-?\d+)/i,
16 | charactersToReplace = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|dddd?|do?|w[o|w]?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|zz?|ZZ?|LT|LL?L?L?)/g,
17 | nonuppercaseLetters = /[^A-Z]/g,
18 | timezoneRegex = /\([A-Za-z ]+\)|:[0-9]{2} [A-Z]{3} /g,
19 | tokenCharacters = /(\\)?(MM?M?M?|dd?d?d|DD?D?D?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|ZZ?|T)/g,
20 | inputCharacters = /(\\)?([0-9]+|([a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+|([\+\-]\d\d:?\d\d))/gi,
21 | isoRegex = /\d{4}.\d\d.\d\d(T(\d\d(.\d\d(.\d\d)?)?)?([\+\-]\d\d:?\d\d)?)?/,
22 | isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
23 | isoTimes = [
24 | ['HH:mm:ss', /T\d\d:\d\d:\d\d/],
25 | ['HH:mm', /T\d\d:\d\d/],
26 | ['HH', /T\d\d/]
27 | ],
28 | timezoneParseRegex = /([\+\-]|\d\d)/gi,
29 | VERSION = "1.5.0",
30 | shortcuts = 'Month|Date|Hours|Minutes|Seconds|Milliseconds'.split('|');
31 |
32 | // Moment prototype object
33 | function Moment(date, isUTC) {
34 | this._d = date;
35 | this._isUTC = !!isUTC;
36 | }
37 |
38 | // left zero fill a number
39 | // see http://jsperf.com/left-zero-filling for performance comparison
40 | function leftZeroFill(number, targetLength) {
41 | var output = number + '';
42 | while (output.length < targetLength) {
43 | output = '0' + output;
44 | }
45 | return output;
46 | }
47 |
48 | // helper function for _.addTime and _.subtractTime
49 | function dateAddRemove(date, _input, adding, val) {
50 | var isString = (typeof _input === 'string'),
51 | input = isString ? {} : _input,
52 | ms, d, M, currentDate;
53 | if (isString && val) {
54 | input[_input] = +val;
55 | }
56 | ms = (input.ms || input.milliseconds || 0) +
57 | (input.s || input.seconds || 0) * 1e3 + // 1000
58 | (input.m || input.minutes || 0) * 6e4 + // 1000 * 60
59 | (input.h || input.hours || 0) * 36e5; // 1000 * 60 * 60
60 | d = (input.d || input.days || 0) +
61 | (input.w || input.weeks || 0) * 7;
62 | M = (input.M || input.months || 0) +
63 | (input.y || input.years || 0) * 12;
64 | if (ms) {
65 | date.setTime(+date + ms * adding);
66 | }
67 | if (d) {
68 | date.setDate(date.getDate() + d * adding);
69 | }
70 | if (M) {
71 | currentDate = date.getDate();
72 | date.setDate(1);
73 | date.setMonth(date.getMonth() + M * adding);
74 | date.setDate(Math.min(new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate(), currentDate));
75 | }
76 | return date;
77 | }
78 |
79 | // check if is an array
80 | function isArray(input) {
81 | return Object.prototype.toString.call(input) === '[object Array]';
82 | }
83 |
84 | // convert an array to a date.
85 | // the array should mirror the parameters below
86 | // note: all values past the year are optional and will default to the lowest possible value.
87 | // [year, month, day , hour, minute, second, millisecond]
88 | function dateFromArray(input) {
89 | return new Date(input[0], input[1] || 0, input[2] || 1, input[3] || 0, input[4] || 0, input[5] || 0, input[6] || 0);
90 | }
91 |
92 | // format date using native date object
93 | function formatMoment(m, inputString) {
94 | var currentMonth = m.month(),
95 | currentDate = m.date(),
96 | currentYear = m.year(),
97 | currentDay = m.day(),
98 | currentHours = m.hours(),
99 | currentMinutes = m.minutes(),
100 | currentSeconds = m.seconds(),
101 | currentZone = -m.zone(),
102 | ordinal = moment.ordinal,
103 | meridiem = moment.meridiem;
104 | // check if the character is a format
105 | // return formatted string or non string.
106 | //
107 | // uses switch/case instead of an object of named functions (like http://phpjs.org/functions/date:380)
108 | // for minification and performance
109 | // see http://jsperf.com/object-of-functions-vs-switch for performance comparison
110 | function replaceFunction(input) {
111 | // create a couple variables to be used later inside one of the cases.
112 | var a, b;
113 | switch (input) {
114 | // MONTH
115 | case 'M' :
116 | return currentMonth + 1;
117 | case 'Mo' :
118 | return (currentMonth + 1) + ordinal(currentMonth + 1);
119 | case 'MM' :
120 | return leftZeroFill(currentMonth + 1, 2);
121 | case 'MMM' :
122 | return moment.monthsShort[currentMonth];
123 | case 'MMMM' :
124 | return moment.months[currentMonth];
125 | // DAY OF MONTH
126 | case 'D' :
127 | return currentDate;
128 | case 'Do' :
129 | return currentDate + ordinal(currentDate);
130 | case 'DD' :
131 | return leftZeroFill(currentDate, 2);
132 | // DAY OF YEAR
133 | case 'DDD' :
134 | a = new Date(currentYear, currentMonth, currentDate);
135 | b = new Date(currentYear, 0, 1);
136 | return ~~ (((a - b) / 864e5) + 1.5);
137 | case 'DDDo' :
138 | a = replaceFunction('DDD');
139 | return a + ordinal(a);
140 | case 'DDDD' :
141 | return leftZeroFill(replaceFunction('DDD'), 3);
142 | // WEEKDAY
143 | case 'd' :
144 | return currentDay;
145 | case 'do' :
146 | return currentDay + ordinal(currentDay);
147 | case 'ddd' :
148 | return moment.weekdaysShort[currentDay];
149 | case 'dddd' :
150 | return moment.weekdays[currentDay];
151 | // WEEK OF YEAR
152 | case 'w' :
153 | a = new Date(currentYear, currentMonth, currentDate - currentDay + 5);
154 | b = new Date(a.getFullYear(), 0, 4);
155 | return ~~ ((a - b) / 864e5 / 7 + 1.5);
156 | case 'wo' :
157 | a = replaceFunction('w');
158 | return a + ordinal(a);
159 | case 'ww' :
160 | return leftZeroFill(replaceFunction('w'), 2);
161 | // YEAR
162 | case 'YY' :
163 | return leftZeroFill(currentYear % 100, 2);
164 | case 'YYYY' :
165 | return currentYear;
166 | // AM / PM
167 | case 'a' :
168 | return currentHours > 11 ? meridiem.pm : meridiem.am;
169 | case 'A' :
170 | return currentHours > 11 ? meridiem.PM : meridiem.AM;
171 | // 24 HOUR
172 | case 'H' :
173 | return currentHours;
174 | case 'HH' :
175 | return leftZeroFill(currentHours, 2);
176 | // 12 HOUR
177 | case 'h' :
178 | return currentHours % 12 || 12;
179 | case 'hh' :
180 | return leftZeroFill(currentHours % 12 || 12, 2);
181 | // MINUTE
182 | case 'm' :
183 | return currentMinutes;
184 | case 'mm' :
185 | return leftZeroFill(currentMinutes, 2);
186 | // SECOND
187 | case 's' :
188 | return currentSeconds;
189 | case 'ss' :
190 | return leftZeroFill(currentSeconds, 2);
191 | // TIMEZONE
192 | case 'zz' :
193 | // depreciating 'zz' fall through to 'z'
194 | case 'z' :
195 | return (m._d.toString().match(timezoneRegex) || [''])[0].replace(nonuppercaseLetters, '');
196 | case 'Z' :
197 | return (currentZone < 0 ? '-' : '+') + leftZeroFill(~~(Math.abs(currentZone) / 60), 2) + ':' + leftZeroFill(~~(Math.abs(currentZone) % 60), 2);
198 | case 'ZZ' :
199 | return (currentZone < 0 ? '-' : '+') + leftZeroFill(~~(10 * Math.abs(currentZone) / 6), 4);
200 | // LONG DATES
201 | case 'L' :
202 | case 'LL' :
203 | case 'LLL' :
204 | case 'LLLL' :
205 | case 'LT' :
206 | return formatMoment(m, moment.longDateFormat[input]);
207 | // DEFAULT
208 | default :
209 | return input.replace(/(^\[)|(\\)|\]$/g, "");
210 | }
211 | }
212 | return inputString.replace(charactersToReplace, replaceFunction);
213 | }
214 |
215 | // date from string and format string
216 | function makeDateFromStringAndFormat(string, format) {
217 | var inArray = [0, 0, 1, 0, 0, 0, 0],
218 | timezoneHours = 0,
219 | timezoneMinutes = 0,
220 | isUsingUTC = false,
221 | inputParts = string.match(inputCharacters),
222 | formatParts = format.match(tokenCharacters),
223 | len = Math.min(inputParts.length, formatParts.length),
224 | i,
225 | isPm;
226 |
227 | // function to convert string input to date
228 | function addTime(format, input) {
229 | var a;
230 | switch (format) {
231 | // MONTH
232 | case 'M' :
233 | // fall through to MM
234 | case 'MM' :
235 | inArray[1] = ~~input - 1;
236 | break;
237 | case 'MMM' :
238 | // fall through to MMMM
239 | case 'MMMM' :
240 | for (a = 0; a < 12; a++) {
241 | if (moment.monthsParse[a].test(input)) {
242 | inArray[1] = a;
243 | break;
244 | }
245 | }
246 | break;
247 | // DAY OF MONTH
248 | case 'D' :
249 | // fall through to DDDD
250 | case 'DD' :
251 | // fall through to DDDD
252 | case 'DDD' :
253 | // fall through to DDDD
254 | case 'DDDD' :
255 | inArray[2] = ~~input;
256 | break;
257 | // YEAR
258 | case 'YY' :
259 | input = ~~input;
260 | inArray[0] = input + (input > 70 ? 1900 : 2000);
261 | break;
262 | case 'YYYY' :
263 | inArray[0] = ~~Math.abs(input);
264 | break;
265 | // AM / PM
266 | case 'a' :
267 | // fall through to A
268 | case 'A' :
269 | isPm = (input.toLowerCase() === 'pm');
270 | break;
271 | // 24 HOUR
272 | case 'H' :
273 | // fall through to hh
274 | case 'HH' :
275 | // fall through to hh
276 | case 'h' :
277 | // fall through to hh
278 | case 'hh' :
279 | inArray[3] = ~~input;
280 | break;
281 | // MINUTE
282 | case 'm' :
283 | // fall through to mm
284 | case 'mm' :
285 | inArray[4] = ~~input;
286 | break;
287 | // SECOND
288 | case 's' :
289 | // fall through to ss
290 | case 'ss' :
291 | inArray[5] = ~~input;
292 | break;
293 | // TIMEZONE
294 | case 'Z' :
295 | // fall through to ZZ
296 | case 'ZZ' :
297 | isUsingUTC = true;
298 | a = (input || '').match(timezoneParseRegex);
299 | if (a && a[1]) {
300 | timezoneHours = ~~a[1];
301 | }
302 | if (a && a[2]) {
303 | timezoneMinutes = ~~a[2];
304 | }
305 | // reverse offsets
306 | if (a && a[0] === '+') {
307 | timezoneHours = -timezoneHours;
308 | timezoneMinutes = -timezoneMinutes;
309 | }
310 | break;
311 | }
312 | }
313 | for (i = 0; i < len; i++) {
314 | addTime(formatParts[i], inputParts[i]);
315 | }
316 | // handle am pm
317 | if (isPm && inArray[3] < 12) {
318 | inArray[3] += 12;
319 | }
320 | // if is 12 am, change hours to 0
321 | if (isPm === false && inArray[3] === 12) {
322 | inArray[3] = 0;
323 | }
324 | // handle timezone
325 | inArray[3] += timezoneHours;
326 | inArray[4] += timezoneMinutes;
327 | // return
328 | return isUsingUTC ? new Date(Date.UTC.apply({}, inArray)) : dateFromArray(inArray);
329 | }
330 |
331 | // compare two arrays, return the number of differences
332 | function compareArrays(array1, array2) {
333 | var len = Math.min(array1.length, array2.length),
334 | lengthDiff = Math.abs(array1.length - array2.length),
335 | diffs = 0,
336 | i;
337 | for (i = 0; i < len; i++) {
338 | if (~~array1[i] !== ~~array2[i]) {
339 | diffs++;
340 | }
341 | }
342 | return diffs + lengthDiff;
343 | }
344 |
345 | // date from string and array of format strings
346 | function makeDateFromStringAndArray(string, formats) {
347 | var output,
348 | inputParts = string.match(inputCharacters),
349 | scores = [],
350 | scoreToBeat = 99,
351 | i,
352 | curDate,
353 | curScore;
354 | for (i = 0; i < formats.length; i++) {
355 | curDate = makeDateFromStringAndFormat(string, formats[i]);
356 | curScore = compareArrays(inputParts, formatMoment(new Moment(curDate), formats[i]).match(inputCharacters));
357 | if (curScore < scoreToBeat) {
358 | scoreToBeat = curScore;
359 | output = curDate;
360 | }
361 | }
362 | return output;
363 | }
364 |
365 | // date from iso format
366 | function makeDateFromString(string) {
367 | var format = 'YYYY-MM-DDT',
368 | i;
369 | if (isoRegex.exec(string)) {
370 | for (i = 0; i < 3; i++) {
371 | if (isoTimes[i][1].exec(string)) {
372 | format += isoTimes[i][0];
373 | break;
374 | }
375 | }
376 | return makeDateFromStringAndFormat(string, format + 'Z');
377 | }
378 | return new Date(string);
379 | }
380 |
381 | // helper function for _date.from() and _date.fromNow()
382 | function substituteTimeAgo(string, number, withoutSuffix) {
383 | var rt = moment.relativeTime[string];
384 | return (typeof rt === 'function') ?
385 | rt(number || 1, !!withoutSuffix, string) :
386 | rt.replace(/%d/i, number || 1);
387 | }
388 |
389 | function relativeTime(milliseconds, withoutSuffix) {
390 | var seconds = round(Math.abs(milliseconds) / 1000),
391 | minutes = round(seconds / 60),
392 | hours = round(minutes / 60),
393 | days = round(hours / 24),
394 | years = round(days / 365),
395 | args = seconds < 45 && ['s', seconds] ||
396 | minutes === 1 && ['m'] ||
397 | minutes < 45 && ['mm', minutes] ||
398 | hours === 1 && ['h'] ||
399 | hours < 22 && ['hh', hours] ||
400 | days === 1 && ['d'] ||
401 | days <= 25 && ['dd', days] ||
402 | days <= 45 && ['M'] ||
403 | days < 345 && ['MM', round(days / 30)] ||
404 | years === 1 && ['y'] || ['yy', years];
405 | args[2] = withoutSuffix;
406 | return substituteTimeAgo.apply({}, args);
407 | }
408 |
409 | moment = function (input, format) {
410 | if (input === null || input === '') {
411 | return null;
412 | }
413 | var date,
414 | matched;
415 | // parse Moment object
416 | if (input && input._d instanceof Date) {
417 | date = new Date(+input._d);
418 | // parse string and format
419 | } else if (format) {
420 | if (isArray(format)) {
421 | date = makeDateFromStringAndArray(input, format);
422 | } else {
423 | date = makeDateFromStringAndFormat(input, format);
424 | }
425 | // evaluate it as a JSON-encoded date
426 | } else {
427 | matched = jsonRegex.exec(input);
428 | date = input === undefined ? new Date() :
429 | matched ? new Date(+matched[1]) :
430 | input instanceof Date ? input :
431 | isArray(input) ? dateFromArray(input) :
432 | typeof input === 'string' ? makeDateFromString(input) :
433 | new Date(input);
434 | }
435 | return new Moment(date);
436 | };
437 |
438 | // creating with utc
439 | moment.utc = function (input, format) {
440 | if (isArray(input)) {
441 | return new Moment(new Date(Date.UTC.apply({}, input)), true);
442 | }
443 | return (format && input) ? moment(input + ' 0', format + ' Z').utc() : moment(input).utc();
444 | };
445 |
446 | // humanizeDuration
447 | moment.humanizeDuration = function (num, type, withSuffix) {
448 | var difference = +num,
449 | rel = moment.relativeTime,
450 | output;
451 | switch (type) {
452 | case "seconds" :
453 | difference *= 1000; // 1000
454 | break;
455 | case "minutes" :
456 | difference *= 60000; // 60 * 1000
457 | break;
458 | case "hours" :
459 | difference *= 3600000; // 60 * 60 * 1000
460 | break;
461 | case "days" :
462 | difference *= 86400000; // 24 * 60 * 60 * 1000
463 | break;
464 | case "weeks" :
465 | difference *= 604800000; // 7 * 24 * 60 * 60 * 1000
466 | break;
467 | case "months" :
468 | difference *= 2592000000; // 30 * 24 * 60 * 60 * 1000
469 | break;
470 | case "years" :
471 | difference *= 31536000000; // 365 * 24 * 60 * 60 * 1000
472 | break;
473 | default :
474 | withSuffix = !!type;
475 | break;
476 | }
477 | output = relativeTime(difference, !withSuffix);
478 | return withSuffix ? (difference <= 0 ? rel.past : rel.future).replace(/%s/i, output) : output;
479 | };
480 |
481 | // version number
482 | moment.version = VERSION;
483 |
484 | // default format
485 | moment.defaultFormat = isoFormat;
486 |
487 | // language switching and caching
488 | moment.lang = function (key, values) {
489 | var i,
490 | param,
491 | req,
492 | parse = [];
493 | if (values) {
494 | for (i = 0; i < 12; i++) {
495 | parse[i] = new RegExp('^' + values.months[i] + '|^' + values.monthsShort[i].replace('.', ''), 'i');
496 | }
497 | values.monthsParse = values.monthsParse || parse;
498 | languages[key] = values;
499 | }
500 | if (languages[key]) {
501 | for (i = 0; i < paramsToParse.length; i++) {
502 | param = paramsToParse[i];
503 | moment[param] = languages[key][param] || moment[param];
504 | }
505 | } else {
506 | if (hasModule) {
507 | req = require('./lang/' + key);
508 | moment.lang(key, req);
509 | }
510 | }
511 | };
512 |
513 | // set default language
514 | moment.lang('en', {
515 | months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
516 | monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
517 | weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
518 | weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
519 | longDateFormat : {
520 | LT : "h:mm A",
521 | L : "MM/DD/YYYY",
522 | LL : "MMMM D YYYY",
523 | LLL : "MMMM D YYYY LT",
524 | LLLL : "dddd, MMMM D YYYY LT"
525 | },
526 | meridiem : {
527 | AM : 'AM',
528 | am : 'am',
529 | PM : 'PM',
530 | pm : 'pm'
531 | },
532 | calendar : {
533 | sameDay : '[Today at] LT',
534 | nextDay : '[Tomorrow at] LT',
535 | nextWeek : 'dddd [at] LT',
536 | lastDay : '[Yesterday at] LT',
537 | lastWeek : '[last] dddd [at] LT',
538 | sameElse : 'L'
539 | },
540 | relativeTime : {
541 | future : "in %s",
542 | past : "%s ago",
543 | s : "a few seconds",
544 | m : "a minute",
545 | mm : "%d minutes",
546 | h : "an hour",
547 | hh : "%d hours",
548 | d : "a day",
549 | dd : "%d days",
550 | M : "a month",
551 | MM : "%d months",
552 | y : "a year",
553 | yy : "%d years"
554 | },
555 | ordinal : function (number) {
556 | var b = number % 10;
557 | return (~~ (number % 100 / 10) === 1) ? 'th' :
558 | (b === 1) ? 'st' :
559 | (b === 2) ? 'nd' :
560 | (b === 3) ? 'rd' : 'th';
561 | }
562 | });
563 |
564 | // compare moment object
565 | moment.isMoment = function (obj) {
566 | return obj instanceof Moment;
567 | };
568 |
569 | // shortcut for prototype
570 | moment.fn = Moment.prototype = {
571 |
572 | clone : function () {
573 | return moment(this);
574 | },
575 |
576 | valueOf : function () {
577 | return +this._d;
578 | },
579 |
580 | 'native' : function () {
581 | return this._d;
582 | },
583 |
584 | toString : function () {
585 | return this._d.toString();
586 | },
587 |
588 | toDate : function () {
589 | return this._d;
590 | },
591 |
592 | utc : function () {
593 | this._isUTC = true;
594 | return this;
595 | },
596 |
597 | local : function () {
598 | this._isUTC = false;
599 | return this;
600 | },
601 |
602 | format : function (inputString) {
603 | return formatMoment(this, inputString ? inputString : moment.defaultFormat);
604 | },
605 |
606 | add : function (input, val) {
607 | this._d = dateAddRemove(this._d, input, 1, val);
608 | return this;
609 | },
610 |
611 | subtract : function (input, val) {
612 | this._d = dateAddRemove(this._d, input, -1, val);
613 | return this;
614 | },
615 |
616 | diff : function (input, val, asFloat) {
617 | var inputMoment = moment(input),
618 | zoneDiff = (this.zone() - inputMoment.zone()) * 6e4,
619 | diff = this._d - inputMoment._d - zoneDiff,
620 | year = this.year() - inputMoment.year(),
621 | month = this.month() - inputMoment.month(),
622 | date = this.date() - inputMoment.date(),
623 | output;
624 | if (val === 'months') {
625 | output = year * 12 + month + date / 30;
626 | } else if (val === 'years') {
627 | output = year + month / 12;
628 | } else {
629 | output = val === 'seconds' ? diff / 1e3 : // 1000
630 | val === 'minutes' ? diff / 6e4 : // 1000 * 60
631 | val === 'hours' ? diff / 36e5 : // 1000 * 60 * 60
632 | val === 'days' ? diff / 864e5 : // 1000 * 60 * 60 * 24
633 | val === 'weeks' ? diff / 6048e5 : // 1000 * 60 * 60 * 24 * 7
634 | diff;
635 | }
636 | return asFloat ? output : round(output);
637 | },
638 |
639 | from : function (time, withoutSuffix) {
640 | return moment.humanizeDuration(this.diff(time), !withoutSuffix);
641 | },
642 |
643 | fromNow : function (withoutSuffix) {
644 | return this.from(moment(), withoutSuffix);
645 | },
646 |
647 | calendar : function () {
648 | var diff = this.diff(moment().sod(), 'days', true),
649 | calendar = moment.calendar,
650 | allElse = calendar.sameElse,
651 | format = diff < -6 ? allElse :
652 | diff < -1 ? calendar.lastWeek :
653 | diff < 0 ? calendar.lastDay :
654 | diff < 1 ? calendar.sameDay :
655 | diff < 2 ? calendar.nextDay :
656 | diff < 7 ? calendar.nextWeek : allElse;
657 | return this.format(typeof format === 'function' ? format.apply(this) : format);
658 | },
659 |
660 | isLeapYear : function () {
661 | var year = this.year();
662 | return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
663 | },
664 |
665 | isDST : function () {
666 | return (this.zone() < moment([this.year()]).zone() ||
667 | this.zone() < moment([this.year(), 5]).zone());
668 | },
669 |
670 | day : function (input) {
671 | var day = this._d.getDay();
672 | return input == null ? day :
673 | this.add({ d : input - day });
674 | },
675 |
676 | sod: function () {
677 | return this.clone()
678 | .hours(0)
679 | .minutes(0)
680 | .seconds(0)
681 | .milliseconds(0);
682 | },
683 |
684 | eod: function () {
685 | // end of day = start of day plus 1 day, minus 1 millisecond
686 | return this.sod().add({
687 | d : 1,
688 | ms : -1
689 | });
690 | },
691 |
692 | zone : function () {
693 | return this._isUTC ? 0 : this._d.getTimezoneOffset();
694 | },
695 |
696 | daysInMonth : function () {
697 | return this.clone().month(this.month() + 1).date(0).date();
698 | }
699 | };
700 |
701 | // helper for adding shortcuts
702 | function makeShortcut(name, key) {
703 | moment.fn[name] = function (input) {
704 | var utc = this._isUTC ? 'UTC' : '';
705 | if (input != null) {
706 | this._d['set' + utc + key](input);
707 | return this;
708 | } else {
709 | return this._d['get' + utc + key]();
710 | }
711 | };
712 | }
713 |
714 | // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
715 | for (i = 0; i < shortcuts.length; i ++) {
716 | makeShortcut(shortcuts[i].toLowerCase(), shortcuts[i]);
717 | }
718 |
719 | // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
720 | makeShortcut('year', 'FullYear');
721 |
722 | // CommonJS module is defined
723 | if (hasModule) {
724 | module.exports = moment;
725 | }
726 | if (typeof window !== 'undefined') {
727 | window.moment = moment;
728 | }
729 | /*global define:false */
730 | if (typeof define === "function" && define.amd) {
731 | define("moment", [], function () {
732 | return moment;
733 | });
734 | }
735 | })(Date);
736 |
--------------------------------------------------------------------------------
/test_sources/reddit.json:
--------------------------------------------------------------------------------
1 | {
2 | "kind": "Listing",
3 | "data": {
4 | "modhash": "48wdey9m7zb6ea56337dcb4921b6b43edda6337903d1cd56b9",
5 | "children": [{
6 | "kind": "t3",
7 | "data": {
8 | "domain": "emberist.herokuapp.com",
9 | "banned_by": null,
10 | "media_embed": {},
11 | "subreddit": "emberjs",
12 | "selftext_html": null,
13 | "selftext": "",
14 | "likes": true,
15 | "saved": false,
16 | "id": "rwwjc",
17 | "clicked": false,
18 | "title": "Difference between {{bind}} and {{bindAttr}}",
19 | "num_comments": 0,
20 | "score": 2,
21 | "approved_by": null,
22 | "over_18": false,
23 | "hidden": false,
24 | "thumbnail": "default",
25 | "subreddit_id": "t5_2tjb2",
26 | "author_flair_css_class": null,
27 | "downs": 0,
28 | "is_self": false,
29 | "permalink": "/r/emberjs/comments/rwwjc/difference_between_bind_and_bindattr/",
30 | "name": "t3_rwwjc",
31 | "created": 1333771949.0,
32 | "url": "https://emberist.herokuapp.com/2012/04/06/bind-and-bindattr.html",
33 | "author_flair_text": null,
34 | "author": "pangratz",
35 | "created_utc": 1333746749.0,
36 | "media": null,
37 | "num_reports": null,
38 | "ups": 2
39 | }
40 | },
41 | {
42 | "kind": "t3",
43 | "data": {
44 | "domain": "sympodial.com",
45 | "banned_by": null,
46 | "media_embed": {},
47 | "subreddit": "emberjs",
48 | "selftext_html": null,
49 | "selftext": "",
50 | "likes": true,
51 | "saved": false,
52 | "id": "rwblv",
53 | "clicked": false,
54 | "title": "An Extremely Gentle Introduction to Ember.js - Part 1",
55 | "num_comments": 0,
56 | "score": 3,
57 | "approved_by": null,
58 | "over_18": false,
59 | "hidden": false,
60 | "thumbnail": "default",
61 | "subreddit_id": "t5_2tjb2",
62 | "author_flair_css_class": null,
63 | "downs": 0,
64 | "is_self": false,
65 | "permalink": "/r/emberjs/comments/rwblv/an_extremely_gentle_introduction_to_emberjs_part_1/",
66 | "name": "t3_rwblv",
67 | "created": 1333749267.0,
68 | "url": "http://sympodial.com/blog/extremely-gentle-introduction-emberjs-part-1/",
69 | "author_flair_text": null,
70 | "author": "pangratz",
71 | "created_utc": 1333724067.0,
72 | "media": null,
73 | "num_reports": null,
74 | "ups": 3
75 | }
76 | },
77 | {
78 | "kind": "t3",
79 | "data": {
80 | "domain": "thesoftwaresimpleton.com",
81 | "banned_by": null,
82 | "media_embed": {},
83 | "subreddit": "emberjs",
84 | "selftext_html": null,
85 | "selftext": "",
86 | "likes": true,
87 | "saved": false,
88 | "id": "rum9j",
89 | "clicked": false,
90 | "title": "Unit Testing Ember.js, How I Learned to Stop Worrying and Love the Runloop",
91 | "num_comments": 0,
92 | "score": 3,
93 | "approved_by": null,
94 | "over_18": false,
95 | "hidden": false,
96 | "thumbnail": "http://b.thumbs.redditmedia.com/6pb8jlvhlleWPVIm.jpg",
97 | "subreddit_id": "t5_2tjb2",
98 | "author_flair_css_class": null,
99 | "downs": 0,
100 | "is_self": false,
101 | "permalink": "/r/emberjs/comments/rum9j/unit_testing_emberjs_how_i_learned_to_stop/",
102 | "name": "t3_rum9j",
103 | "created": 1333660131.0,
104 | "url": "http://www.thesoftwaresimpleton.com/blog/2012/04/03/testing-ember-and-the-runloop/",
105 | "author_flair_text": null,
106 | "author": "pangratz",
107 | "created_utc": 1333634931.0,
108 | "media": null,
109 | "num_reports": null,
110 | "ups": 3
111 | }
112 | },
113 | {
114 | "kind": "t3",
115 | "data": {
116 | "domain": "corner.squareup.com",
117 | "banned_by": null,
118 | "media_embed": {},
119 | "subreddit": "emberjs",
120 | "selftext_html": null,
121 | "selftext": "",
122 | "likes": true,
123 | "saved": false,
124 | "id": "rub67",
125 | "clicked": false,
126 | "title": "Square - Ember and D3: Building responsive analytics",
127 | "num_comments": 0,
128 | "score": 3,
129 | "approved_by": null,
130 | "over_18": false,
131 | "hidden": false,
132 | "thumbnail": "http://f.thumbs.redditmedia.com/uCAKDeDKrMxupYYk.jpg",
133 | "subreddit_id": "t5_2tjb2",
134 | "author_flair_css_class": null,
135 | "downs": 0,
136 | "is_self": false,
137 | "permalink": "/r/emberjs/comments/rub67/square_ember_and_d3_building_responsive_analytics/",
138 | "name": "t3_rub67",
139 | "created": 1333633758.0,
140 | "url": "http://corner.squareup.com/2012/04/building-analytics.html",
141 | "author_flair_text": null,
142 | "author": "pangratz",
143 | "created_utc": 1333608558.0,
144 | "media": null,
145 | "num_reports": null,
146 | "ups": 3
147 | }
148 | },
149 | {
150 | "kind": "t3",
151 | "data": {
152 | "domain": "code418.com",
153 | "banned_by": null,
154 | "media_embed": {},
155 | "subreddit": "emberjs",
156 | "selftext_html": null,
157 | "selftext": "",
158 | "likes": true,
159 | "saved": false,
160 | "id": "rog1y",
161 | "clicked": false,
162 | "title": "Changes in Ember.js v0.9.6",
163 | "num_comments": 0,
164 | "score": 3,
165 | "approved_by": null,
166 | "over_18": false,
167 | "hidden": false,
168 | "thumbnail": "default",
169 | "subreddit_id": "t5_2tjb2",
170 | "author_flair_css_class": null,
171 | "downs": 0,
172 | "is_self": false,
173 | "permalink": "/r/emberjs/comments/rog1y/changes_in_emberjs_v096/",
174 | "name": "t3_rog1y",
175 | "created": 1333312618.0,
176 | "url": "http://code418.com/blog/2012/04/01/changes-in-emberjs-v0.9.6/",
177 | "author_flair_text": null,
178 | "author": "pangratz",
179 | "created_utc": 1333312618.0,
180 | "media": null,
181 | "num_reports": null,
182 | "ups": 3
183 | }
184 | },
185 | {
186 | "kind": "t3",
187 | "data": {
188 | "domain": "code418.com",
189 | "banned_by": null,
190 | "media_embed": {},
191 | "subreddit": "emberjs",
192 | "selftext_html": null,
193 | "selftext": "",
194 | "likes": true,
195 | "saved": false,
196 | "id": "rnq2a",
197 | "clicked": false,
198 | "title": "Useful Ember.Observable functions",
199 | "num_comments": 0,
200 | "score": 1,
201 | "approved_by": null,
202 | "over_18": false,
203 | "hidden": false,
204 | "thumbnail": "default",
205 | "subreddit_id": "t5_2tjb2",
206 | "author_flair_css_class": null,
207 | "downs": 0,
208 | "is_self": false,
209 | "permalink": "/r/emberjs/comments/rnq2a/useful_emberobservable_functions/",
210 | "name": "t3_rnq2a",
211 | "created": 1333271133.0,
212 | "url": "http://code418.com/blog/2012/03/31/useful-observable-functions/",
213 | "author_flair_text": null,
214 | "author": "pangratz",
215 | "created_utc": 1333271133.0,
216 | "media": null,
217 | "num_reports": null,
218 | "ups": 1
219 | }
220 | },
221 | {
222 | "kind": "t3",
223 | "data": {
224 | "domain": "code418.com",
225 | "banned_by": null,
226 | "media_embed": {},
227 | "subreddit": "emberjs",
228 | "selftext_html": null,
229 | "selftext": "",
230 | "likes": true,
231 | "saved": false,
232 | "id": "rg64a",
233 | "clicked": false,
234 | "title": "Advanced Ember.js Bindings",
235 | "num_comments": 0,
236 | "score": 2,
237 | "approved_by": null,
238 | "over_18": false,
239 | "hidden": false,
240 | "thumbnail": "default",
241 | "subreddit_id": "t5_2tjb2",
242 | "author_flair_css_class": null,
243 | "downs": 0,
244 | "is_self": false,
245 | "permalink": "/r/emberjs/comments/rg64a/advanced_emberjs_bindings/",
246 | "name": "t3_rg64a",
247 | "created": 1332871765.0,
248 | "url": "http://code418.com/blog/2012/03/26/advanced-emberjs-bindings/",
249 | "author_flair_text": null,
250 | "author": "pangratz",
251 | "created_utc": 1332871765.0,
252 | "media": null,
253 | "num_reports": null,
254 | "ups": 2
255 | }
256 | },
257 | {
258 | "kind": "t3",
259 | "data": {
260 | "domain": "infoq.com",
261 | "banned_by": null,
262 | "media_embed": {},
263 | "subreddit": "emberjs",
264 | "selftext_html": null,
265 | "selftext": "",
266 | "likes": true,
267 | "saved": false,
268 | "id": "r8e21",
269 | "clicked": false,
270 | "title": "Ember.js: Rich Web Applications Done Right",
271 | "num_comments": 0,
272 | "score": 4,
273 | "approved_by": null,
274 | "over_18": false,
275 | "hidden": false,
276 | "thumbnail": "http://f.thumbs.redditmedia.com/A9YWRpqOgrASVBiS.jpg",
277 | "subreddit_id": "t5_2tjb2",
278 | "author_flair_css_class": null,
279 | "downs": 1,
280 | "is_self": false,
281 | "permalink": "/r/emberjs/comments/r8e21/emberjs_rich_web_applications_done_right/",
282 | "name": "t3_r8e21",
283 | "created": 1332426607.0,
284 | "url": "http://www.infoq.com/articles/emberjs",
285 | "author_flair_text": null,
286 | "author": "CritterM72800",
287 | "created_utc": 1332426607.0,
288 | "media": null,
289 | "num_reports": null,
290 | "ups": 5
291 | }
292 | },
293 | {
294 | "kind": "t3",
295 | "data": {
296 | "domain": "jsfiddle.net",
297 | "banned_by": null,
298 | "media_embed": {},
299 | "subreddit": "emberjs",
300 | "selftext_html": null,
301 | "selftext": "",
302 | "likes": null,
303 | "saved": false,
304 | "id": "qxsfa",
305 | "clicked": false,
306 | "title": "Backbone Events vs. Ember Bindings: A Benchmark",
307 | "num_comments": 1,
308 | "score": 1,
309 | "approved_by": null,
310 | "over_18": false,
311 | "hidden": false,
312 | "thumbnail": "default",
313 | "subreddit_id": "t5_2tjb2",
314 | "author_flair_css_class": null,
315 | "downs": 0,
316 | "is_self": false,
317 | "permalink": "/r/emberjs/comments/qxsfa/backbone_events_vs_ember_bindings_a_benchmark/",
318 | "name": "t3_qxsfa",
319 | "created": 1331821180.0,
320 | "url": "http://jsfiddle.net/jashkenas/CGSd5/",
321 | "author_flair_text": null,
322 | "author": "CritterM72800",
323 | "created_utc": 1331821180.0,
324 | "media": null,
325 | "num_reports": null,
326 | "ups": 1
327 | }
328 | },
329 | {
330 | "kind": "t3",
331 | "data": {
332 | "domain": "code418.com",
333 | "banned_by": null,
334 | "media_embed": {},
335 | "subreddit": "emberjs",
336 | "selftext_html": null,
337 | "selftext": "",
338 | "likes": true,
339 | "saved": false,
340 | "id": "qnk1a",
341 | "clicked": false,
342 | "title": "Useful Ember.js Functions",
343 | "num_comments": 0,
344 | "score": 3,
345 | "approved_by": null,
346 | "over_18": false,
347 | "hidden": false,
348 | "thumbnail": "default",
349 | "subreddit_id": "t5_2tjb2",
350 | "author_flair_css_class": null,
351 | "downs": 0,
352 | "is_self": false,
353 | "permalink": "/r/emberjs/comments/qnk1a/useful_emberjs_functions/",
354 | "name": "t3_qnk1a",
355 | "created": 1331228343.0,
356 | "url": "http://code418.com/blog/2012/03/08/useful-emberjs-functions/",
357 | "author_flair_text": null,
358 | "author": "pangratz",
359 | "created_utc": 1331228343.0,
360 | "media": null,
361 | "num_reports": null,
362 | "ups": 3
363 | }
364 | },
365 | {
366 | "kind": "t3",
367 | "data": {
368 | "domain": "code418.com",
369 | "banned_by": null,
370 | "media_embed": {},
371 | "subreddit": "emberjs",
372 | "selftext_html": null,
373 | "selftext": "",
374 | "likes": true,
375 | "saved": false,
376 | "id": "qkrls",
377 | "clicked": false,
378 | "title": "IRC log viewer using Hubot, CouchDB and Ember.js",
379 | "num_comments": 0,
380 | "score": 2,
381 | "approved_by": null,
382 | "over_18": false,
383 | "hidden": false,
384 | "thumbnail": "default",
385 | "subreddit_id": "t5_2tjb2",
386 | "author_flair_css_class": null,
387 | "downs": 0,
388 | "is_self": false,
389 | "permalink": "/r/emberjs/comments/qkrls/irc_log_viewer_using_hubot_couchdb_and_emberjs/",
390 | "name": "t3_qkrls",
391 | "created": 1331072530.0,
392 | "url": "http://code418.com/blog/2012/03/04/irc-log-viewer-using-hubot-couchdb-emberjs/",
393 | "author_flair_text": null,
394 | "author": "pangratz",
395 | "created_utc": 1331072530.0,
396 | "media": null,
397 | "num_reports": null,
398 | "ups": 2
399 | }
400 | },
401 | {
402 | "kind": "t3",
403 | "data": {
404 | "domain": "cerebris.com",
405 | "banned_by": null,
406 | "media_embed": {},
407 | "subreddit": "emberjs",
408 | "selftext_html": null,
409 | "selftext": "",
410 | "likes": true,
411 | "saved": false,
412 | "id": "qk9m2",
413 | "clicked": false,
414 | "title": "Understanding Ember.Object",
415 | "num_comments": 0,
416 | "score": 2,
417 | "approved_by": null,
418 | "over_18": false,
419 | "hidden": false,
420 | "thumbnail": "http://c.thumbs.redditmedia.com/O1aNWXOOUWj6076D.jpg",
421 | "subreddit_id": "t5_2tjb2",
422 | "author_flair_css_class": null,
423 | "downs": 0,
424 | "is_self": false,
425 | "permalink": "/r/emberjs/comments/qk9m2/understanding_emberobject/",
426 | "name": "t3_qk9m2",
427 | "created": 1331049989.0,
428 | "url": "http://www.cerebris.com/blog/2012/03/06/understanding-ember-object/",
429 | "author_flair_text": null,
430 | "author": "CritterM72800",
431 | "created_utc": 1331049989.0,
432 | "media": null,
433 | "num_reports": null,
434 | "ups": 2
435 | }
436 | },
437 | {
438 | "kind": "t3",
439 | "data": {
440 | "domain": "github.com",
441 | "banned_by": null,
442 | "media_embed": {},
443 | "subreddit": "emberjs",
444 | "selftext_html": null,
445 | "selftext": "",
446 | "likes": null,
447 | "saved": false,
448 | "id": "qehgl",
449 | "clicked": false,
450 | "title": "Quick Notes is a small example to showcase some of the capabilities of sproutcore-statechart and sproutcore-routing in Ember.js",
451 | "num_comments": 0,
452 | "score": 2,
453 | "approved_by": null,
454 | "over_18": false,
455 | "hidden": false,
456 | "thumbnail": "default",
457 | "subreddit_id": "t5_2tjb2",
458 | "author_flair_css_class": null,
459 | "downs": 0,
460 | "is_self": false,
461 | "permalink": "/r/emberjs/comments/qehgl/quick_notes_is_a_small_example_to_showcase_some/",
462 | "name": "t3_qehgl",
463 | "created": 1330698993.0,
464 | "url": "https://github.com/DominikGuzei/ember-routing-statechart-example",
465 | "author_flair_text": null,
466 | "author": "CritterM72800",
467 | "created_utc": 1330698993.0,
468 | "media": null,
469 | "num_reports": null,
470 | "ups": 2
471 | }
472 | },
473 | {
474 | "kind": "t3",
475 | "data": {
476 | "domain": "thesoftwaresimpleton.com",
477 | "banned_by": null,
478 | "media_embed": {},
479 | "subreddit": "emberjs",
480 | "selftext_html": null,
481 | "selftext": "",
482 | "likes": null,
483 | "saved": false,
484 | "id": "qehd1",
485 | "clicked": false,
486 | "title": "Ember.js - Model, View, StateMachine?",
487 | "num_comments": 0,
488 | "score": 1,
489 | "approved_by": null,
490 | "over_18": false,
491 | "hidden": false,
492 | "thumbnail": "http://b.thumbs.redditmedia.com/6pb8jlvhlleWPVIm.jpg",
493 | "subreddit_id": "t5_2tjb2",
494 | "author_flair_css_class": null,
495 | "downs": 0,
496 | "is_self": false,
497 | "permalink": "/r/emberjs/comments/qehd1/emberjs_model_view_statemachine/",
498 | "name": "t3_qehd1",
499 | "created": 1330698835.0,
500 | "url": "http://www.thesoftwaresimpleton.com/blog/2012/02/28/statemachine/",
501 | "author_flair_text": null,
502 | "author": "CritterM72800",
503 | "created_utc": 1330698835.0,
504 | "media": null,
505 | "num_reports": null,
506 | "ups": 1
507 | }
508 | },
509 | {
510 | "kind": "t3",
511 | "data": {
512 | "domain": "frodsan.com",
513 | "banned_by": null,
514 | "media_embed": {},
515 | "subreddit": "emberjs",
516 | "selftext_html": null,
517 | "selftext": "",
518 | "likes": true,
519 | "saved": false,
520 | "id": "qar10",
521 | "clicked": false,
522 | "title": "Submitting your first patch to Ember.js docs",
523 | "num_comments": 0,
524 | "score": 2,
525 | "approved_by": null,
526 | "over_18": false,
527 | "hidden": false,
528 | "thumbnail": "default",
529 | "subreddit_id": "t5_2tjb2",
530 | "author_flair_css_class": null,
531 | "downs": 0,
532 | "is_self": false,
533 | "permalink": "/r/emberjs/comments/qar10/submitting_your_first_patch_to_emberjs_docs/",
534 | "name": "t3_qar10",
535 | "created": 1330485976.0,
536 | "url": "http://www.frodsan.com/2012/02/21/contribute-to-ember-docs.html",
537 | "author_flair_text": null,
538 | "author": "CritterM72800",
539 | "created_utc": 1330485976.0,
540 | "media": null,
541 | "num_reports": null,
542 | "ups": 2
543 | }
544 | },
545 | {
546 | "kind": "t3",
547 | "data": {
548 | "domain": "github.com",
549 | "banned_by": null,
550 | "media_embed": {},
551 | "subreddit": "emberjs",
552 | "selftext_html": null,
553 | "selftext": "",
554 | "likes": true,
555 | "saved": false,
556 | "id": "qaqw1",
557 | "clicked": false,
558 | "title": "A simple Rails app for testing CRUD with Ember Data.",
559 | "num_comments": 0,
560 | "score": 2,
561 | "approved_by": null,
562 | "over_18": false,
563 | "hidden": false,
564 | "thumbnail": "default",
565 | "subreddit_id": "t5_2tjb2",
566 | "author_flair_css_class": null,
567 | "downs": 0,
568 | "is_self": false,
569 | "permalink": "/r/emberjs/comments/qaqw1/a_simple_rails_app_for_testing_crud_with_ember/",
570 | "name": "t3_qaqw1",
571 | "created": 1330485821.0,
572 | "url": "https://github.com/dgeb/ember_data_example",
573 | "author_flair_text": null,
574 | "author": "CritterM72800",
575 | "created_utc": 1330485821.0,
576 | "media": null,
577 | "num_reports": null,
578 | "ups": 2
579 | }
580 | },
581 | {
582 | "kind": "t3",
583 | "data": {
584 | "domain": "vimeo.com",
585 | "banned_by": null,
586 | "media_embed": {
587 | "content": "<iframe src=\"http://player.vimeo.com/video/37539737\" width=\"600\" height=\"338\" frameborder=\"0\" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>",
588 | "width": 600,
589 | "scrolling": false,
590 | "height": 338
591 | },
592 | "subreddit": "emberjs",
593 | "selftext_html": null,
594 | "selftext": "",
595 | "likes": true,
596 | "saved": false,
597 | "id": "qa2ij",
598 | "clicked": false,
599 | "title": "Video: Tom Dale talks about debugging Ember.js apps [58min]",
600 | "num_comments": 0,
601 | "score": 5,
602 | "approved_by": null,
603 | "over_18": false,
604 | "hidden": false,
605 | "thumbnail": "http://c.thumbs.redditmedia.com/eXhJgK50smloyI_n.jpg",
606 | "subreddit_id": "t5_2tjb2",
607 | "author_flair_css_class": null,
608 | "downs": 0,
609 | "is_self": false,
610 | "permalink": "/r/emberjs/comments/qa2ij/video_tom_dale_talks_about_debugging_emberjs_apps/",
611 | "name": "t3_qa2ij",
612 | "created": 1330457435.0,
613 | "url": "http://vimeo.com/37539737",
614 | "author_flair_text": null,
615 | "author": "CritterM72800",
616 | "created_utc": 1330457435.0,
617 | "media": {
618 | "oembed": {
619 | "provider_url": "http://vimeo.com/",
620 | "description": "Tom Dale, co-founder of Tilde Inc, talks about debugging Ember.js apps.",
621 | "title": "Ember.js Meetup (21/2/12): Tom Dale",
622 | "type": "video",
623 | "author_name": "Ember.js",
624 | "height": 338,
625 | "width": 600,
626 | "html": "<iframe src=\"http://player.vimeo.com/video/37539737\" width=\"600\" height=\"338\" frameborder=\"0\" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>",
627 | "thumbnail_width": 960,
628 | "version": "1.0",
629 | "provider_name": "Vimeo",
630 | "thumbnail_url": "http://b.vimeocdn.com/ts/257/997/257997150_960.jpg",
631 | "thumbnail_height": 540,
632 | "author_url": "http://vimeo.com/user10613810"
633 | },
634 | "type": "vimeo.com"
635 | },
636 | "num_reports": null,
637 | "ups": 5
638 | }
639 | },
640 | {
641 | "kind": "t3",
642 | "data": {
643 | "domain": "vimeo.com",
644 | "banned_by": null,
645 | "media_embed": {
646 | "content": "<iframe src=\"http://player.vimeo.com/video/36992934\" width=\"600\" height=\"338\" frameborder=\"0\" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>",
647 | "width": 600,
648 | "scrolling": false,
649 | "height": 338
650 | },
651 | "subreddit": "emberjs",
652 | "selftext_html": null,
653 | "selftext": "",
654 | "likes": true,
655 | "saved": false,
656 | "id": "q8ou5",
657 | "clicked": false,
658 | "title": "Ember.js Lunch Talk at Carbon Five",
659 | "num_comments": 2,
660 | "score": 3,
661 | "approved_by": null,
662 | "over_18": false,
663 | "hidden": false,
664 | "thumbnail": "http://d.thumbs.redditmedia.com/x3B60ddGw7DEjugQ.jpg",
665 | "subreddit_id": "t5_2tjb2",
666 | "author_flair_css_class": null,
667 | "downs": 1,
668 | "is_self": false,
669 | "permalink": "/r/emberjs/comments/q8ou5/emberjs_lunch_talk_at_carbon_five/",
670 | "name": "t3_q8ou5",
671 | "created": 1330379970.0,
672 | "url": "http://vimeo.com/36992934",
673 | "author_flair_text": null,
674 | "author": "billiamthesecond",
675 | "created_utc": 1330379970.0,
676 | "media": {
677 | "oembed": {
678 | "provider_url": "http://vimeo.com/",
679 | "description": "Tom Dale and Yehuda Katz discuss their new JavaScript MVC ember.js over lunch at Carbon Five. There's about 45 minutes of presentation plus 20 minutes of Q & A. Check out more tech goodness at http://blog.carbonfive.com.",
680 | "title": "Ember.js Lunch Talk at Carbon Five",
681 | "type": "video",
682 | "author_name": "Christian Nelson",
683 | "height": 338,
684 | "width": 600,
685 | "html": "<iframe src=\"http://player.vimeo.com/video/36992934\" width=\"600\" height=\"338\" frameborder=\"0\" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>",
686 | "thumbnail_width": 640,
687 | "version": "1.0",
688 | "provider_name": "Vimeo",
689 | "thumbnail_url": "http://b.vimeocdn.com/ts/253/953/253953484_640.jpg",
690 | "thumbnail_height": 357,
691 | "author_url": "http://vimeo.com/christiannelson"
692 | },
693 | "type": "vimeo.com"
694 | },
695 | "num_reports": null,
696 | "ups": 4
697 | }
698 | },
699 | {
700 | "kind": "t3",
701 | "data": {
702 | "domain": "codebrief.com",
703 | "banned_by": null,
704 | "media_embed": {},
705 | "subreddit": "emberjs",
706 | "selftext_html": null,
707 | "selftext": "",
708 | "likes": true,
709 | "saved": false,
710 | "id": "q8ndc",
711 | "clicked": false,
712 | "title": "Anatomy of a Complex Ember.js App Part I: States and Routes",
713 | "num_comments": 0,
714 | "score": 3,
715 | "approved_by": null,
716 | "over_18": false,
717 | "hidden": false,
718 | "thumbnail": "default",
719 | "subreddit_id": "t5_2tjb2",
720 | "author_flair_css_class": null,
721 | "downs": 0,
722 | "is_self": false,
723 | "permalink": "/r/emberjs/comments/q8ndc/anatomy_of_a_complex_emberjs_app_part_i_states/",
724 | "name": "t3_q8ndc",
725 | "created": 1330378227.0,
726 | "url": "http://codebrief.com/2012/02/anatomy-of-a-complex-ember-js-app-part-i-states-and-routes/",
727 | "author_flair_text": null,
728 | "author": "CritterM72800",
729 | "created_utc": 1330378227.0,
730 | "media": null,
731 | "num_reports": null,
732 | "ups": 3
733 | }
734 | },
735 | {
736 | "kind": "t3",
737 | "data": {
738 | "domain": "ngauthier.com",
739 | "banned_by": null,
740 | "media_embed": {},
741 | "subreddit": "emberjs",
742 | "selftext_html": null,
743 | "selftext": "",
744 | "likes": true,
745 | "saved": false,
746 | "id": "q03d1",
747 | "clicked": false,
748 | "title": "Playing with Ember.js",
749 | "num_comments": 0,
750 | "score": 2,
751 | "approved_by": null,
752 | "over_18": false,
753 | "hidden": false,
754 | "thumbnail": "default",
755 | "subreddit_id": "t5_2tjb2",
756 | "author_flair_css_class": null,
757 | "downs": 0,
758 | "is_self": false,
759 | "permalink": "/r/emberjs/comments/q03d1/playing_with_emberjs/",
760 | "name": "t3_q03d1",
761 | "created": 1329869120.0,
762 | "url": "http://ngauthier.com/2012/02/playing-with-ember.html",
763 | "author_flair_text": null,
764 | "author": "CritterM72800",
765 | "created_utc": 1329869120.0,
766 | "media": null,
767 | "num_reports": null,
768 | "ups": 2
769 | }
770 | }],
771 | "after": null,
772 | "before": null
773 | }
774 | }
--------------------------------------------------------------------------------
/app/vendor/twitter-text.js:
--------------------------------------------------------------------------------
1 | if (typeof window === "undefined" || window === null) {
2 | window = { twttr: {} };
3 | }
4 | if (window.twttr == null) {
5 | window.twttr = {};
6 | }
7 | if (typeof twttr === "undefined" || twttr === null) {
8 | twttr = {};
9 | }
10 |
11 | (function() {
12 | twttr.txt = {};
13 | twttr.txt.regexen = {};
14 |
15 | var HTML_ENTITIES = {
16 | '&': '&',
17 | '>': '>',
18 | '<': '<',
19 | '"': '"',
20 | "'": '''
21 | };
22 |
23 | // HTML escaping
24 | twttr.txt.htmlEscape = function(text) {
25 | return text && text.replace(/[&"'><]/g, function(character) {
26 | return HTML_ENTITIES[character];
27 | });
28 | };
29 |
30 | // Builds a RegExp
31 | function regexSupplant(regex, flags) {
32 | flags = flags || "";
33 | if (typeof regex !== "string") {
34 | if (regex.global && flags.indexOf("g") < 0) {
35 | flags += "g";
36 | }
37 | if (regex.ignoreCase && flags.indexOf("i") < 0) {
38 | flags += "i";
39 | }
40 | if (regex.multiline && flags.indexOf("m") < 0) {
41 | flags += "m";
42 | }
43 |
44 | regex = regex.source;
45 | }
46 |
47 | return new RegExp(regex.replace(/#\{(\w+)\}/g, function(match, name) {
48 | var newRegex = twttr.txt.regexen[name] || "";
49 | if (typeof newRegex !== "string") {
50 | newRegex = newRegex.source;
51 | }
52 | return newRegex;
53 | }), flags);
54 | }
55 |
56 | // simple string interpolation
57 | function stringSupplant(str, values) {
58 | return str.replace(/#\{(\w+)\}/g, function(match, name) {
59 | return values[name] || "";
60 | });
61 | }
62 |
63 | function addCharsToCharClass(charClass, start, end) {
64 | var s = String.fromCharCode(start);
65 | if (end !== start) {
66 | s += "-" + String.fromCharCode(end);
67 | }
68 | charClass.push(s);
69 | return charClass;
70 | }
71 |
72 | // Space is more than %20, U+3000 for example is the full-width space used with Kanji. Provide a short-hand
73 | // to access both the list of characters and a pattern suitible for use with String#split
74 | // Taken from: ActiveSupport::Multibyte::Handlers::UTF8Handler::UNICODE_WHITESPACE
75 | var fromCode = String.fromCharCode;
76 | var UNICODE_SPACES = [
77 | fromCode(0x0020), // White_Space # Zs SPACE
78 | fromCode(0x0085), // White_Space # Cc
79 | fromCode(0x00A0), // White_Space # Zs NO-BREAK SPACE
80 | fromCode(0x1680), // White_Space # Zs OGHAM SPACE MARK
81 | fromCode(0x180E), // White_Space # Zs MONGOLIAN VOWEL SEPARATOR
82 | fromCode(0x2028), // White_Space # Zl LINE SEPARATOR
83 | fromCode(0x2029), // White_Space # Zp PARAGRAPH SEPARATOR
84 | fromCode(0x202F), // White_Space # Zs NARROW NO-BREAK SPACE
85 | fromCode(0x205F), // White_Space # Zs MEDIUM MATHEMATICAL SPACE
86 | fromCode(0x3000) // White_Space # Zs IDEOGRAPHIC SPACE
87 | ];
88 | addCharsToCharClass(UNICODE_SPACES, 0x009, 0x00D); // White_Space # Cc [5] ..
89 | addCharsToCharClass(UNICODE_SPACES, 0x2000, 0x200A); // White_Space # Zs [11] EN QUAD..HAIR SPACE
90 |
91 | var INVALID_CHARS = [
92 | fromCode(0xFFFE),
93 | fromCode(0xFEFF), // BOM
94 | fromCode(0xFFFF) // Special
95 | ];
96 | addCharsToCharClass(INVALID_CHARS, 0x202A, 0x202E); // Directional change
97 |
98 | twttr.txt.regexen.spaces_group = regexSupplant(UNICODE_SPACES.join(""));
99 | twttr.txt.regexen.spaces = regexSupplant("[" + UNICODE_SPACES.join("") + "]");
100 | twttr.txt.regexen.invalid_chars_group = regexSupplant(INVALID_CHARS.join(""));
101 | twttr.txt.regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$/;
102 |
103 | var nonLatinHashtagChars = [];
104 | // Cyrillic
105 | addCharsToCharClass(nonLatinHashtagChars, 0x0400, 0x04ff); // Cyrillic
106 | addCharsToCharClass(nonLatinHashtagChars, 0x0500, 0x0527); // Cyrillic Supplement
107 | addCharsToCharClass(nonLatinHashtagChars, 0x2de0, 0x2dff); // Cyrillic Extended A
108 | addCharsToCharClass(nonLatinHashtagChars, 0xa640, 0xa69f); // Cyrillic Extended B
109 | // Hebrew
110 | addCharsToCharClass(nonLatinHashtagChars, 0x0591, 0x05bf); // Hebrew
111 | addCharsToCharClass(nonLatinHashtagChars, 0x05c1, 0x05c2);
112 | addCharsToCharClass(nonLatinHashtagChars, 0x05c4, 0x05c5);
113 | addCharsToCharClass(nonLatinHashtagChars, 0x05c7, 0x05c7);
114 | addCharsToCharClass(nonLatinHashtagChars, 0x05d0, 0x05ea);
115 | addCharsToCharClass(nonLatinHashtagChars, 0x05f0, 0x05f4);
116 | addCharsToCharClass(nonLatinHashtagChars, 0xfb12, 0xfb28); // Hebrew Presentation Forms
117 | addCharsToCharClass(nonLatinHashtagChars, 0xfb2a, 0xfb36);
118 | addCharsToCharClass(nonLatinHashtagChars, 0xfb38, 0xfb3c);
119 | addCharsToCharClass(nonLatinHashtagChars, 0xfb3e, 0xfb3e);
120 | addCharsToCharClass(nonLatinHashtagChars, 0xfb40, 0xfb41);
121 | addCharsToCharClass(nonLatinHashtagChars, 0xfb43, 0xfb44);
122 | addCharsToCharClass(nonLatinHashtagChars, 0xfb46, 0xfb4f);
123 | // Arabic
124 | addCharsToCharClass(nonLatinHashtagChars, 0x0610, 0x061a); // Arabic
125 | addCharsToCharClass(nonLatinHashtagChars, 0x0620, 0x065f);
126 | addCharsToCharClass(nonLatinHashtagChars, 0x066e, 0x06d3);
127 | addCharsToCharClass(nonLatinHashtagChars, 0x06d5, 0x06dc);
128 | addCharsToCharClass(nonLatinHashtagChars, 0x06de, 0x06e8);
129 | addCharsToCharClass(nonLatinHashtagChars, 0x06ea, 0x06ef);
130 | addCharsToCharClass(nonLatinHashtagChars, 0x06fa, 0x06fc);
131 | addCharsToCharClass(nonLatinHashtagChars, 0x06ff, 0x06ff);
132 | addCharsToCharClass(nonLatinHashtagChars, 0x0750, 0x077f); // Arabic Supplement
133 | addCharsToCharClass(nonLatinHashtagChars, 0x08a0, 0x08a0); // Arabic Extended A
134 | addCharsToCharClass(nonLatinHashtagChars, 0x08a2, 0x08ac);
135 | addCharsToCharClass(nonLatinHashtagChars, 0x08e4, 0x08fe);
136 | addCharsToCharClass(nonLatinHashtagChars, 0xfb50, 0xfbb1); // Arabic Pres. Forms A
137 | addCharsToCharClass(nonLatinHashtagChars, 0xfbd3, 0xfd3d);
138 | addCharsToCharClass(nonLatinHashtagChars, 0xfd50, 0xfd8f);
139 | addCharsToCharClass(nonLatinHashtagChars, 0xfd92, 0xfdc7);
140 | addCharsToCharClass(nonLatinHashtagChars, 0xfdf0, 0xfdfb);
141 | addCharsToCharClass(nonLatinHashtagChars, 0xfe70, 0xfe74); // Arabic Pres. Forms B
142 | addCharsToCharClass(nonLatinHashtagChars, 0xfe76, 0xfefc);
143 | addCharsToCharClass(nonLatinHashtagChars, 0x200c, 0x200c); // Zero-Width Non-Joiner
144 | // Thai
145 | addCharsToCharClass(nonLatinHashtagChars, 0x0e01, 0x0e3a);
146 | addCharsToCharClass(nonLatinHashtagChars, 0x0e40, 0x0e4e);
147 | // Hangul (Korean)
148 | addCharsToCharClass(nonLatinHashtagChars, 0x1100, 0x11ff); // Hangul Jamo
149 | addCharsToCharClass(nonLatinHashtagChars, 0x3130, 0x3185); // Hangul Compatibility Jamo
150 | addCharsToCharClass(nonLatinHashtagChars, 0xA960, 0xA97F); // Hangul Jamo Extended-A
151 | addCharsToCharClass(nonLatinHashtagChars, 0xAC00, 0xD7AF); // Hangul Syllables
152 | addCharsToCharClass(nonLatinHashtagChars, 0xD7B0, 0xD7FF); // Hangul Jamo Extended-B
153 | addCharsToCharClass(nonLatinHashtagChars, 0xFFA1, 0xFFDC); // half-width Hangul
154 | // Japanese and Chinese
155 | addCharsToCharClass(nonLatinHashtagChars, 0x30A1, 0x30FA); // Katakana (full-width)
156 | addCharsToCharClass(nonLatinHashtagChars, 0x30FC, 0x30FE); // Katakana Chouon and iteration marks (full-width)
157 | addCharsToCharClass(nonLatinHashtagChars, 0xFF66, 0xFF9F); // Katakana (half-width)
158 | addCharsToCharClass(nonLatinHashtagChars, 0xFF70, 0xFF70); // Katakana Chouon (half-width)
159 | addCharsToCharClass(nonLatinHashtagChars, 0xFF10, 0xFF19); // \
160 | addCharsToCharClass(nonLatinHashtagChars, 0xFF21, 0xFF3A); // - Latin (full-width)
161 | addCharsToCharClass(nonLatinHashtagChars, 0xFF41, 0xFF5A); // /
162 | addCharsToCharClass(nonLatinHashtagChars, 0x3041, 0x3096); // Hiragana
163 | addCharsToCharClass(nonLatinHashtagChars, 0x3099, 0x309E); // Hiragana voicing and iteration mark
164 | addCharsToCharClass(nonLatinHashtagChars, 0x3400, 0x4DBF); // Kanji (CJK Extension A)
165 | addCharsToCharClass(nonLatinHashtagChars, 0x4E00, 0x9FFF); // Kanji (Unified)
166 | // -- Disabled as it breaks the Regex.
167 | //addCharsToCharClass(nonLatinHashtagChars, 0x20000, 0x2A6DF); // Kanji (CJK Extension B)
168 | addCharsToCharClass(nonLatinHashtagChars, 0x2A700, 0x2B73F); // Kanji (CJK Extension C)
169 | addCharsToCharClass(nonLatinHashtagChars, 0x2B740, 0x2B81F); // Kanji (CJK Extension D)
170 | addCharsToCharClass(nonLatinHashtagChars, 0x2F800, 0x2FA1F); // Kanji (CJK supplement)
171 | addCharsToCharClass(nonLatinHashtagChars, 0x3003, 0x3003); // Kanji iteration mark
172 | addCharsToCharClass(nonLatinHashtagChars, 0x3005, 0x3005); // Kanji iteration mark
173 | addCharsToCharClass(nonLatinHashtagChars, 0x303B, 0x303B); // Han iteration mark
174 |
175 | twttr.txt.regexen.nonLatinHashtagChars = regexSupplant(nonLatinHashtagChars.join(""));
176 |
177 | var latinAccentChars = [];
178 | // Latin accented characters (subtracted 0xD7 from the range, it's a confusable multiplication sign. Looks like "x")
179 | addCharsToCharClass(latinAccentChars, 0x00c0, 0x00d6);
180 | addCharsToCharClass(latinAccentChars, 0x00d8, 0x00f6);
181 | addCharsToCharClass(latinAccentChars, 0x00f8, 0x00ff);
182 | // Latin Extended A and B
183 | addCharsToCharClass(latinAccentChars, 0x0100, 0x024f);
184 | // assorted IPA Extensions
185 | addCharsToCharClass(latinAccentChars, 0x0253, 0x0254);
186 | addCharsToCharClass(latinAccentChars, 0x0256, 0x0257);
187 | addCharsToCharClass(latinAccentChars, 0x0259, 0x0259);
188 | addCharsToCharClass(latinAccentChars, 0x025b, 0x025b);
189 | addCharsToCharClass(latinAccentChars, 0x0263, 0x0263);
190 | addCharsToCharClass(latinAccentChars, 0x0268, 0x0268);
191 | addCharsToCharClass(latinAccentChars, 0x026f, 0x026f);
192 | addCharsToCharClass(latinAccentChars, 0x0272, 0x0272);
193 | addCharsToCharClass(latinAccentChars, 0x0289, 0x0289);
194 | addCharsToCharClass(latinAccentChars, 0x028b, 0x028b);
195 | // Okina for Hawaiian (it *is* a letter character)
196 | addCharsToCharClass(latinAccentChars, 0x02bb, 0x02bb);
197 | // Latin Extended Additional
198 | addCharsToCharClass(latinAccentChars, 0x1e00, 0x1eff);
199 | twttr.txt.regexen.latinAccentChars = regexSupplant(latinAccentChars.join(""));
200 |
201 | // A hashtag must contain characters, numbers and underscores, but not all numbers.
202 | twttr.txt.regexen.hashSigns = /[##]/;
203 | twttr.txt.regexen.hashtagAlpha = regexSupplant(/[a-z_#{latinAccentChars}#{nonLatinHashtagChars}]/i);
204 | twttr.txt.regexen.hashtagAlphaNumeric = regexSupplant(/[a-z0-9_#{latinAccentChars}#{nonLatinHashtagChars}]/i);
205 | twttr.txt.regexen.endHashtagMatch = regexSupplant(/^(?:#{hashSigns}|:\/\/)/);
206 | twttr.txt.regexen.hashtagBoundary = regexSupplant(/(?:^|$|[^&a-z0-9_#{latinAccentChars}#{nonLatinHashtagChars}])/);
207 | twttr.txt.regexen.validHashtag = regexSupplant(/(#{hashtagBoundary})(#{hashSigns})(#{hashtagAlphaNumeric}*#{hashtagAlpha}#{hashtagAlphaNumeric}*)/gi);
208 |
209 | // Mention related regex collection
210 | twttr.txt.regexen.validMentionPrecedingChars = /(?:^|[^a-zA-Z0-9_!#$%&*@@]|RT:?)/;
211 | twttr.txt.regexen.atSigns = /[@@]/;
212 | twttr.txt.regexen.validMentionOrList = regexSupplant(
213 | '(#{validMentionPrecedingChars})' + // $1: Preceding character
214 | '(#{atSigns})' + // $2: At mark
215 | '([a-zA-Z0-9_]{1,20})' + // $3: Screen name
216 | '(\/[a-zA-Z][a-zA-Z0-9_\-]{0,24})?' // $4: List (optional)
217 | , 'g');
218 | twttr.txt.regexen.validReply = regexSupplant(/^(?:#{spaces})*#{atSigns}([a-zA-Z0-9_]{1,20})/);
219 | twttr.txt.regexen.endMentionMatch = regexSupplant(/^(?:#{atSigns}|[#{latinAccentChars}]|:\/\/)/);
220 |
221 | // URL related regex collection
222 | twttr.txt.regexen.validUrlPrecedingChars = regexSupplant(/(?:[^A-Za-z0-9@@$###{invalid_chars_group}]|^)/);
223 | twttr.txt.regexen.invalidUrlWithoutProtocolPrecedingChars = /[-_.\/]$/;
224 | twttr.txt.regexen.invalidDomainChars = stringSupplant("#{punct}#{spaces_group}#{invalid_chars_group}", twttr.txt.regexen);
225 | twttr.txt.regexen.validDomainChars = regexSupplant(/[^#{invalidDomainChars}]/);
226 | twttr.txt.regexen.validSubdomain = regexSupplant(/(?:(?:#{validDomainChars}(?:[_-]|#{validDomainChars})*)?#{validDomainChars}\.)/);
227 | twttr.txt.regexen.validDomainName = regexSupplant(/(?:(?:#{validDomainChars}(?:-|#{validDomainChars})*)?#{validDomainChars}\.)/);
228 | twttr.txt.regexen.validGTLD = regexSupplant(/(?:(?:aero|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|xxx)(?=[^0-9a-zA-Z]|$))/);
229 | twttr.txt.regexen.validCCTLD = regexSupplant(/(?:(?:ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|ss|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw)(?=[^0-9a-zA-Z]|$))/);
230 | twttr.txt.regexen.validPunycode = regexSupplant(/(?:xn--[0-9a-z]+)/);
231 | twttr.txt.regexen.validDomain = regexSupplant(/(?:#{validSubdomain}*#{validDomainName}(?:#{validGTLD}|#{validCCTLD}|#{validPunycode}))/);
232 | twttr.txt.regexen.validAsciiDomain = regexSupplant(/(?:(?:[a-z0-9#{latinAccentChars}]+)\.)+(?:#{validGTLD}|#{validCCTLD}|#{validPunycode})/gi);
233 | twttr.txt.regexen.invalidShortDomain = regexSupplant(/^#{validDomainName}#{validCCTLD}$/);
234 |
235 | twttr.txt.regexen.validPortNumber = regexSupplant(/[0-9]+/);
236 |
237 | twttr.txt.regexen.validGeneralUrlPathChars = regexSupplant(/[a-z0-9!\*';:=\+,\.\$\/%#\[\]\-_~|{latinAccentChars}]/i);
238 | // Allow URL paths to contain balanced parens
239 | // 1. Used in Wikipedia URLs like /Primer_(film)
240 | // 2. Used in IIS sessions like /S(dfd346)/
241 | twttr.txt.regexen.validUrlBalancedParens = regexSupplant(/\(#{validGeneralUrlPathChars}+\)/i);
242 | // Valid end-of-path chracters (so /foo. does not gobble the period).
243 | // 1. Allow = for empty URL parameters and other URL-join artifacts
244 | twttr.txt.regexen.validUrlPathEndingChars = regexSupplant(/[\+\-a-z0-9=_#\/#{latinAccentChars}]|(?:#{validUrlBalancedParens})/i);
245 | // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user/
246 | twttr.txt.regexen.validUrlPath = regexSupplant('(?:' +
247 | '(?:' +
248 | '#{validGeneralUrlPathChars}*' +
249 | '(?:#{validUrlBalancedParens}#{validGeneralUrlPathChars}*)*' +
250 | '#{validUrlPathEndingChars}'+
251 | ')|(?:@#{validGeneralUrlPathChars}+\/)'+
252 | ')', 'i');
253 |
254 | twttr.txt.regexen.validUrlQueryChars = /[a-z0-9!?\*'\(\);:&=\+\$\/%#\[\]\-_\.,~|]/i;
255 | twttr.txt.regexen.validUrlQueryEndingChars = /[a-z0-9_&=#\/]/i;
256 | twttr.txt.regexen.extractUrl = regexSupplant(
257 | '(' + // $1 total match
258 | '(#{validUrlPrecedingChars})' + // $2 Preceeding chracter
259 | '(' + // $3 URL
260 | '(https?:\\/\\/)?' + // $4 Protocol (optional)
261 | '(#{validDomain})' + // $5 Domain(s)
262 | '(?::(#{validPortNumber}))?' + // $6 Port number (optional)
263 | '(\\/#{validUrlPath}*)?' + // $7 URL Path
264 | '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $8 Query String
265 | ')' +
266 | ')'
267 | , 'gi');
268 |
269 | twttr.txt.regexen.validTcoUrl = /^https?:\/\/t\.co\/[a-z0-9]+/i;
270 |
271 | // These URL validation pattern strings are based on the ABNF from RFC 3986
272 | twttr.txt.regexen.validateUrlUnreserved = /[a-z0-9\-._~]/i;
273 | twttr.txt.regexen.validateUrlPctEncoded = /(?:%[0-9a-f]{2})/i;
274 | twttr.txt.regexen.validateUrlSubDelims = /[!$&'()*+,;=]/i;
275 | twttr.txt.regexen.validateUrlPchar = regexSupplant('(?:' +
276 | '#{validateUrlUnreserved}|' +
277 | '#{validateUrlPctEncoded}|' +
278 | '#{validateUrlSubDelims}|' +
279 | '[:|@]' +
280 | ')', 'i');
281 |
282 | twttr.txt.regexen.validateUrlScheme = /(?:[a-z][a-z0-9+\-.]*)/i;
283 | twttr.txt.regexen.validateUrlUserinfo = regexSupplant('(?:' +
284 | '#{validateUrlUnreserved}|' +
285 | '#{validateUrlPctEncoded}|' +
286 | '#{validateUrlSubDelims}|' +
287 | ':' +
288 | ')*', 'i');
289 |
290 | twttr.txt.regexen.validateUrlDecOctet = /(?:[0-9]|(?:[1-9][0-9])|(?:1[0-9]{2})|(?:2[0-4][0-9])|(?:25[0-5]))/i;
291 | twttr.txt.regexen.validateUrlIpv4 = regexSupplant(/(?:#{validateUrlDecOctet}(?:\.#{validateUrlDecOctet}){3})/i);
292 |
293 | // Punting on real IPv6 validation for now
294 | twttr.txt.regexen.validateUrlIpv6 = /(?:\[[a-f0-9:\.]+\])/i;
295 |
296 | // Also punting on IPvFuture for now
297 | twttr.txt.regexen.validateUrlIp = regexSupplant('(?:' +
298 | '#{validateUrlIpv4}|' +
299 | '#{validateUrlIpv6}' +
300 | ')', 'i');
301 |
302 | // This is more strict than the rfc specifies
303 | twttr.txt.regexen.validateUrlSubDomainSegment = /(?:[a-z0-9](?:[a-z0-9_\-]*[a-z0-9])?)/i;
304 | twttr.txt.regexen.validateUrlDomainSegment = /(?:[a-z0-9](?:[a-z0-9\-]*[a-z0-9])?)/i;
305 | twttr.txt.regexen.validateUrlDomainTld = /(?:[a-z](?:[a-z0-9\-]*[a-z0-9])?)/i;
306 | twttr.txt.regexen.validateUrlDomain = regexSupplant(/(?:(?:#{validateUrlSubDomainSegment]}\.)*(?:#{validateUrlDomainSegment]}\.)#{validateUrlDomainTld})/i);
307 |
308 | twttr.txt.regexen.validateUrlHost = regexSupplant('(?:' +
309 | '#{validateUrlIp}|' +
310 | '#{validateUrlDomain}' +
311 | ')', 'i');
312 |
313 | // Unencoded internationalized domains - this doesn't check for invalid UTF-8 sequences
314 | twttr.txt.regexen.validateUrlUnicodeSubDomainSegment = /(?:(?:[a-z0-9]|[^\u0000-\u007f])(?:(?:[a-z0-9_\-]|[^\u0000-\u007f])*(?:[a-z0-9]|[^\u0000-\u007f]))?)/i;
315 | twttr.txt.regexen.validateUrlUnicodeDomainSegment = /(?:(?:[a-z0-9]|[^\u0000-\u007f])(?:(?:[a-z0-9\-]|[^\u0000-\u007f])*(?:[a-z0-9]|[^\u0000-\u007f]))?)/i;
316 | twttr.txt.regexen.validateUrlUnicodeDomainTld = /(?:(?:[a-z]|[^\u0000-\u007f])(?:(?:[a-z0-9\-]|[^\u0000-\u007f])*(?:[a-z0-9]|[^\u0000-\u007f]))?)/i;
317 | twttr.txt.regexen.validateUrlUnicodeDomain = regexSupplant(/(?:(?:#{validateUrlUnicodeSubDomainSegment}\.)*(?:#{validateUrlUnicodeDomainSegment}\.)#{validateUrlUnicodeDomainTld})/i);
318 |
319 | twttr.txt.regexen.validateUrlUnicodeHost = regexSupplant('(?:' +
320 | '#{validateUrlIp}|' +
321 | '#{validateUrlUnicodeDomain}' +
322 | ')', 'i');
323 |
324 | twttr.txt.regexen.validateUrlPort = /[0-9]{1,5}/;
325 |
326 | twttr.txt.regexen.validateUrlUnicodeAuthority = regexSupplant(
327 | '(?:(#{validateUrlUserinfo})@)?' + // $1 userinfo
328 | '(#{validateUrlUnicodeHost})' + // $2 host
329 | '(?::(#{validateUrlPort}))?' //$3 port
330 | , "i");
331 |
332 | twttr.txt.regexen.validateUrlAuthority = regexSupplant(
333 | '(?:(#{validateUrlUserinfo})@)?' + // $1 userinfo
334 | '(#{validateUrlHost})' + // $2 host
335 | '(?::(#{validateUrlPort}))?' // $3 port
336 | , "i");
337 |
338 | twttr.txt.regexen.validateUrlPath = regexSupplant(/(\/#{validateUrlPchar}*)*/i);
339 | twttr.txt.regexen.validateUrlQuery = regexSupplant(/(#{validateUrlPchar}|\/|\?)*/i);
340 | twttr.txt.regexen.validateUrlFragment = regexSupplant(/(#{validateUrlPchar}|\/|\?)*/i);
341 |
342 | // Modified version of RFC 3986 Appendix B
343 | twttr.txt.regexen.validateUrlUnencoded = regexSupplant(
344 | '^' + // Full URL
345 | '(?:' +
346 | '([^:/?#]+):\\/\\/' + // $1 Scheme
347 | ')?' +
348 | '([^/?#]*)' + // $2 Authority
349 | '([^?#]*)' + // $3 Path
350 | '(?:' +
351 | '\\?([^#]*)' + // $4 Query
352 | ')?' +
353 | '(?:' +
354 | '#(.*)' + // $5 Fragment
355 | ')?$'
356 | , "i");
357 |
358 |
359 | // Default CSS class for auto-linked URLs
360 | var DEFAULT_URL_CLASS = "tweet-url";
361 | // Default CSS class for auto-linked lists (along with the url class)
362 | var DEFAULT_LIST_CLASS = "list-slug";
363 | // Default CSS class for auto-linked usernames (along with the url class)
364 | var DEFAULT_USERNAME_CLASS = "username";
365 | // Default CSS class for auto-linked hashtags (along with the url class)
366 | var DEFAULT_HASHTAG_CLASS = "hashtag";
367 | // HTML attribute for robot nofollow behavior (default)
368 | var HTML_ATTR_NO_FOLLOW = " rel=\"nofollow\"";
369 | // Options which should not be passed as HTML attributes
370 | var OPTIONS_NOT_ATTRIBUTES = {'urlClass':true, 'listClass':true, 'usernameClass':true, 'hashtagClass':true,
371 | 'usernameUrlBase':true, 'listUrlBase':true, 'hashtagUrlBase':true,
372 | 'usernameUrlBlock':true, 'listUrlBlock':true, 'hashtagUrlBlock':true, 'linkUrlBlock':true,
373 | 'usernameIncludeSymbol':true, 'suppressLists':true, 'suppressNoFollow':true,
374 | 'suppressDataScreenName':true, 'urlEntities':true, 'before':true
375 | };
376 | var BOOLEAN_ATTRIBUTES = {'disabled':true, 'readonly':true, 'multiple':true, 'checked':true};
377 |
378 | // Simple object cloning function for simple objects
379 | function clone(o) {
380 | var r = {};
381 | for (var k in o) {
382 | if (o.hasOwnProperty(k)) {
383 | r[k] = o[k];
384 | }
385 | }
386 |
387 | return r;
388 | }
389 |
390 | twttr.txt.linkToHashtag = function(entity, text, options) {
391 | var d = {
392 | hash: text.substring(entity.indices[0], entity.indices[0] + 1),
393 | preText: "",
394 | text: twttr.txt.htmlEscape(entity.hashtag),
395 | postText: "",
396 | extraHtml: options.suppressNoFollow ? "" : HTML_ATTR_NO_FOLLOW
397 | };
398 | for (var k in options) {
399 | if (options.hasOwnProperty(k)) {
400 | d[k] = options[k];
401 | }
402 | }
403 |
404 | return stringSupplant("#{before}#{hash}#{preText}#{text}#{postText}", d);
405 | };
406 |
407 | twttr.txt.linkToMentionAndList = function(entity, text, options) {
408 | var at = text.substring(entity.indices[0], entity.indices[0] + 1);
409 | var d = {
410 | at: options.usernameIncludeSymbol ? "" : at,
411 | at_before_user: options.usernameIncludeSymbol ? at : "",
412 | user: twttr.txt.htmlEscape(entity.screenName),
413 | slashListname: twttr.txt.htmlEscape(entity.listSlug),
414 | extraHtml: options.suppressNoFollow ? "" : HTML_ATTR_NO_FOLLOW,
415 | preChunk: "",
416 | postChunk: ""
417 | };
418 | for (var k in options) {
419 | if (options.hasOwnProperty(k)) {
420 | d[k] = options[k];
421 | }
422 | }
423 |
424 | if (entity.listSlug && !options.suppressLists) {
425 | // the link is a list
426 | var list = d.chunk = stringSupplant("#{user}#{slashListname}", d);
427 | d.list = twttr.txt.htmlEscape(list.toLowerCase());
428 | return stringSupplant("#{before}#{at}#{preChunk}#{at_before_user}#{chunk}#{postChunk}", d);
429 | } else {
430 | // this is a screen name
431 | d.chunk = d.user;
432 | d.dataScreenName = !options.suppressDataScreenName ? stringSupplant("data-screen-name=\"#{chunk}\" ", d) : "";
433 | return stringSupplant("#{before}#{at}#{preChunk}#{at_before_user}#{chunk}#{postChunk}", d);
434 | }
435 | };
436 |
437 | twttr.txt.linkToUrl = function(entity, text, options) {
438 | var url = entity.url;
439 | var displayUrl = url;
440 | var linkText = twttr.txt.htmlEscape(displayUrl);
441 | // If the caller passed a urlEntities object (provided by a Twitter API
442 | // response with include_entities=true), we use that to render the display_url
443 | // for each URL instead of it's underlying t.co URL.
444 | if (options.urlEntities && options.urlEntities[url] && options.urlEntities[url].display_url) {
445 | var displayUrl = options.urlEntities[url].display_url;
446 | var expandedUrl = options.urlEntities[url].expanded_url;
447 | if (!options.title) {
448 | options.title = expandedUrl;
449 | }
450 |
451 | // Goal: If a user copies and pastes a tweet containing t.co'ed link, the resulting paste
452 | // should contain the full original URL (expanded_url), not the display URL.
453 | //
454 | // Method: Whenever possible, we actually emit HTML that contains expanded_url, and use
455 | // font-size:0 to hide those parts that should not be displayed (because they are not part of display_url).
456 | // Elements with font-size:0 get copied even though they are not visible.
457 | // Note that display:none doesn't work here. Elements with display:none don't get copied.
458 | //
459 | // Additionally, we want to *display* ellipses, but we don't want them copied. To make this happen we
460 | // wrap the ellipses in a tco-ellipsis class and provide an onCopy handler that sets display:none on
461 | // everything with the tco-ellipsis class.
462 | //
463 | // Exception: pic.twitter.com images, for which expandedUrl = "https://twitter.com/#!/username/status/1234/photo/1
464 | // For those URLs, display_url is not a substring of expanded_url, so we don't do anything special to render the elided parts.
465 | // For a pic.twitter.com URL, the only elided part will be the "https://", so this is fine.
466 |
467 | var displayUrlSansEllipses = displayUrl.replace(/…/g, ""); // We have to disregard ellipses for matching
468 | // Note: we currently only support eliding parts of the URL at the beginning or the end.
469 | // Eventually we may want to elide parts of the URL in the *middle*. If so, this code will
470 | // become more complicated. We will probably want to create a regexp out of display URL,
471 | // replacing every ellipsis with a ".*".
472 | if (expandedUrl.indexOf(displayUrlSansEllipses) != -1) {
473 | var displayUrlIndex = expandedUrl.indexOf(displayUrlSansEllipses);
474 | var v = {
475 | displayUrlSansEllipses: displayUrlSansEllipses,
476 | // Portion of expandedUrl that precedes the displayUrl substring
477 | beforeDisplayUrl: expandedUrl.substr(0, displayUrlIndex),
478 | // Portion of expandedUrl that comes after displayUrl
479 | afterDisplayUrl: expandedUrl.substr(displayUrlIndex + displayUrlSansEllipses.length),
480 | precedingEllipsis: displayUrl.match(/^…/) ? "…" : "",
481 | followingEllipsis: displayUrl.match(/…$/) ? "…" : ""
482 | };
483 | $.each(v, function(index, value) {
484 | v[index] = twttr.txt.htmlEscape(value);
485 | });
486 | // As an example: The user tweets "hi http://longdomainname.com/foo"
487 | // This gets shortened to "hi http://t.co/xyzabc", with display_url = "…nname.com/foo"
488 | // This will get rendered as:
489 | //
490 | // …
491 | //
499 | // http://longdomai
500 | //
501 | //
502 | // nname.com/foo
503 | //
504 | //
505 | //
506 | // …
507 | //
508 | v['invisible'] = options.invisibleTagAttrs;
509 | linkText = stringSupplant("#{precedingEllipsis} #{beforeDisplayUrl}#{displayUrlSansEllipses}#{afterDisplayUrl} #{followingEllipsis}", v);
510 | } else {
511 | linkText = displayUrl;
512 | }
513 | }
514 |
515 | var d = {
516 | htmlAttrs: options.htmlAttrs,
517 | url: twttr.txt.htmlEscape(url),
518 | linkText: linkText
519 | };
520 |
521 | return stringSupplant("#{linkText}", d);
522 | };
523 |
524 | twttr.txt.autoLinkEntities = function(text, entities, options) {
525 | options = clone(options || {});
526 |
527 | if (!options.suppressNoFollow) {
528 | options.rel = "nofollow";
529 | }
530 | if (options.urlClass) {
531 | options["class"] = options.urlClass;
532 | }
533 | options.urlClass = options.urlClass || DEFAULT_URL_CLASS;
534 | options.hashtagClass = options.hashtagClass || DEFAULT_HASHTAG_CLASS;
535 | options.hashtagUrlBase = options.hashtagUrlBase || "https://twitter.com/#!/search?q=%23";
536 | options.listClass = options.listClass || DEFAULT_LIST_CLASS;
537 | options.usernameClass = options.usernameClass || DEFAULT_USERNAME_CLASS;
538 | options.usernameUrlBase = options.usernameUrlBase || "https://twitter.com/";
539 | options.listUrlBase = options.listUrlBase || "https://twitter.com/";
540 | options.before = options.before || "";
541 | options.htmlAttrs = twttr.txt.extractHtmlAttrsFromOptions(options);
542 | options.invisibleTagAttrs = options.invisibleTagAttrs || "style='position:absolute;left:-9999px;'";
543 |
544 | // remap url entities to hash
545 | var urlEntities, i, len;
546 | if(options.urlEntities) {
547 | urlEntities = {};
548 | for(i = 0, len = options.urlEntities.length; i < len; i++) {
549 | urlEntities[options.urlEntities[i].url] = options.urlEntities[i];
550 | }
551 | options.urlEntities = urlEntities;
552 | }
553 |
554 | var result = "";
555 | var beginIndex = 0;
556 |
557 | for (var i = 0; i < entities.length; i++) {
558 | var entity = entities[i];
559 | result += text.substring(beginIndex, entity.indices[0]);
560 |
561 | if (entity.url) {
562 | result += twttr.txt.linkToUrl(entity, text, options);
563 | } else if (entity.hashtag) {
564 | result += twttr.txt.linkToHashtag(entity, text, options);
565 | } else if(entity.screenName) {
566 | result += twttr.txt.linkToMentionAndList(entity, text, options);
567 | }
568 | beginIndex = entity.indices[1];
569 | }
570 | result += text.substring(beginIndex, text.length);
571 | return result;
572 | };
573 |
574 | twttr.txt.extractHtmlAttrsFromOptions = function(options) {
575 | var htmlAttrs = "";
576 | for (var k in options) {
577 | var v = options[k];
578 | if (OPTIONS_NOT_ATTRIBUTES[k]) continue;
579 | if (BOOLEAN_ATTRIBUTES[k]) {
580 | v = v ? k : null;
581 | }
582 | if (v == null) continue;
583 | htmlAttrs += stringSupplant(" #{k}=\"#{v}\" ", {k: twttr.txt.htmlEscape(k), v: twttr.txt.htmlEscape(v.toString())});
584 | }
585 | return htmlAttrs;
586 | };
587 |
588 | twttr.txt.autoLink = function(text, options) {
589 | var entities = twttr.txt.extractEntitiesWithIndices(text, {extractUrlWithoutProtocol: false});
590 | return twttr.txt.autoLinkEntities(text, entities, options);
591 | };
592 |
593 | twttr.txt.autoLinkUsernamesOrLists = function(text, options) {
594 | var entities = twttr.txt.extractMentionsOrListsWithIndices(text);
595 | return twttr.txt.autoLinkEntities(text, entities, options);
596 | };
597 |
598 | twttr.txt.autoLinkHashtags = function(text, options) {
599 | var entities = twttr.txt.extractHashtagsWithIndices(text);
600 | return twttr.txt.autoLinkEntities(text, entities, options);
601 | };
602 |
603 | twttr.txt.autoLinkUrlsCustom = function(text, options) {
604 | var entities = twttr.txt.extractUrlsWithIndices(text, {extractUrlWithoutProtocol: false});
605 | return twttr.txt.autoLinkEntities(text, entities, options);
606 | };
607 |
608 | twttr.txt.removeOverlappingEntities = function(entities) {
609 | entities.sort(function(a,b){ return a.indices[0] - b.indices[0]; });
610 |
611 | var prev = entities[0];
612 | for (var i = 1; i < entities.length; i++) {
613 | if (prev.indices[1] > entities[i].indices[0]) {
614 | entities.splice(i, 1);
615 | i--;
616 | } else {
617 | prev = entities[i];
618 | }
619 | }
620 | };
621 |
622 | twttr.txt.extractEntitiesWithIndices = function(text, options) {
623 | var entities = twttr.txt.extractUrlsWithIndices(text, options)
624 | .concat(twttr.txt.extractMentionsOrListsWithIndices(text))
625 | .concat(twttr.txt.extractHashtagsWithIndices(text, {checkUrlOverlap: false}));
626 |
627 | if (entities.length == 0) {
628 | return [];
629 | }
630 |
631 | twttr.txt.removeOverlappingEntities(entities);
632 | return entities;
633 | };
634 |
635 | twttr.txt.extractMentions = function(text) {
636 | var screenNamesOnly = [],
637 | screenNamesWithIndices = twttr.txt.extractMentionsWithIndices(text);
638 |
639 | for (var i = 0; i < screenNamesWithIndices.length; i++) {
640 | var screenName = screenNamesWithIndices[i].screenName;
641 | screenNamesOnly.push(screenName);
642 | }
643 |
644 | return screenNamesOnly;
645 | };
646 |
647 | twttr.txt.extractMentionsWithIndices = function(text) {
648 | var mentions = [];
649 | var mentionsOrLists = twttr.txt.extractMentionsOrListsWithIndices(text);
650 |
651 | for (var i = 0 ; i < mentionsOrLists.length; i++) {
652 | mentionOrList = mentionsOrLists[i];
653 | if (mentionOrList.listSlug == '') {
654 | mentions.push({
655 | screenName: mentionOrList.screenName,
656 | indices: mentionOrList.indices
657 | });
658 | }
659 | }
660 |
661 | return mentions;
662 | };
663 |
664 | /**
665 | * Extract list or user mentions.
666 | * (Presence of listSlug indicates a list)
667 | */
668 | twttr.txt.extractMentionsOrListsWithIndices = function(text) {
669 | if (!text || !text.match(twttr.txt.regexen.atSigns)) {
670 | return [];
671 | }
672 |
673 | var possibleNames = [],
674 | position = 0;
675 |
676 | text.replace(twttr.txt.regexen.validMentionOrList, function(match, before, atSign, screenName, slashListname, offset, chunk) {
677 | var after = chunk.slice(offset + match.length);
678 | if (!after.match(twttr.txt.regexen.endMentionMatch)) {
679 | slashListname = slashListname || '';
680 | var startPosition = text.indexOf(atSign + screenName + slashListname, position);
681 | position = startPosition + screenName.length + slashListname.length + 1;
682 | possibleNames.push({
683 | screenName: screenName,
684 | listSlug: slashListname,
685 | indices: [startPosition, position]
686 | });
687 | }
688 | });
689 |
690 | return possibleNames;
691 | };
692 |
693 |
694 | twttr.txt.extractReplies = function(text) {
695 | if (!text) {
696 | return null;
697 | }
698 |
699 | var possibleScreenName = text.match(twttr.txt.regexen.validReply);
700 | if (!possibleScreenName ||
701 | RegExp.rightContext.match(twttr.txt.regexen.endMentionMatch)) {
702 | return null;
703 | }
704 |
705 | return possibleScreenName[1];
706 | };
707 |
708 | twttr.txt.extractUrls = function(text, options) {
709 | var urlsOnly = [],
710 | urlsWithIndices = twttr.txt.extractUrlsWithIndices(text, options);
711 |
712 | for (var i = 0; i < urlsWithIndices.length; i++) {
713 | urlsOnly.push(urlsWithIndices[i].url);
714 | }
715 |
716 | return urlsOnly;
717 | };
718 |
719 | twttr.txt.extractUrlsWithIndices = function(text, options) {
720 | if (!options) {
721 | options = {extractUrlsWithoutProtocol: true};
722 | }
723 |
724 | if (!text || (options.extractUrlsWithoutProtocol ? !text.match(/\./) : !text.match(/:/))) {
725 | return [];
726 | }
727 |
728 | var urls = [];
729 |
730 | while (twttr.txt.regexen.extractUrl.exec(text)) {
731 | var before = RegExp.$2, url = RegExp.$3, protocol = RegExp.$4, domain = RegExp.$5, path = RegExp.$7;
732 | var endPosition = twttr.txt.regexen.extractUrl.lastIndex,
733 | startPosition = endPosition - url.length;
734 |
735 | // if protocol is missing and domain contains non-ASCII characters,
736 | // extract ASCII-only domains.
737 | if (!protocol) {
738 | if (!options.extractUrlsWithoutProtocol
739 | || before.match(twttr.txt.regexen.invalidUrlWithoutProtocolPrecedingChars)) {
740 | continue;
741 | }
742 | var lastUrl = null,
743 | lastUrlInvalidMatch = false,
744 | asciiEndPosition = 0;
745 | domain.replace(twttr.txt.regexen.validAsciiDomain, function(asciiDomain) {
746 | var asciiStartPosition = domain.indexOf(asciiDomain, asciiEndPosition);
747 | asciiEndPosition = asciiStartPosition + asciiDomain.length;
748 | lastUrl = {
749 | url: asciiDomain,
750 | indices: [startPosition + asciiStartPosition, startPosition + asciiEndPosition]
751 | };
752 | lastUrlInvalidMatch = asciiDomain.match(twttr.txt.regexen.invalidShortDomain);
753 | if (!lastUrlInvalidMatch) {
754 | urls.push(lastUrl);
755 | }
756 | });
757 |
758 | // no ASCII-only domain found. Skip the entire URL.
759 | if (lastUrl == null) {
760 | continue;
761 | }
762 |
763 | // lastUrl only contains domain. Need to add path and query if they exist.
764 | if (path) {
765 | if (lastUrlInvalidMatch) {
766 | urls.push(lastUrl);
767 | }
768 | lastUrl.url = url.replace(domain, lastUrl.url);
769 | lastUrl.indices[1] = endPosition;
770 | }
771 | } else {
772 | // In the case of t.co URLs, don't allow additional path characters.
773 | if (url.match(twttr.txt.regexen.validTcoUrl)) {
774 | url = RegExp.lastMatch;
775 | endPosition = startPosition + url.length;
776 | }
777 | urls.push({
778 | url: url,
779 | indices: [startPosition, endPosition]
780 | });
781 | }
782 | }
783 |
784 | return urls;
785 | };
786 |
787 | twttr.txt.extractHashtags = function(text) {
788 | var hashtagsOnly = [],
789 | hashtagsWithIndices = twttr.txt.extractHashtagsWithIndices(text);
790 |
791 | for (var i = 0; i < hashtagsWithIndices.length; i++) {
792 | hashtagsOnly.push(hashtagsWithIndices[i].hashtag);
793 | }
794 |
795 | return hashtagsOnly;
796 | };
797 |
798 | twttr.txt.extractHashtagsWithIndices = function(text, options) {
799 | if (!options) {
800 | options = {checkUrlOverlap: true};
801 | }
802 |
803 | if (!text || !text.match(twttr.txt.regexen.hashSigns)) {
804 | return [];
805 | }
806 |
807 | var tags = [],
808 | position = 0;
809 |
810 | text.replace(twttr.txt.regexen.validHashtag, function(match, before, hash, hashText, offset, chunk) {
811 | var after = chunk.slice(offset + match.length);
812 | if (after.match(twttr.txt.regexen.endHashtagMatch))
813 | return;
814 | var startPosition = text.indexOf(hash + hashText, position);
815 | position = startPosition + hashText.length + 1;
816 | tags.push({
817 | hashtag: hashText,
818 | indices: [startPosition, position]
819 | });
820 | });
821 |
822 | if (options.checkUrlOverlap) {
823 | // also extract URL entities
824 | var urls = twttr.txt.extractUrlsWithIndices(text);
825 | if (urls.length > 0) {
826 | var entities = tags.concat(urls);
827 | // remove overlap
828 | twttr.txt.removeOverlappingEntities(entities);
829 | // only push back hashtags
830 | tags = [];
831 | for (var i = 0; i < entities.length; i++) {
832 | if (entities[i].hashtag) {
833 | tags.push(entities[i]);
834 | }
835 | }
836 | }
837 | }
838 |
839 | return tags;
840 | };
841 |
842 | twttr.txt.modifyIndicesFromUnicodeToUTF16 = function(text, entities) {
843 | twttr.txt.convertUnicodeIndices(text, entities, false);
844 | };
845 |
846 | twttr.txt.modifyIndicesFromUTF16ToUnicode = function(text, entities) {
847 | twttr.txt.convertUnicodeIndices(text, entities, true);
848 | };
849 |
850 | twttr.txt.convertUnicodeIndices = function(text, entities, indicesInUTF16) {
851 | if (entities.length == 0) {
852 | return;
853 | }
854 |
855 | var charIndex = 0;
856 | var codePointIndex = 0;
857 |
858 | // sort entities by start index
859 | entities.sort(function(a,b){ return a.indices[0] - b.indices[0]; });
860 | var entityIndex = 0;
861 | var entity = entities[0];
862 |
863 | while (charIndex < text.length) {
864 | if (entity.indices[0] == (indicesInUTF16 ? charIndex : codePointIndex)) {
865 | var len = entity.indices[1] - entity.indices[0];
866 | entity.indices[0] = indicesInUTF16 ? codePointIndex : charIndex;
867 | entity.indices[1] = entity.indices[0] + len;
868 |
869 | entityIndex++;
870 | if (entityIndex == entities.length) {
871 | // no more entity
872 | break;
873 | }
874 | entity = entities[entityIndex];
875 | }
876 |
877 | var c = text.charCodeAt(charIndex);
878 | if (0xD800 <= c && c <= 0xDBFF && charIndex < text.length - 1) {
879 | // Found high surrogate char
880 | c = text.charCodeAt(charIndex + 1);
881 | if (0xDC00 <= c && c <= 0xDFFF) {
882 | // Found surrogate pair
883 | charIndex++;
884 | }
885 | }
886 | codePointIndex++;
887 | charIndex++;
888 | }
889 | };
890 |
891 | // this essentially does text.split(/<|>/)
892 | // except that won't work in IE, where empty strings are ommitted
893 | // so "<>".split(/<|>/) => [] in IE, but is ["", "", ""] in all others
894 | // but "<<".split("<") => ["", "", ""]
895 | twttr.txt.splitTags = function(text) {
896 | var firstSplits = text.split("<"),
897 | secondSplits,
898 | allSplits = [],
899 | split;
900 |
901 | for (var i = 0; i < firstSplits.length; i += 1) {
902 | split = firstSplits[i];
903 | if (!split) {
904 | allSplits.push("");
905 | } else {
906 | secondSplits = split.split(">");
907 | for (var j = 0; j < secondSplits.length; j += 1) {
908 | allSplits.push(secondSplits[j]);
909 | }
910 | }
911 | }
912 |
913 | return allSplits;
914 | };
915 |
916 | twttr.txt.hitHighlight = function(text, hits, options) {
917 | var defaultHighlightTag = "em";
918 |
919 | hits = hits || [];
920 | options = options || {};
921 |
922 | if (hits.length === 0) {
923 | return text;
924 | }
925 |
926 | var tagName = options.tag || defaultHighlightTag,
927 | tags = ["<" + tagName + ">", "" + tagName + ">"],
928 | chunks = twttr.txt.splitTags(text),
929 | i,
930 | j,
931 | result = "",
932 | chunkIndex = 0,
933 | chunk = chunks[0],
934 | prevChunksLen = 0,
935 | chunkCursor = 0,
936 | startInChunk = false,
937 | chunkChars = chunk,
938 | flatHits = [],
939 | index,
940 | hit,
941 | tag,
942 | placed,
943 | hitSpot;
944 |
945 | for (i = 0; i < hits.length; i += 1) {
946 | for (j = 0; j < hits[i].length; j += 1) {
947 | flatHits.push(hits[i][j]);
948 | }
949 | }
950 |
951 | for (index = 0; index < flatHits.length; index += 1) {
952 | hit = flatHits[index];
953 | tag = tags[index % 2];
954 | placed = false;
955 |
956 | while (chunk != null && hit >= prevChunksLen + chunk.length) {
957 | result += chunkChars.slice(chunkCursor);
958 | if (startInChunk && hit === prevChunksLen + chunkChars.length) {
959 | result += tag;
960 | placed = true;
961 | }
962 |
963 | if (chunks[chunkIndex + 1]) {
964 | result += "<" + chunks[chunkIndex + 1] + ">";
965 | }
966 |
967 | prevChunksLen += chunkChars.length;
968 | chunkCursor = 0;
969 | chunkIndex += 2;
970 | chunk = chunks[chunkIndex];
971 | chunkChars = chunk;
972 | startInChunk = false;
973 | }
974 |
975 | if (!placed && chunk != null) {
976 | hitSpot = hit - prevChunksLen;
977 | result += chunkChars.slice(chunkCursor, hitSpot) + tag;
978 | chunkCursor = hitSpot;
979 | if (index % 2 === 0) {
980 | startInChunk = true;
981 | } else {
982 | startInChunk = false;
983 | }
984 | } else if(!placed) {
985 | placed = true;
986 | result += tag;
987 | }
988 | }
989 |
990 | if (chunk != null) {
991 | if (chunkCursor < chunkChars.length) {
992 | result += chunkChars.slice(chunkCursor);
993 | }
994 | for (index = chunkIndex + 1; index < chunks.length; index += 1) {
995 | result += (index % 2 === 0 ? chunks[index] : "<" + chunks[index] + ">");
996 | }
997 | }
998 |
999 | return result;
1000 | };
1001 |
1002 | var MAX_LENGTH = 140;
1003 |
1004 | // Characters not allowed in Tweets
1005 | var INVALID_CHARACTERS = [
1006 | // BOM
1007 | fromCode(0xFFFE),
1008 | fromCode(0xFEFF),
1009 |
1010 | // Special
1011 | fromCode(0xFFFF),
1012 |
1013 | // Directional Change
1014 | fromCode(0x202A),
1015 | fromCode(0x202B),
1016 | fromCode(0x202C),
1017 | fromCode(0x202D),
1018 | fromCode(0x202E)
1019 | ];
1020 |
1021 | // Check the text for any reason that it may not be valid as a Tweet. This is meant as a pre-validation
1022 | // before posting to api.twitter.com. There are several server-side reasons for Tweets to fail but this pre-validation
1023 | // will allow quicker feedback.
1024 | //
1025 | // Returns false if this text is valid. Otherwise one of the following strings will be returned:
1026 | //
1027 | // "too_long": if the text is too long
1028 | // "empty": if the text is nil or empty
1029 | // "invalid_characters": if the text contains non-Unicode or any of the disallowed Unicode characters
1030 | twttr.txt.isInvalidTweet = function(text) {
1031 | if (!text) {
1032 | return "empty";
1033 | }
1034 |
1035 | if (text.length > MAX_LENGTH) {
1036 | return "too_long";
1037 | }
1038 |
1039 | for (var i = 0; i < INVALID_CHARACTERS.length; i++) {
1040 | if (text.indexOf(INVALID_CHARACTERS[i]) >= 0) {
1041 | return "invalid_characters";
1042 | }
1043 | }
1044 |
1045 | return false;
1046 | };
1047 |
1048 | twttr.txt.isValidTweetText = function(text) {
1049 | return !twttr.txt.isInvalidTweet(text);
1050 | };
1051 |
1052 | twttr.txt.isValidUsername = function(username) {
1053 | if (!username) {
1054 | return false;
1055 | }
1056 |
1057 | var extracted = twttr.txt.extractMentions(username);
1058 |
1059 | // Should extract the username minus the @ sign, hence the .slice(1)
1060 | return extracted.length === 1 && extracted[0] === username.slice(1);
1061 | };
1062 |
1063 | var VALID_LIST_RE = regexSupplant(/^#{validMentionOrList}$/);
1064 |
1065 | twttr.txt.isValidList = function(usernameList) {
1066 | var match = usernameList.match(VALID_LIST_RE);
1067 |
1068 | // Must have matched and had nothing before or after
1069 | return !!(match && match[1] == "" && match[4]);
1070 | };
1071 |
1072 | twttr.txt.isValidHashtag = function(hashtag) {
1073 | if (!hashtag) {
1074 | return false;
1075 | }
1076 |
1077 | var extracted = twttr.txt.extractHashtags(hashtag);
1078 |
1079 | // Should extract the hashtag minus the # sign, hence the .slice(1)
1080 | return extracted.length === 1 && extracted[0] === hashtag.slice(1);
1081 | };
1082 |
1083 | twttr.txt.isValidUrl = function(url, unicodeDomains, requireProtocol) {
1084 | if (unicodeDomains == null) {
1085 | unicodeDomains = true;
1086 | }
1087 |
1088 | if (requireProtocol == null) {
1089 | requireProtocol = true;
1090 | }
1091 |
1092 | if (!url) {
1093 | return false;
1094 | }
1095 |
1096 | var urlParts = url.match(twttr.txt.regexen.validateUrlUnencoded);
1097 |
1098 | if (!urlParts || urlParts[0] !== url) {
1099 | return false;
1100 | }
1101 |
1102 | var scheme = urlParts[1],
1103 | authority = urlParts[2],
1104 | path = urlParts[3],
1105 | query = urlParts[4],
1106 | fragment = urlParts[5];
1107 |
1108 | if (!(
1109 | (!requireProtocol || (isValidMatch(scheme, twttr.txt.regexen.validateUrlScheme) && scheme.match(/^https?$/i))) &&
1110 | isValidMatch(path, twttr.txt.regexen.validateUrlPath) &&
1111 | isValidMatch(query, twttr.txt.regexen.validateUrlQuery, true) &&
1112 | isValidMatch(fragment, twttr.txt.regexen.validateUrlFragment, true)
1113 | )) {
1114 | return false;
1115 | }
1116 |
1117 | return (unicodeDomains && isValidMatch(authority, twttr.txt.regexen.validateUrlUnicodeAuthority)) ||
1118 | (!unicodeDomains && isValidMatch(authority, twttr.txt.regexen.validateUrlAuthority));
1119 | };
1120 |
1121 | function isValidMatch(string, regex, optional) {
1122 | if (!optional) {
1123 | // RegExp["$&"] is the text of the last match
1124 | // blank strings are ok, but are falsy, so we check stringiness instead of truthiness
1125 | return ((typeof string === "string") && string.match(regex) && RegExp["$&"] === string);
1126 | }
1127 |
1128 | // RegExp["$&"] is the text of the last match
1129 | return (!string || (string.match(regex) && RegExp["$&"] === string));
1130 | }
1131 |
1132 | if (typeof module != 'undefined' && module.exports) {
1133 | module.exports = twttr.txt;
1134 | }
1135 |
1136 | }());
1137 |
--------------------------------------------------------------------------------