port stuff

This commit is contained in:
Sven Fuchs 2012-06-21 01:22:23 +02:00
parent 0b8eb94dd5
commit 697676f6ff
36 changed files with 1973 additions and 132 deletions

43
NOTES.txt Normal file
View File

@ -0,0 +1,43 @@
# Wat
* wat. http://jsfiddle.net/svenfuchs/YPeQp
* Where the frack am i supposed to put view/display related helper logic
when using #each? (e.g. format published_at for each blog post on blog/)
* Oh, uh, now there's `contextBinding`? What's that? How's that different from
`contentBinding`?
# Handlebars
* Can't {{bindAttr}} be just {{attr}}? Who cares it's "bound" in that context?
{{#each}} isn't {{#bindEach}} either.
* Why is {{#collection contentBinding="foo"}} not just {{#collection foo}}?
Also, can {{#collection contentBinding="content"}} be just {{#collection}}?
# Router
* I think `Route#connectOutlets` should be something like `action`, `run`,
`call` or similar (*if* it's intended to be the main user facing method).
* I didn't expect I'd have to pay attention to the timing of loading records
(even when they're present locally as fixtures) in `Router#serialize`. Is it
not possible to make this a bound property? Everything else that I use in
a handlebar template seems to be capable of being updated late except for
the url generation?
* The `Route#serialize` method is called with various types objects as the
second argument, which confused the heck out of me. Depending on what I set
as a context in `connectOutlet` and then define as a context in the action
helper (does it need to be quoted or not? what's the namespace and current
scope there?) I might get a controller or model instance. When I use the
back/forward button I get a plain object?
# Stuff
* I'm sure joining get and getPath was considered, right? I keep forgetting
to change `get` to `getPath` when I move stuff around and now need to
look up a path instead of a single key. What's the reason for separate
methods? Performance?

View File

@ -1,3 +1,4 @@
#= require_tree ./helpers
#= require_tree ./models
#= require_tree ./templates
#= require ./controllers.js
@ -10,4 +11,12 @@ Travis.store = DS.Store.extend(
revision: 4
adapter: Travis.FixtureAdapter.create()
).create()
# apparently fixtures behave weird unless preloaded :/ should move to mockjax for testing
Travis.Build.find()
Travis.Repository.find()
Travis.Commit.find()
Travis.Job.find()
Travis.Artifact.find()
Travis.initialize()

View File

@ -1,8 +1,14 @@
Travis.ApplicationController = Em.Controller.extend()
Travis.RepositoriesController = Em.ArrayController.extend()
Travis.RepositoryController = Em.Controller.extend()
Travis.RepositoryController = Em.ObjectController.extend(Travis.Urls.Repository)
Travis.TabsController = Em.Controller.extend()
Travis.CurrentController = Em.Controller.extend()
Travis.HistoryController = Em.ArrayController.extend()
Travis.BuildController = Em.Controller.extend()
Travis.JobController = Em.ObjectController.extend()
Travis.LoadingController = Em.Controller.extend()
Travis.CurrentController = Travis.BuildController = Em.ObjectController.extend
classes: (->
Travis.Helpers.colorForResult(@getPath('content.result'))
).property('content.result')

View File

@ -0,0 +1,38 @@
safe = (string) ->
new Handlebars.SafeString(string)
Handlebars.registerHelper 'whats_this', (id) ->
safe '<span title="What\'s This?" class="whats_this" onclick="$.facebox({ div: \'#' + id + '\'})">&nbsp;</span>'
Handlebars.registerHelper 'tipsy', (text, tip) ->
safe '<span class="tool-tip" original-title="' + tip + '">' + text + '</span>'
Handlebars.registerHelper 't', (key) ->
safe I18n.t(key)
Ember.registerBoundHelper 'formatTime', (value, options) ->
safe Travis.Helpers.timeAgoInWords(value) || '-'
Ember.registerBoundHelper 'formatDuration', (duration, options) ->
safe Travis.Helpers.timeInWords(duration)
Ember.registerBoundHelper 'formatCommit', (commit, options) ->
branch = commit.get('branch')
branch = " #{branch}" if branch
safe (commit.get('sha') || '').substr(0, 7) + branch
Ember.registerBoundHelper 'formatSha', (sha, options) ->
safe (sha || '').substr(0, 7)
Ember.registerBoundHelper 'pathFrom', (url, options) ->
safe (url || '').split('/').pop()
Ember.registerBoundHelper 'formatMessage', (message, options) ->
safe Travis.Helpers.formatMessage(message, options)
Ember.registerBoundHelper 'formatConfig', (config, options) ->
safe Travis.Helpers.formatConfig(config)
Ember.registerBoundHelper 'formatLog', (log, options) ->
Travis.Log.filter(log) if log

View File

@ -0,0 +1,65 @@
@Travis.Helpers =
colorForResult: (result) ->
(if result is 0 then 'green' else (if result is 1 then 'red' else null))
formatConfig: (config) ->
config = $.only config, 'rvm', 'gemfile', 'env', 'otp_release', 'php', 'node_js', 'scala', 'jdk', 'python', 'perl'
values = $.map config, (value, key) ->
value = (if value && value.join then value.join(', ') else value) || ''
'%@: %@'.fmt $.camelize(key), value
if values.length == 0 then '-' else values.join(', ')
formatMessage: (message, options) ->
message = message or ''
message = message.split(/\n/)[0] if options.short
@_emojize(@_escape(message)).replace /\n/g, '<br/>'
timeAgoInWords: (date) ->
$.timeago.distanceInWords date
durationFrom: (started, finished) ->
started = started and @_toUtc(new Date(@_normalizeDateString(started)))
finished = if finished then @_toUtc(new Date(@_normalizeDateString(finished))) else @_nowUtc()
if started && finished then Math.round((finished - started) / 1000) else 0
timeInWords: (duration) ->
days = Math.floor(duration / 86400)
hours = Math.floor(duration % 86400 / 3600)
minutes = Math.floor(duration % 3600 / 60)
seconds = duration % 60
if days > 0
'more than 24 hrs'
else
result = []
result.push hours + ' hr' if hours is 1
result.push hours + ' hrs' if hours > 1
result.push minutes + ' min' if minutes > 0
result.push seconds + ' sec' if seconds > 0
if result.length > 0 then result.join(' ') else '-'
_normalizeDateString: (string) ->
if window.JHW
string = string.replace('T', ' ').replace(/-/g, '/')
string = string.replace('Z', '').replace(/\..*$/, '')
string
_nowUtc: ->
@_toUtc new Date()
_toUtc: (date) ->
Date.UTC date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()
_emojize: (text) ->
emojis = text.match(/:\S+?:/g)
if emojis isnt null
$.each emojis.uniq(), (ix, emoji) ->
strippedEmoji = emoji.substring(1, emoji.length - 1)
unless EmojiDictionary.indexOf(strippedEmoji) is -1
image = '<img class=\'emoji\' title=\'' + emoji + '\' alt=\'' + emoji + '\' src=\'' + Travis.assets.host + '/' + Travis.assets.version + '/images/emoji/' + strippedEmoji + '.png\'/>'
text = text.replace(new RegExp(emoji, 'g'), image)
text
_escape: (text) ->
text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace />/g, '&gt;'

View File

@ -0,0 +1,36 @@
@Travis.Urls =
Repository:
urlGithub: (->
'http://github.com/%@'.fmt @get('slug')
).property('slug'),
urlGithubWatchers: (->
'http://github.com/%@/watchers'.fmt @get('slug')
).property('slug'),
urlGithubNetwork: (->
'http://github.com/%@/network'.fmt @get('slug')
).property('slug'),
urlGithubAdmin: (->
'http://github.com/%@/admin/hooks#travis_minibucket'.fmt @get('slug')
).property('slug')
statusImage: (->
'%@.png'.fmt @get('slug')
).property('slug')
Commit:
urlAuthor: (->
'mailto:%@'.fmt @getPath('commit.author_email')
).property('commit')
urlCommitter: (->
'mailto:%@'.fmt @getPath('commit.committer_email')
).property('commit')
Build:
githubCommit: (->
'http://github.com/%@/commit/%@'.fmt @getPath('repository.slug'), @getPath('commit.sha')
).property('repository.slug', 'commit.sha')

View File

@ -0,0 +1,11 @@
@Travis.Artifact = Travis.Model.extend
body: DS.attr('string')
@Travis.Artifact.FIXTURES = [
{ id: 1, body: 'log 1' }
{ id: 2, body: 'log 2' }
{ id: 3, body: 'log 3' }
{ id: 4, body: 'log 4' }
{ id: 5, body: 'log 4' }
]

View File

@ -0,0 +1,24 @@
@Travis.Branch = Travis.Model.extend Travis.Helpers,
repository_id: DS.attr('number')
number: DS.attr('number')
branch: DS.attr('string')
message: DS.attr('string')
result: DS.attr('number')
duration: DS.attr('number')
started_at: DS.attr('string')
finished_at: DS.attr('string')
commit: DS.belongsTo('Travis.Commit')
repository: (->
Travis.Repository.find @get('repository_id') if @get('repository_id')
).property('repository_id').cacheable()
tick: ->
@notifyPropertyChange 'started_at'
@notifyPropertyChange 'finished_at'
@Travis.Branch.reopenClass
byRepositoryId: (id) ->
@find repository_id: id

View File

@ -1,5 +1,4 @@
@Travis.Build = Travis.Model.extend # Travis.Helpers,
repository_id: DS.attr('number')
@Travis.Build = Travis.Model.extend
state: DS.attr('string')
number: DS.attr('number')
branch: DS.attr('string')
@ -28,8 +27,8 @@
).property('data.job_ids.length')
isFailureMatrix: (->
@get('allowedFailureJobs').length > 0
).property('allowedFailureJobs')
@getPath('allowedFailureJobs.length') > 0
).property('allowedFailureJobs.length')
# TODO why does the hasMany association not work?
jobs: (->
@ -37,13 +36,21 @@
).property('data.job_ids.length')
requiredJobs: (->
@get('jobs').filter (item, index) -> item.get('allow_failure') isnt true
@get('jobs').filter (job) -> job.get('allow_failure') != true
).property('jobs')
allowedFailureJobs: (->
@get('jobs').filter (item, index) -> item.get 'allow_failure'
@get('jobs').filter (job) -> job.get 'allow_failure'
).property('jobs')
configKeys: (->
config = @get('config')
return [] unless config
keys = $.keys($.only(config, 'rvm', 'gemfile', 'env', 'otp_release', 'php', 'node_js', 'perl', 'python', 'scala'))
headers = [I18n.t('build.job'), I18n.t('build.duration'), I18n.t('build.finished_at')]
$.map(headers.concat(keys), (key) -> return $.camelize(key))
).property('config')
tick: ->
@notifyPropertyChange 'duration'
@notifyPropertyChange 'finished_at'
@ -56,8 +63,8 @@
@find(url: '/repositories/' + id + '/builds.json?bare=true&after_number=' + build_number, repository_id: id, orderBy: 'number DESC')
@Travis.Build.FIXTURES = [
{ id: 1, repository_id: 1, number: 1, event_type: 'push' },
{ id: 2, repository_id: 1, number: 2, event_type: 'push' },
{ id: 3, repository_id: 2, number: 3, event_type: 'push' },
{ id: 4, repository_id: 3, number: 4, event_type: 'push' }
{ id: 1, repository_id: 1, commit_id: 1, job_ids: [1, 2], number: 1, event_type: 'push', config: { rvm: ['rbx', '1.9.3'] }, finished_at: '2012-06-20T00:21:20Z', duration: 35, result: 0 },
{ id: 2, repository_id: 1, commit_id: 2, job_ids: [1], number: 2, event_type: 'push' },
{ id: 3, repository_id: 2, commit_id: 3, job_ids: [2], number: 3, event_type: 'push' },
{ id: 4, repository_id: 3, commit_id: 4, job_ids: [3], number: 4, event_type: 'push' }
]

View File

@ -0,0 +1,19 @@
@Travis.Commit = Travis.Model.extend
sha: DS.attr('string')
branch: DS.attr('string')
message: DS.attr('string')
compare_url: DS.attr('string')
author_name: DS.attr('string')
author_email: DS.attr('string')
committer_name: DS.attr('string')
committer_email: DS.attr('string')
build: DS.belongsTo('Travis.Build')
@Travis.Commit.FIXTURES = [
{ id: 1, sha: '123456', branch: 'master', message: 'the commit message', compare_url: 'http://github.com/compare', author_name: 'Author', author_email: 'author@email.org', committer_name: 'Committer', committer_email: 'committer@email.org' }
{ id: 2, sha: '234567', branch: 'feature', message: 'the commit message', compare_url: 'http://github.com/compare', author_name: 'Author', author_email: 'author@email.org', committer_name: 'Committer', committer_email: 'committer@email.org' }
{ id: 3, sha: '345678', branch: 'master', message: 'the commit message', compare_url: 'http://github.com/compare', author_name: 'Author', author_email: 'author@email.org', committer_name: 'Committer', committer_email: 'committer@email.org' }
{ id: 4, sha: '456789', branch: 'master', message: 'the commit message', compare_url: 'http://github.com/compare', author_name: 'Author', author_email: 'author@email.org', committer_name: 'Committer', committer_email: 'committer@email.org' }
]

View File

@ -0,0 +1,61 @@
@Travis.Job = Travis.Model.extend
repository_id: DS.attr('number')
build_id: DS.attr('number')
log_id: DS.attr('number')
queue: DS.attr('string')
state: DS.attr('string')
number: DS.attr('string')
result: DS.attr('number')
duration: DS.attr('number')
started_at: DS.attr('string')
finished_at: DS.attr('string')
allow_failure: DS.attr('boolean')
repository: DS.belongsTo('Travis.Repository')
commit: DS.belongsTo('Travis.Commit')
build: DS.belongsTo('Travis.Build')
log: DS.belongsTo('Travis.Artifact')
config: (->
@getPath 'data.config'
).property('data.config')
sponsor: (->
@getPath('data.sponsor')
).property('data.sponsor')
configValues: (->
config = @get('config')
return [] unless config
$.values($.only(config, 'rvm', 'gemfile', 'env', 'otp_release', 'php', 'node_js', 'scala', 'jdk', 'python', 'perl'))
).property('config')
appendLog: (log) ->
@set 'log', @get('log') + log
subscribe: ->
Travis.app.subscribe 'job-' + @get('id')
onStateChange: (->
Travis.app.unsubscribe 'job-' + @get('id') if @get('state') == 'finished'
).observes('state')
tick: ->
@notifyPropertyChange 'duration'
@notifyPropertyChange 'finished_at'
@Travis.Job.reopenClass
queued: (queue) ->
@all()
Travis.store.filter this, (job) -> job.get('queue') == 'builds.' + queue
findMany: (ids) ->
Travis.store.findMany this, ids
@Travis.Job.FIXTURES = [
{ id: 1, repository_id: 1, build_id: 1, log_id: 1, number: '1.1', config: { rvm: 'rbx' }, finished_at: '2012-06-20T00:21:20Z', duration: 35, result: 0 }
{ id: 2, repository_id: 1, build_id: 1, log_id: 2, number: '1.2', config: { rvm: '1.9.3' } }
{ id: 3, repository_id: 1, build_id: 2, log_id: 3, number: '2.1' }
{ id: 4, repository_id: 2, build_id: 3, log_id: 4, number: '3.1' }
{ id: 5, repository_id: 3, build_id: 5, log_id: 5, number: '4.1' }
]

View File

@ -1,5 +1,4 @@
@Travis.Repository = Travis.Model.extend # Travis.Helpers,
slug: DS.attr('string')
@Travis.Repository = Travis.Model.extend
name: DS.attr('string')
owner: DS.attr('string')
description: DS.attr('string')
@ -9,6 +8,8 @@
last_build_started_at: DS.attr('string')
last_build_finished_at: DS.attr('string')
lastBuild: DS.belongsTo('Travis.Build')
builds: (->
Travis.Build.byRepositoryId @get('id'), event_type: 'push'
).property()
@ -17,21 +18,22 @@
Travis.Build.byRepositoryId @get('id'), event_type: 'pull_request'
).property()
lastBuild: (->
Travis.Build.find @get('last_build_id')
).property('last_build_id')
slug: (->
"#{@get('owner')}/#{@get('name')}"
).property('owner', 'name'),
last_build_duration: (->
duration = @getPath('data.last_build_duration')
duration = @durationFrom(@get('last_build_started_at'), @get('last_build_finished_at')) unless duration
duration = Travis.Helpers.durationFrom(@get('last_build_started_at'), @get('last_build_finished_at')) unless duration
duration
).property('data.last_build_duration', 'last_build_started_at', 'last_build_finished_at')
stats: (->
return unless Travis.env is 'production'
url = 'https://api.github.com/json/repos/show/' + @get('slug')
@get('_stats') || $.get(url, (data) => @set('_stats', data)) && undefined
).property('_stats')
@get('_stats') || $.get("https://api.github.com/repos/#{@get('slug')}", (data) =>
@set('_stats', data)
@notifyPropertyChange 'stats'
) && {}
).property('slug')
select: ->
Travis.Repository.select(self.get('id'))
@ -59,9 +61,9 @@
repository.set 'selected', repository.get('id') is id
@Travis.Repository.FIXTURES = [
{ id: 1, owner: 'travis-ci', name: 'travis-core', build_ids: [1, 2] },
{ id: 2, owner: 'travis-ci', name: 'travis-assets', build_ids: [3] },
{ id: 3, owner: 'travis-ci', name: 'travis-hub', build_ids: [4] },
{ id: 1, owner: 'travis-ci', name: 'travis-core', build_ids: [1, 2], last_build_id: 1, last_build_number: 1, last_build_result: 0 },
{ id: 2, owner: 'travis-ci', name: 'travis-assets', build_ids: [3] , last_build_id: 3, last_build_number: 3},
{ id: 3, owner: 'travis-ci', name: 'travis-hub', build_ids: [4] , last_build_id: 4, last_build_number: 4},
]

View File

@ -0,0 +1,18 @@
@Travis.ServiceHook = Travis.Model.extend
primaryKey: 'slug'
name: DS.attr('string')
owner_name: DS.attr('string')
active: DS.attr('boolean')
slug: (->
[@get('owner_name'), @get('name')].join('/')
).property()
toggle: ->
@set 'active', !@get('active')
Travis.app.store.commit()
@Travis.ServiceHook.reopenClass
url: 'profile/service_hooks'

View File

@ -0,0 +1,36 @@
@Travis.WorkerGroup = Ember.ArrayProxy.extend
init: ->
@set('content', [])
host: (->
@getPath 'firstObject.host'
).property()
@Travis.Worker = Travis.Model.extend
state: DS.attr('string')
name: DS.attr('string')
host: DS.attr('string')
last_seen_at: DS.attr('string')
isTesting: (->
@get('state') == 'working' && !!@getPath('payload.config')
).property('state', 'config')
number: (->
@get('name').match(/\d+$/)[0]
).property('name')
display: (->
name = @get('name').replace('travis-', '')
state = @get('state')
payload = @get('payload')
if state == 'working' && payload != undefined
repo = if payload.repository then $.truncate(payload.repository.slug, 18) else undefined
number = if payload.build and payload.build.number then ' #' + payload.build.number else ''
state = if repo then repo + number else state
name + ': ' + state
).property('state')
urlJob: (->
'#!/%@/jobs/%@'.fmt @getPath('payload.repository.slug'), @getPath('payload.build.id')
).property('payload', 'state')

View File

@ -26,7 +26,6 @@ Travis.Router = Em.Router.extend
onceLoaded builds, ->
router.connectCurrent builds.get('firstObject')
viewCurrent: Ember.Route.transitionTo('current')
history: Em.Route.extend
route: '/:owner/:name/builds'
serialize: (router, repository) ->
@ -39,18 +38,31 @@ Travis.Router = Em.Router.extend
onceLoaded builds, ->
router.connectHistory builds
viewHistory: Ember.Route.transitionTo('history')
build: Em.Route.extend
route: '/:owner/:name/builds/:id'
serialize: (router, build) ->
router.serializeBuild build
router.serializeObject build
connectOutlets: (router, build) ->
params = router.serializeBuild(build)
params = router.serializeObject(build)
router.connectLayout params, (repository, build) ->
router.connectBuild build
job: Em.Route.extend
route: '/:owner/:name/jobs/:id'
serialize: (router, job) ->
console.log job
router.serializeObject job
connectOutlets: (router, job) ->
params = router.serializeObject(job)
router.connectLayout params, (repository, job) ->
router.connectJob job
viewCurrent: Ember.Route.transitionTo('current')
viewHistory: Ember.Route.transitionTo('history')
viewBuild: Ember.Route.transitionTo('build')
viewJob: Ember.Route.transitionTo('job')
serializeRepository: (repository) ->
if repository instanceof DS.Model
@ -58,14 +70,14 @@ Travis.Router = Em.Router.extend
else
repository or {}
serializeBuild: (build) ->
if build instanceof DS.Model
repository = build.get('repository')
serializeObject: (object) ->
if object instanceof DS.Model
repository = object.get('repository')
params = @serializeRepository(repository)
$.extend params,
id: build.get('id')
id: object.get('id')
else
build or {}
object or {}
connectLayout: (params, callback) ->
repositories = Travis.Repository.find()
@ -130,3 +142,9 @@ Travis.Router = Em.Router.extend
outletName: 'tab'
name: 'build'
context: build
connectJob: (job) ->
@get('repositoryController').connectOutlet
outletName: 'tab'
name: 'job'
context: job

View File

@ -1,6 +1,27 @@
<ul>
{{#each build in content}}
<li><a {{action viewBuild href=true context="build"}}>Build #{{build.number}}</a></li>
{{/each}}
</ul>
<table id="builds">
<thead>
<tr>
<th>{{t builds.name}}</th>
<th>{{t builds.commit}}</th>
<th>{{t builds.message}}</th>
<th>{{t builds.duration}}</th>
<th>{{t builds.finished_at}}</th>
</tr>
</thead>
{{#collection tagName="tbody" contentBinding="content" itemViewClass="Travis.BuildsItemView" itemClassBinding="color"}}
{{#with view.content}}
<td class="number"><a {{action viewBuild href=true context="content"}}>{{number}}</a></td>
<td class="commit"><a {{bindAttr href="urlGithubCommit"}}>{{formatCommit commit}}</a> {{commit.sha}}</td>
<td class="message">{{{formatMessage commit.message short="true"}}}</td>
<td class="duration" {{bindAttr title="started_at"}}>{{formatDuration duration}}</td>
<td class="finished_at timeago" {{bindAttr title="finished_at"}}>{{formatTime finished_at}}</td>
{{/with}}
{{/collection}}
</table>
<p>
<button {{action showMore on="click" target="builds" isVisibleBinding="hasMore"}}>
{{t builds.show_more}}
</button>
</p>

View File

@ -1,2 +1,46 @@
build {{content.id}}
<div {{bindAttr class="classes"}}>
<dl class="summary clearfix">
<div class="left">
<dt>{{t builds.name}}</dt>
<dd class="number"><a {{bindAttr href="urlBuild"}}>{{number}}</a></dd>
<dt class="finished_at_label">{{t builds.finished_at}}</dt>
<dd class="finished_at timeago" {{bindAttr title="finished_at"}}>{{formatTime finished_at}}</dd>
<dt>{{t builds.duration}}</dt>
<dd class="duration" {{bindAttr title="started_at"}}>{{formatDuration duration}}</dd>
</div>
<div class="right">
<dt>{{t builds.commit}}</dt>
<dd class="commit-hash"><a {{bindAttr href="urlGithubCommit"}}>{{formatCommit commit}}</a></dd>
{{#if commit.compare_url}}
<dt>{{t builds.compare}}</dt>
<dd class="compare_view"><a {{bindAttr href="commit.compare_url"}}>{{pathFrom commit.compare_url}}</a></dd>
{{/if}}
{{#if commit.author_name}}
<dt>{{t builds.author}}</dt>
<dd class="author"><a {{bindAttr href="view.urlAuthor"}}>{{commit.author_name}}</a></dd>
{{/if}}
{{#if commit.committer_name}}
<dt>{{t builds.committer}}</dt>
<dd class="committer"><a {{bindAttr href="urlCommitter"}}>{{commit.committer_name}}</a></dd>
{{/if}}
</div>
<dt>{{t builds.message}}</dt>
<dd class="commit-message">{{{formatMessage commit.message}}}</dd>
{{#if isMatrix}}
{{else}}
<dt>{{t builds.config}}</dt>
<dd class="config">{{formatConfig config}}</dd>
{{/if}}
</dl>
{{#if isLoaded}}
{{#if isMatrix}}
{{view Travis.JobsView}}
{{else}}
{{view Travis.LogView}}
{{/if}}
{{/if}}
</div>

View File

@ -0,0 +1,70 @@
<table id="jobs">
<caption>{{t jobs.build_matrix}}</caption>
<thead>
<tr>
{{#each configKeys}}
<th>{{this}}</th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each requiredJobs}}
<tr {{bindAttr class="color"}}>
<td class="number"><a {{action viewJob href=true context=this}}>{{number}}</a></td>
<td class="duration" {{bindAttr title="started_at"}}>{{formatDuration duration}}</td>
<td class="finished_at timeago" {{bindAttr title="finished_at"}}>{{formatTime finished_at}}</td>
{{#each configValues}}
<td>{{this}}</td>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>
{{#if isFailureMatrix}}
<table id="allow_failure_builds">
<caption>
{{t jobs.allowed_failures}}{{whats_this allow_failure_help}}
</caption>
<thead>
<tr>
{{#each configKeys}}
<th>{{this}}</th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each allowedFailureJobs}}
<tr {{bindAttr class="color"}}>
<td class="number"><a {{action viewJob href=true}}>{{number}}</a></td>
<td class="duration" {{bindAttr title="started_at"}}>{{formatDuration duration}}</td>
<td class="finished_at timeago" {{bindAttr title="finished_at"}}>{{formatTime finished_at}}</td>
{{#each configValues}}
<td>{{this}}</td>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>
<div id="allow_failure_help" class="context_help">
<div class="context_help_caption">{{t "jobs.allowed_failures"}}</div>
<div class="context_help_body">
<p>
Allowed Failures are items in your build matrix that are allowed to
fail without causing the entire build to be shown as failed. This lets you add
in experimental and preparatory builds to test against versions or
configurations that you are not ready to officially support.
</p>
<p>
You can define allowed failures in the build matrix as follows:
</p>
<pre>
matrix:
allow_failures:
- rvm: ruby-head
</pre>
</div>
</div>
{{/if}}

View File

@ -0,0 +1,11 @@
{{! ugh ... }}
{{#with jobs.firstObject}}
<pre class="log">{{{formatLog log.body}}}</pre>
{{#if sponsor.name}}
<p class="sponsor">
{{t builds.messages.sponsored_by}}
<a {{bindAttr href="sponsor.url"}}>{{sponsor.name}}</a>
</p>
{{/if}}
{{/with}}

View File

@ -0,0 +1,37 @@
<div {{bindAttr class="color"}}>
<dl class="summary clearfix">
<div class="left">
<dt>Job</dt>
<dd class="number"><a {{bindAttr href="urlJob"}}>{{number}}</a></dd>
<dt class="finished_at_label">{{t jobs.finished_at}}</dt>
<dd class="finished_at timeago" {{bindAttr title="finished_at"}}>{{formatTime finished_at}}</dd>
<dt>{{t jobs.duration}}</dt>
<dd class="duration" {{bindAttr title="started_at"}}>{{formatDuration duration}}</dd>
</div>
<div class="right">
<dt>{{t jobs.commit}}</dt>
<dd class="commit-hash"><a {{bindAttr href="urlGithubCommit"}}>{{formatCommit commit}}</a></dd>
{{#if commit.compare_url}}
<dt>{{t jobs.compare}}</dt>
<dd class="compare_view"><a {{bindAttr href="commit.compare_url"}}>{{pathFrom commit.compare_url}}</a></dd>
{{/if}}
{{#if commit.author_name}}
<dt>{{t jobs.author}}</dt>
<dd class="author"><a {{bindAttr href="urlAuthor"}}>{{commit.author_name}}</a></dd>
{{/if}}
{{#if commit.committer_name}}
<dt>{{t jobs.committer}}</dt>
<dd class="committer"><a {{bindAttr href="urlCommitter"}}>{{commit.committer_name}}</a></dd>
{{/if}}
</div>
<dt>{{t jobs.message}}</dt>
<dd class="commit-message">{{formatMessage commit.message}}</dd>
<dt>{{t jobs.config}}</dt>
<dd class="config">{{formatConfig config}}</dd>
</dl>
{{view Travis.LogView}}
</div>

View File

@ -1,6 +1,22 @@
<ul>
{{#each repository in content}}
<li><a {{action viewRepository href=true context="repository"}}>{{repository.owner}}/{{repository.name}}</a></li>
{{/each}}
</ul>
{{#collection tagName="ul" id="repositories" contentBinding="content" itemViewClass="Travis.RepositoriesItemView" itemClassBinding="classes"}}
{{#with view.content}}
<div class="wrapper">
<a {{action viewCurrent href=true context="content"}} class="slug">{{slug}}</a>
<a {{action viewBuild href=true context="lastBuild"}} class="build">#{{last_build_number}}</a>
<p class="summary">
<span class="duration_label">{{t repositories.duration}}:</span>
<abbr class="duration" {{bindAttr title="last_build_started_at"}}>{{formatDuration last_build_duration}}</abbr>,
<span class="finished_at_label">{{t repositories.finished_at}}:</span>
<abbr class="finished_at timeago" {{bindAttr title="last_build_finished_at"}}>{{formatTime last_build_finished_at}}</abbr>
</p>
{{#if description}}
<p class="description">{{description}}</p>
{{/if}}
<span class="indicator"></span>
</div>
{{/with}}
{{/collection}}
{{^collection contentBinding="repositories" id="list" class="loading"}}
<p></p>
{{/collection}}

View File

@ -1,4 +1,14 @@
<h2>{{content.owner}}/{{content.name}}</h2>
<h3>
<a {{bindAttr href="urlGithub"}}>{{slug}}</a>
</h3>
<p class="description">{{description}}</p>
<ul class="github-stats">
<li class="language">{{last_build_language}}</li>
<li><a class="watchers" title="Watches" {{bindAttr href="urlGithubWatchers"}}>{{stats.watchers}}</a></li>
<li><a class="forks" title="Forks" {{bindAttr href="urlGithubNetwork"}}>{{stats.forks}}</a></li>
</ul>
{{outlet tabs}}

View File

@ -1,5 +1,10 @@
<ul class="tabs">
<li><a {{action viewCurrent href=true context="repository"}} class="current">Current</a></li>
<li><a {{action viewHistory href=true context="repository"}} class="history">History</a></li>
<li><a {{action viewBuild href=true context="build"}} class="build">Build #{{build.number}}</a></li>
{{#if build}}
<li><a {{action viewBuild href=true context="build"}} class="build">Build #{{build.number}}</a></li>
{{/if}}
{{#if job}}
<li><a {{action viewJob href=true context="job"}} class="job">Job #{job.number}}</a></li>
{{/if}}
</ul>

View File

@ -1,9 +1,26 @@
Travis.ApplicationView = Em.View.extend templateName: 'application'
Travis.RepositoriesView = Em.View.extend templateName: 'repositories/list'
Travis.RepositoryView = Em.View.extend templateName: 'repositories/show'
Travis.TabsView = Em.View.extend templateName: 'repositories/tabs'
Travis.CurrentView = Em.View.extend templateName: 'builds/show'
Travis.HistoryView = Em.View.extend templateName: 'builds/list'
Travis.BuildView = Em.View.extend templateName: 'builds/show'
Travis.LoadingView = Em.View.extend templateName: 'loading'
Travis.RepositoriesItemView = Em.View.extend
classes: (->
color = Travis.Helpers.colorForResult(@getPath('content.last_build_result'))
classes = ['repository', color]
classes.push 'selected' if @getPath('content.selected')
classes.join(' ')
).property('content.last_build_result', 'content.selected')
Travis.RepositoryView = Em.View.extend templateName: 'repositories/show'
Travis.TabsView = Em.View.extend templateName: 'repositories/tabs'
Travis.HistoryView = Em.View.extend templateName: 'builds/list'
Travis.BuildsItemView = Em.View.extend
classes: (->
Travis.Helpers.colorForResult(@getPath('content.result'))
).property('content.result')
Travis.CurrentView = Travis.BuildView = Em.View.extend templateName: 'builds/show'
Travis.LoadingView = Em.View.extend templateName: 'loading'
Travis.JobsView = Em.View.extend templateName: 'jobs/list'
Travis.JobView = Em.View.extend templateName: 'jobs/show'
Travis.LogView = Em.View.extend templateName: 'jobs/log'

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,43 @@
@EmojiDictionary = [
'-1', '0', '1', '109', '2', '3', '4', '5', '6', '7', '8', '8ball', '9', 'a', 'ab', 'airplane', 'alien', 'ambulance', 'angel', 'anger', 'angry', 'apple',
'aquarius', 'aries', 'arrow_backward', 'arrow_down', 'arrow_forward', 'arrow_left', 'arrow_lower_left', 'arrow_lower_right', 'arrow_right',
'arrow_up', 'arrow_upper_left', 'arrow_upper_right', 'art', 'astonished', 'atm', 'b', 'baby', 'baby_chick', 'baby_symbol', 'balloon', 'bamboo', 'bank',
'barber', 'baseball', 'basketball', 'bath', 'bear', 'beer', 'beers', 'beginner', 'bell', 'bento', 'bike', 'bikini', 'bird', 'birthday',
'black_square', 'blue_car', 'blue_heart', 'blush', 'boar', 'boat', 'bomb', 'book', 'boot', 'bouquet', 'bow', 'bowtie', 'boy', 'bread', 'briefcase',
'broken_heart', 'bug', 'bulb', 'bullettrain_front', 'bullettrain_side', 'bus', 'busstop', 'cactus', 'cake', 'calling', 'camel', 'camera', 'cancer',
'capricorn', 'car', 'cat', 'cd', 'chart', 'checkered_flag', 'cherry_blossom', 'chicken', 'christmas_tree', 'church', 'cinema', 'city_sunrise',
'city_sunset', 'clap', 'clapper', 'clock1', 'clock10', 'clock11', 'clock12', 'clock2', 'clock3', 'clock4', 'clock5', 'clock6', 'clock7', 'clock8',
'clock9', 'closed_umbrella', 'cloud', 'clubs', 'cn', 'cocktail', 'coffee', 'cold_sweat', 'computer', 'confounded', 'congratulations', 'construction',
'construction_worker', 'convenience_store', 'cool', 'cop', 'copyright', 'couple', 'couple_with_heart', 'couplekiss', 'cow', 'crossed_flags', 'crown',
'cry', 'cupid', 'currency_exchange', 'curry', 'cyclone', 'dancer', 'dancers', 'dango', 'dart', 'dash', 'de', 'department_store', 'diamonds',
'disappointed', 'dog', 'dolls', 'dolphin', 'dress', 'dvd', 'ear', 'ear_of_rice', 'egg', 'eggplant', 'egplant', 'eight_pointed_black_star',
'eight_spoked_asterisk', 'elephant', 'email', 'es', 'european_castle', 'exclamation', 'eyes', 'factory', 'fallen_leaf', 'fast_forward', 'fax',
'fearful', 'feelsgood', 'feet', 'ferris_wheel', 'finnadie', 'fire', 'fire_engine', 'fireworks', 'fish', 'fist', 'flags', 'flushed', 'football',
'fork_and_knife', 'fountain', 'four_leaf_clover', 'fr', 'fries', 'frog', 'fuelpump', 'gb', 'gem', 'gemini', 'ghost', 'gift', 'gift_heart', 'girl',
'goberserk', 'godmode', 'golf', 'green_heart', 'grey_exclamation', 'grey_question', 'grin', 'guardsman', 'guitar', 'gun', 'haircut',
'hamburger', 'hammer', 'hamster', 'hand', 'handbag', 'hankey', 'hash', 'headphones', 'heart', 'heart_decoration', 'heart_eyes', 'heartbeat',
'heartpulse', 'hearts', 'hibiscus', 'high_heel', 'horse', 'hospital', 'hotel', 'hotsprings', 'house', 'hurtrealbad', 'icecream', 'id',
'ideograph_advantage', 'imp', 'information_desk_person', 'iphone', 'it', 'jack_o_lantern', 'japanese_castle', 'joy', 'jp', 'key', 'kimono', 'kiss',
'kissing_face', 'kissing_heart', 'koala', 'koko', 'kr', 'leaves', 'leo', 'libra', 'lips', 'lipstick', 'lock', 'loop', 'loudspeaker', 'love_hotel',
'mag', 'mahjong', 'mailbox', 'man', 'man_with_gua_pi_mao', 'man_with_turban', 'maple_leaf', 'mask', 'massage', 'mega', 'memo', 'mens', 'metal', 'metro',
'microphone', 'minidisc', 'mobile_phone_off', 'moneybag', 'monkey', 'monkey_face', 'moon', 'mortar_board', 'mount_fuji', 'mouse', 'movie_camera',
'muscle', 'musical_note', 'nail_care', 'necktie', 'new', 'no_good', 'no_smoking', 'nose', 'notes', 'o', 'o2', 'ocean', 'octocat', 'octopus',
'oden', 'office', 'ok', 'ok_hand', 'ok_woman', 'older_man', 'older_woman', 'open_hands', 'ophiuchus', 'palm_tree', 'parking', 'part_alternation_mark',
'pencil', 'penguin', 'pensive', 'persevere', 'person_with_blond_hair', 'phone', 'pig', 'pill', 'pisces', 'plus1', 'point_down', 'point_left',
'point_right', 'point_up', 'point_up_2', 'police_car', 'poop', 'post_office', 'postbox', 'pray', 'princess', 'punch', 'purple_heart', 'question', 'rabbit',
'racehorse', 'radio', 'rage', 'rage1', 'rage2', 'rage3', 'rage4', 'rainbow', 'raised_hands', 'ramen', 'red_car', 'red_circle', 'registered', 'relaxed',
'relieved', 'restroom', 'rewind', 'ribbon', 'rice', 'rice_ball', 'rice_cracker', 'rice_scene', 'ring', 'rocket', 'roller_coaster', 'rose',
'ru', 'runner', 'sa', 'sagittarius', 'sailboat', 'sake', 'sandal', 'santa', 'satellite', 'satisfied', 'saxophone', 'school', 'school_satchel',
'scissors', 'scorpius', 'scream', 'seat', 'secret', 'shaved_ice', 'sheep', 'shell', 'ship', 'shipit', 'shirt', 'shit', 'shoe', 'signal_strength',
'six_pointed_star', 'ski', 'skull', 'sleepy', 'slot_machine', 'smile', 'smiley', 'smirk', 'smoking', 'snake', 'snowman', 'sob', 'soccer',
'space_invader', 'spades', 'spaghetti', 'sparkler', 'sparkles', 'speaker', 'speedboat', 'squirrel', 'star', 'star2', 'stars', 'station',
'statue_of_liberty', 'stew', 'strawberry', 'sunflower', 'sunny', 'sunrise', 'sunrise_over_mountains', 'surfer', 'sushi', 'suspect', 'sweat',
'sweat_drops', 'swimmer', 'syringe', 'tada', 'tangerine', 'taurus', 'taxi', 'tea', 'telephone', 'tennis', 'tent', 'thumbsdown', 'thumbsup', 'ticket',
'tiger', 'tm', 'toilet', 'tokyo_tower', 'tomato', 'tongue', 'top', 'tophat', 'traffic_light', 'train', 'trident', 'trophy', 'tropical_fish', 'truck',
'trumpet', 'tshirt', 'tulip', 'tv', 'u5272', 'u55b6', 'u6307', 'u6708', 'u6709', 'u6e80', 'u7121', 'u7533', 'u7a7a', 'umbrella', 'unamused',
'underage', 'unlock', 'up', 'us', 'v', 'vhs', 'vibration_mode', 'virgo', 'vs', 'walking', 'warning', 'watermelon', 'wave', 'wc', 'wedding', 'whale',
'wheelchair', 'white_square', 'wind_chime', 'wink', 'wink2', 'wolf', 'woman', 'womans_hat', 'womens', 'x', 'yellow_heart', 'zap', 'zzz'
]

View File

@ -0,0 +1,68 @@
// https://gist.github.com/2018185
// For reference: https://github.com/wagenet/ember.js/blob/ac66dcb8a1cbe91d736074441f853e0da474ee6e/packages/ember-handlebars/lib/views/bound_property_view.js
var BoundHelperView = Ember.View.extend(Ember._Metamorph, {
context: null,
options: null,
property: null,
// paths of the property that are also observed
propertyPaths: [],
value: Ember.K,
valueForRender: function() {
var value = this.value(Ember.getPath(this.context, this.property), this.options);
if (this.options.escaped) { value = Handlebars.Utils.escapeExpression(value); }
return value;
},
render: function(buffer) {
buffer.push(this.valueForRender());
},
valueDidChange: function() {
if (this.morph.isRemoved()) { return; }
this.morph.html(this.valueForRender());
},
didInsertElement: function() {
this.valueDidChange();
},
init: function() {
this._super();
Ember.addObserver(this.context, this.property, this, 'valueDidChange');
this.get('propertyPaths').forEach(function(propName) {
Ember.addObserver(this.context, this.property + '.' + propName, this, 'valueDidChange');
}, this);
},
destroy: function() {
Ember.removeObserver(this.context, this.property, this, 'valueDidChange');
this.get('propertyPaths').forEach(function(propName) {
this.context.removeObserver(this.property + '.' + propName, this, 'valueDidChange');
}, this);
this._super();
}
});
Ember.registerBoundHelper = function(name, func) {
var propertyPaths = Array.prototype.slice.call(arguments, 2);
Ember.Handlebars.registerHelper(name, function(property, options) {
var data = options.data,
view = data.view,
ctx = this;
var bindView = view.createChildView(BoundHelperView, {
property: property,
propertyPaths: propertyPaths,
context: ctx,
options: options.hash,
value: func
});
view.appendChild(bindView);
});
};

View File

@ -0,0 +1,143 @@
$.fn.extend
outerHtml: ->
$(this).wrap('<div></div>').parent().html()
outerElement: ->
$($(this).outerHtml()).empty()
flash: ->
Utils.flash this
unflash: ->
Utils.unflash this
filterLog: ->
@deansi()
@foldLog()
deansi: ->
@html Utils.deansi(@html())
foldLog: ->
@html Utils.foldLog(@html())
unfoldLog: ->
@html Utils.unfoldLog(@html())
updateTimes: ->
Utils.updateTimes this
activateTab: (tab) ->
Utils.activateTab this, tab
timeInWords: ->
$(this).each ->
$(this).text Utils.timeInWords(parseInt($(this).attr('title')))
updateGithubStats: (repository) ->
Utils.updateGithubStats repository, $(this)
$.extend
keys: (obj) ->
keys = []
$.each obj, (key) ->
keys.push key
keys
values: (obj) ->
values = []
$.each obj, (key, value) ->
values.push value
values
camelize: (string, uppercase) ->
string = $.capitalize(string) if uppercase or typeof uppercase is 'undefined'
string.replace /_(.)?/g, (match, chr) ->
(if chr then chr.toUpperCase() else '')
capitalize: (string) ->
string[0].toUpperCase() + string.substring(1)
compact: (array) ->
$.grep array, (value) ->
!!value
all: (array, callback) ->
args = Array::slice.apply(arguments)
callback = args.pop()
array = args.pop() or this
i = 0
while i < array.length
return false if callback(array[i])
i++
true
detect: (array, callback) ->
args = Array::slice.apply(arguments)
callback = args.pop()
array = args.pop() or this
i = 0
while i < array.length
return array[i] if callback(array[i])
i++
select: (array, callback) ->
args = Array::slice.apply(arguments)
callback = args.pop()
array = args.pop() or this
result = []
i = 0
while i < array.length
result.push array[i] if callback(array[i])
i++
result
slice: (object, key) ->
keys = Array::slice.apply(arguments)
object = (if (typeof keys[0] is 'object') then keys.shift() else this)
result = {}
for key of object
result[key] = object[key] if keys.indexOf(key) > -1
result
only: (object) ->
keys = Array::slice.apply(arguments)
object = (if (typeof keys[0] is 'object') then keys.shift() else this)
result = {}
for key of object
result[key] = object[key] unless keys.indexOf(key) is -1
result
except: (object) ->
keys = Array::slice.apply(arguments)
object = (if (typeof keys[0] is 'object') then keys.shift() else this)
result = {}
for key of object
result[key] = object[key] if keys.indexOf(key) is -1
result
map: (elems, callback, arg) ->
value = undefined
key = undefined
ret = []
i = 0
length = elems.length
isArray = elems instanceof jQuery || length != undefined && typeof length == 'number' && (length > 0 && elems[0] && elems[length - 1]) || length == 0 || jQuery.isArray(elems)
if isArray
while i < length
value = callback(elems[i], i, arg)
ret[ret.length] = value if value?
i++
else
for key of elems
value = callback(elems[key], key, arg)
ret[ret.length] = value if value?
ret.concat.apply [], ret
truncate: (string, length) ->
if string.length > length then string.trim().substring(0, length) + '...' else string

View File

@ -0,0 +1,63 @@
@Travis.Log =
FOLDS:
schema: /(<p.*?\/a>\$ (?:bundle exec )?rake( db:create)? db:schema:load[\s\S]*?<p.*?\/a>-- assume_migrated_upto_version[\s\S]*?<\/p>\n<p.*?\/a>.*<\/p>)/g
migrate: /(<p.*?\/a>\$ (?:bundle exec )?rake( db:create)? db:migrate[\s\S]*== +\w+: migrated \(.*\) =+)/g
bundle: /(<p.*?\/a>\$ bundle install.*<\/p>\n(<p.*?\/a>(Updating|Using|Installing|Fetching|remote:|Receiving|Resolving).*?<\/p>\n|<p.*?\/a><\/p>\n)*)/g
exec: /(<p.*?\/a>[\/\w]*.rvm\/rubies\/[\S]*?\/(ruby|rbx|jruby) .*?<\/p>)/g
filter: (log) ->
log = @escapeHtml(log)
log = @deansi(log)
log = log.replace(/\r/g, '')
log = @numberLines(log)
log = @fold(log)
log = log.replace(/\n/g, '')
log
stripPaths: (log) ->
log.replace /\/home\/vagrant\/builds(\/[^\/\n]+){2}\//g, ''
escapeHtml: (log) ->
Handlebars.Utils.escapeExpression log
escapeRuby: (log) ->
log.replace /#<(\w+.*?)>/, '#&lt;$1&gt;'
numberLines: (log) ->
result = ''
$.each log.trim().split('\n'), (ix, line) ->
number = ix + 1
path = Travis.Log.location().substr(1).replace(/\/L\d+/, '') + '/L' + number
result += '<p><a href=\'#%@\' id=\'%@\' name=\'L%@\'>%@</a>%@</p>\n'.fmt(path, path, number, number, line)
result.trim()
deansi: (log) ->
log = log.replace(/\r\r/g, '\r').replace(/\033\[K\r/g, '\r').replace(/^.*\r(?!$)/g, '').replace(/\[2K/g, '').replace(/\033\(B/g, '')
ansi = ansiparse(log)
text = ''
ansi.forEach (part) ->
classes = []
part.foreground and classes.push(part.foreground)
part.background and classes.push('bg-' + part.background)
part.bold and classes.push('bold')
part.italic and classes.push('italic')
text += (if classes.length then ('<span class=\'' + classes.join(' ') + '\'>' + part.text + '</span>') else part.text)
text.replace /\033/g, ''
fold: (log) ->
log = @unfold(log)
$.each Travis.Log.FOLDS, (name, pattern) ->
log = log.replace(pattern, ->
'<div class=\'fold ' + name + '\'>' + arguments[1].trim() + '</div>'
)
log
unfold: (log) ->
log.replace /<div class='fold[^']*'>([\s\S]*?)<\/div>/g, '$1\n'
location: ->
window.location.hash

View File

@ -0,0 +1,18 @@
@Travis.Ticker = Ember.Object.extend
init: ->
@_super()
@schedule()
tick: ->
context = @get('context')
@get('targets').forEach (target) =>
target = context.get(target)
return unless target
if target.forEach
target.forEach (target) -> target.tick()
else
target.tick()
@schedule()
schedule: ->
Ember.run.later((=> @tick()), Travis.app.TICK_INTERVAL)

View File

@ -1,4 +1,8 @@
//= require_self
//= require ./vendor/ansiparse.js
//= require ./vendor/i18n.js
//= require ./vendor/jquery.timeago.js
//= require_tree ./config
//= require_tree ./lib
//= require app/app.js

View File

@ -0,0 +1,161 @@
ansiparse = function (str) {
//
// I'm terrible at writing parsers.
//
var matchingControl = null,
matchingData = null,
matchingText = '',
ansiState = [],
result = [],
state = {};
//
// General workflow for this thing is:
// \033\[33mText
// | | |
// | | matchingText
// | matchingData
// matchingControl
//
// In further steps we hope it's all going to be fine. It usually is.
//
for (var i = 0; i < str.length; i++) {
if (matchingControl != null) {
if (matchingControl == '\033' && str[i] == '\[') {
//
// We've matched full control code. Lets start matching formating data.
//
//
// "emit" matched text with correct state
//
if (matchingText) {
state.text = matchingText;
result.push(state);
state = {};
matchingText = "";
}
matchingControl = null;
matchingData = '';
}
else {
//
// We failed to match anything - most likely a bad control code. We
// go back to matching regular strings.
//
matchingText += matchingControl + str[i];
matchingControl = null;
}
continue;
}
else if (matchingData != null) {
if (str[i] == ';') {
//
// `;` separates many formatting codes, for example: `\033[33;43m`
// means that both `33` and `43` should be applied.
//
// TODO: this can be simplified by modifying state here.
//
ansiState.push(matchingData);
matchingData = '';
}
else if (str[i] == 'm') {
//
// `m` finished whole formatting code. We can proceed to matching
// formatted text.
//
ansiState.push(matchingData);
matchingData = null;
matchingText = '';
//
// Convert matched formatting data into user-friendly state object.
//
// TODO: DRY.
//
ansiState.forEach(function (ansiCode) {
if (ansiparse.foregroundColors[ansiCode]) {
state.foreground = ansiparse.foregroundColors[ansiCode];
}
else if (ansiparse.backgroundColors[ansiCode]) {
state.background = ansiparse.backgroundColors[ansiCode];
}
else if (ansiCode == 39) {
delete state.foreground;
}
else if (ansiCode == 49) {
delete state.background;
}
else if (ansiparse.styles[ansiCode]) {
state[ansiparse.styles[ansiCode]] = true;
}
else if (ansiCode == 22) {
state.bold = false;
}
else if (ansiCode == 23) {
state.italic = false;
}
else if (ansiCode == 24) {
state.underline = false;
}
});
ansiState = [];
}
else {
matchingData += str[i];
}
continue;
}
if (str[i] == '\033') {
matchingControl = str[i];
}
else {
matchingText += str[i];
}
}
if (matchingText) {
state.text = matchingText + (matchingControl ? matchingControl : '');
result.push(state);
}
return result;
}
ansiparse.foregroundColors = {
'30': 'black',
'31': 'red',
'32': 'green',
'33': 'yellow',
'34': 'blue',
'35': 'magenta',
'36': 'cyan',
'37': 'white',
'90': 'grey'
};
ansiparse.backgroundColors = {
'40': 'black',
'41': 'red',
'42': 'green',
'43': 'yellow',
'44': 'blue',
'45': 'magenta',
'46': 'cyan',
'47': 'white'
};
ansiparse.styles = {
'1': 'bold',
'3': 'italic',
'4': 'underline'
};
if (typeof module == "object" && typeof window == "undefined") {
module.exports = ansiparse;
}

View File

@ -955,7 +955,7 @@ Ember.isArray = function(obj) {
Ember.makeArray(); => []
Ember.makeArray(null); => []
Ember.makeArray(undefined); => []
Ember.makeArray('lindsay'); => ['lindsay']
Ember.makeArray('lindsay'); => ['lindsay']
Ember.makeArray([1,2,42]); => [1,2,42]
var controller = Ember.ArrayProxy.create({ content: [] });
@ -3586,7 +3586,7 @@ Ember.RunLoop = RunLoop;
call.
Ember.run(function(){
// code to be execute within a RunLoop
// code to be execute within a RunLoop
});
@name run
@ -3624,7 +3624,7 @@ var run = Ember.run;
an lower-level way to use a RunLoop instead of using Ember.run().
Ember.run.begin();
// code to be execute within a RunLoop
// code to be execute within a RunLoop
Ember.run.end();
@ -3640,7 +3640,7 @@ Ember.run.begin = function() {
instead of using Ember.run().
Ember.run.begin();
// code to be execute within a RunLoop
// code to be execute within a RunLoop
Ember.run.end();
@returns {void}
@ -5388,7 +5388,7 @@ Ember.inspect = function(obj) {
/**
Compares two objects, returning true if they are logically equal. This is
a deeper comparison than a simple triple equal. For sets it will compare the
internal objects. For any other object that implements `isEqual()` it will
internal objects. For any other object that implements `isEqual()` it will
respect that method.
Ember.isEqual('hello', 'hello'); => true
@ -5570,7 +5570,7 @@ Ember.String = {
> beta
> gamma
@param {String} str
@param {String} str
The string to split
@returns {String} split string
@ -5579,7 +5579,7 @@ Ember.String = {
/**
Converts a camelized string into all lower case separated by underscores.
'innerHTML'.decamelize() => 'inner_html'
'action_name'.decamelize() => 'action_name'
'css-class-name'.decamelize() => 'css-class-name'
@ -5596,7 +5596,7 @@ Ember.String = {
/**
Replaces underscores or spaces with dashes.
'innerHTML'.dasherize() => 'inner-html'
'action_name'.dasherize() => 'action-name'
'css-class-name'.dasherize() => 'css-class-name'
@ -5763,7 +5763,7 @@ if (Ember.EXTEND_PROTOTYPES) {
/**
The `property` extension of Javascript's Function prototype is available
when Ember.EXTEND_PROTOTYPES is true, which is the default.
when Ember.EXTEND_PROTOTYPES is true, which is the default.
Computed properties allow you to treat a function like a property:
@ -5818,7 +5818,7 @@ if (Ember.EXTEND_PROTOTYPES) {
/**
The `observes` extension of Javascript's Function prototype is available
when Ember.EXTEND_PROTOTYPES is true, which is the default.
when Ember.EXTEND_PROTOTYPES is true, which is the default.
You can observe property changes simply by adding the `observes`
call to the end of your method declarations in classes that you write.
@ -5829,7 +5829,7 @@ if (Ember.EXTEND_PROTOTYPES) {
// Executes whenever the "value" property changes
}.observes('value')
});
@see Ember.Observable
*/
Function.prototype.observes = function() {
@ -5839,7 +5839,7 @@ if (Ember.EXTEND_PROTOTYPES) {
/**
The `observesBefore` extension of Javascript's Function prototype is
available when Ember.EXTEND_PROTOTYPES is true, which is the default.
available when Ember.EXTEND_PROTOTYPES is true, which is the default.
You can get notified when a property changes is about to happen by
by adding the `observesBefore` call to the end of your method
@ -5850,7 +5850,7 @@ if (Ember.EXTEND_PROTOTYPES) {
// Executes whenever the "value" property is about to change
}.observesBefore('value')
});
@see Ember.Observable
*/
Function.prototype.observesBefore = function() {
@ -6447,9 +6447,9 @@ Ember.Enumerable = Ember.Mixin.create( /** @lends Ember.Enumerable */ {
/**
Returns a copy of the array with all null elements removed.
var arr = ["a", null, "c", null];
arr.compact(); => ["a", "c"]
arr.compact(); => ["a", "c"]
@returns {Array} the array without null elements.
*/
@ -7449,7 +7449,7 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable,
colors.clear(); => []
colors.length(); => 0
@returns {Ember.Array} An empty Array.
@returns {Ember.Array} An empty Array.
*/
clear: function () {
var len = get(this, 'length');
@ -7643,15 +7643,15 @@ var get = Ember.get, set = Ember.set;
@class
## Overview
This mixin provides properties and property observing functionality, core
features of the Ember object model.
Properties and observers allow one object to observe changes to a
property on another object. This is one of the fundamental ways that
models, controllers and views communicate with each other in an Ember
application.
Any object that has this mixin applied can be used in observer
operations. That includes Ember.Object and most objects you will
interact with as you write your Ember application.
@ -7659,16 +7659,16 @@ var get = Ember.get, set = Ember.set;
Note that you will not generally apply this mixin to classes yourself,
but you will use the features provided by this module frequently, so it
is important to understand how to use it.
## Using get() and set()
Because of Ember's support for bindings and observers, you will always
access properties using the get method, and set properties using the
set method. This allows the observing objects to be notified and
computed properties to be handled properly.
More documentation about `get` and `set` are below.
## Observing Property Changes
You typically observe property changes simply by adding the `observes`
@ -7680,7 +7680,7 @@ var get = Ember.get, set = Ember.set;
// Executes whenever the "value" property changes
}.observes('value')
});
Although this is the most common way to add an observer, this capability
is actually built into the Ember.Object class on top of two methods
defined in this mixin: `addObserver` and `removeObserver`. You can use
@ -7693,12 +7693,12 @@ var get = Ember.get, set = Ember.set;
This will call the `targetAction` method on the `targetObject` to be called
whenever the value of the `propertyKey` changes.
Note that if `propertyKey` is a computed property, the observer will be
called when any of the property dependencies are changed, even if the
Note that if `propertyKey` is a computed property, the observer will be
called when any of the property dependencies are changed, even if the
resulting value of the computed property is unchanged. This is necessary
because computed properties are not computed until `get` is called.
@extends Ember.Mixin
*/
Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ {
@ -7712,7 +7712,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ {
This method is usually similar to using object[keyName] or object.keyName,
however it supports both computed properties and the unknownProperty
handler.
Because `get` unifies the syntax for accessing all these kinds
of properties, it can make many refactorings easier, such as replacing a
simple property with a computed property, or vice versa.
@ -7908,11 +7908,11 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ {
Ember.propertyDidChange(this, keyName);
return this;
},
/**
Convenience method to call `propertyWillChange` and `propertyDidChange` in
succession.
@param {String} keyName The property key to be notified about.
@returns {Ember.Observable}
*/
@ -8004,7 +8004,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ {
This method will be called when a client attempts to get the value of a
property that has not been defined in one of the typical ways. Override
this method to create "virtual" properties.
@param {String} key The name of the unknown property that was requested.
@returns {Object} The property value or undefined. Default is undefined.
*/
@ -8016,7 +8016,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ {
This method will be called when a client attempts to set the value of a
property that has not been defined in one of the typical ways. Override
this method to create "virtual" properties.
@param {String} key The name of the unknown property to be set.
@param {Object} value The value the unknown property is to be set to.
*/
@ -8027,7 +8027,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ {
/**
This is like `get`, but allows you to pass in a dot-separated property
path.
person.getPath('address.zip'); // return the zip
person.getPath('children.firstObject.age'); // return the first kid's age
@ -8043,7 +8043,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ {
/**
This is like `set`, but allows you to specify the property you want to
set as a dot-separated property path.
person.setPath('address.zip', 10011); // set the zip to 10011
person.setPath('children.firstObject.age', 6); // set the first kid's age to 6
@ -8061,9 +8061,9 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ {
/**
Retrieves the value of a property, or a default value in the case that the property
returns undefined.
person.getWithDefault('lastName', 'Doe');
@param {String} keyName The name of the property to retrieve
@param {Object} defaultValue The value to return if the property value is undefined
@returns {Object} The property value or the defaultValue.
@ -8074,10 +8074,10 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ {
/**
Set the value of a property to the current value plus some amount.
person.incrementProperty('age');
team.incrementProperty('score', 2);
@param {String} keyName The name of the property to increment
@param {Object} increment The amount to increment by. Defaults to 1
@returns {Object} The new property value
@ -8087,13 +8087,13 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ {
set(this, keyName, (get(this, keyName) || 0)+increment);
return get(this, keyName);
},
/**
Set the value of a property to the current value minus some amount.
player.decrementProperty('lives');
orc.decrementProperty('health', 5);
@param {String} keyName The name of the property to decrement
@param {Object} increment The amount to decrement by. Defaults to 1
@returns {Object} The new property value
@ -8107,9 +8107,9 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ {
/**
Set the value of a boolean property to the opposite of it's
current value.
starship.toggleProperty('warpDriveEnaged');
@param {String} keyName The name of the property to toggle
@returns {Object} The new property value
*/
@ -11496,7 +11496,7 @@ var invokeForState = {
`Ember.View` is the class in Ember responsible for encapsulating templates of HTML
content, combining templates with data to render as sections of a page's DOM, and
registering and responding to user-initiated events.
## HTML Tag
The default HTML tag name used for a view's DOM representation is `div`. This can be
customized by setting the `tagName` property. The following view class:
@ -11522,7 +11522,7 @@ var invokeForState = {
<div id="ember1" class="ember-view my-class my-other-class"></div>
`class` attribute values can also be set by providing a `classNameBindings` property
set to an array of properties names for the view. The return value of these properties
set to an array of properties names for the view. The return value of these properties
will be added as part of the value for the view's `class` attribute. These properties
can be computed properties:
@ -11551,7 +11551,7 @@ var invokeForState = {
<div id="ember1" class="ember-view hovered"></div>
When using boolean class name bindings you can supply a string value other than the
When using boolean class name bindings you can supply a string value other than the
property name for use as the `class` HTML attribute by appending the preferred value after
a ":" character when defining the binding:
@ -11592,11 +11592,11 @@ var invokeForState = {
<div id="ember1" class="ember-view empty"></div>
Updates to the the value of a class name binding will result in automatic update
Updates to the the value of a class name binding will result in automatic update
of the HTML `class` attribute in the view's rendered HTML representation.
If the value becomes `false` or `undefined` the class name will be removed.
Both `classNames` and `classNameBindings` are concatenated properties.
Both `classNames` and `classNameBindings` are concatenated properties.
See `Ember.Object` documentation for more information about concatenated properties.
## HTML Attributes
@ -11642,7 +11642,7 @@ var invokeForState = {
}.property()
})
Updates to the the property of an attribute binding will result in automatic update
Updates to the the property of an attribute binding will result in automatic update
of the HTML attribute in the view's rendered HTML representation.
`attributeBindings` is a concatenated property. See `Ember.Object` documentation
@ -11733,7 +11733,7 @@ var invokeForState = {
primary templates, layouts can be any function that accepts an optional context
parameter and returns a string of HTML that will be inserted inside view's tag. Views whose HTML
element is self closing (e.g. `<input />`) cannot have a layout and this property will be ignored.
Most typically in Ember a layout will be a compiled Ember.Handlebars template.
A view's layout can be set directly with the `layout` property or reference an
@ -11758,7 +11758,7 @@ var invokeForState = {
See `Handlebars.helpers.yield` for more information.
## Responding to Browser Events
Views can respond to user-initiated events in one of three ways: method implementation,
Views can respond to user-initiated events in one of three ways: method implementation,
through an event manager, and through `{{action}}` helper use in their template or layout.
### Method Implementation
@ -11775,8 +11775,8 @@ var invokeForState = {
### Event Managers
Views can define an object as their `eventManager` property. This object can then
implement methods that match the desired event names. Matching events that occur
on the view's rendered HTML or the rendered HTML of any of its DOM descendants
will trigger this method. A `jQuery.Event` object will be passed as the first
on the view's rendered HTML or the rendered HTML of any of its DOM descendants
will trigger this method. A `jQuery.Event` object will be passed as the first
argument to the method and an `Ember.View` object as the second. The `Ember.View`
will be the view whose rendered HTML was interacted with. This may be the view with
the `eventManager` property or one of its descendent views.
@ -11810,7 +11810,7 @@ var invokeForState = {
Similarly a view's event manager will take precedence for events of any views
rendered as a descendent. A method name that matches an event name will not be called
if the view instance was rendered inside the HTML representation of a view that has
if the view instance was rendered inside the HTML representation of a view that has
an `eventManager` property defined that handles events of the name. Events not handled
by the event manager will still trigger method calls on the descendent.
@ -11832,7 +11832,7 @@ var invokeForState = {
// eventManager doesn't handle click events
},
mouseEnter: function(event){
// will never be called if rendered inside
// will never be called if rendered inside
// an OuterView.
}
})
@ -11853,7 +11853,7 @@ var invokeForState = {
Form events: 'submit', 'change', 'focusIn', 'focusOut', 'input'
HTML5 drag and drop events: 'dragStart', 'drag', 'dragEnter', 'dragLeave', 'drop', 'dragEnd'
## Handlebars `{{view}}` Helper
Other `Ember.View` instances can be included as part of a view's template by using the `{{view}}`
Handlebars helper. See `Handlebars.helpers.view` for additional information.
@ -14230,7 +14230,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt;
@class
`Ember.CollectionView` is an `Ember.View` descendent responsible for managing a
collection (an array or array-like object) by maintaing a child view object and
collection (an array or array-like object) by maintaing a child view object and
associated DOM representation for each item in the array and ensuring that child
views and their associated rendered HTML are updated when items in the array
are added, removed, or replaced.
@ -14274,7 +14274,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt;
## Automatic matching of parent/child tagNames
Setting the `tagName` property of a `CollectionView` to any of
Setting the `tagName` property of a `CollectionView` to any of
"ul", "ol", "table", "thead", "tbody", "tfoot", "tr", or "select" will result
in the item views receiving an appropriately matched `tagName` property.
@ -15118,15 +15118,15 @@ var arrayForEach = Ember.ArrayPolyfills.forEach;
robotManager.getPath('currentState.name') // 'rampaging'
Transition actions can also be created using the `transitionTo` method of the Ember.State class. The
following example StateManagers are equivalent:
following example StateManagers are equivalent:
aManager = Ember.StateManager.create({
stateOne: Ember.State.create({
changeToStateTwo: Ember.State.transitionTo('stateTwo')
}),
stateTwo: Ember.State.create({})
})
bManager = Ember.StateManager.create({
stateOne: Ember.State.create({
changeToStateTwo: function(manager, context){
@ -15207,7 +15207,7 @@ Ember.StateManager = Ember.State.extend(
@default true
*/
errorOnUnhandledEvent: true,
send: function(event, context) {
Ember.assert('Cannot send event "' + event + '" while currentState is ' + get(this, 'currentState'), get(this, 'currentState'));
if (arguments.length === 1) { context = {}; }
@ -17894,7 +17894,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({
Will result in HTML structure:
<body>
<!-- Note: the handlebars template script
<!-- Note: the handlebars template script
also results in a rendered Ember.View
which is the outer <div> here -->
@ -17916,7 +17916,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({
})
aView.appendTo('body')
Will result in HTML structure:
<div id="ember1" class="ember-view">
@ -17990,7 +17990,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({
Will result in the following HTML:
<div id="ember1" class="ember-view">
<div id="ember2" class="ember-view a-custom-view-class-as-property">
<div id="ember2" class="ember-view a-custom-view-class-as-property">
hi
</div>
</div>
@ -18150,7 +18150,7 @@ var get = Ember.get, getPath = Ember.Handlebars.getPath, fmt = Ember.String.fmt;
<p class="ember-view greeting">Howdy Mary</p>
<p class="ember-view greeting">Howdy Sara</p>
</div>
@name Handlebars.helpers.collection
@param {String} path
@param {Hash} options
@ -18479,7 +18479,7 @@ ActionHelper.registerAction = function(actionName, eventName, target, view, cont
</div>
</div>
Clicking "click me" will trigger the `anActionName` method of the `aView` object with a
Clicking "click me" will trigger the `anActionName` method of the `aView` object with a
`jQuery.Event` object as its argument. The `jQuery.Event` object will be extended to include
a `view` property that is set to the original view interacted with (in this case the `aView` object).
@ -18504,9 +18504,9 @@ ActionHelper.registerAction = function(actionName, eventName, target, view, cont
</div>
</script>
Clicking "click me" in the rendered HTML of the above template will trigger the
`anActionName` method of the object at `MyApplication.someObject`. The first argument
to this method will be a `jQuery.Event` extended to include a `view` property that is
Clicking "click me" in the rendered HTML of the above template will trigger the
`anActionName` method of the object at `MyApplication.someObject`. The first argument
to this method will be a `jQuery.Event` extended to include a `view` property that is
set to the original view interacted with.
A path relative to the template's `Ember.View` instance can also be used as a target:
@ -18517,7 +18517,7 @@ ActionHelper.registerAction = function(actionName, eventName, target, view, cont
</div>
</script>
Clicking "click me" in the rendered HTML of the above template will trigger the
Clicking "click me" in the rendered HTML of the above template will trigger the
`anActionName` method of the view's parent view.
The `{{action}}` helper is `Ember.StateManager` aware. If the target of
@ -18561,7 +18561,7 @@ ActionHelper.registerAction = function(actionName, eventName, target, view, cont
See `Ember.EventDispatcher` for a list of acceptable DOM event names.
Because `{{action}}` depends on Ember's event dispatch system it will only function if
an `Ember.EventDispatcher` instance is available. An `Ember.EventDispatcher` instance
an `Ember.EventDispatcher` instance is available. An `Ember.EventDispatcher` instance
will be created when a new `Ember.Application` is created. Having an instance of
`Ember.Application` will satisfy this requirement.
@ -18598,6 +18598,9 @@ EmberHandlebars.registerHelper('action', function(actionName, options) {
target = target || view;
// console.log(hash.context)
// debugger
context = hash.context ? getPath(this, hash.context, options) : options.contexts[0];
var output = [], url;
@ -18759,7 +18762,7 @@ var set = Ember.set, get = Ember.get;
/**
@class
Creates an HTML input of type 'checkbox' with HTML related properties
Creates an HTML input of type 'checkbox' with HTML related properties
applied directly to the input.
{{view Ember.Checkbox classNames="applicaton-specific-checkbox"}}
@ -18778,7 +18781,7 @@ var set = Ember.set, get = Ember.get;
through the Ember object or by interacting with its rendered element representation
via the mouse, keyboard, or touch. Updating the value of the checkbox via jQuery will
result in the checked value of the object and its element losing synchronization.
## Layout and LayoutName properties
Because HTML `input` elements are self closing `layout` and `layoutName` properties will
not be applied. See `Ember.View`'s layout section for more information.
@ -18890,7 +18893,7 @@ var get = Ember.get, set = Ember.set;
## Layout and LayoutName properties
Because HTML `input` elements are self closing `layout` and `layoutName` properties will
not be applied. See `Ember.View`'s layout section for more information.
@extends Ember.TextSupport
*/
Ember.TextField = Ember.View.extend(Ember.TextSupport,
@ -19067,7 +19070,7 @@ var get = Ember.get, set = Ember.set;
## Layout and LayoutName properties
Because HTML `textarea` elements do not contain inner HTML the `layout` and `layoutName`
Because HTML `textarea` elements do not contain inner HTML the `layout` and `layoutName`
properties will not be applied. See `Ember.View`'s layout section for more information.
@extends Ember.TextSupport

532
app/assets/javascripts/vendor/i18n.js vendored Normal file
View File

@ -0,0 +1,532 @@
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(searchElement /*, fromIndex */) {
"use strict";
if (this === void 0 || this === null) {
throw new TypeError();
}
var t = Object(this);
var len = t.length >>> 0;
if (len === 0) {
return -1;
}
var n = 0;
if (arguments.length > 0) {
n = Number(arguments[1]);
if (n !== n) { // shortcut for verifying if it's NaN
n = 0;
} else if (n !== 0 && n !== (Infinity) && n !== -(Infinity)) {
n = (n > 0 || -1) * Math.floor(Math.abs(n));
}
}
if (n >= len) {
return -1;
}
var k = n >= 0
? n
: Math.max(len - Math.abs(n), 0);
for (; k < len; k++) {
if (k in t && t[k] === searchElement) {
return k;
}
}
return -1;
};
}
// Instantiate the object
var I18n = I18n || {};
// Set default locale to english
I18n.defaultLocale = "en";
// Set default handling of translation fallbacks to false
I18n.fallbacks = false;
// Set default separator
I18n.defaultSeparator = ".";
// Set current locale to null
I18n.locale = null;
// Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`.
I18n.PLACEHOLDER = /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm;
I18n.fallbackRules = {
};
I18n.pluralizationRules = {
en: function (n) {
return n == 0 ? ["zero", "none", "other"] : n == 1 ? "one" : "other";
}
};
I18n.getFallbacks = function(locale) {
if (locale === I18n.defaultLocale) {
return [];
} else if (!I18n.fallbackRules[locale]) {
var rules = []
, components = locale.split("-");
for (var l = 1; l < components.length; l++) {
rules.push(components.slice(0, l).join("-"));
}
rules.push(I18n.defaultLocale);
I18n.fallbackRules[locale] = rules;
}
return I18n.fallbackRules[locale];
}
I18n.isValidNode = function(obj, node, undefined) {
return obj[node] !== null && obj[node] !== undefined;
};
I18n.lookup = function(scope, options) {
var options = options || {}
, lookupInitialScope = scope
, translations = this.prepareOptions(I18n.translations)
, locale = options.locale || I18n.currentLocale()
, messages = translations[locale] || {}
, options = this.prepareOptions(options)
, currentScope
;
if (typeof(scope) == "object") {
scope = scope.join(this.defaultSeparator);
}
if (options.scope) {
scope = options.scope.toString() + this.defaultSeparator + scope;
}
scope = scope.split(this.defaultSeparator);
while (messages && scope.length > 0) {
currentScope = scope.shift();
messages = messages[currentScope];
}
if (!messages) {
if (I18n.fallbacks) {
var fallbacks = this.getFallbacks(locale);
for (var fallback = 0; fallback < fallbacks.length; fallbacks++) {
messages = I18n.lookup(lookupInitialScope, this.prepareOptions({locale: fallbacks[fallback]}, options));
if (messages) {
break;
}
}
}
if (!messages && this.isValidNode(options, "defaultValue")) {
messages = options.defaultValue;
}
}
return messages;
};
// Merge serveral hash options, checking if value is set before
// overwriting any value. The precedence is from left to right.
//
// I18n.prepareOptions({name: "John Doe"}, {name: "Mary Doe", role: "user"});
// #=> {name: "John Doe", role: "user"}
//
I18n.prepareOptions = function() {
var options = {}
, opts
, count = arguments.length
;
for (var i = 0; i < count; i++) {
opts = arguments[i];
if (!opts) {
continue;
}
for (var key in opts) {
if (!this.isValidNode(options, key)) {
options[key] = opts[key];
}
}
}
return options;
};
I18n.interpolate = function(message, options) {
options = this.prepareOptions(options);
var matches = message.match(this.PLACEHOLDER)
, placeholder
, value
, name
;
if (!matches) {
return message;
}
for (var i = 0; placeholder = matches[i]; i++) {
name = placeholder.replace(this.PLACEHOLDER, "$1");
value = options[name];
if (!this.isValidNode(options, name)) {
value = "[missing " + placeholder + " value]";
}
regex = new RegExp(placeholder.replace(/\{/gm, "\\{").replace(/\}/gm, "\\}"));
message = message.replace(regex, value);
}
return message;
};
I18n.translate = function(scope, options) {
options = this.prepareOptions(options);
var translation = this.lookup(scope, options);
try {
if (typeof(translation) == "object") {
if (typeof(options.count) == "number") {
return this.pluralize(options.count, scope, options);
} else {
return translation;
}
} else {
return this.interpolate(translation, options);
}
} catch(err) {
return this.missingTranslation(scope);
}
};
I18n.localize = function(scope, value) {
switch (scope) {
case "currency":
return this.toCurrency(value);
case "number":
scope = this.lookup("number.format");
return this.toNumber(value, scope);
case "percentage":
return this.toPercentage(value);
default:
if (scope.match(/^(date|time)/)) {
return this.toTime(scope, value);
} else {
return value.toString();
}
}
};
I18n.parseDate = function(date) {
var matches, convertedDate;
// we have a date, so just return it.
if (typeof(date) == "object") {
return date;
};
// it matches the following formats:
// yyyy-mm-dd
// yyyy-mm-dd[ T]hh:mm::ss
// yyyy-mm-dd[ T]hh:mm::ss
// yyyy-mm-dd[ T]hh:mm::ssZ
// yyyy-mm-dd[ T]hh:mm::ss+0000
//
matches = date.toString().match(/(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2}))?(Z|\+0000)?/);
if (matches) {
for (var i = 1; i <= 6; i++) {
matches[i] = parseInt(matches[i], 10) || 0;
}
// month starts on 0
matches[2] -= 1;
if (matches[7]) {
convertedDate = new Date(Date.UTC(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6]));
} else {
convertedDate = new Date(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6]);
}
} else if (typeof(date) == "number") {
// UNIX timestamp
convertedDate = new Date();
convertedDate.setTime(date);
} else if (date.match(/\d+ \d+:\d+:\d+ [+-]\d+ \d+/)) {
// a valid javascript format with timezone info
convertedDate = new Date();
convertedDate.setTime(Date.parse(date))
} else {
// an arbitrary javascript string
convertedDate = new Date();
convertedDate.setTime(Date.parse(date));
}
return convertedDate;
};
I18n.toTime = function(scope, d) {
var date = this.parseDate(d)
, format = this.lookup(scope)
;
if (date.toString().match(/invalid/i)) {
return date.toString();
}
if (!format) {
return date.toString();
}
return this.strftime(date, format);
};
I18n.strftime = function(date, format) {
var options = this.lookup("date");
if (!options) {
return date.toString();
}
options.meridian = options.meridian || ["AM", "PM"];
var weekDay = date.getDay()
, day = date.getDate()
, year = date.getFullYear()
, month = date.getMonth() + 1
, hour = date.getHours()
, hour12 = hour
, meridian = hour > 11 ? 1 : 0
, secs = date.getSeconds()
, mins = date.getMinutes()
, offset = date.getTimezoneOffset()
, absOffsetHours = Math.floor(Math.abs(offset / 60))
, absOffsetMinutes = Math.abs(offset) - (absOffsetHours * 60)
, timezoneoffset = (offset > 0 ? "-" : "+") + (absOffsetHours.toString().length < 2 ? "0" + absOffsetHours : absOffsetHours) + (absOffsetMinutes.toString().length < 2 ? "0" + absOffsetMinutes : absOffsetMinutes)
;
if (hour12 > 12) {
hour12 = hour12 - 12;
} else if (hour12 === 0) {
hour12 = 12;
}
var padding = function(n) {
var s = "0" + n.toString();
return s.substr(s.length - 2);
};
var f = format;
f = f.replace("%a", options.abbr_day_names[weekDay]);
f = f.replace("%A", options.day_names[weekDay]);
f = f.replace("%b", options.abbr_month_names[month]);
f = f.replace("%B", options.month_names[month]);
f = f.replace("%d", padding(day));
f = f.replace("%e", day);
f = f.replace("%-d", day);
f = f.replace("%H", padding(hour));
f = f.replace("%-H", hour);
f = f.replace("%I", padding(hour12));
f = f.replace("%-I", hour12);
f = f.replace("%m", padding(month));
f = f.replace("%-m", month);
f = f.replace("%M", padding(mins));
f = f.replace("%-M", mins);
f = f.replace("%p", options.meridian[meridian]);
f = f.replace("%S", padding(secs));
f = f.replace("%-S", secs);
f = f.replace("%w", weekDay);
f = f.replace("%y", padding(year));
f = f.replace("%-y", padding(year).replace(/^0+/, ""));
f = f.replace("%Y", year);
f = f.replace("%z", timezoneoffset);
return f;
};
I18n.toNumber = function(number, options) {
options = this.prepareOptions(
options,
this.lookup("number.format"),
{precision: 3, separator: ".", delimiter: ",", strip_insignificant_zeros: false}
);
var negative = number < 0
, string = Math.abs(number).toFixed(options.precision).toString()
, parts = string.split(".")
, precision
, buffer = []
, formattedNumber
;
number = parts[0];
precision = parts[1];
while (number.length > 0) {
buffer.unshift(number.substr(Math.max(0, number.length - 3), 3));
number = number.substr(0, number.length -3);
}
formattedNumber = buffer.join(options.delimiter);
if (options.precision > 0) {
formattedNumber += options.separator + parts[1];
}
if (negative) {
formattedNumber = "-" + formattedNumber;
}
if (options.strip_insignificant_zeros) {
var regex = {
separator: new RegExp(options.separator.replace(/\./, "\\.") + "$")
, zeros: /0+$/
};
formattedNumber = formattedNumber
.replace(regex.zeros, "")
.replace(regex.separator, "")
;
}
return formattedNumber;
};
I18n.toCurrency = function(number, options) {
options = this.prepareOptions(
options,
this.lookup("number.currency.format"),
this.lookup("number.format"),
{unit: "$", precision: 2, format: "%u%n", delimiter: ",", separator: "."}
);
number = this.toNumber(number, options);
number = options.format
.replace("%u", options.unit)
.replace("%n", number)
;
return number;
};
I18n.toHumanSize = function(number, options) {
var kb = 1024
, size = number
, iterations = 0
, unit
, precision
;
while (size >= kb && iterations < 4) {
size = size / kb;
iterations += 1;
}
if (iterations === 0) {
unit = this.t("number.human.storage_units.units.byte", {count: size});
precision = 0;
} else {
unit = this.t("number.human.storage_units.units." + [null, "kb", "mb", "gb", "tb"][iterations]);
precision = (size - Math.floor(size) === 0) ? 0 : 1;
}
options = this.prepareOptions(
options,
{precision: precision, format: "%n%u", delimiter: ""}
);
number = this.toNumber(size, options);
number = options.format
.replace("%u", unit)
.replace("%n", number)
;
return number;
};
I18n.toPercentage = function(number, options) {
options = this.prepareOptions(
options,
this.lookup("number.percentage.format"),
this.lookup("number.format"),
{precision: 3, separator: ".", delimiter: ""}
);
number = this.toNumber(number, options);
return number + "%";
};
I18n.pluralizer = function(locale) {
pluralizer = this.pluralizationRules[locale];
if (pluralizer !== undefined) return pluralizer;
return this.pluralizationRules["en"];
};
I18n.findAndTranslateValidNode = function(keys, translation) {
for (i = 0; i < keys.length; i++) {
key = keys[i];
if (this.isValidNode(translation, key)) return translation[key];
}
return null;
};
I18n.pluralize = function(count, scope, options) {
var translation;
try {
translation = this.lookup(scope, options);
} catch (error) {}
if (!translation) {
return this.missingTranslation(scope);
}
var message;
options = this.prepareOptions(options);
options.count = count.toString();
pluralizer = this.pluralizer(this.currentLocale());
key = pluralizer(Math.abs(count));
keys = ((typeof key == "object") && (key instanceof Array)) ? key : [key];
message = this.findAndTranslateValidNode(keys, translation);
if (message == null) message = this.missingTranslation(scope, keys[0]);
return this.interpolate(message, options);
};
I18n.missingTranslation = function() {
var message = '[missing "' + this.currentLocale()
, count = arguments.length
;
for (var i = 0; i < count; i++) {
message += "." + arguments[i];
}
message += '" translation]';
return message;
};
I18n.currentLocale = function() {
return (I18n.locale || I18n.defaultLocale);
};
// shortcuts
I18n.t = I18n.translate;
I18n.l = I18n.localize;
I18n.p = I18n.pluralize;

View File

@ -0,0 +1,142 @@
/*
* timeago: a jQuery plugin, version: 0.9.2 (2010-09-14)
* @requires jQuery v1.2.3 or later
*
* Timeago is a jQuery plugin that makes it easy to support automatically
* updating fuzzy timestamps (e.g. '4 minutes ago' or 'about 1 day ago').
*
* For usage and examples, visit:
* http://timeago.yarp.com/
*
* Licensed under the MIT:
* http://www.opensource.org/licenses/mit-license.php
*
* Copyright (c) 2008-2010, Ryan McGeary (ryanonjavascript -[at]- mcgeary [*dot*] org)
*/
(function($) {
$.timeago = function(timestamp) {
if (timestamp instanceof Date) return inWords(timestamp);
else if (typeof timestamp == 'string') return inWords($.timeago.parse(timestamp));
else return inWords($.timeago.datetime(timestamp));
};
var $t = $.timeago;
$.extend($.timeago, {
settings: {
refreshMillis: 3000,
allowFuture: true,
strings: {
prefixAgo: null,
prefixFromNow: null,
suffixAgo: 'ago',
suffixFromNow: 'from now',
seconds: 'less than a minute',
minute: 'about a minute',
minutes: '%d minutes',
hour: 'about an hour',
hours: 'about %d hours',
day: 'a day',
days: '%d days',
month: 'about a month',
months: '%d months',
year: 'about a year',
years: '%d years',
numbers: []
}
},
distanceInWords: function(date) {
if(!date) {
return;
}
if(typeof date == 'string') {
date = $.timeago.parse(date);
}
return $.timeago.inWords($.timeago.distance(date));
},
inWords: function(distanceMillis) {
var $l = this.settings.strings;
var prefix = $l.prefixAgo;
var suffix = $l.suffixAgo;
if (this.settings.allowFuture) {
if (distanceMillis < 0) {
prefix = $l.prefixFromNow;
suffix = $l.suffixFromNow;
}
distanceMillis = Math.abs(distanceMillis);
}
var seconds = distanceMillis / 1000;
var minutes = seconds / 60;
var hours = minutes / 60;
var days = hours / 24;
var years = days / 365;
function substitute(stringOrFunction, number) {
var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
var value = ($l.numbers && $l.numbers[number]) || number;
return string.replace(/%d/i, value);
}
var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
seconds < 90 && substitute($l.minute, 1) ||
minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
minutes < 90 && substitute($l.hour, 1) ||
hours < 24 && substitute($l.hours, Math.round(hours)) ||
hours < 48 && substitute($l.day, 1) ||
days < 30 && substitute($l.days, Math.floor(days)) ||
days < 60 && substitute($l.month, 1) ||
days < 365 && substitute($l.months, Math.floor(days / 30)) ||
years < 2 && substitute($l.year, 1) ||
substitute($l.years, Math.floor(years));
return $.trim([prefix, words, suffix].join(' '));
},
distance: function(date) {
return (this.now() - date.getTime());
},
now: function() {
return new Date().getTime();
},
parse: function(iso8601) {
var s = $.trim(iso8601);
s = s.replace(/\.\d\d\d+/,''); // remove milliseconds
s = s.replace(/-/,'/').replace(/-/,'/');
s = s.replace(/T/,' ').replace(/Z/,' UTC');
s = s.replace(/([\+-]\d\d)\:?(\d\d)/,' $1$2'); // -04:00 -> -0400
return new Date(s);
}
});
$.fn.timeago = function() {
this.each(function() {
var data = prepareData(this);
if (!isNaN(data.datetime)) {
$(this).text(inWords(data.datetime));
}
});
return this;
};
function prepareData(element) {
element = $(element);
if (!element.data('timeago') || (element.data('timeago').title != element.attr('title'))) {
element.data('timeago', { datetime: $t.parse(element.attr('title')), title: element.attr('title') });
}
return element.data('timeago');
}
function inWords(date) {
return $t.inWords(distance(date));
}
function distance(date) {
return $t.distance(date);
}
// fix for IE6 suckage
document.createElement('abbr');
document.createElement('time');
})(jQuery);

View File

@ -33,7 +33,7 @@ body {
}
#main {
width: 400px;
width: 800px;
padding-left: 1em;
}
@ -59,3 +59,40 @@ li {
clear: both;
padding-top: 20px;
}
.github-stats {
float: right;
width: 100px;
}
.github-stats li {
float: left;
margin-left: 20px;
}
.summary .left,
.summary .right {
float: left;
width: 350px;
}
dt {
clear: both;
float: left;
width: 60px;
}
dd {
float: left;
}
.green {
border-top: 5px solid lightgreen;
}
.red {
border-top: 5px solid red;
}
#jobs,
.log {
clear: both;
padding-top: 20px;
}