Update the tests.

Update the tests.

--- spec/notifications-spec.coffee
+++ spec/notifications-spec.coffee
@@ -314,10 +314,7 @@

             button = fatalError.querySelector('.btn')
             expect(button.textContent).toContain 'Create issue on the notifications package'
-            unless process.platform is 'win32'
-              expect(button.getAttribute('href')).toContain 'atom/notifications/issues/new'
-            else
-              expect(button.getAttribute('href')).toContain 'git.io/cats'
+            expect(button.getAttribute('href')).toContain 'bit.ly/cats'

             expect(issueTitle).toContain '$ATOM_HOME'
             expect(issueTitle).not.toContain process.env.ATOM_HOME
@@ -586,10 +583,7 @@

           button = fatalError.querySelector('.btn')
           expect(button.textContent).toContain 'Create issue on atom/atom'
-          unless process.platform is 'win32'
-            expect(button.getAttribute('href')).toContain 'atom/atom/issues/new'
-          else
-            expect(button.getAttribute('href')).toContain 'git.io/cats'
+          expect(button.getAttribute('href')).toContain 'bit.ly/cats'

           expect(issueBody).toContain 'ReferenceError: a is not defined'
           expect(issueBody).toContain '**Thrown From**: Atom Core'
@@ -628,10 +622,7 @@
           fatalNotification = fatalError.querySelector('.fatal-notification')
           expect(button.textContent).toContain 'Create issue'
           expect(fatalNotification.textContent).toContain 'You can help by creating an issue'
-          unless process.platform is 'win32'
-            expect(button.getAttribute('href')).toContain 'github.com/atom/notifications/issues/new'
-          else
-            expect(button.getAttribute('href')).toContain 'git.io/cats'
+          expect(button.getAttribute('href')).toContain 'bit.ly/cats'

       describe "when the error has not been reported", ->
         beforeEach ->
@@ -660,44 +651,7 @@
               button = fatalError.querySelector('.btn')
               encodedMessage = encodeURI(truncatedMessage)
               expect(button.textContent).toContain 'Create issue'
-              unless process.platform is 'win32'
-                expect(button.getAttribute('href')).toContain "github.com/atom/notifications/issues/new?title=#{encodedMessage}&body="
-              else
-                expect(button.getAttribute('href')).toContain 'git.io/cats'
-
-        describe "when the system is darwin", ->
-          beforeEach ->
-            UserUtilities = require '../lib/user-utilities'
-            spyOn(UserUtilities, 'getPlatform').andReturn 'darwin'
-
-            generateFakeAjaxResponses()
-            generateException()
-            fatalError = notificationContainer.querySelector('atom-notification.fatal')
-            waitsForPromise ->
-              fatalError.getRenderPromise().then -> issueBody = fatalError.issue.issueBody
-
-          it "asks the user to create an issue", ->
-            button = fatalError.querySelector('.btn')
-            fatalNotification = fatalError.querySelector('.fatal-notification')
-            expect(button.textContent).toContain 'Create issue'
-            expect(fatalNotification.textContent).toContain 'You can help by creating an issue'
-            expect(button.getAttribute('href')).toContain 'github.com/atom/notifications/issues/new'
-
-        describe "when the system is win32", ->
-          beforeEach ->
-            UserUtilities = require '../lib/user-utilities'
-            spyOn(UserUtilities, 'getPlatform').andReturn 'win32'
-
-            generateFakeAjaxResponses()
-            generateException()
-            fatalError = notificationContainer.querySelector('atom-notification.fatal')
-            waitsForPromise ->
-              fatalError.getRenderPromise().then -> issueBody = fatalError.issue.issueBody
-
-          it "uses a shortened url via git.io", ->
-            button = fatalError.querySelector('.btn')
-            expect(button.textContent).toContain 'Create issue'
-            expect(button.getAttribute('href')).toContain 'git.io'
+              expect(button.getAttribute('href')).toContain 'bit.ly/cats'

       describe "when the package is out of date", ->
         beforeEach ->
@@ -921,9 +875,11 @@
 # issuesResponse
 generateFakeAjaxResponses = (options) ->
   $.ajax.andCallFake (url, settings) ->
-    if url.indexOf('git.io') > -1
-      response = options?.shortenerResponse ? ['--', '201', {getResponseHeader: -> 'https://git.io/cats'}]
-      settings.success.apply(settings, response)
+    if url.indexOf('bitly.com') > -1
+      response = options?.shortenerResponse ? {
+        data: url: 'http://bit.ly/cats'
+      }
+      settings.success(response)
     else if url.indexOf('atom.io/api/packages') > -1
       response = options?.packageResponse ? {
         repository: url: 'https://github.com/atom/notifications'


Please do not use require as a method call parameter

--- spec/notifications-spec.coffee
+++ spec/notifications-spec.coffee
@@ -281,7 +281,7 @@
           notificationContainer = workspaceElement.querySelector('atom-notifications')
           fatalError = notificationContainer.querySelector('atom-notification.fatal')

-          spyOn(require('fs'), 'realpathSync').andCallFake (p) -> p
+          spyOn(fs, 'realpathSync').andCallFake (p) -> p
           spyOn(fatalError.issue, 'getPackagePathsByPackageName').andCallFake ->
             'save-session': '/Users/someguy/.atom/packages/save-session'
             'tabs': '/Applications/Atom.app/Contents/Resources/app/node_modules/tabs'


Cleaned error messages in settings workflow.

We should not be displaying an error message that field is invalid if
we don't know anymore if it is invalid or not. We should reset global
messages on any change, we also don't know if it is still relevant
after the change.

--- client/settings.coffee
+++ client/settings.coffee
@@ -1,9 +1,12 @@
 usernameFormMessages = new FormMessages()
+usernameLastValue = null
 usernameReadyForValidation = false
 usernameFieldModified = false
 usernameSubmitted = false

 passwordFormMessages = new FormMessages()
+currentPasswordLastValue = null
+newPasswordLastValue = null
 newPasswordReadyForValidation = false
 newPasswordFieldModified = false

@@ -11,6 +14,7 @@

 resetUsernameForm = ->
   usernameFormMessages.resetMessages()
+  usernameLastValue = null
   usernameReadyForValidation = false
   usernameFieldModified = false

@@ -22,6 +26,8 @@
   else
     $('input.current-password').val('')
     $('input.new-password').val('')
+  currentPasswordLastValue = null
+  newPasswordLastValue = null
   newPasswordReadyForValidation = false
   newPasswordFieldModified = false

@@ -40,16 +46,22 @@
   'submit form.set-username': (event, template) ->
     event.preventDefault()

-    usernameSubmitted = true
+    usernameFormMessages.resetMessages()
     username = $(template.findAll 'input.username').val()
+
+    usernameFormMessages.setErrorMessage 'Username is required.', 'username' unless username
+
     try
-      usernameFormMessages.resetMessages()
+      User.validateUsername username, 'username'
+    catch error
+      usernameFormMessages.setError error
+
+    if usernameFormMessages.isValid()
+      usernameSubmitted = true
       Meteor.call 'set-username', username, (error) ->
         return usernameFormMessages.setError error if error
         resetUsernameForm()
-        usernameFormMessages.setInfoMessage "Username set successfully"
-    catch error
-      usernameFormMessages.setError error
+        usernameFormMessages.setInfoMessage "Username set successfully."

     return # Make sure CoffeeScript does not return anything

@@ -69,6 +81,13 @@
     return # Make sure CoffeeScript does not return anything

   'keyup input.username, paste input.username': (event, template) ->
+    # Proceed only if something really changed
+    newValue = $(template.findAll 'input.username').val()
+    return if newValue is usernameLastValue
+    usernameLastValue = newValue
+
+    usernameFormMessages.resetMessages '' # Reset global messages
+
     usernameFieldModified = true
     username = $(template.findAll 'input.username').val()
     validateUsername username, 'username'
@@ -77,7 +96,6 @@

   'focus input': (event, template) ->
     usernameSubmitted = false
-    usernameFormMessages.resetMessages '' # Reset global messages

     return # Make sure CoffeeScript does not return anything

@@ -111,7 +129,7 @@
     currentPassword = $(template.findAll 'input.current-password').val()
     newPassword = $(template.findAll 'input.new-password').val()

-    passwordFormMessages.setErrorMessage 'Current password is required.', 'current-password' unless currentPassword
+    passwordFormMessages.setErrorMessage "Current password is required.", 'current-password' unless currentPassword

     try
       User.validatePassword newPassword, 'new-password'
@@ -120,11 +138,28 @@

     if passwordFormMessages.isValid()
       changePassword currentPassword, newPassword, (error) ->
-        if error
-          passwordFormMessages.setError error
-        else
-          resetPasswordForm template
-          passwordFormMessages.setInfoMessage "Password changed successfully."
+        return passwordFormMessages.setError error if error
+        resetPasswordForm template
+        passwordFormMessages.setInfoMessage "Password changed successfully."
+
+    return # Make sure CoffeeScript does not return anything
+
+  'keyup input.current-password, paste input.current-password': (event, template) ->
+    # Proceed only if something really changed
+    newValue = $(template.findAll 'input.current-password').val()
+    return if newValue is currentPasswordLastValue
+    currentPasswordLastValue = newValue
+
+    passwordFormMessages.resetMessages '' # Reset global messages
+
+    # We cannot verify validity of the password on the client, so
+    # let's at least remove any error message to not confuse the user
+    # with a message that password is still invalid. In other fields
+    # we are providing feedback immediately, so we have to keep this
+    # the same. We cannot say that field is invalid if in fact maybe
+    # it is not. Better to err on the side of validity than saying that
+    # something is wrong when it is valid.
+    passwordFormMessages.resetMessages 'current-password'

     return # Make sure CoffeeScript does not return anything

@@ -136,14 +171,16 @@
     return # Make sure CoffeeScript does not return anything

   'keyup input.new-password, paste input.new-password': (event, template) ->
+    # Proceed only if something really changed
+    newValue = $(template.findAll 'input.new-password').val()
+    return if newValue is newPasswordLastValue
+    newPasswordLastValue = newValue
+
+    passwordFormMessages.resetMessages '' # Reset global messages
+
     newPasswordFieldModified = true
     newPassword = $(template.findAll 'input.new-password').val()
     validatePassword newPassword, 'new-password'
-
-    return # Make sure CoffeeScript does not return anything
-
-  'focus input': (event, template) ->
-    passwordFormMessages.resetMessages '' # Reset global messages

     return # Make sure CoffeeScript does not return anything

@@ -172,14 +209,12 @@
     callback error
     return

-  try
-    # We check this manually because changePassword error throws global 'Match failed' error if current password is empty string
-    throw new ValidationError "Incorrect password.", 'current-password' unless currentPassword
-    Accounts.changePassword currentPassword, newPassword, (error) ->
-      formError = new ValidationError (error.reason or error.toString() or "Unknown error"), 'current-password' if error
-      callback formError
-  catch error
-    callback error
+  # We check this manually because changePassword error throws global 'Match failed' error if current password is empty string
+  return callback new ValidationError "Current password is required.", 'current-password' unless currentPassword
+
+  Accounts.changePassword currentPassword, newPassword, (error) ->
+    formError = new ValidationError (error.reason or error.toString() or "Unknown error"), 'current-password' if error
+    callback formError

 Template.settingsBackground.checked = ->
   backgroundPaused = !!Meteor.user().settings?.backgroundPaused


Fix merge issue with 401 error code check

--- src/jenkins.coffee
+++ src/jenkins.coffee
@@ -105,12 +105,12 @@
     jenkinsRequest msg, path, (err, req) ->
       if err
         msg.reply "Failed to create jenkins request", err
-      else if res.statusCode == 401
-        msg.send "Invalid credentials"
       else
         req.get() (err, res, body) ->
             if err
               msg.send "Jenkins says: #{err}"
+            else if res.statusCode == 401
+              msg.send "Invalid credentials"
             else
               response = ""
               try
@@ -152,12 +152,12 @@
                 jenkinsRequest msg, path, (err, req) ->
                   if err
                     msg.reply "Failed to create jenkins request", err
-                  else if res.statusCode == 401
-                    msg.send "Invalid credentials"
                   else
                     req.get() (err, res, body) ->
                         if err
                           msg.send "Jenkins says: #{err}"
+                        else if res.statusCode == 401
+                          msg.send "Invalid credentials"
                         else
                           response = ""
                           try


Added conj and disj

Added conj and disj

--- lib/friebyrd.coffee
+++ lib/friebyrd.coffee
@@ -6,7 +6,13 @@

   F.succeed = (result) -> [result]
   F.fail = _.always []
-
+
+  F.disj = (l, r) ->
+    (x) -> _.cat(l(x), r(x))
+
+  F.conj = (l, r) ->
+    (x) -> _.mapcat(l(x), r)
+
   # exports and sundries

   if module?


Fix tests

Fix tests

--- lib/git-view.coffee
+++ lib/git-view.coffee
@@ -14,7 +14,7 @@
         @span outlet: 'gitStatusIcon'

   initialize: (@statusBar) ->
-    @subscribeToBuffer 'saved', @update
+    @statusBar.subscribeToBuffer 'saved', @update
     @subscribe atom.rootView, 'pane-container:active-pane-item-changed', @update
     @subscribe project, 'path-changed', @subscribeToRepo


Call focus check in test

Call focus check in test

--- spec/settings-view-spec.coffee
+++ spec/settings-view-spec.coffee
@@ -202,7 +202,7 @@
     describe "when atom.workspace.open() is used with a config URI", ->
       focusIsWithinActivePanel = ->
         activePanel = settingsView.panelsByName[settingsView.activePanel.name]
-        activePanel.element is document.activeElement or activePanel.contains(document.activeElement)
+        activePanel.element is document.activeElement or activePanel.element.contains(document.activeElement)

       expectActivePanelToBeKeyboardScrollable = ->
         activePanel = settingsView.panelsByName[settingsView.activePanel.name]
@@ -212,7 +212,6 @@
         spyOn(activePanel, 'pageUp')
         atom.commands.dispatch(activePanel.element, 'core:page-up')
         expect(activePanel.pageUp).toHaveBeenCalled()
-

       beforeEach ->
         settingsView = null


Create SettingsView instead of deserializing

--- spec/settings-view-spec.coffee
+++ spec/settings-view-spec.coffee
@@ -10,7 +10,7 @@
   describe "serialization", ->
     it "remembers which panel was visible", ->
       settingsView.showPanel('Packages')
-      newSettingsView = deserialize(settingsView.serialize())
+      newSettingsView = new SettingsView(settingsView.serialize())
       settingsView.remove()
       newSettingsView.attachToDom()
       expect(newSettingsView.activePanelName).toBe 'Packages'
@@ -18,7 +18,7 @@
     it "shows the previously active panel if it is added after deserialization", ->
       settingsView.addPanel('Panel 1', $$ -> @div id: 'panel-1')
       settingsView.showPanel('Panel 1')
-      newSettingsView = deserialize(settingsView.serialize())
+      newSettingsView = new SettingsView(settingsView.serialize())
       settingsView.remove()
       newSettingsView.attachToDom()
       newSettingsView.addPanel('Panel 1', $$ -> @div id: 'panel-1')


fix($tests): fix broken travis tests due to server tests

--- specs/units/server.spec.coffee
+++ specs/units/server.spec.coffee
@@ -1,4 +1,4 @@
-if typeof window isnt 'undefined' and window.options.server
+if typeof window isnt 'undefined' and window.options?.server
   describe 'the server', ->
     describe 'when the source option have been used', ->
       specify 'the sources content', ->


Update batch

Update batch

Not that it didn't work before, but I think Batch should be executed *explicitly* by `cmd.exe`.

--- lib/grammars.coffee
+++ lib/grammars.coffee
@@ -33,8 +33,8 @@

   Batch:
     "File Based":
-      command: ""
-      args: (context) -> [context.filepath]
+      command: "cmd.exe"
+      args: (context) -> ['/q', '/c', context.filepath]

   'Behat Feature':
     "File Based":


Bugfix in use of Mongo update...

Bugfix in use of Mongo update...

--- lib/messages.coffee
+++ lib/messages.coffee
@@ -212,8 +212,10 @@
     unless cmsg.root
       Messages.update
         root: cmsg
-      , $set:
-          root: pmsg.root
+      ,
+        $set: root: pmsg.root
+      ,
+        multi: true
     doc =
       child: child
       parent: parent


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/number_extensions.coffee
+++ test/number_extensions.coffee
@@ -22,7 +22,7 @@
   equals (-5).clamp(0, 1), 0
   equals 1.clamp(0, null), 1
   equals (-1).clamp(0, null), 0
-  equals (-10).clamp()
+  equals (-10).clamp(-5, 0)

 test "#floor", ->
   equals 4.9.floor(), 4, "(4.9).floor() equals 4"


`data/`: rename `trip-stations.csv` to `trips-stations.csv`

--- build/01-download.coffee
+++ build/01-download.coffee
@@ -15,7 +15,7 @@
 	'calendar.txt':			'calendar.csv'
 	'calendar_dates.txt':	'calendar-exceptions.csv'
 	'routes.txt':			'routes.csv'
-	'stop_times.txt':		'trip-stations.csv'
+	'stop_times.txt':		'trips-stations.csv'
 	'stops.txt':			'stations.csv'
 	'transfers.txt':		'transfers.csv'
 	'trips.txt':			'trips.csv'


kite server: throw an error if .listen() is called more than once

--- src/kite-server/kite-server.coffee
+++ src/kite-server/kite-server.coffee
@@ -19,6 +19,7 @@
     @server = null

   listen: (port) ->
+    throw new Error "Already listening!"  if @server?
     @server = new WebSocketServer { port }
     @server.on 'connection', @bound 'onConnection'


Fix for scaffold

Fix for scaffold

--- cake/lib/scaffold.coffee
+++ cake/lib/scaffold.coffee
@@ -3,6 +3,7 @@
 Exec = require './exec'

 module.exports = class Scaffold extends Exec
+  command: './node_modules/.bin/scaffolt'

   @generate: ->
     instance = new this


disabled cyclomatic complexity in authworker.coffee

--- workers/auth/lib/auth/authworker.coffee
+++ workers/auth/lib/auth/authworker.coffee
@@ -1,3 +1,4 @@
+# coffeelint: disable=cyclomatic_complexity
 { EventEmitter } = require 'microemitter'

 module.exports = class AuthWorker extends EventEmitter


fix jasmine tests to exit gulp on test failure

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -132,13 +132,15 @@
 gulp.task 'test-run', ['test-build'], ->
   return gulp.src './temp/**/*.js'
     .pipe jasmine {
+            abortOnFail: true
             integration: true
-            keepRunner: false
+            keepRunner: true
             vendor: [
               'https://code.jquery.com/jquery-2.1.4.min.js'
               'https://ajax.aspnetcdn.com/ajax/knockout/knockout-3.3.0.js'
               'https://cdnjs.cloudflare.com/ajax/libs/knockout-validation/2.0.3/knockout.validation.min.js'
               'https://code.jquery.com/ui/1.11.4/jquery-ui.min.js'
+              'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.11.2/moment.min.js'
             ]
           }


Make update-index task async

Make update-index task async

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -18,11 +18,14 @@
   gulp.src 'home/**/*.{css,js}', base: './home'
     .pipe gulp.dest('./dist')

-gulp.task 'update-index', ['clean'], ->
+gulp.task 'update-index', ['clean'], (cb) ->
   bower.commands.list().on 'end', (pkg) ->
     gulp.src 'home/index.ejs'
       .pipe ejs(pkg, ext: '.html')
       .pipe gulp.dest('./dist')
+      .on 'end', cb
+    return
+  return

 gulp.task 'copy-dependencies', ['clean'], ->
   gulp.src [


gulpfile: feature parity with old Makefile

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -2,7 +2,17 @@
 browserify  = require 'gulp-browserify'
 uglify      = require 'gulp-uglify'
 rename      = require 'gulp-rename'
-coffee      = require 'coffee-script'
+coffee      = require 'gulp-coffee'
+util        = require 'gulp-util'
+replace     = require 'gulp-replace'
+
+coffee4Node = -> (coffee bare: yes, sourceMap: yes).on 'error', util.log
+
+gulp.task 'build kite', ->
+  gulp.src 'src/kite/*.coffee'
+    .pipe coffee4Node()
+    .pipe replace '.coffee', '.js'
+    .pipe gulp.dest 'lib/kite'

 gulp.task 'browserify kite', ->
   gulp.src 'src/kite/kite.coffee', read: no
@@ -12,6 +22,12 @@
       standalone  : 'Kite'
     .pipe rename 'kite-bundle.js'
     .pipe gulp.dest 'browser'
+
+gulp.task 'build kite as promised', ->
+  gulp.src 'src/kite-as-promised/*.coffee'
+    .pipe coffee4Node()
+    .pipe replace '.coffee', '.js'
+    .pipe gulp.dest 'lib/kite-as-promised'

 gulp.task 'browserify kite as promised', ->
   gulp.src 'src/kite-as-promised/kite.coffee', read: no
@@ -23,6 +39,12 @@
     .pipe rename 'kite-promises-bundle.js'
     .pipe gulp.dest 'browser'

+gulp.task 'build kontrol', ->
+  gulp.src 'src/kontrol/*.coffee'
+    .pipe coffee4Node()
+    .pipe replace '.coffee', '.js'
+    .pipe gulp.dest 'lib/kontrol'
+
 gulp.task 'browserify kontrol', ->
   gulp.src 'src/kontrol/kontrol.coffee', read: no
     .pipe browserify
@@ -31,6 +53,12 @@
       standalone  : 'Kontrol'
     .pipe rename 'kontrol-bundle.js'
     .pipe gulp.dest 'browser'
+
+gulp.task 'build kontrol as promised', ->
+  gulp.src 'src/kontrol-as-promised/*.coffee'
+    .pipe coffee4Node()
+    .pipe replace '.coffee', '.js'
+    .pipe gulp.dest 'lib/kontrol-as-promised'

 gulp.task 'browserify kontrol as promised', ->
   gulp.src 'src/kontrol-as-promised/kontrol.coffee', read: no
@@ -42,10 +70,17 @@
     .pipe gulp.dest 'browser'

 gulp.task 'build', [
+  'build kite'
+  'build kite as promised'
+  'build kontrol'
+  'build kontrol as promised'
+]
+
+gulp.task 'browserify', [
   'browserify kite'
   'browserify kite as promised'
   'browserify kontrol'
   'browserify kontrol as promised'
 ]

-gulp.task 'default', ['build']
+gulp.task 'default', ['build', 'browserify']


Require 'gulp'

Require 'gulp'

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -0,0 +1 @@
+gulp = require 'gulp'


Optimize tasks

Optimize tasks

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -1,7 +1,6 @@
 gulp     = require 'gulp'
 plumber  = require 'gulp-plumber'
 coffee   = require 'gulp-coffee'
-iconutil = require './'

 gulp.task 'build', ->
   gulp.src 'src/**/*.coffee'
@@ -11,3 +10,5 @@

 gulp.task 'watch', ['build'], ->
   gulp.watch 'src/**/*.coffee', ['build']
+
+gulp.task 'default', ['watch']


Fixed typo.

Fixed typo.

--- lib/services.coffee
+++ lib/services.coffee
@@ -12,7 +12,7 @@
 	true_values = [1, true, 'true', 'run', 'running', 'start', 'started']
 	false_values = [0, false, 'false', 'stop', 'stopped']

-	for name, value in manifest.services
+	for name, value of manifest.services
 		switch
 			when value in true_values then action = 'restart'
 			when value in false_values then action = 'stop'


modify: change font size and add class attribute

--- assets/js/comments.coffee
+++ assets/js/comments.coffee
@@ -29,7 +29,7 @@
     return new Promise (resolve, reject) ->
       idName     = 'comment' + Math.random().toString(36).slice(-8)
       commentElm =
-        "
" + comment + '
'
+        "
" + comment + '
'
       $('body').append commentElm
       dom = $ "##{idName}"
       resolve dom
@@ -38,6 +38,7 @@
     return new Promise (resolve, reject) ->
       top = Math.floor(Math.random() * 100)
       dom.css
+        'font-size' : '30px'
         display: 'block'
         position: 'fixed'
         left: '100%'


Send definition data

Send definition data

--- src/library.coffee
+++ src/library.coffee
@@ -198,8 +198,8 @@
       source =
         name: basename
         library: library
-        code: '' # TODO: discovery data in YAML
-        language: 'yaml'
+        code: JSON.stringify(component.definition or {}, null, 2)
+        language: 'discovery'
       return callback null, source

     lang = component.language


JCredential: create: use CredentialStore:create instead

--- workers/social/lib/social/models/computeproviders/credential.coffee
+++ workers/social/lib/social/models/computeproviders/credential.coffee
@@ -1,4 +1,5 @@
-jraphical      = require 'jraphical'
+hat       = require 'hat'
+jraphical = require 'jraphical'

 module.exports = class JCredential extends jraphical.Module

@@ -7,6 +8,7 @@
   JGroup             = require '../group'
   JCredentialData    = require './credentialdata'
   { PROVIDERS }      = require './computeutils'
+  CredentialStore    = require './credentialstore'

   KodingError        = require '../../error'

@@ -185,12 +187,10 @@
         callback new KodingError 'Provider is not supported'
         return

-      credData = new JCredentialData { meta, originId }
-      credData.save (err) ->
+      CredentialStore.create client, { meta, originId }, (err, identifier) ->
         return  if failed err, callback

-        { identifier }   = credData
-        _data            = { provider, title, identifier, originId }
+        _data = { provider, title, identifier, originId }

         if provider in ['custom', 'userInput']
           _data.fields   = (Object.keys meta) or []
@@ -199,11 +199,12 @@
         credential = new JCredential _data

         credential.save (err) ->
-          return  if failed err, callback, credData
+          return  if failed err, callback

           delegate.addCredential credential, { as: 'owner' }, (err) ->
-            return  if failed err, callback, credential, credData
-
+            return  if failed err, callback, credential
+
+            callback null, credential

   @fetchByIdentifier = (client, identifier, callback) ->


Fix loading throttled properties

Fix loading throttled properties

These were throwing errors when used through yarn link and i’m surprised they haven’t cause more issues since what  I had there didn’t make a ton of sense.  Like the old throttling value should never have resolved since we don’t have a “this” when the component is created.

--- src/loading.coffee
+++ src/loading.coffee
@@ -35,6 +35,8 @@
 		videoLoaded:     false
 		fallbackLoading: false
 		fallbackLoaded:  false
+		loadingThrottled: false
+		loadedThrottled: false

 	##############################################################################
 	mounted: ->
@@ -89,12 +91,6 @@
 		# Are any assets currenting loading.
 		loading: -> @posterLoading or @imageLoading or @videoLoading or @fallbackLoading
 		loaded: -> @posterLoaded or @imageLoaded or @videoLoaded or @fallbackLoaded
-
-		# Try to reduce the loading state flickering back and forth when one asset
-		# finishes and the next begins.  Also will prevent the loader showing for
-		# super quck loads.
-		loadingThrottled: throttle (-> @loading), @loaderThrottle
-		loadedThrottled: throttle (-> @loaded), @loaderThrottle

 		# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 		# Loader
@@ -125,6 +121,18 @@
 		videoLoaded:     (bool) -> @$emit 'video-loaded' if bool
 		fallbackLoading: (bool) -> @$emit 'fallback-loading' if bool
 		fallbackLoaded:  (bool) -> @$emit 'fallback-loaded' if bool
+
+		# Create throttling functions
+		loaderThrottle:
+			immediate: true
+			handler: (amount) ->
+				@getLoadingThrottled = throttle (=> @loading), amount
+				@getLoadedThrottled = throttle (=> @loaded), amount
+
+		# Create the throttled loading values
+		loading: (bool) -> @loadingThrottled = @getLoadingThrottled()
+		loaded: (bool) -> @loadedThrottled = @getLoadedThrottled()
+

 	##############################################################################
 	methods:


more strip out debug

more strip out debug

--- src/main.iced
+++ src/main.iced
@@ -172,12 +172,9 @@
   load_field : (r, f, dflt) ->
     v = dflt if not (v = r[f])? and dflt?
     @fill_both f, v, "input-#{f.replace '_', '-'}"
-    console.log "load field #{f} -> #{v}"

   load_record_by_host: (h) ->
     r = @e.get_record h
-    console.log "RECORD"
-    console.log r
     # load the algo version first, since there might be race conditions
     # in setting it last with the background computation firing up
     @load_field r, "algo_version", config.input.defaults.algo_version


If I had a nickel for every commit message that read 'I can't read'...

--- scripts/jerk.coffee
+++ scripts/jerk.coffee
@@ -4,5 +4,5 @@
     type: 'GET'
     dataType: 'json'
     success: (data, textStatus, jqXHR) ->
-        post = data.data.children.random
+        post = res.random data.data.children
         res.send "#{post.data.title}", "#{post.data.selftext}"


fixed off by one error

fixed off by one error

getMonth returns a zero indexed result, resulting in incorrect months displayed while classifying.

--- classify/metadata.coffee
+++ classify/metadata.coffee
@@ -5,7 +5,7 @@

   formattedTimestamp: (ts) ->
     date = new Date(ts)
-    "#{date.getMonth()}-#{date.getDate()}-#{date.getFullYear()}"
+    "#{date.getMonth()+1}-#{date.getDate()}-#{date.getFullYear()}"

   setSubject: (subject) -> @subject = subject


set transition and theme in the select lists for an existing record

--- client/edit.views.coffee
+++ client/edit.views.coffee
@@ -40,6 +40,8 @@
       @setEditorContents @model.get "code"
       @modeEl.val(@model.get "mode")
       @secret.setSecret @model.get "secret"
+      @themeEl.val(@model.get "theme")
+      @transitionEl.val(@model.get "transition")

       if e.source is "default"
         @showUnsavedNotification()


Routes: Fixed wrong redirection.

Routes: Fixed wrong redirection.

--- client/ide/routes.coffee
+++ client/ide/routes.coffee
@@ -123,7 +123,8 @@
       return routeToLatestWorkspace()  if err

       try
-        KD.userWorkspaces.forEach (workspace) =>
+
+        KD.userWorkspaces.forEach (workspace, index) =>

           if workspace.channelId is channel.id

@@ -140,7 +141,9 @@
               return loadIDE { machine, workspace, username, channelId }

           else
-            routeToLatestWorkspace()
+
+            if index + 1 is KD.userWorkspaces.length
+              routeToLatestWorkspace()

       catch e


Oops, fix indentation

Oops, fix indentation

--- assets/views/board.coffee
+++ assets/views/board.coffee
@@ -123,10 +123,10 @@
     false

   wheel: (e) ->
-     e = e.originalEvent if e.originalEvent
-     d = e.wheelDelta || -e.detail
-     if d > 0 then @zoomIn() else @zoomOut()
-     false
+    e = e.originalEvent if e.originalEvent
+    d = e.wheelDelta || -e.detail
+    if d > 0 then @zoomIn() else @zoomOut()
+    false

   showZoomLevel: =>
     level = Math.ceil 100 * @viewport.get 'zoom'


Fix unit tests on Windows

Fix unit tests on Windows

--- tests/node-binary.spec.coffee
+++ tests/node-binary.spec.coffee
@@ -1,3 +1,4 @@
+path = require('path')
 sinon = require('sinon')
 _ = require('lodash-contrib')
 chai = require('chai')
@@ -42,8 +43,8 @@
 				os: 'win32'
 				version: 'v0.12.0'

-			@tempFile = '/tmp/node.exe'
-			@destFile = '/dest/node-v0.12.0-win32-x64.exe'
+			@tempFile = path.join('tmp', 'node.exe')
+			@destFile = path.join('dest', 'node-v0.12.0-win32-x64.exe')

 			@utilsDownloadNodePackageStub = sinon.stub(utils, 'downloadNodePackage')
 			@utilsDownloadNodePackageStub.yields(null, @tempFile)
@@ -64,10 +65,10 @@
 			@utilsMkdirp.restore()

 		it 'should complete the download in the correct place', (done) ->
-			binary.download @node, '/dest', (error, binaryFile) =>
+			binary.download @node, 'dest', (error, binaryFile) =>
 				expect(error).to.not.exist
 				expect(binaryFile).to.equal(@destFile)
-				expect(@utilsMkdirp).to.have.been.calledWith('/dest')
+				expect(@utilsMkdirp).to.have.been.calledWith('dest')
 				expect(@fsRenameStub).to.have.been.calledWith(@tempFile, @destFile)
 				expect(@utilsRemoveDirectory).to.have.been.calledWith(@tempFile)
 				done()
@@ -82,8 +83,8 @@
 				os: 'darwin'
 				version: 'v0.12.0'

-			@tempFile = '/tmp/node-v0.12.0-darwin-x64'
-			@destFile = '/dest/node-v0.12.0-darwin-x64'
+			@tempFile = path.join('tmp', 'node-v0.12.0-darwin-x64')
+			@destFile = path.join('dest', 'node-v0.12.0-darwin-x64')

 			@utilsDownloadNodePackageStub = sinon.stub(utils, 'downloadNodePackage')
 			@utilsDownloadNodePackageStub.yields(null, @tempFile)
@@ -104,11 +105,12 @@
 			@utilsMkdirp.restore()

 		it 'should complete the download in the correct place', (done) ->
-			binary.download @node, '/dest', (error, binaryFile) =>
+			binary.download @node, 'dest', (error, binaryFile) =>
 				expect(error).to.not.exist
 				expect(binaryFile).to.equal(@destFile)
-				expect(@utilsMkdirp).to.have.been.calledWith('/dest')
-				expect(@fsRenameStub).to.have.been.calledWith(@tempFile + '/bin/node', @destFile)
+				expect(@utilsMkdirp).to.have.been.calledWith('dest')
+				binPath = path.join(@tempFile, 'bin', 'node')
+				expect(@fsRenameStub).to.have.been.calledWith(binPath, @destFile)
 				expect(@utilsRemoveDirectory).to.have.been.calledWith(@tempFile)
 				done()

@@ -122,8 +124,8 @@
 				os: 'linux'
 				version: 'v0.12.0'

-			@tempFile = '/tmp/node-v0.12.0-linux-x64'
-			@destFile = '/dest/node-v0.12.0-linux-x64'
+			@tempFile = path.join('tmp', 'node-v0.12.0-linux-x64')
+			@destFile = path.join('dest', 'node-v0.12.0-linux-x64')

 			@utilsDownloadNodePackageStub = sinon.stub(utils, 'downloadNodePackage')
 			@utilsDownloadNodePackageStub.yields(null, @tempFile)
@@ -144,11 +146,12 @@
 			@utilsMkdirp.restore()

 		it 'should complete the download in the correct place', (done) ->
-			binary.download @node, '/dest', (error, binaryFile) =>
+			binary.download @node, 'dest', (error, binaryFile) =>
 				expect(error).to.not.exist
 				expect(binaryFile).to.equal(@destFile)
-				expect(@utilsMkdirp).to.have.been.calledWith('/dest')
-				expect(@fsRenameStub).to.have.been.calledWith(@tempFile + '/bin/node', @destFile)
+				expect(@utilsMkdirp).to.have.been.calledWith('dest')
+				binPath = path.join(@tempFile, 'bin', 'node')
+				expect(@fsRenameStub).to.have.been.calledWith(binPath, @destFile)
 				expect(@utilsRemoveDirectory).to.have.been.calledWith(@tempFile)
 				done()

@@ -162,8 +165,8 @@
 				os: 'sunos'
 				version: 'v0.12.0'

-			@tempFile = '/tmp/node-v0.12.0-sunos-x64'
-			@destFile = '/dest/node-v0.12.0-sunos-x64'
+			@tempFile = path.join('tmp', 'node-v0.12.0-sunos-x64')
+			@destFile = path.join('dest', 'node-v0.12.0-sunos-x64')

 			@utilsDownloadNodePackageStub = sinon.stub(utils, 'downloadNodePackage')
 			@utilsDownloadNodePackageStub.yields(null, @tempFile)
@@ -184,10 +187,11 @@
 			@utilsMkdirp.restore()

 		it 'should complete the download in the correct place', (done) ->
-			binary.download @node, '/dest', (error, binaryFile) =>
+			binary.download @node, 'dest', (error, binaryFile) =>
 				expect(error).to.not.exist
 				expect(binaryFile).to.equal(@destFile)
-				expect(@utilsMkdirp).to.have.been.calledWith('/dest')
-				expect(@fsRenameStub).to.have.been.calledWith(@tempFile + '/bin/node', @destFile)
+				expect(@utilsMkdirp).to.have.been.calledWith('dest')
+				binPath = path.join(@tempFile, 'bin', 'node')
+				expect(@fsRenameStub).to.have.been.calledWith(binPath, @destFile)
 				expect(@utilsRemoveDirectory).to.have.been.calledWith(@tempFile)
 				done()


Don’t use Serializable mixin in PaneAxis

--- src/pane-axis.coffee
+++ src/pane-axis.coffee
@@ -1,16 +1,20 @@
 {Emitter, CompositeDisposable} = require 'event-kit'
 {flatten} = require 'underscore-plus'
-Serializable = require 'serializable'
 Model = require './model'

 module.exports =
 class PaneAxis extends Model
   atom.deserializers.add(this)
-  Serializable.includeInto(this)

   parent: null
   container: null
   orientation: null
+
+  @deserialize: (state, params) ->
+    container = params?.container
+    state.container = container
+    state.children = state.children.map (childState) -> atom.deserializers.deserialize(childState, {container})
+    new this(state)

   constructor: ({@container, @orientation, children, flexScale}={}) ->
     @emitter = new Emitter
@@ -21,12 +25,8 @@
       @addChild(child) for child in children
     @flexScale = flexScale ? 1

-  deserializeParams: (params) ->
-    {container} = params
-    params.children = params.children.map (childState) -> atom.deserializers.deserialize(childState, {container})
-    params
-
-  serializeParams: ->
+  serialize: ->
+    deserializer: 'PaneAxis'
     children: @children.map (child) -> child.serialize()
     orientation: @orientation
     flexScale: @flexScale


sending all events to particles

sending all events to particles

--- src/timed_events.coffee
+++ src/timed_events.coffee
@@ -13,7 +13,7 @@

   self.bind "update", (elapsedTime) ->
     for event in I.everyEvents
-      while event
+      while event.lastFired

   ###*
   Execute fn every n frames.


Fixed up the greeting in rain.

Fixed up the greeting in rain.

--- scripts/rain.coffee
+++ scripts/rain.coffee
@@ -16,15 +16,24 @@
 moment = require 'moment'

 # default greeting
-greeting = 'Hello'
- # Time
-currentHour = moment().hour()
-if currentHour < 12
-  greeting = 'Good morning'
-if currentHour >= 12 && currentHour <= 17
-  greeting = 'Good afternoon'
-if currentHour > 17 && currentHour <= 24
-  greeting = 'Good evening'
+
+
+# Time
+currentHour = null
+greeting = null
+getGreeting = (greeting) ->
+	currentHour = moment().hour()
+	if currentHour < 12
+		greeting = 'Good morning'
+	else if currentHour >= 12 && currentHour <= 17
+		greeting = 'Good afternoon'
+	else if currentHour > 17 && currentHour <= 24
+		greeting = 'Good evening'
+	else
+		greeting = 'Hello'
+
+# Weather
+

 # get the weather
 currentWeather = ' '
@@ -105,9 +114,9 @@

 module.exports = (robot) ->
   robot.hear /rain/i, (msg) ->
-    # Weather
-    getWeather msg, (currentWeather) ->
-    	msg.send greeting + '. ' + currentWeather
+  	getWeather msg, (currentWeather) ->
+  		theGreeting = getGreeting(greeting)
+  		msg.send theGreeting + '. ' + currentWeather


updage: games.coffeeのリファクタリング

updage: games.coffeeのリファクタリング

クラスを用いて簡潔にした。
コード量が大幅に減った。

--- app/assets/javascripts/games.coffee
+++ app/assets/javascripts/games.coffee
@@ -1,3 +1,51 @@
+Number::ordinal = ->
+  return 'th' if 11 <= this % 100 <= 13
+  switch this % 10
+    when 1 then 'st'
+    when 2 then 'nd'
+    when 3 then 'rd'
+    else        'th'
+
+Number::ordinalize = ->
+  this + this.ordinal()
+
+################################################################################
+
+class GameDetailAttributes
+
+  constructor: (args) ->
+    console.log 'create GameDetail'
+    return unless args.nth or args.attributes
+    @nth = args.nth ? 0
+    @attributes = args.attributes
+    @prefix = 'game_game_detail' +
+      parseInt(@nth).ordinalize() + 's' +
+      '_attributes_'
+    @regexs = (///^#{@prefix}\d_#{i}$/// for i in @attributes)
+    console.log @nth
+    console.log @attributes
+    console.log @prefix
+    console.log @regexs
+
+  # public methods
+  switch: (event) ->
+    console.log 'switch method runs'
+    console.log event.target.id
+    console.log @nth
+    console.log @regexs
+    return if @nth = 0
+    for regex, i in @regexs
+      if regex.test(event.target.id)
+        i = event.target.id.match(/_\d+_/)[0].replace(/_/g, '')
+        if $('#' + @prefix + i + '_' + @attributes[i])[0].checked
+          $('.' + @attributes[i] + '_' + i).toggleClass('hidden')
+        else
+          $('.' + @attributes[i] + '_' + i).toggleClass('hidden')
+          $('#' + @prefix + i + '_my_' + @attributes[i]).val('')
+          $('#' + @prefix + i + '_opponent_' + @attributes[i]).val('')
+
+################################################################################
+
 $ ->
   # write/rewrite for a label
   replay_label = (counter, element) ->
@@ -15,56 +63,23 @@
         c += 1
         replay_label c, $(@).children('div.enclosure').children('div.replay')

+################################################################################
+
   # run on load
   c = 0
   $('div.replay').each ->
     c += 1
     replay_label c, $(@)
+  console.log 'run on load, gon.contest_nth:' + gon.contest_nth

-  # when game_detail added
-  $('form').on 'fields_added.nested_form_fields', (event, param) ->
-    replay_labels()
-
-  # when game_detail removed
-  $('form').on 'fields_removed.nested_form_fields', (event, param) ->
-    replay_labels()
-
-  # when jury_votes/progress cheched/unchecked
-  switch gon.contest_nth
-    when 29
-      $('form').on 'change', (event) ->
-        if /^game_game_detail29ths_attributes_\d+_jury_votes$/.test(event.target.id)
-          i = event.target.id.match(/_\d+_/)[0].replace(/_/g, '')
-          if $('#game_game_detail29ths_attributes_' + i + '_jury_votes')[0].checked
-            $('.jury_votes_' + i).toggleClass('hidden')
-          else
-            $('.jury_votes_' + i).toggleClass('hidden')
-            $('#game_game_detail29ths_attributes_' + i +
-              '_my_jury_votes').val('')
-            $('#game_game_detail29ths_attributes_' + i +
-              '_opponent_jury_votes').val('')
-        if /^game_game_detail29ths_attributes_\d+_progress$/.test(event.target.id)
-          i = event.target.id.match(/_\d+_/)[0].replace(/_/g, '')
-          if $('#game_game_detail29ths_attributes_' + i + '_progress')[0].checked
-            $('.progress_' + i).toggleClass('hidden')
-          else
-            $('.progress_' + i).toggleClass('hidden')
-            $('#game_game_detail29ths_attributes_' + i +
-              '_my_progress').val('')
-            $('#game_game_detail29ths_attributes_' + i +
-              '_opponent_progress').val('')
-    when 30
-      $('form').on 'change', (event) ->
-        if /^game_game_detail30ths_attributes_\d+_jury_votes$/.test(event.target.id)
-          i = event.target.id.match(/_\d+_/)[0].replace(/_/g, '')
-          if $('#game_game_detail30ths_attributes_' + i + '_jury_votes')[0].checked
-            $('.jury_votes_' + i).toggleClass('hidden')
-          else
-            $('.jury_votes_' + i).toggleClass('hidden')
-            $('#game_game_detail30ths_attributes_' + i +
-              '_my_jury_votes').val('')
-            $('#game_game_detail30ths_attributes_' + i +
-              '_opponent_jury_votes').val('')
+  # モデルでスイッチ表示する属性情報を持たせようと思ったがここで持たせることにした
+  if gon.contest_nth
+    switch gon.contest_nth
+      when 29
+        attr = [ 'jury_votes', 'progress' ]
+      when 30
+        attr = [ 'jury_votes' ]
+    gda = new GameDetailAttributes({nth: gon.contest_nth, attributes: attr})

   # 試合(game)で地区を変更すると回戦も変化させる
   $(document).on 'change', '#game_region_code', ->
@@ -78,7 +93,9 @@
         region_code: $(this).val()
       }
     ).done (results) ->
-      i = 0 # each with indexを使うと意図したとおりに動かなかったので制御変数を使うことにした
+      i = 0
+        # each with indexを使うと意図したとおりに動かなかったので
+        # 制御変数を使うことにした
       $.each results, ->
         option = $('<option>').val(this.round).text(this.name)
         if i == 0
@@ -86,3 +103,14 @@
         else
           $('#game_round').append(option)
         i += 1
+
+  # when game_detail added
+  $('form').on 'fields_added.nested_form_fields', (event, param) ->
+    replay_labels()
+
+  # when game_detail removed
+  $('form').on 'fields_removed.nested_form_fields', (event, param) ->
+    replay_labels()
+
+  $('form').on 'change', (event) ->
+    gda.switch(event)


Fixing config in navurls

Fixing config in navurls

--- app/coffee/modules/base/urls.coffee
+++ app/coffee/modules/base/urls.coffee
@@ -30,7 +30,7 @@

     constructor: (@config) ->
         @.urls = {}
-        @.mainUrl = config.get("api")
+        @.mainUrl = @config.get("api")

     update: (urls) ->
         @.urls = _.merge(@.urls, urls)


removed backbone.sync override

removed backbone.sync override

--- client/main.coffee
+++ client/main.coffee
@@ -3,16 +3,6 @@
   ###
   # Core Model and Logic
   ###
-  parentSyncMethod = Backbone.sync
-  Backbone.sync = (method, model, options) ->
-    old_error = options.old_error
-    options.error = (xhr, text_status, error_thrown) ->
-      if(xhr.status == 302)
-       window.location.replace('http://localhost:8080/login')
-      else
-       old_error?(xhr, text_status, error_thrown)
-    parentSyncMethod(method, model, options)
-
   class Post extends Backbone.Model
     respond: (content) =>
       p = new Post(


fix undefined key error

fix undefined key error

--- app/js/controllers/delegates.coffee
+++ app/js/controllers/delegates.coffee
@@ -9,13 +9,15 @@

     $scope.toggleVoteUp = (name) ->
-        if not name in Wallet.trust_levels or Wallet.trust_levels[name] < 1
+        console.log "CLICK"
+        console.log (name in Wallet.trust_levels)
+        if name not in Wallet.trust_levels or Wallet.trust_levels[name] < 1
             Wallet.set_trust(name, 1)
         else
             Wallet.set_trust(name, 0)

     $scope.toggleVoteDown = (name) ->
-        if not Wallet.trust_levels or Wallet.trust_levels[name] > -1
+        if name not in Wallet.trust_levels or Wallet.trust_levels[name] > -1
             Wallet.set_trust(name, -1)
         else
             Wallet.set_trust(name, 0)


Validators: revert to old logic to pass tests if possible

--- workers/social/lib/social/models/group/validators.coffee
+++ workers/social/lib/social/models/group/validators.coffee
@@ -43,18 +43,23 @@
     return  unless hasDelegate delegate, callback
     return callback null, yes  if delegate.equals this

-    delegateId = delegate.getId()
+    Validators.any client, group, permission, permissionSet, _, (err, allow) =>

-    if @originId? and delegateId.equals @originId
-      callback null, yes
+      if err or not allow
+        return callback err, no

-    else
-      ownerSelector =
-        sourceId    : delegateId
-        targetId    : @getId()
-        as          : 'owner'
+      delegateId = delegate.getId()

-      Relationship.count ownerSelector, createExistenceCallback callback
+      if @originId? and delegateId.equals @originId
+        callback null, yes
+
+      else
+        ownerSelector =
+          sourceId    : delegateId
+          targetId    : @getId()
+          as          : 'owner'
+
+        Relationship.count ownerSelector, createExistenceCallback callback

   any: (client, group, permission, permissionSet, _, callback) ->


permission: add default value

permission: add default value

--- workers/social/lib/social/models/group/validators.coffee
+++ workers/social/lib/social/models/group/validators.coffee
@@ -3,7 +3,7 @@
 getRoles = (permission, permissionSet) ->
   return []  unless permissionSet
   roles = ( perm.role for perm in permissionSet.permissions \
-                      when permission in perm?.permissions   )
+                      when permission in perm?.permissions or [] )
   roles.push 'admin' # admin can do anything!
   return roles


Return

Return

--- src/processor.coffee
+++ src/processor.coffee
@@ -28,14 +28,16 @@
           .tapCatch (err) -> newrelic().noticeError new Error("#{err.type} - #{JSON.stringify(err.detail)}")

       execute()
-        .tap => @_emitEvent "successful", { context, id, notification }
-        .catch (error) =>
-          throw error unless error instanceof NonRetryable
-          @_emitEvent "unsuccessful_non_retryable", { context, id, notification, error }
-        .tapCatch (error) => @_emitEvent "unsuccessful", { context, id, notification, error }
-        .finally => @_emitEvent "finished", { context, id, notification }
-        .asCallback context.done
-
+      .tap => @_emitEvent "successful", { context, id, notification }
+      .catch (error) =>
+        throw error unless error instanceof NonRetryable
+        @_emitEvent "unsuccessful_non_retryable", { context, id, notification, error }
+      .tapCatch (error) => @_emitEvent "unsuccessful", { context, id, notification, error }
+      .finally => @_emitEvent "finished", { context, id, notification }
+      .asCallback context.done
+
+      return
+

     _emitEvent: (eventName, value) =>
       @emit eventName, value if ENABLE_EVENTS


edited a clause in our acceptable use policy

--- client/legal/lib/userpolicyview.coffee
+++ client/legal/lib/userpolicyview.coffee
@@ -116,7 +116,7 @@
           controlled substances or illegal software;

-
+

         In addition to (and/or as some examples of) the violations described in
         our Terms of Service, you may not and may not allow any third party, including your
@@ -213,7 +213,7 @@

-          Modify, adapt, translate, or reverse engineer any portion of the Service
+          Modify, adapt, translate, or reverse engineer any portion of the Service (this includes, but not limited to, attempting to deliberately extend the permitted resource allocation for any of your virtual machines)


abstract role and prog name into vars; add tf plan, refresh, apply, destroy commands

--- src/tf.coffee
+++ src/tf.coffee
@@ -20,17 +20,18 @@
 basepath = process.env.HUBOT_TF_BASEPATH || ''
 privatekey = basepath + "/hubot-tf.key"
 publickey = privatekey + ".pub"
+tfName = tfRole = 'tf'

 module.exports = (robot) ->

   robot.respond /tf help$/, (msg) ->
     cmds = []
     arr = [
-      "tf create key - create rsa key, for git operations"
-      "tf destroy key - erase key"
-      "tf show key - display public key"
-      "tf clone <repourl> <projectname>"
-      "tf (plan|apply|destroy) <projectname> - tf operations"
+      "#{tfName} create key - create rsa key, for git operations"
+      "#{tfName} destroy key - erase key"
+      "#{tfName} show key - display public key"
+      "#{tfName} clone <repourl> <projectname>"
+      "#{tfName} (plan|apply|destroy) <projectname> - tf operations"
     ]

     for str in arr
@@ -43,8 +44,8 @@
       msg.reply cmds.join "\n"

   robot.respond /tf create key$/i, (msg) ->
-    unless robot.auth.isAdmin(msg.envelope.user) or robot.auth.hasRole(msg.envelope.user,'tf')
-      return msg.send {room: msg.message.user.name}, "Not authorized.  Missing 'tf' role."
+    unless robot.auth.isAdmin(msg.envelope.user) or robot.auth.hasRole(msg.envelope.user,tfRole)
+      return msg.send {room: msg.message.user.name}, "Not authorized.  Missing #{tfRole} role."

     if fs.existsSync("#{publickey}")
       return msg.send {room: msg.message.user.name}, "Key exists!  Destroy it first."
@@ -55,8 +56,8 @@
       msg.send {room: msg.message.user.name}, "```\n#{pubkey}\n```"

   robot.respond /tf show key$/i, (msg) ->
-    unless robot.auth.isAdmin(msg.envelope.user) or robot.auth.hasRole(msg.envelope.user,'tf')
-      return msg.send {room: msg.message.user.name}, "Not authorized.  Missing 'tf' role."
+    unless robot.auth.isAdmin(msg.envelope.user) or robot.auth.hasRole(msg.envelope.user,tfRole)
+      return msg.send {room: msg.message.user.name}, "Not authorized.  Missing #{tfRole} role."

     if ! fs.existsSync("#{publickey}")
       return msg.send {room: msg.message.user.name}, "No key on file!  Create one first."
@@ -65,8 +66,8 @@
     return msg.send {room: msg.message.user.name}, "```\n#{pubkey}\n```"

   robot.respond /tf destroy key$/i, (msg) ->
-    unless robot.auth.isAdmin(msg.envelope.user) or robot.auth.hasRole(msg.envelope.user,'tf')
-      return msg.send {room: msg.message.user.name}, "Not authorized.  Missing 'tf' role."
+    unless robot.auth.isAdmin(msg.envelope.user) or robot.auth.hasRole(msg.envelope.user,tfRole)
+      return msg.send {room: msg.message.user.name}, "Not authorized.  Missing #{tfRole} role."

     if ! fs.existsSync("#{publickey}")
       return msg.send {room: msg.message.user.name}, "No key on file!  Create one first."
@@ -76,8 +77,8 @@
     return msg.send {room: msg.message.user.name}, "Key destroyed!"

   robot.respond /tf clone ([^\s]+) ([^\s]+)$/i, (msg) ->
-    unless robot.auth.isAdmin(msg.envelope.user) or robot.auth.hasRole(msg.envelope.user,'tf')
-      return msg.send {room: msg.message.user.name}, "Not authorized.  Missing 'tf' role."
+    unless robot.auth.isAdmin(msg.envelope.user) or robot.auth.hasRole(msg.envelope.user,tfRole)
+      return msg.send {room: msg.message.user.name}, "Not authorized.  Missing #{tfRole} role."

     url = msg.match[1]
     projname = msg.match[2].replace /\//, "_"
@@ -96,12 +97,28 @@
         msg.send {room: msg.message.user.name}, "stdout:\n```\n#{stdout}\n```"

   robot.respond /tf list(\sprojects)?$/i, (msg) ->
-    unless robot.auth.isAdmin(msg.envelope.user) or robot.auth.hasRole(msg.envelope.user,'tf')
-      return msg.send {room: msg.message.user.name}, "Not authorized.  Missing 'tf' role."
+    unless robot.auth.isAdmin(msg.envelope.user) or robot.auth.hasRole(msg.envelope.user,tfRole)
+      return msg.send {room: msg.message.user.name}, "Not authorized.  Missing #{tfRole} role."

     dir = []
     for fn in fs.readdirSync(basepath)
       stat = fs.statSync("#{basepath}/#{fn}")
       dir.push fn if stat.isDirectory()

-    msg.reply dir.join "\n"
+    return msg.send {room: msg.message.user.name}, dir.join "\n"
+
+  robot.respond /tf (plan|refresh|apply|destroy) ([^\s]+)$/i, (msg) ->
+    unless robot.auth.isAdmin(msg.envelope.user) or robot.auth.hasRole(msg.envelope.user,tfRole)
+      return msg.send {room: msg.message.user.name}, "Not authorized.  Missing #{tfRole} role."
+
+    action = msg.match[1]
+    projname = msg.match[2].replace /\//, "_"
+    msg.send {room: msg.message.user.name}, projname
+
+    exec "cd #{basepath}/#{projname}; terraform #{action}", (error, stdout, stderr) ->
+      if error
+        msg.send {room: msg.message.user.name}, "Error: #{error}"
+      if stderr
+        msg.send {room: msg.message.user.name}, "stderr:\n```\n#{stderr}\n```"
+      if stdout
+        msg.send {room: msg.message.user.name}, "stdout:\n```\n#{stdout}\n```"


adding support for calling variadic functions with less than the requested number of arguments.

--- nodes.coffee
+++ nodes.coffee
@@ -773,8 +773,9 @@
       if param instanceof SplatNode and not splat?
         splat: param
         splat.index: i
+        splat.trailings: []
+        splat.arglength: @params.length
         @body.unshift(splat)
-        splat.trailings: []
       else if splat?
         splat.trailings.push(param)
       else
@@ -830,8 +831,11 @@
     o.scope.find name
     len: o.scope.free_variable()
     o.scope.assign len, "arguments.length"
-    for trailing, pos in @trailings
-      o.scope.assign(trailing.compile(o), "arguments[$len - ${@trailings.length + pos}]")
+    variadic: o.scope.free_variable()
+    o.scope.assign variadic, "$len >= $@arglength"
+    for trailing, idx in @trailings
+      pos: @trailings.length - idx
+      o.scope.assign(trailing.compile(o), "arguments[$variadic ? $len - $pos : ${@index + idx}]")
     "$name = ${utility('slice')}.call(arguments, $@index, $len - ${@trailings.length})"

   # A compiling a splat as a destructuring assignment means slicing arguments


eliminate unnecessary assignment

eliminate unnecessary assignment

there's no need to assign this entire if statement to condpart
when condpart is assigned at every endpoint

--- nodes.coffee
+++ nodes.coffee
@@ -659,7 +659,7 @@
     [lt, gt] = ["#{idx} <#{@equals}", "#{idx} >#{@equals}"]

     # Generate the condition.
-    condPart = if @stepNum
+    if @stepNum
       condPart = if +@stepNum > 0 then "#{lt} #{@toVar}" else "#{gt} #{@toVar}"
     else if known
       [from, to] = [+@fromNum, +@toNum]


clip vectors to extent

clip vectors to extent

--- src/ui.coffee
+++ src/ui.coffee
@@ -55,8 +55,10 @@
         return @stream.point(a, b)
   )

+  clip = d3.geo.clipExtent()
+
   path = d3.geo.path()
-      .projection(simplify)
+      .projection(stream: (s) -> simplify.stream(clip.stream(s)))

   svg = d3.select('body').append('svg')

@@ -104,14 +106,12 @@
     for ring in contourLayer
       continue if ring.properties.elevation % 300
       length = turf.lineDistance(ring, 'kilometers') * (s0 * sc) / PXKM
-      labelCount = Math.floor(length / 500)
-      for n in d3.range(0, labelCount)
-        contours.append('text')
-            .attr('class', 'contour-label')
-            .append('textPath')
-              .attr('xlink:href', "#contour-#{ring.id}")
-              .attr('startOffset', "#{(n + .5) / labelCount * 100}%")
-              .text(ring.properties.elevation)
+      contours.append('text')
+          .attr('class', 'contour-label')
+          .append('textPath')
+            .attr('xlink:href', "#contour-#{ring.id}")
+            .attr('startOffset', "50%")
+            .text(ring.properties.elevation)

   renderSymbols = ->
     symbols.selectAll('.symbol').remove()
@@ -177,6 +177,8 @@
     width = parseInt(d3.select('body').style('width'))
     height = parseInt(d3.select('body').style('height'))

+    clip.extent([[0, 0], [width, height]])
+
     projection.scale(s0 = d3.min([width / boxWidth, height / boxHeight]))
     f0 = (db.bbox[2] - db.bbox[0]) / boxWidth / s0


Better delete confirmation for Routes

Better delete confirmation for Routes

--- client/source/views/routes/list.coffee
+++ client/source/views/routes/list.coffee
@@ -36,8 +36,9 @@

   clickDelete: (e) ->
     e.preventDefault()
-    if confirm 'Are you sure?'
-      model = @collection.findWhere _id: @$(e.currentTarget).closest('.route').data('route-id')
+    model = @collection.findWhere _id: @$(e.currentTarget).closest('.route').data('route-id')
+    if model and confirm "Are you sure you want to delete “#{model.get('urlPattern')}”?"
+
       model.destroy(wait: yes).done ->
         toastr.success 'Route deleted'


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/metered.coffee
+++ src/metered.coffee
@@ -21,7 +21,7 @@
       if not I[name]
         I[name] = 100

-      if not I[max]
+      if not I[""]

       ratio = I[name] / I["max#{name.capitalize()}"]


fixing registerNatives metastack-breaking bug

--- src/methods.coffee
+++ src/methods.coffee
@@ -157,9 +157,9 @@
     padding = (' ' for _ in [2...runtime_state.meta_stack.length]).join('')
     console.log "#{padding}entering method #{sig}"
     if @access_flags.native
-      return if sig.indexOf('::registerNatives()V',1) >= 0  # we don't need to register native methods
-      throw "native method NYI: #{sig}" unless native_methods[sig]
-      native_methods[sig](runtime_state)
+      unless sig.indexOf('::registerNatives()V',1) >= 0  # we don't need to register native methods
+        throw "native method NYI: #{sig}" unless native_methods[sig]
+        native_methods[sig](runtime_state)
       s = runtime_state.meta_stack.pop().stack
       switch s.length
         when 2 then runtime_state.push s[0], s[1]


Try to get phoneconnection

Try to get phoneconnection

--- src/public/scripts/app.game.coffee
+++ src/public/scripts/app.game.coffee
@@ -69,6 +69,15 @@
 								)
 						when 'mobile'
 							@inputHandler.selectInput('mobile')
+							# Should be called when a user decides to connect his mobile phone
+							@createControllerNode()
+							console.log '/controller/' + @controllerNode.id
+							@controllerNode.on('peer.added', ( peer ) =>
+									console.log 'CONNECTED WITH PHONE'
+								)
+							@controllerNode.on('joined', ( peer ) =>
+									console.log 'JUST TESTING'
+								)
 				)

 			@container = document.createElement 'div'
@@ -146,9 +155,6 @@
 			window.requestAnimationFrame(@update)
 			$(window).resize(@setDimensions)

-			# Should be called when a user decides to connect his mobile phone
-			@createControllerNode()
-
 		# Sets the dimensions of the viewport and the aspect ration of the camera
 		#
 		# @return [[Integer, Integer]] a tuple of the width and height of the container


Tests: Helpers: Added setCookie helper.

Tests: Helpers: Added setCookie helper.

--- client/test/lib/helpers/helpers.coffee
+++ client/test/lib/helpers/helpers.coffee
@@ -481,6 +481,13 @@
       .pause                   5000

+  setCookie: (browser, name, value) ->
+
+    browser
+      .cookie    'POST', { name, value }
+      .refresh()
+
+
   getUrl: ->

     return 'http://dev.koding.com:8090'


Add delegator to Rectangle

Add delegator to Rectangle

--- src/rectangle.coffee
+++ src/rectangle.coffee
@@ -1,5 +1,6 @@
 {Emitter} = require 'event-kit'
 ObjectAssign = require 'object-assign'
+Delegator = require 'delegato'

 Transform = require './transform'
 Utils = require './utils'
@@ -76,6 +77,14 @@
 module.exports =
 class Rectangle
   Draggable.includeInto(this)
+  Delegator.includeInto(this)
+
+  @delegatesMethods 'on', toProperty: 'emitter'
+  @delegatesMethods 'getPathString',
+    'getPosition', 'setPosition'
+    'getSize', 'setSize'
+    'translate'
+    toProperty: 'model'

   constructor: (@svgDocument, options={}) ->
     @emitter = new Emitter
@@ -83,8 +92,6 @@
     @_setupSVGObject(options)
     @model.on 'change', @onModelChange
     @svgDocument.registerObject(this)
-
-  on: (args...) -> @emitter.on(args...)

   ###
   Section: Public Methods
@@ -95,16 +102,6 @@
   getID: -> "#{@getType()}-#{@id}"

   toString: -> @model.toString()
-
-  getPosition: -> @model.getPosition()
-
-  setPosition: (x, y) -> @model.setPosition(x, y)
-
-  getSize: -> @model.getSize()
-
-  setSize: (w, h) -> @model.setSize(w, h)
-
-  translate: (x, y) -> @model.translate(x, y)

   remove: ->
     @svgEl.remove()


add test for ENUM definition

add test for ENUM definition

--- spec/lib/model-props.coffee
+++ spec/lib/model-props.coffee
@@ -7,66 +7,151 @@

 describe 'ModelProps', ->

-    beforeEach ->
+    describe 'ENUM definition', ->

-        @facade = require('../create-facade').create()
+        it 'throws an error when first argument of @TYPES.ENUM is not an array', ->

-        class A extends Entity
-            @properties:
-                str        : @TYPES.STRING
-                num        : @TYPES.NUMBER
-                date       : @TYPES.DATE
-                createdAt  : @TYPES.CREATED_AT
-                createdAt2 : @TYPES.CREATED_AT
-                updatedAt  : @TYPES.UPDATED_AT
-                updatedAt2 : @TYPES.UPDATED_AT
-                b          : @TYPES.MODEL 'b'
-                c          : @TYPES.MODEL 'c'
-                bs         : @TYPES.MODEL 'b-list'
-                cs         : @TYPES.MODEL 'c-list'
-                en1        : @TYPES.ENUM ['NOT_DISTRIBUTED', 'DISTRIBUTED', 'COMPLETED']
-                en2        : @TYPES.ENUM ['a123', 'a234', 'a345'], 'a234'
-                en3        : @TYPES.ENUM ['Japan', 'Thai', 'Canada'], 2
+            facade = require('../create-facade').create()

-        class BList extends BaseList
-            @itemModelName: 'b'
+            class A extends Entity
+                @properties:
+                    country: @TYPES.ENUM { Japan: 1, Korea: 2, China: 3 }

-        class CList extends BaseList
-            @itemModelName: 'c'
+            facade.addClass('a', A)

-        @facade.addClass('a', A)
-        @facade.addClass('b', class B extends Entity)
-        @facade.addClass('c', class C extends ValueObject)
-        @facade.addClass('b-list', BList)
-        @facade.addClass('c-list', CList)
+            parseProps = ->
+                modelProps = new ModelProps('a', A.properties, facade.getModule())

-        @prop = @facade.getModel('a').properties
-
-        @modelProps = new ModelProps('a', A.properties, @facade.getModule())
+            assert.throws(parseProps, /Values must be an array\./)

-    it 'has dates collecting prop of DATE, CREATED_AT and UPDATED_AT', ->
-        assert.deepEqual @modelProps.dates, ['date', 'createdAt', 'createdAt2', 'updatedAt', 'updatedAt2']
+        it 'throws an error when first argument of @TYPES.ENUM is not an array of string', ->
+
+            facade = require('../create-facade').create()
+
+            class A extends Entity
+                @properties:
+                    country: @TYPES.ENUM [ 1, 2, 3 ]
+
+            facade.addClass('a', A)
+
+            parseProps = ->
+                modelProps = new ModelProps('a', A.properties, facade.getModule())
+
+            assert.throws(parseProps, /Values must be an array of string/)

-    it 'has createdAt whose value is CREATED_AT at last column', ->
-        assert @modelProps.createdAt is 'createdAt2'
+        it 'throws an error when first argument of @TYPES.ENUM contains duplicated entries', ->

-    it 'has updatedAt whose value is UPDATED_AT at last column', ->
-        assert @modelProps.updatedAt is 'updatedAt2'
+            facade = require('../create-facade').create()
+
+            class A extends Entity
+                @properties:
+                    country: @TYPES.ENUM [ 'Mexico', 'Jamaica', 'Mexico']
+
+            facade.addClass('a', A)
+
+            parseProps = ->
+                modelProps = new ModelProps('a', A.properties, facade.getModule())
+
+            assert.throws(parseProps, /Value 'Mexico' is duplicated\./)

-    it 'set enum default values', ->
-        a = @facade.createModel('a')
-        assert a.en1 is undefined
-        assert a.en2 is 1
-        assert a.en3 is 2
+        it 'throws an error when the default value of @TYPES.ENUM is invalid string', ->
+
+            facade = require('../create-facade').create()
+
+            class A extends Entity
+                @properties:
+                    country: @TYPES.ENUM [ 'Mexico', 'Jamaica', 'Japan'], 'The United States'
+
+            facade.addClass('a', A)
+
+            parseProps = ->
+                modelProps = new ModelProps('a', A.properties, facade.getModule())
+
+            assert.throws(parseProps, /Invalid default value 'The United States'/)

-    describe 'isEntity', ->
+        it 'throws an error when the default value of @TYPES.ENUM is invalid number', ->

-        it 'returns whether the prop is entity', ->
-            assert @modelProps.isEntity('b')
-            assert @modelProps.isEntity('c') is false
-            assert @modelProps.isEntity('date') is false
-            assert @modelProps.isEntity('xxx') is false
+            facade = require('../create-facade').create()
+
+            class A extends Entity
+                @properties:
+                    country: @TYPES.ENUM [ 'Mexico', 'Jamaica', 'Japan'], 3
+
+            facade.addClass('a', A)
+
+            parseProps = ->
+                modelProps = new ModelProps('a', A.properties, facade.getModule())
+
+            assert.throws(parseProps, /Invalid default value '3'/)
+
+
+
+    describe 'after parsing a valid model definition', ->
+
+        beforeEach ->
+
+            @facade = require('../create-facade').create()
+
+            class A extends Entity
+                @properties:
+                    str        : @TYPES.STRING
+                    num        : @TYPES.NUMBER
+                    date       : @TYPES.DATE
+                    createdAt  : @TYPES.CREATED_AT
+                    createdAt2 : @TYPES.CREATED_AT
+                    updatedAt  : @TYPES.UPDATED_AT
+                    updatedAt2 : @TYPES.UPDATED_AT
+                    b          : @TYPES.MODEL 'b'
+                    c          : @TYPES.MODEL 'c'
+                    bs         : @TYPES.MODEL 'b-list'
+                    cs         : @TYPES.MODEL 'c-list'
+                    en1        : @TYPES.ENUM ['NOT_DISTRIBUTED', 'DISTRIBUTED', 'COMPLETED']
+                    en2        : @TYPES.ENUM ['a123', 'a234', 'a345'], 'a234'
+                    en3        : @TYPES.ENUM ['Japan', 'Thai', 'Canada'], 2
+
+            class BList extends BaseList
+                @itemModelName: 'b'
+
+            class CList extends BaseList
+                @itemModelName: 'c'
+
+            @facade.addClass('a', A)
+            @facade.addClass('b', class B extends Entity)
+            @facade.addClass('c', class C extends ValueObject)
+            @facade.addClass('b-list', BList)
+            @facade.addClass('c-list', CList)
+
+            @prop = @facade.getModel('a').properties
+
+            @modelProps = new ModelProps('a', A.properties, @facade.getModule())
+
+
+        it 'has dates collecting prop of DATE, CREATED_AT and UPDATED_AT', ->
+            assert.deepEqual @modelProps.dates, ['date', 'createdAt', 'createdAt2', 'updatedAt', 'updatedAt2']
+
+
+        it 'has createdAt whose value is CREATED_AT at last column', ->
+            assert @modelProps.createdAt is 'createdAt2'
+
+        it 'has updatedAt whose value is UPDATED_AT at last column', ->
+            assert @modelProps.updatedAt is 'updatedAt2'
+
+
+        it 'set enum default values', ->
+            a = @facade.createModel('a')
+            assert a.en1 is undefined
+            assert a.en2 is 1
+            assert a.en3 is 2
+
+
+        describe 'isEntity', ->
+
+            it 'returns whether the prop is entity', ->
+                assert @modelProps.isEntity('b')
+                assert @modelProps.isEntity('c') is false
+                assert @modelProps.isEntity('date') is false
+                assert @modelProps.isEntity('xxx') is false


refreshColumns triggers admin:refresh_columns

--- app/assets/javascripts/ab_admin/core/columns_hider.js.coffee
+++ app/assets/javascripts/ab_admin/core/columns_hider.js.coffee
@@ -62,6 +62,7 @@
       @showAll()
       for i in @data[@collection_name]
         @hideByIndex(i + 1)
+      $(document).trigger('admin:refresh_columns')

   hideByIndex: (i) ->
     $("#list > thead > tr > th:nth-child(#{i}), #list > tbody > tr > td:nth-child(#{i})").hide()


Adjust spec to confirmation validator changes

--- spec/validators_spec.coffee
+++ spec/validators_spec.coffee
@@ -285,7 +285,7 @@
           "something else"
         else null
       v.validate @model
-      expect( @model.addError ).toHaveBeenCalledWith "foo", "confirmed"
+      expect( @model.addError ).toHaveBeenCalledWith "foo_confirmation", "confirmed"

     it "adds a validation that adds no error if the attribute is confirmed", ->
       v = new @V.ConfirmationValidator "foo"


Improved see more / fold in feature.

Improved see more / fold in feature.

--- app/assets/javascripts/app/controllers/ticket_zoom.js.coffee
+++ app/assets/javascripts/app/controllers/ticket_zoom.js.coffee
@@ -606,11 +606,14 @@

   show_toogle: (e) ->
     e.preventDefault()
-    $(e.target).hide()
+    #$(e.target).hide()
     if $(e.target).next('div')[0]
-      $(e.target).next('div').removeClass('hide')
-    else
-      $(e.target).parent().next('div').removeClass('hide')
+      if $(e.target).next('div').hasClass('hide')
+        $(e.target).next('div').removeClass('hide')
+        $(e.target).text( App.i18n.translateContent('Fold in') )
+      else
+        $(e.target).text( App.i18n.translateContent('See more') )
+        $(e.target).next('div').addClass('hide')

   checkIfSignatureIsNeeded: (article_type) =>

@@ -717,7 +720,7 @@
       @article_changed = false
       @article['html'] = @article['html'].replace /^-----SEEMORE-----\n/m, (match) =>
         @article_changed = true
-        notify + '
'
+        notify + '
'
       if @article_changed
         @article['html'] = @article['html'] + '
'

@@ -726,7 +729,7 @@
       @article_changed = false
       @article['html'] = @article['html'].replace /^\n{0,10}(--|__)/m, (match) =>
         @article_changed = true
-        notify + '
' + match
+        notify + '
' + match
       if @article_changed
         @article['html'] = @article['html'] + '
'


for #273 -  dont show pagination for a single page

--- app/assets/javascripts/modules/search/pagination_cell.coffee
+++ app/assets/javascripts/modules/search/pagination_cell.coffee
@@ -3,8 +3,7 @@
     return @paginationViewObject()

   paginationViewObject: =>
-    # If no results
-    unless @results.hits.length
+    if @results.nbPages < 2 # If not enough results for pagination
       return {}

     return pagination =


specs are now passing in chrome (firefox has less failures now as well -- only 1)

--- app/assets/utensils/directional/spec/directional_spec.coffee
+++ app/assets/utensils/directional/spec/directional_spec.coffee
@@ -18,7 +18,7 @@

   afterEach ->
     $('.demo').remove()
-    @dom.css(position:'static', top:"auto", left:"auto", bottom:"auto", right:"auto")
+    @dom.css(top:0, left:0, bottom:"auto", right:"auto")

   describe '#constructor', ->
@@ -33,12 +33,12 @@

   describe '#getPlacementAndConstrain', ->
     it 'wraps #getPlacementFromCardinal and #constrainToViewport methods and does not alter', ->
-      @dom.css(position:'absolute', top:200, left:0)
+      @dom.css(width:'auto', top:200, left:0)
       pos = @east_direc.getPlacementAndConstrain()
       expect(pos.cardinal).toEqual('east')

     it 'wraps #getPlacementFromCardinal and #constrainToViewport methods and alters', ->
-      @dom.css(position:'absolute', top:200, right:0)
+      @dom.css(width:'auto', top:200, right:0)
       pos = @east_direc.getPlacementAndConstrain()
       expect(pos.cardinal).toEqual('west')

@@ -74,13 +74,13 @@
   # postions are sometimes awkward
   describe '#constrainToViewport', ->
     it 'repositions the item on stage when north is offscreen', ->
-      @dom.css(position:'absolute', top:-20, left:'50%')
+      @dom.css(left:'50%')
       pos = @north_direc.getPlacementFromCardinal()
       suggested = @north_direc.constrainToViewport(pos)
       expect(suggested.cardinal).toEqual('south')

     it 'repositions the item on stage when south is offscreen', ->
-      @dom.css(position:'absolute', bottom:0, left:'50%')
+      @dom.css(width:'auto', top:'auto', bottom:-400000, left:'50%') # the bottom is set really low because when we're building the report it can be long
       pos = @south_direc.getPlacementFromCardinal()
       suggested = @south_direc.constrainToViewport(pos)
       expect(suggested.cardinal).toEqual('north')


removed debug logs

removed debug logs

Former-commit-id: b2f11870ebe47d0ef109618c9418f33bc57244b4 [formerly 19d2a91bd0b83b1999e9df729f90d1b0d0d9c6c5]
Former-commit-id: 34d704abf2837ff43d38183ee881b019aca0f317

--- src/menu/views/ImportMenu.coffee
+++ src/menu/views/ImportMenu.coffee
@@ -36,11 +36,8 @@
         zoomer.boxRectWidth = 2
         @model.reset []
         @g.zoomer.set zoomer
-        console.log seqs
-        console.log @g.columns
         @model.reset seqs
         @g.columns.calcConservation @model
-        console.log @g.columns.get "conserv"

     menuImport.addNode "CLUSTAL", =>
       url = prompt "URL", "/test/dummy/samples/p53.clustalo.clustal"
@@ -55,7 +52,6 @@
         zoomer.boxRectWidth = 2
         @model.reset []
         @g.zoomer.set zoomer
-        console.log seqs
         @model.reset seqs
         @g.columns.calcConservation @model


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/string_extensions.coffee
+++ src/string_extensions.coffee
@@ -132,7 +132,7 @@
     this.toString()

 ###*
-Returns true if this string starts with the passed in string
+Returns true if this string starts with the given string
 ###
 String::startsWith = (str) ->
   @lastIndexOf(str, 0) is 0


Fixing using parenthesis in conditions

Fixing using parenthesis in conditions


Change dev server to port 4000

Change dev server to port 4000

--- config.coffee
+++ config.coffee
@@ -52,7 +52,7 @@
             joinTo: 'javascripts/app.js'
     server:
         path: 'server.coffee'
-        port: 3333
+        port: 4000
         base: '/'
         app: 'express'
         debug: 'brunch:server'


only set status if status bar exists

only set status if status bar exists

--- lib/wakatime.coffee
+++ lib/wakatime.coffee
@@ -356,13 +356,13 @@
             title = 'Unknown Error (' + proc.exitCode + '); Check your Dev Console and ~/.wakatime.log for more info.'

           console.warn msg
-          statusBarTileView.setStatus(status)
-          statusBarTileView.setTitle(title)
+          statusBarTileView?.setStatus(status)
+          statusBarTileView?.setTitle(title)

         else
-          statusBarTileView.setStatus()
+          statusBarTileView?.setStatus()
           today = new Date()
-          statusBarTileView.setTitle('Last heartbeat sent ' + formatDate(today))
+          statusBarTileView?.setTitle('Last heartbeat sent ' + formatDate(today))
       )
       lastHeartbeat = time
       lastFile = file.path


Optional arguments for draw command

Optional arguments for draw command

--- grammars/tikz.cson
+++ grammars/tikz.cson
@@ -32,4 +32,33 @@
         begin: '(\\\\)draw\\b'
         end: '\\;'
         name: 'support.function.general.tikz'
+        patterns: [
+            {
+                include: '#optionalArgumentScope'
+            }
+        ]
     }
+    optionalArgumentScope: {
+        comment: 'Matches options brackets and everything inside them.'
+        contentName: 'variable.parameter.optional.latex'
+        begin: '\\['
+        beginCaptures:
+            0:
+                name: 'punctuation.definition.optional.begin.latex'
+        end: '\\]'
+        endCaptures:
+            0:
+                name: 'punctuation.definition.optional.end.latex'
+        # patterns: [
+        #     {
+        #         comment: 'AFAIK, the optional parameters never have nested brackets. Will come back to this after a while or if I find they do.'
+        #         include: '#commentMeta'
+        #     }
+        #     {
+        #         include: '#escapedCurlyBracket'
+        #     }
+        #     {
+        #         include: '#openCurlyBracketMeta'
+        #     }
+        # ]
+    }


config: Remove rtmpApplicationName

config: Remove rtmpApplicationName

rtmpApplicationName is no longer used as it is replaced by
liveApplicationName.

--- config.coffee
+++ config.coffee
@@ -49,8 +49,6 @@
   videoRTPServerPort : 7044  # even
   videoRTCPServerPort: 7045  # odd and contiguous

-  # RTMP stream name
-  rtmpApplicationName: 'live'
   liveApplicationName: 'live'

   # MP4 files in recordedDir will be accessible at


use python v3.5.2

use python v3.5.2

--- lib/wakatime.coffee
+++ lib/wakatime.coffee
@@ -260,7 +260,7 @@
     )

 installPython = (callback) ->
-  pyVer = '3.5.1'
+  pyVer = '3.5.2'
   arch = 'win32'
   if os.arch().indexOf('x64') > -1
     arch = 'amd64'


fingering selection filter accounts for barres

--- intervals.coffee
+++ intervals.coffee
@@ -28,7 +28,7 @@
   {name: 'Minor 7th', abbr: 'min7', pitch_classes: '037t'},
   {name: 'Dom 7 b5', abbr: '7b5', pitch_classes: '046t'},
   # following is also half-diminished 7th
-  {name: 'Min 7th b5', abbr: ['ø', 'Ø', 'm7b5'], pitch_classes: '036t'},
+  {name: 'Min 7th b5', abbrs: ['ø', 'Ø', 'm7b5'], pitch_classes: '036t'},
   {name: 'Dim Maj 7th', abbr: '°Maj7', pitch_classes: '036e'},
   {name: 'Min Maj 7th', abbrs: ['min/maj7', 'min(maj7)'], pitch_classes: '037e'},
   {name: '6th', abbrs: ['6', 'M6', 'M6', 'maj6'], pitch_classes: '0479'},
@@ -41,6 +41,9 @@
     chord.abbrs = chord.abbrs.split(/s/) if typeof chord.abbrs == 'string'
     chord.abbr ||= chord.abbrs[0]
     chord.pitch_classes = (keys[c] or parseInt(c, 10) for c in chord.pitch_classes)
+
+compute_chord_name = (chord_root, chord) ->
+  "#{NoteNames[chord_root]}#{chord.abbr}"

 interval_class_between = (pca, pcb) ->
   n = (pcb - pca) % 12
@@ -81,247 +84,6 @@
     return unless interval_class_between(root_note_number, pitch_number_for_position(finger_position)) == semitones
     positions.push finger_position
   return positions
-
-fingerings_for = (chord, root_note) ->
-  positions = do (positions=[]) ->
-    finger_positions_each (pos) ->
-      interval_class = interval_class_between(root_note, pitch_number_for_position(pos))
-      degree_index = chord.pitch_classes.indexOf(interval_class)
-      positions.push {string: pos.string, fret: pos.fret, degree_index} if degree_index >= 0
-    positions
-
-  frets_per_string = do (strings=([] for __ in OpenStringNoteNumbers)) ->
-    strings[position.string].push position for position in positions
-    strings
-
-  compute_choices = (by_string) ->
-    return [[]] unless by_string.length
-    frets = by_string[0]
-    remaining = compute_choices(by_string[1..])
-    return remaining.concat(([n].concat(right) \
-      for n in frets for right in remaining)...)
-
-  count_distinct_notes = (fs) ->
-    _.chain(fs).pluck('degree_index').uniq().value().length
-
-  chord_note_count = count_distinct_notes(positions)
-
-  has_all_notes = (fs) ->
-    return count_distinct_notes(fs) == chord_note_count
-
-  closed_medial_strings = (fs) ->
-    string_frets = (-1 for s in StringNumbers)
-    string_frets[pos.string] = pos.fret for pos in fs
-    p = ((if x >= 0 then x else 'x') for x in string_frets).join('')
-    return p.match(/\dx+\d/)
-
-  closed_treble_strings = (fs) ->
-    string_frets = (-1 for s in StringNumbers)
-    string_frets[pos.string] = pos.fret for pos in fs
-    p = ((if x >= 0 then x else 'x') for x in string_frets).join('')
-    return p.match(/x$/)
-
-  finger_count = (fs) ->
-    (f for f in fs when f.fret).length
-
-  high_note_count = (fs) -> -fs.length
-
-  is_first_position = (fs) ->
-    _(fs).sortBy((f) -> f.string)[0].degree_index == 0
-
-  cmp = (fn) -> (x...) -> !fn(x...)
-
-  filters = [
-    {name: 'has all chord notes', filter: has_all_notes},
-    {name: 'no muted medial strings', filter: cmp(closed_medial_strings)},
-    {name: 'no muted treble strings', filter: cmp(closed_treble_strings)}
-  ]
-
-  sorts = [
-    finger_count,
-    high_note_count,
-    cmp(is_first_position)
-  ]
-
-  chord_name = "#{NoteNames[root_note % 12]}#{chord.abbr}"
-  choices = compute_choices(frets_per_string)
-  for {name, filter} in filters
-    filtered = (choice for choice in choices when filter(choice))
-    unless filtered.length
-      console.error "#{chord_name}: no fingerings pass filter \"#{name}\""
-      filtered = choices
-    choices = filtered
-  for sort in sorts
-    choices = _(choices).sortBy(sort)
-  # for choice in choices
-  #   console.info finger_count(choice)
-  return choices
-
-best_fingering_for = (chord, root_note) ->
-  return fingerings_for(chord, root_note)[0]
-
-
-#
-# Drawing and Layout
-#
-
-BuildDirectory = __dirname + '/build/'
-DefaultFilename = null
-canvas = null
-ctx = null
-
-erase_background = ->
-  ctx.fillStyle = 'white'
-  ctx.fillRect 0, 0, canvas.width, canvas.height
-
-filename = (name) -> DefaultFilename = name
-
-save_canvas_to_png = (canvas, fname) ->
-  out = fs.createWriteStream(BuildDirectory + fname)
-  stream = canvas.pngStream()
-  stream.on 'data', (chunk) -> out.write(chunk)
-  stream.on 'end', () -> console.log "Saved #{fname}"
-
-mode = null
-pdf = false
-page = (width, height, draw_page) ->
-  return [width, height] if mode == 'measure'
-  canvas ||= new Canvas(width, height, pdf and 'pdf')
-  ctx = canvas.getContext('2d')
-  ctx.textDrawingMode = 'glyph' if pdf
-  erase_background()
-  draw_page ctx
-  unless pdf
-    filename = "#{DefaultFilename or 'test'}.png"
-    fs.writeFile BuildDirectory + filename, canvas.toBuffer()
-
-grid = ({cols, rows, cell_width, cell_height, header_height}, draw_page) ->
-  header_height ||= 0
-  page cols * cell_width, header_height + rows * cell_height, (ctx) ->
-    i = 0
-    draw_page (draw_cell) ->
-      return if i >= cols * rows
-      ctx.save()
-      ctx.translate (i % cols) * cell_width, header_height + Math.floor(i / cols) * cell_height
-      draw_cell()
-      ctx.restore()
-      i += 1
-
-book = (filename, options, draw_book) ->
-  draw_book = options if typeof options == 'function'
-  pdf = true
-  mode = 'draw'
-  page_limit = options.pages
-  page_count = 0
-  draw_book (draw_page) ->
-    return if page_limit and page_limit <= page_count
-    page_count += 1
-    draw_page()
-    ctx.addPage()
-  fs.writeFile BuildDirectory + filename + ".pdf", canvas.toBuffer()
-
-
-#
-# Layout Options
-#
-
-h_gutter = 10
-v_gutter = 10
-string_spacing = 20
-fret_width = 45
-fret_overhang = .3 * fret_width
-padded_fretboard_width = 2 * v_gutter + fret_width * FretCount + fret_overhang
-padded_fretboard_height = 2 * h_gutter + (StringCount - 1) * string_spacing
-
-
-#
-# Drawing Fretboard and Diagrams
-#
-
-draw_diagrams = true
-if draw_diagrams
-  string_spacing = 6
-  fret_height = 8
-  fret_overhang = 0
-  h_gutter = 5
-  v_gutter = 5
-  note_radius = 1
-  closed_string_fontsize = 4
-  padded_fretboard_width = 2 * h_gutter + (StringCount - 1) * string_spacing
-  padded_fretboard_height = 2 * v_gutter + fret_height * FretCount + fret_overhang
-  above_fretboard = fret_height
-
-if true
-  string_spacing = 12
-  fret_height = 16
-  note_radius = 3
-  closed_string_fontsize = 8
-  padded_fretboard_width = 2 * h_gutter + (StringCount - 1) * string_spacing
-  padded_fretboard_height = 2 * v_gutter + (fret_height + 2) * FretCount + fret_overhang
-
-draw_strings = ->
-  for i in StringNumbers
-    y = i * string_spacing + h_gutter
-    ctx.beginPath()
-    ctx.moveTo h_gutter, y
-    ctx.lineTo h_gutter + FretCount * fret_width + fret_overhang, y
-    ctx.lineWidth = 1
-    ctx.stroke()
-
-draw_frets = ->
-  for fret_number in FretNumbers
-    x = fret_number * fret_width + h_gutter
-    ctx.beginPath()
-    ctx.moveTo x, h_gutter
-    ctx.lineTo x, h_gutter + (StringCount - 1) * string_spacing
-    ctx.lineWidth = 3 if fret_number == 0
-    ctx.stroke()
-    ctx.lineWidth = 1
-
-draw_finger_position = ({string, fret}, options) ->
-  {is_root, color} = options || {}
-  x = h_gutter + (fret - 1) * fret_width + fret_width / 2
-  x = h_gutter if fret == 0
-  ctx.beginPath()
-  ctx.arc x, v_gutter + (6 - string) * string_spacing, 7, 0, 2 * Math.PI, false
-  ctx.fillStyle = 'red'
-  ctx.fillStyle = color or 'white' unless is_root
-  ctx.lineWidth = 2 unless is_root
-  ctx.fill()
-  ctx.stroke()
-  ctx.strokeStyle = 'black'
-  ctx.lineWidth = 1
-
-if draw_diagrams
-  draw_strings = ->
-    for i in StringNumbers
-      x = i * string_spacing + h_gutter
-      ctx.beginPath()
-      ctx.moveTo x, v_gutter + above_fretboard
-      ctx.lineTo x, v_gutter + above_fretboard + FretCount * fret_height + fret_overhang
-      ctx.stroke()
-
-  draw_frets = ->
-    for fret in FretNumbers
-      y = v_gutter + above_fretboard + fret * fret_height
-      ctx.beginPath()
-      ctx.moveTo v_gutter - 0.5, y
-      ctx.lineTo v_gutter + 0.5 + (StringCount - 1) * string_spacing, y
-      ctx.lineWidth = 3 if fret == 0
-      ctx.stroke()
-      ctx.lineWidth = 1
-
-  draw_finger_position = ({string, fret}, options={}) ->
-    {is_root, color} = options
-    y = v_gutter + above_fretboard + (fret - 1) * fret_height + fret_height / 2
-    ctx.fillStyle = color or (if is_root then 'red' else 'white')
-    ctx.strokeStyle = color or (if is_root then 'red' else 'black')
-    ctx.lineWidth = 1
-    ctx.beginPath()
-    ctx.arc h_gutter + string * string_spacing, y, note_radius, 0, 2 * Math.PI, false
-    ctx.fill() if fret or is_root
-    ctx.stroke()
-    ctx.strokeStyle = 'black'

 find_barres = (positions) ->
   fret_rows = for fn in FretNumbers
@@ -337,16 +99,298 @@
   barres = []
   for fp, fn in fret_rows
     continue if fn == 0
-    m = fp.match(/^[^x]*(x[\.x]+x\.+?)$/)
-    barres.push fret: fn, string: m[0].length - m[1].length, string_count: m[1].length if m
+    m = fp.match(/^[^x]*(x[\.x]+x\.*)$/)
+    continue unless m
+    barres.push
+      fret: fn
+      string: m[0].length - m[1].length
+      string_count: m[1].length
+      subsumption_count: m[1].match(/x/g).length
+  # console.info fret_rows.join("\n")
+  # console.info barres
   barres

+fingerings_for = (chord, root_note) ->
+  #
+  # Generate
+  #
+  positions = do (positions=[]) ->
+    finger_positions_each (pos) ->
+      interval_class = interval_class_between(root_note, pitch_number_for_position(pos))
+      degree_index = chord.pitch_classes.indexOf(interval_class)
+      positions.push {string: pos.string, fret: pos.fret, degree_index} if degree_index >= 0
+    positions
+
+  frets_per_string = do (strings=([] for __ in OpenStringNoteNumbers)) ->
+    strings[position.string].push position for position in positions
+    strings
+
+  collect_fingerings = (by_string) ->
+    return [[]] unless by_string.length
+    frets = by_string[0]
+    remaining = collect_fingerings(by_string[1..])
+    return remaining.concat(([n].concat(right) \
+      for n in frets for right in remaining)...)
+
+  generate_fingerings = -> collect_fingerings(frets_per_string)
+
+  count_distinct_notes = (fs) ->
+    _.chain(fs).pluck('degree_index').uniq().value().length
+
+  chord_note_count = count_distinct_notes(positions)
+
+  has_all_notes = (fs) ->
+    return count_distinct_notes(fs) == chord_note_count
+
+  closed_medial_strings = (fs) ->
+    string_frets = (-1 for s in StringNumbers)
+    string_frets[pos.string] = pos.fret for pos in fs
+    p = ((if x >= 0 then x else 'x') for x in string_frets).join('')
+    return p.match(/\dx+\d/)
+
+  closed_treble_strings = (fs) ->
+    string_frets = (-1 for s in StringNumbers)
+    string_frets[pos.string] = pos.fret for pos in fs
+    p = ((if x >= 0 then x else 'x') for x in string_frets).join('')
+    return p.match(/x$/)
+
+  finger_count = (fs) ->
+    (f for f in fs when f.fret).length
+
+  few_fingers = (fs) ->
+    barres = find_barres(fs)
+    n = (f for f in fs when f.fret).length
+    n -= barre.subsumption_count for barre in barres
+    return n <= 4
+
+  cmp = (fn) -> (x...) -> !fn(x...)
+
+  #
+  # Filter
+  #
+
+  filters = [
+    {name: 'has all chord notes', filter: has_all_notes},
+    {name: 'no muted medial strings', filter: cmp(closed_medial_strings)},
+    {name: 'no muted treble strings', filter: cmp(closed_treble_strings)},
+    {name: 'no more than four fingers', filter: few_fingers}
+  ]
+
+  filter_fingerings = (fingerings) ->
+    for {name, filter} in filters
+      filtered = (fingering for fingering in fingerings when filter(fingering))
+      unless filtered.length
+        console.error "#{chord_name}: no fingerings pass filter \"#{name}\""
+        filtered = fingerings
+      fingerings = filtered
+    return fingerings
+
+  #
+  # Sort
+  #
+
+  high_note_count = (fs) -> -fs.length
+
+  is_first_position = (fs) ->
+    _(fs).sortBy((f) -> f.string)[0].degree_index == 0
+
+  sorts = [
+    finger_count,
+    high_note_count,
+    cmp(is_first_position)
+  ]
+
+  sort_fingerings = (fingerings) ->
+    for sort in sorts
+      fingerings = _(fingerings).sortBy(sort)
+    return fingerings
+
+  #
+  # Generate, filter, and sort
+  #
+
+  chord_name = "#{NoteNames[root_note % 12]}#{chord.abbr}"
+  fingerings = generate_fingerings()
+  fingerings = filter_fingerings(fingerings)
+  fingerings = sort_fingerings(fingerings)
+
+  # for fingering in fingerings
+  #   few_fingers(fingering)
+  return fingerings
+
+best_fingering_for = (chord, root_note) ->
+  return fingerings_for(chord, root_note)[0]
+
+
+#
+# Drawing and Layout
+#
+
+BuildDirectory = __dirname + '/build/'
+DefaultFilename = null
+canvas = null
+ctx = null
+
+erase_background = ->
+  ctx.fillStyle = 'white'
+  ctx.fillRect 0, 0, canvas.width, canvas.height
+
+filename = (name) -> DefaultFilename = name
+
+save_canvas_to_png = (canvas, fname) ->
+  out = fs.createWriteStream(BuildDirectory + fname)
+  stream = canvas.pngStream()
+  stream.on 'data', (chunk) -> out.write(chunk)
+  stream.on 'end', () -> console.log "Saved #{fname}"
+
+mode = null
+pdf = false
+page = (width, height, draw_page) ->
+  return [width, height] if mode == 'measure'
+  canvas ||= new Canvas(width, height, pdf and 'pdf')
+  ctx = canvas.getContext('2d')
+  ctx.textDrawingMode = 'glyph' if pdf
+  erase_background()
+  draw_page ctx
+  unless pdf
+    filename = "#{DefaultFilename or 'test'}.png"
+    fs.writeFile BuildDirectory + filename, canvas.toBuffer()
+
+grid = ({cols, rows, cell_width, cell_height, header_height}, draw_page) ->
+  header_height ||= 0
+  page cols * cell_width, header_height + rows * cell_height, (ctx) ->
+    i = 0
+    draw_page (draw_cell) ->
+      return if i >= cols * rows
+      ctx.save()
+      ctx.translate (i % cols) * cell_width, header_height + Math.floor(i / cols) * cell_height
+      draw_cell()
+      ctx.restore()
+      i += 1
+
+book = (filename, options, draw_book) ->
+  draw_book = options if typeof options == 'function'
+  pdf = true
+  mode = 'draw'
+  page_limit = options.pages
+  page_count = 0
+  draw_book (draw_page) ->
+    return if page_limit and page_limit <= page_count
+    page_count += 1
+    draw_page()
+    ctx.addPage()
+  fs.writeFile BuildDirectory + filename + ".pdf", canvas.toBuffer()
+
+
+#
+# Layout Options
+#
+
+h_gutter = 10
+v_gutter = 10
+string_spacing = 20
+fret_width = 45
+fret_overhang = .3 * fret_width
+padded_fretboard_width = 2 * v_gutter + fret_width * FretCount + fret_overhang
+padded_fretboard_height = 2 * h_gutter + (StringCount - 1) * string_spacing
+
+
+#
+# Drawing Fretboard and Diagrams
+#
+
+draw_diagrams = true
+if draw_diagrams
+  string_spacing = 6
+  fret_height = 8
+  fret_overhang = 0
+  h_gutter = 5
+  v_gutter = 5
+  note_radius = 1
+  closed_string_fontsize = 4
+  padded_fretboard_width = 2 * h_gutter + (StringCount - 1) * string_spacing
+  padded_fretboard_height = 2 * v_gutter + fret_height * FretCount + fret_overhang
+  above_fretboard = fret_height
+
+if true
+  string_spacing = 12
+  fret_height = 16
+  note_radius = 3
+  closed_string_fontsize = 8
+  padded_fretboard_width = 2 * h_gutter + (StringCount - 1) * string_spacing
+  padded_fretboard_height = 2 * v_gutter + (fret_height + 2) * FretCount + fret_overhang
+
+draw_strings = ->
+  for i in StringNumbers
+    y = i * string_spacing + h_gutter
+    ctx.beginPath()
+    ctx.moveTo h_gutter, y
+    ctx.lineTo h_gutter + FretCount * fret_width + fret_overhang, y
+    ctx.lineWidth = 1
+    ctx.stroke()
+
+draw_frets = ->
+  for fret_number in FretNumbers
+    x = fret_number * fret_width + h_gutter
+    ctx.beginPath()
+    ctx.moveTo x, h_gutter
+    ctx.lineTo x, h_gutter + (StringCount - 1) * string_spacing
+    ctx.lineWidth = 3 if fret_number == 0
+    ctx.stroke()
+    ctx.lineWidth = 1
+
+draw_finger_position = ({string, fret}, options) ->
+  {is_root, color} = options || {}
+  x = h_gutter + (fret - 1) * fret_width + fret_width / 2
+  x = h_gutter if fret == 0
+  ctx.beginPath()
+  ctx.arc x, v_gutter + (6 - string) * string_spacing, 7, 0, 2 * Math.PI, false
+  ctx.fillStyle = 'red'
+  ctx.fillStyle = color or 'white' unless is_root
+  ctx.lineWidth = 2 unless is_root
+  ctx.fill()
+  ctx.stroke()
+  ctx.strokeStyle = 'black'
+  ctx.lineWidth = 1
+
+if draw_diagrams
+  draw_strings = ->
+    for i in StringNumbers
+      x = i * string_spacing + h_gutter
+      ctx.beginPath()
+      ctx.moveTo x, v_gutter + above_fretboard
+      ctx.lineTo x, v_gutter + above_fretboard + FretCount * fret_height + fret_overhang
+      ctx.stroke()
+
+  draw_frets = ->
+    for fret in FretNumbers
+      y = v_gutter + above_fretboard + fret * fret_height
+      ctx.beginPath()
+      ctx.moveTo v_gutter - 0.5, y
+      ctx.lineTo v_gutter + 0.5 + (StringCount - 1) * string_spacing, y
+      ctx.lineWidth = 3 if fret == 0
+      ctx.stroke()
+      ctx.lineWidth = 1
+
+  draw_finger_position = ({string, fret}, options={}) ->
+    {is_root, color} = options
+    y = v_gutter + above_fretboard + (fret - 1) * fret_height + fret_height / 2
+    ctx.fillStyle = color or (if is_root then 'red' else 'white')
+    ctx.strokeStyle = color or (if is_root then 'red' else 'black')
+    ctx.lineWidth = 1
+    ctx.beginPath()
+    ctx.arc h_gutter + string * string_spacing, y, note_radius, 0, 2 * Math.PI, false
+    ctx.fill() if fret or is_root
+    ctx.stroke()
+    ctx.strokeStyle = 'black'
+
 draw_fingerboard = (positions, options={}) ->
-  {barres} = options
+  {draw_barres} = options
   draw_strings()
   draw_frets()
-  if barres and (pos for pos in positions when pos.fret > 0).length > 4
-    for {fret, string, fret, string_count} in find_barres(positions)
+  if draw_barres and (pos for pos in positions when pos.fret > 0).length > 4
+    barres = find_barres(positions)
+    for {fret, string, fret, string_count} in barres
       y = v_gutter + above_fretboard + (fret - 1) * fret_height + fret_height / 2
       ctx.beginPath()
       ctx.arc h_gutter + string * string_spacing, y, string_spacing / 2, Math.PI/2, Math.PI*3/2, false
@@ -354,6 +398,7 @@
         , Math.PI*3/2, Math.PI/2, false
       ctx.fillStyle = 'rgba(0,0,0, 0.5)'
       ctx.fill()
+    # positions = remove_barre_fingerings(barres, positions)
   if positions
     fretted_strings = []
     for position in positions
@@ -452,15 +497,16 @@
       for _, semitones in Intervals
         page -> intervals_page semitones

-chord_fingerings_page = (chord, root_note) ->
-  fingerings = fingerings_for(chord, root_note)
-  filename "Fingerings"
+chord_fingerings_page = (chord, chord_root) ->
+  chord_root = NoteNames.indexOf(chord_root) if typeof chord_root == 'string'
+  fingerings = fingerings_for(chord, chord_root)
+  filename "Fingerings for #{compute_chord_name chord_root, chord}"
   grid cols: 10, rows: 10
   , cell_width: padded_fretboard_width + 10
   , cell_height: padded_fretboard_height + 5
   , (cell) ->
-    for fingering, i in fingerings
-      cell -> draw_fingerboard(fingering)
+    for positions in fingerings
+      cell -> draw_fingerboard positions, draw_barres: true

 draw_pitch_diagram = (pitch_classes, degree_colors) ->
   r = 10
@@ -522,7 +568,7 @@
     pitches = ((i * 5 + 3) % 12 for i in [0...12])
     for pitch in pitches
       root_fingering = pitch_fingers[pitch]
-      chord_name = "#{NoteNames[pitch]}#{chord.abbr}"
+      chord_name = compute_chord_name pitch, chord
       continue if options.only unless chord_name == options.only
       cell ->
         ctx.font = '7pt Times'
@@ -536,7 +582,7 @@
         fingering = best_fingering_for(chord, pitch) if best_fingering

         position.color = degree_colors[position.degree_index] for position in fingering
-        draw_fingerboard fingering, barres: best_fingering
+        draw_fingerboard fingering, draw_barres: best_fingering

 chord_book = (options) ->
   page_count = options.pages
@@ -548,5 +594,5 @@
 # chord_page Chords[0], best_fingering: true
 # intervals_book by_root: true
 # intervals_book by_root: false
-chord_book best_fingering: 1, pages: 1#, only: 'G#'
-# chord_fingerings_page Chords[0], 44 + 3
+chord_book best_fingering: 1, pages: 0#, only: 'G#'
+# chord_fingerings_page Chords[6], 'F'


Update usage statistics

Update usage statistics