`updateCart` is not needed now

`updateCart` is not needed now

--- resources/assets/coffee/_classes/store-supporter-tag.coffee
+++ resources/assets/coffee/_classes/store-supporter-tag.coffee
@@ -84,7 +84,6 @@

     .always =>
       @searching = false
-      @updateCart(@user)
       @updateSearchResult()

   calculate: (position) =>
@@ -101,14 +100,12 @@
     @debouncedGetUser(event.currentTarget.value)

   setUserInteraction: (enabled) =>
+    StoreCart.setEnabled(enabled)
     $(@el).toggleClass('store-supporter-tag--disabled', !enabled)
     $('.js-slider').slider('disabled': !enabled)

   sliderValue: (price) ->
     price * @RESOLUTION
-
-  updateCart: (data) ->
-    StoreCart.setEnabled(data?)

   updateSearchResult: =>
     if @searching


Sim: When someone leaves, they're removed from the user hash.

--- server/socket_hash.coffee
+++ server/socket_hash.coffee
@@ -10,11 +10,13 @@

   remove: (socket) ->
     array = @sockets[socket.id]
-    return false  if !array
+    return 0  if !array
     index = array.indexOf(socket)
-    return false  if index == -1
+    return array.length  if index == -1
     array.splice(index, 1)
-    array.length
+    length = array.length
+    delete @sockets[socket.id]  if length == 0
+    return length

   contains: (socketId) ->
     @sockets[socketId]?


Fix mobile arrow (closes #312)

Fix mobile arrow (closes #312)

--- components/layout/body/view.coffee
+++ components/layout/body/view.coffee
@@ -19,11 +19,11 @@

   mobileEvents:
     'tap'                                         : 'bodyClick'
+    'tap #scroll-top'                             : 'scrollToTop'
     'tap a[data-disabled]'                        : 'disable'
     'tap a[data-client]:not([data-disabled])'     : 'intercept'
     'tap span[data-client]:not([data-disabled])'  : 'intercept'
     'tap a'                                       : 'maybeIntercept'
-    'tap #scroll-top'                             : 'scrollToTop'

   initialize: (options) ->
     new Router
@@ -38,7 +38,8 @@
     # view loses event delegation only on a channel.
     _.defer => @delegateEvents()

-    @delegateEvents(@mobileEvents) if $('body').hasClass 'is-mobile'
+    if $('body').hasClass 'is-mobile'
+      _.defer => @delegateEvents(@mobileEvents)

   startLoading: -> $('body').addClass 'is-loading'


added some more so good images

added some more so good images

--- scripts/seal-of-approval.coffee
+++ scripts/seal-of-approval.coffee
@@ -43,7 +43,11 @@
  "http://gifsforum.com/images/gif/clap%20clap%20clap/grand/52514906-claping-.gif",
  "http://gifsforum.com/images/gif/clap%20clap%20clap/grand/SAM-JACKSON-CLAPPING-GIF.gif",
  "http://gifsforum.com/images/gif/clap%20clap%20clap/grand/deniroclap.gif",
- "http://www.threadbombing.com/data/media/2/busey_clapping.gif"
+ "http://www.threadbombing.com/data/media/2/busey_clapping.gif",
+ "http://i.imgur.com/bhKp2.gif",
+ "http://i.imgur.com/3w1ej.gif",
+ "http://i.imgur.com/JSBTl.gif",
+ "http://i.imgur.com/NnBHS.gif"
 ]

 module.exports = (robot) ->


client: pass NO_UGLIFYJS environment variable to `kd` compiler

--- build-client.coffee
+++ build-client.coffee
@@ -89,8 +89,10 @@

   buildFramework:->

-    buildLoggedInFramework  = "cd client/Framework && npm i && gulp compile --uglify --outputDir=../../website/a/"
-    buildLoggedOutFramework = "cd client/Framework && npm i && gulp compile --uglify --outputDir=../../website/a/out/  --entryPath=../entry.coffee"
+    uglify = unless process.env.NO_UGLIFYJS then "--uglify" else ""
+
+    buildLoggedInFramework  = "cd client/Framework && npm i && gulp compile #{uglify} --outputDir=../../website/a/"
+    buildLoggedOutFramework = "cd client/Framework && npm i && gulp compile #{uglify} --outputDir=../../website/a/out/  --entryPath=../entry.coffee"
     exec buildLoggedInFramework, (err, stdout, stderr)->
       console.log """
       ----------------------------------- KD FRAMEWORK COMPILED -----------------------------------


AccountSessionListWrapper: Do not listen ItemDeleted event from list controller anymore

--- client/account/lib/accountsessionlistwrapper.coffee
+++ client/account/lib/accountsessionlistwrapper.coffee
@@ -26,7 +26,7 @@
       fetcherMethod     : (query, options, callback) ->
         whoami().fetchMySessions options, (err, sessions)  -> callback err, sessions

-    @listController.on 'ItemDeleted', (item) ->
+    @listController.getListView().on 'ItemWasRemoved', (item) ->
       session  = item.getData()
       clientId = kookies.get 'clientId'


minor: whitespace

minor: whitespace

--- client/ide/lib/views/chat/idechatmessagepane.coffee
+++ client/ide/lib/views/chat/idechatmessagepane.coffee
@@ -187,7 +187,8 @@

     if channel.lastMessage.payload?
       { systemType } = channel.lastMessage.payload
-      systemType or= channel.lastMessage.payload['system-message']
+      systemType   or= channel.lastMessage.payload['system-message']
+
     isAlreadyUsed   = systemType not in [ 'initiate', 'start' ]
     hasParticipants = channel.participantCount > 1


Do not fake backend events

Do not fake backend events

--- client/ide/lib/views/chat/idechatmessagepane.coffee
+++ client/ide/lib/views/chat/idechatmessagepane.coffee
@@ -215,12 +215,6 @@

   participantAdded: (participant) ->

-    # forward this event to channel, so that
-    # it can change in other views as well.
-    # Kind of observable. ~Umut
-    channel = @getData()
-    channel.emit 'AddedToChannel', participant
-
     @onboarding?.destroy()

     appManager = kd.getSingleton 'appManager'


Improved testiung environment for private methods

--- test/general.test.coffee
+++ test/general.test.coffee
@@ -1,6 +1,8 @@
 expect = require('chai').expect

-streamTweets = require '../index'
+process.env.NODE_ENV = 'test'
+
+streamTweets = require('../index').main

 twitterKeys = {
   consumer_key : 'XXXXXXXXXXXXXXXXXXXXXXXXX'


:memo:

:memo:

--- lib/atom-io-client.coffee
+++ lib/atom-io-client.coffee
@@ -76,6 +76,8 @@
       return callback(err) if err

       try
+        # NOTE: request's json option does not populate err if parsing fails,
+        # so we do it manually
         body = JSON.parse(body)
         delete body.versions

@@ -214,6 +216,8 @@
           reject error
         else
           try
+            # NOTE: request's json option does not populate err if parsing fails,
+            # so we do it manually
             body = JSON.parse(body)
             resolve(
               body.filter (pkg) -> pkg.releases?.latest?


Don't require express-csv any more, we don't use it

--- server/routes/indicators.coffee
+++ server/routes/indicators.coffee
@@ -2,7 +2,6 @@
 Theme = require('../models/theme').model
 _ = require('underscore')
 async = require('async')
-csv = require('express-csv')
 Q = require('q')

 exports.show = (req, res) ->


simplify update get plugins name

simplify update get plugins name

--- src/cli/plugins.coffee
+++ src/cli/plugins.coffee
@@ -203,11 +203,9 @@
 				else
 					scaffolding.plugins.info.getPluginsJson()
 						.then (plugins_data) ->
-							toUpdate = []
-							for k,v of plugins_data
-								# if v != ''
-								toUpdate.push k
-							chainPluginsUpdate toUpdate
+							plugin_names = Object.keys(plugins_data)
+							plugin_names.remove("")
+							chainPluginsUpdate plugin_names

 		getInfo = (name, done, fetch) ->
 			text = ''


not fixed

not fixed

--- src/client/auth.coffee
+++ src/client/auth.coffee
@@ -8,7 +8,7 @@
 try
   h = document.location.hash.replace('#', '')
   if !!h
-    hash = JSON.parse(Base64.atob(h))
+    hash = JSON.parse(atob(h))
     if window?.opener?.Hull? and window?.opener?.__hull_login_status__ and  !!hash
       window.opener.__hull_login_status__(hash)
       window.close()


do not show availability if institutional seller

--- models/artwork.coffee
+++ models/artwork.coffee
@@ -308,6 +308,7 @@
       @get 'sale_message'

   availabilityMessage: ->
+    return if @get('partner')?.type is "Institutional Seller"
     return if @get('availability') is 'for sale'
     if @get('partner').has_limited_fair_partnership
       'Not inquireable'


Fix silly regex mistake

Fix silly regex mistake

--- src/coffee/main.coffee
+++ src/coffee/main.coffee
@@ -36,7 +36,7 @@
       if data then success() else fail()

   checkSmiles = (smiles, success, fail) ->
-    regex = new RegExp "OC[C@@H](O1)[C@@H](O)[C@H](O)[C@@H]2[C@@H]1c3c(O)c(OC)c(O)cc3C(=O)O2"
+    regex = /^([^J][a-z0-9@+\-\[\]\(\)\\\/%=#$]{6,})$/ig
     if regex.test smiles then success() else fail()

   # Actions to take when we refresh the preview.


udpate group if changed

udpate group if changed

--- angular/core/services/intercom_service.coffee
+++ angular/core/services/intercom_service.coffee
@@ -1,5 +1,6 @@
 angular.module('loomioApp').factory 'IntercomService', ($rootScope, $window, AppConfig, Session, LmoUrlService) ->
-  currentGroup = null
+  lastGroup = {}
+
   mapGroup = (group) ->
     return null unless group?
     id: group.id
@@ -31,7 +32,7 @@
     boot: ->
       return unless $window? and $window.Intercom?
       user = Session.user()
-      firstGroup = user.parentGroups()[0]
+      lastGroup = mapGroup(user.parentGroups()[0])

       $window.Intercom 'boot',
        admin_link: LmoUrlService.user(user, {}, { noStub: true, absolute: true, namespace: 'admin/users', key: 'id' })
@@ -44,9 +45,8 @@
        user_id: user.id
        created_at: user.createdAt
        is_coordinator: user.isCoordinator
-       angular_ui: true
        locale: user.locale
-       company: mapGroup(firstGroup)
+       company: lastGroup
        has_profile_photo: user.hasProfilePhoto()
        belongs_to_paying_group: user.belongsToPayingGroup()

@@ -57,14 +57,14 @@
     updateWithGroup: (group) ->
       return unless @available()
       user = Session.user()
-      return if currentGroup == group
+      return if _.isEqual(lastGroup, mapGroup(group))
       return if group.isSubgroup()
       return if !user.isMemberOf(group)
-      currentGroup = group
+      lastGroup = mapGroup(group)
       $window.Intercom 'update',
         email: user.email
         user_id: user.id
-        company: mapGroup(group)
+        company: lastGroup

     contactUs: ->
       if @available()


Added first draft of list route for persons.

--- fetsy-hammertag/server/person.coffee
+++ fetsy-hammertag/server/person.coffee
@@ -9,6 +9,22 @@
 module.exports = express.Router
     caseSensitive: app.get 'case sensitive routing'
     strict: app.get 'strict routing'
+
+
+# List route
+
+# Handle get requests. Retrieve extended list of persons from database.
+.get '/', (request, response) ->
+    database.person().find().sort( id: 1 ).toArray (error, documents) ->
+        if error?
+            response.status(500).json
+                detail: error
+        else
+            # TODO: Add currently applied objects here.
+            response.send
+                persons: documents
+        return
+    return

 ## Detail route
@@ -27,12 +43,13 @@
         if error?
             response.status(500).json
                 detail: error
-        if not result?
-            result =
-                id: request.personId
-                description: 'Unknown'
-        response.send
-            person: result
+        else
+            if not result?
+                result =
+                    id: request.personId
+                    description: 'Unknown'
+            response.send
+                person: result
         return
     return


Change test fill file

Change test fill file

--- client/fill_tests.coffee
+++ client/fill_tests.coffee
@@ -6,17 +6,38 @@

 module.exports = ->
     glob('app/**/*.coffee').on('match', (file) ->
-        file = file.replace('app/', 'test/')
-        fs.readFile(file, (err, data) ->
-            return if not err
-            name = file.split('/').pop().replace('.coffee', '')
-            mkdirp(file.split('/').slice(0, -1).join('/'), ->
-                fs.writeFile(file, """
-                describe('#{name}', ->
-                    it.skip('needs tests', ->
+        testFile = file.replace('app/', 'test/')

+        fs.readFile(testFile, (err, data) ->
+            return unless err
+
+            testPath = file.split('/').slice(0, -1).join('/')
+
+            len = file.split('/').length
+            originalFile = (new Array(len)).join('../') + file
+
+            contextName = file.split('/')
+                .pop()
+                .toLowerCase()
+                .replace('.coffee', '')
+                .replace('_', ' ')
+
+            moduleName = file.split('/')
+                .pop()
+                .toLowerCase()
+                .replace('.coffee', '')
+                .replace(/_([a-z])/g, (g) -> g[1].toUpperCase())
+
+            mkdirp(testPath, ->
+                fs.writeFile(testFile, """
+                    #{moduleName} = require('#{originalFile}')
+                    expect = require('chai').expect
+
+                    describe('#{contextName}', ->
+                        it.skip('needs tests', ->
+                            expect(false).to.be.true
+                        )
                     )
-                )

                 """)
             )


StaticPage: Fix comments are listed in reverse order bug

--- server/lib/crawler/staticpages/activity.coffee
+++ server/lib/crawler/staticpages/activity.coffee
@@ -28,6 +28,7 @@
   commentsList = ""
   return commentsList  unless activityContent?.replies

+  activityContent.replies.reverse()
   for comment in activityContent.replies
     {replier, message} = comment
     {hash, avatar, nickname, fullName} = replier


warmup fix

warmup fix

--- lib/connection/ipc.coffee
+++ lib/connection/ipc.coffee
@@ -42,8 +42,8 @@
       @id += 1
       @callbacks[@id] = {resolve, reject}
       @msg {type, callback: @id}, args...
-    @loading.working()
-    done = => @loading.done()
+    @loading?.working()
+    done = => @loading?.done()
     x.then done, done
     x


Fixing tests

Fixing tests

--- test/credentials.spec.coffee
+++ test/credentials.spec.coffee
@@ -741,20 +741,15 @@

     it 'should call clearCachedId if user is not authorized', ->
       clearCache = helpers.spyOn(creds,'clearCachedId')
-      helpers.mockResponses [
-        {data: null, error: {code: 'NotAuthorizedException'}}
-      ]
-      creds.clearIdOnNotAuthorized()
+      idErr = {code: 'NotAuthorizedException'}
+      creds.clearIdOnNotAuthorized(idErr)
       expect(clearCache.calls.length).to.equal(1)

     it 'should not call clearCachedId if user is authorized', ->
       clearCache = helpers.spyOn(creds,'clearCachedId')
-      helpers.mockResponses [
-        {data: null, error: {code: ''}}
-      ]
-      creds.clearIdOnNotAuthorized()
+      idErr = {code: 'TEST'}
+      creds.clearIdOnNotAuthorized(idErr)
       expect(clearCache.calls.length).to.equal(0)
-

   describe 'createClients', ->
     beforeEach -> setupCreds()
@@ -878,7 +873,7 @@
       expect(creds.cacheId.calls.length).to.equal(0)
       expect(creds.getStorage('id')).not.to.exist

-    it 'clears cache if getId fails', ->
+    it 'does not clear cache if getId fails for authorized user', ->
       creds.setStorage('id', 'MYID')
       helpers.mockResponses [
         {data: {IdentityId: 'IDENTITY-ID'}, error: null},
@@ -889,7 +884,7 @@
       expect(creds.cacheId.calls.length).to.equal(0)
       expect(creds.getStorage('id')).to.exist

-    it 'clears cache if getOpenIdToken fails', ->
+    it 'does not clear cache if getOpenIdToken fails for authorized user', ->
       creds.setStorage('id', 'MYID')
       helpers.mockResponses [
         {data: {IdentityId: 'IDENTITY-ID'}, error: null},
@@ -898,9 +893,9 @@
       helpers.spyOn(creds.webIdentityCredentials, 'refresh').andCallFake(->)
       creds.refresh (err) -> expect(err.message).to.equal('INVALID SERVICE')
       expect(creds.cacheId.calls.length).to.equal(0)
-      expect(creds.getStorage('id')).to.exist
-
-    it 'clears cache if getCredentialsForIdentity fails', ->
+      expect(creds.getStorage('id')).to.equal('MYID')
+
+    it 'does not clear cache if getCredentialsForIdentity fails for authorized user', ->
       delete creds.cognito.config.params.RoleArn
       creds.setStorage('id', 'MYID')
       helpers.mockResponses [
@@ -909,7 +904,7 @@
       ]
       creds.refresh (err) -> expect(err.message).to.equal('INVALID SERVICE')
       expect(creds.cacheId.calls.length).to.equal(0)
-      expect(creds.getStorage('id')).to.exist
+      expect(creds.getStorage('id')).to.equal('MYID')

     it 'does try to load creds second time if service request failed', ->
       reqs = helpers.mockResponses [


CLI: Filling in the 'uninstall' command

CLI: Filling in the 'uninstall' command

--- cli/commands/uninstall.coffee
+++ cli/commands/uninstall.coffee
@@ -5,7 +5,16 @@
 #

 exports.init = (args) ->
-  print "TODO: make it happen"
+  if args.length == 0
+    print_error "You should specify the package name"
+
+  location = lovelyrc.base
+  location[location.length - 1] == '/' || (location += '/')
+  location += "packages/#{args[0]}/#{args[1] || ''}"
+
+  sout "» Uninstalling ~/.lovely/packages/#{args[0]}/#{args[1] || ''}".ljust(61)
+  system "rm -rf #{location}", ->
+    sout "Done\n".green

 exports.help = (args) ->
@@ -13,6 +22,6 @@
   Uninstalls a package

   Usage:
-      lovely uninstall <package-name>
+      lovely uninstall <package-name>[ <version>]

   """


chat: start reacting to channel events on settings

--- client/ide/views/chat/settingspane.coffee
+++ client/ide/views/chat/settingspane.coffee
@@ -23,6 +23,17 @@

     @on 'CollaborationStarted', =>
       @toggleButtons 'started'
+
+    @bindChannelEvents()
+
+
+  bindChannelEvents: ->
+
+    channel = @getData()
+
+    channel
+      .on 'RemovedFromChannel', (acc) => @removeParticipant acc.profile.nickname
+      .on 'AddedToChannel',     (acc) => @addParticipant acc.profile.nickname

   createElements: ->


Set add-row css class for targeting during testing

--- client/lanes/components/grid/Toolbar.cjsx
+++ client/lanes/components/grid/Toolbar.cjsx
@@ -4,7 +4,9 @@
         addRecord: React.PropTypes.func

     addButton: ->
-        <BS.Button className="navbar-btn pull-right" onClick={@props.onAddRecord} bsSize='small'>
+        <BS.Button className="navbar-btn add-row pull-right"
+            onClick={@props.onAddRecord} bsSize='small'
+        >
             Add Row
         </BS.Button>


Fixed up RedMetrics part

Fixed up RedMetrics part

--- client/src/app/io.coffee
+++ client/src/app/io.coffee
@@ -760,6 +760,10 @@
       if not extendedData.customData
         extendedData.customData = {}
       extendedData.customData.ipAddress = playerIpAddress
+
+      # Currently, custom data must contain a string (which itself can be JSON)
+      extendedData.customData = JSON.stringify(extendedData.customData)
+
       return extendedData

     beginGameSession = ->
@@ -886,12 +890,13 @@
           playerInfo = newPlayerInfo

         if createNewGameSession
+          endGameSession()
           beginGameSession()
         else if newPlayerInfo
           jqXhr = $.ajax
             url: options.metrics.host + "/v1/player/" + playerId
             type: "PUT"
-            data: JSON.stringify(JSON.stringify(makeExtendedPlayerInfo()))
+            data: JSON.stringify(makeExtendedPlayerInfo())
             processData: false
             contentType: "application/json"


bugfix nullpointer

bugfix nullpointer

--- lib/Context.coffee
+++ lib/Context.coffee
@@ -485,7 +485,7 @@
                 identity = null
             identity ?= @identity

-            if !identity.id || !identity.token
+            if !identity?.id || !identity.token
                 return done new Error '2. parameter is not identity'

             if !done ## recurse promisifyed


Remove Duplication

Remove Duplication

--- lib/fuzzy-provider.coffee
+++ lib/fuzzy-provider.coffee
@@ -174,10 +174,8 @@

     results = []

+    # dont show matches that are the same as the prefix
     for word in words when word isnt prefix
-      # dont show matches that are the same as the prefix
-      continue if word is prefix
-
       # must match the first char!
       continue unless prefix[0].toLowerCase() is word[0].toLowerCase()


Deploy certs

Deploy certs

--- actions/openldap_server_tls.coffee
+++ actions/openldap_server_tls.coffee
@@ -354,7 +354,9 @@
 module.exports = []

 module.exports.push 'phyla/actions/openldap_server'
-module.exports.push 'phyla/actions/openldap_client'
+# We cant require openldap_client here, since it will deploy
+# and test a secure connection on a server not yet configured
+# module.exports.push 'phyla/actions/openldap_client'

 module.exports.push (ctx) ->
   ctx.config.openldap_server ?= {}
@@ -362,39 +364,59 @@
   # ctx.config.openldap_server.tls_key_file ?= null

 module.exports.push name: 'OpenLDAP TLS # Deploy', callback: (ctx, next) ->
-  { tls, tls_cert_file, tls_key_file } = ctx.config.openldap_server
+  { tls, tls_ca_cert_file, tls_cert_file, tls_key_file } = ctx.config.openldap_server
   return next null, ctx.DISABLED unless tls
-  # tls_cert_filename = path.basename tls_cert_file
-  # tls_key_filename = path.basename tls_key_file
-  # modified = false
-  # ctx.log 'Write certificate files'
-  # ctx.write [
-  #   source: tls_cert_file
-  #   local_source: true
-  #   destination: "/etc/pki/tls/certs/#{tls_cert_filename}"
-  #   uid: 'ldap'
-  #   gid: 'ldap'
-  #   mode: '400'
-  # ,
-  #   source: tls_key_file
-  #   local_source: true
-  #   destination: "/etc/pki/tls/certs/#{tls_key_filename}"
-  #   uid: 'ldap'
-  #   gid: 'ldap'
-  #   mode: '400'
-  # ,
-  #   destination: '/etc/openldap/slapd.d/cn=config.ldif'
-  #   write: [
-  #     match: /^olcTLSCertificateFile.*$/mg
-  #     replace: "olcTLSCertificateFile: /etc/pki/tls/certs/#{tls_cert_filename}"
-  #     append: 'olcRootPW'
-  #   ,
-  #     match: /^olcTLSCertificateKeyFile.*$/mg
-  #     replace: "olcTLSCertificateKeyFile: /etc/pki/tls/certs/#{tls_key_filename}"
-  #     append: 'olcTLSCertificateFile'
-  #   ]
-  # ], (err, written) ->
-  #   return next err, if written then ctx.OK else ctx.PASS
+  tls_ca_cert_filename = path.basename tls_ca_cert_file
+  tls_cert_filename = path.basename tls_cert_file
+  tls_key_filename = path.basename tls_key_file
+  modified = false
+  ctx.log 'Write certificate files'
+  ctx.upload [
+    source: tls_ca_cert_file
+    local_source: true
+    destination: "/etc/pki/tls/certs/#{tls_ca_cert_filename}"
+    uid: 'ldap'
+    gid: 'ldap'
+    mode: '400'
+  ,
+    source: tls_cert_file
+    local_source: true
+    destination: "/etc/pki/tls/certs/#{tls_cert_filename}"
+    uid: 'ldap'
+    gid: 'ldap'
+    mode: '400'
+  ,
+    source: tls_key_file
+    local_source: true
+    destination: "/etc/pki/tls/certs/#{tls_key_filename}"
+    uid: 'ldap'
+    gid: 'ldap'
+    mode: '400'
+  ], (err, written) ->
+    return next err, ctx.PASS if err or not written
+    ctx.write
+      destination: '/etc/openldap/slapd.d/cn=config.ldif'
+      write: [
+        match: /^olcTLSCACertificatePath.*$/mg
+        replace: "olcTLSCACertificatePath: /etc/pki/tls/certs/#{tls_ca_cert_filename}"
+        # append: 'olcRootPW'
+      ,
+        match: /^olcTLSCertificateFile.*$/mg
+        replace: "olcTLSCertificateFile: /etc/pki/tls/certs/#{tls_cert_filename}"
+        # append: 'olcRootPW'
+      ,
+        match: /^olcTLSCertificateKeyFile.*$/mg
+        replace: "olcTLSCertificateKeyFile: /etc/pki/tls/certs/#{tls_key_filename}"
+        # append: 'olcTLSCertificateFile'
+      ]
+    , (err, written) ->
+      return next err if err
+      ctx.service
+        name: 'openldap-servers'
+        srv_name: 'slapd'
+        action: 'restart'
+      , (err, restarted) ->
+        next err, ctx.OK

 module.exports.push name: 'OpenLDAP TLS # Activate LDAPS', callback: (ctx, next) ->
   ctx.write


AdminModal: Adjust height.

AdminModal: Adjust height.

--- client/admin/lib/index.coffee
+++ client/admin/lib/index.coffee
@@ -63,7 +63,7 @@
       title      : 'Team Settings'
       cssClass   : 'AppModal AppModal--admin'
       width      : 1000
-      height     : '100%'
+      height     : '90%'
       overlay    : yes
       tabData    : NAV_ITEMS
     , data


Simplified initialized check

Simplified initialized check

--- lib/debug.coffee
+++ lib/debug.coffee
@@ -92,6 +92,10 @@
       value: new ExtraDebugOptions
       enumerable: true

+  initialized = false
+  isInitialized: ->
+    [r ,initialized] = [initialized, true]
+    r
   init: ->
     @log "Debug Mode"
     if extraOpts.size is 1 then @_log "Extra Option: #{process.env.DEBUG_EXTRA}"
@@ -103,10 +107,8 @@
     txt = "[DEBUG] #{msg}".cyan
     if obj? then console.log txt, obj else console.log txt

-  initialized = false
   init: ->
-    return Debug.wrap('Already initialized') if initialized
-    initialized = true
+    return Debug.wrap('Already initialized') if @isInitialized()
     process.env.DEBUG = 1 if @isEnabled()
     process.env.DEBUG_EXTRA = Array.from(extraOpts).join(',') if extraOpts.size > 0
     super()
@@ -118,10 +120,8 @@
     txt = "%c[DEBUG] #{msg}"
     if obj? then console.debug txt, style, obj else console.debug txt, style

-  initialized = false
   init: ->
-    return Debug.wrap('Already initialized') if initialized
-    initialized = true
+    return Debug.wrap('Already initialized') if @isInitialized()
     @setEnabled process.env.DEBUG?
     process.env.DEBUG_EXTRA?.split(',').forEach @enableExtra.bind @
     super()


修正几个Deferred遗留BUG

修正几个Deferred遗留BUG

--- src/core/entity.coffee
+++ src/core/entity.coffee
@@ -1215,10 +1215,8 @@
 				result = providerInvoker.invokeSync()
 				@initData(result)
 			else
-				dfd = providerInvoker.invokeAsync().done((result)->
+				dfd = providerInvoker.invokeAsync().done((result)=>
 					@initData(result)
-					if callback instanceof Function
-						cola.callback(callback, true, result)
 					return
 				)
 		return cola.util.createDeferredIf(dfd, result)
@@ -1474,7 +1472,7 @@
 						@_setCurrentPage(page, setCurrent)

 					@_dontAutoSetCurrent++
-					dfd = page.loadData(loadMode).done((result)->
+					dfd = page.loadData(loadMode).done((result)=>
 						@_dontAutoSetCurrent--
 						if @_currentPage isnt page
 							@_setCurrentPage(page, setCurrent)
@@ -1482,7 +1480,7 @@
 							@pageCount = pageNo
 						return
 					)
-		return cola.util.createDeferredIf(dfd, result).done(()->
+		return cola.util.createDeferredIf(dfd).done(()->
 			cola.callback(callback, true)
 			return
 		)
@@ -1497,21 +1495,19 @@
 			pageNo = @pageCount
 		return @_loadPage(pageNo, true, loadMode)

-	firstPage: (loadMode)->
-		return gotoPage(1, loadMode)
+	firstPage: (loadMode)-> @gotoPage(1, loadMode)

 	previousPage: (loadMode)->
 		pageNo = @pageNo - 1
 		if pageNo < 1 then pageNo = 1
-		return gotoPage(pageNo, loadMode)
+		return @gotoPage(pageNo, loadMode)

 	nextPage: (loadMode)->
 		pageNo = @pageNo + 1
 		if @pageCountDetermined and pageNo > @pageCount then pageNo = @pageCount
-		return gotoPage(pageNo, loadMode)
-
-	lastPage: (loadMode)->
-		return gotoPage(@pageCount, loadMode)
+		return @gotoPage(pageNo, loadMode)
+
+	lastPage: (loadMode)-> @gotoPage(@pageCount, loadMode)

 	insert: (entity, insertMode, refEntity)->
 		if isFinite(insertMode)
@@ -1628,7 +1624,7 @@
 		if changeCurrent
 			@setCurrent(newCurrent)
 			if newCurrent
-				newCurrent.page.hotIndex = newCurrentIndex
+				newCurrent._page.hotIndex = newCurrentIndex
 		return entity

 	empty: ()->


Using crazy new include lookup for non-string modules

--- src/core_object.coffee
+++ src/core_object.coffee
@@ -172,7 +172,8 @@
         self[name](args...)

     # Initial module inclue, for reconstructing objects from JSON
-    for moduleName is
+    for moduleName is I.includedModules
+

     return self
 )()


Update cycle-times.coffee

Update cycle-times.coffee

Adding documentation for the commands

I believe I have got all of the commands that weren't documented.

--- src/cycle-times.coffee
+++ src/cycle-times.coffee
@@ -15,6 +15,9 @@
 #   hubot cycle offset
 #   hubot cycle set offset [offset]
 #   hubot cycle set offsetname [offset name]
+#   hubot checkpoints|cps on [date] [timezone]
+#   hubot mindunits|mindunit|mu average [ours]|[ours]k [theirs]|theirs]k
+#   hubot mindunits|mindunit|mu needed [ours]|[ours]k [theirs]|theirs]k
 #
 # Author:
 #   impleri


fix error handler

fix error handler

--- lib/index.coffee
+++ lib/index.coffee
@@ -18,9 +18,6 @@
         "'#{error.message}' in #{file}"+"@".bold+"#{loc.first_line}:#{loc.first_column}\n"
         before+first+middle.red.underline+last+after
     )
-module.exports.handleError = handleError
-
-
 createTransform = (ngClassifyOptions = {}) ->
     return  (file) ->
         return through() if not (/\.coffee$/i).test(file)
@@ -37,4 +34,5 @@
         )

 module.exports = createTransform
+module.exports.handleError = handleError
 module.exports.ngclassifyfy = createTransform


refactor code, document it better

refactor code, document it better

--- lib/index.coffee
+++ lib/index.coffee
@@ -1,51 +1,64 @@
 require 'colors'
-fs     = require 'fs'
-path   = require 'path'
-_      = require 'lodash'
-accord = require 'accord'
+fs           = require 'fs'
+path         = require 'path'
+_            = require 'lodash'
+accord       = require 'accord'
+EventEmitter = require('events').EventEmitter

-# return an event emitter
-EventEmitter = require('events').EventEmitter
-cli = new EventEmitter
-module.exports = cli
+module.exports = cli = new EventEmitter

-# run the actual command
+###*
+ * The main export has to be the event emitter to be able to correctly
+ * attach events before run. This function actually runs the cli logic,
+ * given an arguments object.
+ *
+ * First, we get some information we'll need - the locals passed in, the
+ * path to the file being compiled, the file's extension, and the adapter
+ * being used to compile it.
+ *
+ * Then we handle common errors - if the file type isn't supported by accord
+ * or if the actual file passed in was not found.
+ *
+ * After this, we grab the adapter and try to compile the file. If there
+ * was a compile error, that's emitted. If not, we either write or log the
+ * results.
+ *
+ * @param  {Object} argv - command line arguments, parsed by minimist
+ * @return {EventEmitter} returns cli for chaining
+###
+
 module.exports.run = (argv) ->
   locals = get_locals(argv)
   filepath = path.resolve(argv.compile)
   ext = path.extname(filepath).substring(1)
+  name = lookup_adapter(ext)

-  # adapter lookup
-  for k, v of accord.all()
-    a = new v
-    if _.contains(a.extensions, ext) then name = a.name
+  if not name
+    return cli.emit('err', "File extension '#{ext}' not supported".red)
+
+  if not fs.existsSync(filepath)
+    return cli.emit('err', "File '#{filepath}' not found ".red)

-  if not name then return cli.emit('err', "File extension '#{ext}' not supported".red)
-
-  # grab adapter
   adapter = accord.load(name, resolve_path(name))

-  # render the file
-  try
-    promise = adapter.renderFile(filepath, locals)
-  catch err
-    if err.code == 'ENOENT'
-      cli.emit('err', "File '#{filepath}' not found ".red)
-    else
-      cli.emit('err', err)
-    return cli
-
-  # now decide how to render the output
-  if argv.out
-    promise
-      .then((o) -> fs.writeFileSync(path.resolve(argv.out), o))
-      .then(cli.emit.bind(cli, 'done'))
-  else
-    promise.then(cli.emit.bind(cli, 'data'))
+  adapter.renderFile(filepath, locals)
+    .catch(cli.emit.bind(cli, 'err'))
+    .then((o) ->
+      if argv.out
+        fs.writeFileSync(path.resolve(argv.out), o)
+      else
+        cli.emit('data', o)
+    )
+    .then(cli.emit.bind(cli, 'done'))

   return cli

-# remove the functional flags, leaving only the locals
+###*
+ * Remove the functional flags, leaving only the 'locals'.
+ * @param  {Object} argv - args object, parsed my minimist
+ * @return {Object} cloned object, pruned of all functional keys
+###
+
 get_locals = (argv) ->
   res = _.clone(argv)
   delete res._
@@ -57,7 +70,27 @@
   delete res.w
   return res

-# can be found in accord's source
+###*
+ * Given a file extension, finds the adapter's name in accord, or
+ * returns undefined.
+ *
+ * @param  {String} ext - file extension, no dot
+ * @return {?} string adapter name or undefined
+###
+
+lookup_adapter = (ext) ->
+  for name, Adapter of accord.all()
+    a = new Adapter
+    if _.contains(a.extensions, ext) then return a.name
+  return
+
+###*
+ * Given the name of a module, returns it's home folder in node
+ * modules. This was taken from accord's source.
+ * @param  {String} name - name of a node module
+ * @return {String} path to the module
+###
+
 resolve_path = (name) ->
   _path = require.resolve(name).split(path.sep).reverse()
   for p, i in _path


Use strict

Use strict

--- lib/index.coffee
+++ lib/index.coffee
@@ -7,6 +7,8 @@
 # This software is released under the MIT License.
 # http://opensource.org/licenses/mit-license.php
 ###
+
+'use strict'

 class GA
   @version: require './version'


shellEditorView: bugfix remixView header prefixes

a header should be prefixed with its index unless it is an emptyShell
placeholder or there is at most one non-empty shell in the collection

--- coffee/src/views/shell_editor_view.coffee
+++ coffee/src/views/shell_editor_view.coffee
@@ -119,7 +119,9 @@
   renderRemixerViewHeading: (remixerView, index) =>
     index ?= @model.shells().indexOf(remixerView.model)

-    prefix = "Item #{index + 1}" if 1 < (index + 1) < @model.shells().length
+    unless @model.shells().length < 3 or @shellIsEmpty remixerView.model
+      prefix = "Item #{index + 1}"
+
     @renderSectionHeading remixerView, (prefix ? '')
     @


use closure to save async param in for loop

--- src/deploy-webhook.coffee
+++ src/deploy-webhook.coffee
@@ -28,6 +28,8 @@
 options = properties.parse fs.readFileSync(__dirname + "/../deploy.conf", { encoding: "utf8" }), {sections: true, comments: ";", separators: "", strict: true}
 {spawn, exec}  = require 'child_process'

+
+
 module.exports = (robot) ->
   gitlabChannel = process.env.HUBOT_DEPLOY_CUSTOM_CHANNEL or "#gitlab"

@@ -42,14 +44,16 @@
     # search options for deployhook name == hook.repository.name
     for key, value of options when value.deployhook is hook.repository.name
       # execute real sh script
-      exec __dirname + "/../bin/deploy -c " + __dirname + "/../deploy.conf " + key, (err, stdout, stderr) ->
-        message = "Deployed #{key} from webhook. (#{value.deployhook})\r\n" + stdout
-        if not stdout?
-          message +=  "\r\n" + "Error: no stdout something went wrong"
-        if err
-          message +=  "\r\n" + "Error: " + err
-          if stderr
-            message +=  "\r\n" + "stderr:  " + stderr
-        # write into chat that repo got deployed by webhook
-        robot.send user, message
+      executeShell = (key, value) ->
+        exec __dirname + "/../bin/deploy -c " + __dirname + "/../deploy.conf " + key, (err, stdout, stderr) ->
+          message = "Deployed #{key} from webhook. (#{value.deployhook})\r\n" + stdout
+          if not stdout?
+            message +=  "\r\n" + "Error: no stdout something went wrong"
+          if err
+            message +=  "\r\n" + "Error: " + err
+            if stderr
+              message +=  "\r\n" + "stderr:  " + stderr
+          # write into chat that repo got deployed by webhook
+          robot.send user, message
+      executeShell(key, value)
     res.end ""


engine data module

engine data module

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

   Object.defineProperty self, 'data',
     get: ->
-      this * Math.TAU / 360
+      I.data

   get: (key) ->
     I.data[key]


various tweaks

various tweaks

--- lib/csscomb.coffee
+++ lib/csscomb.coffee
@@ -5,16 +5,8 @@
 CSSBeautify = require('cssbeautify')
 CSSComb     = require('csscomb')

-csscomb        = new CSSComb('csscomb')
 userConfigPath = atom.project.getDirectories()[0]?.resolve('.csscomb.json')
 atomConfigPath = path.join(__dirname, '../csscomb.json')
-
-if fs.existsSync(userConfigPath)
-  userConfigJson = require(userConfigPath)
-  csscomb.configure(userConfigJson)
-else if fs.existsSync(atomConfigPath)
-  atomConfigJson = require(atomConfigPath)
-  csscomb.configure(atomConfigJson)

 module.exports =

@@ -34,6 +26,12 @@

     return unless editor isnt no

+    csscomb = new CSSComb('csscomb')
+    if fs.existsSync(userConfigPath)
+      csscomb.configure(require(userConfigPath))
+    else if fs.existsSync(atomConfigPath)
+      csscomb.configure(require(atomConfigPath))
+
     grammarName = editor.getGrammar().name.toLowerCase()
     isCSS  = grammarName is 'css'
     isScss = grammarName is 'scss'
@@ -47,23 +45,29 @@
     if isHTML then syntax = 'css'

     text = editor.getText()
-    selectedText = editor.getSelectedText()
+    selected = editor.getSelectedText()

-    if selectedText.length isnt 0
+    if selected.length isnt 0
       try
-        sorted     = csscomb.processString(selectedText, syntax)
-        beautified = CSSBeautify(sorted, {
-          indent: '  '
+        sorted = csscomb.processString(selected, {
+          syntax: syntax
         })
-        editor.setTextInBufferRange(editor.getSelectedBufferRange(), beautified)
+        if isCSS
+          sorted = CSSBeautify(sorted, {
+            indent: '  '
+          })
+        editor.setTextInBufferRange(editor.getSelectedBufferRange(), sorted)
       catch e
         console.log(e)
     else
       try
-        sorted = csscomb.processString(text, syntax)
-        beautified = CSSBeautify(sorted, {
-          indent: '  '
+        sorted = csscomb.processString(text, {
+          syntax: syntax
         })
-        editor.setText(beautified)
+        if isCSS
+          sorted = CSSBeautify(sorted, {
+            indent: '  '
+          })
+        editor.setText(sorted)
       catch e
         console.log(e)


Better spec descriptions

Better spec descriptions

--- spec/snippets-spec.coffee
+++ spec/snippets-spec.coffee
@@ -399,9 +399,9 @@
         editorView.trigger keydownEvent('tab', target: editorView[0])
         expect(editor.getSelectedBufferRange()).toEqual [[0, 6], [0, 11]]

-    describe "when multiple cursors are active", ->
-      describe "when the letters preceding all cursors trigger the same snippet", ->
-        it "should expand all snippets together and allow editing them simultaneous", ->
+    describe "when there are multiple cursors", ->
+      describe "when the cursors share a common snippet prefix", ->
+        it "expands the snippet for all cursors and allows simultaneous editing", ->
           editor.insertText('t9')
           editor.setCursorBufferPosition([12, 2])
           editor.insertText(' t9')
@@ -420,7 +420,7 @@
           expect(buffer.lineForRow(14)).toBe "without placeholder hello"

         describe "when there are many tabstops", ->
-          it "should expand them all together and follow the tab stops for each cursor", ->
+          it "moves the cursors between the tab stops for their corresponding snippet when tab and shift-tab are pressed", ->
             editor.addCursorAtBufferPosition([7, 5])
             editor.addCursorAtBufferPosition([12, 2])
             editor.insertText('t11')
@@ -463,8 +463,8 @@
             expect(cursors[1].selection.isEmpty()).toBe true
             expect(cursors[2].selection.isEmpty()).toBe true

-      describe "when the letters preceding all cursors trigger different snippets", ->
-        it "should insert tabs as normal", ->
+      describe "when the cursors do not share common snippet prefixes", ->
+        it "inserts tabs as normal", ->
           editor.insertText('t9')
           editor.setCursorBufferPosition([12, 2])
           editor.insertText(' t8')


something

something

--- src/experiments.coffee
+++ src/experiments.coffee
@@ -1,5 +1,5 @@
 Ableton = require '..'
-
+_ = require 'lodash'
 config =
   buttons: [
     {
@@ -22,3 +22,15 @@
   ableton.setButtonColor {x,y, color: {r: 0, g: velocity*2, b: 0}}

 ableton.connect()
+
+randomColors = =>
+  x = _.random 1, 8
+  y = _.random 1, 8
+  r = _.random 0, 255
+  g = _.random 0, 255
+  b = _.random 0, 255
+  ableton.setButtonColor {x,y,color: {r,g,b}}
+
+_.delay =>
+  setInterval randomColors, 100
+, 2000


cover an edgy case

cover an edgy case

--- test/phabs_admin_test.coffee
+++ test/phabs_admin_test.coffee
@@ -184,58 +184,97 @@

     # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     context 'when project has no phid recorded', ->
-      beforeEach ->
-        room.robot.brain.data.phabricator.projects = {
-          'Bug Report': { },
-          'project with phid': { phid: 'PHID-PROJ-1234567' },
-        }
-        room.robot.brain.data.phabricator.aliases = { }
-        do nock.disableNetConnect
-        nock(process.env.PHABRICATOR_URL)
-          .get('/api/project.query')
-          .query({
-            'names[0]': 'project1',
-            'api.token': 'xxx'
-          })
-          .reply(200, { result: {
-            'data': {
-              'PHID-PROJ-qhmexneudkt62wc7o3z4': {
-                'id': '1402',
-                'phid': 'PHID-PROJ-qhmexneudkt62wc7o3z4',
-                'name': 'Bug Report',
-                'profileImagePHID': 'PHID-FILE-2dsjotf2zgtbludzlk4s',
-                'icon': 'bugs',
-                'color': 'yellow',
-                'members': [
-                  'PHID-USER-3yc34eijivr6rqs4vgiw',
-                  'PHID-USER-7k37pmi3jffv46mzs5te'
-                ],
-                'slugs': [
-                  'bug_report'
-                ],
-                'dateCreated': '1449275954',
-                'dateModified': '1468138110'
+      context 'and the project is known', ->
+        beforeEach ->
+          room.robot.brain.data.phabricator.projects = {
+            'Bug Report': { },
+            'project with phid': { phid: 'PHID-PROJ-1234567' },
+          }
+          room.robot.brain.data.phabricator.aliases = { }
+          do nock.disableNetConnect
+          nock(process.env.PHABRICATOR_URL)
+            .get('/api/project.query')
+            .query({
+              'names[0]': 'project1',
+              'api.token': 'xxx'
+            })
+            .reply(200, { result: {
+              'data': {
+                'PHID-PROJ-qhmexneudkt62wc7o3z4': {
+                  'id': '1402',
+                  'phid': 'PHID-PROJ-qhmexneudkt62wc7o3z4',
+                  'name': 'Bug Report',
+                  'profileImagePHID': 'PHID-FILE-2dsjotf2zgtbludzlk4s',
+                  'icon': 'bugs',
+                  'color': 'yellow',
+                  'members': [
+                    'PHID-USER-3yc34eijivr6rqs4vgiw',
+                    'PHID-USER-7k37pmi3jffv46mzs5te'
+                  ],
+                  'slugs': [
+                    'bug_report'
+                  ],
+                  'dateCreated': '1449275954',
+                  'dateModified': '1468138110'
+                }
+              },
+              'slugMap': [ ],
+              'cursor': {
+                'limit': 100,
+                'after': null,
+                'before': null
               }
-            },
-            'slugMap': [ ],
-            'cursor': {
-              'limit': 100,
-              'after': null,
-              'before': null
-            }
-          } })
-
-      afterEach ->
-        room.robot.brain.data.phabricator = { }
-        nock.cleanAll()
-
-      context 'phad Bug Report info', ->
-        hubot 'phad Bug Report info'
-        it 'should reply with proper info', ->
-          expect(hubotResponse()).to.eql 'Bug Report is Bug Report, with no alias, with no feed.'
-        it 'should remember the phid from asking to phabricator', ->
-          expect(room.robot.brain.data.phabricator.projects['Bug Report'].phid)
-            .to.eql 'PHID-PROJ-qhmexneudkt62wc7o3z4'
+            } })
+
+        afterEach ->
+          room.robot.brain.data.phabricator = { }
+          nock.cleanAll()
+
+        context 'phad Bug Report info', ->
+          hubot 'phad Bug Report info'
+          it 'should reply with proper info', ->
+            expect(hubotResponse()).to.eql 'Bug Report is Bug Report, with no alias, with no feed.'
+          it 'should remember the phid from asking to phabricator', ->
+            expect(room.robot.brain.data.phabricator.projects['Bug Report'].phid)
+              .to.eql 'PHID-PROJ-qhmexneudkt62wc7o3z4'
+
+
+      context 'and the project is unknown to phabricator', ->
+        beforeEach ->
+          room.robot.brain.data.phabricator.projects = {
+            'Bug Report': { },
+            'project with phid': { phid: 'PHID-PROJ-1234567' },
+          }
+          room.robot.brain.data.phabricator.aliases = {
+            bugs: 'project with phid',
+            bug: 'project with phid'
+          }
+          do nock.disableNetConnect
+          nock(process.env.PHABRICATOR_URL)
+            .get('/api/project.query')
+            .query({
+              'names[0]': 'project1',
+              'api.token': 'xxx'
+            })
+            .reply(200, { result: {
+              'data': [ ],
+              'slugMap': [ ],
+              'cursor': {
+                'limit': 100,
+                'after': null,
+                'before': null
+              }
+            } })
+
+        afterEach ->
+          room.robot.brain.data.phabricator = { }
+          nock.cleanAll()
+
+        context 'phad unknown info', ->
+          hubot 'phad unknown info'
+          it 'should reply with proper info', ->
+            expect(hubotResponse())
+              .to.eql 'Project unknown not found.'

     # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     context 'when project has phid recorded, and aliases', ->


Better default for cell delimiters

Better default for cell delimiters

Signed-off-by: Jonathan Laurent <4511b8869b28d36762ddaf6ba8083c29700412c2@cs.cmu.edu>

--- lib/package/config.coffee
+++ lib/package/config.coffee
@@ -311,7 +311,7 @@
       cellDelimiter:
         title: 'Cell Delimiter'
         type: 'array'
-        default: ['##', '#---', '#%%', '# %%']
+        default: ['##\\s', '#---', '#%%', '# %%']
         description: 'Regular expressions for determining cell delimiters.'
         order: 11
       highlightCells:


lib/marie.coffee

lib/marie.coffee

--- lib/marie.coffee
+++ lib/marie.coffee
@@ -73,7 +73,6 @@
 			fs.stat @dir, (err, stats) =>
 				if err then @configureSails() else ui.warn 'App already exists.'
 		else
-			console.log ''
 			ui.warn 'Enter app name.'
 			prompt.start()
 			ui.line()


Fix Unicode @ handling for Mongo. Fix #160

--- lib/formats.coffee
+++ lib/formats.coffee
@@ -286,7 +286,7 @@
 @escapeRe = (string) ->
   string.replace /[\\^$*+?.()|{}\[\]]/g, "\\$&"

-@atRe = '[@\\uff20]'  ## FF20 is FULLWIDTH COMMERCIAL AT common in Asian scripts?
+@atRe = '[@\uff20]'  ## FF20 is FULLWIDTH COMMERCIAL AT common in Asian scripts?

 postprocessAtMentions = (text) ->
   return text unless 0 <= text.search ///#{atRe}///


Better {center} environment via flexbox

Better {center} environment via flexbox

--- lib/formats.coffee
+++ lib/formats.coffee
@@ -465,7 +465,7 @@
   .replace /\\begin\s*{(proof|pf)}(\s*\[([^\]]*)\])?/g, (m, env, x, opt) -> "Proof#{if opt then " (#{opt})" else ''}: "
   .replace /\\end\s*{(proof|pf)}/g, ' ∎

'
   .replace /\\begin\s*{center}([^]*?)\\end\s*{center}/g, (m, body) ->
-    '
' +
+    '
' +
     body + '
'
   .replace latexSimpleCommandsRe, (match, name) -> latexSimpleCommands[name]
   ## The following tweaks are not LaTeX actually, but useful in all modes,


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/flickerable.coffee
+++ src/flickerable.coffee
@@ -50,7 +50,8 @@

       player.include(Flickerable)

-      player.flicker(90, 5, 0.3)
+      player.flicker
+        duration: 90, 5, 0.3)
       # => This causes the sprite to flicker between full opacity
       # => and 30% opacity every 5 frames for 90 frames


partial overall avg

partial overall avg

--- src/flux/scores.coffee
+++ src/flux/scores.coffee
@@ -32,6 +32,7 @@
   updateAverages: (task, courseId, period_id, columnIndex, isAccepted, currentValue, acceptValue) ->
     scores = @_get(courseId)
     period = _.findWhere(scores, {period_id})
+    #students = _.findWhere(period,student, {})
     numStudents = period.students.length
     currentValue = currentValue / 100
     acceptValue = acceptValue / 100
@@ -39,13 +40,19 @@
     currentAssignmentAverage = period?.data_headings[columnIndex]?.average_score

     assignmentAverage =
-      (currentAssignmentAverage - (currentValue / numStudents)) + (acceptValue / numStudents)
+      (currentAssignmentAverage - (currentValue / numStudents)) +
+      (acceptValue / numStudents)

     period?.data_headings[columnIndex]?.average_score = assignmentAverage

-    period?.overall_average_score = 79
+    currentPeriodAverage = period?.overall_average_score

+    periodAverage =
+      (currentPeriodAverage - (currentValue / numStudents)) +
+      (acceptValue / numStudents)
+
+    period?.overall_average_score = periodAverage

     students = allStudents @_get(courseId)
@@ -56,7 +63,14 @@
         taskIds = _.pluck student.data, 'id'
         _.indexOf(taskIds, taskId) > -1

-    taskStudent?.average_score = 79
+    currentStudentAverage = taskStudent?.average_score
+    numTasks = taskStudent.data.length
+
+    studentAverage =
+      (currentStudentAverage - (currentValue / numTasks)) +
+      (acceptValue / numTasks)
+
+    taskStudent?.average_score = studentAverage

   acceptLate: (taskId) ->


optimize numTasksStudent count

optimize numTasksStudent count

--- src/flux/scores.coffee
+++ src/flux/scores.coffee
@@ -46,12 +46,9 @@
   ) * 100 ) / 100

   # Student's course average
-  taskStudent =
-    _.find course.students, (student) ->
-      taskIds = _.pluck student.data, 'id'
-      _.indexOf(taskIds, task.id) > -1
+  numTasksStudent = 0
+  numTasksStudent += 1 for studentTask in student.data when studentTask.is_included_in_averages

-  numTasksStudent = _.filter(taskStudent.data, {is_included_in_averages: true}).length
   student.average_score =
     ( student.average_score - ( oldScore / numTasksStudent ) ) +
       ( task.score / numTasksStudent )


:art: Refactor getFullExtension

:art: Refactor getFullExtension

--- lib/helpers.coffee
+++ lib/helpers.coffee
@@ -17,9 +17,8 @@
     styleObject

   getFullExtension: (filePath) ->
-    nextExtension = path.extname(filePath)
     fullExtension = ''
-    while nextExtension isnt ''
-      fullExtension = nextExtension.concat(fullExtension)
-      nextExtension = path.extname(path.basename(filePath, fullExtension))
+    while extension = path.extname(filePath)
+      fullExtension = extension + fullExtension
+      filePath = path.basename(filePath, extension)
     fullExtension


comments for the id function and slight refactoring

--- lib/orpheus.coffee
+++ lib/orpheus.coffee
@@ -179,30 +179,91 @@
 										return true

-			# return OrpheusAPI if we have the id, or it's
-			# a new id. otherwise, hget the id and then call
-			# orpheus api.
+
+			# The `id` function is the main interface for querying Orpheus
+			# models. After the model is created it can be "instantiated"
+			# by calling this function. For example, after a User model
+			# was created the `id` function will be available as
+			# `orpheus.schema.user`.
+			#
+			# The `id` function handles two cases: (1) where the model id
+			# is known - we simply create a new OrpheusAPI instance and
+			# return it; (2) when the model is being requested using a map:
+			# when a model attribute is defined as `@map` we automatically
+			# create a hash of `attribute_value -> model_id` under the key
+			# `prefix:plural_model_name:map:plural_attribute_name`. For
+			# example, if there's a map on the user name the key will look
+			# as `orpheus:users:map:names`. When `id` is called using an
+			# object of the form `{attribute_name: attribute_value}` we
+			# first search the map hash for the model id. If we find it,
+			# we return that model id. Otherwise, we create a new model.
+			#
+			# When using the mapped version of the function, a callback
+			# function must be passed as a second parameter. The callback
+			# receives 3 parameters: `error` if there was an error, `id`
+			# is the model id and `is_new` is a boolean indicating whether
+			# this is a new model and a map was not found.
+			#
+			# Usage:
+			#   Orpheus.schema.user(req.user.id)
+			#     name.set('colombus').exec()
+			#
+			#   Orpheus.scehma.user()
+			#     .name.set('whatever').exec()
+			#
+			#   Orpheus.schema.user name: 'colombus', (err, id, is_new) ->
+			#     req.session.user.id = id
+			#
+			# @param id - {Mixed} optional. either the model is as a number or a
+			# string, or an object map to use to look up the model id, in the form
+			# of `{attribute_name: attribute_value`}. If the id parameter is not
+			# provided a new model id will be generated.
+			# @param fn - {Function} optional. A callback function, used when
+			# calling `id` with a map object.
+			# @return an OrpheusAPI instance ready for querying the model.
+			#
 			id: (id, fn) =>
+				# If there is no ID or there is a regular string/number id
 				if not id or _.isString(id) or _.isNumber(id)
+					# If there's a callback function provided
 					if fn
+						# Create a new OrpheusAPI instance
 						new_model = new OrpheusAPI(id, this)
-						fn null, new_model, new_model.id, false
+						# Call the callback function
+						fn(null, new_model, new_model.id, false)
 					else
+						# Simply create a new OrpheusAPI instance and return it
 						new OrpheusAPI(id, this)
+				# If the `id` parameter is a map object
+				else if _.isObject(id) and fn
+					# Break the map into an attribute name and value
+					for name, value of id
+						# Pluralize the attribute name
+						plural_name = inflector.pluralize(name)
+						# Create the Redis map key
+						key = "#{@prefix}:#{@pname}:map:#{plural_name}"
+						# Try to see if there's a hash record for the model
+						# id using the map object value
+						@redis.hget key, value, (err, model_id) =>
+							return fn(err) if err
+							# If there is an existing model record in the map
+							if model_id
+								# Create a new OrpheusAPI instance with the id
+								new_model = new OrpheusAPI(model_id, this)
+								# Call the callback function
+								fn(null, new_model, model_id, false)
+							# The model record was not found, we'll create a new one
+							else
+								# Create a new OrpheusAPI instance with no id
+								new_model = new OrpheusAPI(null, this)
+								# Add a map for the new model with the specified value
+								new_model._add_map(plural_name, value)
+								# Call the callback function
+								fn(null, new_model, model.id, true)
 				else
-					for k,v of id
-						pk = inflector.pluralize k
-						@redis.hget "#{@prefix}:#{@pname}:map:#{pk}", v, (err, model_id) =>
-							return fn err, false if err
-							if model_id
-								# existing
-								new_model = new OrpheusAPI(model_id, this)
-								fn null, new_model, model_id, false
-							else
-								# new
-								model = new OrpheusAPI(null, this)
-								model._add_map pk, v
-								fn null, model, model.id, true
+					# Throw an error, the `id` parameter was not passed correctly
+					message = "Orpheus Model must be instantiated with a proper id"
+					throw new Error(message)

 		# Converts class Player to 'player'
@@ -559,7 +620,7 @@
 		unless @validation_errors.valid()
 			if @error_func
 			then return @error_func @validation_errors, @id
-			else return fn          @validation_errors, null, @id
+			else return fn @validation_errors, null, @id

 		@redis
 			.multi(@_commands)


tidied code controlling template selection on options page

--- src/lib/options.coffee
+++ src/lib/options.coffee
@@ -183,24 +183,19 @@
     clearErrors()
     templates.data 'quiet', 'true'
     if opt.length is 0
-      # Disable all the controls as no option is selected.
+      # Reset all controls and fields as no option is selected.
       lastSelectedTemplate = {}
       i18n.content '#add_btn', 'opt_add_button'
       $('#moveUp_btn, #moveDown_btn').attr 'disabled', 'disabled'
-      $('.read-only, .read-only-always').removeAttr 'disabled'
-      $('.read-only, .read-only-always').removeAttr 'readonly'
+      $('.read-only').removeAttr 'readonly'
       $('#delete_btn').attr 'disabled', 'disabled'
-      $('#template_content').val ''
+      $('#template_content, #template_shortcut, #template_title').val ''
       $('#template_enabled').attr 'checked', 'checked'
-      $('#template_image option').first().attr 'selected', 'selected'
+      $('#template_image option:first-child').attr 'selected', 'selected'
       $('#template_image').change()
-      $('#template_shortcut').val ''
-      $('#template_title').val ''
     else
       # An option is selected; start cooking.
       i18n.content '#add_btn', 'opt_add_new_button'
-      $('.read-only-always').attr 'disabled', 'disabled'
-      $('.read-only-always').attr 'readonly', 'readonly'
       # Disable the *Up* control since the selected option is at the top of the
       # list.
       if opt.is ':first-child'
@@ -216,7 +211,7 @@
       # Update the fields and controls to reflect selected option.
       imgOpt = $ "#template_image option[value='#{opt.data 'image'}']"
       if imgOpt.length is 0
-        $('#template_image option').first().attr 'selected', 'selected'
+        $('#template_image option:first-child').attr 'selected', 'selected'
       else
         imgOpt.attr 'selected', 'selected'
       $('#template_content').val opt.data 'content'
@@ -228,10 +223,8 @@
       else
         $('#template_enabled').removeAttr 'checked'
       if opt.data('readOnly') is 'true'
-        $('.read-only').attr 'disabled', 'disabled'
         $('.read-only').attr 'readonly', 'readonly'
       else
-        $('.read-only').removeAttr 'disabled'
         $('.read-only').removeAttr 'readonly'
     templates.data 'quiet', 'false'
   templates.change()


Comment tests

Comment tests

--- test/unit/filtersSpec.coffee
+++ test/unit/filtersSpec.coffee
@@ -37,11 +37,11 @@
             expect(momentFormatFilter(date, "DD-MM-YYYY")).to.be.equal("08-02-2013")

         it "should return the same", inject (momentFormatFilter) ->
-            date = "2013-02-08T09:30:26+00:00"
-            result = "2013-02-08T10:30:26+01:00"
-            expect(momentFormatFilter(date, "")).to.be.equal(result)
-            expect(momentFormatFilter(date, null)).to.be.equal(result)
-            expect(momentFormatFilter(date, undefined)).to.be.equal(result)
+            # TODO: Fix problems with UTC between local and TravisCI Server
+            #date = "2013-02-08T09:30:26+00:00"
+            #expect(momentFormatFilter(date, "")).to.be.equal(result)
+            #expect(momentFormatFilter(date, null)).to.be.equal(result)
+            #expect(momentFormatFilter(date, undefined)).to.be.equal(result)

         it "should return nothing", inject (momentFormatFilter) ->
             expect(momentFormatFilter(null)).to.be.equal("")


remove whitespace

remove whitespace

--- tests/InputSolverTest.coffee
+++ tests/InputSolverTest.coffee
@@ -44,7 +44,6 @@
       (expect parse '&^77+9').not.to.include('&','^', '&^')
       (expect parse '77()+9').not.to.include('(',')', '()')

-
   describe '#compute()', ->
     compute = (str) -> InputSolver.compute str
     it 'linearly evaluates numbers', ->


Implementing a File object

Implementing a File object

--- lib/zombie/browser.coffee
+++ lib/zombie/browser.coffee
@@ -8,15 +8,18 @@
 Cookies           = require("./cookies")
 { EventEmitter }  = require("events")
 EventLoop         = require("./eventloop")
+FS                = require("fs")
 History           = require("./history")
 Interact          = require("./interact")
 JSDom             = require("jsdom")
+PATH              = require("path")
 Resources         = require("./resources")
 Storages          = require("./storage")
 URL               = require("url")
 XHR               = require("./xhr")
 WebSocket         = require("./websocket")

+FILE_CLASS = ->

 HTML = JSDom.dom.level3.html
 MOUSE_EVENT_NAMES = ["mousedown", "mousemove", "mouseup"]
@@ -183,6 +186,7 @@
     @_ws.extend newWindow
     newWindow.screen = new Screen()
     newWindow.JSON = JSON
+    newWindow.File = FILE_CLASS
     newWindow.Image = (width, height)->
       img = new HTML.HTMLImageElement(newWindow.document)
       img.width = width
@@ -593,8 +597,14 @@
   attach: (selector, filename, callback)->
     field = @field(selector)
     if field && field.tagName == "INPUT" && field.type == "file"
-      field.value = filename
-      @fire "change", field, callback
+      FS.stat filename, (e, stat) =>
+        field.value = filename
+        file = new FILE_CLASS()
+        file.name = PATH.basename filename
+        file.size = stat.size
+        field.files ?= []
+        field.files.push file
+        @fire "change", field, callback
       unless callback
         return this
     else


requires underscore

requires underscore

--- lib/setup.coffee
+++ lib/setup.coffee
@@ -5,6 +5,8 @@
 #

 { API_URL, NODE_ENV, SESSION_SECRET, SESSION_COOKIE_MAX_AGE, SESSION_COOKIE_KEY, COOKIE_DOMAIN} = require "../config"
+
+_ = require 'underscore'
 express = require "express"
 Backbone = require "backbone"
 sharify = require "sharify"


fix(core): make cron check every 15 minutes

Run activityCheck every 15 minutes instead of every 30 seconds

Closes #2

--- src/model/scrum.coffee
+++ src/model/scrum.coffee
@@ -14,7 +14,7 @@
         that._time = time
         that._id = id
         that._scrumLog = {}
-        that._recentMessage = false
+        that._recentMessage = true

         that.cronJob = new CronJob(time, startScrum, null, true, null, this)

@@ -145,9 +145,10 @@
                 if reqSocket then reqSocket.end()
                 console.log that._scrumLog
                 that._robot.brain.set "scrumlog" + day.toString() + month.toString() + year.toString() + hour.toString() + minutes.toString(), that._scrumLog
+                that._recentMessage = true
             that._recentMessage = false

-        that.checkCronJob = new CronJob('*/30 * * * * *', activityCheck, null, true, null)
+        that.checkCronJob = new CronJob('0 */15 * * * *', activityCheck, null, true, null)

     cancelCronJob: ->
         this.cronJob.stop()


Only show welcome buffer on launch

Only show welcome buffer on launch

--- lib/welcome.coffee
+++ lib/welcome.coffee
@@ -2,9 +2,11 @@

 module.exports =
   configDefaults:
-    showWelcomeBuffer: true
+    showOnStartup: true

   activate: ->
-    return unless atom.config.get('welcom.showWelcomeBuffer')
-    atom.rootView.open path.join(__dirname, 'welcome.md')
-    atom.config.set('welcom.showWelcomeBuffer', false)
+    @show() if atom.config.get('welcome.showOnStartup')
+    atom.config.set('welcome.showOnStartup', false)
+
+  show: ->
+    atom.workspaceView.open path.join(__dirname, 'welcome.md')


minor change

minor change

--- lib/utils.coffee
+++ lib/utils.coffee
@@ -804,13 +804,14 @@
   tokens = []
   separators = []
   stack = []
-  token = ''
-  separator = ''
-  separatorChars = "\t, \n"
+  separatorChars = "\t, \r\n"
   quoteChars = "\"'`"
   openPairChars = "({[<"
   closePairChars = ">]})"
-
+  escapeChar = "\\"
+
+  token = ''
+  separator = ''
   inQuote = false
   isEscaped = false
   # Parse text as list of tokens which is commma separated or white space separated.
@@ -825,7 +826,7 @@
     else
       if isEscaped
         isEscaped = false
-      else if char is "\\"
+      else if char is escapeChar
         isEscaped = true
       else if char in openPairChars
         stack.push(char) unless inQuote


Changed reference to this instead of window object

--- lib/utils.coffee
+++ lib/utils.coffee
@@ -10,7 +10,7 @@

 _requestAnimationFrame = ((win, raf) ->
   win['webkitR' + raf] or win['r' + raf] or win['mozR' + raf] or win['msR' + raf] or (fn) -> setTimeout(fn, 30)
-)(window, 'equestAnimationFrame')
+)(@, 'equestAnimationFrame')

 formatTime = (time, settings) ->
   settings = settings or {}


remove debug dependency

remove debug dependency

--- lib/xikij.coffee
+++ lib/xikij.coffee
@@ -1,4 +1,3 @@
-debug           = require("debug")('xiki')
 path            = require "path"
 CoffeeScript    = require "coffee-script"
 {EventEmitter}  = require 'events'


Empty app files now start a default "blank" app, serving files at /public.

Previously `zappa app.coffee` would only create and initialize any web apps if
app.coffee contained any handler definitions (get, post, msg, etc). If it was
empty, the command above wouldn't do anything. Now a "blank" express app
always gets created by default. If no handlers are defined (or even if
app.coffee is completely empty) the default app will still be initialized,
and serve static files from the /public dir as any zappa app does by default.

--- lib/zappa.coffee
+++ lib/zappa.coffee
@@ -19,9 +19,10 @@
       include: (path) => @include path
       require: require

+    @ensure_app 'default' unless @current_app?
+
     for name in 'get|post|put|del|route|at|msg|client|using|def|helper|postrender|layout|view|style'.split '|'
       @locals[name] = =>
-        @ensure_app 'default' unless @current_app?
         @current_app[name].apply @current_app, arguments

   app: (name) ->
@@ -54,6 +55,7 @@
     options ?= {}

     @define_with code
+    puts inspect @apps

     i = 0
     for k, a of @apps


figured out jquery deferred, using that

figured out jquery deferred, using that

--- src/pseudositer.coffee
+++ src/pseudositer.coffee
@@ -28,10 +28,10 @@
   # Obtain the paths leading up to and including the path of the provided
   # url.  For example:
   #
-  # getTrail( "http://www.site.com/path/to/my/file.txt" ) ->
+  # getTrails( "http://www.site.com/path/to/my/file.txt" ) ->
   #  ["/", "/path/to/", "/path/to/my/", "/path/to/my/file.txt"]
   #
-  # getTrail( "http://www.site.com/path/to/my/#fragment" ) ->
+  # getTrails( "http://www.site.com/path/to/my/#fragment" ) ->
   #  ["/", "/path/to/", "/path/to/my/", "/path/to/my/"]
   #
   # if the last element of the returned array does not end with a '/', then
@@ -41,7 +41,7 @@
   #
   # @return {Array} An array of paths, starting with the most interior
   # path.
-  getTrail = ( url ) ->
+  getTrails = ( url ) ->
     ary = getPathArray url

     # the first path is always a '/'
@@ -59,14 +59,17 @@

   # Obtain the file type suffix (after a period)
   #
-  # @param fileName the String name of the file
+  # @param path the path to the file
   #
   # @return {String} the suffix, or null if there is none.
-  getSuffix = ( fileName ) ->
-    split = fileName.split '.'
-
-    if split.length > 1
-      split[ split.length - 1 ]
+  getSuffix = ( path ) ->
+    splitBySlash = path.split '/'
+    fileName = splitBySlash[ splitBySlash.length - 1 ]
+
+    splitByDot = fileName.split('.')
+
+    if splitByDot.length > 1
+      splitByDot[ splitByDot.length - 1 ]
     else
       null

@@ -92,8 +95,8 @@
     @init = (rootPath) =>
       @options = $.extend {}, $.pseudositer.defaultOptions, options

-      # the root
-      @rootPath = rootPath
+      # Assign the root, ensure that it ends in '/'
+      @rootPath = if rootPath.charAt( rootPath.length - 1 ) is '/' then rootPath else "#{rootPath}/"

       # Create loading div if one doesn't already exist,
       # and keep reference to the div.
@@ -144,8 +147,36 @@
     # Private methods.
     ###

-    absPath = (path) =>
-
+    # Get the class name for links of a specified level.
+    #
+    # @param level the {int} level, starting with 0, of the links.
+    #
+    # @return {String} the name of the class
+    getLinkClassForLevel = ( level ) =>
+
+      @options.linkClass + '-' + level
+
+    # Get the class name for links with a certain path.
+    #
+    # @param path the {String} path to the links.
+    #
+    # @return {String} the name of the class
+    # getLinkClassForPath = ( path ) =>
+
+    #   # generate a safe version of the path to use as the class name
+    #   name = path.replace /[^a-zA-Z0-9]/g, '-'
+    #   @options.linkClass + '-' + name
+
+    # Calculate the actual path for an ajax request from a path from
+    # the address bar.  Resolves the path against @rootPath.
+    #
+    # @param path a path from the address bar
+    #
+    # @return {String} a path that can be used for an ajax request.
+    resolve = (path) =>
+
+      # @rootPath always ends in '/'
+      if path.charAt 0 is '/' then @rootPath + path.substr 1 else @rootPath + path

     # Show the loading notice
     #
@@ -178,120 +209,181 @@
     # Will check the cache to see if this state has been loaded before.
     #
     # @param state the State object to use when updating the view.
-    # Will get the current state using History.getState() if not defined.
-    #
-    # @return this
-    updateView = (state) =>
-
-      showLoading()
-
-      History.replaceState(state)
+    #
+    # @return this
+    updateView = ( state ) =>
+
+      History.replaceState( state )

       # if we've visited the state before and grabbed data, reuse that object
       if state.url in cachedStates
         state = cachedStates[ state.url ]

-      # store new state in cache if it has pseudositer data.
+      # store state in cache if it has pseudositer data.
       else
         if state.data.pseudositer?
           cachedStates[ state.url ] = state

       # if we already have content data for state, use it
       if state.data.pseudositer?
-        showLinks getPathArray state.url
+        showLinks state.url

         showContent state.data.pseudositer.$content

-        hideLoading()
-
       else # load the data for the state
+        showLoading()
+
         # call back updateView after the state is loaded
-        createStateObjectWithData state.title, state.url, updateView
-
-      this
-
-    # Asynchronously create a new state object with freshly loaded data.
-    #
-    # This load new view data from the provided url.  If the state is an index,
-    # make ajax requests to unfold it to the deepest directory and the
-    # first object in that directory.
-    #
-    # @param deferred
-    # @param title the title of the new state object
-    # @param url the url of the new state object
-    #
-    # @return this
-    createStateObjectWithData = (deferred, title, url) =>
-
-      data = {}
-
-      componentPaths = componentPaths sansData.url
-
-
-
-      withData = History.createStateObject data sansData.title sansData.url
-
-      this
-
-    # Load a new set of links into @pathLinks.
-    #
-    # @param deferred the deferred to append this asynchronous operation to.
-    # @param path the path to the links to load.
-    #
-    # @return this
-    loadLinks = (deferred, path) =>
-
-      deferred.then =>
-        ajaxGet path, (data, textStatus, jqXHR) =>
-          @pathLinks[ path ] = $(data).find(options.linkSelector)
-
-      this
-
-    # Load generic content from a path into an object.
-    #
-    # @param deferred the deferred to append this asynchronous operation to.
-    # @param path the path to the links to the content to load.
-    # @param object
-    #
-    # @return this
-    loadContent = (deferred, path, object) =>
-
-      deferred.then =>
-        switch getSuffix path
-          when 'jpg' then
-            loadImage deferred, path, object
-          when 'gif' then
-            loadImage deferred, path, object
-          when 'png' then
-            loadImage deferred, path, object
-          when 'txt' then
-            loadText deferred, path, object
-          when 'html' then
-            loadText deferred, path, object
-          else
-            loadText deferred, path, object
-
-      this
-
-    loadImage = (path, object) =>
-      object.content = $( 'img' ).attr 'src', absPath(path)
-
-    loadText = (deferred, path, object) =>
-
-    # Show the links for a path, in addition to the links for all its
+        $.when( load( state ) )
+          .done   => updateView state
+          .fail   => console.log state # TODO actual logging
+          .always => hideLoading()
+
+      this
+
+    # Obtain a {Promise} object that, once done, has loaded and cached
+    # (into @pathLinks) all the paths leading to the supplied state's content,
+    # and also loads the content into that state.
+    #
+    # @param state the {State} to load
+    #
+    # @return {Promise} object that, when finished, means that
+    # data has been loaded into cache and state.  Callbacks receive
+    # the new {State} as an argument.
+    load = ( state ) =>
+
+      Deferred dfd = new $.Deferred()
+      url = state.url
+
+      trails = getTrails url
+
+      # pipe an additional request for any trails that are not in the cache
+      for level in [ 0...trails.length ]
+
+        trail = trails[ i ]
+        unless trail in @pathLinks
+          dfd = dfd.pipe loadIndex trail
+
+      path = trails[ trails.length - 1 ]
+
+      # unfold the path to content.  once the path is unfolded,
+      # load the content.  once the content is loaded, resolve
+      # the deferred with a new state based off of the content
+      # and unfolded path.
+      $.when( unfold( path ) ).done (unfoldedPath) =>
+        loadContent( unfoldedPath ).done ($content) =>
+          state = History.createStateObject
+            pseudositer :
+              $content  : $content,
+            null,
+            unfoldedPath
+          dfd.resolve state
+
+      dfd.promise()
+
+
+    # Asynchronously determine the nearest content to this path,
+    # loading links into cache all along the way.
+    #
+    # @param path the path to unfold
+    # @param dfd An optional {Deferred} to resolve once the unfolding
+    # is done
+    #
+    # @return {Promise} that will be resolved with an argument
+    # that is the unfolded path
+    unfold = ( path, dfd = $.Deferred ) =>
+
+      # if path already points to content, resolve now
+      if getSuffix path isnt null
+        dfd.resolve( path )
+      else
+
+        # when the index is loaded, call unfold again but reusing
+        # the deferred object
+        $.when( loadIndex ).done ( $links ) =>
+          unfold( $links.first().attr 'href', dfd )
+
+      dfd.promise()
+
+    # If there is no entry for the index,
+    # make an ajax request for a path to the index page, and cache
+    # all the links on that index page into @pathLinks.
+    #
+    # @param path the path to an index page
+    #
+    # @return {Promise} which will resolve with an
+    # argument that is an array of all the links in that index
+    loadIndex = ( path ) =>
+      dfd = new $.Deferred()
+      if @pathLinks[ path ]?
+        dfd.resolve @pathLinks[ path ]
+      else
+        $.when $.get resolve path, data =>
+          @pathLinks[ path ] = $( data ).find options.linkSelector
+          dfd.resolve( @pathLinks [ path ] )
+      dfd
+
+    # Load the content located at a path
+    #
+    # @param pathToContent the path to the content
+    #
+    # @return {Promise} that, when done, contains the loaded content.
+    loadContent = ( pathToContent ) =>
+      dfd = new $.Deferred()
+
+      suffix = getSuffix pathToContent
+      switch suffix
+        when 'png' then
+
+      dfd.promise()
+
+    loadImage = ( pathToImage ) =>
+      object.content = $( 'img' ).attr 'src', resolvedPath
+
+    loadText = ( pathToText ) =>
+
+    # Show the links for an unfolded url, in addition to the links
+    # for all its
     # parents.  This data should already have been loaded into
     # @pathLinks .
     #
-    # @param path the path
-    #
-    # @return this
-    showLinks = (path) =>
-
-      trail = getTrail(path)
-
-      for pathNum in [0...trail.length]
-        path = trail[pathNum]
-        $links = @pathLinks[path]
+    # @param url the URL to show links for
+    #
+    # @return this
+    showLinks = ( url ) =>
+
+      trails = getTrails url
+
+      # mark all link elements as stale before we iterate through the
+      # path
+      $( '.' + @options.linkClass ).addClass @options.staleClass
+
+      for level in [ 0...trails.length ]
+
+        trail = trails[ level ]
+
+        # If a class for the level is not on the page, create it
+        levelClass = getLinkClassForLevel level
+        $linkElem = $( '.' + levelClass )
+        if $linkElem.length is 0
+          $linkElem = $( 'div' )
+            .addClass @options.linkClass + ' ' + levelClass
+            .data 'pseudositer', { }
+          @$el.append $linkElem
+
+        # If the previous path is the same, leave the element alone
+        # otherwise, change the path and append the links
+        prevPath = $linkElem.data('pseudositer').path
+        unless trail is prevPath
+          $linkElem.empty ()
+          $linkElem.data( 'pseudositer', path : trail )
+          $linkElem.append @pathLinks[ trail ]
+
+        # ensure that this link element is not removed.
+        $linkElem.removeClass @options.staleClass
+
+      # remove any link elements that are deeper than the new path
+      $( '.' + @options.linkClass + '.' + @options.staleClass ).remove()

       this

@@ -300,22 +392,22 @@
     # @param $content the DOM content to display.
     #
     # @return this
-    showContent = ($content) =>
-      $(@options.contentClass)
-        .empty ()
+    showContent = ( $content ) =>
+      $( @$content ).empty ()
         .append( $content );
       this
-

     # call init, and return the output
     @init()

   # object literal containing default options
   $.pseudositer.defaultOptions =
-    loadingClass: 'pseudositer-load'
-    contentClass: 'pseudositer-content'
-    directoryClass: 'pseudositer-directory'
+    loadingClass   : 'pseudositer-load'
+    contentClass   : 'pseudositer-content'
+    staleClass     : 'pseudositer-stale'
+    linkClass      : 'pseudositer-links'
   	linkSelector   : 'a:not([href^="?"],[href^="/"])', # Find links from a directory listing that go deeper
+

   $.fn.pseudositer = (options) ->
     $.each @, (i, el) ->


[refactor] inside queues is a better stateCounts

--- src/redis-model.coffee
+++ src/redis-model.coffee
@@ -88,22 +88,6 @@
     else
       #Old list state
       @fullJobNamesFromIds list, callback
-
-  #Returns counts for different statees
-  stateCounts: (callback) ->
-    ideally = errify callback
-    await @jobsByState "active",   ideally defer active
-    await @jobsByState "complete", ideally defer completed
-    await @jobsByState "failed",   ideally defer failed
-    await @jobsByState "wait",     ideally defer pendingJobs
-    await @allJobs                 ideally defer allJobs
-
-    active:   active.count
-    complete: completed.count
-    failed:   failed.count
-    pending:  pendingJobs.count
-    total:    allJobs.length
-    stuck:    allJobs.length - (active.count + completed.count + failed.count + pendingJobs.count)

   #Returns all jobs in object form, with state applied to object. Ex: {id: 101, queue: "video transcoding", state: "pending"}
   formatJobs: (jobs, callback) ->
@@ -288,32 +272,40 @@
     callback null, jobs

   queues: (callback) ->
-    ideally = errify callback
+    ideally   = errify callback
+    allCounts = {}

     await @redis.keys "bull:*:id", ideally defer queues
     for queue in queues
       name = queue[..-3]
-
-      await @redis.lrange name + ":active", 0, -1, ideally defer allActive
-      active = []
-      stuck  = []
-      for job in allActive
-        await @redis.get "#{name}:#{job}:lock", ideally defer lock
-        if lock? then active.push  job
-        else          stuck.push job
-
-      await @redis.llen  "#{name}:wait",      ideally defer pending
-      await @redis.zcard "#{name}:delayed",   ideally defer delayed
-      await @redis.scard "#{name}:completed", ideally defer completed
-      await @redis.scard "#{name}:failed",    ideally defer failed
-
-      callback null,
-        name:      name[5..]
-        active:    active.length
-        stuck:     stuck.length
-        pending:   pending
-        delayed:   delayed
-        completed: completed
-        failed:    failed
+      await stateCounts name, ideally defer allCounts[queue]
+
+    callback null, allCounts
+
+  stateCounts: (queue, callback) ->
+    ideally = errify callback
+
+    await @redis.lrange "#{queue}:active", 0, -1, ideally defer allActive
+    active = []
+    stuck  = []
+    for job in allActive
+      await @redis.get "#{queue}:#{job}:lock", ideally defer lock
+      if lock? then active.push job
+      else          stuck.push  job
+
+    await @redis.llen  "#{queue}:wait",      ideally defer pending
+    await @redis.zcard "#{queue}:delayed",   ideally defer delayed
+    await @redis.scard "#{queue}:completed", ideally defer completed
+    await @redis.scard "#{queue}:failed",    ideally defer failed
+
+    callback null,
+      name:      name[5..]
+      active:    active.length
+      stuck:     stuck.length
+      pending:   pending
+      delayed:   delayed
+      completed: completed
+      failed:    failed
+

 module.exports = RedisModel


Fix moveLineUp and moveLineDown

Fix moveLineUp and moveLineDown

--- src/text-editor.coffee
+++ src/text-editor.coffee
@@ -945,14 +945,14 @@
         # insert delta is know.
         selectionFoldRanges = []
         foldAtSelectionStart =
-          @displayBuffer.largestFoldContainingBufferRow(selection.start.row)
+          @displayLayer.largestFoldContainingBufferRow(selection.start.row)
         foldAtSelectionEnd =
-          @displayBuffer.largestFoldContainingBufferRow(selection.end.row)
+          @displayLayer.largestFoldContainingBufferRow(selection.end.row)
         if fold = foldAtSelectionStart ? foldAtSelectionEnd
-          selectionFoldRanges.push range = fold.getBufferRange()
+          selectionFoldRanges.push range = @displayLayer.bufferRangeForFold(fold)
           newEndRow = range.end.row + 1
           linesRange.end.row = newEndRow if newEndRow > linesRange.end.row
-          fold.destroy()
+          @displayLayer.destroyFold(fold)

         # If selected line range is preceded by a fold, one line above on screen
         # could be multiple lines in the buffer.
@@ -963,16 +963,18 @@
         # Any folds in the text that is moved will need to be re-created.
         # It includes the folds that were intersecting with the selection.
         rangesToRefold = selectionFoldRanges.concat(
-          @outermostFoldsInBufferRowRange(linesRange.start.row, linesRange.end.row).map (fold) ->
-            range = fold.getBufferRange()
-            fold.destroy()
+          @displayLayer.outermostFoldsInBufferRowRange(linesRange.start.row, linesRange.end.row).map (fold) ->
+            range = @displayLayer.bufferRangeForFold(fold)
+            @displayLayer.destroyFold(fold)
             range
         ).map (range) -> range.translate([-insertDelta, 0])

-        # # Make sure the inserted text doesn't go into an existing fold
-        # if fold = @displayBuffer.largestFoldStartingAtBufferRow(precedingBufferRow)
-        #   rangesToRefold.push(fold.getBufferRange().translate([linesRange.getRowCount() - 1, 0]))
-        #   fold.destroy()
+        # Make sure the inserted text doesn't go into an existing fold
+        if fold = @displayLayer.largestFoldStartingAtBufferRow(precedingBufferRow)
+          rangesToRefold.push(
+            @displayLayer.bufferRangeForFold(fold).translate([linesRange.getRowCount() - 1, 0])
+          )
+          @displayLayer.destroyFold(fold)

         # Delete lines spanned by selection and insert them on the preceding buffer row
         lines = @buffer.getTextInRange(linesRange)
@@ -982,7 +984,7 @@

         # Restore folds that existed before the lines were moved
         for rangeToRefold in rangesToRefold
-          @displayBuffer.foldBufferRowRange(rangeToRefold.start.row, rangeToRefold.end.row)
+          @displayLayer.foldBufferRange(rangeToRefold)

         for selection in selectionsToMove
           newSelectionRanges.push(selection.translate([-insertDelta, 0]))
@@ -1027,14 +1029,14 @@
         # insert delta is know.
         selectionFoldRanges = []
         foldAtSelectionStart =
-          @displayBuffer.largestFoldContainingBufferRow(selection.start.row)
+          @displayLayer.largestFoldContainingBufferRow(selection.start.row)
         foldAtSelectionEnd =
-          @displayBuffer.largestFoldContainingBufferRow(selection.end.row)
+          @displayLayer.largestFoldContainingBufferRow(selection.end.row)
         if fold = foldAtSelectionStart ? foldAtSelectionEnd
-          selectionFoldRanges.push range = fold.getBufferRange()
+          selectionFoldRanges.push range = @displayLayer.bufferRangeForFold(fold)
           newEndRow = range.end.row + 1
           linesRange.end.row = newEndRow if newEndRow > linesRange.end.row
-          fold.destroy()
+          @displayLayer.destroyFold(fold)

         # If selected line range is followed by a fold, one line below on screen
         # could be multiple lines in the buffer. But at the same time, if the
@@ -1047,16 +1049,16 @@
         # Any folds in the text that is moved will need to be re-created.
         # It includes the folds that were intersecting with the selection.
         rangesToRefold = selectionFoldRanges.concat(
-          @outermostFoldsInBufferRowRange(linesRange.start.row, linesRange.end.row).map (fold) ->
+          @displayLayer.outermostFoldsInBufferRowRange(linesRange.start.row, linesRange.end.row).map (fold) ->
             range = fold.getBufferRange()
-            fold.destroy()
+            @displayLayer.destroyFold(fold)
             range
         ).map (range) -> range.translate([insertDelta, 0])

         # # Make sure the inserted text doesn't go into an existing fold
-        # if fold = @displayBuffer.largestFoldStartingAtBufferRow(followingBufferRow)
-        #   rangesToRefold.push(fold.getBufferRange().translate([insertDelta - 1, 0]))
-        #   fold.destroy()
+        if fold = @displayLayer.largestFoldStartingAtBufferRow(followingBufferRow)
+          rangesToRefold.push(@displayLayer.bufferRangeForFold(fold).translate([insertDelta - 1, 0]))
+          @displayLayer.destroyFold(fold)

         # Delete lines spanned by selection and insert them on the following correct buffer row
         insertPosition = new Point(selection.translate([insertDelta, 0]).start.row, 0)
@@ -1069,7 +1071,7 @@

         # Restore folds that existed before the lines were moved
         for rangeToRefold in rangesToRefold
-          @displayBuffer.foldBufferRowRange(rangeToRefold.start.row, rangeToRefold.end.row)
+          @displayLayer.foldBufferRange(rangeToRefold)

         for selection in selectionsToMove
           newSelectionRanges.push(selection.translate([insertDelta, 0]))


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/text_effect.coffee
+++ src/text_effect.coffee
@@ -10,7 +10,8 @@
 ###

 ###*
-Updates the position of the text based on the effect velocity. Updates the alpha based on
+Updates the position of the text based on the effect velocity. Updates the
+alpha based on
 Observe or configure a gameObject that has been added
 to the engine.
 @name update


levels and default game states

levels and default game states

--- src/text_screen.coffee
+++ src/text_screen.coffee
@@ -6,7 +6,7 @@

   self = GameState(I).extend
     centerText: (canvas, options={}) ->
-      canvas.font optionI.font
+      canvas.font options.fontI.font

       canvas.centerText
         y: App.height / 2 - 20


levels and default game states

levels and default game states

--- src/text_screen.coffee
+++ src/text_screen.coffee
@@ -1,7 +1,7 @@
 GameOver = (I={}) ->
   Object.reverseMerge I,
     font: '24px Helvetica'
-    fontColor:
+    fontColor: 'white'

   self = GameState(I).extend
     drawTitleText: (canvas, titleText, subtitleText) ->


Use absolute path to node that was used to start the app

--- lib/LongRunningChildProcess.coffee
+++ lib/LongRunningChildProcess.coffee
@@ -144,7 +144,8 @@

     log.debug("LongRunningChildProcess.spawn is spawning '#{command}'")

-    @child = spawn('node', commandArgs, spawnOptions)
+    nodePath = process.execPath
+    @child = spawn(nodePath, commandArgs, spawnOptions)
     @dead = false
     @_setPid(@child.pid)


more tests

more tests

--- test/basic-test.coffee
+++ test/basic-test.coffee
@@ -83,4 +83,20 @@

       client.locationMatch matchData, (err,data) ->
         console.log "RESULT: #{JSON.stringify(data)}"
-        cb err+        cb err
+
+    it 'should geomatch', (cb) ->
+      client = index.client(settings)
+      matchData =
+        locations:[
+            {"latitude":38.789839,
+            "longitude":-77.064791,
+            "name":"Juliano's Pizzeria"}
+          ], "matching_criteria":"NAME_GEOLOCATION","geo_radius":"0.01"
+
+
+      client.locationMatch matchData, (err,data) ->
+        console.log "RESULT: #{JSON.stringify(data)}"
+        cb err
+
+


fix test

fix test

--- test/cache-spec.coffee
+++ test/cache-spec.coffee
@@ -94,7 +94,8 @@
       beforeEach ->
         @client.on 'message', (error, @message) =>

-      it 'should deliver a message', ->
+      it 'should deliver a message', (done) ->
         _.delay =>
           expect(@message).to.equal 'gallows-humour'
+          done()
         , 100


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/collidable.coffee
+++ test/collidable.coffee
@@ -0,0 +1,24 @@
+module "Collidable"
+
+test "#separate", ->
+  leftObject = GameObject
+    includedModules: ["Collidable"]
+    x: 50
+    y: 50
+    width: 20
+    height: 20
+
+  rightObject = GameObject
+    includedModules: ["Collidable"]
+    x: 60
+    y: 50
+    width: 20
+    height: 20
+
+  Collidable.separate(leftObject, rightObject)
+
+  equal leftObject.I.x, 40
+  equal rightObject.I.x, 70
+
+
+module()


Temporary fix for emojis not showing

Temporary fix for emojis not showing

--- packages/rocketchat-emojione/emojione.coffee
+++ packages/rocketchat-emojione/emojione.coffee
@@ -12,10 +12,10 @@

 RocketChat.callbacks.add 'renderMessage', Emojione, RocketChat.callbacks.priority.LOW

-if Meteor.isClient
-	Meteor.startup ->
-		Tracker.autorun ->
-			if Meteor.user()?.settings?.preferences?.useEmojis or not Meteor.user()?.settings?.preferences?.useEmojis?
-				RocketChat.callbacks.add 'renderMessage', Emojione, RocketChat.callbacks.priority.LOW
-			else
-				RocketChat.callbacks.remove 'renderMessage', 'Emojione'+# if Meteor.isClient
+# 	Meteor.startup ->
+# 		Tracker.autorun ->
+# 			if Meteor.user()?.settings?.preferences?.useEmojis or not Meteor.user()?.settings?.preferences?.useEmojis?
+# 				RocketChat.callbacks.add 'renderMessage', Emojione, RocketChat.callbacks.priority.LOW
+# 			else
+# 				RocketChat.callbacks.remove 'renderMessage', 'Emojione'


Revert "Tweaked markdown regex for [text](URI) to ignore close parentheses" (#3976)


Split breakpoint assertions into two separate tests

As noted in #11, using this style of chai-as-promised (without
explicitly synchronizing) can only perform one assertion per test, so
the first assertion was never being run.

--- test/gozer_test.coffee
+++ test/gozer_test.coffee
@@ -34,11 +34,12 @@
       expect(page.getStyle('body', 'font-family'))
         .to.eventually.have.string('Helvetica Neue')

-    context 'with breakpoints', ->
+    describe 'breakpoints', ->
       it 'retrieves the computed style', ->
         expect(page.getStyle('body', 'font-size'))
           .to.eventually.equal('16px')

+      it 'retrieves the computed style at a different breakpoint', ->
         page.resize(width: 600)

         expect(page.getStyle('body', 'font-size'))


Oscillator

Oscillator


fixup

fixup

--- test/phabs_test.coffee
+++ test/phabs_test.coffee
@@ -98,7 +98,7 @@
             ownerPHID: 'PHID-USER-42'
             } })
           .get('/api/user.query')
-          .query(query = {
+          .query({
             'phids[0]': 'PHID-USER-42',
             'api.token': 'xxx'
           })
@@ -179,7 +179,7 @@
             ownerPHID: 'PHID-USER-000000'
             } })
           .get('/api/user.query')
-          .query(query = {
+          .query({
             'phids[0]': 'PHID-USER-000000',
             'api.token': 'xxx'
           })


small fixes

small fixes

--- coffeescripts/cards-de.coffee
+++ coffeescripts/cards-de.coffee
@@ -630,7 +630,7 @@
             text: """Zu Beginn der Kampfphase entferne je 1 Stressmarker von jedem freundlichen Schiff in Reichweite 1."""
         '"Zeta Ace"':
             ship: "TIE/EO-Jäger"
-            name: "Ass Zeta"
+            name: '"Ass Zeta"'
             text: """Sobald du eine Fassrolle ausführst, darfst du die (%STRAIGHT% 2) Manöverschablone verwenden anstatt der (%STRAIGHT% 1) Manöverschablone."""
         "Omega Squadron Pilot":
             ship: "TIE/EO-Jäger"
@@ -799,7 +799,7 @@
             text: '''Zu Beginn der Kampfphase darfst du 1 feindliches Schiff in Reichweite 1 wählen. Falls du innerhalb seines Feuerwinkels bist, legt es alle Fokus- und Ausweichmarker ab.'''
         'Kad Solus':
             ship: "Sternenjäger des Protektors"
-            text: '''Nachdem du ein rotes Manöver ausgeführt hast, ordne deinem SChiff 2 Fokusmarker zu.'''
+            text: '''Nachdem du ein rotes Manöver ausgeführt hast, ordne deinem Schiff 2 Fokusmarker zu.'''
         'Concord Dawn Ace':
             ship: "Sternenjäger des Protektors"
             name: "Fliegerass von Concord Dawn"
@@ -1382,7 +1382,7 @@
             text: '''Ein Mal pro Runde darfst du, sobald du mit einer Primärwaffe angreifst im Schritt "Angriffswürfel modifizieren" 1 deiner %FOCUS% negieren, um deinem Schiff 1 Ausweichmarker zuzuordnen.'''
         'Collision Detector':
             name: "Kollisionssensor"
-            text: '''Sobald du SChub, Fassrolle oder Enttarnen durchführst, können sich dein SChiff und die Manöverschablone mit Hindernissen überschneiden.%LINEBREAK%Sobald du wegen der Überschneidung mit einem Hindernis Schaden auswürfelst, ignorierst du alle %CRIT%.'''
+            text: '''Sobald du Schub, Fassrolle oder Enttarnen durchführst, können sich dein Schiff und die Manöverschablone mit Hindernissen überschneiden.%LINEBREAK%Sobald du wegen der Überschneidung mit einem Hindernis Schaden auswürfelst, ignorierst du alle %CRIT%.'''
         'Sensor Cluster':
             name: "Sensorcluster"
             text: '''Sobald du verteidigst, darfst du einen Fokusmarker ausgeben, um 1 deiner Leerseiten in ein %EVADE% zu ändern.'''
@@ -1390,14 +1390,15 @@
             name: "Furchtlosigkeit"
             text: '''%DE_SCUMONLY%%LINEBREAK%Sobald du angreifst, darfst du 1 %HIT% zu deinem Wurf hinzufügen, falls du innerhalb des Feuerwinkels des Verteidigers in Reichweite 1 zu ihm bist und der Verteidiger innerhalb deines Feuerwinkels ist.'''
         'Ketsu Onyo':
-            text: '''%DE_SCUMONLY%%LINEBREAK%Zu Beginn der Endphase darfst du 1 feindliches Schiff innerhalb deines Feuerwinkels in Reichweite 1-2 wählen. Das Schiff entfernt seine Traktorstrahlmarker nicht..'''
+            name: "Ketsu Onyo (Crew)"
+            text: '''%DE_SCUMONLY%%LINEBREAK%Zu Beginn der Endphase darfst du 1 feindliches Schiff innerhalb deines Feuerwinkels in Reichweite 1-2 wählen. Das Schiff entfernt seine Traktorstrahlmarker nicht.'''
         'Latts Razzi':
             text: '''%DE_SCUMONLY%%LINEBREAK%Sobald du verteidigst, darfst du 1 Stressmarker vom Angreifer entfernen, um 1 %EVADE% zu deinem Wurf hinzuzufügen.'''
         'IG-88D':
             text: '''%DE_SCUMONLY%%LINEBREAK%Du hast die Pilotenfähigkeit jedes freundlichen Schiffs mit der Aufwertungskarte IG-2000 (zusätzlich zu deiner eigenen Pilotenfähigkeit.'''
         'Rigged Cargo Chute':
             name: "Manipulierte Frachtrampe"
-            text: '''%DE_LARGESHIPONLY%%LINEBREAK%Aktion: Lege diese KArte ab, um 1 Frachtmarker zu legen.'''
+            text: '''%DE_LARGESHIPONLY%%LINEBREAK%Aktion: Lege diese Karte ab, um 1 Frachtmarker zu legen.'''
         'Seismic Torpedo':
             name: "Seismische Torpedos"
             text: '''Aktion: Lege diese Karte ab, um ein Hindernis in Reichweite 1-2 und innerhalb deines normalen Feuerwinkels zu wählen. Jedes Schiff in Reichweite 1 zu dem Hindernis wirfst 1 Angriffswürfel und erleidet den gewürfelten Schaden (%HIT%) oder kritischen Schaden (%CRIT%). Dann wird das Hindernis entfernt.'''
@@ -1643,7 +1644,7 @@
             text: '''After you execute a 3-speed bank maneuver (%BANKLEFT% or %BANKRIGHT%), if you are not touching another ship and you are not stressed, you may receive 1 stress token to rotate your ship 180°.'''
         'Alliance Overhaul':
             name: "Generalüberholt von der Allianz"
-            text: '''Nur für ARC-170.%LINEBREAK%Sobald du von deinem normalen Feuerwinkel aus mit der Primärwaffe angreifst, darfst du 1 zusätzlichen Angriffswürfel werfen. Sobald du von deinem Zusatz-Feuerwinkel aus angreifst, darfst du 1 deiner %FOCUS% in ein %CRIT% änern.'''
+            text: '''Nur für ARC-170.%LINEBREAK%Sobald du von deinem normalen Feuerwinkel aus mit der Primärwaffe angreifst, darfst du 1 zusätzlichen Angriffswürfel werfen. Sobald du von deinem Zusatz-Feuerwinkel aus angreifst, darfst du 1 deiner %FOCUS% in ein %CRIT% ändern.'''
         'Special Ops Training':
             name: "Training der Spezialeinheiten"
             ship: "TIE/SE-Jäger"
@@ -1651,10 +1652,10 @@
         'Concord Dawn Protector':
             name: "Protektor von Concord Dawn"
             ship: "Sternenjäger des Protektors"
-            text: '''Nur für Sternenjäger des Protektors.%LINEBREAK%Sobald du verteidigst, darfst du 1 %EVADE% hinzufügen, falls du innerhalb des Feuerwinkels des Angreifers und in Reichweite 1 zu ihm bist un der Angreifer innerhalb deines Feuerwinkels ist.'''
+            text: '''Nur für Sternenjäger des Protektors.%LINEBREAK%Sobald du verteidigst, darfst du 1 %EVADE% hinzufügen, falls du innerhalb des Feuerwinkels des Angreifers und in Reichweite 1 zu ihm bist und der Angreifer innerhalb deines Feuerwinkels ist.'''
         'Shadow Caster':
             ship: "Jagdschiff der Lanzen-Klasse"
-            text: '''Nur für Jagdschiffe der Lanzen-Klasse.%LINEBREAK%NAchdem du einen Angriff durchgeführt hast, der getroffen hat, darfst du dem Verteidiger 1 Traktorstrahlmarker zuordnen, falls er innerhalb deines mobilen Feuerwinkels in Reichweite 1-2 ist.'''
+            text: '''Nur für Jagdschiffe der Lanzen-Klasse.%LINEBREAK%Nachdem du einen Angriff durchgeführt hast, der getroffen hat, darfst du dem Verteidiger 1 Traktorstrahlmarker zuordnen, falls er innerhalb deines mobilen Feuerwinkels in Reichweite 1-2 ist.'''
         # Wave X
         '''Sabine's Masterpiece''':
             text: '''TIE Fighter only.%REBELONLY%%LINEBREAK%Your upgrade bar gains the %CREW% and %ILLICIT% upgrade icons.'''