Ugly trick to figure out editor char width for header adjustment

--- coffee/main.coffee
+++ coffee/main.coffee
@@ -133,23 +133,11 @@
 nextSlide = -> state.slide = (state.slide || 0)+1
 prevSlide = -> state.slide = Math.max (state.slide || 0)-1, 0

+charWidth = null
 updateHeaderAdjust = (lineEl)->
-  #line = editor.getCursor().line
-  #lineEl = document.querySelector('.CodeMirror-code').children[line]
-  if headHash = lineEl.querySelector('.headHash')
-    lineEl.removeChild lineEl.querySelector('.headHash')
+  return if not charWidth?
   m = lineEl.textContent.match /^#+\s*/
-  if m?
-    lineEl.firstChild.textContent = lineEl.textContent.substr(m[0].length)
-    spanEl = document.createElement 'span'
-    spanEl.className = 'headHash'
-    spanEl.textContent = m[0]
-    margin = (m[0].length * -0.58) + 'em'
-    spanEl.style.marginLeft = margin
-    spanEl.style.position = 'absolute'
-    lineEl.insertBefore spanEl, lineEl.childNodes[0]
-  #lineEl.style.marginLeft = margin
-  #document.querySelector('.CodeMirror-cursor').style.marginLeft = margin
+  lineEl.style.marginLeft = m[0].length * -charWidth + 'px' if m?

 saveTimer = null
 editor = CodeMirror.fromTextArea document.getElementById('input-md'),
@@ -159,10 +147,7 @@
   lineWrapping: yes
   dragDrop: no
   autofocus: yes
-editor.on 'cursorActivity', ->
-  #updateHeaderAdjust()
-editor.on 'renderLine', (e, line, lineEl) ->
-  updateHeaderAdjust lineEl
+editor.on 'renderLine', (e, line, lineEl) -> updateHeaderAdjust lineEl
 editor.on 'change', ->
   updateView()
   if initiated
@@ -173,6 +158,18 @@
     saveTimer = setTimeout save, 5000
   else
     updateTitle()
+
+findEditorCharWidth = ->
+  textNode = document.createTextNode new Array(61).join('#')
+  preEl = document.createElement 'pre'
+  preEl.appendChild textNode
+  document.querySelector('.CodeMirror-code').appendChild preEl
+  range = document.createRange()
+  range.selectNodeContents textNode
+  bb = range.getBoundingClientRect()
+  preEl.parentNode.removeChild preEl
+  bb.width/60
+charWidth = findEditorCharWidth()

 restore = (data) ->
   currentText = editor.getValue()


no more ";"

no more ";"

--- coffee/main.coffee
+++ coffee/main.coffee
@@ -43,18 +43,18 @@
       data: {'code': input},
       success: (result) ->
         PyCodeMirror.setValue(result)
-        now = new Date().getTime();
+        now = new Date().getTime()
         $("#build-msgs").text(now + " Updated.")
         $("#repl-root").removeClass("error")
         $("#repl-root").addClass("ok")
       statusCode: {
         500: (response) ->
-          now = new Date().getTime();
+          now = new Date().getTime()
           $("#build-msgs").text(now + " " + response.responseText)
           $("#repl-root").removeClass("ok")
           $("#repl-root").addClass("error")
       }
-  });
+  })

 $(document).ready(->


Modified bspline function to take order parameter

--- coffee/math.coffee
+++ coffee/math.coffee
@@ -1,7 +1,7 @@
 window.LC = window.LC ? {}

-LC.bspline = (points) ->
-  return dual(dual(refine(points)))
+LC.bspline = (points, order) ->
+  return LC.bspline(dual(dual(refine(points))), order - 1) if order else points

 refine = (points) ->
   refined = []


stop static configuration from bleeding out of the tests

--- tests/Backbone.sync.Tests.coffee
+++ tests/Backbone.sync.Tests.coffee
@@ -79,6 +79,9 @@
     beforeEach ->
       V1.Backbone.setDefaultInstance(url: "url", fetch: recorded)

+    afterEach ->
+      V1.Backbone.clearDefaultInstance()
+
     it "can sync an indiviual model", ->
       Member = V1.Backbone.Model.extend
           assetType: "Member"


display fields for sets

display fields for sets

--- node/fabricate.coffee
+++ node/fabricate.coffee
@@ -398,7 +398,9 @@
       id: _.uniqueId()
       key: 'homepage:featured'
       item_type: 'FeaturedLink'
-
+      display_on_mobile: true
+      display_on_desktop: true
+
     when 'sale_artwork'
       id: _.uniqueId()
       user_notes: 'This is a very nice note.'


IE can only postMessage strings across frames, so stringify/parse them

--- login-frame/login.coffee
+++ login-frame/login.coffee
@@ -4,7 +4,8 @@
 host = "//#{location.hostname}:3000" unless +location.port is 80

 postBack = (command, response) ->
-  parent.postMessage {command, response}, "#{location.protocol}//#{location.host}"
+  data = JSON.stringify {command, response}
+  parent.postMessage data, "#{location.protocol}//#{location.host}"

 issueCommand = (command, params = {}, options = {}) ->
   request = $.getJSON "#{host}/#{command}?callback=?", params
@@ -17,7 +18,7 @@
     postBack command, success: false, message: 'Couldn\'t connect to the server'

 $(window).on 'message', ({originalEvent: e}) ->
-  # console.log 'Auth got message', e
-  issueCommand command, params for command, params of e.data
+  data = JSON.parse e.data
+  issueCommand command, params for command, params of data

 issueCommand 'current_user', {}, postAs: 'login', ignoreFailure: true


[ #316302 ] RTE in static readonly, on IE: YUI RTE initialization runs and fails

--- src/resources-packaged/ops/javascript/orbeon/xforms/control/RTE.coffee
+++ src/resources-packaged/ops/javascript/orbeon/xforms/control/RTE.coffee
@@ -30,6 +30,7 @@
     # Initializes the RTE editor for a particular control.
     init: (container) ->
         super container
+
         if Utils.isNewXHTMLLayout()
             # In span mode, get textarea under container and keep current structure
             textarea = @container.getElementsByTagName("textarea")[0]
@@ -119,5 +120,11 @@

 Page.registerControlConstructor RTE, (container) ->
     hasClass = (c) -> YD.hasClass container, c
-    (hasClass "xforms-textarea") and (hasClass "xforms-mediatype-text-html")
-ORBEON.xforms.control.RTE = RTE
+    (hasClass "xforms-textarea") and (hasClass "xforms-mediatype-text-html") and not(hasClass "xforms-static")
+
+# When in static mode, we never want to initialize the RTE, or do anything with it, so we let the base class Control handle it
+Page.registerControlConstructor Control, (container) ->
+    hasClass = (c) -> YD.hasClass container, c
+    (hasClass "xforms-textarea") and (hasClass "xforms-mediatype-text-html") and (hasClass "xforms-static")
+
+ORBEON.xforms.control.RTE = RTE


elektron: make sure previous teams are loaded into tray menu

--- elektron/src/trayicon.coffee
+++ elektron/src/trayicon.coffee
@@ -4,7 +4,7 @@

 Tray           = electron.Tray
 Menu           = electron.Menu
-{ app, shell } = electron
+{ app, shell, ipcMain } = electron

 TRAY_ICON      = path.resolve path.join __dirname, '../assets/icons/kdTemplate.png'
 TRAY_ICON_REV  = path.resolve path.join __dirname, '../assets/icons/kdRev.png'
@@ -44,25 +44,30 @@

 module.exports = class KodingTray

-  constructor: (tray) ->
+  constructor: (mainWindow) ->

     # Globals
+    @_mainWindow = mainWindow
     @_inProgress  = no
     @_isKdRunning = no
     @_contextMenu = []
+    @_previousTeams = {}

     @tray = new Tray TRAY_ICON
     # @tray.setPressedImage TRAY_ICON_REV

     @tray.on 'click', => @handleClick()

-    setTimeout  =>
+    kallback = =>
       @checkKdStatus no
-    , 100
-
-    setInterval =>
-      @checkKdStatus no
-    , 1000 * CHECK_TIMER
+      @loadPreviousTeams()
+
+    @_mainWindow.webContents.on 'did-navigate', kallback
+    @_mainWindow.webContents.on 'did-navigate-in-page', kallback
+
+    setTimeout kallback, 2000
+    setInterval kallback, 1000 * CHECK_TIMER
+

   handleClick: ->
@@ -72,12 +77,15 @@
     else @checkKdStatus yes

-  setMenu: (menu, show = no) ->
+  setMenu: (menu = [], show = no) ->

     if typeof menu is 'string'
       menu = [ label: menu, enabled: no ]

-    @_contextMenu = Menu.buildFromTemplate menu.concat [
+    win = @_mainWindow
+    teams = @_previousTeams
+
+    menuItems = [
       type    : 'separator'
       visible : @_isKdRunning
     ,
@@ -99,7 +107,22 @@
       click   : -> app.quit()
     ]

+
+    if (keys = Object.keys teams).length
+      submenu = \
+        keys.map (key) ->
+          return  if key is 'latest'
+          teamName = teams[key]
+          return { label: teamName, click: -> win.loadURL "http://#{key}.koding.com" }
+        .filter(Boolean)
+
+      menu.unshift { type: 'separator', visible: on }
+      menu.unshift { label: 'Teams', submenu }
+
+    @_contextMenu = Menu.buildFromTemplate menu.concat menuItems
+
     @tray.popUpContextMenu @_contextMenu  if show
+

   setFailed: (err) ->
@@ -248,3 +271,14 @@
       else
         @_isKdRunning = yes
         @loadMachineMenu show
+
+
+  loadPreviousTeams: ->
+
+    ipcMain.once 'answer-previous-teams', (event, previousTeams) =>
+      console.log {previousTeams}
+      @_previousTeams = previousTeams
+      @setMenu()
+
+    console.log 'loading previous teams'
+    @_mainWindow.webContents.send 'get-previous-teams'


Add integration tests for smalldatetime and datetime parameters to procedure calls.

--- test/integration/rpc-test.coffee
+++ test/integration/rpc-test.coffee
@@ -45,6 +45,18 @@
 exports.execProcIntNull = (test) ->
   testProc(test, TYPES.Int, 'int', null)

+exports.execProcSmallDateTime = (test) ->
+  testProc(test, TYPES.SmallDateTime, 'smalldatetime', new Date('December 4, 2011 10:04:00'))
+
+exports.execProcSmallDateTimeNull = (test) ->
+  testProc(test, TYPES.SmallDateTime, 'smalldatetime', null)
+
+exports.execProcDateTime = (test) ->
+  testProc(test, TYPES.DateTime, 'datetime', new Date('December 4, 2011 10:04:23'))
+
+exports.execProcDateTimeNull = (test) ->
+  testProc(test, TYPES.DateTime, 'datetime', null)
+
 exports.execProcOutputVarChar = (test) ->
   testProcOutput(test, TYPES.VarChar, 'varchar(10)', 'test')

@@ -74,6 +86,18 @@

 exports.execProcOutputIntNull = (test) ->
   testProcOutput(test, TYPES.Int, 'int', null)
+
+exports.execProcOutputSmallDateTime = (test) ->
+  testProcOutput(test, TYPES.SmallDateTime, 'smalldatetime', new Date('December 4, 2011 10:04:00'))
+
+exports.execProcOutputSmallDateTimeNull = (test) ->
+  testProcOutput(test, TYPES.SmallDateTime, 'smalldatetime', null)
+
+exports.execProcOutputDateTime = (test) ->
+  testProcOutput(test, TYPES.DateTime, 'datetime', new Date('December 4, 2011 10:04:23'))
+
+exports.execProcOutputDateTimeNull = (test) ->
+  testProcOutput(test, TYPES.DateTime, 'datetime', null)

 exports.execProcWithBadName = (test) ->
   test.expect(3)
@@ -204,7 +228,10 @@
   )

   request.on('row', (columns) ->
-    test.strictEqual(columns[0].value, value)
+      if (value instanceof Date)
+        test.strictEqual(columns[0].value.getTime(), value.getTime())
+      else
+        test.strictEqual(columns[0].value, value)
   )

   connection = new Connection(config)
@@ -263,7 +290,10 @@

   request.on('returnValue', (name, returnValue, metadata) ->
     test.strictEqual(name, 'paramOut')
-    test.strictEqual(returnValue, value)
+    if (value instanceof Date)
+      test.strictEqual(returnValue.getTime(), value.getTime())
+    else
+      test.strictEqual(returnValue, value)
     test.ok(metadata)
   )


add getURL() test for test app

add getURL() test for test app

--- test/integration/tool_rpc.coffee
+++ test/integration/tool_rpc.coffee
@@ -13,8 +13,8 @@
 should = require 'should'

-url = 'http://localhost:3001' # DRY DRY DRY
-login_url = "#{url}/login"
+BASE_URL = 'http://localhost:3001' # DRY DRY DRY
+login_url = "#{BASE_URL}/login"
 browser = wd.remote()

 # Hacky way to extend browser.
@@ -45,24 +45,52 @@

   context "when create a dataset with the test app", ->
     before (done) ->
-      browser.get "#{url}/tools", ->
-        browser.waitForElementByCss '.test-app.tool', 4000, ->
-          browser.elementByCss '.test-app.tool', (err, link) ->
-            link.click done
+      browser.get "#{BASE_URL}/tools", =>
+        browser.waitForElementByCss '.test-app.tool', 4000, =>
+          browser.elementByCss '.test-app.tool', (err, link) =>
+            link.click =>
+              browser.waitForElementByCss 'iframe', 4000, =>
+                browser.trueURL (err, url) =>
+                  @toolURL = url
+                  done()

     context 'when the redirect button is pressed', ->
       before (done) ->
-        browser.waitForElementByCss 'iframe', 4000, ->
-          browser.frame 0, ->
-            browser.waitForElementByCss '#redirect', 4000, ->
-              browser.elementByCss '#redirect', (err, btn) ->
-                btn.click ->
-                  browser.windowHandle (err, handle) ->
-                    browser.window handle, done
+        browser.frame 0, ->
+          browser.waitForElementByCss '#redirect', 4000, ->
+            browser.elementByCss '#redirect', (err, btn) ->
+              btn.click ->
+                browser.windowHandle (err, handle) ->
+                  browser.window handle, done

       it 'redirects the host to the specified URL', (done) ->
         browser.trueURL (err, url) ->
-          url.should.equal url
+          url.should.equal "#{BASE_URL}/"
+          done()
+
+    context 'when the showURL button is pressed', ->
+      before (done) ->
+        browser.get @toolURL, ->
+          browser.waitForElementByCss 'iframe', 4000, ->
+            browser.frame 0, ->
+              browser.waitForElementByCss '#showURL', 4000, ->
+                browser.elementByCss '#showURL', (err, btn) ->
+                  btn.click ->
+                    browser.windowHandle (err, handle) ->
+                      browser.window handle, done
+
+      before (done) ->
+        browser.waitForElementByCss 'iframe', 4000, =>
+          browser.frame 0, =>
+            browser.waitForElementByCss '#textURL', 4000, =>
+              browser.elementByCss '#textURL', (err, el) =>
+                @elURL = el
+                done()
+
+
+      it 'shows the scraperwiki.com URL in an element', (done) ->
+        @elURL.text (err, text) =>
+          text.should.equal @toolURL
           done()

   after (done) ->


explicit return is not necessary

explicit return is not necessary

--- test/mocha/ephemeris.spec.coffee
+++ test/mocha/ephemeris.spec.coffee
@@ -15,7 +15,6 @@
   traverse(obj).forEach (val) ->
     if typeof val is 'number' and val % 1 isnt 0
       @update val.toFixed(digits)
-  return obj

 # Call the ephemeris expecting a `(data) -> ...`


update service name in tests

update service name in tests

--- test/unit/get-id-from-url.coffee
+++ test/unit/get-id-from-url.coffee
@@ -1,7 +1,7 @@

 describe 'getIdFromURL', ->
     $injector = angular.injector ['youtube-embed']
-    getIdFromURL = $injector.get('$youtube').getIdFromURL
+    getIdFromURL = $injector.get('youtubeEmbedUtils').getIdFromURL

     it 'should handle regular query strings', ->
         url = 'https://www.youtube.com/watch?v=nViWpVc1x_4&feature=youtu.be'


Copy messages by clicking on them

Copy messages by clicking on them

--- lib/bottom-dock/output-view.coffee
+++ lib/bottom-dock/output-view.coffee
@@ -19,6 +19,8 @@
       if resultset.statusmessage?.length > 0
           statusmessage = document.createElement('pre')
           statusmessage.classList.add 'statusmessage'
+          statusmessage.classList.add 'notice-bar'
+          @listenCopy(statusmessage, resultset.statusmessage)
           statusmessage.setAttribute 'title', resultset.query
           message = document.createElement('span')
           message.classList.add 'message'
@@ -35,11 +37,14 @@
           else
               runtime.classList.add 'runtime_instant'
               runtime.textContent =  'instant'
+          message.style['min-width'] = '200px'
           statusmessage.appendChild runtime
           statusmessage.appendChild message
           @outputContainer.append statusmessage
       if resultset.error
           error = document.createElement('pre')
+          @listenCopy(error, resultset.error)
+          error.classList.add 'notice-bar'
           error.classList.add 'error'
           errorHeader = document.createElement('span')
           errorHeader.classList.add 'errorheader'
@@ -53,16 +58,41 @@
           @outputContainer.append error
       if resultset.notices?.length > 0
           for n in resultset.notices
-              notice = document.createElement('pre')
-              notice.classList.add 'notice'
-              notice.textContent = n.substr(9)
+              notice = @notice('notice', n.substr(9))
+
               @outputContainer.append notice
       if resultset.messages?.length > 0
           for n in resultset.messages
-              message = document.createElement('pre')
-              message.classList.add 'messages'
-              message.textContent = n
+              message = @notice('messages', n)
               @outputContainer.append message
+
+  listenCopy: (element, text) ->
+      element.addEventListener 'click', () ->
+          element.classList.add 'notice-bar-flash'
+          setTimeout(
+            () ->
+                element.classList.remove 'notice-bar-flash',
+            500
+          )
+          atom.clipboard.write(text)
+
+  notice: (type, text) ->
+      div = document.createElement('div')
+      @listenCopy(div, text)
+      div.classList.add 'notice-bar'
+      notice = document.createElement('pre')
+      notice.classList.add type
+      notice.textContent = text
+      notice.style['float'] = 'left'
+      notice.style['min-width'] = '300px'
+
+      clear = document.createElement('div')
+      clear.style['clear'] = 'both'
+
+      div.appendChild(notice)
+      div.appendChild(clear)
+
+      return div

   clear: ->
     @outputContainer.empty()


test(test/uploader-demo-jquery.coffee): update manual test script

--- test/uploader-demo-jquery.coffee
+++ test/uploader-demo-jquery.coffee
@@ -24,6 +24,8 @@
             forceConfirm: true
             params:
                 foo: "bar"
+        failedUploadTextDisplay:
+            mode: 'custom'
         display:
             fileSizeOnSubmit: true
         paste:


JStacktemplate.one should return one object not an array

--- workers/social/lib/social/models/computeproviders/stacktemplate.coffee
+++ workers/social/lib/social/models/computeproviders/stacktemplate.coffee
@@ -182,10 +182,14 @@

     success: (client, selector, options, callback) ->

+      [options, callback] = [callback, options]  unless callback
+
       options ?= {}
       options.limit = 1

-      @some$ client, selector, options, callback
+      @some$ client, selector, options, (err, templates) ->
+        [template] = templates ? []
+        callback err, template

   checkUsage: (callback) ->


make context menu work properly

make context menu work properly

--- menus/block-comment.cson
+++ menus/block-comment.cson
@@ -1,6 +1,6 @@
 # See https://atom.io/docs/latest/creating-a-package#menus for more details
 'context-menu':
-  '.overlayer': [{
+  'atom-text-editor': [{
     'label': 'Toggle comment', 'command': 'comment:toggle'
   }]


stacktemplate: add Validator.own permission to generate stack template

--- workers/social/lib/social/models/computeproviders/stacktemplate.coffee
+++ workers/social/lib/social/models/computeproviders/stacktemplate.coffee
@@ -325,7 +325,7 @@
   generateStack: permit

     advanced: [
-      { permission: 'update own stack template' }
+      { permission: 'update own stack template', validateWith: Validators.own }
       { permission: 'update stack template' }
     ]


StackTemplate: fix validationError err

StackTemplate: fix validationError err

--- workers/social/lib/social/models/computeproviders/stacktemplate.coffee
+++ workers/social/lib/social/models/computeproviders/stacktemplate.coffee
@@ -161,7 +161,7 @@
         return callback new KodingError 'Title required.'

       validateTemplate data.template, group, (err) ->
-        return callback validationError  if err
+        return callback err  if err

         if data.config?
           data.config.groupStack = no
@@ -395,7 +395,7 @@
           return next()  unless template?

           validateTemplate template, group, (err) =>
-            return next validationError  if err
+            return next err  if err

             data.template = generateTemplateObject \
               template, rawContent, templateDetails


Use argv[2..] to cut both interpreter and script names.

--- docopt.coffee
+++ docopt.coffee
@@ -26,7 +26,7 @@
         formals = @children.join ', '
         "#{@constructor.name}(#{formals})"

-    match: -> throw new Error("""classes inheriting from Pattern
+    match: -> throw new Error("""classes inheriting from Pattern
                                  must overload the match method""")

     flat: ->
@@ -206,7 +206,7 @@
             if not matched
                 return [false, left, collected]
         [true, l, c]
-
+

 class Optional extends Pattern

@@ -368,7 +368,7 @@
     result = []
     if token is '('
         tokens.shift()
-
+
         result = [new Required parse_expr tokens, options]
         if tokens.shift() != ')'
             raise tokens.error "Unmatched '('"
@@ -452,7 +452,7 @@
         when arg not in allowedargs

     argv    = if kwargs.argv is undefined \
-              then process.argv[1..] else kwargs.argv
+              then process.argv[2..] else kwargs.argv
     name    = if kwargs.name is undefined \
               then null else kwargs.name
     help    = if kwargs.help is undefined \
@@ -467,7 +467,7 @@

     extras help, version, options, doc
     formal_pattern   = parse_pattern formal_usage(usage), pot_options
-    print formal_pattern.flat()
+
     pot_arguments = (a for a in formal_pattern.flat() \
         when a.constructor in [Argument, Command])
     [matched, left, argums] = formal_pattern.fix().match argv
@@ -496,4 +496,3 @@
     parse_shorts : parse_shorts
     parse_args   : parse_args
     printable_usage: printable_usage
-


Removed blah tag

Removed blah tag

--- docpad.coffee
+++ docpad.coffee
@@ -31,11 +31,6 @@
         },
         {
           name: 'Dev',
-          href: '/tags/Dev.html',
-          section: 'tag-Dev'
-        },
-        {
-          name: 'Blah',
           href: '/tags/Dev.html',
           section: 'tag-Dev'
         }


Fix a bug

Fix a bug

--- src/6.web.coffee
+++ src/6.web.coffee
@@ -1,7 +1,7 @@
 # According to RFC-7230, for HTTP response status we use "status reason"
 # instead of status text or status message.

-mate.web = {}
+mate.web = web = {}
 web.request = (options) -> new Promise((resolve, reject) ->
     try
         method = options.method


Code review

Code review

--- BackofficeBundle/Resources/public/coffee/tinyMCE/activateTinyMCE.coffee
+++ BackofficeBundle/Resources/public/coffee/tinyMCE/activateTinyMCE.coffee
@@ -15,7 +15,7 @@
   $.ajax
     url: $("#contextual-informations").data("translation-url-pattern").replace("*domain*", "tinymce")
     success: (response) ->
-      tinymce.util.I18n.add response.lcoale, response.catalog
+      tinymce.util.I18n.add response.locale, response.catalog

   do (view, textarea) ->
     initParameter()


cursor test needs to wait for editor to load

--- tests/scripts/unit/cursor.coffee
+++ tests/scripts/unit/cursor.coffee
@@ -4,9 +4,8 @@
     before((done) ->
       initial = '
BoldItalic

Large
'
       $('#test-container').html(Scribe.Utils.cleanHtml(initial, true))
-      editor = new Scribe.Editor('test-container')
+      editor = new Scribe.Editor('test-container', { onReady: done })
       cursorManager = new Scribe.MultiCursor(editor)
-      _.defer(done)
     )

     it('should set cursor', ->


remove duplication

remove duplication

--- src/Bacon.coffee
+++ src/Bacon.coffee
@@ -267,10 +267,8 @@

 class Observable
   constructor: (desc) ->
-    desc = describe(desc)
     @assign = @onValue
-    @deps = desc.deps
-    @toString = desc.toString
+    withDescription(desc, this)
   onValue: ->
     f = makeFunctionArgs(arguments)
     @subscribe (event) ->


refactor, introduce Bacon.fromArray

refactor, introduce Bacon.fromArray

--- src/Bacon.coffee
+++ src/Bacon.coffee
@@ -27,10 +27,6 @@
 Bacon.noMore = "veggies"

 Bacon.more = "moar bacon!"
-
-Bacon.never = => new EventStream (sink) =>
-  sink (end())
-  nop

 Bacon.later = (delay, value) ->
   Bacon.sequentially(delay, [value])
@@ -100,14 +96,19 @@
   Bacon.fromPoll(delay, poll)

 Bacon.constant = (value) ->
-  new Property(just(initial, value))
-
-Bacon.once = (value) ->
-  new EventStream(just(next, value))
-
-just = (wrapper, value) ->
+  new Property(sendWrapped([value], initial))
+
+Bacon.never = -> Bacon.fromArray([])
+
+Bacon.once = (value) -> Bacon.fromArray([value])
+
+Bacon.fromArray = (values) ->
+  new EventStream(sendWrapped(values, next))
+
+sendWrapped = (values, wrapper) ->
   (sink) ->
-    sink (wrapper value)
+    for value in values
+      sink (wrapper value)
     sink (end())
     nop


Move lastLine setter into conditional block to prevent .commands and blank lines from setting lastLine

--- repl.coffee
+++ repl.coffee
@@ -106,7 +106,7 @@
     if code and code.length and code isnt '.history' and lastLine isnt code
       # Save the latest command in the file
       fs.write fd, "#{code}\n"
-    lastLine = code
+      lastLine = code

   process.on 'exit', ->
     fs.closeSync fd


Home: removed the capitalized Pricing

Home: removed the capitalized Pricing

--- client/Intro/home.coffee
+++ client/Intro/home.coffee
@@ -11,7 +11,7 @@
     super options, data

     @pricingButton = new KDButtonView
-      title       : "Check our Pricingfor you or your team"
+      title       : "Check our pricingfor you or your team"
       cssClass    : 'solid green shadowed pricing'
       icon        : 'yes'
       iconClass   : 'dollar'


Utils: add a method for decimal point adjustment

--- client/Main/utils.coffee
+++ client/Main/utils.coffee
@@ -790,3 +790,31 @@
     parts.join ''

+  ###*
+  Decimal adjustment of a number
+  https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/ceil
+
+  @param	{String}	type	The type of adjustment.
+  @param	{Number}	value	The number.
+  @param	{Integer}	exp		The exponent (the 10 logarithm of the adjustment base).
+  @returns	{Number}			The adjusted value.
+  ###
+  decimalAdjust: (type, value, exp) ->
+
+    # If the exp is undefined or zero...
+    return Math[type](value)  if typeof exp is "undefined" or +exp is 0
+    value = +value
+    exp = +exp
+
+    # If the value is not a number or the exp is not an integer...
+    return NaN  if isNaN(value) or not (typeof exp is "number" and exp % 1 is 0)
+
+    # Shift
+    value = value.toString().split("e")
+    value = Math[type](+(value[0] + "e" + ((if value[1] then (+value[1] - exp) else -exp))))
+
+    # Shift back
+    value = value.toString().split("e")
+    +(value[0] + "e" + ((if value[1] then (+value[1] + exp) else exp)))
+
+


Code cleanup

Code cleanup

--- lib/models/result-set-model.coffee
+++ lib/models/result-set-model.coffee
@@ -15,7 +15,7 @@
         @results.push(result)

     addResultSet: (resultSet) ->
-      if typeof resultSet isnt 'undefined' && resultSet.keyword is @keyword
+      if typeof resultSet isnt 'undefined' and resultSet.keyword is @keyword
         @results = @results.concat(resultSet.results)

     isEmpty: ->


#1173 - Cannot drag question out of nested groups

--- jsapp/xlform_model_view/view.surveyApp.coffee
+++ jsapp/xlform_model_view/view.surveyApp.coffee
@@ -360,13 +360,12 @@
       if @expand_all_multioptions()
         @$(".card--expandedchoices").each (i, el)=>
           @_getViewForTarget(currentTarget: el).hideMultioptions()
-          ``
       else
         @$(".card--selectquestion").each (i, el)=>
           @_getViewForTarget(currentTarget: el).showMultioptions()
-          ``

       @set_multioptions_label()
+      return

     getItemPosition: (item) ->
       i = 0
@@ -386,6 +385,7 @@

       sortable_stop = (evt, ui)=>
         $(ui.item).trigger('survey__row-sortablestop')
+        @formEditorEl.find('.group__rows .survey__row').each @_preventSortableIfGroupTooSmall

       @formEditorEl.sortable({
           # PM: commented out axis, because it's better if cards move horizontally and vertically
@@ -410,12 +410,9 @@
             ui.sender.sortable('cancel')
         })
       group_rows = @formEditorEl.find('.group__rows')
-      group_rows.off 'mouseenter', '> .survey__row', @_preventSortableIfGroupTooSmall
-      group_rows.off 'mouseleave', '> .survey__row', @_preventSortableIfGroupTooSmall
-      group_rows.on 'mouseenter', '> .survey__row', @_preventSortableIfGroupTooSmall
-      group_rows.on 'mouseleave', '> .survey__row', @_preventSortableIfGroupTooSmall
-      group_rows.sortable({
-          cancel: 'button, .btn--addrow, .well, ul.list-view, li.editor-message, .editableform, .row-extras, .js-cancel-sort, .js-cancel-group-sort'
+      group_rows.each (index) ->
+        $(@).sortable({
+          cancel: 'button, .btn--addrow, .well, ul.list-view, li.editor-message, .editableform, .row-extras, .js-cancel-sort, .js-cancel-group-sort' + index
           cursor: "move"
           distance: 5
           items: "> li"
@@ -427,15 +424,18 @@
           activate: sortable_activate_deactivate
           deactivate: sortable_activate_deactivate
         })
-      return
-    _preventSortableIfGroupTooSmall: (evt)->
-      $ect = $(evt.currentTarget)
-      if evt.type is 'mouseenter' && $ect.siblings('.survey__row').length is 0
-        $ect.addClass('js-cancel-group-sort')
-        evt.stopPropagation()
-      else
-        $ect.removeClass('js-cancel-group-sort')
-        evt.stopPropagation()
+        $(@).attr('data-sortable-index', index)
+
+      group_rows.find('.survey__row').each @_preventSortableIfGroupTooSmall
+
+      return
+    _preventSortableIfGroupTooSmall: (index, element)->
+      $element = $(element)
+      class_name_matches = element.className.match(/js-cancel-group-sort\d+/g)
+      if class_name_matches?
+        $element.removeClass class_name_matches.join(' ')
+      if $element.siblings('.survey__row').length is 0
+        $element.addClass('js-cancel-group-sort' + ($element.closest('.group__rows').attr('data-sortable-index')))

     validateSurvey: ()->
       true


Pass options through and silently set location when updating in the background

--- models/mixins/geo.coffee
+++ models/mixins/geo.coffee
@@ -11,7 +11,8 @@
     geo.locate
       accuracy: 'low'
       success: _.wrap options.success, (success, geoFormatted) =>
-        @setGeo(geoFormatted) unless @hasLocation()
+        unless @hasLocation()
+          @setGeo geoFormatted, silent: true
         success?()

   # Gravity runs a delayed job after user saves that geolocates
@@ -40,7 +41,7 @@
     else
       @setGeo new GeoFormatter(obj)

-  setGeo: (geoFormatted) ->
+  setGeo: (geoFormatted, options = {}) ->
     @set location:
       city: geoFormatted.getCity() or ''
       state: geoFormatted.getState() or ''
@@ -48,3 +49,4 @@
       postal_code: geoFormatted.getPostalCode() or ''
       coordinates: geoFormatted.getCoordinates() or null
       country: geoFormatted.getCountry() or ''
+    , options


fix simple-form-error-scrollto edge-case

--- app/assets/javascripts/folio/console/_simple-form-error-scrollto.coffee
+++ app/assets/javascripts/folio/console/_simple-form-error-scrollto.coffee
@@ -10,7 +10,10 @@
       id = $tab.attr('id')
       $('.nav-tabs .nav-link').filter(-> @href.split('#').pop() is id).click()

-    $('html, body').animate scrollTop: $field.offset().top - 16, ->
+    offset = $field.offset().top
+    offset = $field.closest('.card').offset().top if offset is 0
+
+    $('html, body').animate scrollTop: offset - 16, ->
       $field.addClass('has-danger-blink')
       setTimeout (->
         $field.removeClass('has-danger-blink')


pricing: update developer resource pack quantities

--- client/Pricing/views/developerplan.coffee
+++ client/Pricing/views/developerplan.coffee
@@ -19,8 +19,8 @@
     cssClass     : "green"
     packFeatures :
       "CPU"      : "4x"
-      "RAM"      : "2GB"
-      "DISK"     : "10GB"
+      "RAM"      : "4GB"
+      "DISK"     : "20GB"
       "VMs"      : "20x"
       "Always On": "2x"
     price        : "$39"
@@ -30,8 +30,8 @@
     cssClass     : "yellow"
     packFeatures :
       "CPU"      : "6x"
-      "RAM"      : "2GB"
-      "DISK"     : "10GB"
+      "RAM"      : "6GB"
+      "DISK"     : "30GB"
       "VMs"      : "30x"
       "Always On": "3x"
     price        : "$59"
@@ -41,8 +41,8 @@
     cssClass     : "orange"
     packFeatures :
       "CPU"      : "8x"
-      "RAM"      : "2GB"
-      "DISK"     : "10GB"
+      "RAM"      : "8GB"
+      "DISK"     : "40GB"
       "VM"       : "40x"
       "Always On": "4x"
     price        : "$79"
@@ -52,8 +52,8 @@
     cssClass     : "red"
     packFeatures :
       "CPU"      : "10x"
-      "RAM"      : "2GB"
-      "DISK"     : "10GB"
+      "RAM"      : "10GB"
+      "DISK"     : "50GB"
       "VMs"      : "50x"
       "Always On": "5x"
     price        : "$99"


Uniform editor change events. Rename setMode to _setMode.

--- client/edit.views.coffee
+++ client/edit.views.coffee
@@ -16,40 +16,39 @@
       jade: new JadeMode()

     @secret = new views.Secret
-    @secret.bind "change", =>
-      @showUnsavedNotification()
+    @secret.bind "change", => @trigger "change"

     @saveButton = $ "#save"
     @saveButton.tipsy
       gravity: 's',
       opacity: 1
+
     @saveButton.click => @save()

     @initAce()

     @modeEl = @$('#mode')
     @modeEl.change =>
-      @setMode()
+      @_setMode()
       @showUnsavedNotification()

     @model.bind "initialfetch", (e) =>
+      @setEditorContents @model.get "code"
+      @modeEl.val(@model.get "mode")
+      @_setMode()
+      @secret.setSecret @model.get "secret"
+
       if e.source is "default"
         @showUnsavedNotification()
       else
         @hideUnsavedNotification()

-      @setEditorContents @model.get "code"
-
-      @modeEl.val(@model.get "mode")
-      @setMode()
-
-      @secret.setSecret @model.get "secret"
-
-      @editor.getSession().on "change", =>
-        console.log "CHANGE"
-        @showUnsavedNotification()
-
-  setMode: ->
+
+
+
+    @bind "change", => @showUnsavedNotification()
+
+  _setMode: ->
     @editor.getSession().setMode(@modes[@modeEl.val()])

   setEditorContents: (code) ->
@@ -60,7 +59,8 @@
     @editor.setShowPrintMargin false
     session = @editor.getSession()
     session.setTabSize(2);
-    @setMode
+    @_setMode
+    @editor.getSession().on "change", => @trigger "change"

     # Hide the line numbering, doesn't work perfectly


replace previous output cell when running

--- editor.coffee
+++ editor.coffee
@@ -202,7 +202,8 @@
     is_clean: -> editor.getValue() is '' and not @.used
     run: ->
       context.used = true
-      run context, editor.getValue()
+      context.output_cell?.$el.remove()
+      context.output_cell = run context, editor.getValue()

   editor.lead_cell = context

@@ -242,12 +243,8 @@
 run = (input_cell, string) ->
   $el = $ '<div class="cell output"/>'

-  if input_cell?
-    input_cell.$el.after $el
-    $top = input_cell.$el
-  else
-    $document.append $el
-    $top = $el
+  input_cell.$el.after $el
+  $top = input_cell.$el

   scroll_to_result = ($result)->
     top = if $result?
@@ -358,7 +355,7 @@
     catch e
       handle_exception e, compiled

-
+  $el: $el

 window.init_editor = ->
   $document = $ '#document'


sidebar: deselect items when the route changes

--- client/Social/Activity/sidebar/activitysidebar.coffee
+++ client/Social/Activity/sidebar/activitysidebar.coffee
@@ -37,6 +37,7 @@
       notificationController
       computeController
       socialapi
+      router
     } = KD.singletons

     @sections     = {}
@@ -46,6 +47,9 @@
     @selectedItem = null

     # @appsList = new DockController
+
+    router
+      .on "RouteInfoHandled",          @bound 'deselectAllItems'

     notificationController
       .on 'AddedToChannel',            @bound 'accountAddedToChannel'


activity: remove debug statement

activity: remove debug statement

--- client/activity/lib/models/channelparticipants.coffee
+++ client/activity/lib/models/channelparticipants.coffee
@@ -76,8 +76,6 @@
   setParticipant: (account) ->

     return @participants  if @participants.get account._id
-
-    console.log {account}, '>>>>>>>>>>>>>>', account._id

     @participants = @participants.set account._id, immutable.fromJS account


EnvironmentListItem: fix indentations

EnvironmentListItem: fix indentations

--- client/app/lib/environment/environmentlistitem.coffee
+++ client/app/lib/environment/environmentlistitem.coffee
@@ -1,16 +1,14 @@
-kd                     = require 'kd'
-JView                  = require 'app/jview'
-isKoding               = require 'app/util/isKoding'
+kd                        = require 'kd'
+JView                     = require 'app/jview'
+isKoding                  = require 'app/util/isKoding'

-remote                 = require('app/remote').getInstance()
+remote                    = require('app/remote').getInstance()

-MachinesList           = require './machineslist'
-MachinesListController = require './machineslistcontroller'
+MachinesList              = require './machineslist'
+MachinesListController    = require './machineslistcontroller'

-ComputeHelpers         = require '../providers/computehelpers'
-
-showNotification       = require 'app/util/showNotification'
-
+ComputeHelpers            = require '../providers/computehelpers'
+showNotification          = require 'app/util/showNotification'
 StackTemplateContentModal = require 'app/stacks/stacktemplatecontentmodal'

@@ -74,8 +72,8 @@

   handleMachineRequest: (provider) ->

-      @getDelegate().emit 'ModalDestroyRequested'
-      ComputeHelpers.handleNewMachineRequest { provider }
+    @getDelegate().emit 'ModalDestroyRequested'
+    ComputeHelpers.handleNewMachineRequest { provider }

   createExtraViews: ->


Use dynamic route handler names in order to prevent collisions

--- emails.coffee
+++ emails.coffee
@@ -159,7 +159,7 @@

       console.log 'Adding route: /' + path

-      Router.route 'emailPreview',
+      Router.route "preview#{template.name}Email",
         path: path
         where: 'server'
         action: ->
@@ -182,7 +182,7 @@

       console.log 'Adding route /' + path

-      Router.route 'sendEmail',
+      Router.route "send#{template.name}Email",
         path: path
         where: 'server'
         action: ->


Use setTimeout

Use setTimeout

--- scripts/peatix.coffee
+++ scripts/peatix.coffee
@@ -91,9 +91,11 @@
             if events[index]
               event = events[index]
               msg.send event.toString()
-            msg.send "他には？\n" +
-              eventsToString(events) + "\n" +
-              "何番のイベントの詳細を見ますか？[番号/見ない]"
+            setTimeout ->
+              msg.send "他には？\n" +
+                eventsToString(events) + "\n" +
+                "何番のイベントの詳細を見ますか？[番号/見ない]"
+            , 3000
           else if /^((見)?ない|(終|お)わり|おしまい)$/i.test text
             msg.send "またご利用ください。"
             @end


Try a new flatten option.

Try a new flatten option.

--- scripts/points.coffee
+++ scripts/points.coffee
@@ -14,8 +14,17 @@
 #   brettlangdon

 points = {}
-flatten = (array) ->
-    Array::concat.apply([], array)
+# flatten = (array) ->
+#     Array::concat.apply([], array)
+
+exports.flatten = flatten = (array) ->
+    flattened = []
+    for element in array
+        if element instanceof Array
+            flattened = flattened.concat flatten element
+        else
+            flattened.push element
+    flattened

 award_points = (msg, username, pts) ->
     points[username] ?= 0
@@ -93,4 +102,4 @@
                 for username in people
                     points[username] += 5
                     return
-    , 30000)
+    , 10000)


Output format tweaked.

Output format tweaked.

--- scripts/points.coffee
+++ scripts/points.coffee
@@ -110,5 +110,5 @@
             pointcount = msg.match[1]
             # leaders = pointcount([username + " with " + points[username] + " points."])

-            msg.send "The top " + pointcount " users with the most points are: "
+            msg.send "The top #{pointcount} users with the most points are: "
             return


...more maybe

...more maybe

--- scripts/trello.coffee
+++ scripts/trello.coffee
@@ -69,43 +69,36 @@
       msg.send "Please specify the Trello token in TRELLO_TOKEN"
       return
     msg.send 'going for it...'
-    msg.http(url)
-      .get() (err, res, body) ->
-        msg.send 'got the url: ' + url
-        if res.statusCode isnt 200
-          msg.send 'bad status code' + res.statusCode
-          return
-        else
-          msg.send 'ok...status code' + res.statusCode
-          jsdom = require 'jsdom'
-          msg.send 'i have required the jsdom lib'
-          jsdom.env(
-            html: url
-            scripts: [
-              'http://code.jquery.com/jquery-1.7.2.min.js'
-            ]
-            done: (errors, window) ->
-              msg.send 'i have finished doing whatever loading thing i need to do'
-              unless errors
-                $ = window.$
-                title = $('title').text()
-                msg.send 'i have the title: ' + title
+    jsdom = require 'jsdom'
+    msg.send 'i have required the jsdom lib'
+    jsdom.env(
+      html: url
+      scripts: [
+        'http://code.jquery.com/jquery-1.7.2.min.js'
+      ]
+      done: (errors, window) ->
+        msg.send 'i have finished doing whatever loading thing i need to do'
+        unless errors
+          $ = window.$
+          title = $('title').text()
+          msg.send 'i have the title: ' + title

-                if title
-                  title = title
-                  list = '510acfdfeca4af654e004e84'
-                else
-                  title = url
-                  list = '510be43bbfd03ea75700314b'
-                msg.send 'ready for my args'
-                args =
-                  name: title
-                  idList: list
-                  desc: url
+          if title
+            title = title
+            list = '510acfdfeca4af654e004e84'
+          else
+            title = url
+            list = '510be43bbfd03ea75700314b'
+          msg.send 'ready for my args'
+          args =
+            name: title
+            idList: list
+            desc: url

-                trelloIt url, args, msg
-          )
-    msg.send 'i have failed you'
+          trelloIt url, args, msg
+    )
+
+    msg.send 'i am the end'

 trelloIt = (subject, args, msg) ->
   Trello = require("node-trello");


Tests: Helpers: IDEHelpers: Added text to createAndSaveNewFile.

--- client/test/lib/helpers/idehelpers.coffee
+++ client/test/lib/helpers/idehelpers.coffee
@@ -1,4 +1,5 @@
 helpers           = require './helpers.js'
+panelSelector     = '.pane-wrapper .kdsplitview-panel.panel-1'
 tabHandleSelector = "#{panelSelector} .application-tab-handle-holder"

 module.exports =
@@ -30,7 +31,7 @@
       .waitForElementVisible  '.kdlistview-contextmenu', 20000 # Assertion

-  createAndSaveNewFile: (browser) ->
+  createAndSaveNewFile: (browser, text) ->

     saveSelector        = '.kdlistview-contextmenu li.save'
     saveAsModalSelector = '.save-as-dialog'
@@ -40,6 +41,10 @@
     saveButtonSelector  = "#{saveAsModalSelector} .kddialog-buttons span.button-title"

     @openNewFile(browser)
+
+    if text
+      @setTextToEditor browser, text
+
     @openContextMenu(browser)

     browser
@@ -54,6 +59,9 @@
       .waitForElementVisible  filesTabSelector, 20000
       .assert.containsText    filesTabSelector, newName # Assertion

+    if text
+      browser.assert.containsText panelSelector, text
+
     return newName


use shell module on Linux

use shell module on Linux

--- lib/browse.coffee
+++ lib/browse.coffee
@@ -1,15 +1,17 @@
+{CompositeDisposable} = require 'atom'
+
 # Dependencies
+{exec} = require('child_process')
 fs   = require 'fs'
-path = require 'path'
 shell = require 'shell'
-
-{CompositeDisposable} = require 'atom'

 module.exports = BrowsePackages =
   self: 'browse'
+  debug: false
   subscriptions: null
-
+
   activate: ->
+
     # Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
     @subscriptions = new CompositeDisposable

@@ -23,7 +25,7 @@
     @subscriptions.dispose()

   browsePackages: ->
-    packageDir: atom.packages.getPackageDirPaths()[0]
+    packageDir = atom.packages.getPackageDirPaths()[0]

     # Does packages folder exist?
     try
@@ -32,8 +34,7 @@
       atom.notifications.addError(@self, detail: error, dismissable: true)
       return

-    # Open packages folder
-    shell.showItemInFolder(packageDir)
+    @openFolder(packageDir)

   revealFile: ->
     # Get parent folder of active file
@@ -41,10 +42,7 @@
     file = editor?.buffer.file

     if file isnt null
-      filePath = path.dirname(file?.path)
-
-      # Reveal file
-      shell.showItemInFolder(file.path)
+      @selectFile(file.path)
       return

     atom.notifications.addWarning("**#{@self}**: No active file", dismissable: false)
@@ -65,18 +63,55 @@
         continue

       # Open project folder
-      shell.showItemInFolder(project)
+      @openFolder(project)

   browseConfig: ->
-    configFile: atom.config.getUserConfigPath()
+    path = require 'path'
+
+    configFile = atom.config.getUserConfigPath()
     configPath = path.dirname(configFile)

-    # Does config folder exist?
-    try
-      fs.accessSync(configPath, fs.F_OK)
-    catch error
-      atom.notifications.addError(@self, detail: error, dismissable: true)
+    if @fileManager isnt null
+      # Does config folder exist?
+      try
+        fs.accessSync(configPath, fs.F_OK)
+      catch error
+        atom.notifications.addError(@self, detail: error, dismissable: true)
+        return
+
+      # Open config folder
+      @openFolder(configPath)
+
+  selectFile: (path) ->
+    # Custom file manager
+    fileManager = atom.config.get('browse.fileManager')
+
+    if fileManager?
+      exec "\"#{fileManager}\" \"#{path}\""
       return

-    # Open config folder
-    shell.openItem(configPath)
+    # Default file manager
+    switch process.platform
+      when "darwin"
+         exec "open -R #{path}"
+      when "win32"
+        exec "explorer /select,#{path}"
+      when "linux"
+        shell.showItemInFolder(path)
+
+  openFolder: (path) ->
+    # Custom file manager
+    fileManager = atom.config.get('browse.fileManager')
+
+    if fileManager?
+      exec "\"#{fileManager}\" \"#{path}\""
+      return
+
+    # Default file manager
+    switch process.platform
+      when "darwin"
+        exec "open #{path}"
+      when "win32"
+        exec "explorer #{path}"
+      when "linux"
+        shell.openItem(path)


simplify

simplify

--- lib/clever.coffee
+++ lib/clever.coffee
@@ -172,23 +172,21 @@
     set: (key, val) => dotty.put @_unsaved_values, key, val

     save: (cb) =>
-      creating = false
-      if not @_properties.id?
-        creating = true
-        w = new Create "#{clever.url_base}#{@constructor.path}", @_properties
-      else
+      update = @_properties.id?
+      if update
         return cb null if not _(@_unsaved_values).keys().length
         w = new Update @_uri, @_unsaved_values
+      else
+        w = new Create "#{clever.url_base}#{@constructor.path}", @_properties
+        w.post 'exec', (resp, body, cb_post) =>
+          self_link = _(body.links).find (link) -> link.rel is 'self'
+          return cb_post Error 'no self link' if not self_link?
+          @_uri = self_link.uri
+          cb_post null, resp, body
       w.post 'exec', (resp, body, cb_post) =>
         @_properties = if _(body.data).isString() then JSON.parse body.data else body.data # httpbin doesn't return json
         @_unsaved_values = {} if not err?
         cb_post null, resp, body
-      if creating
-        w.post 'exec', (resp, body, cb_post) =>
-          self_link = _(body.links).find (link) -> link.rel is 'self'
-          return if not self_link
-          @_uri = self_link.uri
-          cb_post null, resp, body
       w.exec cb

     to_json: () => _(@_properties).clone()


Add scrollBottom to editor

Add scrollBottom to editor

--- src/app/editor.coffee
+++ src/app/editor.coffee
@@ -275,8 +275,11 @@
     if options?.adjustVerticalScrollbar ? true
       @verticalScrollbar.scrollTop(scrollTop)

-  scrollBottom: ->
-    @scrollTop() + @scrollView.height()
+  scrollBottom: (scrollBottom) ->
+    if scrollBottom?
+      @scrollTop(scrollBottom - @scrollView.height())
+    else
+      @scrollTop() + @scrollView.height()

   renderVisibleLines: ->
     @clearLines()


Use correct less stylesheet path variable name

--- src/app/window.coffee
+++ src/app/window.coffee
@@ -164,7 +164,7 @@
   parser = new less.Parser
     syncImport: true
     paths: config.lessSearchPaths
-    filename: patlessStylesheetPath
+    filename: lessStylesheetPath
   try
     content = null
     parser.parse fsUtils.read(lessStylesheetPath), (e, tree) ->


Print exception in error message when an extension fails to load

--- src/app/window.coffee
+++ src/app/window.coffee
@@ -78,8 +78,7 @@
       require extensionKeymapPath if fs.exists(extensionKeymapPath)
       extension
     catch e
-      console.error "Failed to load extension named '#{name}'"
-      console.error e.stack
+      console.error "Failed to load extension named '#{name}'", e

   reload: ->
     if rootView?.getModifiedBuffers().length > 0


log attempted update when throwing error in DiffGenerator

--- app/coffee/DiffGenerator.coffee
+++ app/coffee/DiffGenerator.coffee
@@ -50,7 +50,11 @@

 	rewindUpdates: (content, updates) ->
 		for update in updates.reverse()
-			content = DiffGenerator.rewindUpdate(content, update)
+			try
+				content = DiffGenerator.rewindUpdate(content, update)
+			catch e
+				e.attempted_update = update # keep a record of the attempted update
+				throw e # rethrow the exception
 		return content

 	buildDiff: (initialContent, updates) ->


recommit

recommit

--- server/publish.coffee
+++ server/publish.coffee
@@ -1,5 +1,8 @@
 Users = new Meteor.Collection("users")
 Posts = new Meteor.Collection("posts")
+Instances = new Meteor.Collection("instances");
+
+

 Meteor.publish("my_user", (user_id) ->
   return Users.find( _id: user_id )
@@ -25,4 +28,5 @@

 Meteor.startup( ->
   Session.set 'user_id', Users.findOne( {} )['_id']
-)+  Session.set 'instance_name', 'metaCrunch'
+)


One presence exchange per topic

One presence exchange per topic

The AppController will subscribe to one presence exchange per topic
the user joins to. Presence exchanges are differentiated with normal
exchanges by a presence prefix defined in broker_app.src, or default
to "KDExchange-", following by the topic name.

--- client/landing/app/Applications/Chat.kdapplication/AppController.coffee
+++ client/landing/app/Applications/Chat.kdapplication/AppController.coffee
@@ -17,14 +17,7 @@
     @username = "Guest"+__utils.getRandomNumber() if @username is "Guest"

     @channels = {}
-    @broadcaster = mq.subscribe "private-KDPublicChat"
-
-    # Presence received has format [name, "bind" || "unbind"]
-    mq.presence @username, ([name, status]) =>
-      if status is "bind"
-        @addOnlineUser name, 'public'
-      else if status is "unbind"
-        @removeOfflineUser name, 'public'
+    @broadcaster = mq.subscribe "private-KDPublicChat"

   bringToFront:()->
     super name : 'Chat'#, type : 'background'
@@ -34,6 +27,14 @@

   joinChannel: (name) ->
     view = @getOptions().view
+
+    # Presence received has format [key, "bind" || "unbind"]
+    mq.presence @username, name, ([presence, status]) =>
+      if status is "bind"
+        @addOnlineUser presence, name
+      else if status is "unbind"
+        @removeOfflineUser presence, name
+
     channelPaneInstance = view.addChannelTab name
     channelName = "client-#{name}"

@@ -59,11 +60,11 @@
     channel = @channels[channelName]
     userItemViewInstance = channel.addOnlineUser name
     return if name is @username
-    userItemViewInstance.registerListener
-      KDEventTypes: 'click'
-      listener    : @
-      callback    : =>
-        @joinChannel name
+    # userItemViewInstance.registerListener
+    #   KDEventTypes: 'click'
+    #   listener    : @
+    #   callback    : =>
+    #     @joinChannel name

   removeOfflineUser: (name, channelName) ->
     channel = @channels[channelName]


"One year cookie expiry for plunks without expiry time"

--- servers/api/v1.coffee
+++ servers/api/v1.coffee
@@ -75,7 +75,9 @@
   req.plunker.create req.body, (err, plunk) ->
     if err then return apiError(res, err)
     else
-      res.cookie plunk.id, plunk.token, { expires: new Date(plunk.expires), httpOnly: true, path: "/api/v1/" }
+      expiry = plunk.expires or +new Date() + 1000 * 60 * 60 * 24 * 365 # 1 year
+
+      res.cookie plunk.id, plunk.token, { expires: new Date(expiry), httpOnly: true, path: "/api/v1/" }
       res.json(plunk, 201) # Created

 # Read
@@ -98,7 +100,11 @@
   else
     req.plunker.update req.plunk, req.body, (err, plunk) ->
       if err then return apiError(res, err)
-      else res.json(plunk, 200)
+      else
+        expiry = plunk.expires or +new Date() + 1000 * 60 * 60 * 24 * 365 # 1 year
+
+        res.cookie plunk.id, plunk.token, { expires: new Date(expiry), httpOnly: true, path: "/api/v1/" }
+        res.json(plunk, 200)

 # Delete
 app.del "/plunks/:id", fetchPlunk, checkToken, (req, res) ->


Check for points after output.

Check for points after output.

--- scripts/points.coffee
+++ scripts/points.coffee
@@ -124,19 +124,19 @@

     robot.respond /output$/i, (msg) ->
         if robot.auth.hasRole(msg.envelope.user, ['admin', 'moderator'])
-            points[username] ?= 0
-
             # Returns active users only with their points
             # score = ["#{username} has #{points[username]}" for username in people]

             # Also only returns active users at the moment
             # score = ["#{username} has #{points[username]}" for points in robot.brain.data.points]

-            # output1 = robot.brain.data
-            output2 = robot.brain.data('points')
-            output3 = robot.brain.data('points.username')
-            output4 = robot.brain.data('data.points.username')
+            output1 = robot.brain.get('data')
+            output2 = robot.brain.get('points')
+            output3 = robot.brain.get('points.username')
+            output4 = robot.brain.get('data.points.username')

-            outputs = ["Output 2: #{output2}, Output 3: #{output3}, Output 4: #{output4}"]
+            points[username] ?= 0
+
+            outputs = ["Output 1: #{output1}, Output 2: #{output2}, Output 3: #{output3}, Output 4: #{output4}"]

             msg.send "Brain output tests: #{outputs}"


bugfix: no longer calls onClipHighlightDone if no clip.

--- coffee/src/shells/highlights_shell.coffee
+++ coffee/src/shells/highlights_shell.coffee
@@ -394,7 +394,8 @@

   onAddHighlight: =>
-    @onClipHighlightDone()
+    if @_clippingHighlight
+      @onClipHighlightDone()

     highlight =
       timeStart: 0


Minor Background reorganization.

Minor Background reorganization.

--- src/background.coffee
+++ src/background.coffee
@@ -44,6 +44,9 @@

       @_initDebugMixin()

+    # Public
+    # ------
+
     addImages: (nameTemplate, topZIndex, bottomZIndex = 1) ->
       @topZIndex = topZIndex
       for zIndex in [bottomZIndex..topZIndex]
@@ -57,8 +60,8 @@
       # @group.y = -(@group.height - @group.game.height)

       # Set vertical scroll factor and offset.
-      nearest = @nearestLayer()
-      farthest = @farthestLayer()
+      nearest = @_nearestLayer()
+      farthest = @_farthestLayer()
       for layer in @layers
         {image, zIndex} = layer
         # Factor in z-index exponentially and constrain.
@@ -80,8 +83,11 @@

       @debug 'layers', @layers

-    farthestLayer: -> _.findWhere @layers, { zIndex: 1 }
-    nearestLayer: -> _.findWhere @layers, { zIndex: @topZIndex }
+    # Helpers
+    # -------
+
+    _farthestLayer: -> _.findWhere @layers, { zIndex: 1 }
+    _nearestLayer: -> _.findWhere @layers, { zIndex: @topZIndex }

   _.extend Background::, Helpers.DebugMixin


Use updated sort syntax for sortedPhases.

--- app/assets/javascripts/controllers/paper_manage_controller.js.coffee
+++ app/assets/javascripts/controllers/paper_manage_controller.js.coffee
@@ -1,10 +1,6 @@
 ETahi.PaperManageController = Ember.ObjectController.extend
-  sortedPhases: ( ->
-    Ember.ArrayProxy.createWithMixins(Em.SortableMixin, {
-      content: @get('model.phases')
-      sortProperties: ['position']
-    })
-  ).property('model.phases.[]', 'model.phases.tasks.@each')
+  positionSort: ["position:asc"]
+  sortedPhases: Ember.computed.sort('phases', 'positionSort')

   updatePositions: (phase)->
     relevantPhases = @get('model.phases').filter((p)->


Uses Backbone from the app

Uses Backbone from the app

--- src/client/api.coffee
+++ src/client/api.coffee
@@ -13,11 +13,9 @@
       require:
         paths:
           easyXDM: 'components/easyXDM/easyXDM'
-          backbone: 'components/backbone/backbone'
           cookie: 'components/jquery.cookie/jquery.cookie'
         shim:
           easyXDM: { exports: 'easyXDM' }
-          backbone: { exports: 'Backbone', deps: ['underscore', 'jquery'] }

       # Builds the URL used by easyXDM
@@ -35,7 +33,6 @@
         sandbox = app.sandbox

         _         = require('underscore')
-        Backbone  = app.core.mvc
         easyXDM   = require('easyXDM')

         slice = Array.prototype.slice
@@ -143,7 +140,7 @@
           dfd.fail(options.error)
           dfd

-        BaseHullModel = Backbone.Model.extend
+        BaseHullModel = app.core.mvc.Model.extend
           sync: sync

         RawModel = BaseHullModel.extend
@@ -158,7 +155,7 @@
               url = @collection?.url
             url

-        Collection = Backbone.Collection.extend
+        Collection = app.core.mvc.Collection.extend
           model: Model
           sync: sync


Add a check for Chase's "System Unavailable" error.

--- application/chrome/content/wesabe/fi-scripts/com/chase/errors.coffee
+++ application/chrome/content/wesabe/fi-scripts/com/chase/errors.coffee
@@ -2,9 +2,16 @@
   dispatch: ->
     if page.present e.errors.unableToCompleteAction
       job.fail 503, 'fi.error'
+
+    if page.present e.errors.systemUnavailable
+      job.fail 503, 'fi.unavailable'

   elements:
     errors:
       unableToCompleteAction: [
         '//text()[contains(string(.), "We were unable to process your request")]'
       ]
+
+      systemUnavailable: [
+        '//div[@id="statusPanel"][contains(string(.), "System Unavailable")]'
+      ]


Add support for release candidates when parsing versions for determining archive subfolders

--- src/downloader.coffee
+++ src/downloader.coffee
@@ -70,7 +70,8 @@
 	# @api public
 	###
 	getZipSubFolder: () ->
-		frags = @version.split('.')
+		# Split on '-' to get rid of "rc" part if present.
+		frags = @version.split('-')[0].split('.')
 		major = parseInt frags[0]
 		minor = parseInt frags[1]
 		if @platform is 'linux' or (major is 0 and minor > 9) or (major > 0)


enhance voting

enhance voting

--- scripts/voting.coffee
+++ scripts/voting.coffee
@@ -21,22 +21,22 @@
     return res.message.user.name.toLowerCase()

-  robot.hear /start ?vote ?(.*)/i, (res) ->
+  robot.respond /startvote ?(.*)/i, (res) ->
     title = res.match[1]
     currentPoll = new Poll({
       title: title
     })
     user = getUser(res)
-    reply = "#{user} started a vote. vote with /placevote {{ your vote }}"
+    reply = "#{user} started a vote. vote with “vote {{ your vote }}”"
     res.send reply

-  robot.hear /place ?vote (.*)/i, (res) ->
+  robot.respond /vote (.*)/i, (res) ->
     if currentPoll
       currentPoll.vote getUser(res), res.match[1]
     else
       res.send "No poll currently"

-  robot.hear /show ?vote/i, (res) ->
+  robot.respond /showvote/i, (res) ->
     if currentPoll
       reply =  "#{currentPoll.title}\n"
       reply += "Current votes:\n"


Add custom errors.

Add custom errors.

--- src/encryption.coffee
+++ src/encryption.coffee
@@ -2,7 +2,7 @@

 crypto = require "crypto"

-exports.Encryption = class Encryption
+Encryption = class Encryption

   constructor: (@senderKey, @recipientKey) ->

@@ -38,7 +38,7 @@
     encryptedMessage = @encryptMessage signedMessage, message, key, iv
     new Buffer(iv + encryptedKey + encryptedMessage).toString "base64"

-exports.Decryption = class Decryption
+Decryption = class Decryption

   constructor: (@recipientKey, @senderKey) ->

@@ -56,7 +56,7 @@
     if @verify signature, message
       return [message, signature.toString()[0]]

-    throw new Error "Invalid message signature"
+    throw new InvalidMessageException "Invalid message signature"

   getIv: (raw) ->
     raw.slice 0, 16
@@ -67,7 +67,7 @@
       encryptedKey = raw.slice 16, 16 + recipientKeyKytes
       @recipientKey.decrypt(encryptedKey)
     catch
-      throw new Error "Incorrect message length."
+      throw new DecryptionError "Incorrect message length."

   getDecryptedMessage: (iv, key, raw) ->
     recipientKeyKytes = @recipientKey.keyPair.cache.keyBitLength / 8
@@ -87,3 +87,16 @@
   verify: (signature, message) ->
     hash = crypto.createHash("sha512").update(message).digest()
     @senderKey.verify hash, signature
+
+class InvalidMessageException extends Error
+
+class DecryptionError extends Error
+
+class EncryptionError extends Error
+
+module.exports =
+  Encryption: Encryption
+  Decryption: Decryption
+  InvalidMessageException: InvalidMessageException
+  DecryptionError: DecryptionError
+  EncryptionError: EncryptionError


added prototype of the new statechart for model.

--- src/Model.coffee
+++ src/Model.coffee
@@ -3,8 +3,61 @@
 {AttributeFactory, Attribute} = require './attributes'
 TypeRegister                  = require './TypeRegister'
 _                             = require 'underscore'
+Stateful                      = require 'stateful'

 class Model extends Base
+
+	# New statechart implementation
+	# @StateChart
+	# 	New:
+	# 		transitions: [
+	# 			destination: 'Existing'
+	# 			action: 'save'
+	# 		]
+	# 		methods:
+	# 			save: -> Stateful.Success
+	#
+	# 	Existing:
+	# 		transitions: [
+	# 			destination: 'Existing/Loaded'
+	# 			action: 'parse'
+	# 		]
+	# 		methods:
+	# 			parse: -> Stateful.Success
+	# 		paths:
+	# 			Loaded:
+	# 				transitions: [
+	# 					destination: 'Existing/Loaded/Editing'
+	# 					action: 'startEdit'
+	# 				]
+	# 				methods:
+	# 					startEdit: -> Stateful.Success
+	# 				paths:
+	# 					Editing:
+	# 						transitions: [
+	# 							{
+	# 								destination: 'Existing/Loaded'
+	# 								action: 'cancel'
+	# 							}
+	# 							{
+	# 								destination: 'Existing/Loaded/Editing/Dirty'
+	# 								action: 'save'
+	# 							}
+	# 						]
+	# 						methods:
+	# 							cancel: -> Stateful.Success
+	# 							save: -> Stateful.Success
+	# 						paths:
+	# 							Dirty:
+	# 								transitions: [
+	# 									{destination: 'Existing/Loaded',action: 'commit'}
+	# 									{destination: 'Existing/Loaded',action: 'rollback'}
+	# 								]
+	# 								methods:
+	# 									commit: -> Stateful.Success
+	# 									rollback: -> Stateful.Success
+	#
+	#

 	@addState 'NEW',
 		transitions :


landing: wrap landing app view instantiation in an `IIFE`

--- client/landing/main.coffee
+++ client/landing/main.coffee
@@ -12,7 +12,8 @@
   @classMap:
     spaceship: SpaceshipLandingPage

-KD.landingAppView = new LandingAppView
-  pageClass       : LandingAppView.classMap[window.landingOptions.page]
+do ->
+  landingAppView = new LandingAppView
+    pageClass    : LandingAppView.classMap[landingOptions.page]

-KD.landingAppView.appendToDomBody()
+  landingAppView.appendToDomBody()


[PRODUCTION] last commit to go to production

--- client/scripts/main.coffee
+++ client/scripts/main.coffee
@@ -328,10 +328,10 @@

 		# Use "jsonp" as long as you are on develop and you are running a local server
 		# Additonally you MUST specify a callback parameter at the end of the URL
-		request = $http.jsonp $scope.feedUrl
+		# request = $http.jsonp $scope.feedUrl

 		# Use "get" if you are on file base or you are running it as a Chrome Extension
-		# request = $http.get feedUrl
+		request = $http.get $scope.feedUrl

 		request.success (data, status) ->
 			$scope.rssNotify = false
@@ -344,7 +344,7 @@
 			firstOldEntry = true

 			if not latestStoredItem? or latestStoredItem < latestItem
-				# $store.set 'latestFeed', latestItem
+				$store.set 'latestFeed', latestItem

 				for item in $scope.feedItems


Remove some unused code from collection

Remove some unused code from collection

--- src/collection.coffee
+++ src/collection.coffee
@@ -180,13 +180,6 @@

-class Tails.Collection.Mirror extends Tails.Collection.Filtered
-  constructor: ( collection, options = {} ) ->
-    options.filter = -> true
-    super collection, options
-
-
-
 class Tails.Collection.Multi extends Tails.Collection

   constructor: ( collections = [], options = {} ) ->
@@ -195,13 +188,6 @@

     super([], options)

-    # if collections instanceof Tails.Collection.Plucked
-    #   @collections = collections
-    #   collections = @collections.toArray()
-
-    #   @collections.on 'add',    @addCollection
-    #   @collections.on 'remove', @removeCollection
-
     @addCollection(collection) for collection in collections

   increment: ( model ) ->
@@ -248,34 +234,3 @@
   decrement: ( model ) ->
     @remove model if super is 0

-
-
-# class Tails.Collection.Intersection extends Tails.Collection.Multi
-
-#   constructor: ( ) ->
-#     super
-#     @on 'addCollection removeCollection', ( ) =>
-#       for cid, count of @modelCounts
-#         @add @model.all().get(cid) if count is @collections.length
-#         @remove @get(cid) if count isnt @collections.length
-
-#   increment: ( model ) ->
-#     @add model if super is @collections.length
-#     @remove model if super isnt @collections.length
-
-#   decrement: ( model ) ->
-#     @add model if super is @collections.length
-#     @remove model if super isnt @collections.length
-
-
-
-# class Tails.Collection.Difference extends Tails.Collection.Multi
-
-#   increment: ( model ) ->
-#     @add model if super is 1
-#     @remove model if super is 2
-
-#   decrement: ( model ) ->
-#     @add model if super is 1
-#     @remove model if super is 0
-


Problem was with the query engine dependencies. Update coming soon for it.

--- lib/docpad.coffee
+++ lib/docpad.coffee
@@ -835,7 +835,6 @@
 			# Routing
 			@server.use (req,res,next) =>
 				@documents.findOne {urls:{'$in':req.url}}, (err,document) =>
-					console.log req.url, document
 					if err
 						@error err
 						res.send(err.message, 500)


chore: write express.js logs to the file

--- server/lib/app.coffee
+++ server/lib/app.coffee
@@ -1,8 +1,13 @@
 express = require("express")
+fs = require("fs")
 path = require("path")

 app = express()
-app.use express.logger()
+
+# configure logger
+logFile = fs.createWriteStream("./tmp/express.log", flags: "a")
+app.use express.logger(stream: logFile)
+
 app.use express.bodyParser()
 app.use express.static(path.join(__dirname, "../../dist"))


Permalink now includes name and (optionally) notes

--- coffeescripts/xwing.coffee
+++ coffeescripts/xwing.coffee
@@ -111,19 +111,24 @@
             @addShip()

     resetCurrentSquad: ->
+        default_squad_name = 'Unnamed Squadron'
+        squad_name = $.trim(@squad_name_input.val()) or $.trim $.getParameterByName('sn') or default_squad_name
+        squad_notes = $.trim(@notes.val()) or $.trim $.getParameterByName('sno') or ''
         @current_squad =
             id: null
-            name: $.trim(@squad_name_input.val()) or "Unnamed Squadron"
+            name: squad_name
             dirty: false
             additional_data:
                 points: @total_points
                 description: ''
                 cards: []
-                notes: ''
+                notes: squad_notes
             faction: @faction
         if @total_points > 0
-            @current_squad.name = 'Unsaved Squadron'
+            if squad_name == default_squad_name
+                @current_squad.name = 'Unsaved Squadron'
             @current_squad.dirty = true
+        @notes.val(squad_notes)
         @container.trigger 'xwing-backend:squadNameChanged'
         @container.trigger 'xwing-backend:squadDirtinessChanged'

@@ -758,8 +763,21 @@
         $(window).resize =>
             @select_simple_view_button.click() if $(window).width() < 768 and @list_display_mode != 'simple'

-        @notes.change @onNotesUpdated
+        # separate function for on change that accesses the notes values
+        @notes.change @onNotesChanged
         @notes.on 'keyup', @onNotesUpdated
+
+    updatePermaLink: () =>
+        squad_link = "#{window.location.href.split('?')[0]}?f=#{encodeURI @faction}&d=#{encodeURI @serialize()}&sn=#{encodeURI @current_squad.name}"
+        if @current_squad.additional_data.notes
+            squad_link = squad_link + "&sno=#{encodeURI @current_squad.additional_data.notes}"
+        @permalink.attr 'href', squad_link
+
+    onNotesChanged: =>
+        if @total_points > 0
+            @current_squad.additional_data.notes = @notes.val()
+            @updatePermaLink()
+            @onNotesUpdated()

     onNotesUpdated: =>
         if @total_points > 0
@@ -849,7 +867,8 @@

         @fancy_total_points_container.text @total_points
         # update permalink while we're at it
-        @permalink.attr 'href', "#{window.location.href.split('?')[0]}?f=#{encodeURI @faction}&d=#{encodeURI @serialize()}"
+        @updatePermaLink()
+        # @permalink.attr 'href', "#{window.location.href.split('?')[0]}?f=#{encodeURI @faction}&d=#{encodeURI @serialize()}&sn=#{encodeURI @current_squad.name}&sno=#{encodeURI @notes.val()}"
         # and text list
         @fancy_container.text ''
         @simple_container.html '

'
@@ -901,6 +920,7 @@
         @squad_name_placeholder.text ''
         @squad_name_placeholder.append short_name
         @squad_name_input.val @current_squad.name
+        @updatePermaLink()

     removeAllShips: ->
         while @ships.length > 0


removes session instance variable from publish.coffee

--- server/publish.coffee
+++ server/publish.coffee
@@ -1,7 +1,6 @@
 Users = new Meteor.Collection("users")
 Posts = new Meteor.Collection("posts")
 Instances = new Meteor.Collection("instances");
-

 Meteor.publish("my_user", (user_id) ->
@@ -28,5 +27,4 @@

 Meteor.startup( ->
   Session.set 'user_id', Users.findOne( {} )['_id']
-  Session.set 'instance_name', 'metaCrunch'
 )


micro-refactor of some SimpleHash functions

--- src/batman.coffee
+++ src/batman.coffee
@@ -731,32 +731,27 @@
   $extendsEnumerable(@::)
   propertyClass: Batman.Property
   hasKey: (key) ->
-    matches = @_storage[key] ||= []
-    for match in matches
-      if @equality(match[0], key)
-        pair = match
-        return true
+    if pairs = @_storage[key]
+      for pair in pairs
+        return true if @equality(pair[0], key)
     return false
   get: (key) ->
-    if matches = @_storage[key]
-      for [obj,v] in matches
-        return v if @equality(obj, key)
+    if pairs = @_storage[key]
+      for pair in pairs
+        return pair[1] if @equality(pair[0], key)
   set: (key, val) ->
-    matches = @_storage[key] ||= []
-    for match in matches
-      if @equality(match[0], key)
-        pair = match
-        break
-    unless pair
-      pair = [key]
-      matches.push(pair)
-      @length++
-    pair[1] = val
+    pairs = @_storage[key] ||= []
+    for pair in pairs
+      if @equality(pair[0], key)
+        return pair[1] = val
+    @length++
+    pairs.push([key, val])
+    val
   unset: (key) ->
-    if matches = @_storage[key]
-      for [obj,v], index in matches
+    if pairs = @_storage[key]
+      for [obj,value], index in pairs
         if @equality(obj, key)
-          matches.splice(index,1)
+          pairs.splice(index,1)
           @length--
           return
   getOrSet: Batman.Observable.getOrSet


also close ZMQ sockets

also close ZMQ sockets

--- lib/kernel.coffee
+++ lib/kernel.coffee
@@ -193,3 +193,5 @@
                 content
             ]
         @shellSocket.send message
+        @shellSocket.close()
+        @ioSocket.close()


:bug: Fix a weird bug with Linter loop I had copy/pasted iam4x's code, it had a weird bug

--- lib/linter.coffee
+++ lib/linter.coffee
@@ -65,7 +65,7 @@

     @Linters.forEach (Linter) ->
       return if onChange and not Linter.lintOnFly
-      return unless onChange and Linter.lintOnFly
+      return if (not onChange) and Linter.lintOnFly
       Matching = Scopes.filter (Entry) -> Linter.scopes.indexOf(Entry) isnt -1
       return unless Matching.length
       Promises.push Linter.lint(


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera.coffee
+++ src/camera.coffee
@@ -36,7 +36,7 @@
       height: 2 * deadzone.y

     if objectCenter.x > centerRect.right
-      I.scroll.x
+      I.scroll.x = I.scr
     else if objectCenter.x < centerRect.left


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera.coffee
+++ src/camera.coffee
@@ -14,6 +14,7 @@
     zoom: 1
     transform: Matrix()
     scroll: Point(0, 0)
+

   currentType = "centered"
   currentObject = null


cleanup canary

cleanup canary

--- src/canary.coffee
+++ src/canary.coffee
@@ -63,6 +63,10 @@
         delete @stats.flows[flowUuid]

   getCurrentStats: =>
+    @updateStats()
+    return @stats
+
+  updateStats: =>
     _.each @stats.flows, (flowInfo) =>
       flowInfo.passing = false
       return unless flowInfo.messageTime
@@ -74,8 +78,6 @@
     @stats.passing = _.keys(@stats.flows).length != 0
     _.each @stats.flows, (flowInfo) =>
       @stats.passing = false if !flowInfo.passing
-
-    return @stats

   getFlows: (callback) =>
     @requestOctobluUrl 'GET', '/api/flows', (error, body) =>
@@ -98,10 +100,10 @@

   restartFailedFlows: (callback=->) =>
     debug 'restarting failed flows'
-    stats = @getCurrentStats()
+    @updateStats()
     flowStarters = []
-    _.each _.keys(stats.flows), (flowUuid) =>
-      flowInfo = stats.flows[flowUuid]
+    _.each _.keys(@stats.flows), (flowUuid) =>
+      flowInfo = @stats.flows[flowUuid]
       if flowInfo.currentTimeDiff > @CANARY_RESTART_FLOWS_MAX_TIME
         debug "restarting failed flow #{flowUuid}"
         flowStarters.push @curryStartFlow flowUuid
@@ -109,8 +111,7 @@

   postTriggers: (callback=->) =>
     debug 'posting triggers'
-    triggers = @getTriggers()
-    async.each triggers, (trigger, callback) =>
+    async.each @getTriggers(), (trigger, callback) =>
       flowInfo = @stats.flows[trigger.flowId] ?= {}
       triggerInfo = flowInfo.triggerTime ?= {}
       triggerTime = triggerInfo[trigger.triggerId] ?= []


removed unnccary ?

removed unnccary ?

--- src/chance.coffee
+++ src/chance.coffee
@@ -17,7 +17,7 @@
 coinValues = ['heads', 'tails']

 module.exports = (robot) ->
-  robot.respond /(roll di)?/i, (msg) ->
+  robot.respond /(roll di)/i, (msg) ->
     msg.send msg.random diceValues

   robot.respond /roll( \d+)? dice/i, (msg) ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color2.coffee
+++ src/color2.coffee
@@ -131,8 +131,8 @@
       "##{hexFromNumber(@r)}#{hexFromNumber(@g)}#{hexFromNumber(@b)}"

     toHsl: ->
-      min = Math.min(r, g, b)
-      max = Math.max(r, g, b)
+      min = Math.min(@r, @g, @b)
+      max = Math.max(@r, @g, @b)

       hue = saturation = lightness = (max + min) / 2.0

@@ -143,13 +143,13 @@
         saturation = (if lightness > 0.5 then delta / (2 - max - min) else delta / (max + min))

         switch max
-          when r then hue = (g - b) / delta + (if g < b then 6 else 0)
-          when g then hue = (b - r) / delta + 2
-          when b then hue = (r - g) / delta + 4
+          when @r then hue = (@g - @b) / delta + (if @g < @b then 6 else 0)
+          when @g then hue = (@b - @r) / delta + 2
+          when @b then hue = (@r - @g) / delta + 4

         hue *= 60

-      return [hue, saturation, lightness, channels[3]]
+      return [hue, saturation, lightness, @a]

     toString: ->
       "rgba(#{@r}, #{@g}, #{@b}, #{@a})"


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color2.coffee
+++ src/color2.coffee
@@ -1,4 +1,14 @@
 ( ->
+  rgbParser = /^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),?\s*(\d?\.?\d*)?\)$/
+
+  parseRGB = (colorString) ->
+    return undefined unless channels = rgbParser.exec(colorString)
+
+    parsedColor = (parseFloat channel for channel in channels)
+    parsedColor[3] ||= 1.0
+
+    return parsedColor
+
   Color2 = (args...) ->
     __proto__: Color::


Add Promise.

Add Promise.

--- js/src/service/RegistrationClient.coffee
+++ js/src/service/RegistrationClient.coffee
@@ -12,6 +12,7 @@
   CredentialService       = require 'kryptnostic.credential-service'
   UserRegistrationRequest = require 'kryptnostic.user-registration-request'
   KryptnosticWorkersApi   = require 'kryptnostic.kryptnostic-workers-api'
+  Promise                 = require 'bluebird'

   log = Logger.get('RegistrationClient')


activityticker: guest membership activities are hidden

--- client/Social/Activity/ContentDisplays/ActivityTicker.coffee
+++ client/Social/Activity/ContentDisplays/ActivityTicker.coffee
@@ -115,7 +115,9 @@
       return  if err
       for item in items
         {as, source, target, subject} = item
-        if source and target and as
+
+        isGuest = target.profile?.nickname?.indexOf("guest-") isnt -1
+        if source and target and as and not isGuest
           @listController.addItem item

   pistachio:


Listen enter key event for 2FA in register page.

--- client/landing/site.landing/coffee/login/registerform.coffee
+++ client/landing/site.landing/coffee/login/registerform.coffee
@@ -38,9 +38,6 @@
             minLength     : "Passwords should be at least 8 characters."
         decorateValidation: no

-    @tfcode = @create2FAInput()
-    @tfcode.hide()
-
     @email?.destroy()
     @email = new LoginInputViewWithLoader
       inputOptions        :
@@ -85,6 +82,9 @@

       @button.hideLoader()

+    @tfcode = @create2FAInput()
+    @tfcode.hide()
+
     @bind2FAEvents()

     KD.singletons.router.on 'RouteInfoHandled', =>
@@ -100,6 +100,8 @@
         testPath      : 'register-form-tfcode'
         attributes    :
           testpath    : 'register-form-tfcode'
+        keydown       : (event) =>
+          @button.click event  if event.which is ENTER

   bind2FAEvents: ->


Fixed 'show'/'hide' class

Fixed 'show'/'hide' class

--- core/app/backbone/views/evidence/evidence_bottom_view.coffee
+++ core/app/backbone/views/evidence/evidence_bottom_view.coffee
@@ -39,5 +39,5 @@
     else
       @hideSubCommentsLink()

-  showSubCommentsLink: -> @ui.subCommentsLinkContainer.removeClass 'hide'
-  hideSubCommentsLink: -> @ui.subCommentsLinkContainer.addClass 'hide'
+  showSubCommentsLink: -> @ui.subCommentsLinkContainer.show()
+  hideSubCommentsLink: -> @ui.subCommentsLinkContainer.hide()


Add first iteration of partial pagination. #280

--- core/htdocs_source/src/pods/oxisection-grid/component.coffee
+++ core/htdocs_source/src/pods/oxisection-grid/component.coffee
@@ -36,7 +36,7 @@
         @send "changeStartat",
             startat: Math.floor(startat/limit) * limit

-    pages: Em.computed ->
+    pages: Em.computed "content.content.pager.startat", ->
         pager = @get "content.content.pager"
         return [] if not pager
         return [] if pager.count <= pager.limit
@@ -206,10 +206,25 @@
         changeStartat: (page) ->
             return if page.disabled or page.active
             limit = @get "limit"
-            @container.lookup("route:openxpki").transitionTo
-                queryParams:
-                    limit: limit
-                    startat: page.startat
+
+            pager = @get "content.content.pager"
+            content = @get "content.content"
+            if pager.pagerurl
+                @container.lookup("route:openxpki").sendAjax
+                    data:
+                        page: pager.pagerurl
+                        limit: limit
+                        startat: page.startat
+
+                .then (res) =>
+                    #Em.set content, "data", res.data
+                    @set "content.content.data", res.data
+                    @set "content.content.pager.startat", page.startat
+            else
+                @container.lookup("route:openxpki").transitionTo
+                    queryParams:
+                        limit: limit
+                        startat: page.startat

         sort: (key) ->
             sortNum = @get "sortNum"


Re-organize Input class APIs (fix #88)

Re-organize Input class APIs (fix #88)

* For most subclasses `X` of `Input`, `new X(data, options)` now generally
  takes the file contents as the first argument.  For example,
  `new ASCIIMapping(asc)` will now parse an ASCII file, and
  `new StylusStyle(content)` will now parse Stylus content.
  (Previously, these acted like `new Mapping` and `new Style`
  respectively.)
* In the `Input` internal API, subclasses just need to define a `parse`
  method, which acts like a combination of the old `parse` and `load`.
  Generally the parentmost `parse` acts like the old `load`
  (copying data into an attribute), while children `parse` methods
  call `super` (`super.parent`) instead of `load`.
  There is no more `@make` class method or `filedata` option.
* Add comments documenting all of this.
* Rename `Drawing`'s main attribute from `data` to `keys`,
  and `Mapping`'s main attribute from `data` to `map`. (#88)
* BREAKING API change, but these APIs are pretty internal API;
  new API should be far more useful.

--- src/svgtiler.coffee
+++ src/svgtiler.coffee
@@ -391,7 +391,7 @@
     ## `@value` can be a string or Preact VDOM.
     super()
     unless @value?
-      throw new SVGTilerError "Attempt to create tile '#{@key}' without data"
+      throw new SVGTilerError "Attempt to create tile '#{@key}' without content"

   makeSVG: ->
     return if @svg?
@@ -475,8 +475,7 @@
       for attribute in @dom.documentElement.attributes
         unless attribute.name in ['version', 'id'] or attribute.name.startsWith 'xmlns'
           symbol.setAttribute attribute.name, attribute.value
-      doc = @dom.documentElement
-      @dom.removeChild @dom.documentElement
+      @dom.removeChild doc = @dom.documentElement
     else
       doc = @dom
     for child in (node for node in doc.childNodes)
@@ -617,35 +616,49 @@
 unrecognizedTile.id = '_unrecognized' # cannot be output of escapeId()

 class Input extends HasSettings
+  ###
+  Abstract base class for all inputs to SVG Tiler, in particular
+  `Mapping`, `Style`, and `Drawing` and their format-specific subclasses.
+
+  Each subclass should define:
+  * `parse(data)` method that parses the input contents in the format
+    defined by the subclass (specified manually by the user, or
+    automatically read from the input file).
+  * `skipRead: true` attribute if you don't want `@parseFile` class method
+    to read the file data and pass it into `parse`, in case you want to read
+    from `@filename` directly yourself in a specific way.
+  ###
   constructor: (data, opts) ->
     ###
     `data` is input-specific data for direct creation (e.g. without a file),
-    which is processed via `@load()`.
-    If `data` is unspecified, `@parse` will be called with `@filedata`.
-    `opts` is an object with options attached directly to the Input, including:
-    `filename`, `filedata`, `settings`.
+    which is processed via a `parse` method (which subclass must define).
+    `opts` is an object with options attached directly to the Input
+    (*before* `parse` gets called), including `filename` and `settings`.
     ###
     super()
     @[key] = value for key, value of opts if opts?
-    if data?
-      @load data
-    else
-      @parse @filedata
-  @make: (opts) ->
-    ## Create input when there's no `data`, only `opts`.
-    ## In particular, this is used to parse data from a file in `@parseData`.
-    new @ undefined, opts
+    @parse data
   @encoding: 'utf8'
   @parseFile: (filename, filedata, settings) ->
-    ## Generic method to parse file once we're already in the right class.
+    ###
+    Generic method to parse file once we're already in the correct subclass.
+    Automatically reads the file contents from `filename` unless
+    * `@skipRead` is true, or
+    * file contents are specified via `settings.filedata`.
+      Use this to avoid attempting to use the file system on the browser.
+    ###
     modified = -Infinity
     try
       modified = (fs.statSync filename).mtimeMs
     unless filedata? or @skipRead
       filedata = fs.readFileSync filename, encoding: @encoding
-    @make {filename, filedata, modified, settings}
+    new @ filedata, {filename, modified, settings}
   @recognize: (filename, filedata, settings) ->
-    ## Recognize type of file and call corresponding class's `parseFile`.
+    ###
+    Recognize type of file and call corresponding class's `parseFile`.
+    Meant to be used as `Input.recognize(...)` via top-level Input class,
+    without specific subclass.
+    ###
     extension = extensionOf filename
     if extensionMap.hasOwnProperty extension
       extensionMap[extension].parseFile filename, filedata, settings
@@ -672,19 +685,23 @@
     path.format filename

 class Style extends Input
-  load: (@css) ->
+  ###
+  Base Style class assumes any passed data is in CSS format,
+  stored in `@css` attribute.
+  ###
+  parse: (@css) ->

 class CSSStyle extends Style
+  ## Style in CSS format.  Equivalent to Style base class.
   @title: "CSS style file"
-  parse: (filedata) ->
-    @load filedata

 class StylusStyle extends Style
+  ## Style in Stylus format.
   @title: "Stylus style file (https://stylus-lang.com/)"
-  parse: (filedata) ->
-    styl = require('stylus') filedata,
+  parse: (stylus) ->
+    styl = require('stylus') stylus,
       filename: @filename
-    @load styl.render()
+    super styl.render()

 class ArrayWrapper extends Array
   ###
@@ -738,27 +755,33 @@
     currentMapping = oldMapping

 class Mapping extends Input
+  ###
+  Base Mapping class.
+  The passed-in data can be any supported output from a JavaScript mapping
+  file: an object, a Map, or a function resolving to one of the above
+  or a String (containing SVG or a filename), Preact VDOM, or null/undefined.
+  In this class and subclasses, `@map` stores this data.
+  ###
   constructor: (...args) ->
     super ...args
-    @cache ?= new Map
+    @cache ?= new Map  # for static tiles
     @settings = {...@settings, dirname: path.dirname @filename} if @filename?
-  load: (@data) ->
-    unless typeof @data in ['function', 'object']
-      console.warn "Mapping file #{@filename} returned invalid mapping data of type (#{typeof @data}): should be function or object"
-      @data = null
-    if isPreact @data
+  parse: (@map) ->
+    unless typeof @map in ['function', 'object']
+      console.warn "Mapping file #{@filename} returned invalid mapping data of type (#{typeof @map}): should be function or object"
+      @map = null
+    if isPreact @map
       console.warn "Mapping file #{@filename} returned invalid mapping data (Preact DOM): should be function or object"
-      @data = null
+      @map = null
   lookup: (key, context) ->
-    ## `key` must be a String (should already be coerced by `Drawing::load`).
-    #key = String key
+    ## `key` normally should be a String (via `AutoDrawing::parse` coercion).
     ## Check cache (used for static tiles).
     if (found = @cache.get key)?
       return found

-    ## Repeatedly expand `@data` until we get string, Preact VDOM, or
+    ## Repeatedly expand `@map` until we get string, Preact VDOM, or
     ## null/undefined.
-    value = @data
+    value = @map
     isStatic = undefined
     while value? and typeof value != 'string' and not isPreact value
       if value instanceof Map or value instanceof WeakMap
@@ -806,12 +829,12 @@
       else
         key = line[...separator.index]
       map[key] = line[separator.index + separator[0].length..]
-    @load map
+    super map

 class JSMapping extends Mapping
   @title: "JavaScript mapping file (including JSX notation)"
   @help: "Object mapping tile names to TILE e.g. {dot: 'dot.svg'}"
-  @skipRead: true  # require loads file contents for us
+  @skipRead: true  # `require` loads file contents for us
   parse: (data) ->
     unless data?
       ## Normally use `require` to load code as a real NodeJS module
@@ -824,7 +847,7 @@
       #console.log code
       pirates?.settings = @settings
       @module = runWithMapping @, -> require filename
-      @load @module.default ? {}
+      super @module.default ? {}
       @walkDeps filename
     else
       ## But if file has been explicitly loaded (e.g. in browser),
@@ -841,13 +864,13 @@
       _dirname = path.dirname _filename
       ## Use `new Function` instead of `eval` for improved performance and to
       ## restrict to passed arguments + global scope.
-      #@load eval code
+      #super eval code
       func = new Function \
         'exports', '__filename', '__dirname', 'svgtiler', 'preact', code
       runWithMapping @, ->
         func @module, _filename, _dirname, svgtiler,
           (if code.includes 'preact' then require 'preact')
-      @load @module.default
+      super @module.default
   walkDeps: (filename) ->
     deps = new Set
     recurse = (modname) =>
@@ -874,11 +897,11 @@
       #  {...babelConfig, filename: @filename})
       #console.log code
       ## Normally rely on `require` and `CoffeeScript.register` to load code.
-      super.parse data
+      super data
     else
       ## But if file has been explicitly loaded (e.g. in browser),
       ## compile manually.
-      super.parse require('coffeescript').compile data,
+      super require('coffeescript').compile data,
         bare: true
         inlineMap: true
         filename: @filename
@@ -936,11 +959,38 @@
 ###

 class Drawing extends Input
-  load: (data) ->
+  ###
+  Base Drawing class uses a data format of an Array of Array of keys,
+  where `data[i][j]` represents the key in row `i` and column `j`,
+  without any preprocessing.  This is meant for direct API use,
+  whereas AutoDrawing provides preprocessing for data from mapping files.
+  In this class and subclasses, `@keys` stores the Array of Array of keys.
+  ###
+  parse: (@keys) ->
+  renderDOM: (settings = @settings) ->
+    new Render @, settings
+    .makeDOM()
+  render: (settings = @settings) ->
+    ## Writes SVG and optionally TeX file.
+    ## Returns output SVG filename.
+    r = new Render @, settings
+    filename = r.writeSVG()
+    r.writeTeX() if getSetting settings, 'texText'
+    filename
+
+class AutoDrawing extends Drawing
+  ###
+  Extended Drawing base class that preprocesses the drawing as follows:
+  * Casts all keys to strings, in particular to handle Number data.
+  * Optionally removes margins according to `keepMargins` setting.
+  ###
+  parse: (data) ->
     ## Turn strings into arrays, and turn numbers (e.g. from XLSX) into strings.
-    data = for row in data
-             for cell in row
-               String cell
+    unless @skipStringCast
+      data =
+        for row in data
+          for cell in row
+            String cell
     unless @getSetting 'keepMargins'
       ## Top margin
       while data.length > 0 and allBlank data[0]
@@ -954,33 +1004,27 @@
           for row in data
             row.shift()
         ## Right margin
-        j = Math.max (row.length for row in data)...
+        j = Math.max ...(row.length for row in data)
         while j >= 0 and allBlank (row[j] for row in data)
           for row in data
             if j < row.length
               row.pop()
           j--
-    @data = data
-  renderDOM: (settings = @settings) ->
-    new Render @, settings
-    .makeDOM()
-  render: (settings = @settings) ->
-    ## Writes SVG and optionally TeX file.
-    ## Returns output SVG filename.
-    r = new Render @, settings
-    filename = r.writeSVG()
-    r.writeTeX() if getSetting settings, 'texText'
-    filename
-
-class ASCIIDrawing extends Drawing
+    super data
+
+class ASCIIDrawing extends AutoDrawing
   @title: "ASCII drawing (one character per tile)"
   parse: (data) ->
-    @load(
+    super(
       for line in splitIntoLines data
         graphemeSplitter.splitGraphemes line
     )

-class DSVDrawing extends Drawing
+class DSVDrawing extends AutoDrawing
+  ###
+  Abstract base class for all Delimiter-Separator Value (DSV) drawings.
+  Each subclass must define `@delimiter` class property.
+  ###
   parse: (data) ->
     ## Remove trailing newline / final blank line.
     if data[-2..] == '\r\n'
@@ -988,7 +1032,7 @@
     else if data[-1..] in ['\r', '\n']
       data = data[...-1]
     ## CSV parser.
-    @load require('csv-parse/sync').parse data,
+    super require('csv-parse/sync').parse data,
       delimiter: @constructor.delimiter
       relax_column_count: true

@@ -1008,10 +1052,10 @@
   @delimiter: '\t'

 class Drawings extends Input
-  load: (datas) ->
+  parse: (datas) ->
     @drawings =
       for data in datas
-        new Drawing data,
+        new Drawing parse,
           settings: @settings
           filename: @filename
           subname: data.subname
@@ -1028,7 +1072,7 @@
     xlsx = require 'xlsx'
     workbook = xlsx.read data, type: 'binary'
     ## https://www.npmjs.com/package/xlsx#common-spreadsheet-format
-    @load (
+    super(
       for sheetInfo in workbook.Workbook.Sheets
         subname = sheetInfo.name
         sheet = workbook.Sheets[subname]
@@ -1092,7 +1136,7 @@
     symbolIds = new Set
     context = new Context @
     @tiles =
-      for row, i in @drawing.data
+      for row, i in @drawing.keys
         for key, j in row
           context.move i, j
           tile = @mappings.lookup key, context
@@ -1385,17 +1429,17 @@
   constructor: (@render, i, j) ->
     @drawing = @render.drawing
     ## Use @drawing to access these old properties:
-    #@data = @drawing.data
+    #@keys = @drawing.keys
     #@filename = @drawing.filename
     #@subname = @drawing.subname
     @move i, j if i? and j?
   move: (@i, @j) ->
-    @key = @drawing.data[@i]?[@j]
+    @key = @drawing.keys[@i]?[@j]
   at: (j, i) ->
     if i < 0
-      i += @drawing.data.length
+      i += @drawing.keys.length
     if j < 0
-      j += @drawing.data[i]?.length ? 0
+      j += @drawing.keys[i]?.length ? 0
     new Context @render, i, j
   neighbor: (dj, di) ->
     new Context @render, @i + di, @j + dj
@@ -1405,11 +1449,11 @@
     @key? and @key.match ...args
   row: (di = 0) ->
     i = @i + di
-    for tile, j in @drawing.data[i] ? []
+    for tile, j in @drawing.keys[i] ? []
       new Context @render, i, j
   column: (dj = 0) ->
     j = @j + dj
-    for row, i in @drawing.data
+    for row, i in @drawing.keys
       new Context @render, i, j
 Object.defineProperties Context.prototype,
   filename: get: -> @drawing.filename
@@ -1664,7 +1708,8 @@
   Tile, unrecognizedTile,
   Mapping, ASCIIMapping, JSMapping, CoffeeMapping,
   getMapping, runWithMapping,
-  Drawing, ASCIIDrawing, DSVDrawing, SSVDrawing, CSVDrawing, TSVDrawing,
+  Drawing, AutoDrawing, ASCIIDrawing,
+  DSVDrawing, SSVDrawing, CSVDrawing, TSVDrawing,
   Drawings, XLSXDrawings,
   Style, CSSStyle, StylusStyle,
   SVGFile,