Fix space handling in <set> tag

Fix space handling in <set> tag

--- src/brain.coffee
+++ src/brain.coffee
@@ -621,11 +621,11 @@
         break # No remaining tags!

       match = match[1]
-      parts = match.split(" ", 2)
+      parts = match.split(" ")
       tag   = parts[0].toLowerCase()
       data  = ""
       if parts.length > 1
-        data = parts[1]
+        data = parts.slice(1).join(" ")
       insert = ""

       # Handle the tags.


extract method: branch, leaf, chainWithChainIndicator #347

--- app/assets/javascripts/oxalis/view/skeletontracing/abstract_tree_renderer.coffee
+++ app/assets/javascripts/oxalis/view/skeletontracing/abstract_tree_renderer.coffee
@@ -86,55 +86,18 @@

   drawTreeWithWidths : (tree, left, right, top, mode) ->

-    # get the decision point
     decision = @getNextDecision(tree)
-    middle = @calculateTreeMiddle(decision, left, right)
-    #middle = middle # if decisionPoint is leaf, there's not much to do
-    topChildren = @calculateChildTreeTop(mode, decision.chainCount, top)
-
-    # if the decision point has 2 children, draw them and remember their position
+    middle = @calculateMiddle(left, right)
+
     if decision.isBranch
-
-      leftTree = @drawTreeWithWidths(decision.node.children[0], left,  middle, topChildren, mode)
-      rightTree = @drawTreeWithWidths(decision.node.children[1], middle, right, topChildren, mode)
-
-      # set the root's x coordinate to be in between the decisionPoint's children
-      middle = (leftTree.middle + rightTree.middle) / 2
-
-      # draw edges from last node in 'chain' (or root, if chain empty)
-      # and the decisionPoint's children
-      @drawEdge(middle, topChildren - @nodeDistance, leftTree.middle, leftTree.top)
-      @drawEdge(middle, topChildren - @nodeDistance, rightTree.middle, rightTree.top)
-
+      middle = @drawBranch(decision, left, right, top, mode)
     else if !decision.isLeaf
-
-      childNode = decision.node.children[0]
-      nodeHasComment = @nodeHasComment(decision.node.id)
-
-      if nodeHasComment
-        childNode = decision.node
-        topChildren -= @nodeDistance
-
-      childTree = @drawTreeWithWidths(childNode, left, right, topChildren, mode)
-
-      if !nodeHasComment
-        @drawEdge(middle, topChildren - @nodeDistance, childTree.middle, childTree.top)
-
+      middle = @drawCommentChain(decision, left, right, top, mode)

     if mode == @MODE_NORMAL or decision.chainCount < 3
       @drawChainFromTo(top, middle, tree, decision)
-
     else if mode == @MODE_NOCHAIN
-
-      hasActiveNode = @chainContainsActiveNode(tree, decision.node)
-
-      # Draw root, chain indicator and decision point
-      @drawNode(middle, top, tree.id)
-      @drawEdge(middle, top, middle, top + 0.5 * @nodeDistance)
-      @drawChainIndicator(middle, top + 0.5 * @nodeDistance, top + 1.5 * @nodeDistance, hasActiveNode)
-      @drawEdge(middle, top + 1.5 * @nodeDistance, middle, top + 2 * @nodeDistance)
-      @drawNode(middle, top + 2 * @nodeDistance, decision.node.id)
-
+      @drawChainWithChainIndicatorFromTo(top, middle, tree, decision)

     return { middle, top }

@@ -146,12 +109,14 @@
    * @param  {Number} right          right border in pixels
    * @return {Number}                middle in pixels
   ###
-  calculateTreeMiddle : (decision, left, right) ->
-
-    if decision.isBranch
-      leftChild = decision.node.children[0]
-      rightChild = decision.node.children[1]
-      return (right - left) * leftChild.width / (leftChild.width + rightChild.width) + left
+  calculateBranchMiddle : (decision, left, right) ->
+
+    leftChild = decision.node.children[0]
+    rightChild = decision.node.children[1]
+    return (right - left) * leftChild.width / (leftChild.width + rightChild.width) + left
+
+
+  calculateMiddle : (left, right) ->

     return (left + right) / 2

@@ -224,6 +189,43 @@
     @ctx.lineWidth = 1

+  drawCommentChain : (decision, left, right, top, mode) ->
+
+    middle = @calculateMiddle(left, right)
+    topChild = @calculateChildTreeTop(mode, decision.chainCount, top)
+    childNode = decision.node.children[0]
+    nodeHasComment = @nodeHasComment(decision.node.id)
+
+    if nodeHasComment
+      childNode = decision.node
+      topChild -= @nodeDistance
+
+    childTree = @drawTreeWithWidths(childNode, left, right, topChild, mode)
+
+    if !nodeHasComment
+      @drawEdge(middle, topChild - @nodeDistance, childTree.middle, childTree.top)
+
+    return middle
+
+
+  drawBranch : (decision, left, right, top, mode) ->
+
+    branchMiddle = @calculateBranchMiddle(decision, left, right)
+    topChildren = @calculateChildTreeTop(mode, decision.chainCount, top)
+    leftTree = @drawTreeWithWidths(decision.node.children[0], left,  branchMiddle, topChildren, mode)
+    rightTree = @drawTreeWithWidths(decision.node.children[1], branchMiddle, right, topChildren, mode)
+
+    # set the root's x coordinate to be in between the decisionPoint's children
+    middle = @calculateMiddle(leftTree.middle, rightTree.middle)
+
+    # draw edges from last node in 'chain' (or root, if chain empty)
+    # and the decisionPoint's children
+    @drawEdge(middle, topChildren - @nodeDistance, leftTree.middle, leftTree.top)
+    @drawEdge(middle, topChildren - @nodeDistance, rightTree.middle, rightTree.top)
+
+    return middle
+
+
   drawChainFromTo : (top, left, root, decision) ->

     # Draw the chain and the root, connect them.
@@ -235,6 +237,18 @@
         @drawEdge(left, top + (i - 1) * @nodeDistance, left, top + i * @nodeDistance)

+  drawChainWithChainIndicatorFromTo : (top, middle, tree, decision) ->
+
+    hasActiveNode = @chainContainsActiveNode(tree, decision.node)
+
+    # Draw root, chain indicator and decision point
+    @drawNode(middle, top, tree.id)
+    @drawEdge(middle, top, middle, top + 0.5 * @nodeDistance)
+    @drawChainIndicator(middle, top + 0.5 * @nodeDistance, top + 1.5 * @nodeDistance, hasActiveNode)
+    @drawEdge(middle, top + 1.5 * @nodeDistance, middle, top + 2 * @nodeDistance)
+    @drawNode(middle, top + 2 * @nodeDistance, decision.node.id)
+
+
   getNextDecision : (tree) ->

     # Decision point is any point with point.children.length != 1


Cleanup the whole DispatcherSpec. - Don’t expect implementation details if not necessary. Test the normal stack by publishing router:match events. - Use helpers and variables so the code is more readable. Use to.be.eql tests for params/route/options where applicable.

--- test/spec/dispatcher_spec.coffee
+++ test/spec/dispatcher_spec.coffee
@@ -9,16 +9,16 @@

   describe 'Dispatcher', ->
     # Initialize shared variables
-    dispatcher = params = options = stdOptions = route1 = route2 = null
-
-    # Fake route objects, walk like a route and swim like a route
-
-    redirectToURLRoute =
-      controller: 'test1', action: 'redirectToURL'
-    redirectToControllerRoute =
-      controller: 'test1', action: 'redirectToController'
-
-    # Define test controllers
+    dispatcher = params = path = options = stdOptions = route1 = route2 =
+      redirectToURLRoute = redirectToControllerRoute = null
+
+    # Default options which are added on first dispatching
+
+    addedOptions =
+      changeURL: true
+      forceStartup: false
+
+    # Test controllers

     class Test1Controller extends Controller
       initialize: (params, options) ->
@@ -47,29 +47,20 @@
         #console.debug 'Test2Controller#dispose'
         super

+    # Define an AMD module and return a loader function
+    makeLoadController = (moduleName, module) ->
+      moduleName = "controllers/#{moduleName}_controller"
+      define moduleName, -> module
+      (callback) ->
+        require [moduleName], callback
+
     # Define a test controller AMD modules
-    test1Module = 'controllers/test1_controller'
-    test2Module = 'controllers/test2_controller'
-    define test1Module, -> Test1Controller
-    define test2Module, -> Test2Controller
-
-    # Helpers for asynchronous tests
-    loadTest1Controller = (callback) -> require [test1Module], callback
-    loadTest2Controller = (callback) -> require [test2Module], callback
-
-    # Create a fresh Dispatcher instance for each test
-
-    beforeEach ->
-      dispatcher = new Dispatcher()
-
-    afterEach ->
-      dispatcher.dispose()
-      dispatcher = null
-
-    # Default options which are added on first dispatching
-    addedOptions =
-      changeURL: true
-      forceStartup: false
+    loadTest1Controller = makeLoadController 'test1', Test1Controller
+    loadTest2Controller = makeLoadController 'test2', Test2Controller
+
+    # Shortcut for publishing router:match events
+    publishMatch = ->
+      mediator.publish 'router:match', arguments...

     # Helper for creating params/options to compare with
     create = ->
@@ -79,28 +70,45 @@
     refreshParams = ->
       params = id: _.uniqueId('paramsId')
       path = "test/#{params.id}"
+      options = {}
+      stdOptions = create addedOptions
+
+      # Fake route objects, walk like a route and swim like a route
       route1 = {controller: 'test1', action: 'show', path}
       route2 = {controller: 'test2', action: 'show', path}
-      options = {}
-      stdOptions = create addedOptions
-
-    beforeEach refreshParams
+      redirectToURLRoute =
+        {controller: 'test1', action: 'redirectToURL', path}
+      redirectToControllerRoute =
+        {controller: 'test1', action: 'redirectToController', path}
+
+    # Register before/after handlers
+
+    beforeEach ->
+      # Create a fresh Dispatcher instance for each test
+      dispatcher = new Dispatcher()
+      refreshParams()
+
+    afterEach ->
+      if dispatcher
+        dispatcher.dispose()
+        dispatcher = null
+
+    # The Tests

     it 'should dispatch routes to controller actions', (done) ->
       proto = Test1Controller.prototype
       initialize = sinon.spy proto, 'initialize'
       action     = sinon.spy proto, 'show'

-      mediator.publish 'router:match', route1, params, create options
+      publishMatch route1, params, options

       loadTest1Controller ->
         for spy in [initialize, action]
           expect(spy).was.calledOnce()
-          args = spy.firstCall.args
-          expect(args[0]).to.eql params
-          expect(args[1].controller).to.eql route1.controller
-          expect(args[1].action).to.eql route1.action
-          expect(args[2]).to.eql stdOptions
+          [passedParams, passedRoute, passedOptions] = spy.firstCall.args
+          expect(passedParams).to.eql params
+          expect(passedRoute).to.eql create(route1, previous: {})
+          expect(passedOptions).to.eql stdOptions

         initialize.restore()
         action.restore()
@@ -108,14 +116,14 @@
         done()

     it 'should not start the same controller if params match', (done) ->
-      mediator.publish 'router:match', route1, params, options
+      publishMatch route1, params, options

       loadTest1Controller ->
         proto = Test1Controller.prototype
         initialize = sinon.spy proto, 'initialize'
         action     = sinon.spy proto, 'show'

-        mediator.publish 'router:match', route1, params, options
+        publishMatch route1, params, options

         loadTest1Controller ->
           expect(initialize).was.notCalled()
@@ -131,14 +139,14 @@
       initialize = sinon.spy proto, 'initialize'
       action     = sinon.spy proto, 'show'

-      paramsStore = [params]
-      optionsStore = [options]
-      mediator.publish 'router:match', route1, params, options
-
-      refreshParams()
-      paramsStore.push params
-      optionsStore.push options
-      mediator.publish 'router:match', route1, params, options
+      paramsStore = []
+      optionsStore = []
+
+      for i in [0..1]
+        refreshParams()
+        paramsStore.push params
+        optionsStore.push options
+        publishMatch route1, params, options

       loadTest1Controller ->
         expect(initialize).was.calledTwice()
@@ -146,17 +154,13 @@

         for i in [0..1]
           for spy in [initialize, action]
-            args = spy.args[i]
-            expect(args[0]).to.eql paramsStore[i]
-            expectedOptions = create optionsStore[i], {
-              changeURL: true
-              forceStartup: false
-            }
-            expect(args[1].controller).to.eql route1.controller
-            expect(args[1].action).to.eql route1.action
-            if i isnt 0
-              expect(args[1].previous.controller).to.eql 'test1'
-            expect(args[2]).to.eql expectedOptions
+            [passedParams, passedRoute, passedOptions] = spy.args[i]
+            expect(passedParams).to.eql paramsStore[i]
+            expect(passedRoute.controller).to.eql route1.controller
+            expect(passedRoute.action).to.eql route1.action
+            if i is 1
+              expect(passedRoute.previous.controller).to.eql route1.controller
+            expect(passedOptions).to.eql stdOptions

         initialize.restore()
         action.restore()
@@ -168,93 +172,99 @@
       initialize = sinon.spy proto, 'initialize'
       action     = sinon.spy proto, 'show'

-      paramsStore = [params]
-      optionsStore = [options]
-      mediator.publish 'router:match', route1, params, options
-
-      refreshParams()
-      paramsStore.push params
-      optionsStore.push options
-      options.forceStartup = true
-      mediator.publish 'router:match', route1, params, options
+      paramsStore = []
+      optionsStore = []
+
+      for i in [0..1]
+        refreshParams()
+        paramsStore.push params
+        optionsStore.push options
+        options.forceStartup = true if i is 1
+        publishMatch route1, params, options

       loadTest1Controller ->
         for i in [0..1]
           for spy in [initialize, action]
-            args = spy.args[i]
-            expect(args[0]).to.eql paramsStore[i]
+            [passedParams, passedRoute, passedOptions] = spy.args[i]
+            expect(passedParams).to.eql paramsStore[i]
+            expect(passedRoute.controller).to.be route1.controller
+            expect(passedRoute.action).to.be route1.action
             expectedOptions = create optionsStore[i], {
               changeURL: true
               forceStartup: (if i is 0 then false else true)
             }
-            expect(args[2]).to.eql expectedOptions
+            expect(passedOptions).to.eql expectedOptions

         initialize.restore()
         action.restore()

         done()

-    it 'should save the controller, action, params and url', (done) ->
-      mediator.publish 'router:match', route1, params, options
-      mediator.publish 'router:match', route2, params, options
+    it 'should save the controller, action, params and path', (done) ->
+      publishMatch route1, params, options
+      publishMatch route2, params, options

       # Check that previous route is saved
       loadTest1Controller -> loadTest2Controller ->
         d = dispatcher
         expect(d.previousRoute.controller).to.be 'test1'
         expect(d.currentController).to.be.a Test2Controller
-        expect(d.currentRoute.controller).to.be 'test2'
-        expect(d.currentRoute.action).to.be 'show'
+        expect(d.currentRoute).to.eql create(route2, previous: {})
         expect(d.currentParams).to.eql params
-        expect(d.currentRoute.path).to.be "test/#{params.id}"

         done()

     it 'should add the previous controller name to the route', (done) ->
       action = sinon.spy Test2Controller.prototype, 'show'

-      mediator.publish 'router:match', route1, params, options
-      mediator.publish 'router:match', route2, params, options
-
-      loadTest1Controller -> loadTest2Controller ->
-        expect(action).was.calledOnce()
-        route = action.firstCall.args[1]
-        expect(route).to.be.an 'object'
-        expect(route.previous.controller).to.be 'test1'
-
-        action.restore()
-
-        done()
+      publishMatch route1, params, options
+
+      loadTest1Controller ->
+        publishMatch route2, params, options
+
+        loadTest2Controller ->
+          expect(action).was.calledOnce()
+          route = action.firstCall.args[1]
+          expect(route.controller).to.be route2.controller
+          expect(route.action).to.be route2.action
+          expect(route.previous).to.be.an 'object'
+          expect(route.previous.controller).to.be route1.controller
+          expect(route.previous.action).to.be route1.action
+
+          action.restore()
+
+          done()

     it 'should dispose inactive controllers', (done) ->
       dispose = sinon.spy Test1Controller.prototype, 'dispose'
-      mediator.publish 'router:match', route1, params, options
-      mediator.publish 'router:match', route2, params, options
+      publishMatch route1, params, options
+      publishMatch route2, params, options

       loadTest1Controller -> loadTest2Controller ->
         # It should pass the params and the new controller name
         expect(dispose).was.calledOnce()
-        args = dispose.firstCall.args
-        expect(args[0]).to.eql params
-        expect(args[1].controller).to.eql route2.controller
-        expect(args[1].action).to.eql route2.action
-        expect(args[1].path).to.eql route2.path
+        [passedParams, passedRoute] = dispose.firstCall.args
+        expect(passedParams).to.eql params
+        expect(passedRoute.controller).to.eql route2.controller
+        expect(passedRoute.action).to.eql route2.action
+        expect(passedRoute.path).to.eql route2.path

         dispose.restore()

         done()

     it 'should fire beforeControllerDispose events', (done) ->
-      mediator.publish 'router:match', route1, params, options
+      publishMatch route1, params, options

       beforeControllerDispose = sinon.spy()
       mediator.subscribe 'beforeControllerDispose', beforeControllerDispose

       # Now route to Test2Controller
-      mediator.publish 'router:match', route2, params, options
+      publishMatch route2, params, options

       loadTest1Controller -> loadTest2Controller ->
         expect(beforeControllerDispose).was.calledOnce()
+
         # Event payload should be the now disposed controller
         passedController = beforeControllerDispose.firstCall.args[0]
         expect(passedController).to.be.a Test1Controller
@@ -268,31 +278,29 @@
       dispatch = sinon.spy()
       mediator.subscribe 'dispatcher:dispatch', dispatch

-      mediator.publish 'router:match', route1, params, options
-      mediator.publish 'router:match', route2, params, options
+      publishMatch route1, params, options
+      publishMatch route2, params, options

       loadTest1Controller -> loadTest2Controller ->
         expect(dispatch).was.calledTwice()

         for i in [0..1]
+          firstCall = i is 0
           args = dispatch.getCall(i).args
           expect(args.length).to.be 4
-          expect(args[2].previous.controller).to.be(
-            if i is 0 then undefined else 'test1'
+          [passedController, passedParams, passedRoute, passedOptions] = args
+          expect(passedController).to.be.a(
+            if firstCall then Test1Controller else Test2Controller
           )
-          expect(args[0]).to.be.a(
-            if i is 0 then Test1Controller else Test2Controller
+          expect(passedParams).to.eql params
+          expect(passedRoute.controller).to.be(
+            if firstCall then 'test1' else 'test2'
           )
-          expect(args[2].controller).to.be(
-            if i is 0 then 'test1' else 'test2'
+          expect(passedRoute.action).to.be 'show'
+          expect(passedRoute.previous.controller).to.be(
+            if firstCall then undefined else 'test1'
           )
-          expect(args[1]).to.eql params
-          expect(args[3]).to.eql(
-            if i is 0
-              create(stdOptions)
-            else
-              create(stdOptions)
-          )
+          expect(passedOptions).to.eql stdOptions

         mediator.unsubscribe 'dispatcher:dispatch', dispatch

@@ -305,13 +313,13 @@
       path = 'my-little-path'
       routeA = create route1, {path}
       options = {}
-      mediator.publish 'router:match', routeA, params, options
+      publishMatch routeA, params, options

       loadTest1Controller ->
         expect(spy).was.calledOnce()
-        args = spy.firstCall.args
-        expect(args[0]).to.be path
-        expect(args[1]).to.eql addedOptions
+        [passedPath, passedOptions] = spy.firstCall.args
+        expect(passedPath).to.be path
+        expect(passedOptions).to.eql stdOptions

         mediator.unsubscribe '!router:changeURL', spy

@@ -321,10 +329,7 @@
       spy = sinon.spy()
       mediator.subscribe '!router:changeURL', spy

-      options =
-        path: 'my-little-path', changeURL: false,
-        controller: 'test1', action: 'show'
-      mediator.publish 'router:match', route1, params, options
+      publishMatch route1, params, changeURL: false

       loadTest1Controller ->
         expect(spy).was.notCalled()
@@ -341,13 +346,13 @@
       query = 'foo=bar'

       routeB = create route1, {path, query}
-      mediator.publish 'router:match', routeB, params, options
+      publishMatch routeB, params, options

       loadTest1Controller ->
         expect(spy).was.calledOnce()
-        args = spy.firstCall.args
-        expect(args[0]).to.be "#{path}?#{query}"
-        expect(args[1]).to.eql addedOptions
+        [passedPath, passedOptions]  = spy.firstCall.args
+        expect(passedPath).to.be "#{path}?#{query}"
+        expect(passedOptions).to.eql stdOptions

         mediator.unsubscribe '!router:changeURL', spy

@@ -357,21 +362,22 @@
       dispatch = sinon.spy()
       mediator.subscribe 'dispatcher:dispatch', dispatch

-      # Open a route to check if previous controller info is correct after
+      # Dispatch a route to check if previous controller info is correct after
       # redirection
-      mediator.publish 'router:match', route1, params, options
+      publishMatch route1, params, options

       # Open another route that redirects somewhere
       refreshParams()
-      action = sinon.spy Test1Controller.prototype, 'redirectToURL'
-      mediator.publish 'router:match', redirectToURLRoute, params, options
+      actionName = 'redirectToURL'
+      action = sinon.spy Test1Controller.prototype, actionName
+      publishMatch redirectToURLRoute, params, options

       loadTest1Controller ->
         expect(action).was.calledOnce()
-        args = action.firstCall.args
-        expect(args[0]).to.eql params
-        expect(args[2]).to.eql create(stdOptions)
-        expect(args[1].previous.controller).to.eql 'test1'
+        [passedParams, passedRoute, passedOptions] = action.firstCall.args
+        expect(passedParams).to.eql params
+        expect(passedRoute.previous.controller).to.eql 'test1'
+        expect(passedOptions).to.eql stdOptions

         # Don’t expect that the new controller was called
         # because we’re not testing the router. Just test
@@ -380,8 +386,8 @@
         expect(d.previousRoute.controller).to.be 'test1'
         expect(d.currentRoute.controller).to.be 'test1'
         expect(d.currentController).to.be.a Test1Controller
-        expect(d.currentRoute.action).to.be 'redirectToURL'
-        expect(d.currentRoute.path).not.to.be "test/#{params.id}"
+        expect(d.currentRoute.action).to.be actionName
+        expect(d.currentRoute.path).to.be redirectToURLRoute.path

         expect(dispatch).was.calledOnce()

@@ -392,8 +398,8 @@

     it 'should dispose when redirecting to a URL', (done) ->
       dispose = sinon.spy Test1Controller.prototype, 'dispose'
-      mediator.publish 'router:match', route1, params, options
-      mediator.publish 'router:match', redirectToURLRoute, params, options
+      publishMatch route1, params, options
+      publishMatch redirectToURLRoute, params, options
       loadTest1Controller ->
         expect(dispose).was.calledOnce()
         dispose.restore()
@@ -404,7 +410,7 @@
       dispatcher.dispose()

       initialize = sinon.spy Test1Controller.prototype, 'initialize'
-      mediator.publish 'router:match', route1, params, options
+      publishMatch route1, params, options

       loadTest1Controller ->
         expect(initialize).was.notCalled()
@@ -428,96 +434,73 @@

     describe 'Before actions', ->

-      route = controller: 'before_actions', action: 'show', path: '/'
-
-      class BeforeActionsController extends Controller
+      class BeforeActionController extends Controller
         beforeAction: ->
         show: ->
-        index: ->
-
-      # Define controller AMD module
-      beforeActionsModule = 'controllers/before_actions_controller'
-      define beforeActionsModule, -> BeforeActionsController
-
-      # Helpers for asynchronous tests
-      loadBeforeActionsController = (callback) ->
-        require [beforeActionsModule], callback
-
-      it 'should not run executeAction directly if before actions are present', (done) ->
-        executeAction = sinon.spy dispatcher, 'executeAction'
-        # Replace executeBeforeAction with a no-op stub
-        executeBeforeAction = sinon.stub dispatcher, 'executeBeforeAction'
-
-        mediator.publish 'router:match', route, params, options
-
-        loadBeforeActionsController ->
-          expect(executeAction).was.notCalled()
-          expect(executeBeforeAction).was.called()
-          passedController = executeBeforeAction.firstCall.args[0]
-          expect(passedController).to.be.a BeforeActionsController
-
-          executeAction.restore()
-          executeBeforeAction.restore()
+
+      loadController = makeLoadController 'before_action',
+        BeforeActionController
+
+      beforeActionRoute = {controller: 'before_action', action: 'show', path}
+
+      it 'should run the before action', (done) ->
+        proto = BeforeActionController.prototype
+        beforeAction = sinon.spy proto, 'beforeAction'
+        action = sinon.spy proto, 'show'
+        publishMatch beforeActionRoute, params, options
+
+        loadController ->
+          expect(beforeAction).was.called()
+          expect(action).was.called()
+          expect(beforeAction.calledBefore(action)).to.be true
+
+          beforeAction.restore()
+          action.restore()

           done()

-      it 'should call executeAction after with exactly the same arguments', (done) ->
-        executeAction = sinon.spy dispatcher, 'executeAction'
-
-        mediator.publish 'router:match', route, params, options
-
-        loadBeforeActionsController ->
-          args = executeAction.firstCall.args
-          expect(args.length).to.be 4
-          expect(args[0]).to.be.a BeforeActionsController
-          expect(args[1].controller).to.be 'before_actions'
-          expect(args[1].action).to.be 'show'
-          expect(args[2]).to.eql params
-          expect(args[3]).to.eql create stdOptions
-
-          executeAction.restore()
-
-          done()
-
       it 'should throw an error if a before action method isn’t a function', ->
-        class BrokenBeforeActionController extends Controller
+        class BrokenController extends Controller
           beforeAction: {}
-          index: ->
-
-        controller = new BrokenBeforeActionController()
-
-        failFn = ->
-          failRoute = controller: 'broken_before_action', action: 'index'
-          dispatcher.executeBeforeAction controller, params, options
-
-        expect(failFn).to.throwError()
-
-      it 'should handle sync. before action then pass args', ->
-        showAction = sinon.spy()
+          show: ->
+
+        route = {controller: 'broken', action: 'show', path}
+        failFunction = ->
+          # Assume implementation detail (`controllerLoaded` method)
+          # to bypass the asynchronous require(). An alternative would be
+          # to mock require() so it’s synchronous.
+          dispatcher.controllerLoaded route, params, options, BrokenController
+
+        expect(failFunction).to.throwError()
+
+      it 'should run the before action with the same arguments', (done) ->
+        action = sinon.spy()

         class BeforeActionChainController extends Controller
           beforeAction: (params, route, options) ->
-            params.bar = 'qux1'
-            options.bar = 'qux2'
-
-          show: showAction
-
-        controller = new BeforeActionChainController()
-
-        testRoute = controller: 'before_action_chain', action: 'show'
-        dispatcher.executeBeforeAction controller, testRoute, params, options
-
-        expect(params.bar).to.be 'qux1'
-        expect(options.bar).to.be 'qux2'
-
-        # Ensure the before actions are run synchronous
-        expect(showAction).was.calledOnce()
-        args = showAction.firstCall.args
-        expect(args[0]).to.eql params
-        expect(args[1]).to.eql testRoute
-        expect(args[2]).to.eql options
-
-      it 'should handle single async. before action', ->
+            params.newParam = 'foo'
+            options.newOption = 'bar'
+          show: action
+
+        controllerName = 'before_action_chain'
+        loadController = makeLoadController controllerName,
+          BeforeActionChainController
+
+        route = {controller: controllerName, action: 'show', path}
+        publishMatch route, params, options
+
+        loadController ->
+          expect(action).was.calledOnce()
+          [passedParams, passedRoute, passedOptions] = action.firstCall.args
+          expect(passedParams).to.eql create(params, newParam: 'foo')
+          expect(passedRoute).to.eql create(route, previous: {})
+          expect(passedOptions).to.eql create(stdOptions, newOption: 'bar')
+
+          done()
+
+    describe 'Asynchronous Before Actions', ->
+
+      it 'should handle asynchronous before actions', (done) ->
         deferred = $.Deferred()
         promise = deferred.promise()

@@ -525,58 +508,103 @@
           beforeAction: -> promise
           show: ->

-        controller = new AsyncBeforeActionController()
-        action = sinon.spy controller, 'show'
-        testRoute = controller: 'async_before_action', action: 'show'
-        dispatcher.executeBeforeAction controller, testRoute, params, options
-
-        expect(action).was.notCalled()
-
-        # Resolve the Deferred
-        deferred.resolve()
-
-        expect(action).was.calledOnce()
-
-      it 'should stop async. dispatching when another controller is started', (done) ->
+        controllerName = 'async_before_action'
+        loadController = makeLoadController controllerName,
+          AsyncBeforeActionController
+
+        action = sinon.spy AsyncBeforeActionController.prototype, 'show'
+
+        route = {controller: controllerName, action: 'show', path}
+        publishMatch route, params, options
+
+        loadController ->
+          expect(action).was.notCalled()
+          deferred.resolve()
+          expect(action).was.calledOnce()
+
+          action.restore()
+
+          done()
+
+      it 'should support multiple asynchronous controllers', (done) ->
+
+        class AsyncBeforeActionController extends Controller
+          beforeAction: ->
+            # Return an already resolved Promise
+            { then : (callback) -> callback() }
+          show: ->
+
+        controllerName = 'async_before_action2'
+        loadController = makeLoadController controllerName,
+          AsyncBeforeActionController
+
+        route = {controller: controllerName, action: 'show', path}
+        options.forceStartup = true
+
+        proto = AsyncBeforeActionController.prototype
+        i = 0
+        times = 4
+
+        test = ->
+          beforeAction = sinon.spy proto, 'beforeAction'
+          action = sinon.spy proto, 'show'
+          publishMatch route, params, options
+
+          loadController ->
+            expect(beforeAction).was.called()
+            expect(action).was.called()
+
+            beforeAction.restore()
+            action.restore()
+
+            i++
+            if i < times
+              test()
+            else
+              done()
+
+        test()
+
+      it 'should stop dispatching when another controller is started', (done) ->
         deferred = $.Deferred()
         promise = deferred.promise()
-
-        firstRoute = controller: 'neverending', action: 'show'
-        secondRoute = controller: 'before_actions', action: 'index'

         class NeverendingController extends Controller
           beforeAction: -> promise
           show: ->

-        # Define controller AMD module
-        neverendingModule = 'controllers/neverending_controller'
-        define neverendingModule, -> NeverendingController
-        loadNeverendingController = (callback) ->
-          require [neverendingModule], callback
+        controllerName = 'neverending'
+        loadNeverendingController = makeLoadController controllerName,
+          NeverendingController
+
+        firstRoute = {controller: controllerName, action: 'show', path}
+        secondRoute = route2

         # Spies
-        indexAction = sinon.spy BeforeActionsController.prototype, 'index'
         proto = NeverendingController.prototype
         beforeAction = sinon.spy proto, 'beforeAction'
-        showAction = sinon.spy proto, 'show'
-
-        # Start with the neverending controller
-        mediator.publish 'router:match', firstRoute, params, options
+        firstAction = sinon.spy proto, 'show'
+        secondAction = sinon.spy Test2Controller.prototype, 'show'
+
+        # Start the neverending controller
+        publishMatch firstRoute, params, options

         loadNeverendingController ->
-          # While waiting for the promise, start another controller
-          mediator.publish 'router:match', secondRoute, params, options
-
-          loadBeforeActionsController ->
-            expect(indexAction).was.called()
-
+          expect(beforeAction).was.called()
+          expect(firstAction).was.notCalled()
+
+          # While the promise is pending, start another controller
+          publishMatch secondRoute, params, options
+
+          loadTest2Controller ->
+            expect(secondAction).was.called()
+
+            # Test what happens when the Promise is resolved later
             deferred.resolve()
-            expect(beforeAction).was.called()
-            expect(showAction).was.notCalled()
-
-            indexAction.restore()
+            expect(firstAction).was.notCalled()
+
             beforeAction.restore()
-            showAction.restore()
-            require.undef neverendingModule
+            firstAction.restore()
+            secondAction.restore()

             done()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -329,11 +329,11 @@
     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
+    @name lightness
     @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
+    @param {Number} [newVal] the new lightness value
+
+    @returns {Color|Number} returns the color object if you pass a new lightness value and returns the lightness otherwise
     ###
     lightness: (newVal) ->
       hsl = @toHsl()


Code cleanup

Code cleanup

--- client/landing/app/Applications/StartTab.kdapplication/views/appthumbview.coffee
+++ client/landing/app/Applications/StartTab.kdapplication/views/appthumbview.coffee
@@ -11,7 +11,8 @@

     super options, data

-    {icns, name, version, author, description, authorNick, additionalinfo} = manifest = @getData()
+    {icns, name, version, author, description,
+     authorNick, additionalinfo} = manifest = @getData()

     additionalinfo or= ''
     description    or= ''
@@ -25,13 +26,15 @@

     resourceRoot = "#{KD.appsUri}/#{authorNick}/#{name}/#{version}/"

-    if manifest.devMode?
+    if manifest.devMode
       resourceRoot = "https://#{authorNick}.koding.com/.applications/#{__utils.slugify name}/"

-    if icns and (icns['256'] or icns['512'] or icns['128'] or icns['160'] or icns['64'])
-      thumb = "#{resourceRoot}/#{if icns then icns['256'] or icns['512'] or icns['128'] or icns['160'] or icns['64']}"
-    else
-      thumb = "#{KD.apiUri + '/images/default.app.listthumb.png'}"
+    thumb = "#{KD.apiUri + '/images/default.app.thumb.png'}"
+
+    for size in [512, 256, 160, 128, 64]
+      if icns and icns[String size]
+        thumb = "#{resourceRoot}/#{icns[String size]}"
+        break

     @img = new KDCustomHTMLView
       tagName     : "img"


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com


Enable/Disable auto launch from settings.

--- src/main.coffee
+++ src/main.coffee
@@ -28,9 +28,12 @@
 auto_launcher = window.auto_launcher = new AutoLaunch
     name: 'myou-log'

-if not ewin.isDebug
+if settings.open_on_startup
     auto_launcher.enable().then ()->
-        console.log 'Auto-launch enabled'
+        console.log 'Open on system startup ENABLED'
+else
+    console.log 'Open on system startup DISABLED'
+    auto_launcher.disable()

 {Tray, Menu, app, globalShortcut} = electron.remote
 path = require 'path'


Don't prevent close for specs window.

Don't prevent close for specs window.

--- src/main.coffee
+++ src/main.coffee
@@ -148,6 +148,7 @@
       bootstrapScript: 'spec-bootstrap',
       resourcePath: @resourcePath

+    specWindow.browserWindow.removeAllListeners 'close'
     specWindow.browserWindow.show()
     @windows.push specWindow


return no-op `$pass()` instead of `undefined` for empty pipeline

--- src/main.coffee
+++ src/main.coffee
@@ -396,6 +396,7 @@

 #-----------------------------------------------------------------------------------------------------------
 @pull = ( methods... ) ->
+  return @$pass() if methods.length is 0
   for method, idx in methods
     continue if ( type = CND.type_of method ) is 'function'
     throw new Error "expected a function, got a #{type} for argument # #{idx + 1}"


export lead require function

export lead require function

--- src/node.coffee
+++ src/node.coffee
@@ -4,5 +4,5 @@
   nodeRequire: require
   baseUrl: __dirname

-for k in ['dsl']
+exports.require = (k) ->
   exports[k] = requirejs k


allow Repo#diff to receive commit shas

allow Repo#diff to receive commit shas

--- src/repo.coffee
+++ src/repo.coffee
@@ -62,14 +62,16 @@

   # Public: Get the difference between the trees.
   #
-  # commitA  - A Commit.
-  # commitB  - A Commit.
+  # commitA  - A Commit or String commit id.
+  # commitB  - A Commit or String commit id.
   # paths    - A list of String paths to restrict the difference to (optional).
   # callback - A Function which receives `(err, diffs)`.
   #
   diff: (commitA, commitB, paths, callback) ->
     [callback, paths] = [paths, callback] if !callback
     paths ?= []
+    commitA = commitA.id if _.isObject(commitA)
+    commitB = commitB.id if _.isObject(commitB)
     @git "diff", {}, _.flatten([commitA, commitB, "--", paths])
     , (err, stdout, stderr) =>
       return callback err if err


Add ? for null check

Add ? for null check

--- lib/exception-reporting.coffee
+++ lib/exception-reporting.coffee
@@ -11,7 +11,7 @@
       @originalOnError?(arguments...)

   deactivate: ->
-    if @originalOnError
+    if @originalOnError?
       window.onerror = @originalOnError
     else
       window.onerror = null


Allow decorations to carry their own color

--- lib/minimap-editor-view.coffee
+++ lib/minimap-editor-view.coffee
@@ -174,6 +174,7 @@
     @retrieveStyleFromDom(decoration.params.scope.split(/\s+/), 'background-color')

   getDecorationColor: (decoration) ->
+    return decoration.color if decoration.color?
     if decoration.params.scope not of @decorationColorCache
       color = @retrieveDecorationColorFromDom(decoration)
       @decorationColorCache[decoration.params.scope] = color


updated cache not to error out on filtersplit

--- src/driver/driverCache.coffee
+++ src/driver/driverCache.coffee
@@ -141,17 +141,17 @@
     @_splitPutHelper(condensedQuery, root, condensedQuery[0].filter, 0)
     return

-  _splitPutHelper: (condensedQuery, root, filter, level) ->
-    return unless condensedQuery[level + 1]?
+  _splitPutHelper: (condensedQuery, node, filter, level) ->
+    return unless node.splits?

     splitOp = condensedQuery[level + 1].split
     combineOp = condensedQuery[level + 1].combine
     splitOpName = splitOp.name
-    splitValues = root.splits.map((node) -> return node.prop[splitOpName])
+    splitValues = node.splits.map((node) -> return node.prop[splitOpName])
     @hashmap[generateHash(filter, splitOp, combineOp)] = splitValues

     if condensedQuery[level + 2]?
-      for split in root.splits
+      for split in node.splits
         @_splitPutHelper(condensedQuery, split, addToFilter(filter, createFilter(split.prop[splitOpName], splitOp)), level + 1)
     return

@@ -310,7 +310,8 @@
       callback(null, root)
       return

-    return driver unknownQuery, (err, root) ->
+    querySetter(async, unknownQuery)
+    return driver async, (err, root) ->
       if err?
         callback(err, null)
         return


Add test to ensure 'show' method causes submenu to be visible.

--- tests/specs/SuperfishTest.coffee
+++ tests/specs/SuperfishTest.coffee
@@ -79,7 +79,15 @@
       it "should fail silently if Superfish is uninitialised", ->
         $menu.superfish('destroy')
         expect( $liHasUl.superfish('show') ).toBe 'li'
-
+
+      it "should cause child ul to be visible", ->
+        $submenu = $liHasUl.children('ul')
+        expect($submenu).toBeHidden()
+        expect($liHasUl).not.toBe '.sfHover'
+        $liHasUl.superfish 'show'
+        expect($submenu).toBeVisible()
+        expect($liHasUl).toBe '.sfHover'
+

     describe "'hide' method", ->
       it "should fail silently if Superfish is uninitialised", ->


Fast play button.

Fast play button.

--- app/assets/javascripts/audio.js.coffee
+++ app/assets/javascripts/audio.js.coffee
@@ -3,6 +3,7 @@
 $.extend feedbin,

   nowPlayingdata: {}
+  forcePlay: false

   hasDuration: ->
     if isNaN(window.player.duration) || window.player.duration == 0
@@ -43,6 +44,7 @@
         mediaElement.addEventListener 'pause', feedbin.playState
         mediaElement.addEventListener 'loadedmetadata', feedbin.loadedmetadata
         if play
+          feedbin.play()
           mediaElement.play()
         else
           feedbin.timeRemaining(feedbin.nowPlayingData.entryId)
@@ -59,15 +61,16 @@
         field = form.find('#recently_played_entry_duration').val(window.player.duration)
         form.submit()

+  play: ->
+    feedbin.forcePlay = true
+    buttons = $("[data-behavior~=audio_play_#{feedbin.nowPlayingData.entryId}]")
+    buttons.addClass('playing')
+    buttons.removeClass('paused')
+
   togglePlay: ->
-    play = $("[data-behavior~=audio_play_#{feedbin.nowPlayingData.entryId}]")
     if window.player.paused
-      play.addClass('playing')
-      play.removeClass('paused')
       window.player.play()
     else
-      play.addClass('paused')
-      play.removeClass('playing')
       window.player.pause()

   setDuration: (entryId, duration) ->
@@ -80,8 +83,8 @@
     if progress = feedbin.loadProgress(entryId)
       durationElement = $("[data-behavior~=audio_duration_#{entryId}]")
       left = progress.duration - progress.progress
-      minutes = Math.ceil(left / 60);
-      if minutes == 1
+      minutes = Math.floor(left / 60);
+      if minutes <= 1
         message = "1 minute left"
       else
         message = "#{minutes} minutes left"
@@ -92,13 +95,16 @@
       if feedbin.hasDuration()
         feedbin.setDuration(feedbin.nowPlayingData.entryId, window.player.duration)

-      play = $("[data-behavior~=audio_play_#{feedbin.nowPlayingData.entryId}]")
-      if window.player.paused
-        play.addClass('paused')
-        play.removeClass('playing')
-      else
-        play.addClass('playing')
-        play.removeClass('paused')
+      if !feedbin.forcePlay
+        play = $("[data-behavior~=audio_play_#{feedbin.nowPlayingData.entryId}]")
+        if window.player.paused
+          play.addClass('paused')
+          play.removeClass('playing')
+        else
+          play.addClass('playing')
+          play.removeClass('paused')
+
+      feedbin.forcePlay = false

   audioJump: (time) ->
     window.player.setCurrentTime(window.player.currentTime + time)


Increase Mekko SVG height with long unwrapped labels

--- app/assets/javascripts/d3/mekko.coffee
+++ app/assets/javascripts/d3/mekko.coffee
@@ -239,7 +239,13 @@
         )
         .attr("data-tooltip-text", (d) => @main_formatter()(d.val()))

-      @wrapLabels()
+      if ! @has_wrapped_labels
+        # The first time the chart is loaded, adjust labels to wrap long text
+        # and increase height.
+        @wrapLabels()
+        @fitHeightToLabels()
+        @has_wrapped_labels = true
+
       @arrangeLabels()
       @moveArrows()

@@ -274,6 +280,20 @@
                      .text(word)
       )

+    # Increase the height of the SVG element to fit the largest unwrapped label.
+    fitHeightToLabels: ->
+      # @svg is actually the <g> element...
+      real_svg   = @svg.node().parentNode
+      svg_height = real_svg.getBoundingClientRect().height
+      g_height   = @svg.node().getBoundingClientRect().height
+
+      # Need to adjust the height for the transform of the <g> element. Only the
+      # transform from the top of the element matters.
+      transform  = d3.transform(@svg.attr('transform')).translate[1] / 2
+
+      if g_height > (svg_height - transform)
+        d3.select(real_svg).style('height', g_height + transform)
+
     # Arranges the labels for each sector so that no labels overlap.
     #
     # Tests the position of each label against every other label and moves any


interface pretty much working

interface pretty much working

--- app/assets/javascripts/lists.js.coffee
+++ app/assets/javascripts/lists.js.coffee
@@ -24,10 +24,10 @@
     keydown: "handleKey"
     "keydown input": "handleInputKey"
     "blur input": "stopEditing"
-
+
   initialize: (itemData) ->
     @itemData = itemData
-
+
   submit: ->
     @stopEditing()
     App.mainList.view.newItem()
@@ -56,14 +56,14 @@
       if prev
         @stopEditing()
         prev.select()
-        prev.view.switchToForm()
+        prev.switchToForm()
       false
     else if keyCode == "40"
       next = App.selection().next()
       if next
         @stopEditing()
         next.select()
-        next.view.switchToForm()
+        next.switchToForm()
       false

   render: ->
@@ -116,34 +116,35 @@

   setBody: ->
     @body.html @itemData.body + " "
-
+
   moveUp: ->
     $(@el).insertBefore $(@el).prev()
-
+
   moveDown: ->
     $(@el).insertAfter $(@el).next()
-
+
   next: ->
     all = $("#app li").toArray()
     index = all.indexOf(@el)
-    if index < all.length - 1
+    if index < all.length - 1
       index++
     all[index].view
-
+
   previous: ->
     all = $("#app li").toArray()
     index = all.indexOf(@el)
     if index > 0
       index--
     all[index].view
-
+
   indent: ->
-    $(@childrenView.el).append @el
-
+    if $(@el).prev("li").length
+      $($(@el).prev("li")[0].view.childrenView.el).append @el
+
   outdent: ->
     if $(@el).parents("li").length
-      $(@el).insertAfter $(el).parents("li")[0]
-
+      $(@el).insertAfter $(@el).parents("li")[0]
+
   render: ->
     that = this
     $(@el).html @template(@itemData)
@@ -224,7 +225,7 @@

   initialize: ->
     _.bindAll @, "selectPrevious", "selectNext", "switchItem", "toggleStatus", "moveSelectionUp", "moveSelectionDown", "indentItem", "outdentItem", "newItem", "deleteItem"
-
+
   firstChild: ->
     @el.firstChild.view

@@ -238,7 +239,7 @@
   selectNext: ->
     if not App.selected and App.selection()
       App.selection().select()
-    else
+    else
       App.selection()?.next()?.select()
     false

@@ -265,33 +266,30 @@

   newItem: (placement) ->
     selection = App.selection()
-    if selection
-      parent = selection.parent
-    else
-      parent = App.root
-    itemView = new App.backbone.ItemView(item)
+    itemView = new App.backbone.ItemView
+      body: ""
+      status: "incomplete"
+      children: []
+
     itemView.render()
     if selection
       if placement == "previous"
-        item.insertBefore selection
-        $(item.view.el).insertBefore selection.view.el
+        $(itemView.el).insertBefore selection.el
       else
-        if selection.children.size() > 0
-          selection = selection.children.first()
-          item.insertBefore selection
-          $(item.view.el).insertBefore selection.view.el
+        if $(selection.el).children("li").size() > 0
+          selection = $(selection.el).children("li")[0]
+          $(itemView.el).insertBefore selection
         else
           if placement == "indent"
-            item.insertAfter selection
-            item.indent()
+            $(itemView.el).insertAfter selection.el
+            itemView.indent()
           else # insert after
-            item.insertAfter selection
-            $(item.view.el).insertAfter selection.view.el
+            $(itemView.el).insertAfter selection.el
     else
       $(@el).append itemView.el
-    item.select()
+    itemView.select()
     _.defer ->
-      App.selection().view.switchToForm()
+      App.selection().switchToForm()

   deleteItem: ->
     App.selection().remove()
@@ -337,7 +335,7 @@
       App.mainList.view.outdentItem()

   setupItems: (items) ->
-
+

   setup: ->
     list = App.data.list


fixed parseInt in count

fixed parseInt in count

--- app/assets/javascripts/progress.coffee
+++ app/assets/javascripts/progress.coffee
@@ -70,7 +70,7 @@
 			iconCreateFunction: (c) ->
 				count = 0
 				for child in c.getAllChildMarkers()
-					count = count + child.options.flag_count
+					count = count + parseInt(child.options.flag_count)
 				c = 'cluster-large'
 				if count < 10
 					c = 'cluster-small'


不自动引用simditor/init

不自动引用simditor/init

--- app/assets/javascripts/simditor.coffee
+++ app/assets/javascripts/simditor.coffee
@@ -1,4 +1,3 @@
 #= require simditor/module
 #= require simditor/uploader
 #= require simditor/simditor
-#= require simditor/init


pets should show their profession name where possible

--- shared/idle/IdleServer.coffee
+++ shared/idle/IdleServer.coffee
@@ -82,6 +82,7 @@
     type: 1
     owner: 1
     createdAt: 1
+    professionName: 1

   Meteor.publish 'allPets', (filter = {}) ->
     IdlePets.find filter, {sort: {name: 1}, fields: petListFields}
@@ -153,4 +154,4 @@
       items = _.groupBy _.pluck IdleItems.find().fetch(), 'type'
       _.each (_.keys items), (itemType) ->
         ret[itemType] = items[itemType].length
-      ret+      ret


Add task translations to drawing subtasks

--- app/classifier/drawing-tools/root.cjsx
+++ app/classifier/drawing-tools/root.cjsx
@@ -1,6 +1,7 @@
 React = require 'react'
 StickyModalForm = require 'modal-form/sticky'
 ModalFocus = require('../../components/modal-focus').default
+TaskTranslations = require('../tasks/translations').default

 STROKE_WIDTH = 1.5
 SELECTED_STROKE_WIDTH = 2.5
@@ -14,6 +15,9 @@

 module.exports = React.createClass
   displayName: 'DrawingToolRoot'
+
+  contextTypes:
+    store: React.PropTypes.object

   statics:
     distance: (x1, y1, x2, y2) ->
@@ -76,7 +80,9 @@
             {for detailTask, i in toolProps.details
               detailTask._key ?= Math.random()
               TaskComponent = tasks[detailTask.type]
-              <TaskComponent autoFocus={i is 0} key={detailTask._key} task={detailTask} translation={detailTask} annotation={toolProps.mark.details[i]} onChange={@handleDetailsChange.bind this, i} />}
+              <TaskTranslations key={detailTask._key} taskKey={"TASK-KEY.tools.TOOL-INDEX.details.#{i}"} task={detailTask} store={@context.store}>
+                <TaskComponent autoFocus={i is 0} task={detailTask} annotation={toolProps.mark.details[i]} onChange={@handleDetailsChange.bind this, i} />
+              </TaskTranslations>}

               <button autoFocus={toolProps.details[0].type in ['single', 'multiple']} type="submit" className="standard-button" disabled={not detailsAreComplete}>OK</button>


Cleanup

Cleanup

--- jquery.transparency.coffee
+++ jquery.transparency.coffee
@@ -1,22 +1,17 @@
 renderKey = (key, value, buffer) ->
-  [klass, attribute] = key.split('@')
-  element = klass # We don't know wheter the user means img class or element, so look for both
-  assignValue buffer, attribute, value if buffer.hasClass klass or buffer.is(element)
+  [klass, attribute] = [element, _] = key.split('@')
+  assignValue buffer, attribute, value if buffer.hasClass klass or buffer.is element
   buffer.find("#{element}, .#{klass}").each ->
-    console.log attribute if attribute
     assignValue jQuery(this), attribute, value

 assignValue = (node, attribute, value) ->
-  #allowed_attributes = ['id', 'src']
   #throw "#{attribute}: Unsafe attribute assignment" if attribute
   if attribute
-    #console.log attribute
-    #console.log value
     node.attr attribute, value
   else
     children = node.children().detach()
     node.text value
-    node.append(children)
+    node.append children

 valid_attribute = (attribute) ->
   valid_attributes = ['src', 'alt', 'id', 'href', 'class', /data-*/]
@@ -44,11 +39,11 @@
       renderKey key, value, buffer

     for key, directive of local_directives
-      value = directive.call(object)
+      value = directive.call object
       renderKey key, value, buffer

-    for key, value of objects
-      buffer.find(".#{key}").add(key).render value, directives[key]
+    for klass, value of objects
+      buffer.find(".#{klass}").add(key).render value, directives[klass]

     # Add the rendered template to the dom
     context.before(buffer)


fixing latex regex (#111)

fixing latex regex (#111)

--- src/sloc.coffee
+++ src/sloc.coffee
@@ -38,7 +38,7 @@
         /\/{2}/

       when "latex", "tex", "sty", "cls"
-        start = /\%{2}/
+        start = /\%/
       when "lua", "hs", "sql"
         /--/
       when "erl"


Throw an error if a pipe has an unexpected return type

--- src/index.coffee
+++ src/index.coffee
@@ -100,6 +100,8 @@
         res.status 400 if res.statusCode is 200
         return res.send {error: result.message}

+      throw Error "Invalid return type: #{result.constructor}"
+
     # Start with the first pipe.
     Promise.try next


test child assets (component)

test child assets (component)

--- src/index.coffee
+++ src/index.coffee
@@ -21,6 +21,7 @@

       {{message}}
+      <hoge></hoge>

   '''
   replace: true # default: true
@@ -40,6 +41,11 @@
     console.log 'on before destroy'
   destroyed: ->
     console.log 'on destroyed'
+  components:
+    hoge:
+      data: ->
+        message: 'hoge'
+      template: '

'
   inherit: false # default: false
   events:
     'hook:created': ->


Fix typo bug

Fix typo bug

--- js/inner/action_log.coffee
+++ js/inner/action_log.coffee
@@ -26,7 +26,7 @@

     # these should be the only options
     @success = @komp => @status() == "success"
-    @success = @komp => @status() == "running"
+    @running = @komp => @status() == "running"
     @failed = @komp => @status() == "failed" or @status() == "timedout" or @status() == "cancelled" || @status() == "infrastructure_fail"
     @infrastructure_fail = @komp => @status() == "infrastructure_fail"


Now using the request parser for request token and access token

--- lib/oauth1.coffee
+++ lib/oauth1.coffee
@@ -92,7 +92,7 @@
 		# do request to request_token
 		request options, (e, r, body) ->
 			return callback(e) if e
-			responseParser = new OAuth1ResponseParser(r, body, request_token.format)
+			responseParser = new OAuth1ResponseParser(r, body, request_token.format, 'request_token')
 			return callback(responseParser.error) if responseParser.error

 			dbstates.setToken state.id, responseParser.oauth_token_secret, (e, r) ->
@@ -169,45 +169,14 @@

 		# do request to access_token
 		request options, (e, r, body) ->
-			return callback e if e
-
-			if not body && r.statusCode == 200
-				return callback new check.Error 'Http error while requesting access_token (empty response)'
-
-			if body
-				if access_token.format == 'json' or r.headers['content-type'] == 'application/json'
-					body = JSON.parse(body)
-				else if access_token.format == 'url' or r.headers['content-type'] == 'application/x-www-form-urlencoded'
-					body = querystring.parse(body)
-				else
-					try
-						body = JSON.parse(body)
-					catch err
-						try
-							body = querystring.parse(body)
-						catch err
-							err = new check.Error 'Unable to parse body of access_token response'
-							err.body.body = body
-							return callback err
-				if body.error || body.error_description
-					err = new check.Error
-					err.error body.error_description || 'Error while requesting token'
-					err.body = body
-					return callback err
-
-			if r.statusCode != 200
-				err = new check.Error 'Http error while requesting token (' + r.statusCode + ')'
-				err.body = body
-				return callback err
-
-			if not body.oauth_token
-				return callback new check.Error 'Could not find oauth_token in response'
-			if not body.oauth_token_secret
-				return callback new check.Error 'Could not find oauth_token_secret in response'
-			expire = body.expire
-			expire ?= body.expires
-			expire ?= body.expires_in
-			expire ?= body.expires_at
+			return callback(e) if e
+			responseParser = new OAuth1ResponseParser(r, body, access_token.format, 'access_token')
+			return callback(responseParser.error) if responseParser.error
+
+			expire = responseParser.body.expire
+			expire ?= responseParser.body.expires
+			expire ?= responseParser.body.expires_in
+			expire ?= responseParser.body.expires_at
 			if expire
 				expire = parseInt expire
 				now = (new Date).getTime()
@@ -219,12 +188,12 @@
 					requestclone.parameters ?= {}
 					requestclone.parameters[k] = parameters[k]
 			result =
-				oauth_token: body.oauth_token
-				oauth_token_secret: body.oauth_token_secret
+				oauth_token: responseParser.oauth_token
+				oauth_token_secret: responseParser.oauth_token_secret
 				expires_in: expire
 				request: requestclone
 			for extra in (access_token.extra||[])
-				result[extra] = body[extra] if body[extra]
+				result[extra] = responseParser.body[extra] if responseParser.body[extra]
 			for extra in (provider.oauth1.authorize.extra||[])
 				result[extra] = req.params[extra] if req.params[extra]
 			callback null, result


bare fastRender support

bare fastRender support

--- lib/router.coffee
+++ lib/router.coffee
@@ -1,4 +1,6 @@
 Router.configure
   layoutTemplate: 'defaultLayout'

-Router.route '/', name: 'homepage'+Router.route '/',
+  name: 'homepage'
+  fastRender: true


Improve assert messages

Improve assert messages

--- src/test.coffee
+++ src/test.coffee
@@ -72,7 +72,11 @@
     assert.isNotNull response, 'Response'

     # Status code
-    assert.equal response.statusCode, @response.status, 'Response Code'
+    assert.equal response.statusCode, @response.status, """
+      Got unexpected response code:
+      #{body}
+      Error
+    """

     # Body
     assert.isNotNull body


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

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

   self.unbind "draw"
   self.bind "afterTransform", (canvas) ->
-    canvas.font('20px VT323
+    canvas.font20px
     canvas.drawText
       color: "rgba(0, 0, 0, #{I.alpha})"
       x: I.x + 1


fix value end regexp bug (tss.cson)

fix value end regexp bug (tss.cson)

--- grammars/tss.cson
+++ grammars/tss.cson
@@ -208,7 +208,7 @@
             '1':
               'name': 'punctuation.separator.key-value.css.tss'
           # 'end': '\\s(,)|(?=\\})'
-          'end': '\\s*(,)|(\\})'
+          'end': '\\s*(,)|(?=\\})'
           'endCaptures':
             '1':
               'name': 'punctuation.terminator.rule.css.tss'


Clear high-order bytes in BytesArray. Closes #6.

--- src/util.coffee
+++ src/util.coffee
@@ -53,7 +53,6 @@
   has_bytes: -> @index < @raw_array.length

   get_uint: (bytes_count) ->
-    return @raw_array[@index++] if bytes_count == 1
     rv = root.read_uint @raw_array.slice(@index, @index+bytes_count)
     @index += bytes_count
     return rv


simplified check for existing binder in priority sorting

--- src/view.coffee
+++ src/view.coffee
@@ -105,7 +105,7 @@
     parse el for el in @els

     @bindings.sort (a, b) ->
-      (if b.binder? then (b.binder.priority or 0) else 0) - (if a.binder? then (a.binder.priority or 0) else 0)
+      (b.binder?.priority or 0) - (a.binder?.priority or 0)

     return


custom eval for inputs

custom eval for inputs

--- lib/console/console.coffee
+++ lib/console/console.coffee
@@ -114,7 +114,9 @@

   eval: (item) ->
     item = @itemForView item
-    if item.input
+    if item.eval?
+      item.eval()
+    else if item.input
       @emitter.emit 'eval', item
     else if (input = @getInput())
       input.editor.setText item.editor.getText()


More logs

More logs

--- scripts/bungie.coffee
+++ scripts/bungie.coffee
@@ -289,12 +289,18 @@

     parseActivityHash(bot, activityDetails.activityHash).then (details) ->

+      console.log 'parseActivityHash success:'
+      console.log details
+
       combinedDetails = Object.assign {}, activityDetails, details
       console.log "Resolving combined activity details for #{activityKey}:"
       console.log combinedDetails
       deferred.resolve(combinedDetails)

     ,(err) ->
+
+      console.log 'parseActivityHash failed:'
+      console.log err

       deferred.reject(err)

@@ -327,11 +333,12 @@
     .header('X-API-Key', BUNGIE_API_KEY)
     .get() (err, response, body) ->

-      console.log "response: #{response}"
+      console.log "response:"
+      console.log response

       if err
         console.log("error: #{err}")
         return callback(err)

-      object = JSON.parse(body)
-      callback(null, object.Response)
+      json = JSON.parse(body)
+      callback(null, json.Response)


Rework unless awebot.

Rework unless awebot.

--- scripts/points.coffee
+++ scripts/points.coffee
@@ -104,8 +104,9 @@
             people = flatten([chat.chatters.moderators, chat.chatters.staff, chat.chatters.admins, chat.chatters.global_mods, chat.chatters.viewers])

             for username in people
-                points[username] ?= 0 unless username = "awebot"
-                points[username] += 5 unless username = "awebot"
+                unless username = "awebot"
+                    points[username] ?= 0
+                    points[username] += 5
                 save(robot)

                 # Winners (and then recall) is equal to all users


Focus on the first input control for signin and signup form

--- scripts/themis.coffee
+++ scripts/themis.coffee
@@ -138,6 +138,9 @@
                     navigationBar.present()

                     $form = $main.find 'form.themis-form-signup'
+
+                    $form.find('input[name="team"]').focus()
+
                     $successAlert = $main.find 'div.themis-alert-signup'

                     $submitError = $form.find '.submit-error > p'
@@ -200,6 +203,8 @@

                     $form = $main.find 'form.themis-form-signin'

+                    $form.find('input[name="team"]').focus()
+
                     $submitError = $form.find '.submit-error > p'
                     $submitButton = $form.find 'button'


don't jump on empty filter results. REsolves issue #31

--- lib/go-outline-view.coffee
+++ lib/go-outline-view.coffee
@@ -122,9 +122,8 @@
     editorView = atom.views.getView(@searchField)
     editorView.addEventListener 'keydown', (e) =>
       if e.keyCode == 13 # pressed enter
-        console.log d3.select(@list[0]).select("li ol li")
         hits = d3.select(@list[0]).select("li ol li").data()
-        if hits.length
+        if hits.length>0 and hits[0]?
           @jumpToEntry(hits[0])
           @resetFilter()
       else if e.keyCode == 27 # pressed ESC


Fix #191

Fix #191

--- lib/models/git-init.coffee
+++ lib/models/git-init.coffee
@@ -14,7 +14,7 @@
     args: ['init']
     cwd: path
     stdout: (data) ->
-      notifer.addSuccess data
+      notifier.addSuccess data
       atom.project.setPaths([path])

 module.exports = gitInit


Better text matching for new terms

Better text matching for new terms

--- lib/models/terminal.coffee
+++ lib/models/terminal.coffee
@@ -37,7 +37,7 @@

       lastNode = null
       for node in newNodes
-        if node.innerText.match(/vm/)
+        if node.innerText.match(/\S/)
           lastNode = node

       if lastNode


Don't build out of empty transactions

Don't build out of empty transactions

--- lib/operators/input.coffee
+++ lib/operators/input.coffee
@@ -133,6 +133,7 @@
   constructor: (@transaction) ->

   buildInsertText: ->
+    return "" unless @transaction.patches
     chars = []
     for patch in @transaction.patches
       switch


activate pane on click

activate pane on click

--- lib/pdf-editor-view.coffee
+++ lib/pdf-editor-view.coffee
@@ -86,6 +86,7 @@
       e.preventDefault()

     @on 'mousedown', (e) =>
+      atom.workspace.paneForItem(this).activate()
       @dragging = x: e.screenX, y: e.screenY, scrollTop: @scrollTop(), scrollLeft: @scrollLeft()
       $(document).on 'mousemove', @onMouseMove
       $(document).on 'mouseup', @onMouseUp


cli test tweaks

cli test tweaks

--- test/cli.coffee
+++ test/cli.coffee
@@ -49,7 +49,7 @@
   return

 testCount = 0
-num_tests = 7
+num_tests = 8
 testsDone = (count) ->
   testCount += count
   num_tests -= 1
@@ -58,6 +58,7 @@

 generateApp = (opts, i) ->
+  console.log "generateApp", i
   plug = []
   plug.push "Plugin = function(name){this.name = (name != null) ? name : 'defaultName';};"
   plug.push "Plugin.prototype.data = function(){return {plugData:true};};"
@@ -81,9 +82,9 @@
   fs.writeFileSync(dir+'/input/'+i+'/main/temp.js', "require('./code1')") # write blank entry point (but require one of the plugin files)

 exports["test CLI#complicated"] = ->
-  num_tests = 8
-  initDirs(num_tests)
-  for k in [0...num_tests] then do (k) ->
+  num = 8
+  initDirs(num)
+  for k in [0...num] then do (k) ->
     opts = {}
     opts =
       dom     : k % 2 is 0
@@ -169,5 +170,7 @@
         count += 2

       testsDone(count)
-    null
+      return
+    return
+  return


activity: `MessagePane::addMessage` method should prepend the message

--- client/Social/Activity/views/messagepane.coffee
+++ client/Social/Activity/views/messagepane.coffee
@@ -30,7 +30,7 @@

   addMessage: (message) ->

-    @listController.addItem message
+    @listController.addItem message, 0

   removeMessage: (message) ->


Extended AccountCredentialList from KodingListView

--- client/account/lib/accountcredentiallist.coffee
+++ client/account/lib/accountcredentiallist.coffee
@@ -8,11 +8,13 @@
 showError                   = require 'app/util/showError'
 applyMarkdown               = require 'app/util/applyMarkdown'

+KodingListView              = require 'app/kodinglist/kodinglistview'
+
 AccountCredentialListItem   = require './accountcredentiallistitem'
 AccountCredentialEditModal  = require './accountcredentialeditmodal'

-module.exports = class AccountCredentialList extends KDListView
+module.exports = class AccountCredentialList extends KodingListView

   constructor: (options = {}, data) ->


remote/machine: add getters for view usage

Signed-off-by: Umut Sirin <4055abbc0e40a1254e715b952ccd414607e0b365@gmail.com>

--- client/app/lib/remote-extensions/machine.coffee
+++ client/app/lib/remote-extensions/machine.coffee
@@ -222,3 +222,38 @@
       @setApproved()
       storage.machines.push this
       callback null
+
+
+  getType: ->
+
+    { Own, Shared, Reassigned, Collaboration } = JMachine.Type
+
+    switch
+      when @isMine()      then Own
+      when @isPermanent() then Shared
+      when @getOldOwner() then Reassigned
+      else Collaboration
+
+
+  getTitle: ->
+
+    { Shared, Reassigned, Collaboration } = JMachine.Type
+
+    switch @getType()
+      when Shared, Collaboration then "#{@label} (#{@getOwner()})"
+      when Reassigned then "#{@label} (#{@getOldOwner()})"
+      else @label
+
+
+  getIDELink: ->
+
+    { Own, Shared, Reassigned, Collaboration } = JMachine.Type
+
+    switch @getType()
+      when Own then "/IDE/#{@slug or @label}"
+      when Collaboration then "/IDE/#{@getChannelId()}"
+      when Shared, Reassigned then "/IDE/#{@uid}"
+      else ''
+
+
+  getDashboardLink: -> "/Home/stacks/virtual-machines/#{@getId()}"


some unnecessary code removed

some unnecessary code removed

--- client/landing/app/MainApp/book/bookview.coffee
+++ client/landing/app/MainApp/book/bookview.coffee
@@ -159,10 +159,9 @@

   fillPrevPage:->
     if BookView.navigateNewPages
-      @getNewPages (pages)=>
+      @getNewPages =>
         prev = @prevUnreadPage()
         @fillPage prev if prev?
-        @checkBoundaries pages
       return

     return if @currentIndex - 1 < 0
@@ -170,10 +169,9 @@

   fillNextPage:->
     if BookView.navigateNewPages
-      @getNewPages (pages)=>
+      @getNewPages =>
         next = @nextUnreadPage()
         @fillPage next if next?
-        @checkBoundaries pages
       return

     return if __bookPages.length is @currentIndex + 1


Fix incorrect reference to FieldControl events

This is part of the class definition, and therefore defined on
the prototype.

--- coffee/cilantro/ui/field/controls/number.coffee
+++ coffee/cilantro/ui/field/controls/number.coffee
@@ -11,7 +11,7 @@
                 value: controls.RangeInput

         events: ->
-            events = c._.clone base.FieldControl.events
+            events = c._.clone base.FieldControl::events
             events["change #{@dataSelectors.operator}"] = 'toggleRange'
             return events


reset elapsed loops on changing them

reset elapsed loops on changing them

--- coffee/src/views/timed_media_player_view.coffee
+++ coffee/src/views/timed_media_player_view.coffee
@@ -13,6 +13,10 @@
     @on 'Media:DidPlay', => @timer.startTick() if @rendering
     @on 'Media:DidPause', => @timer.stopTick()
     @on 'Media:DidEnd', => @timer.stopTick()
+
+    # reset loops on changing them
+    @listenTo @model, 'change:loops', =>
+      @elapsedLoops 0

   render: =>


Added _createId that generates ids for ES

--- app/facebook.coffee
+++ app/facebook.coffee
@@ -58,6 +58,14 @@
 		return true
 	return false

+# Create a unique ID based on URL (should handle differing URL formats)
+# @param [string] url
+# @return [string] id
+# @private
+_createId: (url) ->
+	# The Facebook module only handles event ids anyway
+	return "facebook.com/events/#{_getEventId(url)}"
+
 # Get a facebook event
 # @param [int] eventId
 # @option query [int] eventId
@@ -79,6 +87,7 @@

 				db.set
 					events: [
+						id: @_createId(query.url)
 						title: res.name
 						#description: res.description
 						source: query.url


Made convenience functions respond404 and respond500

--- app/frontend.coffee
+++ app/frontend.coffee
@@ -8,6 +8,18 @@
 fs = require 'fs'
 url = require 'url'

+respond404 = (req, res) ->
+	fs.readFile(path.join(process.cwd(), '/public/404.html'), (err, file) ->
+		res.writeHead(404)
+		res.write(file, 'binary')
+		res.end()
+	)
+
+respond500 = (req, res) ->
+	res.writeHead(500, {"Content-Type": "text/plain"})
+	res.write(err + "\n")
+	res.end()
+
 exports.handle = (request, response) ->
 	uri = url.parse(request.url).pathname
 	filename = path.join(process.cwd(), '/public/' + uri)
@@ -16,11 +28,8 @@

 		# We have a file that doesn't exist, respond with 404
 		if !exists
-			fs.readFile(path.join(process.cwd(), '/public/404.html'), (err, file) ->
-				response.writeHead(404)
-				response.write(file, 'binary')
-				response.end()
-			)
+			respond404(request, response)
+
 			return

 		# We have a directory, see if we have a static index file
@@ -28,17 +37,15 @@
 			if fs.existsSync(path.join(filename, 'index.html'))
 				filename = path.join(filename, 'index.html')
 			else
-				filename = path.join(process.cwd(), '/public/404.html')
+				respond404()
+				return

 		fs.readFile(filename, "binary", (err, file) ->
 			#Something went wrong when reading file
 			if err
-				response.writeHead(500, {"Content-Type": "text/plain"})
-				response.write(err + "\n")
-				response.end()
+				respond500(request, response)
 				return

-
 			# Everything went well, return
 			response.writeHead(200)
 			response.write(file, "binary")


Fix issue with negative index

Fix issue with negative index

--- app/annotation/annotation.cjsx
+++ app/annotation/annotation.cjsx
@@ -67,8 +67,7 @@
     window.removeEventListener 'keydown', @onPressSpaceBar

   zoomImage: (i, preview) ->
-    @setState zoomImageCurrent: i
-    @setState zoomImageSrc: preview
+    @setState({zoomImageCurrent: i, zoomImageSrc: preview})
     @refs.imageZoom.show() if window.innerWidth > 600

   onClickGuide: ->
@@ -124,6 +123,8 @@
       i = @state.zoomImageCurrent
       if e.keyCode is 37
         i = (i - 1) % @props.previews.length
+        if i < 0
+          i = @props.previews.length - 1
       else
         i = (i + 1) % @props.previews.length


Fix last commit

Fix last commit

--- lib/TermView.coffee
+++ lib/TermView.coffee
@@ -99,7 +99,7 @@
     @off 'focus', @onFocus
     $(window).off 'resize'

-  focus: ->
+  onFocus: ->
     @resizeToPane()
     @focusTerm()


better sourcemap integration internally

better sourcemap integration internally

--- lib/compiler.coffee
+++ lib/compiler.coffee
@@ -90,7 +90,7 @@
       .then((o) => @content = o)
       .then(=> sequence(hooks('compile_hooks.before_file'), @))
       .then(each_pass)
-      .tap((o) => @content = o)
+      .tap((o) => @content = o.result)
       .tap(=> @roots.emit('compile', @file))
       .then(=> sequence(hooks('compile_hooks.after_file'), @))
       .then(write_file)
@@ -243,7 +243,7 @@

   each_pass = ->
     pass = new CompilePass(@)
-    pipeline(@adapters.map((a,i) -> pass.run.bind(pass, a, i + 1)), @content)
+    pipeline(@adapters.map((a,i) -> pass.run.bind(pass, a, i + 1)), { result: @content })

   ###*
    * Returns the absolute path to the file as requested through the browser,
@@ -292,8 +292,10 @@
    * @todo is there a way to yield(@content)?
   ###

-  run: (@adapter, @index, @content) ->
+  run: (@adapter, @index, @input) ->
     hooks = (cat) => @file.extensions.hooks(cat, @file.category)
+
+    @content = @input.result

     sequence(hooks('compile_hooks.before_pass'), @)
       .with(@)
@@ -301,8 +303,11 @@
       .then(compile_or_pass)
       .then (o) =>
         @content = o.result
-        @sourcemap = o.sourcemap
-        return @content
+        res = { result: @content }
+        if o.sourcemap
+          @sourcemap = o.sourcemap
+          res.sourcemap = @sourcemap
+        return res
       .tap(=> sequence(hooks('compile_hooks.after_pass'), @))

   ###*
@@ -347,5 +352,5 @@
   ###

   compile_or_pass = ->
-    if not @adapter.name then return { result: @content }
+    if not @adapter.name then return @input
     @adapter.render(@content, @opts)


Revert load path

Revert load path

--- lib/grammars.coffee
+++ lib/grammars.coffee
@@ -99,4 +99,4 @@
   Gherkin:
     command: "cucumber"
     #"Selection Based": (code) -> ['-e', code]
-    "File Based": (filename) -> ['-r', '""', filename]
+    "File Based": (filename) -> [filename]


Alphabetize grammars

Alphabetize grammars

--- lib/grammars.coffee
+++ lib/grammars.coffee
@@ -7,52 +7,31 @@
     "Selection Based": (code) -> ['-e', code]
     "File Based": (filename) -> [filename]

-  JavaScript:
-    command: "node"
+  'CoffeeScript (Literate)':
+    command: "coffee"
     "Selection Based": (code) -> ['-e', code]
     "File Based": (filename) -> [filename]

-  Ruby:
-    command: "ruby"
+  Elixir:
+    command: "elixir"
     "Selection Based": (code) -> ['-e', code]
-    "File Based": (filename) -> [filename]
+    "File Based": (filename) -> ['-r', filename]

-  RSpec:
-    command: "rspec"
-    #"Selection Based": (code) -> []
-    "File Based": (filename) -> [filename]
+  Erlang:
+    command: "erl"
+    "Selection Based": (code) -> ['-noshell', '-eval', code+', init:stop().']

-  Perl:
-    command: "perl"
-    "Selection Based": (code) -> ['-e', code]
-    "File Based": (filename) -> [filename]
-
-  PHP:
-    command: "php"
-    "Selection Based": (code) -> ['-r', code]
-    "File Based": (filename) -> [filename]
-
-  Python:
-    command: "python"
-    "Selection Based": (code) -> ['-c', code]
-    "File Based": (filename) -> [filename]
-
-  'Shell Script (Bash)':
-    command: "bash"
-    "Selection Based": (code) -> ['-c', code]
-    "File Based": (filename) -> [filename]
+  'F#':
+    command: "fsharpi"
+    "File Based": (filename) -> ['--exec', filename]

   Go:
     command: "go"
     #"Selection Based": (code) -> []
     "File Based": (filename) -> ['run', filename]

-  'F#':
-    command: "fsharpi"
-    "File Based": (filename) -> ['--exec', filename]
-
-  newLISP:
-    command: "newlisp"
+  Groovy:
+    command: "groovy"
     "Selection Based": (code) -> ['-e', code]
     "File Based": (filename) -> [filename]

@@ -62,27 +41,13 @@
     #command: "ghc"
     #"Selection Based": (code) -> ['-e', code]

-  Erlang:
-    command: "erl"
-    "Selection Based": (code) -> ['-noshell', '-eval', code+', init:stop().']
-
-  Elixir:
-    command: "elixir"
+  JavaScript:
+    command: "node"
     "Selection Based": (code) -> ['-e', code]
-    "File Based": (filename) -> ['-r', filename]
+    "File Based": (filename) -> [filename]

   Julia:
     command: "julia"
-    "Selection Based": (code) -> ['-e', code]
-    "File Based": (filename) -> [filename]
-
-  Groovy:
-    command: "groovy"
-    "Selection Based": (code) -> ['-e', code]
-    "File Based": (filename) -> [filename]
-
-  Scala:
-    command: "scala"
     "Selection Based": (code) -> ['-e', code]
     "File Based": (filename) -> [filename]

@@ -91,8 +56,40 @@
     "Selection Based": (code) -> ['-e', code]
     "File Based": (filename) -> [filename]

-  "CoffeeScript (Literate)":
-    command: "coffee"
+  PHP:
+    command: "php"
+    "Selection Based": (code) -> ['-r', code]
+
+  Perl:
+    command: "perl"
+    "Selection Based": (code) -> ['-e', code]
+    "File Based": (filename) -> [filename]
+
+  Python:
+    command: "python"
+    "Selection Based": (code) -> ['-c', code]
+
+  RSpec:
+    command: "rspec"
+    #"Selection Based": (code) -> []
+    "File Based": (filename) -> [filename]
+
+  Ruby:
+    command: "ruby"
+    "Selection Based": (code) -> ['-e', code]
+    "File Based": (filename) -> [filename]
+
+  'Shell Script (Bash)':
+    command: "bash"
+    "Selection Based": (code) -> ['-c', code]
+
+  Scala:
+    command: "scala"
+    "Selection Based": (code) -> ['-e', code]
+    "File Based": (filename) -> [filename]
+
+  newLISP:
+    command: "newlisp"
     "Selection Based": (code) -> ['-e', code]
     "File Based": (filename) -> [filename]


Cleared lint errors.

Cleared lint errors.

--- lib/matchers.coffee
+++ lib/matchers.coffee
@@ -1,27 +1,67 @@
+###*
+# Provides methods to check for different "line types" (comment, empty...)
+# and to get their contents.
+#
+# NOTE: Strings are trimmed before trying the match against those RegExp.
+# So they don't care about whitespace at the beginning or the end of the input.
+# @class Matcher
+# @static
+###
+
 class Matcher
+	###*
+	# Checks if a string constains only white space.
+	# @property emptyLineMatcher
+	# @type RegExp
+	###
 	emptyLineMatcher = /^\s*$/

+	###*
+	# Matches new line characters.
+	# @property newLineMatcher
+	# @type RegExp
+	###
 	newlineMatcher = /[\r\n]+/g

-	# NOTE: Lines are trimmed before trying the match against those RegExp.
-	# So they don't care about whitespace at the beginning or the end of the input.
-
-	# A comment:
+	###*
+	# Matches strings of type "comment".
+	# A comment has the following features:
+	#
 	# 	-starts with #
+	# @property commentLineMatcher
+	# @type RegExp
+	###
 	commentLineMatcher = /^#/

+	###*
+	# Extracts content from strings of type "comment".
+	# See {{#crossLink "Matcher/commentLineMatcher:property"}}{{/crossLink}}
+	# @property commentMatcher
+	# @type RegExp
+	###
 	commentMatcher = /^#(.*)$/i

-	# A section header:
+	###*
+	# Matches strings of type "section header".
+	# A section header has the following features:
+	#
 	#	-starts with [
+	#
 	#	-ends with ]
-	# 	-between those 2 symbols it must contain just a valid javascript object key, although it can be surrounded by whitespace
+	#
+	# 	-between those 2 symbols it must contain just a
+	# valid javascript object key, although it can be surrounded by whitespace.
+	#
 	# [HEADER] => it's a header
 	# [ HEADER ] => it's a header
 	# [ HEADER_NAME ] => it's a header
 	# [ HEADER.NAME ] => it's a header
 	# [ HEADER NAME ] => it's NOT a header
-	sectionHeaderMatcher = /^\[(\s*[a-zA-Z_][a-zA-Z0-9._]*\s*)\]$/i;
+	#
+	# @property sectionHeaderMatcher
+	# @type RegExp
+	###
+	sectionHeaderMatcher = /^\[(\s*[a-zA-Z_][a-zA-Z0-9._]*\s*)\]$/i

 	# A key=value line:
 	#	-starts with a valid javascript key


Labyrinth item name change for 2.4. Fixed more typos and case mismatches.

--- lib/provider.coffee
+++ lib/provider.coffee
@@ -145,7 +145,7 @@
     { snippet: '"Cogs of Disruption"' },
     { snippet: '"Cube of Absorption"' },
     { snippet: '"Heart of the Gargoyle"' },
-    { snippet: '"Orb of Elemental Essence"' },
+    { snippet: '"Orb of Elemental Dispersion"' },
     { snippet: '"Portal Shredder"' },
     { snippet: '"Rod of Detonation"' },
     { snippet: '"Sand of Eternity"' },
@@ -241,7 +241,7 @@
     { snippet: '"Armourer\'s Scrap"' },
     { snippet: '"Blacksmith\'s Whetstone"' },
     { snippet: '"Blessed Orb"' },
-    { snippet: '"Cartographer\s Chisel"' },
+    { snippet: '"Cartographer\'s Chisel"' },
     { snippet: '"Chaos Orb"' },
     { snippet: '"Chromatic Orb"' },
     { snippet: '"Divine Orb"' },
@@ -373,7 +373,7 @@
     { snippet: '"Double Strike"' },
     { snippet: '"Dual Strike"' },
     { snippet: '"Elemental Hit"' },
-    { snippet: '"Ethereal knives"' },
+    { snippet: '"Ethereal Knives"' },
     { snippet: '"Explosive Arrow"' },
     { snippet: '"Fire Trap"' },
     { snippet: '"Flicker Strike"' },
@@ -490,7 +490,7 @@
     { snippet: '"Kinetic Blast"' },
     { snippet: '"Lightning Tendrils"' },
     { snippet: '"Lightning Trap"' },
-    { snippet: '"Lightning warp"' },
+    { snippet: '"Lightning Warp"' },
     { snippet: '"Magma Orb"' },
     { snippet: '"Orb of Storms"' },
     { snippet: '"Power Siphon"' },
@@ -519,7 +519,7 @@
     { snippet: '"Chance to Ignite Support"' },
     { snippet: '"Concentrated Effect Support"' },
     { snippet: '"Controlled Destruciton Support"' },
-    { snippet: '"Curse on Hit Support"' },
+    { snippet: '"Curse On Hit Support"' },
     { snippet: '"Elemental Focus Support"' },
     { snippet: '"Elemental Proliferation Support"' },
     { snippet: '"Enlighten Support"' },
@@ -769,7 +769,7 @@
     { snippet: '"Sapphire Flask"' },
     { snippet: '"Topaz Flask"' },
     { snippet: '"Amethyst Flask"' },
-    { snippet: '"Granity Flask"' },
+    { snippet: '"Granite Flask"' },
     { snippet: '"Diamond Flask"' },
     { snippet: '"Jade Flask"' },
     { snippet: '"Quartz Flask"' }


Sequence: the internal make() function filters out empty sequences.

--- lib/sequence.coffee
+++ lib/sequence.coffee
@@ -14,7 +14,8 @@
 class Sequence
   constructor: (src) ->
     if not src?
-      throw new Error('use null for the empty sequence')
+      @first = ->
+      @rest  = -> null
     else if typeof src.sequence == 'function'
       seq = src.sequence()
       @first = -> seq.first()
@@ -29,7 +30,7 @@
       @first = -> src[0]
       @rest  = -> partial(1)
     else
-      throw new Error('cannot make a sequence from #{src}')
+      throw new Error("cannot make a sequence from #{src}")

   @conj: (first, rest = (-> null), mode = null) ->
     r = rest() if mode == 'forced'
@@ -47,7 +48,11 @@

   @::S = this

-  make = (seq) -> seq and new Sequence(seq)
+  make = (seq) ->
+    if seq and (res = new Sequence(seq)) and typeof res.first() != 'undefined'
+      res
+    else
+      null

   @memo: (name, f) ->
     @[name]        = (seq) -> f.call(this, make(seq))
@@ -159,7 +164,7 @@

   @method 'flatten', (seq) ->
     if seq and seq.first()
-      lazyConcat new Sequence(seq.first()), => @flatten__ seq.rest()
+      lazyConcat make(seq.first()), => @flatten__ seq.rest()
     else if seq and seq.rest()
       @flatten__ @dropWhile__ seq.rest(), (x) -> not x.first()
     else


fi.coffee translated: teachers_survey

fi.coffee translated: teachers_survey

--- app/locale/fi.coffee
+++ app/locale/fi.coffee
@@ -633,33 +633,33 @@
     sys_requirements_1: "Nykyaikainen selainohjelma, kuten Chrome, Firefox, tai Safari joista uusi versio. Internet Explorer 9 tai uudempi."
     sys_requirements_2: "iPad app CodeCombatista on vasta kehitteillä."

-#  teachers_survey:
-#    title: "Teacher Survey"
-#    must_be_logged: "You must be logged in first. Please create an account or log in from the menu above."
-#    retrieving: "Retrieving information..."
-#    being_reviewed_1: "Your application for a free trial subscription is being"
-#    being_reviewed_2: "reviewed."
-#    approved_1: "Your application for a free trial subscription was"
-#    approved_2: "approved."
-#    approved_3: "Further instructions have been sent to"
-#    denied_1: "Your application for a free trial subscription has been"
-#    denied_2: "denied."
-#    contact_1: "Please contact"
-#    contact_2: "if you have further questions."
-#    description_1: "We offer free subscriptions to teachers for evaluation purposes. You can find more information on our"
-#    description_2: "teachers"
-#    description_3: "page."
-#    description_4: "Please fill out this quick survey and we’ll email you setup instructions."
-#    email: "Email Address"
-#    school: "Name of School"
-#    location: "Name of City"
-#    age_students: "How old are your students?"
-#    under: "Under"
-#    other: "Other:"
-#    amount_students: "How many students do you teach?"
-#    hear_about: "How did you hear about CodeCombat?"
-#    fill_fields: "Please fill out all fields."
-#    thanks: "Thanks! We'll send you setup instructions shortly."
+  teachers_survey:
+    title: "Opettajan kysely"
+    must_be_logged: "Teidän täytyy olla kirjautuneena sisään. Ole hyvä ja luo tili tai kirjaudu sisään valikosta ylhäällä."
+    retrieving: "Noudetaan tietoja..."
+    being_reviewed_1: "Hakemuksenne ilmaiseen tutustumistilaukseen on"
+    being_reviewed_2: "käsittelyssä."
+    approved_1: "Hakemuksenne ilmaiseen tutustumistilaukseen on"
+    approved_2: "hyväksytty."
+    approved_3: "Jatko-ohjeet on lähetetty"
+    denied_1: "Hakemuksenne ilmaiseen tutustumistilaukseen on"
+    denied_2: "hylätty."
+    contact_1: "Ole hyvä ja ota yhteyttä"
+    contact_2: "jos teillä on jotain kysyttävää asiasta."
+    description_1: "Tarjoamme ilmaista kuukausitilausta opettajille arviointikäyttöön. Lisätietoa tilauksesta saat"
+    description_2: "opettajat"
+    description_3: "sivulta."
+    description_4: "Ole hyvä ja täytä alla oleva kysely, niin lähetämme teille ohjeet käyttöönottoa varten."
+    email: "Sähköpostiosoite"
+    school: "Koulun nimi"
+    location: "Kaupungin nimi"
+    age_students: "Minkä ikäisiä ovat oppilasi?"
+    under: "alle"
+    other: "Muu:"
+    amount_students: "Montaako oppilasta opetatte?"
+    hear_about: "Mistä kuulitte CodeCombatista?"
+    fill_fields: "Ole hyvä ja täytä kaikki kentät."
+    thanks: "Kiitos! Lähetämme teille ohjeet pikimmiten."

 #  versions:
 #    save_version_title: "Save New Version"


Fix error not being defined in wrong describe block

--- services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee
+++ services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee
@@ -99,6 +99,7 @@
 			@req.session.save = sinon.stub().callsArgWith(0, null)
 			@req.sessionStore = {generate: sinon.stub()}
 			@passport.authenticate.callsArgWith(1, null, @user, @info)
+			@err = new Error('woops')

 		it 'should call passport.authenticate', () ->
 			@AuthenticationController.passportLogin @req, @res, @next
@@ -107,7 +108,6 @@
 		describe 'when authenticate produces an error', ->

 			beforeEach ->
-				@err = new Error('woops')
 				@passport.authenticate.callsArgWith(1, @err)

 			it 'should return next with an error', () ->


Roll that back a bit

Roll that back a bit

--- lib/views/synced-fs.coffee
+++ lib/views/synced-fs.coffee
@@ -36,8 +36,6 @@
       file_sys.rmdirSync(path)

   handleEvents: ->
-    sep = /^win/.test(process.platform) ? '\\' : '/'
-
     @ws.onopen = (e) =>
       @element.style.color = '#73c990'
     @ws.onmessage = (e) =>
@@ -46,12 +44,12 @@
         event = JSON.parse(e.data)
         switch event.event
           when 'remote_create'
-            console.log('Created: ' + event.location + sep + event.file)
+            console.log('Created: ' + event.location + '/' + event.file)
             if event.directory
-              mkdirp.sync(atom.getUserWorkingDirPath() + sep + event.location + sep + event.file)
+              mkdirp.sync(atom.getUserWorkingDirPath() + '/' + event.location + '/' + event.file)
             else
-              mkdirp.sync(atom.getUserWorkingDirPath() + sep + event.location)
-              file_sys.openSync(atom.getUserWorkingDirPath() + sep + event.location + sep + event.file, 'a')
+              mkdirp.sync(atom.getUserWorkingDirPath() + '/' + event.location)
+              file_sys.openSync(atom.getUserWorkingDirPath() + '/' + event.location + '/' + event.file, 'a')

               @ws.send JSON.stringify({
                 action: 'request_content',
@@ -60,12 +58,12 @@
               })
           when 'content_response'
             content = new Buffer(event.content, 'base64').toString()
-            file_sys.writeFileSync atom.getUserWorkingDirPath() + sep + event.location + sep + event.file, content
+            file_sys.writeFileSync atom.getUserWorkingDirPath() + '/' + event.location + '/' + event.file, content
           when 'remote_delete'
             if event.directory
-              this.deleteDirectoryRecursive atom.getUserWorkingDirPath() + sep + event.location + sep + event.file
+              this.deleteDirectoryRecursive atom.getUserWorkingDirPath() + '/' + event.location + '/' + event.file
             else
-              delPath = atom.getUserWorkingDirPath() + sep + event.location + sep + event.file
+              delPath = atom.getUserWorkingDirPath() + '/' + event.location + '/' + event.file
               if file_sys.existsSync(delPath)
                 file_sys.unlinkSync(delPath)
           when 'remote_move_from'
@@ -74,8 +72,8 @@
             console.log('move_to')
           when 'remote_modify'
             if !event.directory
-              mkdirp.sync(atom.getUserWorkingDirPath() + sep + event.location)
-              file_sys.openSync(atom.getUserWorkingDirPath() + sep + event.location + sep + event.file, 'a')
+              mkdirp.sync(atom.getUserWorkingDirPath() + '/' + event.location)
+              file_sys.openSync(atom.getUserWorkingDirPath() + '/' + event.location + '/' + event.file, 'a')

               @ws.send JSON.stringify({
                 action: 'request_content',
@@ -83,12 +81,12 @@
                 file: event.file
               })
           when 'remote_open'
-            console.log('Opened: ' + event.location + sep + event.file)
+            console.log('Opened: ' + event.location + '/' + event.file)

             if event.location == ''
               atom.workspace.open(event.file)
             else
-              atom.workspace.open(event.location + sep + event.file)
+              atom.workspace.open(event.location + '/' + event.file)

       catch err
         console.log(err)


Darken asteroids a little further

Darken asteroids a little further

--- app/js/world.coffee
+++ app/js/world.coffee
@@ -581,7 +581,7 @@

     density = Utils.randomInt(0,4)
     lineColor = Math.floor(192 - density * 32)
-    color = Math.floor(96 - density * 16)
+    color = Math.floor(54 - density * 9)

     @addBody new Asteroid size,
       position: position


Tidy up auth

Tidy up auth

--- app/lib/auth.coffee
+++ app/lib/auth.coffee
@@ -1,14 +1,3 @@
-checkStatus = (response) ->
-  if response.status >= 200 && response.status < 300
-    return response
-  else
-    error = new Error response.statusText
-    error.response = response
-    throw error
-
-parseJson = (response) ->
-  response.json()
-
 extractToken = (hash) ->
   match = hash.match(/access_token=(\w+)/)
   if !!match && match[1]
@@ -23,13 +12,11 @@
       @_setupAuth token

   getUser: ->
-    token = @_getToken()
-
     @api.get '/me'
       .then ([user]) =>
         user
       .catch (error) =>
-        console.log error
+        null

   signOut: ->
     @_removeToken()


don't check SSL cert when downloading wakatime cli, for people behind corporate proxies.

--- lib/wakatime.coffee
+++ lib/wakatime.coffee
@@ -155,7 +155,7 @@

 installCLI = (callback) ->
     console.log 'Downloading wakatime cli...'
-    url = 'https://github.com/wakatime/wakatime/archive/master.zip'
+    url = { url: 'https://github.com/wakatime/wakatime/archive/master.zip', strictSSL: false }
     zipFile = __dirname + path.sep + 'wakatime-master.zip'
     downloadFile(url, zipFile, ->
         console.log 'Extracting wakatime-master.zip file...'


remove log line

remove log line

--- assets/coffeescript/pupil_store.coffee
+++ assets/coffeescript/pupil_store.coffee
@@ -585,7 +585,6 @@

           country = countryList[_val]

-          console.log "country is: #{country}"
           # if country US or CA force state abbreviations
           if country.countryISO is 'US' or country.countryISO is 'CA'
             placeholderTxt = if country.countryISO is 'US' then "state abbreviation (e.g. CA for California)" else "province abbreviation (e.g. QC for Quebec)"


Update zh-HANT.coco.coffee (#6631)

Update zh-HANT.coco.coffee (#6631)

Translated some tags.


ParseInt correctly

ParseInt correctly

--- app/modules/tool_loader.coffee
+++ app/modules/tool_loader.coffee
@@ -5,7 +5,7 @@
   project = Manager.get('project')
   Manager.set 'tools', projects[project].tools

-  if location.port > 3332
+  if parseInt(location.port) > 3332
     Ubret.Loader Manager.get('tools'), cb
   else
     Ubret.ToolsetLoader Manager.get('project'), cb


operations: BrowseNodeItems for subscriptions node calls getSubscriptions not getSubscription

--- src/local/operations.coffee
+++ src/local/operations.coffee
@@ -297,7 +297,7 @@
 class BrowseNodeItems extends PrivilegedOperation
     privilegedTransaction: (t, cb) ->
         if @subscriptionsNodeOwner?
-            t.getSubscription @subscriptionsNodeOwner, (err, subscriptions) =>
+            t.getSubscriptions @subscriptionsNodeOwner, (err, subscriptions) =>
                 if err
                     return cb err


updating Victoria's bio

updating Victoria's bio

--- app/pages/about/team-page.cjsx
+++ app/pages/about/team-page.cjsx
@@ -172,9 +172,7 @@
         bio: '''Veronica is a Data Scientist who works on discovering patterns in Zooniverse data, by manipulating it and using it to create machine learning models. She received a PhD in Information Science and Technology from Syracuse University.'''
       victoriaVanHyning:
         title: 'Researcher'
-        bio: '''Victoria is a Digital Humanities postdoc for the Zooniverse. She holds a masters
-        in Medieval English literature from Oxford and a PhD in early modern English literature
-        from the University of Sheffield. Coffee is her lifeblood.'''
+        bio: '''Victoria is a British Academy postdoctoral fellow in English at Oxford, and is the humanities PI at Zooniverse. She works on Science Gossip, Annotate, and Shakespeare's World and more. Coffee is her lifeblood.'''

 teamMembers =
   adamMcMaster:


Hide the module title when editor is closed.

This includes a small improvement where the editor element is find through
traversal from @$el rather than a global jquery search. It also solves the
problem where deleting a module/book leaves the title hanging.

--- scripts/views/workspace/content/layouts/editor.coffee
+++ scripts/views/workspace/content/layouts/editor.coffee
@@ -1,9 +1,8 @@
 define [
-  'jquery'
   'marionette'
   'cs!views/workspace/content/content-edit'
   'hbs!templates/workspace/content/layouts/editor'
-], ($, Marionette, ContentEditView, editorTemplate) ->
+], (Marionette, ContentEditView, editorTemplate) ->

   return class EditorLayout extends Marionette.Layout
     template: editorTemplate
@@ -14,8 +13,15 @@
     onRender: () ->
       @edit.show(new ContentEditView({model: @model}))

+    onShow: () ->
       # FIXME. Set the title of the edited content. This could likely be
       # done better, and would normally be unnecessary because this info is
       # also in @title above, but presently this is being hidden in css pending
       # other changes, so we need this feedback here.
-      $('#module-title-indicator').text(@model.get('title'))
+      @$el.parent().parent().parent().parent().find(
+        '#module-title-indicator').text(@model.get('title'))
+
+    onClose: () ->
+      # Clear the title of the edited content
+      @$el.parent().parent().parent().parent().find(
+        '#module-title-indicator').text('')


more Bacon.spy spec

more Bacon.spy spec

--- spec/BaconSpec.coffee
+++ spec/BaconSpec.coffee
@@ -2689,6 +2689,8 @@
       testSpy 1, -> Bacon.constant(1)
     it "map", ->
       testSpy 2, -> Bacon.once(1).map(->)
+    it "combineTemplate (also called for the intermediate combineAsArray property)", ->
+      testSpy 4, -> Bacon.combineTemplate(Bacon.once(1), Bacon.constant(2))

 describe "Infinite synchronous sequences", ->
   describe "Limiting length with take(n)", ->