Clear code in dismiss handlers

Clear code in dismiss handlers

--- scripts/themis.coffee
+++ scripts/themis.coffee
@@ -201,12 +201,7 @@
                     navigationBar.present()

         dismiss: ->
-            $main = $ '#main'
-            $form = $main.find 'form.themis-form-signup'
-            if $form.length > 0
-                $form.find('input:file').filestyle 'destroy'
-                $form.off 'submit'
-            $main.html ''
+            $('#main').html ''
             navigationBar.dismiss()

     new SignupView()
@@ -261,11 +256,7 @@
                     navigationBar.present()

         dismiss: ->
-            $main = $ '#main'
-            $form = $main.find 'form.themis-form-signin'
-            if $form.length > 0
-                $form.off 'submit'
-            $main.html ''
+            $('#main').html ''
             navigationBar.dismiss()

     new SigninView()
@@ -318,11 +309,7 @@
                     navigationBar.present()

         dismiss: ->
-            $main = $ '#main'
-            $form = $main.find 'form.themis-form-login'
-            if $form.length > 0
-                $form.off 'submit'
-            $main.html ''
+            $('#main').html ''
             navigationBar.dismiss()

     new LoginView()
@@ -563,46 +550,7 @@
                                         $changePasswordModal.modal 'show'

         dismiss: ->
-            $main = $ '#main'
-            $buttonChangePassword = $main.find 'button[data-action="change-password"]'
-            if $buttonChangePassword.length
-                $buttonChangePassword.off 'click'
-                $changePasswordModal = $ '#change-password-modal'
-                $changePasswordForm = $changePasswordModal.find 'form'
-                $changePasswordForm.off 'submit'
-                $changePasswordSubmitButton = $changePasswordModal.find 'button[data-action="complete-change-password"]'
-                $changePasswordSubmitButton.off 'click'
-
-            $buttonEditProfile = $main.find 'button[data-action="edit-profile"]'
-            if $buttonEditProfile.length
-                $buttonEditProfile.off 'click'
-                $editProfileModal = $ '#edit-profile-modal'
-                $editProfileForm = $editProfileModal.find 'form'
-                $editProfileForm.off 'submit'
-                $editProfileSubmitButton = $editProfileModal.find 'button[data-action="complete-edit-profile"]'
-                $editProfileSubmitButton.off 'click'
-
-
-            $buttonChangeEmail = $main.find 'button[data-action="change-email"]'
-            if $buttonChangeEmail.length
-                $buttonChangeEmail.off 'click'
-                $changeEmailModal = $ '#change-email-modal'
-                $changeEmailForm = $changeEmailModal.find 'form'
-                $changeEmailForm.off 'submit'
-                $changeEmailSubmitButton = $changeEmailModal.find 'button[data-action="complete-change-email"]'
-                $changeEmailSubmitButton.off 'click'
-
-            $buttonResendConfirmation = $main.find 'button[data-action="resend-confirmation"]'
-            if $buttonResendConfirmation.length
-                $buttonResendConfirmation.off 'click'
-                $resendConfirmationModal = $ '#resend-confirmation-modal'
-                $resendConfirmationForm = $resendConfirmationModal.find 'form'
-                $resendConfirmationForm.off 'submit'
-                $resendConfirmationSubmitButton = $resendConfirmationModal.find 'button[data-action="complete-resend-confirmation"]'
-                $resendConfirmationSubmitButton.off 'click'
-
-
-            $main.html ''
+            $('#main').html ''
             navigationBar.dismiss()

     new ProfileView()
@@ -640,8 +588,7 @@
                             $result.addClass('text-success').text 'Email verified! Thank you!'

         dismiss: ->
-            $main = $ '#main'
-            $main.html ''
+            $('#main').html ''
             navigationBar.dismiss()

     new VerifyEmailView()
@@ -774,10 +721,7 @@
                             console.log errorThrown

         dismiss: ->
-            $navbar = $ '#themis-navbar'
-            $signout = $navbar.find 'a[data-action="signout"]'
-            $signout.off 'click'
-            $navbar.html ''
+            $('#themis-navbar').html ''

     new NavigationBar()


:minibus: Trigger views.render instead of view.render

--- lib/editor-linter.coffee
+++ lib/editor-linter.coffee
@@ -59,7 +59,7 @@
         else @messages.set linter, results

         @emitter.emit 'did-update'
-        @linter.view.render() if @editor is @linter.activeEditor
+        @linter.views.render() if @editor is @linter.activeEditor

   # This method sets or gets the lock status of given type
   _lock: (wasTriggeredOnChange, value)->


fix markdown rendering

fix markdown rendering

in case we ever want that

--- lib/editor/result.coffee
+++ lib/editor/result.coffee
@@ -10,14 +10,15 @@
 # ```
 # where `ed` is the current text editor and `range` is a line range compatible array,
 # e.g. `[3, 4]`. `options` is an object with the mandatory field
-#   `content`: DOM-node that will be diplayed inside of the `Result`.
+# - `content`: DOM-node that will be diplayed inside of the `Result`.
+#
 # and the optional fields
 # - `error`: Default `false`. If true, adds the `error`-style to the `Result`.
 # - `fade`:  Default `false`. If true, the `Result` will fade in on creation.
 # - `type`:  Default `inline`, can also be `block`. Inline-`Result`s will be
-#            displayed after the end of the last line contained in `range`,
-#            whereas block-`Result`s will be displayed below it and span the
-#            whole width of the current editor.
+# displayed after the end of the last line contained in `range`, whereas
+# block-`Result`s will be displayed below it and span the whole width of
+# the current editor.
 #
 # #### Static Methods
 # There are also some static methods provided to deal with `Result`s without


Fix bug with last commit

Fix bug with last commit

--- src/util.coffee
+++ src/util.coffee
@@ -48,7 +48,7 @@
           new File(bufferOrFileOrString)
     dir =
       if Util.isDirectory(file)
-        file
+        new Directory(file.getPath())
       else
         file?.getParent?() ? Util.getRootDirFallback file
     dir = findProjectRoot(dir, dirHasCabalFile) ? findProjectRoot(dir, dirHasSandboxFile)


add new way to close drivers

add new way to close drivers

--- src/viff.coffee
+++ src/viff.coffee
@@ -103,13 +103,16 @@
               iterator.next()
     , ->
       endTime = Date.now() - start
-      # that.drivers[browser].quit() for browser in browsers
+      that.closeDrivers()
       that.emit 'after', cases, endTime

       defer.resolve cases, endTime
     ).start()

     defer.promise()
+
+  closeDrivers: () ->
+    @drivers[browser].quit() for browser of @drivers

   @getPathKey: (url) ->
     [path, selector, preHandle, description] = Viff.parseUrl url


show nominations sort for pending beatmaps only

--- resources/assets/coffee/react/beatmaps/search-sort.coffee
+++ resources/assets/coffee/react/beatmaps/search-sort.coffee
@@ -45,12 +45,14 @@

     if @props.filters.status in ['graveyard', 'pending']
       fields.updated = true
-      fields.nominations = true
     else if @props.filters.status in ['any', 'favourites', 'mine']
       fields.updated = true
       fields.ranked = true
     else
       fields.ranked = true
+
+    if @props.filters.status == 'pending'
+      fields.nominations = true

     field for own field, enabled of fields when enabled


fix debug error

fix debug error

--- packages/rocketchat-ui-sidenav/side-nav/chatRoomItem.coffee
+++ packages/rocketchat-ui-sidenav/side-nav/chatRoomItem.coffee
@@ -12,9 +12,8 @@
 		return 'status-' + (Session.get('user_' + this.name + '_status') or 'offline')

 	name: ->
-		project_info = ChatRoom.findOne(this.rid).details
-		console.log this.rid
-		return project_info.name
+		project_info = ChatRoom.findOne(this.rid)?.details
+		return project_info?.name

 	roomIcon: ->
 		return RocketChat.roomTypes.getIcon this.t


Fix and close #59

Fix and close #59

--- lib/linter-pylama.coffee
+++ lib/linter-pylama.coffee
@@ -30,7 +30,7 @@
     (executablePath) =>
       if @executablePath
         @executablePath = executablePath
-        do initPylama
+        do @initPylama
       else
         @executablePath = executablePath


consider 403 and 404 as NotFound errors

consider 403 and 404 as NotFound errors

--- services/filestore/app/coffee/S3PersistorManager.coffee
+++ services/filestore/app/coffee/S3PersistorManager.coffee
@@ -74,7 +74,9 @@
 		s3Stream = s3Client.get(key, headers)
 		s3Stream.end()
 		s3Stream.on 'response', (res) ->
-			if res.statusCode == 404
+			if res.statusCode in [403, 404]
+				# S3 returns a 403 instead of a 404 when the user doesn't have
+				# permission to list the bucket contents.
 				logger.log bucketName:bucketName, key:key, "file not found in s3"
 				return callback new Errors.NotFoundError("File not found in S3: #{bucketName}:#{key}"), null
 			if res.statusCode not in [200, 206]


Control the visibility of the custom toggler when panes are hidden externally.

--- services/web/public/coffee/ide/directives/layout.coffee
+++ services/web/public/coffee/ide/directives/layout.coffee
@@ -119,6 +119,7 @@
 						customTogglerScope = scope.$new()

 						customTogglerScope.isOpen = true
+						customTogglerScope.isVisible = true

 						if state[customTogglerPane]?.initClosed == true
 							customTogglerScope.isOpen = false
@@ -132,6 +133,7 @@
 							repositionCustomToggler()
 						customTogglerEl = $compile("
 							<a href
+							   ng-show=\"isVisible\"
 							   class=\"custom-toggler #{ 'custom-toggler-' + customTogglerPane }\"
 							   ng-class=\"isOpen ? 'custom-toggler-open' : 'custom-toggler-closed'\"
 							   tooltip=\"{{ isOpen ? tooltipMsgWhenOpen : tooltipMsgWhenClosed }}\"
@@ -186,6 +188,11 @@
 								element.layout().hide("east")
 							else
 								element.layout().show("east")
+							if hasCustomToggler
+								customTogglerEl.scope().$applyAsync () ->
+									customTogglerEl.scope().isOpen = !value
+									customTogglerEl.scope().isVisible = !value
+

 				post: (scope, element, attrs) ->
 					name = attrs.layout


Remove promise wrapping for private ensureSupervisorCompatibility

Change-type: patch

--- lib/models/device.coffee
+++ lib/models/device.coffee
@@ -132,26 +132,18 @@
 	# @name ensureSupervisorCompatibility
 	# @private
 	# @function
-	# @memberof balena.models.device
 	#
 	# @param {String} version - version under check
 	# @param {String} minVersion - minimum accepted version
-	# @fulfil {} - is compatible
-	# @reject {Error} Will reject if the given version is < than the given minimum version
-	# @returns {Promise}
-	#
-	# @example
-	# balena.models.device.ensureSupervisorCompatibility(version, MIN_VERSION).then(function() {
-	# 	console.log('Is compatible');
-	# });
-	#
-	# @example
-	# balena.models.device.ensureSupervisorCompatibility(version, MIN_VERSION, function(error) {
-	# 	if (error) throw error;
-	# 	console.log('Is compatible');
-	# });
-	###
-	ensureSupervisorCompatibility = Promise.method (version, minVersion) ->
+	# @throws {Error} Will reject if the given version is < than the given minimum version
+	# @returns {void}
+	#
+	# @example
+	# ensureSupervisorCompatibility(version, MIN_VERSION)
+	# console.log('Is compatible');
+	#
+	###
+	ensureSupervisorCompatibility = (version, minVersion) ->
 		if semver.lt(version, minVersion)
 			throw new Error("Incompatible supervisor version: #{version} - must be >= #{minVersion}")

@@ -563,18 +555,18 @@
 			$select: ['id', 'supervisor_version']
 			$expand: belongs_to__application: $select: 'id'
 		.then (device) ->
-			ensureSupervisorCompatibility(device.supervisor_version, MIN_SUPERVISOR_APPS_API).then ->
-				appId = device.belongs_to__application[0].id
-				return request.send
-					method: 'POST'
-					url: "/supervisor/v1/apps/#{appId}"
-					baseUrl: apiUrl
-					body:
-						deviceId: device.id
-						appId: appId
-						method: 'GET'
-			.get('body')
-			.asCallback(callback)
+			ensureSupervisorCompatibility(device.supervisor_version, MIN_SUPERVISOR_APPS_API)
+			appId = device.belongs_to__application[0].id
+			return request.send
+				method: 'POST'
+				url: "/supervisor/v1/apps/#{appId}"
+				baseUrl: apiUrl
+				body:
+					deviceId: device.id
+					appId: appId
+					method: 'GET'
+		.get('body')
+		.asCallback(callback)

 	###*
 	# @summary Check if a device exists
@@ -964,16 +956,16 @@
 			$select: ['id', 'supervisor_version']
 			$expand: belongs_to__application: $select: 'id'
 		.then (device) ->
-			ensureSupervisorCompatibility(device.supervisor_version, MIN_SUPERVISOR_APPS_API).then ->
-				appId = device.belongs_to__application[0].id
-				return request.send
-					method: 'POST'
-					url: "/supervisor/v1/apps/#{appId}/start"
-					baseUrl: apiUrl
-					body:
-						deviceId: device.id
-						appId: appId
-					timeout: CONTAINER_ACTION_ENDPOINT_TIMEOUT
+			ensureSupervisorCompatibility(device.supervisor_version, MIN_SUPERVISOR_APPS_API)
+			appId = device.belongs_to__application[0].id
+			return request.send
+				method: 'POST'
+				url: "/supervisor/v1/apps/#{appId}/start"
+				baseUrl: apiUrl
+				body:
+					deviceId: device.id
+					appId: appId
+				timeout: CONTAINER_ACTION_ENDPOINT_TIMEOUT
 		.get('body')
 		.get('containerId')
 		.asCallback(callback)
@@ -1014,16 +1006,16 @@
 			$select: ['id', 'supervisor_version']
 			$expand: belongs_to__application: $select: 'id'
 		.then (device) ->
-			ensureSupervisorCompatibility(device.supervisor_version, MIN_SUPERVISOR_APPS_API).then ->
-				appId = device.belongs_to__application[0].id
-				return request.send
-					method: 'POST'
-					url: "/supervisor/v1/apps/#{appId}/stop"
-					baseUrl: apiUrl
-					body:
-						deviceId: device.id
-						appId: appId
-					timeout: CONTAINER_ACTION_ENDPOINT_TIMEOUT
+			ensureSupervisorCompatibility(device.supervisor_version, MIN_SUPERVISOR_APPS_API)
+			appId = device.belongs_to__application[0].id
+			return request.send
+				method: 'POST'
+				url: "/supervisor/v1/apps/#{appId}/stop"
+				baseUrl: apiUrl
+				body:
+					deviceId: device.id
+					appId: appId
+				timeout: CONTAINER_ACTION_ENDPOINT_TIMEOUT
 		.get('body')
 		.get('containerId')
 		.asCallback(callback)
@@ -1100,20 +1092,20 @@
 			ensureSupervisorCompatibility(
 				device.supervisor_version,
 				MIN_SUPERVISOR_MC_API
-			).then ->
-				appId = device.belongs_to__application[0].id
-				return request.send
-					method: 'POST'
-					url: "/supervisor/v2/applications/#{appId}/start-service"
-					baseUrl: apiUrl
-					body:
-						deviceId: device.id
-						appId: appId
-						data: {
-							appId
-							imageId
-						}
-					timeout: CONTAINER_ACTION_ENDPOINT_TIMEOUT
+			)
+			appId = device.belongs_to__application[0].id
+			return request.send
+				method: 'POST'
+				url: "/supervisor/v2/applications/#{appId}/start-service"
+				baseUrl: apiUrl
+				body:
+					deviceId: device.id
+					appId: appId
+					data: {
+						appId
+						imageId
+					}
+				timeout: CONTAINER_ACTION_ENDPOINT_TIMEOUT
 		.asCallback(callback)

 	###*
@@ -1151,20 +1143,20 @@
 			ensureSupervisorCompatibility(
 				device.supervisor_version,
 				MIN_SUPERVISOR_MC_API
-			).then ->
-				appId = device.belongs_to__application[0].id
-				return request.send
-					method: 'POST'
-					url: "/supervisor/v2/applications/#{appId}/stop-service"
-					baseUrl: apiUrl
-					body:
-						deviceId: device.id
-						appId: appId
-						data: {
-							appId
-							imageId
-						}
-					timeout: CONTAINER_ACTION_ENDPOINT_TIMEOUT
+			)
+			appId = device.belongs_to__application[0].id
+			return request.send
+				method: 'POST'
+				url: "/supervisor/v2/applications/#{appId}/stop-service"
+				baseUrl: apiUrl
+				body:
+					deviceId: device.id
+					appId: appId
+					data: {
+						appId
+						imageId
+					}
+				timeout: CONTAINER_ACTION_ENDPOINT_TIMEOUT
 		.asCallback(callback)

 	###*
@@ -1202,20 +1194,20 @@
 			ensureSupervisorCompatibility(
 				device.supervisor_version,
 				MIN_SUPERVISOR_MC_API
-			).then ->
-				appId = device.belongs_to__application[0].id
-				return request.send
-					method: 'POST'
-					url: "/supervisor/v2/applications/#{appId}/restart-service"
-					baseUrl: apiUrl
-					body:
-						deviceId: device.id
-						appId: appId
-						data: {
-							appId
-							imageId
-						}
-					timeout: CONTAINER_ACTION_ENDPOINT_TIMEOUT
+			)
+			appId = device.belongs_to__application[0].id
+			return request.send
+				method: 'POST'
+				url: "/supervisor/v2/applications/#{appId}/restart-service"
+				baseUrl: apiUrl
+				body:
+					deviceId: device.id
+					appId: appId
+					data: {
+						appId
+						imageId
+					}
+				timeout: CONTAINER_ACTION_ENDPOINT_TIMEOUT
 		.asCallback(callback)

 	###*


opt: trace format

opt: trace format

--- src/yaku.coffee
+++ src/yaku.coffee
@@ -231,18 +231,19 @@

 		stackStr = '\n' + stackInfo.join('\n')

-		if typeof __filename == 'string'
-			stackStr = stackStr.replace ///.+#{__filename}.+\n?///g, ''
+		clean = (stack) ->
+			if typeof __filename == 'string'
+				stack.replace ///.+#{__filename}.+\n?///g, ''

 		console.error 'Unhandled Rejection:', (
 			if reason
 				if reason.stack
-					reason.stack.trim()
+					clean reason.stack.trim()
 				else
 					reason
 			else
 				reason
-		), stackStr
+		), clean(stackStr)

 		stackInfo.unshift reason
 		return stackInfo


Remove Java from the arena language options (#5158)

--- app/views/ladder/LadderPlayModal.coffee
+++ app/views/ladder/LadderPlayModal.coffee
@@ -36,7 +36,8 @@
       {id: 'javascript', name: 'JavaScript'}
       {id: 'coffeescript', name: 'CoffeeScript (Experimental)'}
       {id: 'lua', name: 'Lua'}
-      {id: 'java', name: 'Java'}
+      # TODO: Bring java back once it's supported
+      # {id: 'java', name: 'Java'}
     ]
     @myName = me.get('name') || 'Newcomer'


json answer of languagetool might be received in multiple chunks

--- lib/linter-provider.coffee
+++ lib/linter-provider.coffee
@@ -21,6 +21,7 @@

   lint: (TextEditor) ->
     new Promise (Resolve) ->
+      received = ""
       toReturn = []

       editorPath = TextEditor.getPath()
@@ -58,7 +59,9 @@

       req = http.request options, (res) ->
         res.on 'data', (chunk) ->
-          jsonObject = JSON.parse chunk
+          received = received + chunk
+        res.on 'end', () ->
+          jsonObject = JSON.parse received
           matches = jsonObject["matches"]
           for match in matches
             offset = match['offset']
@@ -73,5 +76,6 @@
               severity: 'error'
             }
           Resolve toReturn
+
       req.write(post_data)
       req.end()


a simple implementation of commands to create criticmarkup tags

i think it is more like a draft of a possible implementation

--- lib/markdown-writer.coffee
+++ lib/markdown-writer.coffee
@@ -44,7 +44,7 @@
       editorCommands["markdown-writer:insert-#{media}"] =
         @registerView("./views/insert-#{media}-view")

-    ["code", "codeblock", "bold", "italic", "strikethrough", "keystroke","deletion","addition","substitution","comment","highlight"].forEach (style) =>
+    ["code", "codeblock", "bold", "italic", "strikethrough", "keystroke", "deletion", "addition", "substitution", "comment", "highlight"].forEach (style) =>
       editorCommands["markdown-writer:toggle-#{style}-text"] =
         @registerCommand("./commands/style-text", args: style)


remove unused xapptoken

remove unused xapptoken

--- lib/middleware/auth.coffee
+++ lib/middleware/auth.coffee
@@ -7,6 +7,7 @@
 _ = require 'underscore'

 module.exports = (p_req, res, next) ->
+  console.log 'editing request'

   Backbone.sync.editRequest = (req) ->
     if p_req.user?


fix crash with referencing undefined cookie array when running phantomjs directly

--- lib/mocha-phantomjs.coffee
+++ lib/mocha-phantomjs.coffee
@@ -38,7 +38,7 @@
     @page = webpage.create
       settings: @config.settings
     @page.customHeaders = @config.headers if @config.headers
-    @page.addCookie(cookie) for cookie in @config.cookies
+    @page.addCookie(cookie) for cookie in @config.cookies or []
     @page.viewportSize = @config.viewportSize if @config.viewportSize
     @page.onConsoleMessage = (msg) -> system.stdout.writeLine(msg)
     @page.onError = (msg, traces) =>


Don't start transactions for repeated changes

--- lib/operators/input.coffee
+++ lib/operators/input.coffee
@@ -36,7 +36,9 @@
   #
   # Returns nothing.
   execute: (count=1) ->
-    @editor.beginTransaction()
+    # If we've typed, we're being repeated. If we're being repeated,
+    # undo transactions are already handled.
+    @editor.beginTransaction() unless @typed
     operator = new Delete(@editor, @vimState, allowEOL: true, selectOptions: {excludeWhitespace: true})
     operator.compose(@motion)
     operator.execute(count)


Fix typo to actually load outdated packages

--- lib/package-manager.coffee
+++ lib/package-manager.coffee
@@ -171,7 +171,7 @@

   getOutdated: ->
     new Promise (resolve, reject) =>
-      @loadInstalled (error, result) ->
+      @loadOutdated (error, result) ->
         if error
           reject(error)
         else


add testing helpers

add testing helpers

--- app/coffee/tests/tests.coffee
+++ app/coffee/tests/tests.coffee
@@ -103,11 +103,47 @@

 # ============================================================================

+module "helper function for testing"
+
+boardFromChars = (chars2d) ->
+  "Produce a board state from a character-based shorthand.
+
+  eg. ['.b.', 'bw.', '.b.']"
+  for row in chars2d
+    for char in row
+      switch char
+        when 'b' then 'black'
+        when 'w' then 'white'
+        else 'empty'
+
+test "test of testing helper function boardFromChars", (assert) ->
+  assert.deepEqual (boardFromChars ['.b.', 'bw.', '.b.']),
+                   [['empty', 'black', 'empty']
+                    ['black', 'white', 'empty']
+                    ['empty', 'black', 'empty']]
+
+# ============================================================================

 module "common game page functions",
   setup: ->
     setInputSgf ''
     tesuji_charm.game_common.initialize()
+
+QUnit.assert.boardState = (chars2d, message) ->
+  expected = boardFromChars chars2d
+  actual = tesuji_charm.game_common.readBoardState()
+  result = true
+  for row, j in expected
+    for point, i in row
+      (result = false) if actual[j][i] != point
+  @push result, actual, expected, (message or "")
+
+test "helper function readBoardState and assert.boardState", (assert) ->
+  setInputSgf '(;SZ[3];B[ca];W[bc])'
+  tesuji_charm.game_common.initialize()
+  assert.boardState ['..b'
+                     '...'
+                     '.w.']

 test "initialize creates board from SGF data", (assert) ->
@@ -126,10 +162,9 @@
   # array for one and a single value for the other
   setInputSgf '(;SZ[3];AB[ba][ab][bc]AW[bb])'
   tesuji_charm.game_common.initialize()
-  expected = [ [ 'empty', 'black', 'empty' ]
-               [ 'black', 'white', 'empty' ]
-               [ 'empty', 'black', 'empty' ] ]
-  assert.deepEqual tesuji_charm.game_common.readBoardState(), expected
+  assert.boardState ['.b.'
+                     'bw.'
+                     '.b.']

 test "prisoner counts set from SGF", (assert) ->
   testSgf = (sgf, black, white) ->
@@ -144,14 +179,6 @@
     "only one black prisoner element"
   testSgf '(;SZ[3];AW[ab]B[aa];W[ba])', 1, 0
   testSgf '(;SZ[3];AW[aa]AB[ba]B[ab])', 0, 1
-
-test "helper function readBoardState", (assert) ->
-  setInputSgf '(;SZ[3];B[ca];W[bc])'
-  tesuji_charm.game_common.initialize()
-  expected = [ [ 'empty', 'empty', 'black' ]
-               [ 'empty', 'empty', 'empty' ]
-               [ 'empty', 'white', 'empty' ] ]
-  assert.deepEqual tesuji_charm.game_common.readBoardState(), expected

 # ============================================================================


Bug fix

Bug fix

--- lib/relations/one.coffee
+++ lib/relations/one.coffee
@@ -108,14 +108,12 @@

     # belongs to, update the model
     if @type is 'belongsTo'
-      if model.isLoaded(@key)
-        model.save {}, bbCallback callback
-      else
-        @model_type.cursor({id: model.id, $one: true}).toJSON (err, model_json) =>
-          return callback(err) if err
-          return callback(new Error "Failed to fetch model with id: #{model.id}") unless model_json
-          model_json[@foreign_key] = related_id
-          Utils.modelJSONSave(model_json, @model_type, callback)
+      # need to fetch or cases where just setting the id could delete data and _rev could be out of date - TODO: write an incremental insert
+      @model_type.cursor({id: model.id, $one: true}).toJSON (err, model_json) =>
+        return callback(err) if err
+        return callback(new Error "Failed to fetch model with id: #{model.id}") unless model_json
+        model_json[@foreign_key] = related_id
+        model.save model_json, bbCallback callback

     # not belongs to, update the related
     else


Removed debug log

Removed debug log

--- lib/ghc-mod/ghc-modi-process.coffee
+++ lib/ghc-mod/ghc-modi-process.coffee
@@ -269,7 +269,6 @@

     if cmd is 'lint'
       args = [].concat atom.config.get('haskell-ghc-mod.hlintOptions').map((v) -> ['--hlintOpt', v])...
-      console.error args

     @queueCmd 'checklint',
       interactive: fast


just removing log statement

just removing log statement

--- test/unit/app/result/result.spec.coffee
+++ test/unit/app/result/result.spec.coffee
@@ -50,7 +50,6 @@

       it 'should be total days state', ->
         @scope.showTotalDays()
-        console.log "$scope.subViewState #{@scope.subViewState}"
         expect(@scope.isDaysTotalViewState()).toBeTruthy()


made autoindent more tolerant to deliberate misindentation


Update appending to workspace view

Update appending to workspace view

--- lib/keybinding-resolver-view.coffee
+++ lib/keybinding-resolver-view.coffee
@@ -29,7 +29,7 @@
       @attach()

   attach: ->
-    atom.workspaceView.vertical.append(this)
+    atom.workspaceView.prependToBottom(this)

   handleEvent: (event) ->
     keystroke = atom.keymap.keystrokeStringForEvent(event)


Add error and exception handling to node server code.

--- server-src/dev.coffee
+++ server-src/dev.coffee
@@ -34,6 +34,9 @@
     STATIC_URL + fpath

 requestHandler = (req, res, next) ->
+    unless req.path? and req.host?
+        next()
+        return
     match = false
     for pattern in ALLOWED_URLS
         if req.path.match pattern
@@ -114,7 +117,7 @@

     timeout = setTimeout sendResponse, 2000

-    http.get url, (httpResp) ->
+    request = http.get url, (httpResp) ->
         if httpResp.statusCode != 200
             clearTimeout timeout
             sendResponse()
@@ -127,6 +130,10 @@
             unitInfo = JSON.parse respData
             clearTimeout timeout
             sendResponse()
+    request.on 'error', (error) =>
+        console.error 'Error making API request', error
+        return
+

 server.configure ->
     staticDir = __dirname + '/../static'


Point feedback to real repo

Point feedback to real repo

--- app/js/models/feedback.coffee
+++ app/js/models/feedback.coffee
@@ -1,5 +1,5 @@
 class app.Models.Feedback extends Backbone.Model
-  url: 'https://api.github.com/repos/bkeepers/test/issues'
+  url: 'https://api.github.com/repos/bkeepers/github-notifications/issues'
   template: JST['app/templates/feedback_body.us']

   defaults:


make tutorial Id a var

make tutorial Id a var

--- app/lib/small_tutorial.coffee
+++ app/lib/small_tutorial.coffee
@@ -4,6 +4,8 @@
 User = require "zooniverse/models/user"

 class SmallTutorial
+  tutorialId = "small-tutorial"
+
   html = """

@@ -17,10 +19,10 @@

   create: ->
     $("body").append(html)
-    @dots = $("#small-tutorial").find(".dots")
+    @dots = $("##{tutorialId}").find(".dots")

     for slide, i in @slides
-      $("#small-tutorial #slides-container").append "
+      $("##{tutorialId} #slides-container").append "

@@ -35,7 +37,7 @@

   constructor: ({@slides}) ->
     @create()
-    @el = $("#small-tutorial").hide()
+    @el = $("##{tutorialId}").hide()

     @nextBtn = @el.find("button#next")
     @closeBtn = @el.find("button#close")
@@ -52,7 +54,7 @@
     @showSlide(1)
     window.addEventListener "click", @exitIfClickOutside

-  exitIfClickOutside: (e) => @exit() if e.target.id is "tutorial"
+  exitIfClickOutside: (e) => @exit() if e.target.id is tutorialId

   currentSlide: -> @el.find('.dot.active').index() + 1


Changed Camera min/max zoom.

Changed Camera min/max zoom.

--- app/lib/surface/Camera.coffee
+++ app/lib/surface/Camera.coffee
@@ -5,8 +5,8 @@
 r2d = (radians) -> radians * 180 / Math.PI
 d2r = (degrees) -> degrees / 180 * Math.PI

-MAX_ZOOM = 8
-MIN_ZOOM = 0.1
+MAX_ZOOM = 4
+MIN_ZOOM = 0.05
 DEFAULT_ZOOM = 2.0
 DEFAULT_TARGET = {x:0, y:0}
 DEFAULT_TIME = 1000


Workaround: Support Grafana 8

Workaround: Support Grafana 8

--- src/grafana.coffee
+++ src/grafana.coffee
@@ -118,7 +118,7 @@

   # Get a specific dashboard with options
   robot.respond /(?:grafana|graph|graf) (?:dash|dashboard|db) ([A-Za-z0-9\-\:_]+)(.*)?/i, (msg) ->
-    slug = msg.match[1].trim()
+    uid = msg.match[1].trim()
     remainder = msg.match[2]
     timespan = {
       from: "now-#{grafana_query_time_range}"
@@ -134,16 +134,15 @@
       height: process.env.HUBOT_GRAFANA_DEFAULT_HEIGHT or 500
       tz: process.env.HUBOT_GRAFANA_DEFAULT_TIME_ZONE or ""
       orgId: process.env.HUBOT_GRAFANA_ORG_ID or ""
-      apiEndpoint: process.env.HUBOT_GRAFANA_API_ENDPOINT or "dashboard-solo"
-      useUid: process.env.HUBOT_GRAFANA_USE_UID or ""
+      apiEndpoint: process.env.HUBOT_GRAFANA_API_ENDPOINT or "d-solo"
     endpoint = get_grafana_endpoint(msg)
     if not endpoint
       sendError 'No Grafana endpoint configured.', msg
       return
     # Parse out a specific panel
-    if /\:/.test slug
-      parts = slug.split(':')
-      slug = parts[0]
+    if /\:/.test uid
+      parts = uid.split(':')
+      uid = parts[0]
       visualPanelId = parseInt parts[1], 10
       if isNaN visualPanelId
         visualPanelId = false
@@ -174,7 +173,7 @@
           timespan[timeFields.shift()] = part.trim()

     robot.logger.debug msg.match
-    robot.logger.debug slug
+    robot.logger.debug uid
     robot.logger.debug timespan
     robot.logger.debug variables
     robot.logger.debug template_params
@@ -183,7 +182,7 @@
     robot.logger.debug pname

     # Call the API to get information about this dashboard
-    callGrafana endpoint, "dashboards/db/#{slug}", (dashboard) ->
+    callGrafana endpoint, "dashboards/uid/#{uid}", (dashboard) ->
       robot.logger.debug dashboard

       # Check dashboard information
@@ -238,13 +237,13 @@

           # Build links for message sending
           title = formatTitleWithTemplate(panel.title, template_map)
-          db = if query.useUid then dashboard.dashboard.uid else "db"
-          imageUrl = "#{endpoint.host}/render/#{query.apiEndpoint}/#{db}/#{slug}/?panelId=#{panel.id}&width=#{query.width}&height=#{query.height}&from=#{timespan.from}&to=#{timespan.to}#{variables}"
+          uid = dashboard.dashboard.uid
+          imageUrl = "#{endpoint.host}/render/#{query.apiEndpoint}/#{uid}/?panelId=#{panel.id}&width=#{query.width}&height=#{query.height}&from=#{timespan.from}&to=#{timespan.to}#{variables}"
           if query.tz
             imageUrl += "&tz=#{encodeURIComponent query.tz}"
           if query.orgId
             imageUrl += "&orgId=#{encodeURIComponent query.orgId}"
-          link = "#{endpoint.host}/dashboard/db/#{slug}/?panelId=#{panel.id}&fullscreen&from=#{timespan.from}&to=#{timespan.to}#{variables}"
+          link = "#{endpoint.host}/d/#{uid}/?panelId=#{panel.id}&fullscreen&from=#{timespan.from}&to=#{timespan.to}#{variables}"

           sendDashboardChart msg, title, imageUrl, link

@@ -363,12 +362,7 @@
       return

     for dashboard in list
-      # Handle refactor done for version 2.0.2+
-      if dashboard.uri
-        slug = dashboard.uri.replace /^db\//, ''
-      else
-        slug = dashboard.slug
-      response = response + "- #{slug}: #{dashboard.title}\n"
+      response = response + "- #{dashboard.uid}: #{dashboard.title}\n"

     # Remove trailing newline
     response.trim()


ENHANCE: Add number of files uploaded to task uploader analytics

--- src/app/tasks/partials/directives/submit-task-form.coffee
+++ src/app/tasks/partials/directives/submit-task-form.coffee
@@ -85,6 +85,7 @@
       taskService.processTaskStatusChange $scope.unit, $scope.project, $scope.task, response.status, response
       asUser = if $scope.assessingUnitRole? then $scope.assessingUnitRole.role else 'Student'
       analyticsService.event 'Task Submit Form', "Updated Status as #{asUser}", taskService.statusLabels[$scope.task.status]
+      analyticsService.event 'Task Submit Form', "Files Uploaded", undefined, $scope.numberOfFiles

     $scope.onComplete = ->
       $scope.uploadType = null


Fix a critical bug with avatars

Fix a critical bug with avatars

--- server/methods.coffee
+++ server/methods.coffee
@@ -11,7 +11,7 @@
   $set.updatedAt = now
   $setOnInsert = {createdAt: $set.updatedAt}
   upsertRes = Avatars.upsert(avatarSelector, {$setOnInsert: $setOnInsert, $set: $set})
-  avatarId = upsertRes.insertedId or Avatars.find(avatarSelector)._id
+  avatarId = upsertRes.insertedId or Avatars.findOne(avatarSelector)._id

   credentialSelector =
     scopes: {$all: credentialValues.scopes}


Fix plan editing

Fix plan editing

--- server/methods.coffee
+++ server/methods.coffee
@@ -219,7 +219,7 @@

     params = {
       name: name || ""
-      description: description || ""
+      desc: description || ""
       idOrganization: organization || ""
     }
     logger.debug("Editing Trello board with ID #{id}, params:", params)
@@ -234,7 +234,11 @@
       throw new Meteor.Error("trelloEdit", "Failed to edit Trello board with ID #{id}")

     logger.debug("Edit Trello board successfully (ID: #{id}), updating database, params:", params)
-    TrelloBoards.upsert({id: id}, {$set: params})
+    TrelloBoards.upsert({id: id}, {$set: {
+      name: params.name
+      description: params.desc
+      idOrganization: params.idOrganization
+    }})
   removeTrelloBoard: (token, id) ->
     verifyArg(id, 'id')
     verifyArg(token, 'token')


Fix “tree” state URL.

Fix “tree” state URL.

--- app/modules/tree/index.coffee
+++ app/modules/tree/index.coffee
@@ -11,7 +11,7 @@
 ]
   .config ($stateProvider) ->
     $stateProvider.state 'tree',
-      url: '/'
+      url: '/tree'
       controller: 'TreeCtrl'
       template: require './view'
   .controller 'TreeCtrl', ($scope, modal, $upload, xo, dateFilter, deleteVmsModal, notify) ->


Whoops, actually hide dev classifier link in production

--- app/partials/main-footer.cjsx
+++ app/partials/main-footer.cjsx
@@ -93,7 +93,7 @@
             <Link to="/lab"><Translate content="footer.discover.projectBuilder" /></Link>
             <Link to="/lab-how-to"><Translate content="footer.discover.howToGuide" /></Link>
             <Link to="/lab-policies"><Translate content="footer.discover.projectBuilderPolicies" /></Link>
-            {if process.env isnt 'production'
+            {if process.env.NODE_ENV isnt 'production'
               <Link to="/dev/classifier">Dev Classifier</Link>}
           </div>
           <div className="site-map-section">


feat: add shake animation when form is invalid

--- app/scripts/validation.coffee
+++ app/scripts/validation.coffee
@@ -6,9 +6,14 @@

   validation =

-    init: (form) ->
+    init: (param) ->
+
+      form = $(param)
       # Call the validation plugin on the contact form
-      $(form).validate
+      form.validate
+
+        invalidHandler: ->
+          $('.btn-next').animateCSS('shake')

         highlight: (element) ->
           $(element).parent().addClass('has-additional')


:fire: console log

:fire: console log

--- lib/table-element.coffee
+++ lib/table-element.coffee
@@ -1480,7 +1480,6 @@
   updateWidthAndHeight: ->
     width = @tableEditor.getContentWidth()
     height = @tableEditor.getContentHeight()
-    console.log(width, height)

     if @scrollPastEnd
       columnWidth = @tableEditor.getScreenColumnWidth()


spelling

spelling

--- app/translations/en_us.coffee
+++ app/translations/en_us.coffee
@@ -785,12 +785,12 @@

 TropicMind.com: Ecosystem and Food Chain (aimed for younger children)

-
Please visit Lincoln Park Zoo's Educator's Resources  webpage to access curriculum guides, Zoo field trip and self-tour information, accesssibility kits, and links to several multi-media apps.
+
Please visit Lincoln Park Zoo's Educator's Resources  webpage to access curriculum guides, Zoo field trip and self-tour information, accessibility  kits, and links to several multi-media apps.

-
The Adler Planetarium is dedicated to helping the public experience science and discover the universe through field trips, school parternships, child and teen programming. See the Teach and Learn section of the Adler's website for details.
+
The Adler Planetarium is dedicated to helping the public experience science and discover the universe through field trips, school partnerships, child and teen programming. See the Teach and Learn section of the Adler's website for details.

-

+

We also recommend checking out the Urban Wildlife Institute webpage for more information on the organization.

       """


Additional "often confused with" text

Additional "often confused with" text

--- app/translations/en_us.coffee
+++ app/translations/en_us.coffee
@@ -608,6 +608,9 @@
       '''

     buffalo:
+      cattle: '''
+        Cattle are less heavy than buffalo, and have a more pronounced shoulder hump. The horns of cattle don't fuse in the middle and cattle are often more variable in color.
+      '''
       wildebeest: '''
         A wildebeest is skinnier than a buffalo, with a large head and long beard.  Its horns don’t fuse together in the center.
       '''
@@ -621,6 +624,14 @@
         Lions are larger and more muscular than caracals. They don't have large pointed ears. Their tails are smooth with a tuft of dark fur at the end.
       '''

+    cattle:
+      buffalo: '''
+        Buffalo are stockier than cattle and lack the prominent shoulder hump. The horns of cattle don't join together in the middle.
+      '''
+      wildebeest: '''
+        The horns of cattle are shorter and don't extend as far back as those of wildebeest. While cattle have a prominent shoulder hump, they don't have the sloping back of wildebeest and lack a chin-beard.
+      '''
+
     cheetah:
       leopard: '''
         Leopards are covered with black rosettes, not spots. They don't have black facial lines like a cheetah.
@@ -636,14 +647,14 @@
       '''

     dikDik:
-      reedbuck: '''
-        A reedbuck is bigger than a dik dik. Their undersides are white, not gray, and they lack the ‘tear drop’ beneath the eye.
-      '''
+      reedbuck: 'A reedbuck is bigger than a dik dik. Their undersides are white, not gray, and they lack the ‘tear drop’ beneath the eye.'
+      steenbok: 'Steenbok have larger ears than dik dik and lack the dark suborbital gland. They are slightly bigger in size.'
+      duiker: 'Dik dik have more petite, tapering noses that lack the dark muzzle stripe of a duiker. Dik dik have more prominent tufts of hair between their ears and a rounded posture.'

     duiker:
-      reedbuck: ''
-      dikDik: ''
-      steenbok: ''
+      reedbuck: 'Reedbuck lack the dark muzzle and forelegs of a duiker. The horns of a male duiker are straighter and smoother than those of a male reedbuck.'
+      dikDik: 'Dik dik are smaller and less stocky than a duiker, lacking the prominently dark muzzle and foreleg stripes.'
+      steenbok: 'Steenbok are more petite than duiker. The black facial glands of a steenbok do not extend to cover as much of the face as the dark muzzle stripe of the duiker.'

     gazelleGrants:
       gazelleThomsons: '''
@@ -757,8 +768,8 @@
       '''

     steenbok:
-      duiker: ''
-      dikDik: ''
+      duiker: 'A duiker is larger than a steenbok, with a more prominent black facial stripe and dark forelimbs.'
+      dikDik: 'Dik dik have a more rounded back and hunched posture than a steenbok. Dik dik have a more prominent tuft of hair between their ears.'

     topi:
       hartebeest: '''
@@ -777,6 +788,9 @@
     wildebeest:
       buffalo: '''
         The buffalo is larger and stockier. Horns tend to be more fused together at the center.
+      '''
+      cattle: '''
+        Wildebeest have more pronounced, curving horns than cattle. Cattle have a less bulbous muzzle and they lack the wildebeest's chin-beard.
       '''

     zorilla:


Remove empty private tags

Remove empty private tags

--- lib/status-bar-view.coffee
+++ lib/status-bar-view.coffee
@@ -21,7 +21,6 @@

     @storeActiveBuffer()

-  # Private:
   attach: ->
     atom.workspaceView.appendToBottom(this) unless @hasParent()

@@ -45,7 +44,6 @@
   getActiveItem: ->
     atom.workspaceView.getActivePaneItem()

-  # Private:
   storeActiveBuffer: ->
     @buffer = @getActiveItem()?.getBuffer?()

@@ -54,13 +52,11 @@
     @bufferSubscriptions.push([event, callback])
     @buffer.on(event, callback) if @buffer

-  # Private:
   subscribeAllToBuffer: ->
     return unless @buffer
     for [event, callback] in @bufferSubscriptions
       @buffer.on(event, callback)

-  # Private:
   unsubscribeAllFromBuffer: ->
     return unless @buffer
     for [event, callback] in @bufferSubscriptions


Fix eventric DI for repository

Fix eventric DI for repository

--- src/repository/repository.coffee
+++ src/repository/repository.coffee
@@ -35,7 +35,7 @@

         if not domainEvents.length
           err = "No domainEvents for #{@_aggregateName} Aggregate with #{aggregateId} available"
-          eventric.log.error err
+          @_eventric.log.error err
           callback err, null
           reject err
           return
@@ -99,7 +99,7 @@
       aggregate = @_aggregateInstances[commandId][aggregateId]
       if not aggregate
         err = "Tried to save unknown aggregate #{@_aggregateName}"
-        eventric.log.error err
+        @_eventric.log.error err
         err = new Error err
         callback? err, null
         reject err
@@ -108,19 +108,19 @@
       domainEvents   = aggregate.getDomainEvents()
       if domainEvents.length < 1
         err = "Tried to save 0 DomainEvents from Aggregate #{@_aggregateName}"
-        eventric.log.debug err, @_command
+        @_eventric.log.debug err, @_command
         err = new Error err
         callback? err, null
         reject err
         return

-      eventric.log.debug "Going to Save and Publish #{domainEvents.length} DomainEvents from Aggregate #{@_aggregateName}"
+      @_eventric.log.debug "Going to Save and Publish #{domainEvents.length} DomainEvents from Aggregate #{@_aggregateName}"

       # TODO: this should be an transaction to guarantee consistency
-      eventric.eachSeries domainEvents, (domainEvent, next) =>
+      @_eventric.eachSeries domainEvents, (domainEvent, next) =>
         domainEvent.command = @_command
         @_store.saveDomainEvent domainEvent, =>
-          eventric.log.debug "Saved DomainEvent", domainEvent
+          @_eventric.log.debug "Saved DomainEvent", domainEvent
           next null
       , (err) =>
         if err
@@ -129,13 +129,13 @@
         else
           if not @_context.isWaitingModeEnabled()
             for domainEvent in domainEvents
-              eventric.log.debug "Publishing DomainEvent", domainEvent
+              @_eventric.log.debug "Publishing DomainEvent", domainEvent
               @_context.getEventBus().publishDomainEvent domainEvent
             resolve aggregate.id
             callback null, aggregate.id
           else
-            eventric.eachSeries domainEvents, (domainEvent, next) =>
-              eventric.log.debug "Publishing DomainEvent in waiting mode", domainEvent
+            @_eventric.eachSeries domainEvents, (domainEvent, next) =>
+              @_eventric.log.debug "Publishing DomainEvent in waiting mode", domainEvent
               @_context.getEventBus().publishDomainEventAndWait domainEvent
               .then ->
                 next()


OHM-52: Converted api/users to coffee

OHM-52: Converted api/users to coffee

--- src/api/users.coffee
+++ src/api/users.coffee
@@ -8,26 +8,26 @@
 contact = require '../contact'
 config = require "../config/config"
 config.newUserExpiry = config.get('newUserExpiry')
+utils = require "../utils"
+

 ###
 # Get authentication details
 ###
-exports.authenticate = `function *authenticate(email) {
-  email = unescape(email)
-
-  try {
-    var user = yield User.findOne({ email: email }).exec();
-
-    this.body = {
-      salt: user.passwordSalt,
-      ts: new Date()
-    }
-  } catch(e) {
-    logger.error('Could not find user by email ' + email + ': ' + e);
-    this.status = 'not found';
-    this.body = 'Could not find user by email ' + email;
-  }
-}`
+exports.authenticate = (email) ->
+  email = unescape email
+
+  try
+    user = yield User.findOne(email: email).exec()
+
+    if not user
+      util.logAndSetResponse this, 'not found', "Could not find user by email #{email}", 'info'
+    else
+      this.body =
+        salt: user.passwordSalt
+        ts: new Date()
+  catch e
+    util.logAndSetResponse this, 'internal server error', "Error during authentication #{e}", 'error'

 #######################################
@@ -35,287 +35,228 @@
 #######################################

 # get the new user details
-exports.getNewUser = `function *getNewUser(token) {
-  token = unescape(token)
-
-  try {
-
-    projectionRestriction = { "firstname": 1, "surname": 1, "msisdn": 1, "token": 1, "locked": 1, "expiry": 1, "_id": 0 };
-
-    var result = yield User.findOne({ token: token }, projectionRestriction).exec();
-    if (result === null) {
-      this.body = "User with token '"+token+"' could not be found.";
-      this.status = 'not found';
-    } else {
-      // if expiry date has past
-      if ( moment(result.expiry).utc().format() < moment().utc().format() ){
-        // new user- set password - expired
-        this.body = "User with token '"+token+"' has expired to set their password.";
-        this.status = 'gone';
-      }else{
-        // valid new user - set password
-        this.body = result;
-      }
-
-    }
-  } catch(e) {
-    logger.error('Could not find user with token '+token+' via the API: ' + e);
-    this.body = e.message;
-    this.status = 'internal server error';
-  }
-}`
+exports.getNewUser = (token) ->
+  token = unescape token
+
+  try
+    projectionRestriction = "firstname": 1, "surname": 1, "msisdn": 1, "token": 1, "locked": 1, "expiry": 1, "_id": 0
+
+    result = yield User.findOne(token: token, projectionRestriction).exec()
+    if not result
+      this.body = "User with token #{token} could not be found."
+      this.status = 'not found'
+    else
+      # if expiry date has past
+      if moment(result.expiry).utc().format() < moment().utc().format()
+        # new user- set password - expired
+        this.body = "User with token #{token} has expired to set their password."
+        this.status = 'gone'
+      else
+        # valid new user - set password
+        this.body = result
+  catch e
+    utils.logAndSetResponse this, 'internal server error', "Could not find user with token #{token} via the API #{e}", 'error'
+

 # update the password/details for the new user
-exports.updateNewUser = `function *updateNewUser(token) {
-  token = unescape(token)
-  var userData = this.request.body;
+exports.updateNewUser = (token) ->
+  token = unescape token
+  userData = this.request.body

-  try {
-    // first try get new user details to check expiry date
-    var newUserOldData = yield User.findOne({ token: token }).exec();
-
-    if (newUserOldData === null) {
-      this.body = "User with token '"+token+"' could not be found.";
-      this.status = 'not found';
-      return;
-    } else {
-      // if expiry date has past
-      if ( moment(newUserOldData.expiry).utc().format() < moment().utc().format() ){
-        // new user- set password - expired
-        this.body = "User with token '"+token+"' has expired to set their password.";
-        this.status = 'gone';
-        return;
-      }
-    }
-  } catch(e) {
-    logger.error('Could not find user with token '+token+' via the API: ' + e);
-    this.body = e.message;
-    this.status = 'internal server error';
-    return;
-  }
-
-
-  // check to make sure 'msisdn' isnt 'undefined' when saving
-  var msisdn = null;
-  if ( userData.msisdn ){
-    msisdn = userData.msisdn;
-  }
-
-  // construct user object to prevent other properties from being updated
-  newUserUpdate = {
-    firstname: userData.firstname,
-    surname: userData.surname,
-    token: null,
-    locked: false,
-    expiry: null,
-    msisdn: msisdn,
-    passwordAlgorithm: userData.passwordAlgorithm,
-    passwordSalt: userData.passwordSalt,
+  try
+    # first try get new user details to check expiry date
+    newUserOldData = yield User.findOne(token: token).exec()
+
+    if not newUserOldData
+      this.body = "User with token #{token} could not be found."
+      this.status = 'not found'
+      return
+    else
+      # if expiry date has past
+      if moment(newUserOldData.expiry).utc().format() < moment().utc().format()
+        # new user- set password - expired
+        this.body = "User with token #{token} has expired to set their password."
+        this.status = 'gone'
+        return
+
+  catch e
+    utils.logAndSetResponse this, 'internal server error', "Could not find user with token #{token} via the API #{e}", 'error'
+    return
+
+
+  # check to make sure 'msisdn' isnt 'undefined' when saving
+  if userData.msisdn then msisdn = userData.msisdn else msisdn = null
+
+  # construct user object to prevent other properties from being updated
+  newUserUpdate =
+    firstname: userData.firstname
+    surname: userData.surname
+    token: null
+    locked: false
+    expiry: null
+    msisdn: msisdn
+    passwordAlgorithm: userData.passwordAlgorithm
+    passwordSalt: userData.passwordSalt
     passwordHash: userData.passwordHash
-  }
-
-  try {
-    yield User.findOneAndUpdate({ token: token }, newUserUpdate).exec();
+
+  try
+    yield User.findOneAndUpdate(token: token, newUserUpdate).exec()
     this.body = "Successfully set new user password."
-    logger.info('New user updated by token %s', token);
-  } catch(e) {
-    logger.error('Could not update user by token '+token+' via the API: ' + e);
-    this.body = e.message;
-    this.status = 'internal server error';
-  }
-}`
+    logger.info "New user updated by token #{token}"
+  catch e
+    utils.logAndSetResponse this, 'internal server error', "Could not update user with token #{token} via the API #{e}", 'error'
+

 #######################################
 ### New User Set Password Functions ###
 #######################################

+plainMessageTemplate = (firstname, setPasswordLink) -> """
+<---------- New User - Set Password ---------->
+Hi #{firstname},
+
+A profile has been created for you on OpenHIM
+Follow the below link to set your password and log into OpenHIM Console
+#{setPasswordLink}
+<---------- New User - Set Password ---------->
+"""
+
+htmlMessageTemplate = (firstname, setPasswordLink) -> """
+
New OpenHIM Profile

+
Hi #{firstname},

A profile has been created for you on OpenHIM

+
Follow the below link to set your password and log into OpenHIM Console

+
#{setPasswordLink}

+"""
+
 ###
 # Adds a user
 ###
-exports.addUser = `function *addUser() {
-
-  // Test if the user is authorised
-  if (authorisation.inGroup('admin', this.authenticated) === false) {
-    logger.info('User ' +this.authenticated.email+ ' is not an admin, API access to addUser denied.')
-    this.body = 'User ' +this.authenticated.email+ ' is not an admin, API access to addUser denied.'
-    this.status = 'forbidden';
-    return;
-  }
-
-  var userData = this.request.body
-
-  /*
+exports.addUser = ->
+  # Test if the user is authorised
+  if not authorisation.inGroup 'admin', this.authenticated
+    utils.logAndSetResponse this, 'forbidden', "User #{this.authenticated.email} is not an admin, API access to addUser denied.", 'info'
+    return
+
+  userData = this.request.body
+
   # Generate the new user token here
   # set locked = true
   # set expiry date = true
-  */
-
-  var token = randtoken.generate(32);
-  userData.token = token;
-  userData.locked = true;
-
-  duration = config.newUserExpiry.duration;
-  var durationType = config.newUserExpiry.durationType;
-  userData.expiry = moment().add(duration, durationType).utc().format();
-
-  var consoleURL = config.alerts.consoleURL;
-  var setPasswordLink = consoleURL + '/#/set-password/'+token;
-
-  try {
-    var user = new User(userData);
-    var result = yield Q.ninvoke(user, 'save');
-
-    /*
+
+  token = randtoken.generate 32
+  userData.token = token
+  userData.locked = true
+
+  duration = config.newUserExpiry.duration
+  durationType = config.newUserExpiry.durationType
+  userData.expiry = moment().add(duration, durationType).utc().format()
+
+  consoleURL = config.alerts.consoleURL
+  setPasswordLink = "#{consoleURL}/#/set-password/#{token}"
+
+  try
+    user = new User userData
+    result = yield Q.ninvoke user, 'save'
+
     # Send email to new user to set password
-    */
-
-    var plainMessage;
-    plainMessage = '<---------- New User - Set Password ----------> \r\n \r\n';
-    plainMessage += 'Hi '+userData.firstname+', A profile has been created for you on OpenHIM \r\n';
-    plainMessage += 'Follow the below link to set your password and log into OpenHIM Console  \r\n';
-    plainMessage += setPasswordLink + '\r\n';
-    plainMessage += '<---------- New User - Set Password ----------> \r\n \r\n';
-    plainMessage += '\r\n';
-
-    var htmlMessage;
-    htmlMessage = '
Hi '+userData.firstname+', A profile has been created for you on OpenHIM
';
-    htmlMessage += '
Follow the below link to set your password and log into OpenHIM Console
';
-    htmlMessage += '
'+setPasswordLink+'
';
-
-    contact.contactUser('email', userData.email, 'OpenHIM Console Profile', plainMessage, htmlMessage, function(){
-      logger.info('The email has been sent to the new user');
-    })
+
+    plainMessage = plainMessageTemplate userData.firstname, setPasswordLink
+    htmlMessage = htmlMessageTemplate userData.firstname, setPasswordLink
+
+    contact.contactUser 'email', userData.email, 'OpenHIM Console Profile', plainMessage, htmlMessage, ->
+      logger.info 'The email has been sent to the new user'

-    this.body = 'User successfully created';
-    this.status = 'created';
-    logger.info('User %s created user %s', this.authenticated.email, userData.email);
-  } catch(e) {
-    logger.error('Could not add a user via the API: ' + e);
-    this.body = e.message;
-    this.status = 'bad request';
-  }
-}`
+    this.body = 'User successfully created'
+    this.status = 'created'
+    logger.info "User #{this.authenticated.email} created user #{userData.email}"
+  catch e
+    utils.logAndSetResponse this, 'internal server error', "Could not add user via the API #{e}", 'error'
+

 ###
 # Retrieves the details of a specific user
 ###
-exports.getUser = `function *findUserByUsername(email) {
-
-  var email = unescape(email);
-
-  // Test if the user is authorised, allow a user to fetch their own details
-  if (authorisation.inGroup('admin', this.authenticated) === false && this.authenticated.email !== email) {
-    logger.info('User ' +this.authenticated.email+ ' is not an admin, API access to findUserByUsername denied.')
-    this.body = 'User ' +this.authenticated.email+ ' is not an admin, API access to findUserByUsername denied.'
-    this.status = 'forbidden';
-    return;
-  }
-
-  try {
-    var result = yield User.findOne({ email: email }).exec();
-    if (result === null) {
-      this.body = "User with email '"+email+"' could not be found.";
-      this.status = 'not found';
-    } else {
-      this.body = result;
-    }
-  } catch(e) {
-    logger.error('Could not find user with email '+email+' via the API: ' + e);
-    this.body = e.message;
-    this.status = 'internal server error';
-  }
-}`
-
-exports.updateUser = `function *updateUser(email) {
-
-  var email = unescape(email);
-
-  // Test if the user is authorised, allow a user to update their own details
-  if (authorisation.inGroup('admin', this.authenticated) === false && this.authenticated.email !== email) {
-    logger.info('User ' +this.authenticated.email+ ' is not an admin, API access to updateUser denied.')
-    this.body = 'User ' +this.authenticated.email+ ' is not an admin, API access to updateUser denied.'
-    this.status = 'forbidden';
-    return;
-  }
-
-  var userData = this.request.body;
-
-  // reset token/locked/expiry when user is updated and password supplied
-  if ( userData.passwordAlgorithm && userData.passwordHash && userData.passwordSalt ){
-    userData.token = null;
-    userData.locked = false;
-    userData.expiry = null;
-  }
-
-  // Don't allow a non-admin user to change their groups
-  if (this.authenticated.email === email && authorisation.inGroup('admin', this.authenticated) === false) {
-    delete userData.groups
-  }
-
-  //Ignore _id if it exists (update is by email)
-  if (userData._id) {
-    delete userData._id;
-  }
-
-  try {
-    yield User.findOneAndUpdate({ email: email }, userData).exec();
+exports.getUser = (email) ->
+
+  email = unescape email
+
+  # Test if the user is authorised, allow a user to fetch their own details
+  if not authorisation.inGroup('admin', this.authenticated) and this.authenticated.email isnt email
+    utils.logAndSetResponse this, 'forbidden', "User #{this.authenticated.email} is not an admin, API access to getUser denied.", 'info'
+    return
+
+  try
+    result = yield User.findOne(email: email).exec()
+    if not result
+      this.body = "User with email #{email} could not be found."
+      this.status = 'not found'
+    else
+      this.body = result
+  catch e
+    utils.logAndSetResponse this, 'internal server error', "Could not get user via the API #{e}", 'error'
+
+
+exports.updateUser = (email) ->
+
+  email = unescape email
+
+  # Test if the user is authorised, allow a user to update their own details
+  if not authorisation.inGroup('admin', this.authenticated) and this.authenticated.email isnt email
+    utils.logAndSetResponse this, 'forbidden', "User #{this.authenticated.email} is not an admin, API access to updateUser denied.", 'info'
+    return
+
+  userData = this.request.body
+
+  # reset token/locked/expiry when user is updated and password supplied
+  if userData.passwordAlgorithm and userData.passwordHash and userData.passwordSalt
+    userData.token = null
+    userData.locked = false
+    userData.expiry = null
+
+  # Don't allow a non-admin user to change their groups
+  if this.authenticated.email is email and not authorisation.inGroup 'admin', this.authenticated then delete userData.groups
+
+  #Ignore _id if it exists (update is by email)
+  if userData._id then delete userData._id
+
+  try
+    yield User.findOneAndUpdate(email: email, userData).exec()
     this.body = "Successfully updated user."
-    logger.info('User %s updated user %s', this.authenticated.email, userData.email);
-  } catch(e) {
-    logger.error('Could not update user by email '+email+' via the API: ' + e);
-    this.body = e.message;
-    this.status = 'internal server error';
-  }
-}`
-
-exports.removeUser = `function *removeUser(email) {
-
-  // Test if the user is authorised
-  if (authorisation.inGroup('admin', this.authenticated) === false) {
-    logger.info('User ' +this.authenticated.email+ ' is not an admin, API access to removeUser denied.')
-    this.body = 'User ' +this.authenticated.email+ ' is not an admin, API access to removeUser denied.'
-    this.status = 'forbidden';
-    return;
-  }
-
-  var email = unescape (email);
-
-  // Test if the user is root@openhim.org
-  if ( email === 'root@openhim.org' ) {
-    logger.info('User ' +this.authenticated.email+ ' is OpenHIM root, User cannot be deleted through the API')
-    this.body = 'User ' +this.authenticated.email+ ' is OpenHIM root, User cannot be deleted through the API'
-    this.status = 'forbidden';
-    return;
-  }
-
-  try {
-    yield User.findOneAndRemove({ email: email }).exec();
-    this.body = "Successfully removed user with email '"+email+"'";
-    logger.info('User %s removed user %s', this.authenticated.email, email);
-  }catch(e){
-    logger.error('Could not remove user by email '+email+' via the API: ' + e);
-    this.body = e.message;
-    this.status = 'internal server error';
-  }
-
-}`
-
-exports.getUsers = `function *getUsers() {
-
-  // Test if the user is authorised
-  if (authorisation.inGroup('admin', this.authenticated) === false) {
-    logger.info('User ' +this.authenticated.email+ ' is not an admin, API access to getUsers denied.')
-    this.body = 'User ' +this.authenticated.email+ ' is not an admin, API access to getUsers denied.'
-    this.status = 'forbidden';
-    return;
-  }
-
-  try {
-    this.body = yield User.find().exec();
-  }catch (e){
-    logger.error('Could not fetch all users via the API: ' + e);
-    this.message = e.message;
-    this.status = 'internal server error';
-  }
-}`
+    logger.info "User #{this.authenticated.email} updated user #{userData.email}"
+  catch e
+    utils.logAndSetResponse this, 'internal server error', "Could not update user #{email} via the API #{e}", 'error'
+
+
+exports.removeUser = (email) ->
+
+  # Test if the user is authorised
+  if not authorisation.inGroup 'admin', this.authenticated
+    utils.logAndSetResponse this, 'forbidden', "User #{this.authenticated.email} is not an admin, API access to removeUser denied.", 'info'
+    return
+
+  email = unescape email
+
+  # Test if the user is root@openhim.org
+  if email is 'root@openhim.org'
+    utils.logAndSetResponse this, 'forbidden', "User #{this.authenticated.email} is OpenHIM root, User cannot be deleted through the API", 'info'
+    return
+
+  try
+    yield User.findOneAndRemove(email: email).exec()
+    this.body = "Successfully removed user with email #{email}"
+    logger.info "User #{this.authenticated.email} removed user #{email}"
+  catch e
+    utils.logAndSetResponse this, 'internal server error', "Could not remove user #{email} via the API #{e}", 'error'
+
+
+exports.getUsers = ->
+  # Test if the user is authorised
+  if not authorisation.inGroup 'admin', this.authenticated
+    utils.logAndSetResponse this, 'forbidden', "User #{this.authenticated.email} is not an admin, API access to getUsers denied.", 'info'
+    return
+
+  try
+    this.body = yield User.find().exec()
+  catch e
+    utils.logAndSetResponse this, 'internal server error', "Could not fetch all users via the API #{e}", 'error'


Sort packages by name

Sort packages by name

--- src/available.coffee
+++ src/available.coffee
@@ -42,6 +42,7 @@
           else if response.statusCode is 200
             packages = body.filter (pack) -> pack.releases?.latest?
             packages = packages.map ({readme, metadata}) -> _.extend({}, metadata, {readme})
+            packages = _.sortBy(packages, 'name')
             callback(null, packages)
           else
             message = body.message ? body.error ? body


redirect to daily when changing template

--- src/daily-report/states/edit-template/controller.coffee
+++ src/daily-report/states/edit-template/controller.coffee
@@ -1,6 +1,7 @@
 angular.module 'Scrumble.daily-report'
 .controller 'EditTemplateCtrl', (
   $scope
+  $state
   $mdToast
   mailer
   reportBuilder
@@ -23,4 +24,4 @@

   $scope.save = ->
     $scope.dailyReport.save().then ->
-      $mdToast.show saveFeedback
+      $state.go 'tab.daily-report'


Dedupe the symbols during wordlist generation

--- lib/symbol-provider.coffee
+++ lib/symbol-provider.coffee
@@ -130,8 +130,9 @@
     candidates = []
     for symbol in symbolList
       score = fuzzaldrin.score(symbol.word, prefix)
-      if symbol.bufferRow?
-        rowDifference = symbol.bufferRow - position.row
+      if symbol.bufferRows?
+        rowDifference = Number.MAX_VALUE
+        rowDifference = Math.min(rowDifference, bufferRow - position.row) for bufferRow in symbol.bufferRows
         locality = @computeLocalityModifier(rowDifference)
         score *= locality
       candidates.push({symbol, score, locality, rowDifference}) if score > 0
@@ -196,8 +197,7 @@
     # without warning.
     tokenizedLines = editor.displayBuffer.tokenizedBuffer.tokenizedLines

-    symbolTypes = {}
-    matchedSymbols = []
+    symbols = {}

     # Handle the case where a symbol is a variable in some cases and, say, a
     # class in others. We want all symbols of the same name to have the same type. e.g.
@@ -207,22 +207,21 @@
     # class MyClass extends SomeModule # This line parses SomeModule as a class
     # ```
     # `class` types are higher priority than `variables`
-    cacheSymbolType = (word, currentType) =>
-      word = getWordKey(word)
-      return symbolTypes[word] = currentType unless symbolTypes[word]?
-      currentTypePriority = @config[currentType].priority
-      cachedTypePriority = @config[symbolTypes[word]].priority
-      symbolTypes[word] = currentType if currentTypePriority > cachedTypePriority
-
-    getWordKey = (word) ->
-       # some words are reserved, like 'constructor' :/
-      word + '$$'
-
-    normalizeTypesForAllSymbols = ->
-      for symbol in matchedSymbols
-        wordKey = getWordKey(symbol.word)
-        symbol.type = symbolTypes[wordKey] if symbolTypes[wordKey]?
-      return
+    cacheSymbol = (word, type, bufferRow, scopes) =>
+      wordKey = getWordKey(word)
+      cachedSymbol = symbols[wordKey]
+      if cachedSymbol?
+        currentTypePriority = @config[type].priority
+        cachedTypePriority = @config[cachedSymbol.type].priority
+        cachedSymbol.type = type if currentTypePriority > cachedTypePriority
+        console.log cachedSymbol
+        cachedSymbol.bufferRows.push(bufferRow)
+        cachedSymbol.scopes.push(scopes)
+      else
+        symbols[wordKey] = {word, type, bufferRows: [bufferRow], scopes: [scopes]}
+
+     # some words are reserved, like 'constructor' :/
+    getWordKey = (word) -> word + '$$'

     for {tokens}, bufferRow in tokenizedLines
       for token in tokens
@@ -232,12 +231,10 @@
             if selector.matches(scopes) and matches = token.value.match(options.wordRegex)
               for word in matches
                 if word.length >= minimumWordLength
-                  matchedSymbols.push {type, word, bufferRow, scopes}
-                  cacheSymbolType(word, type)
+                  cacheSymbol(word, type, bufferRow, scopes)
               break

-    normalizeTypesForAllSymbols()
-    matchedSymbols
+    (symbol for wordKey, symbol of symbols)

   cssSelectorFromScopes: (scopes) ->
     selector = ''


Only auto-select document if all ranges are empty

--- lib/table-formatter.coffee
+++ lib/table-formatter.coffee
@@ -39,7 +39,9 @@
     selectionsRanges = editor.getSelectedBufferRanges()

     bufferRange=editor.getBuffer().getRange()
-    if force or (selectionsRanges[0].isEmpty() and @autoSelectEntireDocument)
+    selectionsRangesEmpty =
+      selectionsRanges.every (i) -> i.isEmpty()
+    if force or (selectionsRangesEmpty and @autoSelectEntireDocument)
       selectionsRanges = [bufferRange]
     else
       selectionsRanges=


Code cleanup

Code cleanup

--- lib/table-formatter.coffee
+++ lib/table-formatter.coffee
@@ -1,9 +1,9 @@
 {CompositeDisposable, Range} = require('atom')
 wcswidth = require 'wcwidth'

-# DEBUG = true
-
 swidth = (str) ->
+  # zero-width Unicode characters that we should ignore for
+  # purposes of computing string "display" width
   zwcrx = /[\u200B-\u200D\uFEFF\u00AD]/g
   wcswidth(str) - ( str.match(zwcrx)?.length or 0 )

@@ -72,98 +72,79 @@
       editor.backwardsScanInBufferRange(@regex, range, myIterator)

   formatTable: (text) ->
-    just = (string, type, n) ->
-      length = n - swidth(string)
-      if type is '::'
-        return ' '.repeat(length / 2) + string + ' '.repeat((length + 1) / 2)
-      else if type is '-:'
-        return ' '.repeat(length) + string
-      else if type is ':-'
-        return string + ' '.repeat(length)
+    padding = (len, str = ' ') -> str.repeat len
+
+    stripTailPipes = (str) ->
+      str.trim().replace /(^\||\|$)/g, ""
+
+    splitCells = (str) ->
+      str.split '|'
+
+    addTailPipes = (str) =>
+      if @keepFirstAndLastPipes
+        "|#{str}|"
       else
-        return string
+        str
+
+    joinCells = (arr) ->
+      arr.join '|'

     formatline = text[2].trim()
     headerline = text[1].trim()

-    if headerline.length is 0
-      formatrow = 0
-      lines = text[3].trim().split('\n')
-    else
-      formatrow = 1
-      lines = (text[1] + text[3]).trim().split('\n')
-    rows = lines.length
+    [formatrow, data] =
+      if headerline.length is 0
+        [ 0, text[3] ]
+      else
+        [ 1, text[1] + text[3] ]
+    lines = data.trim().split('\n')

-    formatline = formatline.trim().replace(/(^\||\|$)/g, "")
-    fstrings = formatline.split('|')
-    justify = []
-    for cell in fstrings
-      cell = cell.trim()
-      ends = cell[0] + (cell[cell.length - 1] or '')
-      if ends is '::'
-        justify.push('::')
-      else if ends is '-:'
-        justify.push('-:')
-      else
-        justify.push(':-')
+    justify = for cell in splitCells stripTailPipes formatline
+      [first, ..., last] = cell.trim()
+      switch ends = (first ? ':') + (last ? '')
+        when '::', '-:' then ends
+        else ':-'

     columns = justify.length

-    content = []
-    for line in lines
-      line = line.trim().replace(/(^\||\|$)/g, "")
-      cells = line.split('|')
+    content = for line in lines
+      cells =  splitCells stripTailPipes line
       #put all extra content into last cell
-      cells[columns - 1] = cells.slice(columns - 1).join('|')
-      linecontent =
-        for x in cells
-          ' '.repeat(@spacePadding) +
-          (if x? then x.trim() else "") +
-          ' '.repeat(@spacePadding)
-      content.push(linecontent)
+      cells[columns - 1] = joinCells cells.slice(columns - 1)
+      for cell in cells
+        padding(@spacePadding) +
+        (cell?.trim?() ? '') +
+        padding(@spacePadding)

-    rows = content.length
-    for i in [0..rows - 1]
-      while content[i].length < columns
-        content[i].push(' ')
+    widths = for i in [0..columns - 1]
+      Math.max 2, (swidth(cells[i]) for cells in content)...

-    widths = []
-    widths.push(2) for c in [0..columns - 1]
+    just = (string, col) ->
+      length = widths[col] - swidth(string)
+      switch justify[col]
+        when '::'
+          [front, back] = padding
+          padding(length / 2) + string + padding((length + 1) / 2)
+        when '-:'
+          padding(length) + string
+        when ':-'
+          string + padding(length)
+        else
+          string

-    max = (x, y) -> if x > y then x else y
+    formatted = for cells in content
+      addTailPipes joinCells (just(cells[i], i) for i in [0..columns - 1])

-    for row in content
+    formatline = addTailPipes joinCells (
       for i in [0..columns - 1]
-        widths[i] = max(swidth(row[i]), widths[i])
-
-    formatted = []
-    for row in content
-      line = []
-      for i in [0..columns - 1]
-        newtext = just(row[i], justify[i], widths[i])
-        line.push(newtext)
-
-      if @keepFirstAndLastPipes
-        formatted.push('|' + line.join('|') + '|')
-      else
-        formatted.push(line.join('|'))
-
-    formattedformatline = []
-    for i in [0..columns - 1]
-      newtext = justify[i][0] +
-        '-'.repeat((widths[i] - 2)) +
-        justify[i][justify[i].length - 1]
-      formattedformatline.push(newtext)
-
-    if @keepFirstAndLastPipes
-      formatline = '|' + formattedformatline.join('|') + '|'
-    else
-      formatline = formattedformatline.join('|')
+        [front, back] = justify[i]
+        front + padding(widths[i] - 2, '-') + back
+      )

     formatted.splice(formatrow, 0, formatline)
-    fmtstr = formatted.join('\n') + '\n'
-    fmtstr = '\n' + fmtstr if headerline.length is 0 and text[1] isnt ''
-    return fmtstr
+
+    return (if headerline.length is 0 and text[1] isnt '' then '\n' else '') +
+      formatted.join('\n') + '\n'

   regex: ///
     ( # header capture


Extend selections to whole tables

Extend selections to whole tables

--- lib/table-formatter.coffee
+++ lib/table-formatter.coffee
@@ -1,4 +1,4 @@
-{CompositeDisposable} = require('atom')
+{CompositeDisposable,Range} = require('atom')
 wcswidth = require 'wcwidth'

 module.exports =
@@ -38,8 +38,23 @@

     selectionsRanges = editor.getSelectedBufferRanges()

+    bufferRange=editor.getBuffer().getRange()
     if force or (selectionsRanges[0].isEmpty() and @autoSelectEntireDocument)
-      selectionsRanges = [editor.getBuffer().getRange()]
+      selectionsRanges = [bufferRange]
+    else
+      selectionsRanges=
+        for srange in selectionsRanges
+          start = bufferRange.start
+          end = bufferRange.end
+          editor.scanInBufferRange /^$/m,
+            new Range(srange.start,bufferRange.end),
+            ({range})->
+              end=range.start
+          editor.backwardsScanInBufferRange /^$/m,
+            new Range(bufferRange.start,srange.end),
+            ({range})->
+              start=range.start
+          new Range(start,end)

     myIterator = (obj) =>
       obj.replace(@formatTable(obj.match))


Highlighting of markdown options for Weave

--- grammars/weave_md.cson
+++ grammars/weave_md.cson
@@ -10,15 +10,25 @@
       'include' : 'source.weave.noweb'
     }
     {
-    'begin': '(?:^(?:`|~){3,}\\s*(?:\\{|\{\\.|)julia(?:;|\\s|)(.*)\\}\s*$)|(?:^(?:`|~){3,}\\s*julia\\s*$)'
+    'begin': '^([`~]{3,})(\\{|\\{\\.|)(julia)(;|)(.*?)(\\}|)\\s*$'
     'beginCaptures':
-      '0':
-        'name': 'support.weave'
-    'end': '^(`|~){3,}\\s*$'
+      '1':
+        'name': 'markup.heading.weave.md'
+      '3':
+        'name': 'markup.bold.weave.md'
+      '5':
+        #For some reason highlighting for options doesn't work
+        #with source.julia, cheat using python
+        'contentName' : 'source.python'
+        'patterns': [
+          {
+            'include': 'source.python'
+          }
+        ]
+    'end': '^[`~]{3,}\\s*$'
     'endCaptures':
       '0':
-        'name': 'support.weave'
-    'name': 'markup.code.julia.weave'
+        'name': 'markup.heading.weave.md'
     'contentName': 'source.julia'
     'patterns': [
       {


Finally getting the elasticsearch query working

--- src/scripts/elasticsearch.coffee
+++ src/scripts/elasticsearch.coffee
@@ -19,9 +19,8 @@
     msg.http("http://#{server}/_search?#{query}")
       .get() (err, res, body) ->
         json = JSON.parse(body)
-        hits = json['hits']
-        totalhits = hits["hits"]
-        msg.send("There are #{totalhits} for the query \n#{query}")
+        firsthit = JSON.stringify(json.hits.hits[0])
+        msg.send("There are #{json.hits.total} for the search http://#{server}/_search?#{query} The first result is /n#{firsthit}")

   cluster_health = (msg, server) ->
     msg.http("http://#{server}/_cluster/health")


Check for null when there are no images

Check for null when there are no images

if I did something like "hubot image me ablederfl33wo", I would get an error like:

/opt/hubot/scripts/google-images.coffee:47
      images = images.responseData.results;
                                  ^
TypeError: Cannot read property 'results' of null
    at imageMe (/opt/hubot/scripts/google-images.coffee:47:35)
    at IncomingMessage.ScopedClient.request (/opt/hubot/node_modules/hubot/node_modules/scoped-http-client/lib/index.js:61:20)
    at IncomingMessage.EventEmitter.emit (events.js:126:20)
    at IncomingMessage._emitEnd (http.js:366:10)
    at HTTPParser.parserOnMessageComplete [as onMessageComplete] (http.js:149:23)
    at Socket.socketOnEnd [as onend] (http.js:1356:12)
    at TCP.onread (net.js:418:26)

--- src/scripts/google-images.coffee
+++ src/scripts/google-images.coffee
@@ -37,8 +37,8 @@
     .query(q)
     .get() (err, res, body) ->
       images = JSON.parse(body)
-      images = images.responseData.results
-      if images.length > 0
+      images = images.responseData?.results
+      if images?.length > 0
         image  = msg.random images
         cb "#{image.unescapedUrl}#.png"


В том случае если авто-прокрутка тумбнейлов выключена, не нужно делать дополнительных вычеслений

--- src/chocolate.coffee
+++ src/chocolate.coffee
@@ -318,12 +318,13 @@
     else
       offset = element - (width / 2) + (thumbnail / 2)

-    right = offset + width
-
-    if right < element
-      offset = thumbnail + left
-    else if offset > element
-      offset = 0
+    if @options.repeat
+      right = offset + width
+
+      if right < element
+        offset = thumbnail + element
+      else if offset > element
+        offset = 0

     @thumbnails.scrollLeft offset
     @


Removed some jquery

Removed some jquery

--- src/chocolate.coffee
+++ src/chocolate.coffee
@@ -5,41 +5,6 @@
 isHistory = not not (window.history and history.pushState)
 isStorage = 'localStorage' of window and window['localStorage']?
 isIE8     = document.documentMode? and document.documentMode is 8
-
-getTarget = (element, selector) ->
-  if selector?
-    target = element.querySelector selector
-  else
-    target = element
-  target
-
-addHandler = (element, event, listener, selector) ->
-  target = getTarget element, selector
-  if target?
-    unless event instanceof Array
-      event = [event]
-    for ev in event
-      target.addEventListener ev, listener, false
-
-addClass = (element, className, selector) ->
-  target = getTarget element, selector
-  if target.hasOwnProperty 'classList'
-    target.classList.add className
-
-removeClass = (element, className, selector) ->
-  target = getTarget element, selector
-  if target.hasOwnProperty 'classList'
-    target.classList.remove className
-
-toggleClass = (element, className, selector) ->
-  target = getTarget element, selector
-  if target.hasOwnProperty 'classList'
-    target.classList.toggle className
-
-hasClass = (element, className, selector) ->
-  target = getTarget element, selector
-  if target.hasOwnProperty 'classList'
-    target.classList.contains className

 class Chocolate
   images: {}
@@ -68,9 +33,8 @@
     template = template.replace '{{spinner}}', templates['spinner']
     template = template.replace '{{thumbnails}}', if @options.thumbnails then templates['thumbnails'] else ''

-    # jQuery
-    @overlay = $(template).appendTo 'body'
-    @overlay = @overlay[0]
+    document.body.insertAdjacentHTML 'beforeend', template
+    @overlay = document.body.lastChild

     ###
      Получаем необходимые контейнеры
@@ -144,7 +108,6 @@
       isElement = if typeof HTMLElement is 'object' then object instanceof HTMLElement else typeof object is 'object' and object.nodeType is 1 and typeof object.nodeName is 'string'

       if isElement
-        #jquery
         image  = object
         object =
           source:    image.getAttribute('data-src') or image.parentNode.getAttribute('href')
@@ -180,7 +143,7 @@
       history.pushState null, null, '#' if isHistory

       if @options.thumbnails
-        @thumbnails.html ''
+        @thumbnails.innerHTML = ''
         $(@overlay.querySelector('.choco-thumbnails-toggle')).off 'click'

       $(window).off 'resize'
@@ -239,11 +202,11 @@

       @updateDimensions image.width, image.height

-      @container.css
+      $(@container).css
         'background-image': 'url(' + image.source + ')'
         '-ms-filter': "\"progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + image.source + "',sizingMethod='scale')\""

-      $(@header).html if image.title then templates['image-title'].replace '{{title}}', image.title else ''
+      @header.innerHTML = if image.title then templates['image-title'].replace '{{title}}', image.title else ''
     @

   ###
@@ -259,7 +222,7 @@

       element = new Image()

-      element.onload = =>
+      addHandler element, 'load', =>
         if cid is @current
           @images[cid].width  = element.width
           @images[cid].height = element.height
@@ -268,12 +231,12 @@

           fn()

-      element.onerror = =>
+      addHandler element, 'error', =>
         if cid is @current
           addClass @spinner, 'choco-hide'
           addClass @container, 'choco-show'
           addClass @container, 'choco-error'
-          @updateDimensions $(@container).width(), $(@container).height()
+          @updateDimensions @container.offsetWidth, @container.offsetHeight

       element.src = image.source

@@ -289,7 +252,7 @@
     thumbnails = headerHeight = 0

     if @dimensions.thumbnails isnt false and not hasClass @thumbnails, 'choco-hide'
-      @dimensions.thumbnails = $(@thumbnails).height() if @dimensions.thumbnails is 0
+      @dimensions.thumbnails = @thumbnails.offsetHeight if @dimensions.thumbnails is 0
       thumbnails = @dimensions.thumbnails

     headerHeight = @dimensions.header if title
@@ -321,7 +284,8 @@
     $(@rightside).css style

     if title
-      @header.addClass('choco-show').css
+      addClass @header, 'choco-show'
+      $(@header).css
         'width':       toInt width
         'margin-left': '-' + toInt(left) + 'px'
         'margin-top':  '-' + toInt(top + headerHeight) + 'px'
@@ -366,20 +330,23 @@
     addHandler @overlay, 'click', ->
       current = _this.images[_this.current]
       status  = hasClass _this.thumbnails, 'choco-hide'
-      method  = if status then 'removeClass' else 'addClass'

       if isStorage
         localStorage.setItem 'choco-thumbnails', if status then 1 else 0

-      _this.thumbnails[method] 'choco-hide'
-      $(@)[method] 'choco-hide'
+      if status
+        removeClass _this.thumbnails, 'choco-hide'
+        removeClass this, 'choco-hide'
+      else
+        addClass _this.thumbnails, 'choco-hide'
+        addClass this, 'choco-hide'

       _this.updateDimensions current.width, current.height if current
     , '.choco-thumbnails-toggle'

     if isStorage and not hasClass @thumbnails, 'choco-hide'
       status = localStorage.getItem('choco-thumbnails') or 1
-      $(@overlay).find('.choco-thumbnails-toggle').trigger 'click' if toInt(status) is 0
+      @overlay.querySelector('.choco-thumbnails-toggle').click() if toInt(status) is 0

     @

@@ -456,9 +423,9 @@
         $(image).after templates['image-hover'].replace '{{cid}}', cid

         popover = $('[data-pid="' + cid + '"]').css
-          'width':      $(image).width()
-          'height':     $(image).height()
-          'margin-top': '-' + $(image).height() + 'px'
+          'width':      image.offsetWidth
+          'height':     image.offsetHeight
+          'margin-top': -1 * image.offsetHeight + 'px'

         addHandler image, ['mouseenter', 'mouseleave'], ->
           toggleClass popover[0], 'choco-hover'
@@ -475,7 +442,7 @@
     method = @options.actions[container] if @options.actions[container] in existActions

     if method
-      verify = @[container].getAttribute 'class'
+      verify = @[container].classList[0]

       addHandler @[container], 'click', (event) =>
         @[method]() if hasClass event.target, verify
@@ -545,6 +512,42 @@
     width

+getTarget = (element, selector) ->
+  if selector?
+    target = element.querySelector selector
+  else
+    target = element
+  target
+
+addHandler = (element, event, listener, selector) ->
+  target = getTarget element, selector
+  if target?
+    unless event instanceof Array
+      event = [event]
+    for ev in event
+      target.addEventListener ev, listener, false
+
+addClass = (element, className, selector) ->
+  target = getTarget element, selector
+  if target.hasOwnProperty 'classList'
+    target.classList.add className
+
+removeClass = (element, className, selector) ->
+  target = getTarget element, selector
+  if target.hasOwnProperty 'classList'
+    target.classList.remove className
+
+toggleClass = (element, className, selector) ->
+  target = getTarget element, selector
+  if target.hasOwnProperty 'classList'
+    target.classList.toggle className
+
+hasClass = (element, className, selector) ->
+  target = getTarget element, selector
+  if target.hasOwnProperty 'classList'
+    target.classList.contains className
+
+
 toInt = (string) -> parseInt string, 10

 cssAnimationsSupport = ->


Clampable#clampToBounds

Clampable#clampToBounds

--- src/clampable.coffee
+++ src/clampable.coffee
@@ -10,4 +10,4 @@
     Object.extend(I.clampData, data)

   clampToBounds: (bounds) ->
-    bounds ||= Rectangle x: 0, 0, +    bounds ||= Rectangle x: 0, y: 0, A


fixing attach_cm

fixing attach_cm

--- src/client/cm.coffee
+++ src/client/cm.coffee
@@ -39,14 +39,14 @@
   check = ->
     window.setTimeout ->
         editorText = editor.getValue()
-        otText = sharedoc.getValue()
+        otText = sharedoc.getText()

         if editorText != otText
           console.error "Text does not match!"
           console.error "editor: #{editorText}"
           console.error "ot:     #{otText}"
           # Replace the editor text with the doc snapshot.
-          editor.setValue sharedoc.getValue()
+          editor.setValue sharedoc.getText()
       , 0

   if keepEditorContents


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/collision.coffee
+++ src/collision.coffee
@@ -215,7 +215,7 @@
     @returns {Boolean} true if the line intersects the rectangle, false otherwise
     ###
     rayRectangle: (source, direction, target) ->
-      if target.
+      unr target.
       xw = target.xw
       yw = target.yw


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/collision.coffee
+++ src/collision.coffee
@@ -216,7 +216,7 @@
     ###
     rayRectangle: (source, direction, target) ->
       unless target.xw? and target.yw?
-        if
+
         return
       xw = target.xw
       yw = target.yw


fixes

fixes

--- src/brain.coffee
+++ src/brain.coffee
@@ -10,23 +10,23 @@
   #
   # Returns a new Brain with no external storage.
   constructor: (@robot) ->
-    @users = new Map()
-    @data = new Map()
+    @_users = new Map()
+    @_data = new Map()
     @ready = Q(@)

   # Reset the datastore. destroys all data.
   #
   # returns promise
   reset: ->
-    @users = new Map()
-    @data = new Map()
+    @_users = new Map()
+    @_data = new Map()
     Q()

   # get the length of the list stored at `key`
   #
   # Returns promise for int
   llen: (key) ->
-    list = @data.get(@key(key))
+    list = @_data.get(@key(key))

     if list is undefined
       Q(null)
@@ -38,11 +38,11 @@
   # Returns promise
   lset: (key, index, value) ->
     key = @key(key)
-    list = @data.get(key)
+    list = @_data.get(key)

     if list is undefined
       list = []
-      @data.set(key, list)
+      @_data.set(key, list)

     list[index] = @serialize(value)
     Q()
@@ -52,7 +52,7 @@
   # Returns promise
   linsert: (key, placement, pivot, value) ->
     key = @key(key)
-    list = @data.get(key)
+    list = @_data.get(key)

     if list isnt undefined
       pivot = @serialize(pivot)
@@ -71,11 +71,11 @@
   # Returns promise
   lpush: (key, value) ->
     key = @key(key)
-    list = @data.get(key)
+    list = @_data.get(key)

     if list is undefined
       list = []
-      @data.set(key, list)
+      @_data.set(key, list)

     list.unshift(@serialize(value))
     Q()
@@ -85,11 +85,11 @@
   # Returns promise
   rpush: (key, value) ->
     key = @key(key)
-    list = @data.get(key)
+    list = @_data.get(key)

     if list is undefined
       list = []
-      @data.set(key, list)
+      @_data.set(key, list)

     list.push(@serialize(value))
     Q()
@@ -98,19 +98,19 @@
   #
   # Returns promise for list item
   lpop: (key) ->
-    Q(@deserialize(@data.get(@key(key))?.shift()))
+    Q(@deserialize(@_data.get(@key(key))?.shift()))

   # pop a value off of the right-side of the list
   #
   # Returns promise for list item
   rpop: (key) ->
-    Q(@deserialize(@data.get(@key(key))?.pop()))
+    Q(@deserialize(@_data.get(@key(key))?.pop()))

   # get a list item by index
   #
   # Returns promise for list item
   lindex: (key, index) ->
-    Q(@deserialize(@data.get(@key(key))?[index] or null))
+    Q(@deserialize(@_data.get(@key(key))?[index] or null))

   # get an entire list
   #
@@ -122,7 +122,7 @@
   #
   # Returns promise for array
   lrange: (key, start, end) ->
-    list = @data.get(@key(key))
+    list = @_data.get(@key(key))

     if list is undefined
       return Q(null)
@@ -136,7 +136,7 @@
   #
   # Returns promise
   lrem: (key, value) ->
-    list = @data.get(@key(key))
+    list = @_data.get(@key(key))

     if list
       value = @serialize(value)
@@ -150,11 +150,11 @@
   # Returns promise
   sadd: (key, value) ->
     key = @key(key)
-    set = @data.get(key)
+    set = @_data.get(key)

     if set is undefined
       set = new Set()
-      @data.set(key, set)
+      @_data.set(key, set)

     set.add(@serialize(value))

@@ -164,7 +164,7 @@
   #
   # Returns promise for boolean
   sismember: (key, value) ->
-    set = @data.get(@key(key))
+    set = @_data.get(@key(key))
     if not set
       Q(null)
     else
@@ -174,7 +174,7 @@
   #
   # Returns promise
   srem: (key, value) ->
-    set = @data.get(@key(key))
+    set = @_data.get(@key(key))

     if set
       set.delete(@serialize(value))
@@ -185,7 +185,7 @@
   #
   # Returns promise for int
   scard: (key) ->
-    set = @data.get(@key(key))
+    set = @_data.get(@key(key))
     if set is undefined
       Q(null)
     else
@@ -195,7 +195,7 @@
   #
   # Returns promise for a set member
   spop: (key) ->
-    set = @data.get(@key(key))
+    set = @_data.get(@key(key))

     if set is undefined
       return Q(null)
@@ -211,7 +211,7 @@
   #
   # Returns promise for a set member
   srandmember: (key) ->
-    set = @data.get(@key(key))
+    set = @_data.get(@key(key))
     if set is undefined or set.size is 0
       return Q(null)

@@ -221,14 +221,14 @@
   #
   # Returns promise for array
   smembers: (key) ->
-    @data.get(@key(key))?.values() or null
+    @_data.get(@key(key))?.values() or null

   # get all the keys, optionally restricted to keys prefixed with `searchKey`
   #
   # Returns promise for array
   keys: (searchKey = '') ->
     searchKey = @key(searchKey)
-    Q(_.map(_.filter(@data.keys(), (key) -> key.indexOf(searchKey) is 0), @unkey.bind(@)))
+    Q(_.map(_.filter(@_data.keys(), (key) -> key.indexOf(searchKey) is 0), @unkey.bind(@)))

   # transform a key from internal brain key, to user-facing key
   #
@@ -254,21 +254,21 @@
   #
   # Returns promise
   set: (key, value) ->
-    @data.set(@key(key), @serialize(value))
-    Q()
-
-  # Get value by key from the private namespace in @data
+    @_data.set(@key(key), @serialize(value))
+    Q()
+
+  # Get value by key from the private namespace in @_data
   # or return null if not found.
   #
   # Returns promise
   get: (key) ->
-    Q(@deserialize(@data.get(@key(key)) ? null))
+    Q(@deserialize(@_data.get(@key(key)) ? null))

   # Get the type of the value at `key`
   #
   # Returns promise
   type: (key) ->
-    val = @deserialize(@data.get(@key(key)))
+    val = @deserialize(@_data.get(@key(key)))

     if val is undefined
       null
@@ -276,7 +276,7 @@
       'hash'
     else if val instanceof Set
       'set'
-    else if _.isArray(val)
+    else if val instanceof Array
       'list'
     else
       'object'
@@ -285,27 +285,27 @@
   #
   # Return promise for boolean
   exists: (key) ->
-    Q(@data.has(@key(key)))
+    Q(@_data.has(@key(key)))

   # increment the value by num atomically
   #
   # Returns promise
   incrby: (key, num) ->
     key = @key(key)
-    @data.set(key, (@data.get(key) or 0) + num)
-    Q(@data.get(key))
+    @_data.set(key, (@_data.get(key) or 0) + num)
+    Q(@_data.get(key))

   # Get all the keys for the given hash table name
   #
   # Returns promise for array.
   hkeys: (table) ->
-    Q(@data.get(@key(table))?.keys() or null)
+    Q(@_data.get(@key(table))?.keys() or null)

   # Get all the values for the given hash table name
   #
   # Returns promise for array.
   hvals: (table) ->
-    val = @data.get(@key(table))
+    val = @_data.get(@key(table))
     if val is undefined
       Q(null)
     else
@@ -315,7 +315,7 @@
   #
   # Returns promise for int
   hlen: (table) ->
-    val = @data.get(@key(table))
+    val = @_data.get(@key(table))
     if val is undefined
       Q(null)
     else
@@ -326,11 +326,11 @@
   # Returns promise for the value.
   hset: (table, key, value) ->
     table = @key(table)
-    val = @data.get(table)
+    val = @_data.get(table)

     if val is undefined
       val = new Map()
-      @data.set(table, val)
+      @_data.set(table, val)

     val.set(key, @serialize(value))

@@ -340,7 +340,7 @@
   #
   # Returns: promise for the value.
   hget: (table, key) ->
-    val = @data.get(@key(table))
+    val = @_data.get(@key(table))
     if val is undefined
       Q(null)
     else
@@ -350,7 +350,7 @@
   #
   # Returns promise
   hdel: (table, key) ->
-    val = @data.get(@key(table))
+    val = @_data.get(@key(table))
     if val isnt undefined
       val.delete(key)
     Q()
@@ -359,28 +359,27 @@
   #
   # Returns: promise for Map.
   hgetall: (table) ->
-    Q(new Map(@data.get(@key(table))?.entries() or null))
+    Q(new Map(@_data.get(@key(table))?.entries() or null))

   # increment the hash value by num atomically
   #
   # Returns promise
   hincrby: (table, key, num) ->
     table = @key(table)
-    val = @data.get(table)
+    val = @_data.get(table)

     if val is undefined
       val = new Map()
-      @data.set(table, val)
-
-    val.set(key, (val.get(key) or 0) + num
+      @_data.set(table, val)
+
+    val.set(key, (val.get(key) or 0) + num)
     Q(val.get(key))

-  # Remove value by key from the private namespace in @data
-  # if it exists
+  # delete the value at `key`
   #
   # Returns promise
   remove: (key) ->
-    @data.delete(@key(key))
+    @_data.delete(@key(key))
     Q()
   # alias for remove
   del: (key) ->
@@ -396,32 +395,32 @@
   #
   # Returns serialized value
   serialize: (value) ->
-    JSON.stringify(value)
+    value

   # Perform any necessary post-get deserialization on a value
   #
   # Returns deserialized value
   deserialize: (value) ->
-    JSON.parse(value)
+    value

   # Get an Array of User objects stored in the brain.
   #
   # Returns promise for an Array of User objects.
   users: ->
-    Q(@users)
+    Q(@_users.values())

   # Add a user to the data-store
   #
   # Returns promise for user
   addUser: (user) ->
-    @users.set(user.id, user)
+    @_users.set(user.id, user)
     Q(user)

   # Get or create a User object given a unique identifier.
   #
   # Returns promise for a User instance of the specified user.
   userForId: (id, options) ->
-    user = @users.get(id)
+    user = @_users.get(id)

     if user is undefined or (options and options.room and (user.room isnt options.room))
       return @addUser(new User(id, options))
@@ -433,7 +432,7 @@
   # Returns promise for a User instance for the user with the specified name.
   userForName: (name) ->
     lowerName = name.toLowerCase()
-    user = _.find @users.values(), (user) ->
+    user = _.find @_users.values(), (user) ->
       user.name and user.name.toString().toLowerCase() is lowerName
     Q(user or null)

@@ -444,7 +443,7 @@
   # Returns promise an Array of User instances matching the fuzzy name.
   usersForRawFuzzyName: (fuzzyName) ->
     lowerFuzzyName = fuzzyName.toLowerCase()
-    users = _.filter @users.values(), (user) ->
+    users = _.filter @_users.values(), (user) ->
       user.name.toLowerCase().lastIndexOf(lowerFuzzyName, 0) is 0

     Q(users)


新增cola.util.concatUrl方法

新增cola.util.concatUrl方法

--- src/core/util.coffee
+++ src/core/util.coffee
@@ -17,6 +17,21 @@
 	if value == null or value == undefined then return false
 	type = typeof value
 	return type != "object" and type != "function" or type instanceof Date
+
+cola.util.concatUrl = (parts...) ->
+	last = parts.length - 1
+	for part, i in parts
+		changed = false
+		if i > 0 and part.charCodeAt(0) is 47 # `/`
+			part = part.substring(1)
+			changed = true
+
+		if i < last and part.charCodeAt(part.length - 1) is 47 # `/`
+			part = part.substring(0, part.length - 1)
+			changed = true
+
+		if changed then parts[i] = part
+	return parts.join("/")

 cola.util.parseStyleLikeString = (styleStr, headerProp) ->
 	return false unless styleStr


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -306,7 +306,7 @@

     ###*
     A getter / setter for the hue value of the color. Passing no argument returns the
-    current hue value. Passing a value will set the hue to that value.
+    current hue value. Passing a value will set the hue to that value and return the color.

     @name hue
     @methodOf Color#
@@ -325,6 +325,16 @@
       else
         return hsl[0]

+    ###*
+    A getter / setter for the lightness value of the color. Passing no argument returns the
+    current lightness value. Passing a value will set the lightness to that value and return the color.
+
+    @name hue
+    @methodOf Color#
+    @param {Number} [newVal] the new hue value
+
+    @returns {Color|Number} returns the color object if you pass a new hue value and returns the hue otherwise
+    ###
     lightness: (newVal) ->
       hsl = @toHsl()
       if newVal?


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -921,6 +921,26 @@

       return this

+    ###*
+    A copy of the calling color with its alpha increased by `amount`.
+
+

+    color = Color(0, 0, 0, 0)
+
+    color.a
+    # => 1
+
+    opaqueColor = color.opacify(0.25)
+
+    opaqueColor.a
+    # => 0.25
+

+
+    @name opacify
+    @methodOf Color#
+
+    @returns {Color} A copy of the calling color with its alpha increased by `amount`
+    ###
     opacify: (amount) ->
       @copy().opacify$(amount)